From e94da54aa985388eee1ef4898d023689db3063b8 Mon Sep 17 00:00:00 2001 From: "santasri.pachhal" Date: Wed, 22 Jul 2026 14:46:34 +0530 Subject: [PATCH] feat(ticket): add issue-by-issue repair review page --- .../review/components/IssueEvidencePanel.tsx | 179 ++++++++++ .../review/components/IssueReviewAction.tsx | 216 ++++++++++++ .../review/components/IssueReviewHeader.tsx | 113 ++++++ .../review/components/IssueReviewQueue.tsx | 193 +++++++++++ .../review/components/IssueReviewSkeleton.tsx | 30 ++ .../review/components/reviewTypes.ts | 32 ++ .../[ticketId]/[defectClass]/review/page.tsx | 325 ++++++++++++++++++ .../components/TicketStatusActions.tsx | 4 +- .../actions/OpenRepairReviewAction.tsx | 37 ++ .../components/actions/ReviewRepairAction.tsx | 66 ---- .../ticket/hooks/useTicketQueries.ts | 108 ++++-- .../(modules)/ticket/queries/ticketKeys.ts | 11 + src/constants/apiRoutes.ts | 3 +- src/services/api/detection.service.ts | 15 + src/services/api/ticket.service.ts | 12 - src/services/api/video.service.ts | 1 + src/types/detection.ts | 17 + src/types/ticket/actions.ts | 6 - src/utils/routes.ts | 3 + 19 files changed, 1262 insertions(+), 109 deletions(-) create mode 100644 src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueEvidencePanel.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueReviewAction.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueReviewHeader.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueReviewQueue.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueReviewSkeleton.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/reviewTypes.ts create mode 100644 src/app/(modules)/ticket/[ticketId]/[defectClass]/review/page.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/actions/OpenRepairReviewAction.tsx delete mode 100644 src/app/(modules)/ticket/[ticketId]/components/actions/ReviewRepairAction.tsx diff --git a/src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueEvidencePanel.tsx b/src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueEvidencePanel.tsx new file mode 100644 index 0000000..4be1d3f --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueEvidencePanel.tsx @@ -0,0 +1,179 @@ +import { CalendarCheck2, CheckCircle2, ImageIcon, MapPin } from 'lucide-react'; + +import DetectionLocationMap from '@/components/map/detectionLocationMap'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import AnnotatedDetectionImage from '@/components/video/annotatedDetectionImage'; +import { cn } from '@/lib/utils'; +import type { DetectionResultItem } from '@/types'; +import { formatDate } from '@/utils/date'; + +import { RepairProofGallery } from '../../../components/repair/RepairProofGallery'; +import { formatVideoTimestamp, type IssueReviewStatus } from './reviewTypes'; + +const REVIEW_STATUS_LABEL: Record = { + not_submitted: 'Awaiting Repair', + pending: 'Under Review', + approved: 'Approved', + rejected: 'Rejected', +}; + +const REVIEW_STATUS_CLASS: Record = { + not_submitted: 'bg-zinc-500/10 text-zinc-600 dark:text-zinc-300', + pending: 'bg-violet-500/10 text-violet-600 dark:text-violet-400', + approved: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', + rejected: 'bg-destructive/10 text-destructive', +}; + +function MetadataItem({ + label, + value, +}: { + label: string; + value: React.ReactNode; +}) { + return ( +
+

{label}

+
+ {value} +
+
+ ); +} + +interface IssueEvidencePanelProps { + detection: DetectionResultItem; + issueNumber: number; + videoWidth: number; + videoHeight: number; + reviewStatus: IssueReviewStatus; +} + +export function IssueEvidencePanel({ + detection, + issueNumber, + videoWidth, + videoHeight, + reviewStatus, +}: IssueEvidencePanelProps) { + const repairProof = detection.repair_proof; + const displayName = detection.detection.display_name; + const confidence = Math.round(detection.detection.confidence * 100); + + return ( +
+ + +
+ + {displayName} #{issueNumber} + + + {REVIEW_STATUS_LABEL[reviewStatus]} + +
+
+ +
+ + + + +
+
+
+ +
+
+

+ + Detection Image (Before) +

+ +
+ +
+

+ + Detection Location +

+ +
+
+ +
+
+

+ + Repair Proof (After Repair) +

+ {repairProof?.submitted ? ( + + + Repair Submitted + + ) : null} +
+
+ {!repairProof?.submitted ? ( +
+

+ Repair proof is not submitted yet. +

+

+ This issue cannot be reviewed until repair evidence is + available. +

+
+ ) : ( + <> + {repairProof.submitted_at ? ( +
+

+ Submitted {formatDate(repairProof.submitted_at)} +

+
+ ) : null} + + + +
+

+ Repair Notes +

+

+ {repairProof.notes?.trim() || + 'No repair notes were provided.'} +

+
+ + )} +
+
+
+ ); +} diff --git a/src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueReviewAction.tsx b/src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueReviewAction.tsx new file mode 100644 index 0000000..336a4a2 --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueReviewAction.tsx @@ -0,0 +1,216 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { + Check, + CheckCircle2, + ClipboardCheck, + Info, + Loader2, + X, + XCircle, +} from 'lucide-react'; + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; +import type { + DetectionResultItem, + ReviewDetectionRepairProofPayload, +} from '@/types'; +import { formatDate } from '@/utils/date'; + +import type { IssueReviewStatus } from './reviewTypes'; + +type ReviewDecision = ReviewDetectionRepairProofPayload['action']; + +interface IssueReviewActionProps { + detection: DetectionResultItem; + issueLabel: string; + status: IssueReviewStatus; + isPending: boolean; + onSave: (payload: ReviewDetectionRepairProofPayload) => Promise; +} + +export function IssueReviewAction({ + detection, + issueLabel, + status, + isPending, + onSave, +}: IssueReviewActionProps) { + const [comment, setComment] = useState(''); + const [decision, setDecision] = useState(); + + useEffect(() => { + setComment(''); + setDecision(undefined); + }, [detection.detection.id]); + + if (status === 'not_submitted') { + return ( + + + + + Review & Action + + + +
+ +

Repair proof required

+

+ {issueLabel} is not ready for review. +

+
+
+
+ ); + } + + if (status === 'approved' || status === 'rejected') { + const isApproved = status === 'approved'; + const repairProof = detection.repair_proof; + + return ( + + + + + Review Result + + + +
+ {isApproved ? ( + + ) : ( + + )} + + {isApproved ? 'Approved' : 'Rejected'} + + {repairProof?.reviewed_by_name || repairProof?.reviewed_at ? ( +

+ {repairProof.reviewed_by_name + ? `By ${repairProof.reviewed_by_name}` + : 'Reviewed'} + {repairProof.reviewed_at + ? ` on ${formatDate(repairProof.reviewed_at)}` + : ''} +

+ ) : null} +
+ + {repairProof?.review_comment ? ( +
+

Review Comment

+

+ {repairProof.review_comment} +

+
+ ) : null} +
+
+ ); + } + + const submitDecision = async (nextDecision: ReviewDecision) => { + setDecision(nextDecision); + + try { + await onSave({ + action: nextDecision, + comment: comment.trim() || undefined, + }); + } catch { + // The mutation owns user-facing error feedback. + } + }; + + return ( + + + + + Review & Action + + + +
+ +