From bfa9fb5c8313b4b4aa2aea0dc0967b6922285ad1 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 27 Apr 2026 14:47:32 +0200 Subject: [PATCH] feat: reset to system-calculated ranking order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 'Reset to system order' button per category (Startups and Business Concepts). The button only appears when admins have drag-reordered that category — otherwise there's nothing to reset. Clicking it wipes that category's reorder history from the snapshot's reordersJson via a new ranking.clearReorders mutation, after which the dashboard re-initializes and falls back to the live composite ranking (balanced/raw score, optionally blended with balanced pass rate per the toggles). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../admin/round/ranking-dashboard.tsx | 31 ++++++++++++++++++- src/server/routers/ranking.ts | 26 ++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/components/admin/round/ranking-dashboard.tsx b/src/components/admin/round/ranking-dashboard.tsx index ac42464..1bd64e0 100644 --- a/src/components/admin/round/ranking-dashboard.tsx +++ b/src/components/admin/round/ranking-dashboard.tsx @@ -350,6 +350,15 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran // Do NOT invalidate getSnapshot — would reset localOrder }) + const clearReordersMutation = trpc.ranking.clearReorders.useMutation({ + onSuccess: () => { + toast.success('Reverted to system-calculated order') + initialized.current = false // allow re-init on next snapshot load + void utils.ranking.getSnapshot.invalidate() + }, + onError: (err) => toast.error(`Failed to reset: ${err.message}`), + }) + const updateRoundMutation = trpc.round.update.useMutation({ onSuccess: () => { toast.success('Ranking config saved') @@ -953,7 +962,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran {/* Per-category sections */} {(['STARTUP', 'BUSINESS_CONCEPT'] as const).map((category) => ( - + {categoryLabels[category]} {evalConfig && evalConfig.advanceMode === 'threshold' && evalConfig.advanceScoreThreshold != null ? ( @@ -966,6 +975,26 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran ) : null} + {(() => { + const reorders = (snapshot?.reordersJson as Array<{ + category: 'STARTUP' | 'BUSINESS_CONCEPT' + }> | null) ?? [] + const hasManualOrder = reorders.some((r) => r.category === category) + if (!hasManualOrder || !latestSnapshotId) return null + return ( + + ) + })()} {localOrder[category].length === 0 ? ( diff --git a/src/server/routers/ranking.ts b/src/server/routers/ranking.ts index a2cb1ab..39c66f3 100644 --- a/src/server/routers/ranking.ts +++ b/src/server/routers/ranking.ts @@ -264,6 +264,32 @@ export const rankingRouter = router({ return { ok: true } }), + /** Clear all manual drag-reorders for a snapshot, optionally filtered by + * category. The next dashboard render falls back to the system-computed + * order. Append-only history of reorders is wiped — that's the point. */ + clearReorders: adminProcedure + .input( + z.object({ + snapshotId: z.string(), + category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']).optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + const snapshot = await ctx.prisma.rankingSnapshot.findUniqueOrThrow({ + where: { id: input.snapshotId }, + select: { reordersJson: true }, + }) + const existing = (snapshot.reordersJson as ReorderEvent[] | null) ?? [] + const next = input.category + ? existing.filter((r) => r.category !== input.category) + : [] + await ctx.prisma.rankingSnapshot.update({ + where: { id: input.snapshotId }, + data: { reordersJson: next as unknown as Prisma.InputJsonValue }, + }) + return { ok: true, cleared: existing.length - next.length } + }), + /** * RANK-09 — Manual trigger for auto-rank (admin button on round detail page). * Reads ranking criteria from round configJson and executes quickRank.