From 955098b6a0bcca6faa09adba11d7748e76a4ebe1 Mon Sep 17 00:00:00 2001 From: bjorndown Date: Mon, 5 Feb 2024 08:21:09 +0100 Subject: [PATCH] WIP. --- .eslintrc.json | 3 - next.config.js | 2 +- src/components/Activities.tsx | 6 +- src/components/CapacityCheck.module.css | 3 - src/components/CapacityCheck.tsx | 4 +- src/components/MatchedActivity.tsx | 55 ++- src/components/Message.module.css | 3 +- src/components/Message.tsx | 2 +- src/components/Participants.tsx | 6 +- src/components/Results.tsx | 8 +- src/components/Statistics.tsx | 71 +++- src/components/UnassignableParticipants.tsx | 107 ++--- src/core/matcher.test.ts | 441 ++++++++++++-------- src/core/matcher.ts | 157 ++++--- src/core/model.test.ts | 129 +++--- src/core/model.ts | 68 ++- src/core/store.ts | 0 src/pages/index.tsx | 2 +- tsconfig.json | 2 +- 19 files changed, 654 insertions(+), 415 deletions(-) delete mode 100644 .eslintrc.json delete mode 100644 src/components/CapacityCheck.module.css delete mode 100644 src/core/store.ts diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bffb357..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/next.config.js b/next.config.js index d4d61e7..ad519fa 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,3 @@ module.exports = { - basePath: process.env.NODE_ENV === 'production' ? '/zuweiser' : '', + basePath: process.env.GITHUB_ACTIONS === 'true' ? '/zuweiser' : '', } \ No newline at end of file diff --git a/src/components/Activities.tsx b/src/components/Activities.tsx index eef1bd9..0b6c8a2 100644 --- a/src/components/Activities.tsx +++ b/src/components/Activities.tsx @@ -19,12 +19,12 @@ export const Activities = ({ onChange }: Props) => { const isIdColumn = (column: string) => column.toLowerCase() === 'id' const isTitleColumn = (column: string) => column.toLowerCase().startsWith('name') || - column.toLowerCase().startsWith('title') + column.toLowerCase().startsWith('titel') const isLimitColumn = (column: string) => - column.toLowerCase().startsWith('limit') || + column.toLowerCase().includes('limit') || column.toLowerCase().includes('max') const isMinimumColumn = (column: string) => - column.toLowerCase().startsWith('minimum') + column.toLowerCase().includes('minimum') const columnHeads = useMemo(() => table?.[0] ?? [], [table]) const rows = useMemo(() => table?.slice(1) ?? [], [table]) diff --git a/src/components/CapacityCheck.module.css b/src/components/CapacityCheck.module.css deleted file mode 100644 index b5d3dba..0000000 --- a/src/components/CapacityCheck.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.container { - padding: 0.3rem 0.2rem; -} diff --git a/src/components/CapacityCheck.tsx b/src/components/CapacityCheck.tsx index e070db3..6cf87f5 100644 --- a/src/components/CapacityCheck.tsx +++ b/src/components/CapacityCheck.tsx @@ -13,9 +13,7 @@ export const CapacityCheck: FunctionComponent = ({ activitiesConfig }) => { const minimumCapacity = useMemo( - () => - participantsConfig.participants.length * - participantsConfig.activitiesPerPerson, + () => participantsConfig.participants.length, [activitiesConfig, participantsConfig] ) const actualCapacity = useMemo(() => { diff --git a/src/components/MatchedActivity.tsx b/src/components/MatchedActivity.tsx index 6fceba2..141529d 100644 --- a/src/components/MatchedActivity.tsx +++ b/src/components/MatchedActivity.tsx @@ -1,16 +1,48 @@ -import { FunctionComponent } from 'react' -import type { AssignableActivity } from '../core/model' +import { FunctionComponent, useMemo } from 'react' +import { + AssignableActivity, + AssignableParticipant, + ParticipantsConfig +} from '../core/model' type Props = { activity: AssignableActivity participantsData: Record + participantsConfig: ParticipantsConfig } + export const MatchedActivity: FunctionComponent = ({ activity, - participantsData + participantsData, + participantsConfig }) => { + const participants: { + participant: AssignableParticipant + execution: number + }[] = useMemo( + () => + Array.from(activity.allParticipants()).map((participants) => ({ + participant: participants, + execution: participants.activities.find( + (value) => value.activity.id === activity.id + ).execution + })), + [activity] + ) + const buildActivityResultHeader = (activity: AssignableActivity) => - `${activity.title} (${activity.participants.length} / ${activity.limit})` + `${activity.title} (${participants.length} / ${activity.limit * participantsConfig.activitiesPerPerson})` + + const getOtherPriorities = ( + participant: AssignableParticipant + ): string | undefined => { + return participant.activities + .filter((value) => value.activity.id !== activity.id) + .map( + ({ activity, execution }) => `${activity.title} (Prio ${participant.priorities.indexOf(activity.id) + 1})` + ) + .join(', ') + } return (
= ({ Priorität + {participantsConfig.activitiesPerPerson > 1 && ( + Durchführung + )} - {activity.participants.map((participant, index) => ( - + {participants.map(({ participant, execution }, index) => ( + {index + 1} - {participantsData[participant.id].map((p, i) => ( + {participantsData[participant.id]?.map((p, i) => ( {p} ))} {participant.priorities.indexOf(activity.id) + 1} + {participantsConfig.activitiesPerPerson > 1 && ( + {execution} + )} ))} diff --git a/src/components/Message.module.css b/src/components/Message.module.css index b5d3dba..ef850c3 100644 --- a/src/components/Message.module.css +++ b/src/components/Message.module.css @@ -1,3 +1,4 @@ .container { - padding: 0.3rem 0.2rem; + padding: 0.35rem 0.5rem; + border-radius: 0.25rem; } diff --git a/src/components/Message.tsx b/src/components/Message.tsx index 483ac51..7341b54 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -1,6 +1,6 @@ import { FunctionComponent, PropsWithChildren } from 'react' import classNames from 'classnames' -import styles from './CapacityCheck.module.css' +import styles from './Message.module.css' type Props = { type: 'ok' | 'bad' | 'warn' diff --git a/src/components/Participants.tsx b/src/components/Participants.tsx index edbda59..3c5ff2b 100644 --- a/src/components/Participants.tsx +++ b/src/components/Participants.tsx @@ -32,7 +32,7 @@ export const Participants = ({ onChange }: Props) => { const isIdColumn = (column: string) => column.toLowerCase() === 'id' const isPriorityColumn = (column: string) => - column.toLowerCase().startsWith('prio') + column.toLowerCase().includes('prio') || column.toLowerCase().includes('wahl') const columnHeads = useMemo(() => table?.[0] ?? [], [table]) const rows = useMemo(() => table?.slice(1) ?? [], [table]) @@ -56,9 +56,7 @@ export const Participants = ({ onChange }: Props) => { return [] } - const priorityColumns = columnHeads.filter((columnHead) => - columnHead.toLowerCase().startsWith('prio') - ) + const priorityColumns = columnHeads.filter(isPriorityColumn) return columnHeads.map((column) => { if (isIdColumn(column)) { return { label: column, matchedAsLabel: 'ID', matchedAs: 'id' } diff --git a/src/components/Results.tsx b/src/components/Results.tsx index e3f3806..5aca208 100644 --- a/src/components/Results.tsx +++ b/src/components/Results.tsx @@ -3,23 +3,26 @@ import { FunctionComponent } from 'react' import type { MatchResult } from '../core/matcher' import { UnassignableParticipants } from './UnassignableParticipants' import { MatchedActivity } from './MatchedActivity' +import {ParticipantsConfig} from '../core/model' type Props = { result: MatchResult participantsData: Record + participantsConfig: ParticipantsConfig } export const Results: FunctionComponent = ({ result, - participantsData + participantsData, participantsConfig }) => { return (

Resultat

- +
@@ -30,6 +33,7 @@ export const Results: FunctionComponent = ({ key={`activity-${activity.id}`} activity={activity} participantsData={participantsData} + participantsConfig={participantsConfig} /> ))}
diff --git a/src/components/Statistics.tsx b/src/components/Statistics.tsx index 83b0ad7..e319887 100644 --- a/src/components/Statistics.tsx +++ b/src/components/Statistics.tsx @@ -1,24 +1,32 @@ import { BarChart } from './BarChart' import { MatchResult } from '../core/matcher' import { FunctionComponent } from 'react' +import _range from 'lodash/range' +import { ParticipantsConfig } from '../core/model' type Props = { result: MatchResult + particpantsConfig: ParticipantsConfig } -export const Statistics: FunctionComponent = ({ result }) => { +export const Statistics: FunctionComponent = ({ + result, + particpantsConfig +}) => { const getXLabel = (priority: number) => `${priority + 1}. Priorität` const computePriorityDistribution = (results: MatchResult): number[] => { const distribution: Record = {} for (const activity of results.activities) { - for (const participant of activity.participants) { - const fullfilledPriority = participant.priorities.indexOf( - activity.id - ) - if (!distribution[getXLabel(fullfilledPriority)]) { - distribution[getXLabel(fullfilledPriority)] = 1 - } else { - distribution[getXLabel(fullfilledPriority)]++ + for (const execution of _range(1)) { + for (const participant of activity.allParticipants()) { + const fullfilledPriority = participant.priorities.indexOf( + activity.id + ) + if (!distribution[getXLabel(fullfilledPriority)]) { + distribution[getXLabel(fullfilledPriority)] = 1 + } else { + distribution[getXLabel(fullfilledPriority)]++ + } } } } @@ -35,7 +43,12 @@ export const Statistics: FunctionComponent = ({ result }) => { Kurs - Zugewiesene Teilnehmer + {_range( + 0, + particpantsConfig.activitiesPerPerson + ).map((execution) => ( + Durchführung {execution + 1} + ))} Minimum Limit @@ -44,9 +57,18 @@ export const Statistics: FunctionComponent = ({ result }) => { {result.activities.map((activity) => ( {activity.title} - - {activity.participants.length} - + {_range( + 0, + particpantsConfig.activitiesPerPerson + ).map((execution) => ( + + { + activity.participants[ + execution + 1 + ]?.length + } + + ))} {activity.minimum} @@ -55,14 +77,21 @@ export const Statistics: FunctionComponent = ({ result }) => { ))} Total - - { - result.participants.filter( - (student) => - !student.needsMoreActivities() - ).length - } - + {_range( + 0, + particpantsConfig.activitiesPerPerson + ).map((execution) => ( + + {result.activities.reduce( + (sum, activity) => + activity.participantsByExecution( + execution + 1 + ).length + sum, + 0 + )} + + ))} + {result.activities .map((course) => course.minimum) diff --git a/src/components/UnassignableParticipants.tsx b/src/components/UnassignableParticipants.tsx index de2e170..53d62b1 100644 --- a/src/components/UnassignableParticipants.tsx +++ b/src/components/UnassignableParticipants.tsx @@ -1,32 +1,36 @@ import React, { FunctionComponent, useState } from 'react' -import { AssignableParticipant } from '../core/model' +import { ParticipantsConfig } from '../core/model' import range from 'lodash/range' +import _range from 'lodash/range' import { MatchResult } from '../core/matcher' import classNames from 'classnames' -import _range from 'lodash/range' import _groupBy from 'lodash/groupBy' type Props = { result: MatchResult participantsData: Record // TODO get rid of this, move into Participant class + participantsConfig: ParticipantsConfig } export const UnassignableParticipants: FunctionComponent = ({ result, - participantsData + participantsData, + participantsConfig }: Props) => { const [showAllParticipants, setShowAllParticipants] = useState(false) - const activitiesPerPerson = 2 // TODO FIXME - const numberOfPriorities = (participants: AssignableParticipant[]) => - range(1, participants[0].priorities.length + 1) console.log( _groupBy( result.participants, (participants) => participants.activities.length ) ) + console.log(participantsData) return (
-

Teilnehmer

+

+ {showAllParticipants + ? 'Alle Teilnehmer' + : 'Teilnehmer mit zu wenig Aktivitäten'} +

= ({ ID - {participantsData['1'].map(() => ( - // TODO FIXME - - ))} - {numberOfPriorities(result.participants).map( - (priority) => ( - Priorität {priority} + {participantsData[result.participants[0].id].map( + () => ( + // TODO FIXME + ) )} - Kurs 1 - Kurs 2 + {range( + 0, + participantsConfig.prioritiesPerPerson + ).map((priority) => ( + Priorität {priority + 1} + ))} + {_range( + 0, + participantsConfig.activitiesPerPerson + ).map((index) => ( + Kurs {index + 1} + ))} @@ -88,44 +99,42 @@ export const UnassignableParticipants: FunctionComponent = ({ activity.id === activityId ) + const assignedTo = + participant.activities.find( + (value) => + value.activity.id === + assignableActivity.id + ) return ( - + <> - { - assignableActivity.title - } - {participant.needsMoreActivities() && - participant.canBeAssignedTo( - assignableActivity - ) && ( - - )} + {!!assignedTo + ? + `${assignableActivity.title} (${assignedTo.execution})`:assignableActivity.title} ) } )} - {_range(0, activitiesPerPerson).map( - (index) => { - const activity = - participant.activities[index] - return activity ? ( - - {activity.title} - - ) : ( - - ) - } - )} + {_range( + 0, + participantsConfig.activitiesPerPerson + ).map((index) => { + const activity = + participant.activities[index] + return activity ? ( + + {`${activity.activity.title} ${activity.execution}`} + + ) : ( + + ) + })} ))} @@ -136,6 +145,10 @@ export const UnassignableParticipants: FunctionComponent = ({ //max-width: 800px; } + .assigned { + font-weight: bold; + } + .needs1More { background-color: var(--warn); } diff --git a/src/core/matcher.test.ts b/src/core/matcher.test.ts index 734e024..dd94298 100644 --- a/src/core/matcher.test.ts +++ b/src/core/matcher.test.ts @@ -1,58 +1,64 @@ import { - match, getMinimalPriorityToFillToMinimum, getParticipantsInPriorityOrder, + match, sortByDeltaToMinimum } from './matcher' import { - Activity, AssignableActivity, AssignableParticipant, - Participant, validateModel } from './model' -const newParticipant = ({ - id, - priorities = [] -}: Partial): AssignableParticipant => - new AssignableParticipant( - { - id, - priorities - }, - { activitiesPerPerson: 2 } - ) - -const newActivity = ({ - id, - minimum = 10, - limit = 10 -}: Partial): AssignableActivity => { - return new AssignableActivity({ id, title: '', minimum, limit }) -} - describe('sortByDeltaToMinimum', () => { it('must return activitys ordered by how many participants are missing to reach its minimum', () => { - const activity1 = newActivity({ - id: '1', - minimum: 3, - limit: 4 - }) - activity1.assignParticipant(newParticipant({ id: '1' })) - const activity2 = newActivity({ - id: '2', - minimum: 2, - limit: 4 - }) - activity2.assignParticipant(newParticipant({ id: '2' })) - const activity3 = newActivity({ - id: '3', - minimum: 1, - limit: 4 - }) - activity3.assignParticipant(newParticipant({ id: '2' })) - const sorted = sortByDeltaToMinimum([activity2, activity3, activity1]) + const activitiesPerPerson = 1 + const activity1 = new AssignableActivity( + { id: '1', title: '', minimum: 3, limit: 4 }, + { activitiesPerPerson } + ) + activity1.assignParticipant( + new AssignableParticipant( + { + id: '1', + priorities: [] + }, + { activitiesPerPerson } + ), + 1 + ) + const activity2 = new AssignableActivity( + { id: '2', title: '', minimum: 2, limit: 4 }, + { activitiesPerPerson } + ) + activity2.assignParticipant( + new AssignableParticipant( + { + id: '2', + priorities: [] + }, + { activitiesPerPerson } + ), + 1 + ) + const activity3 = new AssignableActivity( + { id: '3', title: '', minimum: 1, limit: 4 }, + { activitiesPerPerson } + ) + activity3.assignParticipant( + new AssignableParticipant( + { + id: '2', + priorities: [] + }, + { activitiesPerPerson } + ), + 1 + ) + const sorted = sortByDeltaToMinimum( + [activity2, activity3, activity1], + 1 + ) expect(sorted[0]).toStrictEqual(activity1) expect(sorted[1]).toStrictEqual(activity2) expect(sorted[2]).toStrictEqual(activity3) @@ -61,29 +67,44 @@ describe('sortByDeltaToMinimum', () => { describe('getMinimalPriorityToFillToMinimum', () => { it('must return the priority and index of participant needed to fill activity to the minimum', () => { - const activity = newActivity({ id: '1', minimum: 3 }) - const activity2 = newActivity({ id: '2' }) - const activity3 = newActivity({ id: '3' }) - const participant1 = newParticipant({ - id: '1', - priorities: ['1', '2', '3'] - }) - const participant2 = newParticipant({ - id: '2', - priorities: ['2', '1', '3'] - }) - const participant3 = newParticipant({ - id: '3', - priorities: ['3', '1', '2'] - }) - const participant4 = newParticipant({ - id: '4', - priorities: ['2', '3', '1'] - }) - const participant5 = newParticipant({ - id: '5', - priorities: ['3', '2', '1'] - }) + const activity = new AssignableActivity({id: '1', title: '', minimum: 3, limit: 10}, {activitiesPerPerson: 1}) + const activity2 = new AssignableActivity({id: '2', title: '', minimum: 10, limit: 10}, {activitiesPerPerson: 1}) + const activity3 = new AssignableActivity({id: '3', title: '', minimum: 10, limit: 10}, {activitiesPerPerson: 1}) + const participant1 = new AssignableParticipant( + { + id: '1', + priorities: ['1', '2', '3'] + }, + {activitiesPerPerson: 2} + ) + const participant2 = new AssignableParticipant( + { + id: '2', + priorities: ['2', '1', '3'] + }, + {activitiesPerPerson: 2} + ) + const participant3 = new AssignableParticipant( + { + id: '3', + priorities: ['3', '1', '2'] + }, + {activitiesPerPerson: 2} + ) + const participant4 = new AssignableParticipant( + { + id: '4', + priorities: ['2', '3', '1'] + }, + {activitiesPerPerson: 2} + ) + const participant5 = new AssignableParticipant( + { + id: '5', + priorities: ['3', '2', '1'] + }, + {activitiesPerPerson: 2} + ) const participantsInOrder = [ [participant1], [participant2, participant3], @@ -100,17 +121,23 @@ describe('getMinimalPriorityToFillToMinimum', () => { }) it('must return priority and index = -1 if activity cannot be filled to minimum with the given priorities', () => { - const activity = newActivity({ id: '1', limit: 3 }) - const activity2 = newActivity({ id: '2' }) - const activity3 = newActivity({ id: '3' }) - const participant1 = newParticipant({ - id: '1', - priorities: ['1', '2', '3'] - }) - const participant2 = newParticipant({ - id: '2', - priorities: ['2', '1', '3'] - }) + const activity = new AssignableActivity({id: '1', title: '', minimum: 10, limit: 3}, {activitiesPerPerson: 1}) + const activity2 = new AssignableActivity({id: '2', title: '', minimum: 10, limit: 10}, {activitiesPerPerson: 1}) + const activity3 = new AssignableActivity({id: '3', title: '', minimum: 10, limit: 10}, {activitiesPerPerson: 1}) + const participant1 = new AssignableParticipant( + { + id: '1', + priorities: ['1', '2', '3'] + }, + {activitiesPerPerson: 2} + ) + const participant2 = new AssignableParticipant( + { + id: '2', + priorities: ['2', '1', '3'] + }, + {activitiesPerPerson: 2} + ) const participantsInOrder = [[participant1], [participant2], []] validateModel(participantsInOrder.flat(), [ activity, @@ -125,33 +152,51 @@ describe('getMinimalPriorityToFillToMinimum', () => { describe('getParticipantsInPriorityOrder', () => { it('must return array of arrays containing participants in order of the priority they have given for that activity', () => { - const activity1 = newActivity({ id: '1' }) - const activity2 = newActivity({ id: '2' }) - const activity3 = newActivity({ id: '3' }) - const participant1 = newParticipant({ - id: '1', - priorities: ['1', '2', '3'] - }) - const participant2 = newParticipant({ - id: '2', - priorities: ['3', '2', '1'] - }) - const participant3 = newParticipant({ - id: '3', - priorities: ['2', '3', '1'] - }) - const participant4 = newParticipant({ - id: '4', - priorities: ['1', '3', '2'] - }) - const participant5 = newParticipant({ - id: '5', - priorities: ['2', '3', '1'] - }) - const participant6 = newParticipant({ - id: '6', - priorities: ['2', '1', '3'] - }) + const activity1 = new AssignableActivity({id: '1', title: '', minimum: 10, limit: 10}, {activitiesPerPerson: 1}) + const activity2 = new AssignableActivity({id: '2', title: '', minimum: 10, limit: 10}, {activitiesPerPerson: 1}) + const activity3 = new AssignableActivity({id: '3', title: '', minimum: 10, limit: 10}, {activitiesPerPerson: 1}) + const participant1 = new AssignableParticipant( + { + id: '1', + priorities: ['1', '2', '3'] + }, + {activitiesPerPerson: 2} + ) + const participant2 = new AssignableParticipant( + { + id: '2', + priorities: ['3', '2', '1'] + }, + {activitiesPerPerson: 2} + ) + const participant3 = new AssignableParticipant( + { + id: '3', + priorities: ['2', '3', '1'] + }, + {activitiesPerPerson: 2} + ) + const participant4 = new AssignableParticipant( + { + id: '4', + priorities: ['1', '3', '2'] + }, + {activitiesPerPerson: 2} + ) + const participant5 = new AssignableParticipant( + { + id: '5', + priorities: ['2', '3', '1'] + }, + {activitiesPerPerson: 2} + ) + const participant6 = new AssignableParticipant( + { + id: '6', + priorities: ['2', '1', '3'] + }, + {activitiesPerPerson: 2} + ) const participants = [ participant1, participant2, @@ -187,8 +232,14 @@ describe('getParticipantsInPriorityOrder', () => { describe('match', () => { it('should match single participant to single activity', () => { - const participant = newParticipant({ id: '1', priorities: ['1'] }) - const activity = newActivity({ id: '1', limit: 1 }) + const participant = new AssignableParticipant( + { + id: '1', + priorities: ['1'] + }, + {activitiesPerPerson: 2} + ) + const activity = new AssignableActivity({id: '1', title: '', minimum: 10, limit: 1}, {activitiesPerPerson: 1}) const { activities, participants } = match( { @@ -200,97 +251,121 @@ describe('match', () => { { activities: [activity], shuffleBeforeMatch: false } ) - expect(participants[0].isAssignedTo(activity)).toBe(true) - expect(activities[0].participants[0].id).toBe(participant.id) - expect(activities[0].participants.length).toBe(1) + expect(participants[0].isAssignedTo(activity,1)).toBe(true) + expect(activities[0].participants[1][0].id).toBe(participant.id) + expect(activities[0].participants[1].length).toBe(1) }) it('should not match any more participants to an activity once its limit is reached', () => { - const participant1 = newParticipant({ priorities: ['1'], id: '1' }) - const participant2 = newParticipant({ priorities: ['1'], id: '2' }) - const activity = newActivity({ id: '1', limit: 1 }) + const activitiesPerPerson = 1 + const participant1 = new AssignableParticipant( + { + id: '1', + priorities: ['1'] + }, + {activitiesPerPerson} + ) + const participant2 = new AssignableParticipant( + { + id: '2', + priorities: ['1'] + }, + {activitiesPerPerson} + ) + const activity = new AssignableActivity({id: '1', title: '', minimum: 10, limit: 1}, {activitiesPerPerson}) const { participants, activities } = match( { participants: [participant1, participant2], shuffleBeforeMatch: false, - activitiesPerPerson: 1, + activitiesPerPerson, prioritiesPerPerson: participant1.priorities.length }, { activities: [activity], shuffleBeforeMatch: false } ) expect(participants[0].needsMoreActivities()).toBe(false) - expect(activities[0].participants[0].id).toBe(participant1.id) - expect(activities[0].participants.map((p) => p.id)).toStrictEqual(['1']) + expect(activities[0].participants[1][0].id).toBe(participant1.id) + expect(activities[0].participants[1].map((p) => p.id)).toStrictEqual(['1']) expect(participant2.needsMoreActivities()).toBe(true) }) it('should match multiple participants and activities', () => { - const participant1 = newParticipant({ - id: '1', - priorities: ['1', '2', '3'] - }) - const participant2 = newParticipant({ - id: '2', - priorities: ['4', '3', '2'] - }) - const participant3 = newParticipant({ - id: '3', - priorities: ['1', '3', '2'] - }) - const participant4 = newParticipant({ - id: '4', - priorities: ['3', '2', '1'] - }) - const participant5 = newParticipant({ - id: '5', - priorities: ['2', '1', '3'] - }) - const participant6 = newParticipant({ - id: '6', - priorities: ['3', '4', '1'] - }) - const participant7 = newParticipant({ - id: '7', - priorities: ['1', '3', '2'] - }) - const participant8 = newParticipant({ - id: '8', - priorities: ['3', '4', '2'] - }) - const participant9 = newParticipant({ - id: '9', - priorities: ['3', '1', '4'] - }) - const participant10 = newParticipant({ - id: '10', - priorities: ['4', '1', '2'] - }) - const activity1 = newActivity({ - id: '1', - title: 'activity 1', - minimum: 1, - limit: 3 - }) - const activity2 = newActivity({ - id: '2', - title: 'activity 2', - minimum: 2, - limit: 3 - }) - const activity3 = newActivity({ - id: '3', - title: 'activity 3', - minimum: 2, - limit: 4 - }) - const activity4 = newActivity({ - id: '4', - title: 'activity 3', - minimum: 1, - limit: 4 - }) + const activitiesPerPerson = 1 + const participant1 = new AssignableParticipant( + { + id: '1', + priorities: ['1', '2', '3'] + }, + {activitiesPerPerson} + ) + const participant2 = new AssignableParticipant( + { + id: '2', + priorities: ['4', '3', '2'] + }, + {activitiesPerPerson} + ) + const participant3 = new AssignableParticipant( + { + id: '3', + priorities: ['1', '3', '2'] + }, + {activitiesPerPerson} + ) + const participant4 = new AssignableParticipant( + { + id: '4', + priorities: ['3', '2', '1'] + }, + {activitiesPerPerson} + ) + const participant5 = new AssignableParticipant( + { + id: '5', + priorities: ['2', '1', '3'] + }, + {activitiesPerPerson} + ) + const participant6 = new AssignableParticipant( + { + id: '6', + priorities: ['3', '4', '1'] + }, + {activitiesPerPerson} + ) + const participant7 = new AssignableParticipant( + { + id: '7', + priorities: ['1', '3', '2'] + }, + {activitiesPerPerson} + ) + const participant8 = new AssignableParticipant( + { + id: '8', + priorities: ['3', '4', '2'] + }, + {activitiesPerPerson} + ) + const participant9 = new AssignableParticipant( + { + id: '9', + priorities: ['3', '1', '4'] + }, + {activitiesPerPerson} + ) + const participant10 = new AssignableParticipant( + { + id: '10', + priorities: ['4', '1', '2'] + }, + {activitiesPerPerson} + ) + const activity1 = new AssignableActivity({id: '1', title: '', minimum: 1, limit: 3}, {activitiesPerPerson}) + const activity2 = new AssignableActivity({id: '2', title: '', minimum: 2, limit: 3}, {activitiesPerPerson}) + const activity3 = new AssignableActivity({id: '3', title: '', minimum: 2, limit: 4}, {activitiesPerPerson}) + const activity4 = new AssignableActivity({id: '4', title: '', minimum: 1, limit: 4}, {activitiesPerPerson}) const { participants, activities } = match( { @@ -307,7 +382,7 @@ describe('match', () => { participant10 ], shuffleBeforeMatch: false, - activitiesPerPerson: 1, + activitiesPerPerson, prioritiesPerPerson: participant1.priorities.length }, { @@ -319,19 +394,19 @@ describe('match', () => { expect( activities .find((a) => a.id === activity1.id) - .participants.map((p) => p.id) + .participants[1].map((p) => p.id) ).toStrictEqual( [participant1, participant7, participant10].map((p) => p.id) ) expect( activities .find((a) => a.id === activity2.id) - .participants.map((p) => p.id) + .participants[1].map((p) => p.id) ).toStrictEqual([participant5, participant3].map((p) => p.id)) expect( activities .find((a) => a.id === activity3.id) - .participants.map((p) => p.id) + .participants[1].map((p) => p.id) ).toStrictEqual( [participant4, participant6, participant8, participant9].map( (p) => p.id @@ -340,7 +415,7 @@ describe('match', () => { expect( activities .find((a) => a.id === activity4.id) - .participants.map((p) => p.id) + .participants[1].map((p) => p.id) ).toStrictEqual([participant2].map((p) => p.id)) }) diff --git a/src/core/matcher.ts b/src/core/matcher.ts index 3dc0173..92902ab 100644 --- a/src/core/matcher.ts +++ b/src/core/matcher.ts @@ -7,6 +7,7 @@ import { ParticipantsConfig, validateModel } from './model' +import _range from 'lodash/range' type FillToMinimumConfiguration = { readonly priority: number @@ -36,19 +37,21 @@ export function match( activitiesConfig.shuffleBeforeMatch ? shuffle(activitiesConfig.activities) : activitiesConfig.activities - ).map((activity) => new AssignableActivity(activity)) - - tryToMatch(participantsToMatch, activitiesToMatch) + ).map((activity) => new AssignableActivity(activity, participantsConfig)) + tryToMatch(participantsToMatch, activitiesToMatch, participantsConfig) + console.log(participantsToMatch) + console.log(activitiesToMatch) return { participants: participantsToMatch, activities: activitiesToMatch } } const tryToMatch = ( participants: AssignableParticipant[], - activities: AssignableActivity[] + activities: AssignableActivity[], + participantsConfig: ParticipantsConfig ): void => { const assignableActivities = getAssignableActivities(activities) - const totalNumPriorities = participants[0].priorities.length // TODO FIXME + const totalNumPriorities = participantsConfig.prioritiesPerPerson // TODO FIXME or remove // for (const participant of participants) { // const activity = activities.find( @@ -60,50 +63,68 @@ const tryToMatch = ( // } for (const activity of assignableActivities) { - const participantsInOrder = getParticipantsInPriorityOrder( - participants, - activity, - totalNumPriorities - ) - console.log(activity.title) - const minimalPriority = getMinimalPriorityToFillToMinimum( - activity, - participantsInOrder, - totalNumPriorities - ) - console.log( - `Minimal priority to fill to minimum ${ - minimalPriority.priority + 1 - }, index ${minimalPriority.index}` - ) - for (const priority of range(totalNumPriorities)) { + for (const execution of _range( + 1, + participantsConfig.activitiesPerPerson + 1 + )) { + const participantsInOrder = getParticipantsInPriorityOrder( + participants, + activity, + totalNumPriorities + ) + console.log(activity.title) + const minimalPriority = getMinimalPriorityToFillToMinimum( + activity, + participantsInOrder, + totalNumPriorities + ) console.log( - `Prio ${priority + 1}: ${participantsInOrder[priority].length}` + `Minimal priority to fill to minimum ${ + minimalPriority.priority + 1 + }, index ${minimalPriority.index}` + ) + for (const priority of range(totalNumPriorities)) { + console.log( + `Prio ${priority + 1}: ${participantsInOrder[priority].length}` + ) + } + tryToFillToMinimum( + activity, + participantsInOrder, + minimalPriority, + execution ) } - tryToFillToMinimum(activity, participantsInOrder, minimalPriority) } console.log('Second round') + for (const execution of _range( + 1, + participantsConfig.activitiesPerPerson + 1 + )) { + for (const activity of sortByDeltaToMinimum( + assignableActivities, + execution + )) { + const participantsInOrder = getParticipantsInPriorityOrder( + participants, + activity, + totalNumPriorities + ) + const delta = + activity.minimum - activity.participants[execution].length + const participantsFlat = participantsInOrder.flat() - for (const activity of sortByDeltaToMinimum(assignableActivities)) { - const participantsInOrder = getParticipantsInPriorityOrder( - participants, - activity, - totalNumPriorities - ) - const delta = activity.minimum - activity.participants.length - const participantsFlat = participantsInOrder.flat() - - console.log(`Try to fill delta ${delta} for ${activity.title}`) + console.log(`Try to fill delta ${delta} for ${activity.title}`) - while ( - participantsFlat.length > 0 && - activity.minimum - activity.participants.length > 0 - ) { - const participant = participantsFlat.shift() - if (participant.canBeAssignedTo(activity)) { - activity.assignParticipant(participant) + while ( + participantsFlat.length > 0 && + activity.minimum - activity.participants[execution].length > 0 + ) { + const participant = participantsFlat.shift() + if (participant.canBeAssignedTo(activity, execution)) { + activity.assignParticipant(participant, execution) + } } } } @@ -115,17 +136,23 @@ const tryToMatch = ( participants, totalNumPriorities )) { - console.log(activity.title) - const participantsInOrder = getParticipantsInPriorityOrder( - participants, - activity, - totalNumPriorities - ) - for (const priority of range(totalNumPriorities)) { - for (const index of range(participantsInOrder[priority].length)) { - const participant = participantsInOrder[priority].shift() - if (participant.canBeAssignedTo(activity)) { - activity.assignParticipant(participant) + for (const execution of _range( + 1, + participantsConfig.activitiesPerPerson + 1 + )) { + const participantsInOrder = getParticipantsInPriorityOrder( + participants, + activity, + totalNumPriorities + ) + for (const priority of range(totalNumPriorities)) { + for (const index of range( + participantsInOrder[priority].length + )) { + const participant = participantsInOrder[priority].shift() + if (participant.canBeAssignedTo(activity, execution)) { + activity.assignParticipant(participant, execution) + } } } } @@ -162,10 +189,12 @@ const getUnmatchedParticipants = ( participants.filter((participant) => participant.needsMoreActivities()) const getActivitiesWithMinimumReached = ( - activities: AssignableActivity[] + activities: AssignableActivity[], + execution: number ): AssignableActivity[] => activities.filter( - (activity) => activity.participants.length >= activity.minimum + (activity) => + activity.participants[execution].length >= activity.minimum ) export const getMinimalPriorityToFillToMinimum = ( @@ -215,31 +244,35 @@ export const getMinimalPriorityToFillToMinimum = ( export const tryToFillToMinimum = ( activity: AssignableActivity, participantsInOrder: AssignableParticipant[][], - minimalPriority: FillToMinimumConfiguration + minimalPriority: FillToMinimumConfiguration, + execution: number ): void => { for (const priority of range(minimalPriority.priority)) { for (const index of range(participantsInOrder[priority].length)) { const participant = participantsInOrder[priority].shift() - if (participant.canBeAssignedTo(activity)) { - activity.assignParticipant(participant) + if (participant.canBeAssignedTo(activity, execution)) { + activity.assignParticipant(participant, execution) } } } for (const index of range(minimalPriority.index + 1)) { const participant = participantsInOrder[minimalPriority.priority].shift() - if (participant.canBeAssignedTo(activity)) { - activity.assignParticipant(participant) + if (participant.canBeAssignedTo(activity, execution)) { + activity.assignParticipant(participant, execution) } } } export const sortByDeltaToMinimum = ( - activities: AssignableActivity[] + activities: AssignableActivity[], + execution: number ): AssignableActivity[] => { return activities.sort((activity1, activity2) => { - const delta1 = activity1.minimum - activity1.participants.length - const delta2 = activity2.minimum - activity2.participants.length + const delta1 = + activity1.minimum - activity1.participants[execution].length + const delta2 = + activity2.minimum - activity2.participants[execution].length return delta2 - delta1 }) } diff --git a/src/core/model.test.ts b/src/core/model.test.ts index c29af3b..df785f7 100644 --- a/src/core/model.test.ts +++ b/src/core/model.test.ts @@ -17,7 +17,7 @@ describe('AssignableActivity', () => { limit: 1, minimum: 1, title: 'activity' - }) + },{activitiesPerPerson: 1}) const participant1 = new AssignableParticipant( { id: '1', @@ -33,9 +33,9 @@ describe('AssignableActivity', () => { config ) - activity.assignParticipant(participant1) + activity.assignParticipant(participant1,1) - expect(() => activity.assignParticipant(participant2)).toThrowError( + expect(() => activity.assignParticipant(participant2, 1)).toThrowError( /full/ ) }) @@ -47,7 +47,7 @@ describe('AssignableActivity', () => { limit: 1, minimum: 1, title: 'activity' - }) + },{activitiesPerPerson: 1}) const participant1 = new AssignableParticipant( { id: '1', @@ -56,9 +56,9 @@ describe('AssignableActivity', () => { config ) - activity.assignParticipant(participant1) + activity.assignParticipant(participant1, 1) - expect(activity.isNotFull()).toBe(false) + expect(activity.isNotFull(1)).toBe(false) }) it('must return true if below limit', () => { const activity = new AssignableActivity({ @@ -66,7 +66,7 @@ describe('AssignableActivity', () => { limit: 2, minimum: 1, title: 'activity' - }) + },{activitiesPerPerson: 1}) const participant1 = new AssignableParticipant( { id: '1', @@ -75,9 +75,9 @@ describe('AssignableActivity', () => { config ) - activity.assignParticipant(participant1) + activity.assignParticipant(participant1, 1) - expect(activity.isNotFull()).toBe(true) + expect(activity.isNotFull(1)).toBe(true) }) }) }) @@ -98,11 +98,11 @@ describe('AssignableParticipant', () => { limit: 2, minimum: 2, title: 'a' - }) + },{activitiesPerPerson: 1}) - participant1.assign(activity) + participant1.assign(activity, 0) - expect(() => participant1.assign(activity)).toThrowError( + expect(() => participant1.assign(activity, 0)).toThrowError( /already assigned/ ) }) @@ -147,24 +147,33 @@ describe('validateModel', () => { }) it('should fail if activity ids are not unique', () => { - const activity1 = new AssignableActivity({ - id: '1', - title: '', - minimum: 0, - limit: 0 - }) - const activity2 = new AssignableActivity({ - id: '1', - title: '', - minimum: 0, - limit: 0 - }) - const activity3 = new AssignableActivity({ - id: '2', - title: '', - minimum: 0, - limit: 0 - }) + const activity1 = new AssignableActivity( + { + id: '1', + title: '', + minimum: 0, + limit: 0 + }, + { activitiesPerPerson: 1 } + ) + const activity2 = new AssignableActivity( + { + id: '1', + title: '', + minimum: 0, + limit: 0 + }, + { activitiesPerPerson: 1 } + ) + const activity3 = new AssignableActivity( + { + id: '2', + title: '', + minimum: 0, + limit: 0 + }, + { activitiesPerPerson: 1 } + ) expect(() => match( @@ -190,18 +199,24 @@ describe('validateModel', () => { }, config ) - const activity1 = new AssignableActivity({ - id: '1', - title: 'activity 1', - limit: 1, - minimum: 0 - }) - const activity3 = new AssignableActivity({ - id: '3', - title: 'activity 3', - limit: 1, - minimum: 0 - }) + const activity1 = new AssignableActivity( + { + id: '1', + title: 'activity 1', + limit: 1, + minimum: 0 + }, + { activitiesPerPerson: 1 } + ) + const activity3 = new AssignableActivity( + { + id: '3', + title: 'activity 3', + limit: 1, + minimum: 0 + }, + { activitiesPerPerson: 1 } + ) expect(() => match( @@ -236,18 +251,24 @@ describe('validateModel', () => { }, config ) - const activity1 = new AssignableActivity({ - id: '1', - title: 'activity 1', - limit: 1, - minimum: 0 - }) - const activity2 = new AssignableActivity({ - id: '2', - title: 'activity 2', - limit: 1, - minimum: 0 - }) + const activity1 = new AssignableActivity( + { + id: '1', + title: 'activity 1', + limit: 1, + minimum: 0 + }, + { activitiesPerPerson: 2 } + ) + const activity2 = new AssignableActivity( + { + id: '2', + title: 'activity 2', + limit: 1, + minimum: 0 + }, + { activitiesPerPerson: 2 } + ) expect(() => match( diff --git a/src/core/model.ts b/src/core/model.ts index 5017041..fac63dc 100644 --- a/src/core/model.ts +++ b/src/core/model.ts @@ -1,3 +1,5 @@ +import _range from 'lodash/range' + export type ActivitiesConfig = { readonly activities: Activity[] readonly shuffleBeforeMatch: boolean @@ -23,7 +25,8 @@ export type Participant = { } export class AssignableParticipant { - public activities: AssignableActivity[] = [] + public activities: { activity: AssignableActivity; execution: number }[] = + [] constructor( private participant: Participant, @@ -38,29 +41,39 @@ export class AssignableParticipant { return this.participant.priorities } - assign(activity: AssignableActivity) { + assign(activity: AssignableActivity, execution: number) { if (!this.needsMoreActivities()) { throw new Error( 'Participant has reached limit of activities per person' ) } - if (this.isAssignedTo(activity)) { + if (this.isAssignedTo(activity, execution)) { throw new Error('Participant is already assigned to this activity') } - this.activities.push(activity) + this.activities.push({ activity, execution }) } - isAssignedTo(activity: Activity) { + isAssignedTo(activity: Activity, execution: number) { return this.activities.some( - (assignedActivity) => assignedActivity.id === activity.id + (assignedActivity) => + assignedActivity.activity.id === activity.id && + assignedActivity.execution === execution ) } - canBeAssignedTo(activity: AssignableActivity): boolean { + canBeAssignedTo(activity: AssignableActivity, execution: number): boolean { + const anyOtherActivityInSameExecution = this.activities.some( + (activity) => activity.execution === execution + ) + const sameActivityInOtherExecution = this.activities.some( + (value) => value.activity.id === activity.id + ) return ( this.needsMoreActivities() && - !this.isAssignedTo(activity) && - activity.isNotFull() + !this.isAssignedTo(activity, execution) && + activity.isNotFull(execution) && + !anyOtherActivityInSameExecution && + !sameActivityInOtherExecution ) } @@ -70,9 +83,28 @@ export class AssignableParticipant { } export class AssignableActivity { - public readonly participants: Participant[] = [] + public readonly participants: Record = {} - constructor(private activity: Activity) {} + constructor( + private activity: Activity, + participantsConfig: Pick + ) { + _range(1, participantsConfig.activitiesPerPerson + 1).forEach( + (execution) => (this.participants[execution] = []) + ) + } + + *allParticipants(): Generator { + for (const [_, participants] of Object.entries(this.participants)) { + for (const participant of participants) { + yield participant + } + } + } + + participantsByExecution(execution: number): AssignableParticipant[] { + return this.participants[execution] + } get id() { return this.activity.id @@ -90,12 +122,12 @@ export class AssignableActivity { return this.activity.title } - assignParticipant(participant: AssignableParticipant) { - if (!this.isNotFull()) { + assignParticipant(participant: AssignableParticipant, execution: number) { + if (!this.isNotFull(execution)) { throw new Error(`Activity title="${this.title} is full`) } if ( - this.participants.some( + this.participants[execution].some( (existingParticipant) => existingParticipant.id === participant.id ) @@ -103,12 +135,12 @@ export class AssignableActivity { throw new Error('Participant already in this activity') } - this.participants.push(participant) - participant.assign(this) + this.participants[execution].push(participant) + participant.assign(this, execution) } - isNotFull() { - return this.participants.length < this.limit + isNotFull(execution: number) { + return this.participants[execution].length < this.limit } } diff --git a/src/core/store.ts b/src/core/store.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/index.tsx b/src/pages/index.tsx index c042cae..dbc6a6a 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -85,7 +85,7 @@ const Index = () => { )} {result && ( - + )}