From 5e66b7061157a85a8148b3459be1622b24b81c11 Mon Sep 17 00:00:00 2001 From: "santasri.pachhal" Date: Wed, 29 Jul 2026 15:44:19 +0530 Subject: [PATCH] feat(ticket): scope issue preview to selected assignment lot --- src/app/(modules)/layout.tsx | 8 +- .../components/TicketAssignmentsCard.tsx | 100 ++++++++++-- .../TicketClassDetectionPreview.tsx | 54 ++++--- .../components/TicketDefectClassTabs.tsx | 146 ++++-------------- src/app/(modules)/ticket/[ticketId]/page.tsx | 20 ++- .../ticket/hooks/useTicketQueries.ts | 14 +- src/services/api/video.service.ts | 1 + src/types/detection.ts | 1 + 8 files changed, 166 insertions(+), 178 deletions(-) diff --git a/src/app/(modules)/layout.tsx b/src/app/(modules)/layout.tsx index b6d61b6..ba25eb9 100644 --- a/src/app/(modules)/layout.tsx +++ b/src/app/(modules)/layout.tsx @@ -13,11 +13,11 @@ const ModulesLayout = ({ -
-
-
+
+
+
-
{children}
+
{children}
diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketAssignmentsCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketAssignmentsCard.tsx index 8d5850c..1922b8c 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketAssignmentsCard.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketAssignmentsCard.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useEffect } from 'react'; import { BriefcaseBusiness, CalendarClock, ListChecks } from 'lucide-react'; import { PersonInfo } from '@/components/person-avatar'; @@ -14,6 +15,7 @@ import { } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { getDefectVisual } from '@/constants/defectVisualConfig'; +import { cn } from '@/lib/utils'; import type { TicketActor, TicketAssignmentSummary } from '@/types'; import { formatDate } from '@/utils/date'; @@ -22,6 +24,8 @@ import { useTicketAssignmentsQuery } from '../../hooks/useTicketQueries'; interface TicketAssignmentsCardProps { ticketId: string; initialAssignments?: TicketAssignmentSummary[]; + selectedAssignmentId?: number; + onAssignmentChange: (assignmentId: number) => void; } function formatLabel(value: string) { @@ -60,22 +64,22 @@ function AssignmentProgress({ const progress = total > 0 ? Math.min((approved / total) * 100, 100) : 0; return ( -
-
+
+
Repair progress {approved} of {total} approved
-
- +
+ {assignment.approved_detections} approved - + {assignment.pending_detections} pending - + {assignment.not_submitted_detections} not submitted
@@ -102,7 +106,7 @@ function AssignmentLot({ }; return ( -
+

Lot {assignment.id} @@ -110,7 +114,7 @@ function AssignmentLot({

-
+
{classNames.map((className) => ( ))} @@ -120,7 +124,7 @@ function AssignmentLot({ {assignment.item_count === 1 ? 'issue' : 'issues'} {assignment.due_at ? ( - + Due {formatDate(assignment.due_at)} @@ -135,18 +139,36 @@ function AssignmentLot({ export function TicketAssignmentsCard({ ticketId, initialAssignments, + selectedAssignmentId, + onAssignmentChange, }: TicketAssignmentsCardProps) { const assignmentsQuery = useTicketAssignmentsQuery( ticketId, initialAssignments, ); const assignments = assignmentsQuery.data?.items ?? []; + const selectedAssignment = + assignments.find((assignment) => assignment.id === selectedAssignmentId) ?? + assignments[0]; + const activeAssignmentId = selectedAssignment + ? String(selectedAssignment.id) + : ''; + const resolvedAssignmentId = selectedAssignment?.id; + + useEffect(() => { + if ( + resolvedAssignmentId !== undefined && + resolvedAssignmentId !== selectedAssignmentId + ) { + onAssignmentChange(resolvedAssignmentId); + } + }, [onAssignmentChange, resolvedAssignmentId, selectedAssignmentId]); return ( - - + +
-
+
Assignment lots @@ -160,7 +182,7 @@ export function TicketAssignmentsCard({ ) : null}
- + {assignmentsQuery.isLoading ? ( Array.from({ length: 2 }).map((_, index) => (
- ) : assignments.length ? ( - assignments.map((assignment) => ( - - )) + ) : selectedAssignment ? ( +
+ + +
+ +
+
) : (

No assignments yet

diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketClassDetectionPreview.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketClassDetectionPreview.tsx index 14e9cac..7cbc004 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketClassDetectionPreview.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketClassDetectionPreview.tsx @@ -13,15 +13,14 @@ import { cn } from '@/lib/utils'; import { useDiscardDetectionMutation, - useTicketClassDetectionsQuery, + useTicketDetectionsQuery, } from '../../hooks/useTicketQueries'; import { DetectionDiscardDialog } from './DetectionDiscardDialog'; interface TicketClassDetectionPreviewProps { ticketId: string; videoId?: string | null; - defectClassName: string; - displayName: string; + assignmentId?: number; totalCount?: number; } @@ -96,41 +95,45 @@ function TicketClassDetectionPreviewSkeleton() { export function TicketClassDetectionPreview({ ticketId, videoId, - defectClassName, - displayName, + assignmentId, totalCount, }: TicketClassDetectionPreviewProps) { const [currentIndex, setCurrentIndex] = useState(0); const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false); - const visual = getDefectVisual(defectClassName); - const IssueIcon = visual.icon; useEffect(() => { setCurrentIndex(0); - }, [defectClassName]); + }, [assignmentId, videoId]); const queryParams = useMemo( () => ({ - class_name: defectClassName, skip: currentIndex, limit: 1, + assignment_id: assignmentId, sort: 'timestamp_asc', }), - [currentIndex, defectClassName], + [assignmentId, currentIndex], ); - const detectionsQuery = useTicketClassDetectionsQuery( + const detectionsQuery = useTicketDetectionsQuery( videoId ?? undefined, queryParams, ); + const detectionResult = detectionsQuery.data; + const activeDetection = detectionResult?.items[0]; + const detectionCount = + detectionResult?.total ?? (assignmentId ? 0 : (totalCount ?? 0)); + const activeVisual = getDefectVisual( + activeDetection?.detection.class_name ?? '', + ); + const ActiveIssueIcon = activeVisual.icon; + const activeDisplayName = + activeDetection?.detection.display_name ?? 'Detection'; const discardMutation = useDiscardDetectionMutation( ticketId, videoId ?? undefined, - defectClassName, + activeDetection?.detection.class_name, ); - const detectionResult = detectionsQuery.data; - const activeDetection = detectionResult?.items[0]; - const detectionCount = detectionResult?.total ?? totalCount ?? 0; useEffect(() => { if (detectionCount === 0 && currentIndex !== 0) { @@ -153,27 +156,22 @@ export function TicketClassDetectionPreview({ - +
-

{displayName}

- +

{activeDisplayName}

+ {detectionCount} Detections

- Review detections in ascending timestamp order. + Review all detections in ascending timestamp order.

@@ -258,7 +256,7 @@ export function TicketClassDetectionPreview({ className="min-w-0" latitude={activeDetection.location.latitude} longitude={activeDetection.location.longitude} - label={`${displayName} - Frame ${activeDetection.frame.number}`} + label={`${activeDetection.detection.display_name} - Frame ${activeDetection.frame.number}`} title="Location on Map" variant="plain" aspectRatio="16 / 9" diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx index a3ce4dc..1dd1ae0 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx @@ -1,14 +1,9 @@ 'use client'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect } from 'react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { Component, ListChecks } from 'lucide-react'; -import { Badge } from '@/components/ui/badge'; import { Card, CardContent } from '@/components/ui/card'; -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { DEFECT_CLASS_STATUS_CONFIG } from '@/constants/defectClassStatus'; -import { cn } from '@/lib/utils'; import type { TicketOverviewDetail } from '@/types'; import { useTicketDefectClassesQuery } from '../../hooks/useTicketQueries'; @@ -17,148 +12,59 @@ import { TicketClassDetectionPreview } from './TicketClassDetectionPreview'; interface TicketDefectClassTabsProps { ticketId: string; ticket?: TicketOverviewDetail; + assignmentId?: number; } export function TicketDefectClassTabs({ ticketId, ticket, + assignmentId, }: TicketDefectClassTabsProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const defectClassParam = searchParams.get('defect_class'); const defectClassesQuery = useTicketDefectClassesQuery(ticketId); - const defectClasses = useMemo( - () => defectClassesQuery.data?.defect_classes ?? [], - [defectClassesQuery.data?.defect_classes], - ); - const [selectedClass, setSelectedClass] = useState(); useEffect(() => { + const defectClasses = defectClassesQuery.data?.defect_classes ?? []; + if (!defectClasses.length) return; const hasUrlClass = defectClasses.some( (item) => item.class_name === defectClassParam, ); - if (hasUrlClass) { - setSelectedClass(defectClassParam ?? undefined); - return; - } + if (hasUrlClass) return; const firstClass = defectClasses[0].class_name; const nextParams = new URLSearchParams(searchParams.toString()); nextParams.set('defect_class', firstClass); - setSelectedClass(firstClass); router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false }); - }, [defectClassParam, defectClasses, pathname, router, searchParams]); + }, [ + defectClassParam, + defectClassesQuery.data?.defect_classes, + pathname, + router, + searchParams, + ]); - const handleClassChange = (className: string) => { - const nextParams = new URLSearchParams(searchParams.toString()); - - nextParams.set('defect_class', className); - setSelectedClass(className); - router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false }); - }; - const selectedIssue = ticket?.ai_result.detections_by_class.find( - (item) => item.class_name === selectedClass, - ); - const selectedClassItem = defectClasses.find( - (item) => item.class_name === selectedClass, - ); - const previewDisplayName = - selectedIssue?.display_name ?? - selectedClassItem?.display_name ?? - selectedClass ?? - 'Detection'; const previewVideoId = ticket?.video_id ?? defectClassesQuery.data?.video_id; - if (defectClassesQuery.isLoading) { - return ( - - -
-
- {Array.from({ length: 3 }).map((_, index) => ( -
- ))} -
- - - ); - } - - if (defectClassesQuery.isError || !defectClasses.length || !selectedClass) { - return null; - } + if (!previewVideoId) return null; return ( - - - -
-
- -

Detected Issues

- - {defectClasses.length} - -
- - - {defectClasses.map((item) => { - const statusConfig = - DEFECT_CLASS_STATUS_CONFIG[item.assignment_status]; - - return ( - - - {item.display_name} - - - - {statusConfig.label} - - - ); - })} - -
- -
- -
-
-
-
+ + +
+ +
+
+
); } diff --git a/src/app/(modules)/ticket/[ticketId]/page.tsx b/src/app/(modules)/ticket/[ticketId]/page.tsx index 6bb429b..6f39459 100644 --- a/src/app/(modules)/ticket/[ticketId]/page.tsx +++ b/src/app/(modules)/ticket/[ticketId]/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState } from 'react'; import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { ArrowLeft } from 'lucide-react'; @@ -31,11 +32,14 @@ export default function TicketDetailPage() { const searchParams = useSearchParams(); const { ticketId } = useParams() as { ticketId: string }; const defectClass = searchParams.get('defect_class') ?? undefined; + const [selectedAssignmentId, setSelectedAssignmentId] = useState(); const overviewQuery = useTicketOverviewQuery(ticketId); const classDetailQuery = useTicketClassDetailQuery(ticketId, defectClass); const overview = overviewQuery.data; const classDetail = classDetailQuery.data; + const activeAssignmentId = + selectedAssignmentId ?? overview?.assignments?.[0]?.id; const isClassDetailLoading = Boolean( defectClass && classDetailQuery.isLoading, ); @@ -59,7 +63,7 @@ export default function TicketDetailPage() { } return ( -
+
{overview ? ( @@ -68,8 +72,8 @@ export default function TicketDetailPage() { )}
-
-
+
+
{overview ? ( ) : ( @@ -79,16 +83,22 @@ export default function TicketDetailPage() { ) : null} - + {isClassDetailLoading ? : null} {!isClassDetailLoading && classDetail ? ( ) : null}
-
+
{isClassDetailLoading ? : null} {!isClassDetailLoading && classDetail ? ( diff --git a/src/app/(modules)/ticket/hooks/useTicketQueries.ts b/src/app/(modules)/ticket/hooks/useTicketQueries.ts index 7c37315..f9c8e16 100644 --- a/src/app/(modules)/ticket/hooks/useTicketQueries.ts +++ b/src/app/(modules)/ticket/hooks/useTicketQueries.ts @@ -120,12 +120,14 @@ export function useTicketDetectionsQuery( () => ({ skip: params.skip ?? 0, limit: params.limit ?? 1, + assignment_id: params.assignment_id, class_name: params.class_name, min_confidence: params.min_confidence, sort: params.sort ?? 'timestamp_asc', search: params.search, }), [ + params.assignment_id, params.class_name, params.limit, params.min_confidence, @@ -220,7 +222,7 @@ export function useTicketClassReviewDetectionsQuery( export function useDiscardDetectionMutation( ticketId: string, videoId: string | undefined, - defectClass: string, + defectClass?: string, ) { const queryClient = useQueryClient(); @@ -244,9 +246,13 @@ export function useDiscardDetectionMutation( queryClient.invalidateQueries({ queryKey: ticketKeys.overview(ticketId), }), - queryClient.invalidateQueries({ - queryKey: ticketKeys.classDetail(ticketId, defectClass), - }), + ...(defectClass + ? [ + queryClient.invalidateQueries({ + queryKey: ticketKeys.classDetail(ticketId, defectClass), + }), + ] + : []), queryClient.invalidateQueries({ queryKey: ticketKeys.defectClasses(ticketId), }), diff --git a/src/services/api/video.service.ts b/src/services/api/video.service.ts index e037515..faf3077 100644 --- a/src/services/api/video.service.ts +++ b/src/services/api/video.service.ts @@ -73,6 +73,7 @@ export const videoService = { params: { skip: params?.skip ?? 0, limit: params?.limit ?? 1, + assignment_id: params?.assignment_id, class_name: params?.class_name, proof_status: params?.proof_status, min_confidence: params?.min_confidence, diff --git a/src/types/detection.ts b/src/types/detection.ts index 1483389..3784b1b 100644 --- a/src/types/detection.ts +++ b/src/types/detection.ts @@ -110,6 +110,7 @@ export type DetectionResultLog = DetectionResultItem & { export type VideoDetectionsParams = { skip?: number; limit?: number; + assignment_id?: number; class_name?: string | string[]; proof_status?: DetectionProofStatus; min_confidence?: number;