Skip to content

Commit

Permalink
Add simulations for pot balancing for missed attendances.
Browse files Browse the repository at this point in the history
  • Loading branch information
uncaught committed Apr 25, 2020
1 parent 6ecdd6d commit f33aa12
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 8 deletions.
27 changes: 27 additions & 0 deletions client/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ input[type=number] {
text-align: center !important;
}

.u-text-right {
text-align: right !important;
}

.u-line-through {
text-decoration: line-through;
}
Expand Down Expand Up @@ -381,6 +385,29 @@ input[type=number] {
margin-top: 0.5em;
}

.potGrid {
display: grid;
grid-row-gap: 10px;
white-space: nowrap;
}

.splitPotAttendance,
.topUpByAverage {
grid-template-columns: auto 5em minmax(6em, 8em) 5em;
}

.potGrid .value {
text-align: right;
}

.potGrid .value.pos {
color: #1aa62a;
}

.potGrid .value.neg {
color: #9f3a38;
}

@media only screen and (max-width: 767.98px) {
.ui.selection.dropdown .menu {
max-height: 15rem;
Expand Down
6 changes: 4 additions & 2 deletions client/src/pages/group/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import Members from './Members';
import {Divider} from 'semantic-ui-react';
import RoundsInfo from './RoundsInfo';
import GroupName from './GroupName';
import Pot from './Pot';

export default function Group(): ReactElement {
return <div>
<GroupName/>
<Divider />
<Divider/>
<RoundsInfo/>
<Divider />
<Divider/>
<Pot/>
<Members/>
</div>;
}
4 changes: 1 addition & 3 deletions client/src/pages/group/Members.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {ReactElement} from 'react';
import {Header, Icon, Label, List} from 'semantic-ui-react';
import {Icon, Label, List} from 'semantic-ui-react';
import {useSortedGroupMembers} from '../../store/GroupMembers';
import {useGroup} from '../../store/Groups';
import {asLink} from '../../AsLink';
Expand All @@ -11,8 +11,6 @@ export default function Members(): ReactElement {
const groupMembers = useSortedGroupMembers();

return <section>
<Header as='h4'>Mitglieder</Header>

{groupMembers.length > 0 && <div className="">
<List divided relaxed>
{groupMembers.map(({id, name, pointBalance = 0, pointDiffToTopPlayer = 0, roundsCount = 0, euroBalance, isYou}) =>
Expand Down
18 changes: 18 additions & 0 deletions client/src/pages/group/Pot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, {ReactElement} from 'react';
import {asLink} from '../../AsLink';
import {useRouteMatch} from 'react-router-dom';
import FullPot from '../pot/FullPot';
import {Divider} from 'semantic-ui-react';
import {useGroup} from '../../store/Groups';

export default function Pot(): ReactElement | null {
const group = useGroup();
const {url} = useRouteMatch();
if (!group?.settings.eurosPerPointDiffToTopPlayer) {
return null;
}
return <>
<FullPot as={asLink(`${url}/pot`)}/>
<Divider/>
</>;
}
4 changes: 1 addition & 3 deletions client/src/pages/group/RoundsInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {ReactElement} from 'react';
import {Header, Icon, Label} from 'semantic-ui-react';
import {Icon, Label} from 'semantic-ui-react';
import {useSortedRounds} from '../../store/Rounds';
import dayjs from 'dayjs';
import {asLink} from '../../AsLink';
Expand All @@ -13,8 +13,6 @@ export default function RoundsInfo(): ReactElement {
const lastRound = rounds[0];

return <section>
<Header as='h4'>Runden</Header>

<div className="memberDetail">
<Label as={asLink(`${url}/rounds`)} color={'orange'}>
Alle Runden
Expand Down
4 changes: 4 additions & 0 deletions client/src/pages/group/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Settings from './Settings';
import {useGroup} from '../../store/Groups';
import Statistics from '../statistics/Statistics';
import EnableIrregularMembersMenuItem from '../statistics/EnableIrregularMembersMenuItem';
import PotIndex from '../pot';

export default function GroupIndex(): ReactElement | null {
useLoadGroupMembers();
Expand All @@ -39,6 +40,9 @@ export default function GroupIndex(): ReactElement | null {
<Page path={`${url}/statistics`} displayName={'Mitglieder'} menuItems={[EnableIrregularMembersMenuItem]}>
<Statistics/>
</Page>
<Page path={`${url}/pot`} displayName={'Pott'}>
<PotIndex/>
</Page>
<Page path={`${url}`} menuItems={[
{icon: 'user plus', route: `${url}/addMembers`, title: 'Mitglieder hinzufügen'},
]}>
Expand Down
19 changes: 19 additions & 0 deletions client/src/pages/pot/FullPot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, {ReactElement, useMemo} from 'react';
import {useSortedGroupMembers} from '../../store/GroupMembers';
import {Icon, Label} from 'semantic-ui-react';

export default function FullPot({as}: { as?: any }): ReactElement {
const groupMembers = useSortedGroupMembers();
const fullPot = useMemo(() => groupMembers.reduce((acc, {euroBalance}) => acc + (euroBalance || 0), 0),
[groupMembers]);
return <section>
<div className="memberDetail">
<Label as={as} color={'purple'}>
Gesamter Pott
<Label.Detail>
{fullPot} <Icon name={'euro'}/>
</Label.Detail>
</Label>
</div>
</section>;
}
51 changes: 51 additions & 0 deletions client/src/pages/pot/SplitPotAttendance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, {Fragment, ReactElement} from 'react';
import {useSortedGroupMembers} from '../../store/GroupMembers';
import {Header, Icon, Message} from 'semantic-ui-react';
import {useSortedRounds} from '../../store/Rounds';
import classNames from 'classnames';
import {useGroup} from '../../store/Groups';

export default function SplitPotAttendance(): ReactElement {
const {completedRoundsCount} = useGroup()!;
const groupMembers = useSortedGroupMembers();
let fullPot = 0;
let attendances = 0;
groupMembers.forEach(({euroBalance, roundsCount}) => {
fullPot += euroBalance || 0;
attendances += roundsCount || 0;
});

const mapped = groupMembers.map(({id, name, euroBalance, roundsCount}) => {
const euros = euroBalance || 0;
const attendance = roundsCount / attendances;
const partial = fullPot * attendance;
const balance = partial - euros;
const row = <Fragment key={id}>
<div className={'name'}>{name}</div>
<div className={'value'}>{euros.toFixed(2)}</div>
<div className={'value'}>{roundsCount} / {partial.toFixed(2)}</div>
<div className={classNames('value', {pos: balance >= 0, neg: balance < 0})}>{balance.toFixed(2)}</div>
</Fragment>;
return {id, row, balance};
});
mapped.sort((a, b) => b.balance - a.balance);

return <section>
<Header>#1 Split-Pott nach Teilnahmen</Header>
<Message>
<Message.Content>
<p>Jeder Spieler hat durch seine Teilnahmen zum Pott beigetragen.</p>
<p>Der Pott wird anteilig angerechnet.</p>
<p>Es gibt insgesamt {completedRoundsCount} beendete Runden, mit {attendances} Teilnahmen.</p>
</Message.Content>
</Message>

<div className={'potGrid splitPotAttendance'}>
<div className={'name'}/>
<div className={'value'}>Beitrag</div>
<div className={'value'}>Anteil</div>
<div className={'value'}><Icon name={'balance scale'}/></div>
{mapped.map(({row}) => row)}
</div>
</section>;
}
50 changes: 50 additions & 0 deletions client/src/pages/pot/TopUpByAverage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, {Fragment, ReactElement} from 'react';
import {useSortedGroupMembers} from '../../store/GroupMembers';
import {Header, Message} from 'semantic-ui-react';
import {useGroup} from '../../store/Groups';

export default function TopUpByAverage(): ReactElement {
const {completedRoundsCount} = useGroup()!;
const groupMembers = useSortedGroupMembers();
let realPot = 0;
let topUpPot = 0;

const mapped = groupMembers.map(({id, name, euroBalance, roundsCount}) => {
const euros = euroBalance || 0;
const missedRounds = completedRoundsCount - roundsCount;
const averageBalance = euros / roundsCount;
realPot += euros;
const topUp = missedRounds * averageBalance;
topUpPot += topUp;
const total = euros + topUp;
const row = <Fragment key={id}>
<div className={'name'}>{name}</div>
<div className={'value'}>{euros.toFixed(2)}</div>
<div className={'value'}>{missedRounds} x {averageBalance.toFixed(2)}</div>
<div className={'value'}>{total.toFixed(2)}</div>
</Fragment>;
return {id, row, total};
});
mapped.sort((a, b) => a.total - b.total);

const toppedUpPot = realPot + topUpPot;

return <section>
<Header>#2 Pott-Auffüllung via Durchschnitt</Header>
<Message>
<Message.Content>
<p>Der Pott wird mit dem durchschnittlichen Geldbetrag pro Runde eines jeden Spielers aufgestockt.</p>
<p>Es wird so die 100% Teilnahme jedes Spielers simuliert.</p>
<p>Der Pott erhöht sich von {realPot.toFixed(2)} € auf {toppedUpPot.toFixed(2)} €.</p>
</Message.Content>
</Message>

<div className={'potGrid topUpByAverage'}>
<div className={'name'}/>
<div className={'value'}>Beitrag</div>
<div className={'value'}>Ausgleich</div>
<div className={'value'}>Total</div>
{mapped.map(({row}) => row)}
</div>
</section>;
}
15 changes: 15 additions & 0 deletions client/src/pages/pot/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, {ReactElement} from 'react';
import {Divider} from 'semantic-ui-react';
import FullPot from './FullPot';
import SplitPotAttendance from './SplitPotAttendance';
import TopUpByAverage from './TopUpByAverage';

export default function PotIndex(): ReactElement | null {
return <section>
<FullPot/>
<Divider section/>
<SplitPotAttendance/>
<Divider section/>
<TopUpByAverage/>
</section>;
}

0 comments on commit f33aa12

Please sign in to comment.