diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/awards/utils/schema.ts b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/awards/utils/schema.ts index 2e4c91cbb..1586a9d4d 100644 --- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/awards/utils/schema.ts +++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/awards/utils/schema.ts @@ -75,7 +75,7 @@ export function parseSchemaToApiRequest( const award = awardName as Award; const isPersonal = (PERSONAL_AWARDS as readonly string[]).includes(award); - const isOptional = !(OPTIONAL_AWARDS as readonly string[]).includes(award); + const isOptional = (OPTIONAL_AWARDS as readonly string[]).includes(award); const allowNominations = (CORE_VALUES_AWARDS as readonly string[]).includes(award); const automaticAssignment = (AUTOMATIC_ASSIGNMENT_AWARDS as readonly string[]).includes(award); const showPlaces = !(HIDE_PLACES as readonly string[]).includes(award); diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/advance-final-deliberation-stage.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/advance-final-deliberation-stage.ts index 1bc2ae1a9..b0744936f 100644 --- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/advance-final-deliberation-stage.ts +++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/advance-final-deliberation-stage.ts @@ -1,6 +1,5 @@ import { GraphQLFieldResolver } from 'graphql'; import { FinalDeliberationStage } from '@lems/database'; -import { OPTIONAL_AWARDS } from '@lems/shared/awards'; import { RedisEventTypes } from '@lems/types/api/lems/redis'; import { MutationError, MutationErrorCode } from '@lems/types/api/lems'; import type { GraphQLContext } from '../../../apollo-server'; @@ -83,10 +82,8 @@ export const advanceFinalDeliberationStageResolver: GraphQLFieldResolver< // Get awards for validation const awards = await db.awards.byDivisionId(divisionId).getAll(); - const hasOptionalAwards = awards.some(award => - (OPTIONAL_AWARDS as readonly string[]) - .filter(name => name !== 'excellence-in-engineering') - .includes(award.name) + const hasOptionalAwards = awards.some( + award => award.is_optional && award.name !== 'excellence-in-engineering' ); if (nextStage === 'optional-awards' && !hasOptionalAwards) { // Skip optional-awards stage if no optional awards exist diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/optional-awards.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/optional-awards.ts index 179654f18..2f6a2ef2c 100644 --- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/optional-awards.ts +++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/optional-awards.ts @@ -1,5 +1,4 @@ import { MutationError, MutationErrorCode } from '@lems/types/api/lems'; -import { OPTIONAL_AWARDS } from '@lems/shared/awards'; import { Award, FinalDeliberationAwards } from '@lems/database'; import db from '../../../../../database'; import { updateFinalDeliberationAwards } from './utils'; @@ -13,9 +12,7 @@ export function validateOptionalAwardsStage( ): Promise { const divisionOptionalAwards = divisionAwards.filter( award => - (OPTIONAL_AWARDS as readonly string[]) - .filter(name => name !== 'excellence-in-engineering') - .includes(award.name) && award.type === 'TEAM' + award.is_optional && award.name !== 'excellence-in-engineering' && award.type === 'TEAM' ); for (const award of divisionOptionalAwards) { if (!optionalAwards[award.name] || optionalAwards[award.name].length === 0) { diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/update-final-deliberation-awards.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/update-final-deliberation-awards.ts index 3ff8309ad..8288b4bd7 100644 --- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/update-final-deliberation-awards.ts +++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/update-final-deliberation-awards.ts @@ -1,7 +1,7 @@ import { GraphQLFieldResolver } from 'graphql'; import { RedisEventTypes } from '@lems/types/api/lems/redis'; import { MutationError, MutationErrorCode } from '@lems/types/api/lems'; -import { Award, OPTIONAL_AWARDS } from '@lems/shared/awards'; +import { Award } from '@lems/shared/awards'; import type { GraphQLContext } from '../../../apollo-server'; import db from '../../../../database'; import { getRedisPubSub } from '../../../../redis/redis-pubsub'; @@ -65,12 +65,18 @@ export const updateFinalDeliberationAwardsResolver: GraphQLFieldResolver< ); } + // Fetch awards from database to check is_optional field + const divisionAwards = await db.awards.byDivisionId(divisionId).getAll(); + const awardOptionalMap = new Map(divisionAwards.map(award => [award.name, award.is_optional])); + const optionalAwards: Partial> = {}; const mandatoryAwards: Partial> = {}; const championsAward: { '1'?: string; '2'?: string; '3'?: string; '4'?: string } = {}; Object.entries(awards).forEach(([awardName, awardData]) => { - if ((OPTIONAL_AWARDS as readonly string[]).includes(awardName)) { + const isOptional = awardOptionalMap.get(awardName) ?? false; + + if (isOptional) { if (awardName === 'champions') { Object.assign(championsAward, awardData); } else { diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/final-deliberation-grid.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/final-deliberation-grid.tsx index 5ec543598..76a41b551 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/final-deliberation-grid.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/final-deliberation-grid.tsx @@ -15,7 +15,6 @@ import { } from '@mui/material'; import { useTranslations } from 'next-intl'; import { useMemo } from 'react'; -import { OPTIONAL_AWARDS } from '@lems/shared'; import { ArrowBack } from '@mui/icons-material'; import { useRouter } from 'next/navigation'; import { useFinalDeliberation } from '../final-deliberation-context'; @@ -31,7 +30,7 @@ export const FinalDeliberationGrid: React.FC = () => { const t = useTranslations('pages.deliberations.final'); const theme = useTheme(); const router = useRouter(); - const { awardCounts, deliberation, anomalies } = useFinalDeliberation(); + const { deliberation, anomalies, deliberationAwards } = useFinalDeliberation(); // Determine visible stages based on whether optional awards exist const visibleStages = useMemo(() => { @@ -39,14 +38,12 @@ export const FinalDeliberationGrid: React.FC = () => { return STAGES; } - const hasOptionalAwards = Object.keys(awardCounts).some(award => - (OPTIONAL_AWARDS as readonly string[]) - .filter(name => name !== 'excellence-in-engineering') - .includes(award) + const hasOptionalAwards = deliberationAwards.some( + award => award.isOptional && award.name !== 'excellence-in-engineering' ); return hasOptionalAwards ? STAGES : STAGES.filter(stage => stage !== 'optional-awards'); - }, [awardCounts, deliberation]); + }, [deliberation, deliberationAwards]); // Get current stage index const currentStageIndex = useMemo(() => { diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-award-lists.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-award-lists.tsx index a5db80686..d942a96c9 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-award-lists.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-award-lists.tsx @@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react'; import { useTranslations } from 'next-intl'; import { Box, Paper, Stack, Typography, alpha, useTheme, IconButton, Tooltip } from '@mui/material'; import { Close } from '@mui/icons-material'; -import { Award, OPTIONAL_AWARDS } from '@lems/shared'; +import { Award } from '@lems/shared'; import { useAwardTranslations } from '@lems/localization'; import { useFinalDeliberation } from '../../final-deliberation-context'; import type { EnrichedTeam } from '../../types'; @@ -108,7 +108,7 @@ const AwardListItem: React.FC = ({ team, index, onRemove }) export const OptionalAwardsAwardLists: React.FC = () => { const theme = useTheme(); const t = useTranslations('pages.deliberations.final.optional-awards'); - const { teams, awards, awardCounts, updateAward } = useFinalDeliberation(); + const { teams, awards, awardCounts, updateAward, deliberationAwards } = useFinalDeliberation(); const { getName } = useAwardTranslations(); const handleRemoveAward = useCallback( @@ -123,10 +123,15 @@ export const OptionalAwardsAwardLists: React.FC = () => { // Filter out excellence-in-engineering from optional awards const displayedAwards = useMemo( () => - OPTIONAL_AWARDS.filter( - award => award !== 'excellence-in-engineering' && (awardCounts[award] ?? 0) > 0 - ) as Award[], - [awardCounts] + deliberationAwards + .filter( + award => + award.isOptional && + award.name !== 'excellence-in-engineering' && + (awardCounts[award.name as Award] ?? 0) > 0 + ) + .map(award => award.name as Award), + [awardCounts, deliberationAwards] ); const awardSelections = useMemo( diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-controls-panel.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-controls-panel.tsx index 378404028..e0b708a30 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-controls-panel.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-controls-panel.tsx @@ -5,7 +5,7 @@ import { Box, Button, LinearProgress, Stack, Typography, alpha, useTheme } from import dayjs from 'dayjs'; import { useTranslations } from 'next-intl'; import { useCallback, useMemo, useState } from 'react'; -import { OPTIONAL_AWARDS, Award, PERSONAL_AWARDS } from '@lems/shared'; +import { Award } from '@lems/shared'; import { Countdown } from '../../../../../../../../lib/time/countdown'; import { useTime } from '../../../../../../../../lib/time/hooks'; import { useFinalDeliberation } from '../../final-deliberation-context'; @@ -31,7 +31,7 @@ const getProgressColor = (progressPercent: number) => { export const OptionalAwardsControlsPanel: React.FC = () => { const theme = useTheme(); const t = useTranslations('pages.deliberations.final.optional-awards'); - const { deliberation, startDeliberation, awards, awardCounts, advanceStage } = + const { deliberation, startDeliberation, awards, awardCounts, advanceStage, deliberationAwards } = useFinalDeliberation(); const currentTime = useTime({ interval: 1000 }); const [isLoading, setIsLoading] = useState(false); @@ -59,20 +59,24 @@ export const OptionalAwardsControlsPanel: React.FC = () => { const isNotStarted = useMemo(() => !isInProgress && !isCompleted, [isInProgress, isCompleted]); // Check if all optional awards are filled (or have their max limit) + // Filter out personal awards and excellence-in-engineering const isOptionalAwardsComplete = useMemo( () => deliberation - ? OPTIONAL_AWARDS.filter( - award => - award !== 'excellence-in-engineering' && - !(PERSONAL_AWARDS as readonly string[]).includes(award) - ).every(award => { - const selectedCount = getAwardArray(awards, award as Award).length; - const maxCount = awardCounts[award as Award] ?? 0; - return selectedCount === maxCount; - }) + ? deliberationAwards + .filter( + award => + award.isOptional && + award.name !== 'excellence-in-engineering' && + award.type === 'TEAM' + ) + .every(award => { + const selectedCount = getAwardArray(awards, award.name as Award).length; + const maxCount = awardCounts[award.name as Award] ?? 0; + return selectedCount === maxCount; + }) : false, - [deliberation, awards, awardCounts] + [deliberation, awards, awardCounts, deliberationAwards] ); // Calculate timer values diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-data-grid.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-data-grid.tsx index cd8514b6d..003e0c9b5 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-data-grid.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-data-grid.tsx @@ -17,7 +17,7 @@ import { } from '@mui/material'; import { Stars, Add, CompareArrows } from '@mui/icons-material'; import { purple } from '@mui/material/colors'; -import { OPTIONAL_AWARDS, Award } from '@lems/shared'; +import { Award } from '@lems/shared'; import { useAwardTranslations } from '@lems/localization'; import { useFinalDeliberation } from '../../final-deliberation-context'; import type { EnrichedTeam } from '../../types'; @@ -29,8 +29,15 @@ export function OptionalAwardsDataGrid() { const theme = useTheme(); const t = useTranslations('pages.deliberations.final.optional-awards'); const tTable = useTranslations('pages.deliberations.category.table'); - const { teams, eligibleTeams, deliberation, awards, updateAward, awardCounts } = - useFinalDeliberation(); + const { + teams, + eligibleTeams, + deliberation, + awards, + updateAward, + awardCounts, + deliberationAwards + } = useFinalDeliberation(); const [anchorEl, setAnchorEl] = useState(null); const [selectedTeamForAward, setSelectedTeamForAward] = useState(null); const { getName } = useAwardTranslations(); @@ -72,13 +79,15 @@ export function OptionalAwardsDataGrid() { // Get teams that have been selected for any optional award const selectedTeamIds = useMemo>(() => { const set = new Set(); - OPTIONAL_AWARDS.filter(award => award !== 'excellence-in-engineering').forEach(award => { - const awardArray = awards[award] as string[]; - if (!awardArray) return; - awardArray.forEach(teamId => set.add(teamId)); - }); + deliberationAwards + .filter(award => award.isOptional && award.name !== 'excellence-in-engineering') + .forEach(award => { + const awardArray = awards[award.name as Award] as string[]; + if (!awardArray) return; + awardArray.forEach(teamId => set.add(teamId)); + }); return set; - }, [awards]); + }, [awards, deliberationAwards]); const handleOpenPopover = useCallback( (event: React.MouseEvent, teamId: string) => { @@ -290,10 +299,15 @@ export function OptionalAwardsDataGrid() { // Get all available awards that can be assigned const availableAwards: Award[] = useMemo( () => - OPTIONAL_AWARDS.filter( - award => award !== 'excellence-in-engineering' && (awardCounts[award] ?? 0) > 0 - ) as Award[], - [awardCounts] + deliberationAwards + .filter( + award => + award.isOptional && + award.name !== 'excellence-in-engineering' && + (awardCounts[award.name as Award] ?? 0) > 0 + ) + .map(award => award.name as Award), + [awardCounts, deliberationAwards] ); return ( diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/final-deliberation-context.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/final-deliberation-context.tsx index eefaf98e6..48fe557b3 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/final-deliberation-context.tsx +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/final-deliberation-context.tsx @@ -248,6 +248,7 @@ export const FinalDeliberationProvider = ({ categoryPicklists, awards, awardCounts, + deliberationAwards, roomMetrics, anomalies, startDeliberation: handleStartFinalDeliberation, @@ -261,6 +262,7 @@ export const FinalDeliberationProvider = ({ categoryPicklists, awards, awardCounts, + deliberationAwards, handleStartFinalDeliberation, handleUpdateFinalDeliberationAwards, handleAdvanceStage, diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/types.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/types.ts index a06050838..55173f593 100644 --- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/types.ts +++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/types.ts @@ -59,6 +59,7 @@ export interface FinalDeliberationContextValue { awards: DeliberationAwards; awardCounts: Partial>; + deliberationAwards: Array<{ name: string; isOptional: boolean; type: string }>; roomMetrics: RoomMetricsMap;