Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +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[]
tournament Tournament @relation(fields: [tournamentKey], references: [key], onDelete: Cascade)
formResponses FormResponse[]
tournament Tournament @relation(fields: [tournamentKey], references: [key], onDelete: Cascade)

@@index([tournamentKey, teamNumber])
}
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -137,6 +139,7 @@ model Team {
number Int @id
name String
registeredTeam RegisteredTeam?
formResponses FormResponse[]
}

model RegisteredTeam {
Expand All @@ -151,6 +154,7 @@ model RegisteredTeam {
scouters Scouter[]
scouterScheduleShifts ScouterScheduleShift[]
slackChannels SlackWorkspace[]
customForms Form[]
users User[]
}

Expand Down Expand Up @@ -233,6 +237,58 @@ model CachedAnalysis {
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 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)
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
LEFT_BUMP
Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
58 changes: 58 additions & 0 deletions src/handler/manager/forms/createForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import z from "zod";
import { AuthenticatedRequest } from "../../../lib/middleware/requireAuth.js";
import { FormPartType, UserRole } from "@prisma/client";
import prismaClient from "../../../prismaClient.js";
import { Response } from "express";

const createFormParamsSchema = z.object({
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<void> => {
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: req.user.teamNumber },
});
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." });
}
};
43 changes: 43 additions & 0 deletions src/handler/manager/forms/deleteForm.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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, teamNumber: req.user.teamNumber },
});
Comment thread
jackattack-4 marked this conversation as resolved.

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." });
}
}
};
28 changes: 28 additions & 0 deletions src/handler/manager/forms/getForms.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
try {
const forms = await prismaClient.form.findMany({
where: {
teamNumber: req.user.teamNumber,
},
include: {
formParts: {
orderBy: { order: "asc" },
},
},
});

res.status(200).json({ forms });
} catch (error) {
console.error("Error getting forms:", error);
res
.status(500)
.json({ error: "An error occurred while getting the forms." });
}
};
75 changes: 75 additions & 0 deletions src/handler/manager/forms/parts/createFormPart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import z from "zod";
import { AuthenticatedRequest } from "../../../../lib/middleware/requireAuth";
import prismaClient from "../../../../prismaClient";
import { Response } from "express";
import { FormPartType, UserRole } 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<void> => {
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,
});
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,
type: params.type,
caption: params.caption,
name: params.name,
options: params.options,
order: params.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({
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." });
}
};
56 changes: 56 additions & 0 deletions src/handler/manager/forms/parts/deleteFormPart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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<void> => {
try {
if (req.user.role !== UserRole.SCOUTING_LEAD) {
res.status(403).json({ error: "Forbidden" });
return;
}
const params = deleteFormPartParamsSchema.parse(req.params);
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) {
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." });
}
}
};
Loading
Loading