diff --git a/src/lib/wcif/validation/personAssignmentValidation.test.ts b/src/lib/wcif/validation/personAssignmentValidation.test.ts index 417418e..8fbba4b 100644 --- a/src/lib/wcif/validation/personAssignmentValidation.test.ts +++ b/src/lib/wcif/validation/personAssignmentValidation.test.ts @@ -387,6 +387,222 @@ describe('validatePersonAssignmentScheduleConflicts', () => { const errors = validatePersonAssignmentScheduleConflicts(wcif); expect(errors).toHaveLength(0); }); + + it('should not detect conflicts for grouped activities in cumulative time-limit rounds', () => { + const wcif: Competition = { + ...mockWcif, + events: [ + { + id: '444bf', + rounds: [ + { + id: '444bf-r1', + timeLimit: { centiseconds: 540000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] }, + }, + ], + }, + { + id: '555bf', + rounds: [ + { + id: '555bf-r1', + timeLimit: { centiseconds: 540000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] }, + }, + ], + }, + ] as Competition['events'], + schedule: { + ...mockWcif.schedule, + venues: [ + { + ...mockWcif.schedule.venues[0], + rooms: [ + { + ...mockWcif.schedule.venues[0].rooms[0], + activities: [ + { + id: 11, + name: '4BLD Round 1', + activityCode: '444bf-r1', + startTime: '2024-01-01T09:00:00.000Z', + endTime: '2024-01-01T10:00:00.000Z', + childActivities: [ + { + id: 142, + name: '4BLD Round 1, Group 1', + activityCode: '444bf-r1-g1', + startTime: '2024-01-01T09:00:00.000Z', + endTime: '2024-01-01T10:00:00.000Z', + childActivities: [], + extensions: [], + }, + ], + extensions: [], + }, + { + id: 12, + name: '5BLD Round 1', + activityCode: '555bf-r1', + startTime: '2024-01-01T09:00:00.000Z', + endTime: '2024-01-01T10:00:00.000Z', + childActivities: [ + { + id: 143, + name: '5BLD Round 1, Group 1', + activityCode: '555bf-r1-g1', + startTime: '2024-01-01T09:00:00.000Z', + endTime: '2024-01-01T10:00:00.000Z', + childActivities: [], + extensions: [], + }, + ], + extensions: [], + }, + ], + }, + ], + }, + ], + }, + persons: [ + createPerson({ + assignments: [ + { activityId: 142, stationNumber: null, assignmentCode: 'competitor' }, + { activityId: 143, stationNumber: null, assignmentCode: 'competitor' }, + ], + }), + ], + }; + + const errors = validatePersonAssignmentScheduleConflicts(wcif); + expect(errors).toHaveLength(0); + }); + + it('should not detect conflicts for same-event rounds that share cumulative time limits', () => { + const wcif: Competition = { + ...mockWcif, + events: [ + { + id: '333fm', + rounds: [ + { + id: '333fm-r1', + timeLimit: { centiseconds: 360000, cumulativeRoundIds: ['333fm-r1', '333fm-r2'] }, + }, + { + id: '333fm-r2', + timeLimit: { centiseconds: 360000, cumulativeRoundIds: ['333fm-r1', '333fm-r2'] }, + }, + ], + }, + ] as Competition['events'], + schedule: { + ...mockWcif.schedule, + venues: [ + { + ...mockWcif.schedule.venues[0], + rooms: [ + { + ...mockWcif.schedule.venues[0].rooms[0], + activities: [ + { + id: 21, + name: 'FMC Round 1', + activityCode: '333fm-r1-a1', + startTime: '2024-01-01T09:00:00.000Z', + endTime: '2024-01-01T10:00:00.000Z', + childActivities: [], + extensions: [], + }, + { + id: 22, + name: 'FMC Round 2', + activityCode: '333fm-r2-a1', + startTime: '2024-01-01T09:00:00.000Z', + endTime: '2024-01-01T10:00:00.000Z', + childActivities: [], + extensions: [], + }, + ], + }, + ], + }, + ], + }, + persons: [ + createPerson({ + assignments: [ + { activityId: 21, stationNumber: null, assignmentCode: 'competitor' }, + { activityId: 22, stationNumber: null, assignmentCode: 'competitor' }, + ], + }), + ], + }; + + const errors = validatePersonAssignmentScheduleConflicts(wcif); + expect(errors).toHaveLength(0); + }); + + it('should still detect conflicts within the same cumulative time-limit round', () => { + const wcif: Competition = { + ...mockWcif, + events: [ + { + id: '444bf', + rounds: [ + { + id: '444bf-r1', + timeLimit: { centiseconds: 540000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] }, + }, + ], + }, + ] as Competition['events'], + schedule: { + ...mockWcif.schedule, + venues: [ + { + ...mockWcif.schedule.venues[0], + rooms: [ + { + ...mockWcif.schedule.venues[0].rooms[0], + activities: [ + { + id: 31, + name: '4BLD Round 1, Group 1', + activityCode: '444bf-r1-g1', + startTime: '2024-01-01T09:00:00.000Z', + endTime: '2024-01-01T10:00:00.000Z', + childActivities: [], + extensions: [], + }, + { + id: 32, + name: '4BLD Round 1, Group 2', + activityCode: '444bf-r1-g2', + startTime: '2024-01-01T09:30:00.000Z', + endTime: '2024-01-01T10:30:00.000Z', + childActivities: [], + extensions: [], + }, + ], + }, + ], + }, + ], + }, + persons: [ + createPerson({ + assignments: [ + { activityId: 31, stationNumber: null, assignmentCode: 'competitor' }, + { activityId: 32, stationNumber: null, assignmentCode: 'staff-judge' }, + ], + }), + ], + }; + + const errors = validatePersonAssignmentScheduleConflicts(wcif); + expect(errors).toHaveLength(1); + }); }); describe('validatePersonAssignments', () => { diff --git a/src/lib/wcif/validation/personAssignmentValidation.ts b/src/lib/wcif/validation/personAssignmentValidation.ts index 023304c..0e1c4fb 100644 --- a/src/lib/wcif/validation/personAssignmentValidation.ts +++ b/src/lib/wcif/validation/personAssignmentValidation.ts @@ -1,23 +1,38 @@ import { + acceptedRegistrations, activitiesOverlap, findActivityById, findAllActivities, parseActivityCode, roomByActivity, } from '../../domain'; -import { acceptedRegistrations } from '../../domain'; import { MISSING_ACTIVITY_FOR_PERSON_ASSIGNMENT, PERSON_ASSIGNMENT_SCHEDULE_CONFLICT, type ConflictingAssignment, type ValidationError, } from './types'; -import type { Competition, Person } from '@wca/helpers'; +import type { Competition, Person, Round } from '@wca/helpers'; const pluralizeWord = (count: number, singular: string, plural?: string) => count === 1 ? singular : plural || singular + 's'; -const roundsShareCumulativeTimeLimit = ( +const roundIdForActivity = (activityCode: string) => { + const { eventId, roundNumber } = parseActivityCode(activityCode); + + if (!eventId || !roundNumber) { + return null; + } + + return `${eventId}-r${roundNumber}`; +}; + +const findRoundById = (wcif: Competition, roundId: string): Round | undefined => + wcif.events.flatMap((event) => event.rounds || []).find((round) => round.id === roundId); + +const cumulativeRoundIdsFor = (round?: Round) => round?.timeLimit?.cumulativeRoundIds || []; + +const activitiesShareCumulativeTimeLimit = ( wcif: Competition, activityIdA: number, activityIdB: number @@ -29,34 +44,17 @@ const roundsShareCumulativeTimeLimit = ( return false; } - const roundA = parseActivityCode(activityA.activityCode); - const roundB = parseActivityCode(activityB.activityCode); - if ( - !roundA.eventId || - !roundA.roundNumber || - !roundB.eventId || - !roundB.roundNumber || - roundA.eventId === roundB.eventId - ) { - return false; - } + const roundIdA = roundIdForActivity(activityA.activityCode); + const roundIdB = roundIdForActivity(activityB.activityCode); - const eventA = wcif.events.find((event) => event.id === roundA.eventId); - const eventB = wcif.events.find((event) => event.id === roundB.eventId); - if (!eventA || !eventB) { + if (!roundIdA || !roundIdB || roundIdA === roundIdB) { return false; } - const roundAData = eventA.rounds?.find((round) => round.id === `${roundA.eventId}-r${roundA.roundNumber}`); - const roundBData = eventB.rounds?.find((round) => round.id === `${roundB.eventId}-r${roundB.roundNumber}`); - if (!roundAData?.timeLimit?.cumulativeRoundIds || !roundBData?.timeLimit?.cumulativeRoundIds) { - return false; - } + const cumulativeRoundIdsA = cumulativeRoundIdsFor(findRoundById(wcif, roundIdA)); + const cumulativeRoundIdsB = cumulativeRoundIdsFor(findRoundById(wcif, roundIdB)); - return ( - roundAData.timeLimit.cumulativeRoundIds.includes(roundBData.id) && - roundBData.timeLimit.cumulativeRoundIds.includes(roundAData.id) - ); + return cumulativeRoundIdsA.includes(roundIdB) || cumulativeRoundIdsB.includes(roundIdA); }; /** @@ -117,7 +115,9 @@ const findConflictingAssignmentsForPerson = ( return; } - if (roundsShareCumulativeTimeLimit(wcif, assignment.activityId, otherAssignment.activityId)) { + if ( + activitiesShareCumulativeTimeLimit(wcif, assignment.activityId, otherAssignment.activityId) + ) { return; } diff --git a/src/store/actions.test.ts b/src/store/actions.test.ts index 40ee0e3..32b006a 100644 --- a/src/store/actions.test.ts +++ b/src/store/actions.test.ts @@ -238,7 +238,7 @@ describe('store actions', () => { expect(dispatch.mock.calls[2][0]).toEqual({ type: ActionType.UPDATE_WCIF_ERRORS, errors: [], - replace: false, + replace: true, }); expect(dispatch.mock.calls[3][0]).toEqual({ type: ActionType.FETCHING_WCIF, diff --git a/src/store/actions.ts b/src/store/actions.ts index 03b6c37..bcedf93 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -136,7 +136,7 @@ export const fetchWCIF = (competitionId: string) => async (dispatch: Dispatch) = }; dispatch(updateWCIF(updatedWcif)); - dispatch(updateWcifErrors(validateWcif(updatedWcif))); + dispatch(updateWcifErrors(validateWcif(updatedWcif), true)); } catch (e) { dispatch(updateWcifErrors([e as ValidationError], true)); }