From 529ca553e9f1dad0663b0a12b8d3374f6d76b90f Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Sun, 5 Apr 2026 19:01:39 -0700 Subject: [PATCH 1/8] update schema --- prisma/schema.prisma | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4f2e1a8d..7cc2c50c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -32,6 +32,7 @@ model TeamMatchData { teamNumber Int matchType MatchType scoutReports ScoutReport[] + formResponses FormResponse[] tournament Tournament @relation(fields: [tournamentKey], references: [key], onDelete: Cascade) @@index([tournamentKey, teamNumber]) @@ -99,6 +100,7 @@ model Scouter { scouterReliability Int @default(0) archived Boolean @default(false) scoutReports ScoutReport[] + formResponses FormResponse[] sourceTeam RegisteredTeam @relation(fields: [sourceTeamNumber], references: [number], onDelete: Cascade) team1Shifts ScouterScheduleShift[] @relation("Team1") team2Shifts ScouterScheduleShift[] @relation("Team2") @@ -137,6 +139,7 @@ model Team { number Int @id name String registeredTeam RegisteredTeam? + formResponses FormResponse[] } model RegisteredTeam { @@ -151,6 +154,7 @@ model RegisteredTeam { scouters Scouter[] scouterScheduleShifts ScouterScheduleShift[] slackChannels SlackWorkspace[] + customForms Form[] users User[] } @@ -232,6 +236,58 @@ model CachedAnalysis { teamDependencies Int[] @default([]) tournamentDependencies String[] @default([]) } +model Form { + uuid String @id @default(uuid()) + name String + teamNumber Int + team RegisteredTeam @relation(fields: [teamNumber], references: [number], onDelete: Cascade) + formParts FormPart[] + formResponses FormResponse[] +} + +model FormPart { + uuid String @id @default(uuid()) + formUuid String + form Form @relation(fields: [formUuid], references: [uuid], onDelete: Cascade) + type FormPartType + caption String + name String + options Json @default("{}") + order String + formResponseParts FormResponsePart[] +} + +model FormResponse { + uuid String @id @default(uuid()) + formUuid String + form Form @relation(fields: [formUuid], references: [uuid], onDelete: Cascade) + scouterUuid String + scouter Scouter @relation(fields: [scouterUuid], references: [uuid], onDelete: Cascade) + teamNumber Int + team Team @relation(fields: [teamNumber], references: [number], onDelete: Cascade) + teamMatchKey String? + teamMatchData TeamMatchData? @relation(fields: [teamMatchKey], references: [key], onDelete: Cascade) + formResponseParts FormResponsePart[] +} + +model FormResponsePart { + uuid String @id @default(uuid()) + formResponseUuid String + formResponse FormResponse @relation(fields: [formResponseUuid], references: [uuid], onDelete: Cascade) + formPartUuid String + formPart FormPart @relation(fields: [formPartUuid], references: [uuid], onDelete: Cascade) + response Json +} + +enum FormPartType { + RATING + SHORT_ANSWER + SINGLE_CHOICE + MULTIPLE_CHOICE + MULTIPLE_CHOICE_GRID + IMAGE +} + enum Position { LEFT_TRENCH From 95c2946e3e193e44398159bca60ea70ca4655e30 Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Sun, 5 Apr 2026 21:13:02 -0700 Subject: [PATCH 2/8] create skeleton and start working on manager endpoints --- prisma/schema.prisma | 30 ++++---- src/handler/manager/forms/createForm.ts | 59 +++++++++++++++ src/handler/manager/forms/deleteForm.ts | 43 +++++++++++ src/handler/manager/forms/getForms.ts | 28 +++++++ .../manager/forms/parts/deleteFormParts.ts | 0 .../manager/forms/parts/getFormParts.ts | 0 .../manager/forms/parts/reorderFormParts.ts | 0 .../manager/forms/parts/updateFormParts.ts | 0 .../manager/forms/responses/deleteResponse.ts | 0 .../manager/forms/responses/getResponse.ts | 45 ++++++++++++ .../manager/forms/responses/getResponses.ts | 49 +++++++++++++ .../manager/forms/responses/submitForm.ts | 73 +++++++++++++++++++ .../manager/forms/responses/uploadImage.ts | 0 src/handler/manager/forms/updateForm.ts | 0 14 files changed, 312 insertions(+), 15 deletions(-) create mode 100644 src/handler/manager/forms/createForm.ts create mode 100644 src/handler/manager/forms/deleteForm.ts create mode 100644 src/handler/manager/forms/getForms.ts create mode 100644 src/handler/manager/forms/parts/deleteFormParts.ts create mode 100644 src/handler/manager/forms/parts/getFormParts.ts create mode 100644 src/handler/manager/forms/parts/reorderFormParts.ts create mode 100644 src/handler/manager/forms/parts/updateFormParts.ts create mode 100644 src/handler/manager/forms/responses/deleteResponse.ts create mode 100644 src/handler/manager/forms/responses/getResponse.ts create mode 100644 src/handler/manager/forms/responses/getResponses.ts create mode 100644 src/handler/manager/forms/responses/submitForm.ts create mode 100644 src/handler/manager/forms/responses/uploadImage.ts create mode 100644 src/handler/manager/forms/updateForm.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7cc2c50c..d0342ca8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,14 +26,14 @@ model FeatureToggle { } model TeamMatchData { - key String @id + key String @id tournamentKey String - matchNumber Int @db.SmallInt + matchNumber Int @db.SmallInt teamNumber Int matchType MatchType scoutReports ScoutReport[] formResponses FormResponse[] - tournament Tournament @relation(fields: [tournamentKey], references: [key], onDelete: Cascade) + tournament Tournament @relation(fields: [tournamentKey], references: [key], onDelete: Cascade) @@index([tournamentKey, teamNumber]) } @@ -100,7 +100,7 @@ model Scouter { scouterReliability Int @default(0) archived Boolean @default(false) scoutReports ScoutReport[] - formResponses FormResponse[] + formResponses FormResponse[] sourceTeam RegisteredTeam @relation(fields: [sourceTeamNumber], references: [number], onDelete: Cascade) team1Shifts ScouterScheduleShift[] @relation("Team1") team2Shifts ScouterScheduleShift[] @relation("Team2") @@ -236,6 +236,7 @@ model CachedAnalysis { teamDependencies Int[] @default([]) tournamentDependencies String[] @default([]) } + model Form { uuid String @id @default(uuid()) name String @@ -253,20 +254,20 @@ model FormPart { caption String name String options Json @default("{}") - order String + order Int formResponseParts FormResponsePart[] } model FormResponse { - uuid String @id @default(uuid()) - formUuid String - form Form @relation(fields: [formUuid], references: [uuid], onDelete: Cascade) - scouterUuid String - scouter Scouter @relation(fields: [scouterUuid], references: [uuid], onDelete: Cascade) - teamNumber Int - team Team @relation(fields: [teamNumber], references: [number], onDelete: Cascade) - teamMatchKey String? - teamMatchData TeamMatchData? @relation(fields: [teamMatchKey], references: [key], onDelete: Cascade) + uuid String @id @default(uuid()) + formUuid String + form Form @relation(fields: [formUuid], references: [uuid], onDelete: Cascade) + scouterUuid String + scouter Scouter @relation(fields: [scouterUuid], references: [uuid], onDelete: Cascade) + teamNumber Int? + team Team? @relation(fields: [teamNumber], references: [number], onDelete: Cascade) + teamMatchKey String? + teamMatchData TeamMatchData? @relation(fields: [teamMatchKey], references: [key], onDelete: Cascade) formResponseParts FormResponsePart[] } @@ -288,7 +289,6 @@ enum FormPartType { IMAGE } - enum Position { LEFT_TRENCH LEFT_BUMP diff --git a/src/handler/manager/forms/createForm.ts b/src/handler/manager/forms/createForm.ts new file mode 100644 index 00000000..a6470cff --- /dev/null +++ b/src/handler/manager/forms/createForm.ts @@ -0,0 +1,59 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../lib/middleware/requireAuth"; +import { FormPartType, UserRole } from "@prisma/client"; +import prismaClient from "../../../prismaClient"; +import { Response } from "express"; + +const createFormParamsSchema = z.object({ + team: z.number(), + name: z.string(), + parts: z.array( + z.object({ + name: z.string(), + type: z.nativeEnum(FormPartType), + caption: z.string(), + options: z.record(z.string(), z.unknown()).optional(), + }), + ), +}); + +export const createForm = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + if (req.user.role !== UserRole.SCOUTING_LEAD) { + res.status(403).json({ error: "Forbidden" }); + return; + } + const params = createFormParamsSchema.parse(req.body); + + const [form, formParts] = await prismaClient.$transaction(async (tx) => { + const form = await tx.form.create({ + data: { name: params.name, teamNumber: params.team }, + }); + const formParts = await tx.formPart.createManyAndReturn({ + data: params.parts.map((part, index) => ({ + name: part.name, + formUuid: form.uuid, + type: part.type, + caption: part.caption ?? "", + options: part.options ?? {}, + order: index, + })), + }); + return [form, formParts]; + }); + + res.status(200).json({ form, formParts }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } + console.error("Error creating form:", error); + res + .status(500) + .json({ error: "An error occurred while creating the form." }); + } +}; diff --git a/src/handler/manager/forms/deleteForm.ts b/src/handler/manager/forms/deleteForm.ts new file mode 100644 index 00000000..e057da42 --- /dev/null +++ b/src/handler/manager/forms/deleteForm.ts @@ -0,0 +1,43 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../lib/middleware/requireAuth"; +import { UserRole, Prisma } from "@prisma/client"; +import prismaClient from "../../../prismaClient"; +import { Response } from "express"; + +const deleteFormParamsSchema = z.object({ + uuid: z.string(), +}); + +export const deleteForm = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + if (req.user.role !== UserRole.SCOUTING_LEAD) { + res.status(403).json({ error: "Forbidden" }); + return; + } + const params = deleteFormParamsSchema.parse(req.params); + + const form = await prismaClient.form.delete({ + where: { uuid: params.uuid }, + }); + + res.status(200).json({ form }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } else if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === "P2025") { + res.status(404).json({ error: "Form not found" }); + return; + } + } else { + console.error("Error deleting form:", error); + res + .status(500) + .json({ error: "An error occurred while deleting the form." }); + } + } +}; diff --git a/src/handler/manager/forms/getForms.ts b/src/handler/manager/forms/getForms.ts new file mode 100644 index 00000000..926b0438 --- /dev/null +++ b/src/handler/manager/forms/getForms.ts @@ -0,0 +1,28 @@ +import { AuthenticatedRequest } from "../../../lib/middleware/requireAuth"; +import prismaClient from "../../../prismaClient"; +import { Response } from "express"; + +export const getForms = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + const form = await prismaClient.form.findMany({ + where: { + teamNumber: req.user.teamNumber, + }, + include: { + formParts: { + orderBy: { order: "asc" }, + }, + }, + }); + + res.status(200).json({ form }); + } catch (error) { + console.error("Error getting forms:", error); + res + .status(500) + .json({ error: "An error occurred while getting the forms." }); + } +}; diff --git a/src/handler/manager/forms/parts/deleteFormParts.ts b/src/handler/manager/forms/parts/deleteFormParts.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/handler/manager/forms/parts/getFormParts.ts b/src/handler/manager/forms/parts/getFormParts.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/handler/manager/forms/parts/reorderFormParts.ts b/src/handler/manager/forms/parts/reorderFormParts.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/handler/manager/forms/parts/updateFormParts.ts b/src/handler/manager/forms/parts/updateFormParts.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/handler/manager/forms/responses/deleteResponse.ts b/src/handler/manager/forms/responses/deleteResponse.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/handler/manager/forms/responses/getResponse.ts b/src/handler/manager/forms/responses/getResponse.ts new file mode 100644 index 00000000..ec1643da --- /dev/null +++ b/src/handler/manager/forms/responses/getResponse.ts @@ -0,0 +1,45 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; +import prismaClient from "../../../../prismaClient"; +import { Response } from "express"; + +const getResponseParamsSchema = z.object({ + responseUuid: z.string(), +}); + +export const getFormResponse = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + const params = getResponseParamsSchema.parse(req.params); + + const formResponse = await prismaClient.formResponse.findUnique({ + where: { uuid: params.responseUuid }, + include: { + formResponseParts: { + orderBy: { formPartUuid: "asc" }, + }, + scouter: { + select: { name: true }, + }, + }, + }); + + if (!formResponse) { + res.status(404).json({ error: "Form response not found" }); + return; + } + + res.status(200).json({ formResponse }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } + console.error("Error getting form response:", error); + res + .status(500) + .json({ error: "An error occurred while getting the form response." }); + } +}; diff --git a/src/handler/manager/forms/responses/getResponses.ts b/src/handler/manager/forms/responses/getResponses.ts new file mode 100644 index 00000000..283c4a0b --- /dev/null +++ b/src/handler/manager/forms/responses/getResponses.ts @@ -0,0 +1,49 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; +import prismaClient from "../../../../prismaClient"; +import { Response } from "express"; + +const getResponsesParamsSchema = z.object({ + formUuid: z.string(), +}); + +export const getFormResponses = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + const params = getResponsesParamsSchema.parse(req.params); + + const form = await prismaClient.form.findUnique({ + where: { uuid: params.formUuid }, + include: { + formResponses: { + include: { + formResponseParts: { + orderBy: { formPartUuid: "asc" }, + }, + scouter: { + select: { name: true }, + }, + }, + }, + }, + }); + + if (!form) { + res.status(404).json({ error: "Form not found" }); + return; + } + + res.status(200).json({ form }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } + console.error("Error getting form responses:", error); + res + .status(500) + .json({ error: "An error occurred while getting the form responses." }); + } +}; diff --git a/src/handler/manager/forms/responses/submitForm.ts b/src/handler/manager/forms/responses/submitForm.ts new file mode 100644 index 00000000..806cfaa4 --- /dev/null +++ b/src/handler/manager/forms/responses/submitForm.ts @@ -0,0 +1,73 @@ +import z from "zod"; +import { Prisma } from "@prisma/client"; +import prismaClient from "../../../../prismaClient"; +import { Request, Response } from "express"; + +const submitFormParamsSchema = z.object({ + team: z.number().optional(), + scouterUuid: z.string(), + matchKey: z.string().optional(), + formUuid: z.string(), + parts: z.array( + z.object({ + formPartUuid: z.string(), + response: z.union([z.string(), z.number(), z.array(z.string())]), + }), + ), +}); + +export const submitForm = async ( + req: Request, + res: Response, +): Promise => { + try { + const params = submitFormParamsSchema.parse(req.body); + + if (!params.matchKey && !params.team) { + res.status(400).json({ error: "Either matchKey or team is required" }); + return; + } + + const formResponseCreateData: Prisma.FormResponseCreateInput = + params.matchKey + ? { + teamMatchData: { connect: { key: params.matchKey } }, + scouter: { connect: { uuid: params.scouterUuid } }, + form: { connect: { uuid: params.formUuid } }, + } + : { + team: { connect: { number: params.team } }, + scouter: { connect: { uuid: params.scouterUuid } }, + form: { connect: { uuid: params.formUuid } }, + }; + + const [formResponse, formResponseParts] = await prismaClient.$transaction( + async (tx) => { + const formResponse = await tx.formResponse.create({ + data: formResponseCreateData, + }); + const formResponseParts = await tx.formResponsePart.createManyAndReturn( + { + data: params.parts.map((part) => ({ + formResponseUuid: formResponse.uuid, + formPartUuid: part.formPartUuid, + response: part.response, + })), + }, + ); + return [formResponse, formResponseParts]; + }, + ); + + res.status(200).json({ formResponse, formResponseParts }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } + console.error("Error submitting form:", error); + res + .status(500) + .json({ error: "An error occurred while submitting the form." }); + } +}; diff --git a/src/handler/manager/forms/responses/uploadImage.ts b/src/handler/manager/forms/responses/uploadImage.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/handler/manager/forms/updateForm.ts b/src/handler/manager/forms/updateForm.ts new file mode 100644 index 00000000..e69de29b From 123a7ccfa257e3875bbc2b0cf5bbb8d8978857f8 Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Sun, 5 Apr 2026 21:42:18 -0700 Subject: [PATCH 3/8] coding version of yap sesh --- .../manager/forms/parts/createFormPart.ts | 72 ++++ .../manager/forms/parts/deleteFormPart.ts | 43 +++ .../manager/forms/parts/deleteFormParts.ts | 0 .../manager/forms/parts/getFormParts.ts | 42 +++ .../manager/forms/parts/reorderFormParts.ts | 72 ++++ .../manager/forms/parts/updateFormPart.ts | 51 +++ .../manager/forms/parts/updateFormParts.ts | 0 .../manager/forms/responses/deleteResponse.ts | 43 +++ src/handler/manager/forms/updateForm.ts | 0 src/handler/manager/forms/updateFormName.ts | 45 +++ src/routes/manager/forms.routes.ts | 338 ++++++++++++++++++ 11 files changed, 706 insertions(+) create mode 100644 src/handler/manager/forms/parts/createFormPart.ts create mode 100644 src/handler/manager/forms/parts/deleteFormPart.ts delete mode 100644 src/handler/manager/forms/parts/deleteFormParts.ts create mode 100644 src/handler/manager/forms/parts/updateFormPart.ts delete mode 100644 src/handler/manager/forms/parts/updateFormParts.ts delete mode 100644 src/handler/manager/forms/updateForm.ts create mode 100644 src/handler/manager/forms/updateFormName.ts create mode 100644 src/routes/manager/forms.routes.ts diff --git a/src/handler/manager/forms/parts/createFormPart.ts b/src/handler/manager/forms/parts/createFormPart.ts new file mode 100644 index 00000000..1e5b2bc8 --- /dev/null +++ b/src/handler/manager/forms/parts/createFormPart.ts @@ -0,0 +1,72 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; +import prismaClient from "../../../../prismaClient"; +import { Response } from "express"; +import { FormPartType } from "@prisma/client"; + +const createFormPartParamsSchema = z.object({ + formUuid: z.string(), + type: z.nativeEnum(FormPartType), + caption: z.string(), + name: z.string(), + options: z.record(z.string(), z.unknown()), + order: z.number(), +}); + +export const createFormPart = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + const params = createFormPartParamsSchema.parse({ + formUuid: req.params.formUuid, + ...req.body, + }); + + const formPart = await prismaClient.$transaction(async (tx) => { + const formPart = await tx.formPart.create({ + data: { + formUuid: params.formUuid, + type: params.type, + caption: params.caption, + name: params.name, + options: params.options, + order: params.order, + }, + }); + const formParts = await tx.formPart.findMany({ + where: { formUuid: params.formUuid }, + orderBy: { order: "asc" }, + }); + const reordered = formParts.map((p) => ({ + ...p, + order: + p.uuid === formPart.uuid + ? params.order + : p.order >= params.order + ? p.order + 1 + : p.order, + })); + await Promise.all( + reordered.map((p) => + tx.formPart.update({ + where: { uuid: p.uuid }, + data: { order: p.order }, + }), + ), + ); + return formPart; + }); + + res.status(201).json({ formPart }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } + console.error("Error creating form part:", error); + res + .status(500) + .json({ error: "An error occurred while creating the form part." }); + } +}; diff --git a/src/handler/manager/forms/parts/deleteFormPart.ts b/src/handler/manager/forms/parts/deleteFormPart.ts new file mode 100644 index 00000000..9b94f10f --- /dev/null +++ b/src/handler/manager/forms/parts/deleteFormPart.ts @@ -0,0 +1,43 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; +import { UserRole, Prisma } from "@prisma/client"; +import prismaClient from "../../../../prismaClient"; +import { Response } from "express"; + +const deleteFormPartParamsSchema = z.object({ + uuid: z.string(), +}); + +export const deleteFormPart = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + if (req.user.role !== UserRole.SCOUTING_LEAD) { + res.status(403).json({ error: "Forbidden" }); + return; + } + const params = deleteFormPartParamsSchema.parse(req.params); + + const form = await prismaClient.formPart.delete({ + where: { uuid: params.uuid }, + }); + + res.status(200).json({ form }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } else if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === "P2025") { + res.status(404).json({ error: "Form part not found" }); + return; + } + } else { + console.error("Error deleting form part:", error); + res + .status(500) + .json({ error: "An error occurred while deleting the form part." }); + } + } +}; diff --git a/src/handler/manager/forms/parts/deleteFormParts.ts b/src/handler/manager/forms/parts/deleteFormParts.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/handler/manager/forms/parts/getFormParts.ts b/src/handler/manager/forms/parts/getFormParts.ts index e69de29b..db9fd878 100644 --- a/src/handler/manager/forms/parts/getFormParts.ts +++ b/src/handler/manager/forms/parts/getFormParts.ts @@ -0,0 +1,42 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; +import prismaClient from "../../../../prismaClient"; +import { Response } from "express"; + +const getFormPartParamsSchema = z.object({ + formPartUuid: z.string(), +}); + +export const getFormPart = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + const params = getFormPartParamsSchema.parse(req.params); + + const formPart = await prismaClient.formPart.findUnique({ + where: { uuid: params.formPartUuid }, + include: { + formResponseParts: { + orderBy: { formResponseUuid: "asc" }, + }, + }, + }); + + if (!formPart) { + res.status(404).json({ error: "Form part not found" }); + return; + } + + res.status(200).json({ formPart }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } + console.error("Error getting form parts:", error); + res + .status(500) + .json({ error: "An error occurred while getting the form parts." }); + } +}; diff --git a/src/handler/manager/forms/parts/reorderFormParts.ts b/src/handler/manager/forms/parts/reorderFormParts.ts index e69de29b..f575d474 100644 --- a/src/handler/manager/forms/parts/reorderFormParts.ts +++ b/src/handler/manager/forms/parts/reorderFormParts.ts @@ -0,0 +1,72 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; +import prismaClient from "../../../../prismaClient"; +import { Response } from "express"; +import { Prisma } from "@prisma/client"; + +const updateFormPartParamsSchema = z.object({ + formUuid: z.string(), + uuid: z.string(), + order: z.number(), +}); + +export const reorderFormParts = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + const params = updateFormPartParamsSchema.parse({ + uuid: req.params.uuid, + formUuid: req.params.formUuid, + ...req.body, + }); + + const formParts = await prismaClient.formPart.findMany({ + where: { formUuid: params.formUuid }, + orderBy: { order: "asc" }, + }); + + const target = formParts.find((p) => p.uuid === params.uuid); + + if (!target) { + res.status(404).json({ error: "Form part not found" }); + return; + } + + const reordered = formParts.map((p) => ({ + ...p, + order: + p.uuid === params.uuid + ? params.order + : p.order >= params.order + ? p.order + 1 + : p.order, + })); + + const updatedFormParts = await prismaClient.$transaction( + reordered.map((formPart) => + prismaClient.formPart.update({ + where: { uuid: formPart.uuid }, + data: { order: formPart.order }, + }), + ), + ); + const formPart = updatedFormParts.find((part) => part.uuid === params.uuid); + + res.status(200).json({ formPart }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } else if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === "P2025") { + res.status(404).json({ error: "Form part not found" }); + return; + } + } + console.error("Error reordering form part:", error); + res + .status(500) + .json({ error: "An error occurred while reordering the form part." }); + } +}; diff --git a/src/handler/manager/forms/parts/updateFormPart.ts b/src/handler/manager/forms/parts/updateFormPart.ts new file mode 100644 index 00000000..48128087 --- /dev/null +++ b/src/handler/manager/forms/parts/updateFormPart.ts @@ -0,0 +1,51 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; +import prismaClient from "../../../../prismaClient"; +import { Response } from "express"; +import { FormPartType, Prisma } from "@prisma/client"; + +const updateFormPartParamsSchema = z.object({ + uuid: z.string(), + type: z.nativeEnum(FormPartType), + caption: z.string(), + name: z.string(), + options: z.record(z.string(), z.unknown()), +}); + +export const updateFormPart = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + const params = updateFormPartParamsSchema.parse({ + uuid: req.params.uuid, + ...req.body, + }); + + const formPart = await prismaClient.formPart.update({ + where: { uuid: params.uuid }, + data: { + type: params.type, + caption: params.caption, + name: params.name, + options: params.options, + }, + }); + + res.status(200).json({ formPart }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } else if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === "P2025") { + res.status(404).json({ error: "Form part not found" }); + return; + } + } + console.error("Error updating form part:", error); + res + .status(500) + .json({ error: "An error occurred while updating the form part." }); + } +}; diff --git a/src/handler/manager/forms/parts/updateFormParts.ts b/src/handler/manager/forms/parts/updateFormParts.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/handler/manager/forms/responses/deleteResponse.ts b/src/handler/manager/forms/responses/deleteResponse.ts index e69de29b..972c06ac 100644 --- a/src/handler/manager/forms/responses/deleteResponse.ts +++ b/src/handler/manager/forms/responses/deleteResponse.ts @@ -0,0 +1,43 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; +import { UserRole, Prisma } from "@prisma/client"; +import prismaClient from "../../../../prismaClient"; +import { Response } from "express"; + +const deleteFormResponseParamsSchema = z.object({ + uuid: z.string(), +}); + +export const deleteFormResponse = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + if (req.user.role !== UserRole.SCOUTING_LEAD) { + res.status(403).json({ error: "Forbidden" }); + return; + } + const params = deleteFormResponseParamsSchema.parse(req.params); + + const form = await prismaClient.formResponse.delete({ + where: { uuid: params.uuid }, + }); + + res.status(200).json({ form }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } else if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === "P2025") { + res.status(404).json({ error: "Form response not found" }); + return; + } + } else { + console.error("Error deleting form response:", error); + res + .status(500) + .json({ error: "An error occurred while deleting the form response." }); + } + } +}; diff --git a/src/handler/manager/forms/updateForm.ts b/src/handler/manager/forms/updateForm.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/handler/manager/forms/updateFormName.ts b/src/handler/manager/forms/updateFormName.ts new file mode 100644 index 00000000..5853f651 --- /dev/null +++ b/src/handler/manager/forms/updateFormName.ts @@ -0,0 +1,45 @@ +import z from "zod"; +import { AuthenticatedRequest } from "../../../lib/middleware/requireAuth"; +import prismaClient from "../../../prismaClient"; +import { Response } from "express"; +import { Prisma } from "@prisma/client"; + +const updateFormNameParamsSchema = z.object({ + formUuid: z.string(), + name: z.string(), +}); + +export const updateFormName = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + const params = updateFormNameParamsSchema.safeParse({ + formUuid: req.params.formUuid, + name: req.body.name, + }); + + if (!params.success) { + res.status(400).json({ error: "Invalid input", details: params.error }); + return; + } + + const form = await prismaClient.form.update({ + where: { uuid: params.data.formUuid }, + data: { name: params.data.name }, + }); + + res.status(200).json({ form }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === "P2025") { + res.status(404).json({ error: "Form not found" }); + return; + } + } + console.error("Error updating form name:", error); + res + .status(500) + .json({ error: "An error occurred while updating the form name." }); + } +}; diff --git a/src/routes/manager/forms.routes.ts b/src/routes/manager/forms.routes.ts new file mode 100644 index 00000000..4780d210 --- /dev/null +++ b/src/routes/manager/forms.routes.ts @@ -0,0 +1,338 @@ +import { Router } from "express"; +import { requireAuth } from "../../lib/middleware/requireAuth.js"; +import { requireVerifiedTeam } from "../../lib/middleware/requireVerifiedTeam.js"; +import { registry } from "../../lib/openapi.js"; +import { z } from "zod"; +import { createForm } from "../../handler/manager/forms/createForm.js"; +import { deleteForm } from "../../handler/manager/forms/deleteForm.js"; +import { getForms } from "../../handler/manager/forms/getForms.js"; +import { updateFormName } from "../../handler/manager/forms/updateFormName.js"; +import { createFormPart } from "../../handler/manager/forms/parts/createFormPart.js"; +import { deleteFormPart } from "../../handler/manager/forms/parts/deleteFormPart.js"; +import { getFormPart } from "../../handler/manager/forms/parts/getFormParts.js"; +import { updateFormPart } from "../../handler/manager/forms/parts/updateFormPart.js"; +import { reorderFormParts } from "../../handler/manager/forms/parts/reorderFormParts.js"; +import { deleteFormResponse } from "../../handler/manager/forms/responses/deleteResponse.js"; +import { getFormResponse } from "../../handler/manager/forms/responses/getResponse.js"; +import { getFormResponses } from "../../handler/manager/forms/responses/getResponses.js"; +import { submitForm } from "../../handler/manager/forms/responses/submitForm.js"; + +// Forms +registry.registerPath({ + method: "post", + path: "/v1/manager/forms", + tags: ["Manager - Forms"], + summary: "Create a new form", + request: { + body: { + content: { + "application/json": { + schema: z.object({ + team: z.number(), + name: z.string(), + parts: z.array( + z.object({ + name: z.string(), + type: z.string(), + caption: z.string(), + options: z.record(z.string(), z.unknown()).optional(), + }), + ), + }), + }, + }, + }, + }, + responses: { + 200: { description: "Created" }, + 400: { description: "Invalid request parameters" }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +registry.registerPath({ + method: "delete", + path: "/v1/manager/forms/{formUuid}", + tags: ["Manager - Forms"], + summary: "Delete a form", + request: { params: z.object({ formUuid: z.string() }) }, + responses: { + 200: { description: "Deleted" }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, + 404: { description: "Form not found" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +registry.registerPath({ + method: "get", + path: "/v1/manager/forms", + tags: ["Manager - Forms"], + summary: "List forms for the authenticated team", + responses: { + 200: { description: "Forms" }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +registry.registerPath({ + method: "put", + path: "/v1/manager/forms/{formUuid}", + tags: ["Manager - Forms"], + summary: "Update form name", + request: { + params: z.object({ formUuid: z.string() }), + body: { + content: { + "application/json": { schema: z.object({ name: z.string() }) }, + }, + }, + }, + responses: { + 200: { description: "Updated" }, + 400: { description: "Invalid request parameters" }, + 401: { description: "Unauthorized" }, + 404: { description: "Form not found" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Form parts +registry.registerPath({ + method: "post", + path: "/v1/manager/forms/{formUuid}/parts", + tags: ["Manager - Form Parts"], + summary: "Add a part to a form", + request: { + params: z.object({ formUuid: z.string() }), + body: { + content: { + "application/json": { + schema: z.object({ + name: z.string(), + type: z.string(), + caption: z.string(), + options: z.record(z.string(), z.unknown()), + order: z.number(), + }), + }, + }, + }, + }, + responses: { + 201: { description: "Created" }, + 400: { description: "Invalid request parameters" }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +registry.registerPath({ + method: "delete", + path: "/v1/manager/forms/{formUuid}/parts/{partUuid}", + tags: ["Manager - Form Parts"], + summary: "Delete a form part", + request: { + params: z.object({ formUuid: z.string(), partUuid: z.string() }), + }, + responses: { + 200: { description: "Deleted" }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, + 404: { description: "Form part not found" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +registry.registerPath({ + method: "get", + path: "/v1/manager/forms/{formUuid}/parts/{partUuid}", + tags: ["Manager - Form Parts"], + summary: "Get a form part with its responses", + request: { + params: z.object({ formUuid: z.string(), partUuid: z.string() }), + }, + responses: { + 200: { description: "Form part" }, + 401: { description: "Unauthorized" }, + 404: { description: "Form part not found" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +registry.registerPath({ + method: "put", + path: "/v1/manager/forms/{formUuid}/parts/{partUuid}", + tags: ["Manager - Form Parts"], + summary: "Update a form part", + request: { + params: z.object({ formUuid: z.string(), partUuid: z.string() }), + body: { + content: { + "application/json": { + schema: z.object({ + name: z.string(), + type: z.string(), + caption: z.string(), + options: z.record(z.string(), z.unknown()), + }), + }, + }, + }, + }, + responses: { + 200: { description: "Updated" }, + 400: { description: "Invalid request parameters" }, + 401: { description: "Unauthorized" }, + 404: { description: "Form part not found" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +registry.registerPath({ + method: "put", + path: "/v1/manager/forms/{formUuid}/parts/{partUuid}/reorder", + tags: ["Manager - Form Parts"], + summary: "Reorder a form part", + request: { + params: z.object({ formUuid: z.string(), partUuid: z.string() }), + body: { + content: { + "application/json": { schema: z.object({ order: z.number() }) }, + }, + }, + }, + responses: { + 200: { description: "Reordered" }, + 400: { description: "Invalid request parameters" }, + 401: { description: "Unauthorized" }, + 404: { description: "Form part not found" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +// Form responses +registry.registerPath({ + method: "post", + path: "/v1/manager/forms/{formUuid}/responses", + tags: ["Manager - Form Responses"], + summary: "Submit a form response", + request: { + params: z.object({ formUuid: z.string() }), + body: { + content: { + "application/json": { + schema: z.object({ + team: z.number().optional(), + scouterUuid: z.string(), + matchKey: z.string().optional(), + parts: z.array( + z.object({ + formPartUuid: z.string(), + response: z.union([ + z.string(), + z.number(), + z.array(z.string()), + ]), + }), + ), + }), + }, + }, + }, + }, + responses: { + 200: { description: "Submitted" }, + 400: { description: "Invalid request parameters" }, + 500: { description: "Server error" }, + }, +}); + +registry.registerPath({ + method: "get", + path: "/v1/manager/forms/{formUuid}/responses", + tags: ["Manager - Form Responses"], + summary: "Get all responses for a form", + request: { params: z.object({ formUuid: z.string() }) }, + responses: { + 200: { description: "Form responses" }, + 401: { description: "Unauthorized" }, + 404: { description: "Form not found" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +registry.registerPath({ + method: "get", + path: "/v1/manager/forms/{formUuid}/responses/{responseUuid}", + tags: ["Manager - Form Responses"], + summary: "Get a single form response", + request: { + params: z.object({ formUuid: z.string(), responseUuid: z.string() }), + }, + responses: { + 200: { description: "Form response" }, + 401: { description: "Unauthorized" }, + 404: { description: "Form response not found" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +registry.registerPath({ + method: "delete", + path: "/v1/manager/forms/{formUuid}/responses/{responseUuid}", + tags: ["Manager - Form Responses"], + summary: "Delete a form response", + request: { + params: z.object({ formUuid: z.string(), responseUuid: z.string() }), + }, + responses: { + 200: { description: "Deleted" }, + 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, + 404: { description: "Form response not found" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +const router = Router(); + +router.post("/:formUuid/responses", submitForm); + +router.use(requireAuth, requireVerifiedTeam); + +// Forms +router.post("/", createForm); +router.delete("/:formUuid", deleteForm); +router.get("/", getForms); +router.put("/:formUuid", updateFormName); + +// Form parts +router.post("/:formUuid/parts", createFormPart); +router.delete("/:formUuid/parts/:uuid", deleteFormPart); +router.get("/:formUuid/parts/:uuid", getFormPart); +router.put("/:formUuid/parts/:uuid", updateFormPart); +router.put("/:formUuid/parts/:uuid/reorder", reorderFormParts); + +// Form responses — submitForm is unauthenticated +router.use(requireAuth, requireVerifiedTeam); +router.get("/:formUuid/responses", getFormResponses); +router.get("/:formUuid/responses/:responseUuid", getFormResponse); +router.delete("/:formUuid/responses/:responseUuid", deleteFormResponse); + +export default router; From f3e1f4eb3a9973eb7e6e661a3aca473d4fa5025c Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Sun, 5 Apr 2026 21:50:21 -0700 Subject: [PATCH 4/8] opencode locked in? --- src/handler/manager/forms/deleteForm.ts | 2 +- src/handler/manager/forms/getForms.ts | 4 ++-- src/handler/manager/forms/parts/createFormPart.ts | 7 ++++++- src/handler/manager/forms/parts/deleteFormPart.ts | 9 ++++++--- src/handler/manager/forms/parts/getFormParts.ts | 4 ++-- src/handler/manager/forms/parts/reorderFormParts.ts | 7 ++++++- src/handler/manager/forms/parts/updateFormPart.ts | 7 ++++++- src/handler/manager/forms/responses/submitForm.ts | 5 ++++- src/routes/manager/forms.routes.ts | 1 - src/routes/manager/manager.routes.ts | 2 ++ 10 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/handler/manager/forms/deleteForm.ts b/src/handler/manager/forms/deleteForm.ts index e057da42..10a17b06 100644 --- a/src/handler/manager/forms/deleteForm.ts +++ b/src/handler/manager/forms/deleteForm.ts @@ -20,7 +20,7 @@ export const deleteForm = async ( const params = deleteFormParamsSchema.parse(req.params); const form = await prismaClient.form.delete({ - where: { uuid: params.uuid }, + where: { uuid: params.uuid, teamNumber: req.user.teamNumber }, }); res.status(200).json({ form }); diff --git a/src/handler/manager/forms/getForms.ts b/src/handler/manager/forms/getForms.ts index 926b0438..c8193064 100644 --- a/src/handler/manager/forms/getForms.ts +++ b/src/handler/manager/forms/getForms.ts @@ -7,7 +7,7 @@ export const getForms = async ( res: Response, ): Promise => { try { - const form = await prismaClient.form.findMany({ + const forms = await prismaClient.form.findMany({ where: { teamNumber: req.user.teamNumber, }, @@ -18,7 +18,7 @@ export const getForms = async ( }, }); - res.status(200).json({ form }); + res.status(200).json({ forms }); } catch (error) { console.error("Error getting forms:", error); res diff --git a/src/handler/manager/forms/parts/createFormPart.ts b/src/handler/manager/forms/parts/createFormPart.ts index 1e5b2bc8..b94ed1a1 100644 --- a/src/handler/manager/forms/parts/createFormPart.ts +++ b/src/handler/manager/forms/parts/createFormPart.ts @@ -2,7 +2,7 @@ import z from "zod"; import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; import prismaClient from "../../../../prismaClient"; import { Response } from "express"; -import { FormPartType } from "@prisma/client"; +import { FormPartType, UserRole } from "@prisma/client"; const createFormPartParamsSchema = z.object({ formUuid: z.string(), @@ -18,6 +18,11 @@ export const createFormPart = async ( res: Response, ): Promise => { try { + if (req.user.role !== UserRole.SCOUTING_LEAD) { + res.status(403).json({ error: "Forbidden" }); + return; + } + const params = createFormPartParamsSchema.parse({ formUuid: req.params.formUuid, ...req.body, diff --git a/src/handler/manager/forms/parts/deleteFormPart.ts b/src/handler/manager/forms/parts/deleteFormPart.ts index 9b94f10f..9d097ec9 100644 --- a/src/handler/manager/forms/parts/deleteFormPart.ts +++ b/src/handler/manager/forms/parts/deleteFormPart.ts @@ -19,11 +19,14 @@ export const deleteFormPart = async ( } const params = deleteFormPartParamsSchema.parse(req.params); - const form = await prismaClient.formPart.delete({ - where: { uuid: params.uuid }, + const formPart = await prismaClient.formPart.delete({ + where: { + uuid: params.uuid, + form: { teamNumber: req.user.teamNumber }, + }, }); - res.status(200).json({ form }); + res.status(200).json({ formPart }); } catch (error) { if (error instanceof z.ZodError) { res.status(400).json({ error: "Invalid input", details: error }); diff --git a/src/handler/manager/forms/parts/getFormParts.ts b/src/handler/manager/forms/parts/getFormParts.ts index db9fd878..9171ffa5 100644 --- a/src/handler/manager/forms/parts/getFormParts.ts +++ b/src/handler/manager/forms/parts/getFormParts.ts @@ -4,7 +4,7 @@ import prismaClient from "../../../../prismaClient"; import { Response } from "express"; const getFormPartParamsSchema = z.object({ - formPartUuid: z.string(), + uuid: z.string(), }); export const getFormPart = async ( @@ -15,7 +15,7 @@ export const getFormPart = async ( const params = getFormPartParamsSchema.parse(req.params); const formPart = await prismaClient.formPart.findUnique({ - where: { uuid: params.formPartUuid }, + where: { uuid: params.uuid }, include: { formResponseParts: { orderBy: { formResponseUuid: "asc" }, diff --git a/src/handler/manager/forms/parts/reorderFormParts.ts b/src/handler/manager/forms/parts/reorderFormParts.ts index f575d474..681978db 100644 --- a/src/handler/manager/forms/parts/reorderFormParts.ts +++ b/src/handler/manager/forms/parts/reorderFormParts.ts @@ -2,7 +2,7 @@ import z from "zod"; import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; import prismaClient from "../../../../prismaClient"; import { Response } from "express"; -import { Prisma } from "@prisma/client"; +import { Prisma, UserRole } from "@prisma/client"; const updateFormPartParamsSchema = z.object({ formUuid: z.string(), @@ -15,6 +15,11 @@ export const reorderFormParts = async ( res: Response, ): Promise => { try { + if (req.user.role !== UserRole.SCOUTING_LEAD) { + res.status(403).json({ error: "Forbidden" }); + return; + } + const params = updateFormPartParamsSchema.parse({ uuid: req.params.uuid, formUuid: req.params.formUuid, diff --git a/src/handler/manager/forms/parts/updateFormPart.ts b/src/handler/manager/forms/parts/updateFormPart.ts index 48128087..fd320a81 100644 --- a/src/handler/manager/forms/parts/updateFormPart.ts +++ b/src/handler/manager/forms/parts/updateFormPart.ts @@ -2,7 +2,7 @@ import z from "zod"; import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; import prismaClient from "../../../../prismaClient"; import { Response } from "express"; -import { FormPartType, Prisma } from "@prisma/client"; +import { FormPartType, Prisma, UserRole } from "@prisma/client"; const updateFormPartParamsSchema = z.object({ uuid: z.string(), @@ -17,6 +17,11 @@ export const updateFormPart = async ( res: Response, ): Promise => { try { + if (req.user.role !== UserRole.SCOUTING_LEAD) { + res.status(403).json({ error: "Forbidden" }); + return; + } + const params = updateFormPartParamsSchema.parse({ uuid: req.params.uuid, ...req.body, diff --git a/src/handler/manager/forms/responses/submitForm.ts b/src/handler/manager/forms/responses/submitForm.ts index 806cfaa4..850ef581 100644 --- a/src/handler/manager/forms/responses/submitForm.ts +++ b/src/handler/manager/forms/responses/submitForm.ts @@ -21,7 +21,10 @@ export const submitForm = async ( res: Response, ): Promise => { try { - const params = submitFormParamsSchema.parse(req.body); + const params = submitFormParamsSchema.parse({ + formUuid: req.params.formUuid, + ...req.body, + }); if (!params.matchKey && !params.team) { res.status(400).json({ error: "Either matchKey or team is required" }); diff --git a/src/routes/manager/forms.routes.ts b/src/routes/manager/forms.routes.ts index 4780d210..331c5a5c 100644 --- a/src/routes/manager/forms.routes.ts +++ b/src/routes/manager/forms.routes.ts @@ -330,7 +330,6 @@ router.put("/:formUuid/parts/:uuid", updateFormPart); router.put("/:formUuid/parts/:uuid/reorder", reorderFormParts); // Form responses — submitForm is unauthenticated -router.use(requireAuth, requireVerifiedTeam); router.get("/:formUuid/responses", getFormResponses); router.get("/:formUuid/responses/:responseUuid", getFormResponse); router.delete("/:formUuid/responses/:responseUuid", deleteFormResponse); diff --git a/src/routes/manager/manager.routes.ts b/src/routes/manager/manager.routes.ts index 6291cca1..a493b207 100644 --- a/src/routes/manager/manager.routes.ts +++ b/src/routes/manager/manager.routes.ts @@ -10,6 +10,7 @@ import scoutreports from "./scoutreports.routes.js"; import scoutershifts from "./scoutershifts.routes.js"; import settings from "./settings.routes.js"; import apikey from "./apikey.routes.js"; +import forms from "./forms.routes.js"; import { getTournaments } from "../../handler/manager/getTournaments.js"; import { getTeams } from "../../handler/manager/getTeams.js"; @@ -414,6 +415,7 @@ router.use("/tournament", tournaments); router.use("/scoutreports", scoutreports); router.use("/settings", settings); router.use("/apikey", apikey); +router.use("/forms", forms); router.get("/teams", requireAuth, getTeams); router.get("/tournaments", requireAuth, getTournaments); From 49ccf19af7de4e3b3ee23ce4eb7431a8b7f1d96e Mon Sep 17 00:00:00 2001 From: jackattack-4 <142643773+jackattack-4@users.noreply.github.com> Date: Mon, 6 Apr 2026 08:05:49 -0700 Subject: [PATCH 5/8] Update createForm.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/handler/manager/forms/createForm.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handler/manager/forms/createForm.ts b/src/handler/manager/forms/createForm.ts index a6470cff..a7ed6617 100644 --- a/src/handler/manager/forms/createForm.ts +++ b/src/handler/manager/forms/createForm.ts @@ -1,7 +1,7 @@ import z from "zod"; -import { AuthenticatedRequest } from "../../../lib/middleware/requireAuth"; +import { AuthenticatedRequest } from "../../../lib/middleware/requireAuth.js"; import { FormPartType, UserRole } from "@prisma/client"; -import prismaClient from "../../../prismaClient"; +import prismaClient from "../../../prismaClient.js"; import { Response } from "express"; const createFormParamsSchema = z.object({ From 43f83da36d74a80326e144b29fafc46a07e82f47 Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:39:57 -0700 Subject: [PATCH 6/8] add checks for ownership and fix docs --- src/handler/manager/forms/createForm.ts | 3 +- .../manager/forms/parts/createFormPart.ts | 30 ++++---- .../manager/forms/parts/deleteFormPart.ts | 14 +++- .../manager/forms/parts/getFormParts.ts | 8 ++ .../manager/forms/parts/reorderFormParts.ts | 74 ++++++++++--------- .../manager/forms/parts/updateFormPart.ts | 15 +++- .../manager/forms/responses/deleteResponse.ts | 41 ++++++++-- .../manager/forms/responses/getResponse.ts | 5 +- .../manager/forms/responses/getResponses.ts | 2 +- src/handler/manager/forms/updateFormName.ts | 7 +- src/routes/manager/forms.routes.ts | 49 ++++++------ 11 files changed, 161 insertions(+), 87 deletions(-) diff --git a/src/handler/manager/forms/createForm.ts b/src/handler/manager/forms/createForm.ts index a7ed6617..c97c1e0a 100644 --- a/src/handler/manager/forms/createForm.ts +++ b/src/handler/manager/forms/createForm.ts @@ -5,7 +5,6 @@ import prismaClient from "../../../prismaClient.js"; import { Response } from "express"; const createFormParamsSchema = z.object({ - team: z.number(), name: z.string(), parts: z.array( z.object({ @@ -30,7 +29,7 @@ export const createForm = async ( const [form, formParts] = await prismaClient.$transaction(async (tx) => { const form = await tx.form.create({ - data: { name: params.name, teamNumber: params.team }, + data: { name: params.name, teamNumber: req.user.teamNumber }, }); const formParts = await tx.formPart.createManyAndReturn({ data: params.parts.map((part, index) => ({ diff --git a/src/handler/manager/forms/parts/createFormPart.ts b/src/handler/manager/forms/parts/createFormPart.ts index b94ed1a1..3d1a20c4 100644 --- a/src/handler/manager/forms/parts/createFormPart.ts +++ b/src/handler/manager/forms/parts/createFormPart.ts @@ -22,13 +22,22 @@ export const createFormPart = async ( res.status(403).json({ error: "Forbidden" }); return; } - const params = createFormPartParamsSchema.parse({ formUuid: req.params.formUuid, ...req.body, }); - + const form = await prismaClient.form.findUnique({ + where: { uuid: params.formUuid }, + }); + if (!form) { + res.status(404).json({ error: "Form not found" }); + return; + } const formPart = await prismaClient.$transaction(async (tx) => { + const existing = await tx.formPart.findMany({ + where: { formUuid: params.formUuid }, + orderBy: { order: "asc" }, + }); const formPart = await tx.formPart.create({ data: { formUuid: params.formUuid, @@ -39,19 +48,9 @@ export const createFormPart = async ( order: params.order, }, }); - const formParts = await tx.formPart.findMany({ - where: { formUuid: params.formUuid }, - orderBy: { order: "asc" }, - }); - const reordered = formParts.map((p) => ({ - ...p, - order: - p.uuid === formPart.uuid - ? params.order - : p.order >= params.order - ? p.order + 1 - : p.order, - })); + const without = existing.filter((p) => p.uuid !== formPart.uuid); + without.splice(params.order, 0, formPart); + const reordered = without.map((p, index) => ({ ...p, order: index })); await Promise.all( reordered.map((p) => tx.formPart.update({ @@ -62,7 +61,6 @@ export const createFormPart = async ( ); return formPart; }); - res.status(201).json({ formPart }); } catch (error) { if (error instanceof z.ZodError) { diff --git a/src/handler/manager/forms/parts/deleteFormPart.ts b/src/handler/manager/forms/parts/deleteFormPart.ts index 9d097ec9..e695bc6a 100644 --- a/src/handler/manager/forms/parts/deleteFormPart.ts +++ b/src/handler/manager/forms/parts/deleteFormPart.ts @@ -18,14 +18,24 @@ export const deleteFormPart = async ( return; } const params = deleteFormPartParamsSchema.parse(req.params); - - const formPart = await prismaClient.formPart.delete({ + const existingFormPart = await prismaClient.formPart.findFirst({ where: { uuid: params.uuid, form: { teamNumber: req.user.teamNumber }, }, }); + if (!existingFormPart) { + res.status(404).json({ error: "Form part not found" }); + return; + } + + const formPart = await prismaClient.formPart.delete({ + where: { + uuid: existingFormPart.uuid, + }, + }); + res.status(200).json({ formPart }); } catch (error) { if (error instanceof z.ZodError) { diff --git a/src/handler/manager/forms/parts/getFormParts.ts b/src/handler/manager/forms/parts/getFormParts.ts index 9171ffa5..bb30bf40 100644 --- a/src/handler/manager/forms/parts/getFormParts.ts +++ b/src/handler/manager/forms/parts/getFormParts.ts @@ -20,6 +20,9 @@ export const getFormPart = async ( formResponseParts: { orderBy: { formResponseUuid: "asc" }, }, + form: { + select: { teamNumber: true }, + }, }, }); @@ -28,6 +31,11 @@ export const getFormPart = async ( return; } + if (formPart.form.teamNumber !== req.user.teamNumber) { + res.status(403).json({ error: "Forbidden" }); + return; + } + res.status(200).json({ formPart }); } catch (error) { if (error instanceof z.ZodError) { diff --git a/src/handler/manager/forms/parts/reorderFormParts.ts b/src/handler/manager/forms/parts/reorderFormParts.ts index 681978db..a8040540 100644 --- a/src/handler/manager/forms/parts/reorderFormParts.ts +++ b/src/handler/manager/forms/parts/reorderFormParts.ts @@ -19,50 +19,58 @@ export const reorderFormParts = async ( res.status(403).json({ error: "Forbidden" }); return; } - const params = updateFormPartParamsSchema.parse({ uuid: req.params.uuid, formUuid: req.params.formUuid, ...req.body, }); - - const formParts = await prismaClient.formPart.findMany({ - where: { formUuid: params.formUuid }, - orderBy: { order: "asc" }, + const formPart = await prismaClient.$transaction(async (tx) => { + const form = await tx.form.findUnique({ + where: { uuid: params.formUuid }, + }); + if (!form) { + throw new Error("Form not found"); + } + if (form.teamNumber !== req.user.teamNumber) { + throw new Error("Forbidden"); + } + const formParts = await tx.formPart.findMany({ + where: { formUuid: params.formUuid }, + orderBy: { order: "asc" }, + }); + const target = formParts.find((p) => p.uuid === params.uuid); + if (!target) { + throw new Error("Form part not found"); + } + const without = formParts.filter((p) => p.uuid !== params.uuid); + without.splice(params.order, 0, target); + const reordered = without.map((p, index) => ({ ...p, order: index })); + const updatedFormParts = await Promise.all( + reordered.map((p) => + tx.formPart.update({ + where: { uuid: p.uuid }, + data: { order: p.order }, + }), + ), + ); + return updatedFormParts.find((p) => p.uuid === params.uuid); }); - - const target = formParts.find((p) => p.uuid === params.uuid); - - if (!target) { - res.status(404).json({ error: "Form part not found" }); - return; - } - - const reordered = formParts.map((p) => ({ - ...p, - order: - p.uuid === params.uuid - ? params.order - : p.order >= params.order - ? p.order + 1 - : p.order, - })); - - const updatedFormParts = await prismaClient.$transaction( - reordered.map((formPart) => - prismaClient.formPart.update({ - where: { uuid: formPart.uuid }, - data: { order: formPart.order }, - }), - ), - ); - const formPart = updatedFormParts.find((part) => part.uuid === params.uuid); - res.status(200).json({ formPart }); } catch (error) { if (error instanceof z.ZodError) { res.status(400).json({ error: "Invalid input", details: error }); return; + } else if (error instanceof Error) { + if (error.message === "Form not found") { + res.status(404).json({ error: "Form not found" }); + return; + } else if (error.message === "Forbidden") { + res.status(403).json({ error: "Forbidden" }); + return; + } else if (error.message === "Form part not found") { + res.status(404).json({ error: "Form part not found" }); + return; + } } else if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === "P2025") { res.status(404).json({ error: "Form part not found" }); diff --git a/src/handler/manager/forms/parts/updateFormPart.ts b/src/handler/manager/forms/parts/updateFormPart.ts index fd320a81..38988af9 100644 --- a/src/handler/manager/forms/parts/updateFormPart.ts +++ b/src/handler/manager/forms/parts/updateFormPart.ts @@ -21,12 +21,22 @@ export const updateFormPart = async ( res.status(403).json({ error: "Forbidden" }); return; } - const params = updateFormPartParamsSchema.parse({ uuid: req.params.uuid, ...req.body, }); - + const existing = await prismaClient.formPart.findUnique({ + where: { uuid: params.uuid }, + include: { form: true }, + }); + if (!existing) { + res.status(404).json({ error: "Form part not found" }); + return; + } + if (existing.form.teamNumber !== req.user.teamNumber) { + res.status(403).json({ error: "Forbidden" }); + return; + } const formPart = await prismaClient.formPart.update({ where: { uuid: params.uuid }, data: { @@ -36,7 +46,6 @@ export const updateFormPart = async ( options: params.options, }, }); - res.status(200).json({ formPart }); } catch (error) { if (error instanceof z.ZodError) { diff --git a/src/handler/manager/forms/responses/deleteResponse.ts b/src/handler/manager/forms/responses/deleteResponse.ts index 972c06ac..de352331 100644 --- a/src/handler/manager/forms/responses/deleteResponse.ts +++ b/src/handler/manager/forms/responses/deleteResponse.ts @@ -5,7 +5,8 @@ import prismaClient from "../../../../prismaClient"; import { Response } from "express"; const deleteFormResponseParamsSchema = z.object({ - uuid: z.string(), + formUuid: z.string(), + responseUuid: z.string(), }); export const deleteFormResponse = async ( @@ -19,8 +20,35 @@ export const deleteFormResponse = async ( } const params = deleteFormResponseParamsSchema.parse(req.params); + const existingFormResponse = await prismaClient.formResponse.findFirst({ + where: { + uuid: params.responseUuid, + form: { teamNumber: req.user.teamNumber }, + }, + include: { + form: true, + }, + }); + + if (!existingFormResponse) { + res.status(404).json({ error: "Form response not found" }); + return; + } + + if (existingFormResponse.formUuid !== params.formUuid) { + res + .status(400) + .json({ error: "Form response does not belong to the specified form" }); + return; + } + + if (existingFormResponse.form.teamNumber !== req.user.teamNumber) { + res.status(403).json({ error: "Forbidden" }); + return; + } + const form = await prismaClient.formResponse.delete({ - where: { uuid: params.uuid }, + where: { uuid: params.responseUuid }, }); res.status(200).json({ form }); @@ -33,11 +61,10 @@ export const deleteFormResponse = async ( res.status(404).json({ error: "Form response not found" }); return; } - } else { - console.error("Error deleting form response:", error); - res - .status(500) - .json({ error: "An error occurred while deleting the form response." }); } + console.error("Error deleting form response:", error); + res + .status(500) + .json({ error: "An error occurred while deleting the form response." }); } }; diff --git a/src/handler/manager/forms/responses/getResponse.ts b/src/handler/manager/forms/responses/getResponse.ts index ec1643da..aa1d565f 100644 --- a/src/handler/manager/forms/responses/getResponse.ts +++ b/src/handler/manager/forms/responses/getResponse.ts @@ -15,7 +15,10 @@ export const getFormResponse = async ( const params = getResponseParamsSchema.parse(req.params); const formResponse = await prismaClient.formResponse.findUnique({ - where: { uuid: params.responseUuid }, + where: { + uuid: params.responseUuid, + form: { teamNumber: req.user.teamNumber }, + }, include: { formResponseParts: { orderBy: { formPartUuid: "asc" }, diff --git a/src/handler/manager/forms/responses/getResponses.ts b/src/handler/manager/forms/responses/getResponses.ts index 283c4a0b..a7090cb6 100644 --- a/src/handler/manager/forms/responses/getResponses.ts +++ b/src/handler/manager/forms/responses/getResponses.ts @@ -15,7 +15,7 @@ export const getFormResponses = async ( const params = getResponsesParamsSchema.parse(req.params); const form = await prismaClient.form.findUnique({ - where: { uuid: params.formUuid }, + where: { uuid: params.formUuid, teamNumber: req.user.teamNumber }, include: { formResponses: { include: { diff --git a/src/handler/manager/forms/updateFormName.ts b/src/handler/manager/forms/updateFormName.ts index 5853f651..00b7edfc 100644 --- a/src/handler/manager/forms/updateFormName.ts +++ b/src/handler/manager/forms/updateFormName.ts @@ -14,6 +14,11 @@ export const updateFormName = async ( res: Response, ): Promise => { try { + if (req.user.role !== "SCOUTING_LEAD") { + res.status(403).json({ error: "Forbidden" }); + return; + } + const params = updateFormNameParamsSchema.safeParse({ formUuid: req.params.formUuid, name: req.body.name, @@ -25,7 +30,7 @@ export const updateFormName = async ( } const form = await prismaClient.form.update({ - where: { uuid: params.data.formUuid }, + where: { uuid: params.data.formUuid, teamNumber: req.user.teamNumber }, data: { name: params.data.name }, }); diff --git a/src/routes/manager/forms.routes.ts b/src/routes/manager/forms.routes.ts index 331c5a5c..122fbd0a 100644 --- a/src/routes/manager/forms.routes.ts +++ b/src/routes/manager/forms.routes.ts @@ -16,6 +16,7 @@ import { deleteFormResponse } from "../../handler/manager/forms/responses/delete import { getFormResponse } from "../../handler/manager/forms/responses/getResponse.js"; import { getFormResponses } from "../../handler/manager/forms/responses/getResponses.js"; import { submitForm } from "../../handler/manager/forms/responses/submitForm.js"; +import { FormPartType } from "@prisma/client"; // Forms registry.registerPath({ @@ -28,12 +29,11 @@ registry.registerPath({ content: { "application/json": { schema: z.object({ - team: z.number(), name: z.string(), parts: z.array( z.object({ name: z.string(), - type: z.string(), + type: z.nativeEnum(FormPartType), caption: z.string(), options: z.record(z.string(), z.unknown()).optional(), }), @@ -47,6 +47,7 @@ registry.registerPath({ 200: { description: "Created" }, 400: { description: "Invalid request parameters" }, 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], @@ -54,10 +55,10 @@ registry.registerPath({ registry.registerPath({ method: "delete", - path: "/v1/manager/forms/{formUuid}", + path: "/v1/manager/forms/:uuid", tags: ["Manager - Forms"], summary: "Delete a form", - request: { params: z.object({ formUuid: z.string() }) }, + request: { params: z.object({ uuid: z.string() }) }, responses: { 200: { description: "Deleted" }, 401: { description: "Unauthorized" }, @@ -83,7 +84,7 @@ registry.registerPath({ registry.registerPath({ method: "put", - path: "/v1/manager/forms/{formUuid}", + path: "/v1/manager/forms/:formUuid", tags: ["Manager - Forms"], summary: "Update form name", request: { @@ -98,6 +99,7 @@ registry.registerPath({ 200: { description: "Updated" }, 400: { description: "Invalid request parameters" }, 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, 404: { description: "Form not found" }, 500: { description: "Server error" }, }, @@ -107,7 +109,7 @@ registry.registerPath({ // Form parts registry.registerPath({ method: "post", - path: "/v1/manager/forms/{formUuid}/parts", + path: "/v1/manager/forms/:formUuid/parts", tags: ["Manager - Form Parts"], summary: "Add a part to a form", request: { @@ -117,7 +119,7 @@ registry.registerPath({ "application/json": { schema: z.object({ name: z.string(), - type: z.string(), + type: z.nativeEnum(FormPartType), caption: z.string(), options: z.record(z.string(), z.unknown()), order: z.number(), @@ -130,6 +132,8 @@ registry.registerPath({ 201: { description: "Created" }, 400: { description: "Invalid request parameters" }, 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, + 404: { description: "Form not found" }, 500: { description: "Server error" }, }, security: [{ bearerAuth: [] }], @@ -137,11 +141,11 @@ registry.registerPath({ registry.registerPath({ method: "delete", - path: "/v1/manager/forms/{formUuid}/parts/{partUuid}", + path: "/v1/manager/forms/:formUuid/parts/:uuid", tags: ["Manager - Form Parts"], summary: "Delete a form part", request: { - params: z.object({ formUuid: z.string(), partUuid: z.string() }), + params: z.object({ formUuid: z.string(), uuid: z.string() }), }, responses: { 200: { description: "Deleted" }, @@ -155,15 +159,16 @@ registry.registerPath({ registry.registerPath({ method: "get", - path: "/v1/manager/forms/{formUuid}/parts/{partUuid}", + path: "/v1/manager/forms/:formUuid/parts/:uuid", tags: ["Manager - Form Parts"], summary: "Get a form part with its responses", request: { - params: z.object({ formUuid: z.string(), partUuid: z.string() }), + params: z.object({ formUuid: z.string(), uuid: z.string() }), }, responses: { 200: { description: "Form part" }, 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, 404: { description: "Form part not found" }, 500: { description: "Server error" }, }, @@ -172,17 +177,17 @@ registry.registerPath({ registry.registerPath({ method: "put", - path: "/v1/manager/forms/{formUuid}/parts/{partUuid}", + path: "/v1/manager/forms/:formUuid/parts/:uuid", tags: ["Manager - Form Parts"], summary: "Update a form part", request: { - params: z.object({ formUuid: z.string(), partUuid: z.string() }), + params: z.object({ formUuid: z.string(), uuid: z.string() }), body: { content: { "application/json": { schema: z.object({ name: z.string(), - type: z.string(), + type: z.nativeEnum(FormPartType), caption: z.string(), options: z.record(z.string(), z.unknown()), }), @@ -194,6 +199,7 @@ registry.registerPath({ 200: { description: "Updated" }, 400: { description: "Invalid request parameters" }, 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, 404: { description: "Form part not found" }, 500: { description: "Server error" }, }, @@ -202,11 +208,11 @@ registry.registerPath({ registry.registerPath({ method: "put", - path: "/v1/manager/forms/{formUuid}/parts/{partUuid}/reorder", + path: "/v1/manager/forms/:formUuid/parts/:uuid/reorder", tags: ["Manager - Form Parts"], summary: "Reorder a form part", request: { - params: z.object({ formUuid: z.string(), partUuid: z.string() }), + params: z.object({ formUuid: z.string(), uuid: z.string() }), body: { content: { "application/json": { schema: z.object({ order: z.number() }) }, @@ -217,6 +223,7 @@ registry.registerPath({ 200: { description: "Reordered" }, 400: { description: "Invalid request parameters" }, 401: { description: "Unauthorized" }, + 403: { description: "Forbidden" }, 404: { description: "Form part not found" }, 500: { description: "Server error" }, }, @@ -226,7 +233,7 @@ registry.registerPath({ // Form responses registry.registerPath({ method: "post", - path: "/v1/manager/forms/{formUuid}/responses", + path: "/v1/manager/forms/:formUuid/responses", tags: ["Manager - Form Responses"], summary: "Submit a form response", request: { @@ -262,7 +269,7 @@ registry.registerPath({ registry.registerPath({ method: "get", - path: "/v1/manager/forms/{formUuid}/responses", + path: "/v1/manager/forms/:formUuid/responses", tags: ["Manager - Form Responses"], summary: "Get all responses for a form", request: { params: z.object({ formUuid: z.string() }) }, @@ -277,7 +284,7 @@ registry.registerPath({ registry.registerPath({ method: "get", - path: "/v1/manager/forms/{formUuid}/responses/{responseUuid}", + path: "/v1/manager/forms/:formUuid/responses/:responseUuid", tags: ["Manager - Form Responses"], summary: "Get a single form response", request: { @@ -294,7 +301,7 @@ registry.registerPath({ registry.registerPath({ method: "delete", - path: "/v1/manager/forms/{formUuid}/responses/{responseUuid}", + path: "/v1/manager/forms/:formUuid/responses/:responseUuid", tags: ["Manager - Form Responses"], summary: "Delete a form response", request: { @@ -318,7 +325,7 @@ router.use(requireAuth, requireVerifiedTeam); // Forms router.post("/", createForm); -router.delete("/:formUuid", deleteForm); +router.delete("/:uuid", deleteForm); router.get("/", getForms); router.put("/:formUuid", updateFormName); From 7878528fc0178e5520b6263d572d6c623448fae1 Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:03:18 -0700 Subject: [PATCH 7/8] add new files + check for scouter --- src/handler/analysis/coreAnalysis/averageForm.ts | 0 src/handler/analysis/csv/getFormCSV.ts | 0 src/handler/analysis/teamLookUp/formDetails.ts | 0 src/handler/analysis/teamLookUp/formMetrics.ts | 0 src/handler/manager/forms/responses/submitForm.ts | 9 +++++++++ 5 files changed, 9 insertions(+) create mode 100644 src/handler/analysis/coreAnalysis/averageForm.ts create mode 100644 src/handler/analysis/csv/getFormCSV.ts create mode 100644 src/handler/analysis/teamLookUp/formDetails.ts create mode 100644 src/handler/analysis/teamLookUp/formMetrics.ts diff --git a/src/handler/analysis/coreAnalysis/averageForm.ts b/src/handler/analysis/coreAnalysis/averageForm.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/handler/analysis/csv/getFormCSV.ts b/src/handler/analysis/csv/getFormCSV.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/handler/analysis/teamLookUp/formDetails.ts b/src/handler/analysis/teamLookUp/formDetails.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/handler/analysis/teamLookUp/formMetrics.ts b/src/handler/analysis/teamLookUp/formMetrics.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/handler/manager/forms/responses/submitForm.ts b/src/handler/manager/forms/responses/submitForm.ts index 850ef581..334c1300 100644 --- a/src/handler/manager/forms/responses/submitForm.ts +++ b/src/handler/manager/forms/responses/submitForm.ts @@ -31,6 +31,15 @@ export const submitForm = async ( return; } + const scouter = await prismaClient.scouter.findUnique({ + where: { uuid: params.scouterUuid }, + }); + + if (!scouter) { + res.status(404).json({ error: "Scouter not found" }); + return; + } + const formResponseCreateData: Prisma.FormResponseCreateInput = params.matchKey ? { From 56ff4f008b18732cc0168cfe15ceb5bd99ae8fd5 Mon Sep 17 00:00:00 2001 From: JackAttack-365 <142643773+jackattack-4@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:15:46 -0700 Subject: [PATCH 8/8] add submitFormDashboard --- .../forms/responses/submitFormDashboard.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/handler/manager/forms/responses/submitFormDashboard.ts diff --git a/src/handler/manager/forms/responses/submitFormDashboard.ts b/src/handler/manager/forms/responses/submitFormDashboard.ts new file mode 100644 index 00000000..335e7e22 --- /dev/null +++ b/src/handler/manager/forms/responses/submitFormDashboard.ts @@ -0,0 +1,86 @@ +import z from "zod"; +import { Prisma } from "@prisma/client"; +import prismaClient from "../../../../prismaClient"; +import { Response } from "express"; +import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth"; + +const submitFormDashboardParamsSchema = z.object({ + team: z.number().optional(), + scouterUuid: z.string(), + matchKey: z.string().optional(), + formUuid: z.string(), + parts: z.array( + z.object({ + formPartUuid: z.string(), + response: z.union([z.string(), z.number(), z.array(z.string())]), + }), + ), +}); + +export const submitFormDashboard = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + const params = submitFormDashboardParamsSchema.parse({ + formUuid: req.params.formUuid, + ...req.body, + }); + + if (!params.matchKey && !params.team) { + res.status(400).json({ error: "Either matchKey or team is required" }); + return; + } + + const scouter = await prismaClient.scouter.findUnique({ + where: { uuid: params.scouterUuid }, + }); + + if (!scouter) { + res.status(404).json({ error: "Scouter not found" }); + return; + } + + const formResponseCreateData: Prisma.FormResponseCreateInput = + params.matchKey + ? { + teamMatchData: { connect: { key: params.matchKey } }, + scouter: { connect: { uuid: params.scouterUuid } }, + form: { connect: { uuid: params.formUuid } }, + } + : { + team: { connect: { number: params.team } }, + scouter: { connect: { uuid: params.scouterUuid } }, + form: { connect: { uuid: params.formUuid } }, + }; + + const [formResponse, formResponseParts] = await prismaClient.$transaction( + async (tx) => { + const formResponse = await tx.formResponse.create({ + data: formResponseCreateData, + }); + const formResponseParts = await tx.formResponsePart.createManyAndReturn( + { + data: params.parts.map((part) => ({ + formResponseUuid: formResponse.uuid, + formPartUuid: part.formPartUuid, + response: part.response, + })), + }, + ); + return [formResponse, formResponseParts]; + }, + ); + + res.status(200).json({ formResponse, formResponseParts }); + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ error: "Invalid input", details: error }); + return; + } + console.error("Error submitting form:", error); + res + .status(500) + .json({ error: "An error occurred while submitting the form." }); + } +};