import { useState } from "react"; import { FlaskConical, Play, Send, CheckCircle, XCircle, RotateCcw, Loader2, Shield, ShieldCheck, AlertTriangle, MessageSquare, X, UserCheck, PauseCircle, PlayCircle, } from "lucide-react"; import { useMutation } from "@tanstack/react-query"; import { requestDiscussion } from "../../api/tests"; import type { Test, TestState, User, DataClassification } from "../../types/models"; import type { OperatorOut } from "../../api/users"; import LiveTimer from "./LiveTimer"; import DataClassificationBadge from "./DataClassificationBadge"; import AssigneeControl from "./AssigneeControl"; // ── Progress steps ───────────────────────────────────────────────── const PROGRESS_STEPS: { key: TestState; label: string }[] = [ { key: "draft", label: "Draft" }, { key: "red_executing", label: "Red Exec" }, { key: "red_review", label: "Red Review" }, { key: "blue_evaluating", label: "Blue Eval" }, { key: "blue_review", label: "Blue Review" }, { key: "in_review", label: "Review" }, { key: "validated", label: "Validated" }, ]; const STATE_INDEX: Record = { draft: 0, red_executing: 1, red_review: 2, blue_evaluating: 3, blue_review: 4, in_review: 5, validated: 6, rejected: -1, disputed: 5, // same step as in_review (still in validation phase) }; // ── Badge colors ─────────────────────────────────────────────────── const STATE_BADGE: Record = { draft: "bg-gray-800/50 text-gray-400 border-gray-600/30", red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30", red_review: "bg-amber-900/50 text-amber-400 border-amber-500/30", blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30", blue_review: "bg-purple-900/50 text-purple-400 border-purple-500/30", in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30", validated: "bg-green-900/50 text-green-400 border-green-500/30", rejected: "bg-red-900/50 text-red-400 border-red-500/30", disputed: "bg-amber-900/50 text-amber-400 border-amber-500/30", }; /** "Queued Blue Team" is a display-only label for blue_evaluating + unassigned. */ function getStateLabel(test: Test): string { if (test.state === "blue_evaluating" && !test.blue_work_started_at) { return "Queued Blue Team"; } return test.state.replace(/_/g, " "); } // ── Props ────────────────────────────────────────────────────────── interface TestDetailHeaderProps { test: Test; user: User | null; isTransitioning: boolean; onStartExecution: () => void; onSubmitRed: () => void; onSubmitBlue: () => void; onStartBlueWork: () => void; onOpenValidateModal: (side: "red" | "blue") => void; onOpenReviewModal: (side: "red" | "blue") => void; onOpenResolveDisputeModal: () => void; onReopen: () => void; onPauseTimer: () => void; onResumeTimer: () => void; isTogglingTimer: boolean; onHold: () => void; onResume: () => void; isTogglingHold: boolean; onUpdateClassification: (value: DataClassification) => void; isUpdatingClassification: boolean; operators: OperatorOut[]; onAssignOperator: ( field: "red_tech_assignee" | "blue_tech_assignee" | "red_reviewer_assignee" | "blue_reviewer_assignee", userId: string | null, ) => void; isAssigningOperator: boolean; } // ── Component ────────────────────────────────────────────────────── export default function TestDetailHeader({ test, user, isTransitioning, onStartExecution, onSubmitRed, onSubmitBlue, onStartBlueWork, onOpenValidateModal, onOpenReviewModal, onOpenResolveDisputeModal, onReopen, onPauseTimer, onResumeTimer, isTogglingTimer, onHold, onResume, isTogglingHold, onUpdateClassification, isUpdatingClassification, operators, onAssignOperator, isAssigningOperator, }: TestDetailHeaderProps) { const role = user?.role ?? ""; const currentIdx = STATE_INDEX[test.state]; const [showDiscussModal, setShowDiscussModal] = useState(false); const [discussionSent, setDiscussionSent] = useState(false); const [discussResult, setDiscussResult] = useState<{ username: string; email: string | null; role: string; } | null>(null); const discussMutation = useMutation({ mutationFn: () => requestDiscussion(test.id), onSuccess: (data) => { setDiscussResult({ username: data.rejector_username, email: data.rejector_email, role: data.rejector_role, }); setDiscussionSent(true); }, }); const formatDate = (d: string | null) => { if (!d) return null; return new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; // ── Contextual action buttons ──────────────────────────────────── const HOLDABLE_STATES: TestState[] = ["draft", "red_executing", "blue_evaluating"]; const canHold = HOLDABLE_STATES.includes(test.state) && (role === "red_tech" || role === "blue_tech"); const renderActions = () => { const buttons: React.ReactNode[] = []; // On Hold banner + Resume button (shown first when test is on hold) if (test.is_on_hold && canHold) { buttons.push( , ); return
{buttons}
; } // Red Team in draft -> Start Execution if ( test.state === "draft" && (role === "red_tech" || role === "red_lead") ) { buttons.push( , ); } // Red Team in red_executing -> Submit to Blue Team (requires ≥1 red evidence) if ( test.state === "red_executing" && (role === "red_tech" || role === "red_lead") ) { const hasRedEvidence = (test.red_evidences?.length ?? 0) > 0; buttons.push(
{!hasRedEvidence && ( ⚠ Upload evidence first )}
, ); } // Red Lead assigned to review this submission -> Review Red Submission if ( test.state === "red_review" && role === "red_lead" && test.red_reviewer_assignee === user?.id ) { buttons.push( , ); } // Blue Lead assigned to review this submission -> Review Blue Submission if ( test.state === "blue_review" && role === "blue_lead" && test.blue_reviewer_assignee === user?.id ) { buttons.push( , ); } // Blue Team in blue_evaluating: // - if not picked up yet: show "Start Evaluation" button // - if already picked up: show "Submit for Review" button if ( test.state === "blue_evaluating" && (role === "blue_tech" || role === "blue_lead") ) { if (!test.blue_work_started_at) { buttons.push( , ); } else { // Submit for Review requires ≥1 blue evidence const hasBlueEvidence = (test.blue_evidences?.length ?? 0) > 0; buttons.push(
{!hasBlueEvidence && ( ⚠ Upload evidence first )}
, ); } } // Red Lead in in_review -> Validate Red if ( test.state === "in_review" && role === "red_lead" && !test.red_validation_status ) { buttons.push( , ); } // Blue Lead in in_review -> Validate Blue if ( test.state === "in_review" && role === "blue_lead" && !test.blue_validation_status ) { buttons.push( , ); } // Disputed state: symmetric actions for both leads if (test.state === "disputed") { const isApproving = (role === "red_lead" && test.red_validation_status === "approved") || (role === "blue_lead" && test.blue_validation_status === "approved"); const isRejecting = (role === "red_lead" && test.red_validation_status === "rejected") || (role === "blue_lead" && test.blue_validation_status === "rejected"); const rejectingSide: "red" | "blue" = test.red_validation_status === "rejected" ? "red" : "blue"; if (isApproving) { buttons.push(
{/* Discussion request button — shows ✓ after sent */} {discussionSent ? ( Discussion Requested ✓ ) : ( )} {/* Change approving vote to rejected — routes to the team at fault */}

You approved — change your vote or request a discussion

, ); } if (isRejecting && !isApproving) { buttons.push(
{/* Change rejecting vote to approved */}

You rejected — change your vote if you agree after discussion

, ); } } // Leads on rejected -> Reopen if ( test.state === "rejected" && (role === "red_lead" || role === "blue_lead") ) { buttons.push( , ); } // On Hold button — appears alongside action buttons in pre-validation states if (canHold && !test.is_on_hold) { buttons.push( , ); } return buttons.length > 0 ? (
{buttons}
) : null; }; // ── Dual validation indicators ─────────────────────────────────── const renderValidationIndicators = () => { if (!["in_review", "validated", "rejected", "disputed"].includes(test.state)) { return null; } const redStatus = test.red_validation_status; const blueStatus = test.blue_validation_status; const indicator = (label: string, status: string | null) => { if (status === "approved") return ( {label}: Approved ); if (status === "rejected") return ( {label}: Rejected ); return ( {label}: Pending ); }; return (
{indicator("Red Lead", redStatus)} {indicator("Blue Lead", blueStatus)}
); }; // ── Live timer ─────────────────────────────────────────────────── const canControlTimer = (test.state === "red_executing" && (role === "red_tech" || role === "red_lead")) || (test.state === "blue_evaluating" && (role === "blue_tech" || role === "blue_lead")); const renderLiveTimer = () => { if (test.state === "red_executing" && test.red_started_at) { return ( ); } if (test.state === "blue_evaluating" && test.blue_work_started_at) { return ( ); } return null; }; // ── Render ─────────────────────────────────────────────────────── return (
{/* Top row: name + badge + actions */}

{test.name}

{getStateLabel(test)}

Created {formatDate(test.created_at)}

{renderValidationIndicators()}
{test.state === "red_review" && ( onAssignOperator("red_reviewer_assignee", userId)} size="lg" /> )} {test.state === "blue_review" && ( onAssignOperator("blue_reviewer_assignee", userId)} size="lg" /> )} onAssignOperator("red_tech_assignee", userId)} size="lg" /> onAssignOperator("blue_tech_assignee", userId)} size="lg" />
{renderLiveTimer()} {renderActions()}
{/* Disputed conflict banner */} {test.state === "disputed" && (

Validation Conflict — Leads Disagree

One lead approved and the other rejected this test. {test.red_validation_status === "rejected" && test.red_validation_notes && ( <> Red Lead's reason: "{test.red_validation_notes}" )} {test.blue_validation_status === "rejected" && test.blue_validation_notes && ( <> Blue Lead's reason: "{test.blue_validation_notes}" )}

The lead who approved should review the rejection reason and either change their vote or discuss with the other lead to resolve the disagreement.

)} {/* On Hold banner */} {test.is_on_hold && (

Test On Hold

{test.hold_reason && (

Reason: {test.hold_reason}

)}

This test is paused. No action required until it is resumed.

)} {/* Progress bar */} {test.state !== "rejected" && (
{PROGRESS_STEPS.map((step, idx) => { const isCompleted = idx < currentIdx; const isCurrent = idx === currentIdx; return (
{/* Connector left */} {idx > 0 && (
)} {/* Dot */}
{/* Connector right */} {idx < PROGRESS_STEPS.length - 1 && (
)}
{step.label}
); })}
)} {/* Rejected banner */} {test.state === "rejected" && (
This test was rejected and needs to be reopened to restart the workflow.
)} {/* Confirm My Validation — discussion request modal */} {showDiscussModal && (
{/* Header */}

Confirm Your Validation

{!discussResult ? (
{/* Conflict summary */}

⚠ Both leads must agree before a test can be finalised

You are confirming that your approval is correct and that the other lead's rejection should be reviewed.

{/* Other lead's rejection reason */} {(test.red_validation_status === "rejected" && test.red_validation_notes) && (

Red Lead's rejection reason

"{test.red_validation_notes}"

)} {(test.blue_validation_status === "rejected" && test.blue_validation_notes) && (

Blue Lead's rejection reason

"{test.blue_validation_notes}"

)} {/* What happens next */}

What happens when you send the discussion request:

  • The other lead receives a notification to contact you
  • The test remains in Disputed state
  • Either lead can change their vote at any time to resolve it
  • If both reject → test becomes Rejected; if both approve → Validated
{discussMutation.isError && (

Failed to send notification. Try again.

)}
) : ( /* Success state with contact info */

Discussion request sent

{discussResult?.role} has been notified that you want to discuss.

{/* Contact card */}

Contact details

{discussResult?.username} {discussResult?.role}
{discussResult?.email && ( ✉ {discussResult.email} )}

Reach out directly via the contact above or your team's communication channels (Slack, Teams, etc.) to resolve the disagreement. The test will remain in Disputed state until one lead changes their vote.

)} {/* Footer */}
{!discussResult && ( )}
)}
); }