From 19c4866103a5dd190d3c452ae77f1d885439119a Mon Sep 17 00:00:00 2001 From: kitos Date: Mon, 6 Jul 2026 12:40:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(tests):=20dispute=20resolution=20UI=20?= =?UTF-8?q?=E2=80=94=20manager=20escalation=20+=20red/blue=20queue=20selec?= =?UTF-8?q?tor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/tests.ts | 13 ++ .../test-detail/ResolveDisputeModal.tsx | 112 ++++++++++++++++++ .../test-detail/TestDetailHeader.tsx | 8 +- frontend/src/pages/TestDetailPage.tsx | 29 +++++ 4 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/test-detail/ResolveDisputeModal.tsx diff --git a/frontend/src/api/tests.ts b/frontend/src/api/tests.ts index 0c789a5..5f12da4 100644 --- a/frontend/src/api/tests.ts +++ b/frontend/src/api/tests.ts @@ -248,6 +248,19 @@ export async function validateAsBlueLead( return data; } +// ── Dispute resolution ─────────────────────────────────────────────── + +export interface ResolveDisputePayload { + target_team: "red" | "blue"; + notes?: string; +} + +/** The approving lead flips to reject, choosing which team must redo the work. */ +export async function resolveDispute(testId: string, payload: ResolveDisputePayload): Promise { + const { data } = await client.post(`/tests/${testId}/resolve-dispute`, payload); + return data; +} + // ── Reopen ───────────────────────────────────────────────────────── /** Reopen a rejected test — moves back to draft. */ diff --git a/frontend/src/components/test-detail/ResolveDisputeModal.tsx b/frontend/src/components/test-detail/ResolveDisputeModal.tsx new file mode 100644 index 0000000..c279b90 --- /dev/null +++ b/frontend/src/components/test-detail/ResolveDisputeModal.tsx @@ -0,0 +1,112 @@ +import { useState } from "react"; +import { Shield, ShieldCheck, Loader2, X, XCircle } from "lucide-react"; + +// ── Props ────────────────────────────────────────────────────────── + +interface ResolveDisputeModalProps { + isSubmitting: boolean; + onSubmit: (targetTeam: "red" | "blue", notes: string) => void; + onClose: () => void; +} + +// ── Component ────────────────────────────────────────────────────── + +export default function ResolveDisputeModal({ + isSubmitting, + onSubmit, + onClose, +}: ResolveDisputeModalProps) { + const [targetTeam, setTargetTeam] = useState<"red" | "blue" | null>(null); + const [notes, setNotes] = useState(""); + + const canSubmit = targetTeam !== null && !isSubmitting; + + return ( +
+
+ {/* Header */} +
+
+ +

Change Vote to Rejected

+
+ +
+ + {/* Body */} +
+

+ You're agreeing that this test needs rework. Instead of restarting from scratch, + pick which team's work is actually the problem — the test goes straight back to + their queue and the other team's work is left untouched. +

+ +
+

Send back to

+
+ + +
+
+ +
+ +