Skip to content

Commit 342e43c

Browse files
feat: Standardize CSV export for certificates and final results with consistent columns and top 5 filtering
1 parent a36884b commit 342e43c

3 files changed

Lines changed: 335 additions & 69 deletions

File tree

app/admin/event/group/page.js

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default function GroupEventLeaderboardPage() {
4747
};
4848
}
4949
acc[key].members.push({
50+
...participant,
5051
name: participant.studentFullName || 'Unknown',
5152
id: participant.studentId || 'Unknown ID',
5253
ATTENDEE_STATUS: participant.ATTENDEE_STATUS,
@@ -169,6 +170,84 @@ export default function GroupEventLeaderboardPage() {
169170
fetchJudgeNames();
170171
}, [eventMetadata, groups, eventName]);
171172

173+
const exportForCert = () => {
174+
if (!groups || !eventMetadata) return;
175+
176+
const topGroups = groups.slice(0, 5);
177+
const flatList = [];
178+
179+
topGroups.forEach((group, index) => {
180+
const rank = index + 1;
181+
group.members.forEach((member) => {
182+
const row = {
183+
eventName: eventName,
184+
Rank: rank,
185+
...member,
186+
// Rename keys to match Individual export
187+
studentFullName: member.name,
188+
studentId: member.id,
189+
district: group.district || 'Unknown',
190+
OverallTotal: parseFloat(group.overallTotal ?? 0).toFixed(2),
191+
};
192+
flatList.push(row);
193+
});
194+
});
195+
196+
if (flatList.length === 0) return;
197+
198+
const allKeys = new Set();
199+
flatList.forEach((item) => {
200+
Object.keys(item).forEach((key) => {
201+
const val = item[key];
202+
if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
203+
return;
204+
}
205+
allKeys.add(key);
206+
});
207+
});
208+
209+
const headers = Array.from(allKeys).sort();
210+
const prioritized = [
211+
'eventName',
212+
'Rank',
213+
'studentFullName',
214+
'studentId',
215+
'district',
216+
'studentGroup',
217+
'OverallTotal',
218+
];
219+
const sortedHeaders = [
220+
...prioritized.filter((h) => headers.includes(h)),
221+
...headers.filter((h) => !prioritized.includes(h)),
222+
];
223+
224+
const escapeCSV = (value) => {
225+
if (value == null) return '';
226+
if (Array.isArray(value)) return `"${value.join('; ')}"`;
227+
const str = String(value);
228+
if (str.includes(',') || str.includes('\n') || str.includes('"')) {
229+
return `"${str.replace(/"/g, '""')}"`;
230+
}
231+
return str;
232+
};
233+
234+
const csv = [
235+
sortedHeaders.map(escapeCSV).join(','),
236+
...flatList.map((row) =>
237+
sortedHeaders.map((header) => escapeCSV(row[header])).join(','),
238+
),
239+
].join('\r\n');
240+
241+
const BOM = '\uFEFF';
242+
const blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8;' });
243+
const url = URL.createObjectURL(blob);
244+
const link = document.createElement('a');
245+
link.href = url;
246+
link.download = `${eventName}_top5_cert_export.csv`;
247+
link.click();
248+
URL.revokeObjectURL(url);
249+
};
250+
172251
// Add this function for CSV export
173252
const exportToCSV = () => {
174253
if (!groups || !eventMetadata || orderedJudges.length === 0) return;
@@ -377,12 +456,20 @@ export default function GroupEventLeaderboardPage() {
377456
<div className="rounded-2xl p-4 my-4 bg-white border overflow-x-auto">
378457
<div className="w-full flex justify-between">
379458
<h1 className="text-2xl font-bold">Leaderboard</h1>
380-
<button
381-
onClick={exportToCSV}
382-
className="px-4 py-1 text-md bg-blue-500 text-white rounded-lg hover:bg-blue-600"
383-
>
384-
Export
385-
</button>
459+
<div className="flex gap-2">
460+
<button
461+
onClick={exportForCert}
462+
className="px-4 py-1 text-md bg-green-500 text-white rounded-lg hover:bg-green-600"
463+
>
464+
Export for Cert
465+
</button>
466+
<button
467+
onClick={exportToCSV}
468+
className="px-4 py-1 text-md bg-blue-500 text-white rounded-lg hover:bg-blue-600"
469+
>
470+
Export
471+
</button>
472+
</div>
386473
</div>
387474
<table className="table-auto w-full mt-4">
388475
<thead>

app/admin/event/individual/page.js

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,112 @@ export default function EventLeaderboardIndiPage() {
131131
}
132132
}, [router, searchParams]);
133133

134+
const exportForCert = () => {
135+
if (!filteredParticipants || !eventMetadata) return;
136+
137+
// Filter top 5 participants based on overallTotal (descending)
138+
// Assuming filteredParticipants are already sorted by overallTotal,
139+
// otherwise we would need to sort first:
140+
// const sorted = [...filteredParticipants].sort((a, b) => (b.overallTotal || 0) - (a.overallTotal || 0));
141+
const topParticipants = filteredParticipants.slice(0, 5);
142+
143+
if (topParticipants.length === 0) return;
144+
145+
const flatList = topParticipants.map((p, index) => {
146+
// Determine effective student name and related details if substituted
147+
const isSubstituted = p.substitute && p.substitute[eventMetadata.name];
148+
const studentName = isSubstituted
149+
? p.substitute[eventMetadata.name].newStudentName
150+
: p.studentFullName;
151+
152+
const studentGender = isSubstituted
153+
? p.substitute[eventMetadata.name].newStudentGender
154+
: p.gender;
155+
156+
const studentDOB = isSubstituted
157+
? p.substitute[eventMetadata.name].newStudentDOB
158+
: p.dateOfBirth;
159+
160+
const studentGroup = isSubstituted
161+
? p.substitute[eventMetadata.name].newStudentGroup
162+
: p.studentGroup;
163+
164+
return {
165+
eventName: eventName,
166+
Rank: index + 1,
167+
...p,
168+
// Override with substituted values where applicable for the certificate
169+
studentFullName: studentName,
170+
gender: studentGender,
171+
dateOfBirth: studentDOB,
172+
studentGroup: studentGroup,
173+
// Format the total score
174+
OverallTotal: parseFloat(p.overallTotal ?? 0).toFixed(2),
175+
};
176+
});
177+
178+
const allKeys = new Set();
179+
flatList.forEach((item) => {
180+
Object.keys(item).forEach((key) => {
181+
const val = item[key];
182+
// Skip complex objects/arrays except for specific ones if needed.
183+
// We generally want scalar values.
184+
if (
185+
val !== null &&
186+
typeof val === 'object' &&
187+
!Array.isArray(val) &&
188+
key !== 'Rank' // Rank is scalar but good to be explicit
189+
) {
190+
return;
191+
}
192+
allKeys.add(key);
193+
});
194+
});
195+
196+
const headers = Array.from(allKeys).sort();
197+
// Prioritize common certificate fields
198+
const prioritized = [
199+
'eventName',
200+
'Rank',
201+
'studentFullName',
202+
'studentId',
203+
'district',
204+
'samithiName',
205+
'OverallTotal',
206+
'studentGroup',
207+
];
208+
const sortedHeaders = [
209+
...prioritized.filter((h) => headers.includes(h)),
210+
...headers.filter((h) => !prioritized.includes(h)),
211+
];
212+
213+
const escapeCSV = (value) => {
214+
if (value == null) return '';
215+
if (Array.isArray(value)) return `"${value.join('; ')}"`;
216+
const str = String(value);
217+
if (str.includes(',') || str.includes('\n') || str.includes('"')) {
218+
return `"${str.replace(/"/g, '""')}"`;
219+
}
220+
return str;
221+
};
222+
223+
const csv = [
224+
sortedHeaders.map(escapeCSV).join(','),
225+
...flatList.map((row) =>
226+
sortedHeaders.map((header) => escapeCSV(row[header])).join(','),
227+
),
228+
].join('\r\n');
229+
230+
const BOM = '\uFEFF';
231+
const blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8;' });
232+
const url = URL.createObjectURL(blob);
233+
const link = document.createElement('a');
234+
link.href = url;
235+
link.download = `${eventName}_top5_cert_export.csv`;
236+
link.click();
237+
URL.revokeObjectURL(url);
238+
};
239+
134240
const exportToCSV = () => {
135241
if (!filteredParticipants || !eventMetadata || orderedJudges.length === 0)
136242
return;
@@ -345,12 +451,20 @@ export default function EventLeaderboardIndiPage() {
345451
<div className="rounded-2xl p-4 my-4 bg-white border overflow-x-auto">
346452
<div className="w-full flex justify-between">
347453
<h1 className="text-2xl font-bold">Leaderboard</h1>
348-
<button
349-
onClick={exportToCSV}
350-
className="px-4 py-1 text-md bg-blue-500 text-white rounded-lg hover:bg-blue-600"
351-
>
352-
Export
353-
</button>
454+
<div className="flex gap-2">
455+
<button
456+
onClick={exportForCert}
457+
className="px-4 py-1 text-md bg-green-500 text-white rounded-lg hover:bg-green-600"
458+
>
459+
Export for Cert
460+
</button>
461+
<button
462+
onClick={exportToCSV}
463+
className="px-4 py-1 text-md bg-blue-500 text-white rounded-lg hover:bg-blue-600"
464+
>
465+
Export
466+
</button>
467+
</div>
354468
</div>
355469
<table className="table-auto w-full mt-4">
356470
<thead>

0 commit comments

Comments
 (0)