Skip to content

Commit 19aa90f

Browse files
authored
Merge pull request #84 from coder13/agent/sync-main-into-beta
Sync main into beta
2 parents d6c5103 + ecafb30 commit 19aa90f

3 files changed

Lines changed: 155 additions & 32 deletions

File tree

src/containers/PersonalSchedule/utils.test.ts

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,71 @@ const worldsAssignmentsPerson = {
3838
assignments: [
3939
{
4040
staff: 'Stage Stream - Main',
41-
startTime: '2025-07-03T18:00:00Z',
42-
endTime: '2025-07-03T19:00:00Z',
41+
startTime: '2025-07-04T01:00:00Z',
42+
endTime: '2025-07-04T02:00:00Z',
4343
},
4444
],
4545
},
4646
},
4747
],
4848
} as unknown as Person;
4949

50+
const fmcWcif = {
51+
...wcif,
52+
schedule: {
53+
...wcif.schedule,
54+
venues: [
55+
{
56+
...wcif.schedule.venues[0],
57+
rooms: [
58+
{
59+
id: 1,
60+
name: 'FMC Room',
61+
color: '#ffffff',
62+
extensions: [],
63+
activities: [
64+
{
65+
id: 100,
66+
name: 'Fewest Moves Attempt 1',
67+
activityCode: '333fm-r1-a1',
68+
startTime: '2025-07-03T18:00:00Z',
69+
endTime: '2025-07-03T19:00:00Z',
70+
extensions: [],
71+
childActivities: [
72+
{
73+
id: 101,
74+
name: 'Fewest Moves Attempt 1 Group 1',
75+
activityCode: '333fm-r1-g1-a1',
76+
startTime: '2025-07-03T18:00:00Z',
77+
endTime: '2025-07-03T19:00:00Z',
78+
extensions: [],
79+
childActivities: [],
80+
},
81+
],
82+
},
83+
],
84+
},
85+
],
86+
},
87+
],
88+
},
89+
} as unknown as Competition;
90+
91+
const fmcPerson = {
92+
registrantId: 216,
93+
assignments: [
94+
{
95+
activityId: 101,
96+
assignmentCode: 'competitor',
97+
stationNumber: null,
98+
},
99+
],
100+
registration: {
101+
eventIds: ['333fm'],
102+
},
103+
extensions: [],
104+
} as unknown as Person;
105+
50106
describe('PersonalSchedule utils', () => {
51107
it('creates parse-safe activities for worlds assignments with free-form staff names', () => {
52108
const [assignment] = getAllAssignments(wcif, worldsAssignmentsPerson);
@@ -66,4 +122,42 @@ describe('PersonalSchedule utils', () => {
66122
expect(scheduleDay.assignments).toHaveLength(1);
67123
expect(scheduleDay.assignments[0].assignment.assignmentCode).toBe('Stage Stream - Main');
68124
});
125+
126+
it('keeps standard other activities in the other namespace', () => {
127+
expect(parseActivityCodeFlexible('other-lunch-g2')).toMatchObject({
128+
eventId: 'other-lunch',
129+
roundNumber: 1,
130+
groupNumber: 2,
131+
attemptNumber: null,
132+
});
133+
expect(parseActivityCodeFlexible('other-misc')).toMatchObject({
134+
eventId: 'other-misc',
135+
});
136+
});
137+
138+
it('only includes the FMC attempt activity assigned to the competitor', () => {
139+
const fmcAssignments = getAllAssignments(fmcWcif, fmcPerson).filter(({ activity }) =>
140+
activity?.activityCode.startsWith('333fm'),
141+
);
142+
143+
expect(fmcAssignments).toHaveLength(1);
144+
expect(fmcAssignments[0].activityId).toBe(101);
145+
});
146+
147+
it('does not create FMC assignments without a matching activity assignment', () => {
148+
const personWithoutFmcAssignment = {
149+
...fmcPerson,
150+
assignments: [],
151+
} as Person;
152+
153+
expect(getAllAssignments(fmcWcif, personWithoutFmcAssignment)).toHaveLength(0);
154+
});
155+
156+
it('includes a day that only has an assigned FMC attempt', () => {
157+
const [scheduleDay] = getGroupedAssignmentsByDate(fmcWcif, fmcPerson);
158+
159+
expect(scheduleDay.date).toBe('Thursday, 7/3/2025');
160+
expect(scheduleDay.assignments).toHaveLength(1);
161+
expect(scheduleDay.assignments[0].assignment.activityId).toBe(101);
162+
});
69163
});

src/containers/PersonalSchedule/utils.ts

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
import { Activity, Assignment, Competition, Person } from '@wca/helpers';
22
import { getWorldAssignmentsExtension } from '@/extensions/com.competitiongroups.worldsassignments';
33
import i18n from '@/i18n';
4-
import { getAllActivities, getRooms } from '@/lib/activities';
4+
import { getAllActivities } from '@/lib/activities';
55
import { isUnofficialParsedActivityCode, parseActivityCodeFlexible } from '@/lib/activityCodes';
66
import { eventById, isOfficialEventId } from '@/lib/events';
77
import { formatNumericDate, getNumericDateFormatter } from '@/lib/time';
88
import { byDate } from '@/lib/utils';
99

10+
type ActivityWithLocation = Activity & {
11+
room?: { id: number };
12+
parent?: { room?: { id: number } };
13+
};
14+
15+
const getActivityTimeZone = (
16+
venues: Competition['schedule']['venues'],
17+
activity: ActivityWithLocation,
18+
) => {
19+
const roomId = activity.room?.id ?? activity.parent?.room?.id;
20+
const venue = venues.find((candidate) => candidate.rooms.some((room) => room.id === roomId));
21+
22+
return venue?.timezone ?? venues[0]?.timezone;
23+
};
24+
1025
export const getNormalAssignments = (wcif: Competition, person: Person) => {
1126
const allActivities = getAllActivities(wcif);
1227

@@ -105,20 +120,24 @@ const getFmcAttemptAssignments = (wcif: Competition, person: Person) => {
105120
return parsed.eventId === '333fm' && parsed.attemptNumber !== null;
106121
});
107122

108-
return fmcAttemptActivities.map(
109-
(
110-
activity,
111-
): Assignment & {
112-
type: 'extra';
113-
activity: Activity;
114-
} => ({
115-
type: 'extra',
116-
assignmentCode: 'competitor',
117-
activityId: activity.id,
118-
stationNumber: null,
119-
activity,
120-
}),
121-
);
123+
const personActivities = person.assignments?.map((ass) => ass.activityId);
124+
125+
return fmcAttemptActivities
126+
.filter((activity) => personActivities?.includes(activity.id))
127+
.map(
128+
(
129+
activity,
130+
): Assignment & {
131+
type: 'extra';
132+
activity: Activity;
133+
} => ({
134+
type: 'extra',
135+
assignmentCode: 'competitor',
136+
activityId: activity.id,
137+
stationNumber: null,
138+
activity,
139+
}),
140+
);
122141
};
123142

124143
export const getAllAssignments = (wcif: Competition, person: Person) => {
@@ -157,18 +176,14 @@ export const getGroupedAssignmentsByDate = (wcif: Competition, person: Person) =
157176
};
158177
}
159178

160-
const roomId = a.activity && 'room' in a.activity && a.activity.room?.id;
161-
const parent = a.activity && 'parent' in a.activity && a.activity.parent;
162-
163-
const venue = venues.find((v) => v.rooms.some((r) => r.id === roomId || parent));
164-
165179
const dateTime = new Date(a.activity?.startTime);
166-
const date = formatNumericDate(dateTime, venue?.timezone);
180+
const timeZone = getActivityTimeZone(venues, a.activity);
181+
const date = formatNumericDate(dateTime, timeZone);
167182

168183
return {
169184
approxDateTime: dateTime.getTime(),
170185
date: date,
171-
dateParts: getNumericDateFormatter(venue?.timezone).formatToParts(dateTime),
186+
dateParts: getNumericDateFormatter(timeZone).formatToParts(dateTime),
172187
};
173188
})
174189
.filter((v, i, arr) => arr.findIndex(({ date }) => date === v.date) === i);
@@ -186,7 +201,6 @@ export const getGroupedAssignmentsByDate = (wcif: Competition, person: Person) =
186201
export const getAssignmentsWithParsedDate = (wcif: Competition, person: Person) => {
187202
const allAssignments = getAllAssignments(wcif, person);
188203
const venues = wcif.schedule.venues;
189-
const rooms = getRooms(wcif);
190204

191205
return allAssignments
192206
.map((assignment) => {
@@ -200,7 +214,10 @@ export const getAssignmentsWithParsedDate = (wcif: Competition, person: Person)
200214
return {
201215
assignment,
202216
activity,
203-
date: formatNumericDate(new Date(activity.startTime), venues[0].timezone),
217+
date: formatNumericDate(
218+
new Date(activity.startTime),
219+
getActivityTimeZone(venues, activity),
220+
),
204221
};
205222
}
206223

@@ -212,16 +229,12 @@ export const getAssignmentsWithParsedDate = (wcif: Competition, person: Person)
212229
};
213230
}
214231

215-
const roomId = (activity.room || activity.parent?.room)?.id;
216-
217-
const venue = activity?.room?.id ? rooms.find((r) => r.id === roomId)?.venue : venues[0];
218-
219232
const dateTime = new Date(activity.startTime);
220233

221234
return {
222235
assignment,
223236
activity,
224-
date: formatNumericDate(dateTime, venue?.timezone),
237+
date: formatNumericDate(dateTime, getActivityTimeZone(venues, activity)),
225238
};
226239
}
227240
})

src/lib/activityCodes.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export const parseActivityCodeFlexible = (
5252
activityCode: string,
5353
): ParsedActivityCode | UnofficialParsedActivityCode => {
5454
const eventId = activityCode.split('-')[0];
55-
if (activityCode.startsWith('other') || !isOfficialEventId(eventId)) {
55+
if (activityCode.startsWith('other')) {
56+
return parseOtherActivityCode(activityCode);
57+
}
58+
if (!isOfficialEventId(eventId)) {
5659
return parseUnofficialActivityCode(activityCode);
5760
}
5861

@@ -62,6 +65,19 @@ export const parseActivityCodeFlexible = (
6265
const normalizeEventId = (eventId: string): string =>
6366
eventId.replace('other-', '').replace('unofficial-', '');
6467

68+
export const parseOtherActivityCode = (activityCode: string): UnofficialParsedActivityCode => {
69+
const regex = /other-(?:(\w+))?(?:-g(\d+))?/;
70+
const [, e, g] = activityCode.match(regex) as string[];
71+
72+
return {
73+
rawEventId: e,
74+
eventId: `other-${e}`,
75+
roundNumber: 1,
76+
groupNumber: g ? parseInt(g, 10) : null,
77+
attemptNumber: null,
78+
};
79+
};
80+
6581
export const parseUnofficialActivityCode = (activityCode: string): UnofficialParsedActivityCode => {
6682
const regex = /^([\w-]+?)(?:-r(\d+))?(?:-g(\d+))?(?:-a(\d+))?$/;
6783
const matches = activityCode.match(regex);

0 commit comments

Comments
 (0)