Skip to content

Commit 6937f65

Browse files
feat: replace numeric table assignment with dynamic two-floor lettere… (#426)
* feat: replace numeric table assignment with dynamic two-floor lettered seating system * update migration, pre-set row and team number limit, added numeric comparison * capacity error, cleaner 2nd floor handlig, changed str | num value * update tableNumber in tests * updated string context * convert capacity overflow to warning * add global warning --------- Co-authored-by: michelleyeoh <74385095+michelleyeoh@users.noreply.github.com> Co-authored-by: michelleyeoh <michellew.yeoh@gmail.com>
1 parent 1d25ac9 commit 6937f65

16 files changed

Lines changed: 438 additions & 73 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { validateCsvBlob } from '@utils/csv-ingestion/csvAlgorithm';
2+
3+
type TeamRow = {
4+
tableNumber: number;
5+
projectTitle: string;
6+
status: string;
7+
primaryTrack: string;
8+
};
9+
10+
function buildCsv(rows: TeamRow[]): string {
11+
const header = [
12+
'Table Number',
13+
'Project Status',
14+
'Project Title',
15+
'Track #1 (Primary Track)',
16+
'Track #2',
17+
'Track #3',
18+
'Opt-In Prizes',
19+
].join(',');
20+
21+
const lines = rows.map((row) =>
22+
[
23+
String(row.tableNumber),
24+
row.status,
25+
row.projectTitle,
26+
row.primaryTrack,
27+
'',
28+
'',
29+
'',
30+
].join(',')
31+
);
32+
33+
return `${header}\n${lines.join('\n')}`;
34+
}
35+
36+
describe('csvAlgorithm table assignment', () => {
37+
it('assigns the first hardware team to A1', async () => {
38+
const csv = buildCsv([
39+
{
40+
tableNumber: 1,
41+
status: 'Submitted',
42+
projectTitle: 'Hardware Team',
43+
primaryTrack: 'Best Hardware Hack',
44+
},
45+
{
46+
tableNumber: 2,
47+
status: 'Submitted',
48+
projectTitle: 'Other Team',
49+
primaryTrack: 'Best AI/ML Hack',
50+
},
51+
]);
52+
53+
const res = await validateCsvBlob(new Blob([csv], { type: 'text/csv' }));
54+
55+
expect(res.ok).toBe(true);
56+
expect(res.body).not.toBeNull();
57+
58+
const hardwareTeam = res.body?.find((t) => t.name === 'Hardware Team');
59+
expect(hardwareTeam?.tableNumber).toBe('A1');
60+
});
61+
62+
it('starts floor 2 at I1 after floor 1 capacity is filled', async () => {
63+
// Floor 1: 8 rows * 13 seats = 104. Floor 2 starts at 105th team
64+
const rows: TeamRow[] = Array.from({ length: 105 }, (_, i) => ({
65+
tableNumber: i + 1,
66+
status: 'Submitted',
67+
projectTitle: `Team ${i + 1}`,
68+
primaryTrack: 'Best AI/ML Hack',
69+
}));
70+
71+
const csv = buildCsv(rows);
72+
const res = await validateCsvBlob(new Blob([csv], { type: 'text/csv' }));
73+
74+
if (!res.ok) {
75+
console.error('Unexpected error:', res.error);
76+
}
77+
expect(res.ok).toBe(true);
78+
expect(res.body).not.toBeNull();
79+
expect(res.body?.length).toBe(105);
80+
// Last team on floor 1
81+
expect(res.body?.[103].tableNumber).toBe('H13');
82+
// First team on floor 2
83+
expect(res.body?.[104].tableNumber).toBe('I1');
84+
});
85+
86+
it('returns a global warning when team count exceeds venue capacity', async () => {
87+
// Total capacity: 104 (F1) + 60 (F2) = 164
88+
// We provide 167 teams to trigger WAIT-n overflow labels.
89+
const rows: TeamRow[] = Array.from({ length: 167 }, (_, i) => ({
90+
tableNumber: i + 1,
91+
status: 'Submitted',
92+
projectTitle: `Team ${i + 1}`,
93+
primaryTrack: 'Best AI/ML Hack',
94+
}));
95+
96+
const csv = buildCsv(rows);
97+
const res = await validateCsvBlob(new Blob([csv], { type: 'text/csv' }));
98+
99+
// Capacity overflow is non-blocking and surfaced as a global warning.
100+
expect(res.ok).toBe(true);
101+
expect(res.error).toBeNull();
102+
expect(res.report.errorRows).toBe(0);
103+
expect(res.report.warningRows).toBe(0);
104+
expect(res.report.globalWarnings).toBeDefined();
105+
expect(res.report.globalWarnings?.[0]).toContain('Capacity Exceeded');
106+
107+
// Verify that even with overflow, data was processed.
108+
expect(res.body).toHaveLength(167);
109+
110+
// Verify the last team got a WAIT label.
111+
const lastTeam = res.body?.[166];
112+
expect(lastTeam?.tableNumber).toBe('WAIT-3');
113+
});
114+
});

__tests__/logic/ingestTeams.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ describe('ingestTeams', () => {
2323
{
2424
name: 'Team 1',
2525
teamNumber: 1,
26-
tableNumber: 1,
26+
tableNumber: 'A1',
2727
tracks: ['Track A'],
2828
active: true,
2929
},
3030
{
3131
name: 'Team 2',
3232
teamNumber: 2,
33-
tableNumber: 2,
33+
tableNumber: 'A2',
3434
tracks: ['Track B'],
3535
active: true,
3636
},
@@ -74,7 +74,7 @@ describe('ingestTeams', () => {
7474
{
7575
name: 'Invalid Team',
7676
teamNumber: 999,
77-
tableNumber: 999,
77+
tableNumber: '999',
7878
tracks: ['Invalid Track'],
7979
active: true,
8080
},
@@ -99,14 +99,14 @@ describe('ingestTeams', () => {
9999
{
100100
name: 'Team 1',
101101
teamNumber: 1,
102-
tableNumber: 1,
102+
tableNumber: 'A1',
103103
tracks: ['Track A'],
104104
active: true,
105105
},
106106
{
107107
name: 'Team 1 Duplicate',
108108
teamNumber: 1,
109-
tableNumber: 2,
109+
tableNumber: 'A2',
110110
tracks: ['Track B'],
111111
active: true,
112112
},
@@ -131,7 +131,7 @@ describe('ingestTeams', () => {
131131
{
132132
name: 'Solo Team',
133133
teamNumber: 1,
134-
tableNumber: 1,
134+
tableNumber: 'A1',
135135
tracks: ['Track A', 'Track B'],
136136
active: true,
137137
},
@@ -157,7 +157,7 @@ describe('ingestTeams', () => {
157157
{
158158
name: 'Complete Team',
159159
teamNumber: 42,
160-
tableNumber: 10,
160+
tableNumber: 'A10',
161161
tracks: ['Track A', 'Track B', 'Track C'],
162162
active: false,
163163
},

__tests__/logic/validateCSV.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('validateCSV', () => {
4040
{
4141
name: 'Test Project',
4242
teamNumber: 1,
43-
tableNumber: 1,
43+
tableNumber: 'A1',
4444
tracks: ['Track A'],
4545
active: true,
4646
},
@@ -134,7 +134,7 @@ describe('validateCSV', () => {
134134
{
135135
name: 'Test',
136136
teamNumber: 1,
137-
tableNumber: 1,
137+
tableNumber: 'A1',
138138
tracks: ['Track A'],
139139
active: true,
140140
},
@@ -178,7 +178,7 @@ describe('validateCSV', () => {
178178
{
179179
name: 'Test',
180180
teamNumber: 1,
181-
tableNumber: 1,
181+
tableNumber: 'A1',
182182
tracks: ['Track A'],
183183
active: true,
184184
},
@@ -218,14 +218,14 @@ describe('validateCSV', () => {
218218
{
219219
name: 'Valid Team',
220220
teamNumber: 1,
221-
tableNumber: 1,
221+
tableNumber: 'A1',
222222
tracks: ['Track A'],
223223
active: true,
224224
},
225225
{
226226
name: 'Invalid Team',
227227
teamNumber: NaN,
228-
tableNumber: 2,
228+
tableNumber: 'A2',
229229
tracks: ['Track B'],
230230
active: true,
231231
},
@@ -235,7 +235,7 @@ describe('validateCSV', () => {
235235
{
236236
name: 'Valid Team',
237237
teamNumber: 1,
238-
tableNumber: 1,
238+
tableNumber: 'A1',
239239
tracks: ['Track A'],
240240
active: true,
241241
},

__tests__/parseInviteCSV.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ describe('parseInviteCSV', () => {
3636
});
3737

3838
it('detects header with "first" keyword', () => {
39-
const csv =
40-
'First,Last,Contact\nAlice,Smith,alice@example.com\n';
39+
const csv = 'First,Last,Contact\nAlice,Smith,alice@example.com\n';
4140

4241
const result = parseInviteCSV(csv);
4342
expect(result.ok).toBe(true);
@@ -165,8 +164,7 @@ describe('parseInviteCSV', () => {
165164

166165
it('handles quoted fields with commas', () => {
167166
const csv =
168-
'First Name,Last Name,Email\n' +
169-
'"Alice, Jr.",Smith,alice@example.com\n';
167+
'First Name,Last Name,Email\n' + '"Alice, Jr.",Smith,alice@example.com\n';
170168

171169
const result = parseInviteCSV(csv);
172170
expect(result.ok).toBe(true);

__tests__/processBulkInvites.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,12 @@ describe('processBulkInvites', () => {
107107
it('uses preprocess to filter items and include early results', async () => {
108108
mockParseCSV.mockReturnValue({ ok: true, body: [ALICE, BOB, CHARLIE] });
109109

110-
const processOne = jest.fn(async (item: InviteData): Promise<InviteResult> => ({
111-
email: item.email,
112-
success: true,
113-
}));
110+
const processOne = jest.fn(
111+
async (item: InviteData): Promise<InviteResult> => ({
112+
email: item.email,
113+
success: true,
114+
})
115+
);
114116

115117
const result = await processBulkInvites('csv', {
116118
label: 'Test',

0 commit comments

Comments
 (0)