refactor(awards): remove AWARD_MASTER role, fold features into jury chair flow
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m5s

The AWARD_MASTER role split sponsor jurors into a parallel UI that hid
project files (only showed when the award was anchored to an evaluation
round) and duplicated the jury voting path with no real difference in
authority — tie-break and finalize were already governed by AwardJuror.isChair
regardless of the user's global role. Inviting a juror via the award page
defaulted to AWARD_MASTER, randomly fragmenting jury panels.

This collapses the role into JURY_MEMBER + isChair:

- specialAward.getMyAwardDetail now returns evaluation scores, chair
  visibility into other jurors' votes, and juror roster
- specialAward.submitVote accepts an optional justification per vote
- specialAward.confirmWinner moves from awardMasterProcedure to
  protectedProcedure (juror+chair check inside)
- bulkInviteJurors creates JURY_MEMBER accounts and, when the award has
  a juryGroupId, also adds them to that JuryGroup so they appear on
  the round-page jury panel
- jury award page renders justification, eval-score badges, and a
  chair tools panel with vote tally + finalize-winner CTA
- juryGroup.list includes attached SpecialAwards; the jury-list UI
  shows a trophy pill alongside round pills
- (award-master) route group, awardMasterProcedure, AWARD_MASTER role
  enum value, and AWARD_MASTER_DECISION decisionMode are deleted
- migration demotes any residual AWARD_MASTER users to JURY_MEMBER and
  recreates the UserRole enum without the value

Coup de Coeur on prod: Didier (the sponsor juror added today as
AWARD_MASTER by the buggy invite form) was migrated to JURY_MEMBER and
attached to the existing "Coup de Coeur" JuryGroup; the SpecialAward
itself was linked to that group (juryGroupId was NULL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-05-07 15:21:09 +02:00
parent a9116b5833
commit 7bc2b84d1d
26 changed files with 344 additions and 912 deletions

View File

@@ -35,7 +35,7 @@ import {
import { Textarea } from '@/components/ui/textarea'
import { toast } from 'sonner'
import { cn, formatEnumLabel } from '@/lib/utils'
import { Plus, Scale, Users, Loader2, ArrowRight, CircleDot } from 'lucide-react'
import { Plus, Scale, Users, Loader2, ArrowRight, CircleDot, Trophy } from 'lucide-react'
const capModeLabels = {
HARD: 'Hard Cap',
@@ -301,10 +301,10 @@ function CompetitionJuriesSection({ competition }: CompetitionJuriesSectionProps
</div>
</div>
{/* Round assignments */}
{(group as any).rounds?.length > 0 && (
{/* Round + Special-award assignments */}
{((group as any).rounds?.length > 0 || (group as any).awards?.length > 0) && (
<div className="flex flex-wrap gap-1.5">
{(group as any).rounds.map((r: any) => (
{(group as any).rounds?.map((r: any) => (
<Badge
key={r.id}
variant="outline"
@@ -319,6 +319,21 @@ function CompetitionJuriesSection({ competition }: CompetitionJuriesSectionProps
{r.name}
</Badge>
))}
{(group as any).awards?.map((a: any) => (
<Badge
key={a.id}
variant="outline"
className={cn(
'text-[10px] gap-1',
a.status === 'VOTING_OPEN' && 'border-amber-300 bg-amber-50 text-amber-700',
a.status === 'CLOSED' && 'border-emerald-300 bg-emerald-50 text-emerald-700',
(a.status === 'DRAFT' || a.status === 'NOMINATIONS_OPEN') && 'border-slate-200 text-slate-500',
)}
>
<Trophy className="h-2.5 w-2.5" />
{a.name}
</Badge>
))}
</div>
)}