Skip to content

Commit 0baab92

Browse files
Add export schedule as CSV, style my matches modal
1 parent 83b438a commit 0baab92

File tree

3 files changed

+137
-9
lines changed

3 files changed

+137
-9
lines changed

components/ViewMatchesModal.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { report } from "process";
66
import Checkbox from "./forms/Checkboxes";
77
import { CheckmarkIcon } from "react-hot-toast";
88
import { useState } from "react";
9+
import { toDict } from "@/lib/client/ClientUtils";
910

1011
type MatchData = {
1112
number: number;
@@ -17,7 +18,10 @@ type MatchData = {
1718

1819
function ViewMatchCard(props: MatchData) {
1920
return (
20-
<Card title={"Match " + props.number}>
21+
<Card
22+
title={"Match " + props.number}
23+
className="w-full"
24+
>
2125
{props.message}
2226
<a
2327
href={props.url}
@@ -35,18 +39,14 @@ export default function ViewMatchesModal(props: {
3539
user: User;
3640
matchPathway: string;
3741
}) {
38-
const reportsById: { [id: string]: Report } = {};
42+
const reportsById = toDict(props.reports);
3943
const myMatches: MatchData[] = [];
4044
const [showSubmittedReports, setShowSubmittedReports] = useState(false);
4145

4246
async function toggleShowSubmittedReports() {
4347
setShowSubmittedReports(!showSubmittedReports);
4448
}
4549

46-
for (const report of props.reports) {
47-
reportsById[report._id?.toString()!] = report;
48-
}
49-
5050
for (const match of props.matches) {
5151
if (match.subjectiveScouter == props.user._id?.toString()) {
5252
myMatches.push({

components/competition/InsightsAndSettingsCard.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export default function InsightsAndSettingsCard(props: {
6464
const exportAsCsv = async () => {
6565
setExportPending(true);
6666

67-
const res = await api.exportCompAsCsv(comp?._id!).catch((e) => {
67+
const res = await api.exportCompDataAsCsv(comp?._id!).catch((e) => {
6868
console.error(e);
6969
return { csv: undefined };
7070
});
@@ -82,6 +82,31 @@ export default function InsightsAndSettingsCard(props: {
8282
setExportPending(false);
8383
};
8484

85+
async function exportScheduleAsCsv() {
86+
setExportPending(true);
87+
88+
const res = await api.exportCompScheduleAsCsv(comp?._id!).catch((e) => {
89+
console.error(e);
90+
return { csv: undefined };
91+
});
92+
93+
if (!res) {
94+
console.error("failed to export");
95+
}
96+
97+
if (res.csv) {
98+
download(
99+
`${comp?.name ?? "Competition"}Schedule.csv`,
100+
res.csv,
101+
"text/csv",
102+
);
103+
} else {
104+
console.error("No CSV data returned from server");
105+
}
106+
107+
setExportPending(false);
108+
}
109+
85110
const createMatch = async () => {
86111
try {
87112
await api.createMatch(
@@ -255,6 +280,18 @@ export default function InsightsAndSettingsCard(props: {
255280
"Export Scouting Data as CSV"
256281
)}
257282
</button>
283+
<button
284+
className={`btn ${
285+
exportPending ? "btn-disabled" : "btn-primary"
286+
} `}
287+
onClick={exportScheduleAsCsv}
288+
>
289+
{exportPending ? (
290+
<div className="loading loading-bars loading-sm"></div>
291+
) : (
292+
"Export Scouting Schedule as CSV"
293+
)}
294+
</button>
258295
<div className="flex flex-row items-center justify-between w-full">
259296
<label className="label ml-4">Show Submitted Matches</label>
260297
<input

lib/api/ClientApi.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import {
2121
LeaderboardTeam,
2222
LinkedList,
2323
} from "@/lib/Types";
24-
import { NotLinkedToTba, removeDuplicates } from "../client/ClientUtils";
24+
import {
25+
NotLinkedToTba,
26+
removeDuplicates,
27+
toDict,
28+
} from "../client/ClientUtils";
2529
import {
2630
addXp,
2731
deleteComp,
@@ -1120,7 +1124,7 @@ export default class ClientApi extends NextApiTemplate<ApiDependencies> {
11201124
},
11211125
});
11221126

1123-
exportCompAsCsv = createNextRoute<
1127+
exportCompDataAsCsv = createNextRoute<
11241128
[string],
11251129
{ csv: string },
11261130
ApiDependencies,
@@ -1180,6 +1184,93 @@ export default class ClientApi extends NextApiTemplate<ApiDependencies> {
11801184
},
11811185
});
11821186

1187+
exportCompScheduleAsCsv = createNextRoute<
1188+
[string],
1189+
{ csv: string },
1190+
ApiDependencies,
1191+
{ team: Team; comp: Competition }
1192+
>({
1193+
isAuthorized: (req, res, deps, [compId]) =>
1194+
AccessLevels.IfCompOwner(req, res, deps, compId),
1195+
handler: async (
1196+
req,
1197+
res,
1198+
{ db: dbPromise, userPromise },
1199+
{ team, comp },
1200+
[compId],
1201+
) => {
1202+
const db = await dbPromise;
1203+
1204+
const matches = await db.findObjects(CollectionId.Matches, {
1205+
_id: { $in: comp.matches.map((matchId) => new ObjectId(matchId)) },
1206+
});
1207+
const reports = await db.findObjects(CollectionId.Reports, {
1208+
match: { $in: matches.map((match) => match?._id?.toString()) },
1209+
});
1210+
1211+
if (reports.length == 0) {
1212+
return res
1213+
.status(200)
1214+
.send({ error: "No reports found for competition" });
1215+
}
1216+
1217+
const users = await db.findObjects(CollectionId.Users, {
1218+
_id: {
1219+
$in: reports
1220+
.map((r) => r.user)
1221+
.concat(matches.map((m) => m.subjectiveScouter))
1222+
.flat()
1223+
.map((id) => new ObjectId(id)),
1224+
},
1225+
});
1226+
1227+
const reportsById = toDict(reports);
1228+
const usersById = toDict(users);
1229+
1230+
interface Row {
1231+
matchNumber: string;
1232+
quantScouters: string[];
1233+
subjectiveScouter: string;
1234+
}
1235+
1236+
const rows: Row[] = [
1237+
// Headers
1238+
{
1239+
matchNumber: "Match #",
1240+
quantScouters: matches[0].reports.map(
1241+
(_, index) => `Scouter ${index + 1}`,
1242+
),
1243+
subjectiveScouter: "Subjective Scouter",
1244+
},
1245+
];
1246+
1247+
for (const match of matches) {
1248+
rows.push({
1249+
matchNumber: match.number.toString(),
1250+
quantScouters: match.reports.map((id) =>
1251+
reportsById[id].user ? usersById[reportsById[id].user].name! : "",
1252+
),
1253+
subjectiveScouter: match.subjectiveScouter
1254+
? usersById[match.subjectiveScouter].name!
1255+
: "",
1256+
});
1257+
}
1258+
1259+
const headers = Object.values(rows[0]).flat();
1260+
1261+
let csv = "";
1262+
for (const row of rows) {
1263+
csv +=
1264+
Object.values(row)
1265+
.flat()
1266+
.map((str) => str.replace(",", ""))
1267+
.join(",") + "\n";
1268+
}
1269+
1270+
res.status(200).send({ csv });
1271+
},
1272+
});
1273+
11831274
teamCompRanking = createNextRoute<
11841275
[string, number],
11851276
{ place: number | string; max: number | string },

0 commit comments

Comments
 (0)