From 2f07f53af5b498a103f652692c2ab4d46c9e2872 Mon Sep 17 00:00:00 2001 From: johnmeshulam <55348702+johnmeshulam@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:53:16 +0200 Subject: [PATCH 1/4] Add session status to query --- .../(dashboard)/deliberation/[category]/graphql/query.ts | 1 + .../(dashboard)/deliberation/[category]/graphql/types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/query.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/query.ts index 5ef64b81c..21cd78f41 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/query.ts +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/query.ts @@ -23,6 +23,7 @@ export const GET_CATEGORY_DELIBERATION: TypedDocumentNode< slug judgingSession { id + status room { id name diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/types.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/types.ts index 69c27950d..9c8ae85a5 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/types.ts +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/types.ts @@ -38,6 +38,7 @@ export interface Room { export interface JudgingSession { id: string; room: Room; + status: 'not-started' | 'in-progress' | 'completed'; } export interface GPValue { From 36c97aa03b5f7b52e8332f408bf221a2dbc91b67 Mon Sep 17 00:00:00 2001 From: johnmeshulam <55348702+johnmeshulam@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:58:06 +0200 Subject: [PATCH 2/4] Deliberation context --- .../[category]/deliberation-computation.ts | 263 ++++++++++ .../[category]/deliberation-context.tsx | 219 +++++++++ .../deliberation/[category]/page-content.tsx | 452 ++++++++++++++++++ .../deliberation/[category]/page.tsx | 433 +---------------- .../deliberation/[category]/types.ts | 72 +++ 5 files changed, 1016 insertions(+), 423 deletions(-) create mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-computation.ts create mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-context.tsx create mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page-content.tsx create mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/types.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-computation.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-computation.ts new file mode 100644 index 000000000..33d0addac --- /dev/null +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-computation.ts @@ -0,0 +1,263 @@ +import { JudgingCategory } from '@lems/types/judging'; +import type { Team, JudgingDeliberation } from './graphql/types'; +import type { MetricPerCategory, RoomMetricsMap } from './types'; + +/** + * Maximum picklist limit when using default formula. + * Picklist cannot exceed 12 teams (Enforced only in the UI). + */ +export const MAX_PICKLIST_LIMIT = 12; + +/** + * Default multiplier for calculating picklist limit. + * Picklist limit = min(12, ceil(teamCount * 0.35)) + */ +export const PICKLIST_LIMIT_MULTIPLIER = 0.35; + +/** + * Computes raw category scores and total score for a team. + * + * Category scores are the sum of all field values in that rubric. + * For core-values, also includes sum of GP scores (default 3 if not present). + * Total score is the sum of all three categories. + * + * @param team - The team to compute scores for + * @returns TeamScores object with all category and total scores + */ +export const computeTeamScores = (team: Team): MetricPerCategory => { + // Compute innovation-project score + const ipRubric = team.rubrics.innovation_project; + const ipScore = ipRubric?.data?.fields + ? Object.values(ipRubric.data.fields).reduce((sum, field) => sum + (field.value ?? 0), 0) + : 0; + + // Compute robot-design score + const rdRubric = team.rubrics.robot_design; + const rdScore = rdRubric?.data?.fields + ? Object.values(rdRubric.data.fields).reduce((sum, field) => sum + (field.value ?? 0), 0) + : 0; + + // Compute core-values score (rubric fields + GP scores) + const cvRubric = team.rubrics.core_values; + const cvRubricScore = cvRubric?.data?.fields + ? Object.values(cvRubric.data.fields).reduce((sum, field) => sum + (field.value ?? 0), 0) + : 0; + const gpScoresSum = (team.scoresheets ?? []).reduce( + (sum, scoresheet) => sum + (scoresheet.data?.gp?.value ?? 3), + 0 + ); + const cvScore = cvRubricScore + gpScoresSum; + + const totalScore = ipScore + rdScore + cvScore; + + return { + 'innovation-project': ipScore, + 'robot-design': rdScore, + 'core-values': cvScore, + total: totalScore + }; +}; + +/** + * Computes room normalization metrics by aggregating team scores per room. + * + * Room metrics are used to normalize team scores based on the difficulty of their room. + * This is computed by averaging scores of all teams in each room. + * + * @param teamScores - Array of TeamScores for all teams (parallel to teams array) + * @param teams - Array of teams (provides room information) + * @returns Map of room ID to RoomMetrics + */ +export function computeRoomMetrics( + teamScores: MetricPerCategory[], + teams?: Team[] +): RoomMetricsMap { + const roomAggregates: Record = {}; + + teamScores.forEach((scores, index) => { + const room = teams?.[index]?.judgingSession?.room; + if (!room) return; + + if (!roomAggregates[room.id]) { + roomAggregates[room.id] = { scores: [], count: 0 }; + } + roomAggregates[room.id].scores.push(scores); + roomAggregates[room.id].count++; + }); + + const result: RoomMetricsMap = {}; + + Object.entries(roomAggregates).forEach(([roomId, { scores }]) => { + const avgIP = scores.reduce((sum, s) => sum + s['innovation-project'], 0) / scores.length; + const avgRD = scores.reduce((sum, s) => sum + s['robot-design'], 0) / scores.length; + const avgCV = scores.reduce((sum, s) => sum + s['core-values'], 0) / scores.length; + const avgTotal = scores.reduce((sum, s) => sum + s.total, 0) / scores.length; + + result[roomId] = { + avgScores: { + 'innovation-project': avgIP, + 'robot-design': avgRD, + 'core-values': avgCV, + total: avgTotal + }, + teamCount: scores.length + }; + }); + + return result; +} + +/** + * Computes normalized scores using room normalization factors. + * + * Normalization formula: normalizedScore = rawScore * (divisionAvg / roomAvg) + * This adjusts for room difficulty by comparing each room's average to the division average. + * + * @param scores - Raw team scores + * @param roomMetrics - Room metrics map + * @param roomId - The team's room ID (can be null/undefined) + * @returns Object with normalized scores for each category and total + */ +export function computeNormalizedScores( + scores: MetricPerCategory, + roomMetrics: RoomMetricsMap, + roomId?: string | null +): Record { + // If no room data, return raw scores + if (!roomId || !roomMetrics[roomId]) { + return { + 'innovation-project': scores['innovation-project'], + 'robot-design': scores['robot-design'], + 'core-values': scores['core-values'], + total: scores.total + }; + } + + // Compute division averages across all rooms + const allRoomIds = Object.keys(roomMetrics); + const divisionAvgIP = + allRoomIds.reduce((sum, rid) => sum + roomMetrics[rid].avgScores['innovation-project'], 0) / + allRoomIds.length; + const divisionAvgRD = + allRoomIds.reduce((sum, rid) => sum + roomMetrics[rid].avgScores['robot-design'], 0) / + allRoomIds.length; + const divisionAvgCV = + allRoomIds.reduce((sum, rid) => sum + roomMetrics[rid].avgScores['core-values'], 0) / + allRoomIds.length; + const divisionAvgTotal = + allRoomIds.reduce((sum, rid) => sum + roomMetrics[rid].avgScores.total, 0) / allRoomIds.length; + + const roomMetric = roomMetrics[roomId]; + + // Apply normalization formula: normalized = raw * (divisionAvg / roomAvg) + return { + 'innovation-project': + scores['innovation-project'] * (divisionAvgIP / roomMetric.avgScores['innovation-project']), + 'robot-design': scores['robot-design'] * (divisionAvgRD / roomMetric.avgScores['robot-design']), + 'core-values': scores['core-values'] * (divisionAvgCV / roomMetric.avgScores['core-values']), + total: scores.total * (divisionAvgTotal / roomMetric.avgScores.total) + }; +} + +/** + * Computes ranking for teams based on scores. + * + * Teams are ranked by score (descending). Tied teams receive the same rank, + * and the next rank accounts for the number of tied teams. + * + * @param teamScores - The team's raw scores + * @param allTeamScores - Scores for all teams (used for relative ranking) + * @returns MetricPerCategory object with ranks for each category and total + */ +export function computeRanks( + teamScores: MetricPerCategory, + allTeamScores: MetricPerCategory[] +): MetricPerCategory { + // Helper: compute rank by category + const computeRankByCategory = (category: keyof MetricPerCategory, teamScore: number): number => { + // Count how many teams have a higher score + const higherScoreCount = allTeamScores.filter(ts => { + const score = ts[category]; + return score > teamScore; + }).length; + return higherScoreCount + 1; + }; + + return { + 'innovation-project': computeRankByCategory( + 'innovation-project', + teamScores['innovation-project'] + ), + 'robot-design': computeRankByCategory('robot-design', teamScores['robot-design']), + 'core-values': computeRankByCategory('core-values', teamScores['core-values']), + total: computeRankByCategory('total', teamScores.total) + }; +} + +/** + * Determines if a team is eligible for the picklist. + * + * A team is eligible if: + * 1. It has arrived at the event + * 2. It is not disqualified + * 3. Its judging session status is 'completed' + * 4. It is not already in the picklist + * + * @param team - The team to check + * @param deliberation - The deliberation containing the picklist + * @returns true if the team is eligible, false otherwise + */ +export function computeEligibility(team: Team, deliberation: JudgingDeliberation | null): boolean { + if (!team.arrived) return false; + + if (team.disqualified) return false; + + if (!team.judgingSession?.status || team.judgingSession.status !== 'completed') { + return false; + } + + if (deliberation?.picklist.includes(team.id)) { + return false; + } + + return true; +} + +/** + * Extracts and flattens rubric field values for grid display. + * + * For the current category, returns field values as a flat object. + * For core-values category, also includes GP scores keyed as 'gp-{round}'. + * + * @param team - The team to extract fields from + * @param categoryKey - The deliberation category (underscore-separated: e.g., 'core_values') + * @returns Object with field IDs as keys and values as field values + */ +export function getFlattenedRubricFields( + team: Team, + categoryKey: 'innovation_project' | 'robot_design' | 'core_values' +): Record { + const result: Record = {}; + + const categoryMap = { + innovation_project: team.rubrics.innovation_project, + robot_design: team.rubrics.robot_design, + core_values: team.rubrics.core_values + } as const; + + const rubric = categoryMap[categoryKey]; + if (rubric?.data?.fields) { + Object.entries(rubric.data.fields).forEach(([fieldId, fieldValue]) => { + result[fieldId] = (fieldValue as unknown as { value: number | null }).value ?? null; + }); + } + + // For core-values, add GP scores + if (categoryKey === 'core_values') { + (team.scoresheets ?? []).forEach(scoresheet => { + result[`gp-${scoresheet.round}`] = scoresheet.data?.gp?.value ?? null; + }); + } + + return result; +} diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-context.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-context.tsx new file mode 100644 index 000000000..356265e41 --- /dev/null +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-context.tsx @@ -0,0 +1,219 @@ +'use client'; + +import { createContext, useContext, useMemo, ReactNode, useCallback } from 'react'; +import { useMutation } from '@apollo/client/react'; +import { JudgingCategory } from '@lems/types/judging'; +import { hyphensToUnderscores } from '@lems/shared/utils'; +import { + START_DELIBERATION_MUTATION, + UPDATE_DELIBERATION_PICKLIST_MUTATION +} from './graphql/mutations'; +import type { DeliberationContextValue, EnrichedTeam } from './types'; +import type { Division } from './graphql/types'; +import { + computeTeamScores, + computeRoomMetrics, + computeNormalizedScores, + computeRanks, + computeEligibility, + getFlattenedRubricFields, + PICKLIST_LIMIT_MULTIPLIER, + MAX_PICKLIST_LIMIT +} from './deliberation-computation'; + +const DeliberationContext = createContext(null); + +interface DeliberationProviderProps { + divisionId: string; + category: JudgingCategory; + division: Division; + children?: ReactNode; +} + +export function CategoryDeliberationProvider({ + divisionId, + category, + division, + children +}: DeliberationProviderProps) { + const deliberation = division.judging.deliberation; + + // Mutations + const [startDeliberation] = useMutation(START_DELIBERATION_MUTATION); + const [updateDeliberationPicklist] = useMutation(UPDATE_DELIBERATION_PICKLIST_MUTATION); + + // Helper functions for mutations - use useCallback to ensure stable references + const handleStartDeliberation = useCallback(async () => { + await startDeliberation({ + variables: { divisionId, category } + }); + }, [startDeliberation, divisionId, category]); + + const handleUpdatePicklist = useCallback( + async (teamIds: string[]) => { + await updateDeliberationPicklist({ + variables: { divisionId, category, picklist: teamIds } + }); + }, + [updateDeliberationPicklist, divisionId, category] + ); + + const handleAddToPicklist = useCallback( + async (teamId: string) => { + const currentPicklist = deliberation?.picklist ?? []; + const newPicklist = [...currentPicklist, teamId]; + await handleUpdatePicklist(newPicklist); + }, + [deliberation, handleUpdatePicklist] + ); + + const handleRemoveFromPicklist = useCallback( + async (teamId: string) => { + const currentPicklist = deliberation?.picklist ?? []; + const newPicklist = currentPicklist.filter((id: string) => id !== teamId); + await handleUpdatePicklist(newPicklist); + }, + [deliberation, handleUpdatePicklist] + ); + + const handleReorderPicklist = useCallback( + async (sourceIndex: number, destIndex: number) => { + const currentPicklist = deliberation?.picklist ?? []; + const newPicklist = [...currentPicklist]; + const [removed] = newPicklist.splice(sourceIndex, 1); + newPicklist.splice(destIndex, 0, removed); + await handleUpdatePicklist(newPicklist); + }, + [deliberation, handleUpdatePicklist] + ); + + const value = useMemo(() => { + // Convert hyphenated category to underscore format for GraphQL keys + const categoryKey = hyphensToUnderscores(category) as + | 'innovation_project' + | 'robot_design' + | 'core_values'; + + // Step 1: Compute base team scores (category scores and GP) + const teamScores = division.teams.map(team => computeTeamScores(team)); + + // Step 2: Compute room metrics (aggregated scores per room) + const roomMetrics = computeRoomMetrics(teamScores, division.teams); + + // Step 3: Compute normalized scores and ranks + const enrichedTeams = division.teams.map((team, index) => { + const scores = teamScores[index]; + const normalizedScores = computeNormalizedScores( + scores, + roomMetrics, + team.judgingSession?.room.id + ); + const ranks = computeRanks(scores, teamScores); + const isEligible = computeEligibility(team, deliberation); + const rubricFields = getFlattenedRubricFields(team, categoryKey); + + return { + id: team.id, + number: team.number, + name: team.name, + affiliation: team.affiliation, + city: team.city, + region: team.region, + arrived: team.arrived, + disqualified: team.disqualified, + slug: team.slug, + room: team.judgingSession?.room ?? null, + scores, + normalizedScores, + ranks, + eligible: isEligible, + rubricFields, + rubricIds: { + 'innovation-project': team.rubrics.innovation_project?.id ?? null, + 'robot-design': team.rubrics.robot_design?.id ?? null, + 'core-values': team.rubrics.core_values?.id ?? null + }, + awardNominations: team.rubrics.core_values?.data?.awards ?? {}, + gpScores: (team.scoresheets || []) + .map(s => ({ round: s.round, score: s.data?.gp?.value ?? 3 })) + .sort((a, b) => a.round - b.round) + } as EnrichedTeam; + }); + + // Step 3a: Compute picklist and availability + const picklistLimit = Math.min( + MAX_PICKLIST_LIMIT, + Math.ceil(division.teams.length * PICKLIST_LIMIT_MULTIPLIER) + ); + const picklistTeamIds = deliberation?.picklist ?? []; + const picklistTeams = picklistTeamIds + .map(id => enrichedTeams.find(t => t.id === id)) + .filter((t): t is EnrichedTeam => t !== undefined); + const eligibleTeams = enrichedTeams.filter(t => t.eligible).map(t => t.id); + const availableTeams = eligibleTeams.filter(id => !picklistTeamIds.includes(id)); + + // Step 3b: Find suggested team (top-scoring available team) + let suggestedTeam: EnrichedTeam | null = null; + if (availableTeams.length > 0) { + const availableEnriched = availableTeams + .map(id => enrichedTeams.find(t => t.id === id)) + .filter((t): t is EnrichedTeam => t !== undefined); + + // Sort by score descending, tiebreak by normalized score + availableEnriched.sort((a, b) => { + const scoreDiff = b.scores[category] - a.scores[category]; + if (scoreDiff !== 0) return scoreDiff; + return b.normalizedScores[category] - a.normalizedScores[category]; + }); + + // Check if there's a clear top team (no tie) + if (availableEnriched.length > 1) { + const topTeam = availableEnriched[0]; + const secondTeam = availableEnriched[1]; + const isTie = + topTeam.scores[category] === secondTeam.scores[category] && + topTeam.normalizedScores[category] === secondTeam.normalizedScores[category]; + if (!isTie) { + suggestedTeam = topTeam; + } + } else if (availableEnriched.length === 1) { + suggestedTeam = availableEnriched[0]; + } + } + + return { + division, + deliberation: deliberation ?? null, + teams: enrichedTeams, + eligibleTeams, + availableTeams, + picklistTeams, + suggestedTeam, + picklistLimit, + startDeliberation: handleStartDeliberation, + updatePicklist: handleUpdatePicklist, + addToPicklist: handleAddToPicklist, + removeFromPicklist: handleRemoveFromPicklist, + reorderPicklist: handleReorderPicklist + }; + }, [ + division, + deliberation, + category, + handleStartDeliberation, + handleUpdatePicklist, + handleAddToPicklist, + handleRemoveFromPicklist, + handleReorderPicklist + ]); + + return {children}; +} + +export function useCategoryDeliberation(): DeliberationContextValue { + const context = useContext(DeliberationContext); + if (!context) { + throw new Error('useCategoryDeliberation must be used within a CategoryDeliberationProvider'); + } + return context; +} diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page-content.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page-content.tsx new file mode 100644 index 000000000..95a6a417c --- /dev/null +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page-content.tsx @@ -0,0 +1,452 @@ +'use client'; + +import { + Container, + Box, + Stack, + Paper, + Typography, + Chip, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Grid, + Divider +} from '@mui/material'; +import { useJudgingCategoryTranslations } from '@lems/localization'; +import { PageHeader } from '../../components/page-header'; +import { useCategoryDeliberation } from './deliberation-context'; + +interface CategoryDeliberationPageContentProps { + category: string; +} + +export default function CategoryDeliberationPageContent({ + category +}: CategoryDeliberationPageContentProps) { + const { getCategory } = useJudgingCategoryTranslations(); + const { division, deliberation } = useCategoryDeliberation(); + + if (!division) { + return null; + } + + const statusColor: Record = { + NOT_STARTED: 'default', + IN_PROGRESS: 'warning', + COMPLETED: 'success' + }; + + return ( + <> + + + + + {/* Division Information */} + + + Division + + + {division.name} + + + + {/* Deliberation Information */} + {deliberation && ( + + + Deliberation + + + + + + ID + + + {deliberation.id} + + + + + Status + + + + + + Category + + {deliberation.category} + + + + Started at + + + {deliberation.startTime + ? new Date(deliberation.startTime).toLocaleString() + : '—'} + + + + + )} + + {/* Picklist */} + {deliberation?.picklist && deliberation.picklist.length > 0 && ( + + + Picklist ({deliberation.picklist.length}) + + + + {deliberation.picklist.map((teamId: string, index: number) => { + const team = division.teams.find( + (t: (typeof division.teams)[0]) => t.id === teamId + ); + return ( + + + {index + 1}. {team?.number} - {team?.name} + + + ); + })} + + + )} + + {/* Teams Data */} + + + Teams ({division.teams.length}) + + + + {division.teams.map((team: (typeof division.teams)[0]) => ( + + {/* Team Header */} + + + + Team number + + {team.number} + + + + Team name + + {team.name} + + + + Afiliation + + {team.affiliation} + + + + Slug + + + {team.slug} + + + + + City + + {team.city} + + + + Region + + {team.region} + + + + Arrived + + + + + + Disqualified + + + + + + {/* Judging Session */} + {team.judgingSession && ( + <> + + + Judging Session + + + + + ID + + + {team.judgingSession.id} + + + + + Room + + + {team.judgingSession.room.name} (ID: {team.judgingSession.room.id}) + + + + + Status + + + + + + )} + + {/* Scoresheets */} + {team.scoresheets && team.scoresheets.length > 0 && ( + <> + + + Scoresheete ({team.scoresheets.length}) + + + + + + Round + Slug + Score + GP + + + + {team.scoresheets.map(scoresheet => ( + + {scoresheet.round} + + {scoresheet.slug} + + {scoresheet.data?.score ?? '—'} + + {scoresheet.data?.gp ? ( + + + Value: {scoresheet.data.gp.value ?? '—'} + + {scoresheet.data.gp.notes && ( + + Notes: {scoresheet.data.gp.notes} + + )} + + ) : ( + '—' + )} + + + ))} + +
+
+ + )} + + {/* Rubrics */} + {team.rubrics && ( + <> + + + Rubrics + + + {['innovation_project', 'robot_design', 'core_values'].map(rubricType => { + const rubric = team.rubrics[rubricType as keyof typeof team.rubrics]; + if (!rubric) return null; + + return ( + + + {rubricType.replace(/_/g, ' ').toUpperCase()} + + + + + ID + + + {rubric.id} + + + + + Status + + + + + + {rubric.data && ( + <> + {Object.keys(rubric.data.fields).length > 0 && ( + <> + + Fields: + + + {Object.entries(rubric.data.fields).map( + ([fieldName, fieldValue]) => ( + + + {fieldName}: + + + Value: {fieldValue.value ?? '—'} + {fieldValue.notes && ` (${fieldValue.notes})`} + + + ) + )} + + + )} + + {rubric.data.feedback && ( + <> + + Feedback: + + + + Great Job: + + + {rubric.data.feedback.greatJob} + + + Think About: + + + {rubric.data.feedback.thinkAbout} + + + + )} + + {rubric.data.awards && + Object.keys(rubric.data.awards).length > 0 && ( + <> + + Awards: + + + {Object.entries(rubric.data.awards).map( + ([awardName, isAwarded]) => ( + + {awardName}: {isAwarded ? '✓' : '✗'} + + ) + )} + + + )} + + )} + + ); + })} + + + )} +
+ ))} +
+
+
+
+ + ); +} diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx index e812f225f..4b9b88c52 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx @@ -2,29 +2,12 @@ import { useMemo } from 'react'; import { useParams } from 'next/navigation'; -import { - Container, - CircularProgress, - Box, - Stack, - Paper, - Typography, - Chip, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Grid, - Divider -} from '@mui/material'; +import { CircularProgress, Box } from '@mui/material'; import { JudgingCategory } from '@lems/types/judging'; import { hyphensToUnderscores } from '@lems/shared/utils'; -import { useJudgingCategoryTranslations } from '@lems/localization'; -import { PageHeader } from '../../components/page-header'; import { useEvent } from '../../../components/event-context'; import { usePageData } from '../../../hooks/use-page-data'; +import { CategoryDeliberationProvider } from './deliberation-context'; import { GET_CATEGORY_DELIBERATION, parseCategoryDeliberationData, @@ -33,9 +16,9 @@ import { createRubricUpdatedSubscription, createScoresheetUpdatedSubscription } from './graphql'; +import CategoryDeliberationPageContent from './page-content'; export default function CategoryDeliberationPage() { - const { getCategory } = useJudgingCategoryTranslations(); const { currentDivision } = useEvent(); const { category }: { category: JudgingCategory } = useParams(); @@ -76,409 +59,13 @@ export default function CategoryDeliberationPage() { ); } - const deliberation = division.judging.deliberation; - const statusColor: Record = { - NOT_STARTED: 'default', - IN_PROGRESS: 'warning', - COMPLETED: 'success' - }; - return ( - <> - - - - - {/* Division Information */} - - - Division - - - {division.name} - - - - {/* Deliberation Information */} - {deliberation && ( - - - Deliberation - - - - - - ID - - - {deliberation.id} - - - - - Status - - - - - - Category - - {deliberation.category} - - - - Started at - - - {deliberation.startTime - ? new Date(deliberation.startTime).toLocaleString() - : '—'} - - - - - )} - - {/* Picklist */} - {deliberation?.picklist && deliberation.picklist.length > 0 && ( - - - Picklist ({deliberation.picklist.length}) - - - - {deliberation.picklist.map((teamId: string, index: number) => { - const team = division.teams.find( - (t: (typeof division.teams)[0]) => t.id === teamId - ); - return ( - - - {index + 1}. {team?.number} - {team?.name} - - - ); - })} - - - )} - - {/* Teams Data */} - - - Teams ({division.teams.length}) - - - - {division.teams.map((team: (typeof division.teams)[0]) => ( - - {/* Team Header */} - - - - Team number - - {team.number} - - - - Team name - - {team.name} - - - - Afiliation - - {team.affiliation} - - - - Slug - - - {team.slug} - - - - - City - - {team.city} - - - - Region - - {team.region} - - - - Arrived - - - - - - Disqualified - - - - - - {/* Judging Session */} - {team.judgingSession && ( - <> - - - Judging Session - - - - - ID - - - {team.judgingSession.id} - - - - - Room - - - {team.judgingSession.room.name} (ID: {team.judgingSession.room.id}) - - - - - )} - - {/* Scoresheets */} - {team.scoresheets && team.scoresheets.length > 0 && ( - <> - - - Scoresheete ({team.scoresheets.length}) - - - - - - Round - Slug - Score - GP - - - - {team.scoresheets.map(scoresheet => ( - - {scoresheet.round} - - {scoresheet.slug} - - {scoresheet.data?.score ?? '—'} - - {scoresheet.data?.gp ? ( - - - Value: {scoresheet.data.gp.value ?? '—'} - - {scoresheet.data.gp.notes && ( - - Notes: {scoresheet.data.gp.notes} - - )} - - ) : ( - '—' - )} - - - ))} - -
-
- - )} - - {/* Rubrics */} - {team.rubrics && ( - <> - - - Rubrics - - - {['innovation_project', 'robot_design', 'core_values'].map(rubricType => { - const rubric = team.rubrics[rubricType as keyof typeof team.rubrics]; - if (!rubric) return null; - - return ( - - - {rubricType.replace(/_/g, ' ').toUpperCase()} - - - - - ID - - - {rubric.id} - - - - - Status - - - - - - {rubric.data && ( - <> - {Object.keys(rubric.data.fields).length > 0 && ( - <> - - Fields: - - - {Object.entries(rubric.data.fields).map( - ([fieldName, fieldValue]) => ( - - - {fieldName}: - - - Value: {fieldValue.value ?? '—'} - {fieldValue.notes && ` (${fieldValue.notes})`} - - - ) - )} - - - )} - - {rubric.data.feedback && ( - <> - - Feedback: - - - - Great Job: - - - {rubric.data.feedback.greatJob} - - - Think About: - - - {rubric.data.feedback.thinkAbout} - - - - )} - - {rubric.data.awards && - Object.keys(rubric.data.awards).length > 0 && ( - <> - - Awards: - - - {Object.entries(rubric.data.awards).map( - ([awardName, isAwarded]) => ( - - {awardName}: {isAwarded ? '✓' : '✗'} - - ) - )} - - - )} - - )} - - ); - })} - - - )} -
- ))} -
-
-
-
- + + + ); } diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/types.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/types.ts new file mode 100644 index 000000000..3fbf9650d --- /dev/null +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/types.ts @@ -0,0 +1,72 @@ +import { JudgingCategory } from '@lems/types/judging'; +import type { Division, JudgingDeliberation, Room } from './graphql/types'; + +export type MetricPerCategory = Record; + +/** + * Metrics aggregated across all teams in a room. + * Used to compute normalization factors. + */ +export interface RoomMetrics { + avgScores: MetricPerCategory; + teamCount: number; +} + +/** + * Aggregated room metrics indexed by room ID. + */ +export type RoomMetricsMap = Record; + +/** + * Enriched team data with all computed values for deliberation. + */ +export interface EnrichedTeam { + id: string; + number: string; + name: string; + affiliation: string; + city: string; + region: string; + arrived: boolean; + disqualified: boolean; + slug: string; + room: Room | null; + + scores: MetricPerCategory; + normalizedScores: MetricPerCategory; + ranks: MetricPerCategory; + + eligible: boolean; + + rubricFields: Record; + + rubricIds: { + 'innovation-project': string | null; + 'robot-design': string | null; + 'core-values': string | null; + }; + + awardNominations: Record; + + gpScores: Array<{ round: number; score: number }>; +} + +export interface DeliberationContextValue { + division: Division | null; + deliberation: JudgingDeliberation | null; + + teams: EnrichedTeam[]; + + eligibleTeams: string[]; + availableTeams: string[]; + picklistTeams: EnrichedTeam[]; + suggestedTeam: EnrichedTeam | null; + + picklistLimit: number; + + startDeliberation(): Promise; + updatePicklist(teamIds: string[]): Promise; + addToPicklist(teamId: string): Promise; + removeFromPicklist(teamId: string): Promise; + reorderPicklist(sourceIndex: number, destIndex: number): Promise; +} From 2e24273fc0115525208508bb759885f69ac27140 Mon Sep 17 00:00:00 2001 From: johnmeshulam <55348702+johnmeshulam@users.noreply.github.com> Date: Tue, 23 Dec 2025 22:29:13 +0200 Subject: [PATCH 3/4] First frontend draft --- .../[category]/components/controls-panel.tsx | 191 ++++++++ .../components/deliberation-grid.tsx | 77 +++ .../components/deliberation-table.tsx | 150 ++++++ .../[category]/components/picklist-panel.tsx | 246 ++++++++++ .../[category]/components/scores-chart.tsx | 146 ++++++ .../components/small-screen-block.tsx | 44 ++ .../deliberation/[category]/page-content.tsx | 452 ------------------ .../deliberation/[category]/page.tsx | 10 +- 8 files changed, 862 insertions(+), 454 deletions(-) create mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/controls-panel.tsx create mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-grid.tsx create mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-table.tsx create mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/picklist-panel.tsx create mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/scores-chart.tsx create mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/small-screen-block.tsx delete mode 100644 apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page-content.tsx diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/controls-panel.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/controls-panel.tsx new file mode 100644 index 000000000..ba542de10 --- /dev/null +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/controls-panel.tsx @@ -0,0 +1,191 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { + Stack, + Box, + Typography, + Button, + Autocomplete, + TextField, + Chip, + alpha, + useTheme +} from '@mui/material'; +import { PlayArrow, Lock } from '@mui/icons-material'; +import { useCategoryDeliberation } from '../deliberation-context'; + +export function ControlsPanel() { + const theme = useTheme(); + const { deliberation, teams, startDeliberation, availableTeams } = useCategoryDeliberation(); + const [selectedTeam1, setSelectedTeam1] = useState(null); + const [selectedTeam2, setSelectedTeam2] = useState(null); + + const handleStartDeliberation = useCallback(async () => { + await startDeliberation(); + }, [startDeliberation]); + + const isInProgress = deliberation?.status === 'in-progress'; + + const teamOptions = teams.map(t => ({ + label: `${t.number} - ${t.name}`, + value: t.id + })); + + return ( + + {/* Status Card */} + + + Status + + + {deliberation?.status === 'in-progress' ? 'In Progress' : 'Not Started'} + + + {!isInProgress ? ( + + ) : ( + + )} + + + {/* Comparison Card */} + + + Compare Teams + + + + o.value === selectedTeam1) || null} + onChange={(_, value) => setSelectedTeam1(value?.value || null)} + renderInput={params => } + size="small" + fullWidth + sx={{ flex: 1 }} + /> + o.value === selectedTeam2) || null} + onChange={(_, value) => setSelectedTeam2(value?.value || null)} + renderInput={params => } + size="small" + fullWidth + sx={{ flex: 1 }} + /> + + + + + Coming Soon + + + + {/* Available Teams Pool */} + + + Available Teams + + + + {availableTeams.length === 0 ? ( + + No available teams + + ) : ( + availableTeams.map(teamId => { + const team = teams.find(t => t.id === teamId); + if (!team) return null; + + return ( + + ); + }) + )} + + + + ); +} diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-grid.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-grid.tsx new file mode 100644 index 000000000..323057682 --- /dev/null +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-grid.tsx @@ -0,0 +1,77 @@ +'use client'; + +import { Grid, Box, Paper } from '@mui/material'; +import { DeliberationTable } from './deliberation-table'; +import { PicklistPanel } from './picklist-panel'; +import { ControlsPanel } from './controls-panel'; +import { ScoresChart } from './scores-chart'; + +export function DeliberationGrid() { + return ( + + {/* Main content grid */} + + {/* Left column - Data Table */} + + theme.shadows[2] + }} + > + + + + + {/* Right columns - Controls and Picklist */} + + {/* Controls Panel - Top */} + theme.shadows[2], + display: 'flex', + flexDirection: 'column', + overflow: 'hidden' + }} + > + + + + {/* Picklist Panel - Bottom */} + theme.shadows[2], + display: 'flex', + flexDirection: 'column', + overflow: 'hidden' + }} + > + + + + + + {/* Bottom - Chart */} + theme.shadows[2], + p: 2 + }} + > + + + + ); +} diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-table.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-table.tsx new file mode 100644 index 000000000..8809e3587 --- /dev/null +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-table.tsx @@ -0,0 +1,150 @@ +'use client'; + +import { useMemo } from 'react'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { useTheme, alpha, IconButton, Tooltip } from '@mui/material'; +import { OpenInNew, Add } from '@mui/icons-material'; +import { useCategoryDeliberation } from '../deliberation-context'; +import type { EnrichedTeam } from '../types'; + +export function DeliberationTable() { + const theme = useTheme(); + const { teams, suggestedTeam, picklistTeams, addToPicklist } = useCategoryDeliberation(); + + const pickedTeamIds = useMemo(() => new Set(picklistTeams.map(t => t.id)), [picklistTeams]); + + const columns: GridColDef[] = useMemo( + () => [ + { + field: 'rank', + headerName: 'Rank', + width: 60, + sortable: true, + filterable: false, + renderCell: params => params.row.ranks.total || '-' + }, + { + field: 'number', + headerName: 'Team #', + width: 100, + sortable: true, + filterable: false + }, + { + field: 'room', + headerName: 'Room', + width: 100, + sortable: true, + filterable: false, + renderCell: params => params.row.room?.name || '-' + }, + { + field: 'innovationProject', + headerName: 'Innovation', + width: 110, + sortable: true, + filterable: false, + renderCell: params => (params.row.normalizedScores['innovation-project'] || 0).toFixed(2) + }, + { + field: 'robotDesign', + headerName: 'Robot Design', + width: 110, + sortable: true, + filterable: false, + renderCell: params => (params.row.normalizedScores['robot-design'] || 0).toFixed(2) + }, + { + field: 'coreValues', + headerName: 'Core Values', + width: 110, + sortable: true, + filterable: false, + renderCell: params => (params.row.normalizedScores['core-values'] || 0).toFixed(2) + }, + { + field: 'total', + headerName: 'Total', + width: 100, + sortable: true, + filterable: false, + renderCell: params => (params.row.normalizedScores.total || 0).toFixed(2) + }, + { + field: 'actions', + type: 'actions', + headerName: 'Actions', + width: 80, + getActions: params => { + const team = params.row as EnrichedTeam; + const isPicked = pickedTeamIds.has(team.id); + + return [ + + + + + , + !isPicked && team.eligible ? ( + + addToPicklist(team.id)} + sx={{ color: theme.palette.success.main }} + > + + + + ) : null + ].filter(Boolean); + } + } + ], + [theme, pickedTeamIds, addToPicklist] + ); + + return ( + { + const team = params.row as EnrichedTeam; + if (suggestedTeam?.id === team.id) { + return 'suggested-team'; + } + if (pickedTeamIds.has(team.id)) { + return 'picked-team'; + } + return ''; + }} + sx={{ + width: '100%', + height: '100%', + '& .suggested-team': { + backgroundColor: alpha(theme.palette.warning.main, 0.15), + '&:hover': { + backgroundColor: alpha(theme.palette.warning.main, 0.25) + } + }, + '& .picked-team': { + backgroundColor: alpha(theme.palette.primary.main, 0.1), + '&:hover': { + backgroundColor: alpha(theme.palette.primary.main, 0.2) + } + }, + '& .MuiDataGrid-cell:focus': { + outline: 'none' + }, + '& .MuiDataGrid-row:hover': { + cursor: 'default' + } + }} + /> + ); +} diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/picklist-panel.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/picklist-panel.tsx new file mode 100644 index 000000000..d314b59c4 --- /dev/null +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/picklist-panel.tsx @@ -0,0 +1,246 @@ +'use client'; + +import { useCallback } from 'react'; +import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd'; +import { Box, Stack, Typography, Paper, IconButton, Tooltip, alpha, useTheme } from '@mui/material'; +import { Close, EmojiEvents, Add } from '@mui/icons-material'; +import { useCategoryDeliberation } from '../deliberation-context'; + +const MEDAL_COLORS = { + 0: '#FFD700', // Gold + 1: '#C0C0C0', // Silver + 2: '#CD7F32' // Bronze +}; + +export function PicklistPanel() { + const theme = useTheme(); + const { + picklistTeams, + suggestedTeam, + picklistLimit, + reorderPicklist, + addToPicklist, + removeFromPicklist + } = useCategoryDeliberation(); + + const handleDragEnd = useCallback( + async (result: DropResult) => { + const { source, destination, draggableId } = result; + + if (!destination) { + return; + } + + // If dropped in trash + if (destination.droppableId === 'picklist-trash') { + const teamId = draggableId.replace('picklist-item-', ''); + await removeFromPicklist(teamId); + return; + } + + // If dropped in picklist + if (destination.droppableId === 'picklist-items') { + if (source.droppableId === 'picklist-items' && source.index !== destination.index) { + await reorderPicklist(source.index, destination.index); + } + } + }, + [reorderPicklist, removeFromPicklist] + ); + + const canAddMore = picklistTeams.length < picklistLimit; + + return ( + + + {/* Header */} + + Picklist + + + {/* Suggested team slot */} + {suggestedTeam && canAddMore && !picklistTeams.find(t => t.id === suggestedTeam.id) && ( + + + + Suggested + + + {suggestedTeam.number} + + + + addToPicklist(suggestedTeam.id)} + sx={{ color: theme.palette.success.main }} + > + + + + + )} + + {/* Picklist items */} + + {(provided, snapshot) => ( + + {picklistTeams.length === 0 ? ( + + Drag teams here or use the Add button + + ) : ( + picklistTeams.map((team, index) => ( + + {(provided, snapshot) => ( + + + {index < 3 && ( + + )} + {index >= 3 && ( + + {index + 1} + + )} + + + {team.number} + + + {team.name} + + + + + removeFromPicklist(team.id)} + sx={{ color: theme.palette.error.main }} + > + + + + + )} + + )) + )} + {provided.placeholder} + + )} + + + {/* Limit indicator */} + + {picklistTeams.length} / {picklistLimit} + + + {/* Trash zone */} + + {(provided, snapshot) => ( + + + Drop to Remove + + {provided.placeholder} + + )} + + + + ); +} diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/scores-chart.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/scores-chart.tsx new file mode 100644 index 000000000..42c592771 --- /dev/null +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/scores-chart.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { useMemo } from 'react'; +import { + ComposedChart, + Bar, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer +} from 'recharts'; +import { Box, Typography, useTheme } from '@mui/material'; +import { blue, green, red } from '@mui/material/colors'; +import { useCategoryDeliberation } from '../deliberation-context'; +import { computeRoomMetrics } from '../deliberation-computation'; + +export function ScoresChart() { + const theme = useTheme(); + const { teams } = useCategoryDeliberation(); + + // Compute room metrics with useMemo to avoid unnecessary recalculations + const chartData = useMemo(() => { + const roomMetrics = computeRoomMetrics(teams.map(t => t.scores)); + + // Transform room metrics into chart-friendly format + const data = Object.entries(roomMetrics) + .sort(([, a], [, b]) => a.teamCount - b.teamCount) // Sort by team count for better visualization + .map(([roomId, metrics]) => { + const room = teams.find(t => t.room?.id === roomId)?.room; + + return { + name: room?.name || `Room ${roomId.slice(0, 4)}`, + 'innovation-project': parseFloat( + (metrics.avgScores['innovation-project'] || 0).toFixed(2) + ), + 'robot-design': parseFloat((metrics.avgScores['robot-design'] || 0).toFixed(2)), + 'core-values': parseFloat((metrics.avgScores['core-values'] || 0).toFixed(2)), + aggregate: parseFloat((metrics.avgScores.total || 0).toFixed(2)), + teamCount: metrics.teamCount + }; + }); + + return data; + }, [teams]); + + // Calculate domain for Y axis + const maxScore = useMemo(() => { + if (chartData.length === 0) return 100; + return Math.ceil( + Math.max( + ...chartData.map(d => + Math.max(d['innovation-project'], d['robot-design'], d['core-values'], d.aggregate) + ) + ) + ); + }, [chartData]); + + return ( + + + Room Scores Distribution + + + {chartData.length === 0 ? ( + + + No data available + + + ) : ( + + + + + + + + + + + + + { + if (typeof value === 'number') { + return value.toFixed(2); + } + return value; + }} + contentStyle={{ + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + borderRadius: '8px' + }} + /> + + + {/* Category bars */} + + + + + {/* Aggregate line */} + + + + )} + + ); +} diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/small-screen-block.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/small-screen-block.tsx new file mode 100644 index 000000000..6ab682ea8 --- /dev/null +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/small-screen-block.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { Container, Paper, Typography, Box, useTheme } from '@mui/material'; +import { useTranslations } from 'next-intl'; + +export const SmallScreenBlock = () => { + const t = useTranslations('pages.deliberations.category.small-screen-block'); + const theme = useTheme(); + + return ( + + + + + {t('title')} + + + {t('message')} + + + {t('minimum-width')} + + + + + ); +}; diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page-content.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page-content.tsx deleted file mode 100644 index 95a6a417c..000000000 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page-content.tsx +++ /dev/null @@ -1,452 +0,0 @@ -'use client'; - -import { - Container, - Box, - Stack, - Paper, - Typography, - Chip, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Grid, - Divider -} from '@mui/material'; -import { useJudgingCategoryTranslations } from '@lems/localization'; -import { PageHeader } from '../../components/page-header'; -import { useCategoryDeliberation } from './deliberation-context'; - -interface CategoryDeliberationPageContentProps { - category: string; -} - -export default function CategoryDeliberationPageContent({ - category -}: CategoryDeliberationPageContentProps) { - const { getCategory } = useJudgingCategoryTranslations(); - const { division, deliberation } = useCategoryDeliberation(); - - if (!division) { - return null; - } - - const statusColor: Record = { - NOT_STARTED: 'default', - IN_PROGRESS: 'warning', - COMPLETED: 'success' - }; - - return ( - <> - - - - - {/* Division Information */} - - - Division - - - {division.name} - - - - {/* Deliberation Information */} - {deliberation && ( - - - Deliberation - - - - - - ID - - - {deliberation.id} - - - - - Status - - - - - - Category - - {deliberation.category} - - - - Started at - - - {deliberation.startTime - ? new Date(deliberation.startTime).toLocaleString() - : '—'} - - - - - )} - - {/* Picklist */} - {deliberation?.picklist && deliberation.picklist.length > 0 && ( - - - Picklist ({deliberation.picklist.length}) - - - - {deliberation.picklist.map((teamId: string, index: number) => { - const team = division.teams.find( - (t: (typeof division.teams)[0]) => t.id === teamId - ); - return ( - - - {index + 1}. {team?.number} - {team?.name} - - - ); - })} - - - )} - - {/* Teams Data */} - - - Teams ({division.teams.length}) - - - - {division.teams.map((team: (typeof division.teams)[0]) => ( - - {/* Team Header */} - - - - Team number - - {team.number} - - - - Team name - - {team.name} - - - - Afiliation - - {team.affiliation} - - - - Slug - - - {team.slug} - - - - - City - - {team.city} - - - - Region - - {team.region} - - - - Arrived - - - - - - Disqualified - - - - - - {/* Judging Session */} - {team.judgingSession && ( - <> - - - Judging Session - - - - - ID - - - {team.judgingSession.id} - - - - - Room - - - {team.judgingSession.room.name} (ID: {team.judgingSession.room.id}) - - - - - Status - - - - - - )} - - {/* Scoresheets */} - {team.scoresheets && team.scoresheets.length > 0 && ( - <> - - - Scoresheete ({team.scoresheets.length}) - - - - - - Round - Slug - Score - GP - - - - {team.scoresheets.map(scoresheet => ( - - {scoresheet.round} - - {scoresheet.slug} - - {scoresheet.data?.score ?? '—'} - - {scoresheet.data?.gp ? ( - - - Value: {scoresheet.data.gp.value ?? '—'} - - {scoresheet.data.gp.notes && ( - - Notes: {scoresheet.data.gp.notes} - - )} - - ) : ( - '—' - )} - - - ))} - -
-
- - )} - - {/* Rubrics */} - {team.rubrics && ( - <> - - - Rubrics - - - {['innovation_project', 'robot_design', 'core_values'].map(rubricType => { - const rubric = team.rubrics[rubricType as keyof typeof team.rubrics]; - if (!rubric) return null; - - return ( - - - {rubricType.replace(/_/g, ' ').toUpperCase()} - - - - - ID - - - {rubric.id} - - - - - Status - - - - - - {rubric.data && ( - <> - {Object.keys(rubric.data.fields).length > 0 && ( - <> - - Fields: - - - {Object.entries(rubric.data.fields).map( - ([fieldName, fieldValue]) => ( - - - {fieldName}: - - - Value: {fieldValue.value ?? '—'} - {fieldValue.notes && ` (${fieldValue.notes})`} - - - ) - )} - - - )} - - {rubric.data.feedback && ( - <> - - Feedback: - - - - Great Job: - - - {rubric.data.feedback.greatJob} - - - Think About: - - - {rubric.data.feedback.thinkAbout} - - - - )} - - {rubric.data.awards && - Object.keys(rubric.data.awards).length > 0 && ( - <> - - Awards: - - - {Object.entries(rubric.data.awards).map( - ([awardName, isAwarded]) => ( - - {awardName}: {isAwarded ? '✓' : '✗'} - - ) - )} - - - )} - - )} - - ); - })} - - - )} -
- ))} -
-
-
-
- - ); -} diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx index 4b9b88c52..db3c2b6e2 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx @@ -4,10 +4,13 @@ import { useMemo } from 'react'; import { useParams } from 'next/navigation'; import { CircularProgress, Box } from '@mui/material'; import { JudgingCategory } from '@lems/types/judging'; +import { ResponsiveComponent } from '@lems/shared'; import { hyphensToUnderscores } from '@lems/shared/utils'; import { useEvent } from '../../../components/event-context'; import { usePageData } from '../../../hooks/use-page-data'; import { CategoryDeliberationProvider } from './deliberation-context'; +import { SmallScreenBlock } from './components/small-screen-block'; +import { DeliberationGrid } from './components/deliberation-grid'; import { GET_CATEGORY_DELIBERATION, parseCategoryDeliberationData, @@ -16,7 +19,6 @@ import { createRubricUpdatedSubscription, createScoresheetUpdatedSubscription } from './graphql'; -import CategoryDeliberationPageContent from './page-content'; export default function CategoryDeliberationPage() { const { currentDivision } = useEvent(); @@ -65,7 +67,11 @@ export default function CategoryDeliberationPage() { category={categoryEnum} division={division} > - + } + mobile={} + /> ); } From 7c8bc2274b0c7eb5ef4f47e48d42503b0f8d1096 Mon Sep 17 00:00:00 2001 From: johnmeshulam <55348702+johnmeshulam@users.noreply.github.com> Date: Tue, 23 Dec 2025 22:43:30 +0200 Subject: [PATCH 4/4] Move deliberations out of dashboard --- .../deliberation/[category]/components/controls-panel.tsx | 4 ++-- .../deliberation/[category]/components/deliberation-grid.tsx | 0 .../deliberation/[category]/components/deliberation-table.tsx | 0 .../deliberation/[category]/components/picklist-panel.tsx | 0 .../deliberation/[category]/components/scores-chart.tsx | 0 .../deliberation/[category]/components/small-screen-block.tsx | 0 .../deliberation/[category]/deliberation-computation.ts | 0 .../deliberation/[category]/deliberation-context.tsx | 0 .../deliberation/[category]/graphql/index.ts | 0 .../deliberation/[category]/graphql/mutations.ts | 0 .../deliberation/[category]/graphql/query.ts | 0 .../[category]/graphql/subscriptions/deliberation-updated.ts | 0 .../deliberation/[category]/graphql/subscriptions/index.ts | 0 .../[category]/graphql/subscriptions/rubric-updated.ts | 0 .../[category]/graphql/subscriptions/scoresheet-updated.ts | 0 .../[category]/graphql/subscriptions/team-arrived.ts | 0 .../deliberation/[category]/graphql/types.ts | 0 .../{(dashboard) => }/deliberation/[category]/layout.tsx | 2 +- .../{(dashboard) => }/deliberation/[category]/page.tsx | 4 ++-- .../{(dashboard) => }/deliberation/[category]/types.ts | 0 .../{(dashboard) => }/deliberation/final/layout.tsx | 0 .../(volunteer)/{(dashboard) => }/deliberation/final/page.tsx | 0 .../(volunteer)/{(dashboard) => }/deliberation/layout.tsx | 4 ++-- 23 files changed, 7 insertions(+), 7 deletions(-) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/components/controls-panel.tsx (98%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/components/deliberation-grid.tsx (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/components/deliberation-table.tsx (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/components/picklist-panel.tsx (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/components/scores-chart.tsx (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/components/small-screen-block.tsx (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/deliberation-computation.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/deliberation-context.tsx (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/graphql/index.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/graphql/mutations.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/graphql/query.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/graphql/subscriptions/deliberation-updated.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/graphql/subscriptions/index.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/graphql/subscriptions/rubric-updated.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/graphql/subscriptions/scoresheet-updated.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/graphql/subscriptions/team-arrived.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/graphql/types.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/layout.tsx (93%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/page.tsx (94%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/[category]/types.ts (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/final/layout.tsx (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/final/page.tsx (100%) rename apps/frontend/src/app/[locale]/lems/(volunteer)/{(dashboard) => }/deliberation/layout.tsx (68%) diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/controls-panel.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/controls-panel.tsx similarity index 98% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/controls-panel.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/controls-panel.tsx index ba542de10..616c243f5 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/controls-panel.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/controls-panel.tsx @@ -65,10 +65,10 @@ export function ControlsPanel() { startIcon={} onClick={handleStartDeliberation} sx={{ - bgcolor: 'white', + bgcolor: '#fff', color: theme.palette.primary.main, '&:hover': { - bgcolor: alpha('white', 0.9) + bgcolor: alpha('#fff', 0.9) }, fontWeight: 600 }} diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-grid.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/deliberation-grid.tsx similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-grid.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/deliberation-grid.tsx diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-table.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/deliberation-table.tsx similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/deliberation-table.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/deliberation-table.tsx diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/picklist-panel.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/picklist-panel.tsx similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/picklist-panel.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/picklist-panel.tsx diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/scores-chart.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/scores-chart.tsx similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/scores-chart.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/scores-chart.tsx diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/small-screen-block.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/small-screen-block.tsx similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/components/small-screen-block.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/small-screen-block.tsx diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-computation.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/deliberation-computation.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-computation.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/deliberation-computation.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-context.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/deliberation-context.tsx similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/deliberation-context.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/deliberation-context.tsx diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/index.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/index.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/index.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/index.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/mutations.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/mutations.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/mutations.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/mutations.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/query.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/query.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/query.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/query.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/subscriptions/deliberation-updated.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/subscriptions/deliberation-updated.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/subscriptions/deliberation-updated.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/subscriptions/deliberation-updated.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/subscriptions/index.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/subscriptions/index.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/subscriptions/index.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/subscriptions/index.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/subscriptions/rubric-updated.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/subscriptions/rubric-updated.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/subscriptions/rubric-updated.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/subscriptions/rubric-updated.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/subscriptions/scoresheet-updated.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/subscriptions/scoresheet-updated.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/subscriptions/scoresheet-updated.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/subscriptions/scoresheet-updated.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/subscriptions/team-arrived.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/subscriptions/team-arrived.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/subscriptions/team-arrived.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/subscriptions/team-arrived.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/types.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/types.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/graphql/types.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/graphql/types.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/layout.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/layout.tsx similarity index 93% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/layout.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/layout.tsx index f7e16d08e..44ef8c177 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/layout.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/layout.tsx @@ -3,7 +3,7 @@ import { redirect, useParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { toast } from 'react-hot-toast'; -import { useUser } from '../../../../components/user-context'; +import { useUser } from '../../../components/user-context'; interface RubricLayoutProps { children: React.ReactNode; diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/page.tsx similarity index 94% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/page.tsx index db3c2b6e2..79f920798 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/page.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/page.tsx @@ -6,8 +6,8 @@ import { CircularProgress, Box } from '@mui/material'; import { JudgingCategory } from '@lems/types/judging'; import { ResponsiveComponent } from '@lems/shared'; import { hyphensToUnderscores } from '@lems/shared/utils'; -import { useEvent } from '../../../components/event-context'; -import { usePageData } from '../../../hooks/use-page-data'; +import { useEvent } from '../../components/event-context'; +import { usePageData } from '../../hooks/use-page-data'; import { CategoryDeliberationProvider } from './deliberation-context'; import { SmallScreenBlock } from './components/small-screen-block'; import { DeliberationGrid } from './components/deliberation-grid'; diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/types.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/types.ts similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/[category]/types.ts rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/types.ts diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/final/layout.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/layout.tsx similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/final/layout.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/layout.tsx diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/final/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/page.tsx similarity index 100% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/final/page.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/page.tsx diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/layout.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/layout.tsx similarity index 68% rename from apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/layout.tsx rename to apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/layout.tsx index 4bf21a9e5..238b2ebaa 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/deliberation/layout.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/layout.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useUser } from '../../../components/user-context'; -import { authorizeUserRole } from '../../../lib/role-authorizer'; +import { useUser } from '../../components/user-context'; +import { authorizeUserRole } from '../../lib/role-authorizer'; export default function ScorekeeperLayout({ children }: { children: React.ReactNode }) { const user = useUser();