diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketAssignmentsCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketAssignmentsCard.tsx index b1abce7..19f6cec 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketAssignmentsCard.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketAssignmentsCard.tsx @@ -20,9 +20,11 @@ import type { TicketActor, TicketAssignmentSummary } from '@/types'; import { formatDate } from '@/utils/date'; import { useTicketAssignmentsQuery } from '../../hooks/useTicketQueries'; +import { TicketDetectionPreview } from './TicketDetectionPreview'; interface TicketAssignmentsCardProps { ticketId: string; + videoId?: string | null; initialAssignments?: TicketAssignmentSummary[]; selectedAssignmentId?: number; onAssignmentChange: (assignmentId: number) => void; @@ -87,7 +89,7 @@ function AssignmentProgress({ ); } -function AssignmentLot({ +function AssignmentOverview({ assignment, }: { assignment: TicketAssignmentSummary; @@ -109,7 +111,7 @@ function AssignmentLot({

- Lot {assignment.id} + Assignment {assignment.id}

@@ -138,6 +140,7 @@ function AssignmentLot({ export function TicketAssignmentsCard({ ticketId, + videoId, initialAssignments, selectedAssignmentId, onAssignmentChange, @@ -173,14 +176,17 @@ export function TicketAssignmentsCard({
- Assignment lots + Work Assignments - Live ownership, criteria, and repair progress for this ticket. + Issues grouped by assignee, scope, and repair progress.
{assignmentsQuery.data ? ( - {assignments.length} + + {assignments.length}{' '} + {assignments.length === 1 ? 'Assignment' : 'Assignments'} + ) : null}
@@ -195,7 +201,7 @@ export function TicketAssignmentsCard({ ) : assignmentsQuery.isError ? (

- Unable to load assignment lots. + Unable to load work assignments.

); })} @@ -252,14 +258,23 @@ export function TicketAssignmentsCard({ aria-labelledby={`assignment-tab-${activeAssignmentId}`} className="min-w-0" > - + + {videoId ? ( +
+ +
+ ) : null}
) : (
-

No assignments yet

+

No work assignments yet

- Assignment lots will appear here after work is allocated. + Work assignments will appear here after issues are allocated.

)} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx index b7f9a76..b30696c 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx @@ -3,6 +3,7 @@ import { ArrowLeft } from 'lucide-react'; import { useRouter } from 'next/navigation'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { PERMISSIONS } from '@/constants/permissions'; import { PermissionGuard } from '@/guards'; @@ -17,7 +18,7 @@ export function TicketDetailHeader({ ticket: TicketOverviewDetail; }) { const router = useRouter(); - const ticketLabel = ticket.ticket_name || ticket.id; + const ticketName = ticket.ticket_name || 'Unnamed ticket'; return (
@@ -32,11 +33,17 @@ export function TicketDetailHeader({ > -
+

Ticket Detail

-

Ticket: {ticketLabel}

+ + {ticketName} +
diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketDetectionPreview.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketDetectionPreview.tsx index 66848dd..8b6cc28 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketDetectionPreview.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketDetectionPreview.tsx @@ -4,7 +4,6 @@ import { useEffect, useMemo, useState } from 'react'; import { AlertTriangle, ChevronLeft, ChevronRight, Trash2 } from 'lucide-react'; import DetectionLocationMap from '@/components/map/detectionLocationMap'; -import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import AnnotatedDetectionImage from '@/components/video/annotatedDetectionImage'; @@ -20,7 +19,9 @@ import { DetectionDiscardDialog } from './DetectionDiscardDialog'; interface TicketDetectionPreviewProps { ticketId: string; videoId?: string | null; - assignmentId: number; + assignmentId?: number; + assignmentStatus?: 'unassigned'; + onDetectionCountChange?: (count: number | undefined) => void; } function DetectionMetadataBar({ @@ -95,22 +96,28 @@ export function TicketDetectionPreview({ ticketId, videoId, assignmentId, + assignmentStatus, + onDetectionCountChange, }: TicketDetectionPreviewProps) { const [currentIndex, setCurrentIndex] = useState(0); const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false); + const isUnassignedPreview = assignmentStatus === 'unassigned'; useEffect(() => { setCurrentIndex(0); - }, [assignmentId, videoId]); + }, [assignmentId, assignmentStatus, videoId]); const queryParams = useMemo( () => ({ skip: currentIndex, limit: 1, - assignment_id: assignmentId, + assignment_id: isUnassignedPreview ? undefined : assignmentId, + assignment_status: isUnassignedPreview + ? ('unassigned' as const) + : undefined, sort: 'timestamp_asc', }), - [assignmentId, currentIndex], + [assignmentId, currentIndex, isUnassignedPreview], ); const detectionsQuery = useTicketDetectionsQuery( @@ -120,7 +127,7 @@ export function TicketDetectionPreview({ const detectionResult = detectionsQuery.data; const activeDetection = detectionResult?.items[0]; const confirmedDetectionCount = detectionResult?.total; - const detectionCount = detectionResult?.total ?? 0; + const detectionCount = confirmedDetectionCount ?? 0; const activeVisual = getDefectVisual( activeDetection?.detection.class_name ?? '', ); @@ -148,8 +155,15 @@ export function TicketDetectionPreview({ } }, [confirmedDetectionCount, currentIndex]); + useEffect(() => { + onDetectionCountChange?.(confirmedDetectionCount); + }, [confirmedDetectionCount, onDetectionCountChange]); + const isNavigationDisabled = detectionCount === 0 || detectionsQuery.isLoading; + const isInitialLoading = detectionsQuery.isLoading; + const isError = detectionsQuery.isError; + const retry = () => void detectionsQuery.refetch(); return (
@@ -165,16 +179,8 @@ export function TicketDetectionPreview({ className={cn('size-5', activeVisual.colorClassName)} /> -
-
-

{activeDisplayName}

- - {detectionCount} Detections - -
-

- Review all detections in ascending timestamp order. -

+
+

{activeDisplayName}

@@ -215,28 +221,27 @@ export function TicketDetectionPreview({
- {detectionsQuery.isLoading && !detectionsQuery.data ? ( + {isInitialLoading && !activeDetection ? ( - ) : detectionsQuery.isError ? ( + ) : isError ? (

Failed to load detection preview.

-
) : !activeDetection || !detectionResult ? (
- No detections are available for this issue. + {detectionCount > 0 + ? 'The selected detection details are unavailable. Try refreshing the preview.' + : isUnassignedPreview + ? 'No unassigned issues remain on this ticket.' + : 'No detections are available for this issue.'}
) : ( <> @@ -283,30 +288,34 @@ export function TicketDetectionPreview({ ]} /> -
- -
+ {isUnassignedPreview ? ( + <> +
+ +
- { - await discardMutation.mutateAsync({ - detectionId: activeDetection.detection.id, - payload, - }); - setIsDiscardDialogOpen(false); - }} - /> + { + await discardMutation.mutateAsync({ + detectionId: activeDetection.detection.id, + payload, + }); + setIsDiscardDialogOpen(false); + }} + /> + + ) : null} )} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx index 9e964d0..cba8d42 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx @@ -143,7 +143,7 @@ export function TicketOverviewCard({
{uploadedOn || uploaderName || locationLabel ? ( - + {uploadedOn ? (
diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketUnassignedIssuesCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketUnassignedIssuesCard.tsx new file mode 100644 index 0000000..2f85c81 --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketUnassignedIssuesCard.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { useState } from 'react'; +import { ListChecks } from 'lucide-react'; + +import { Badge } from '@/components/ui/badge'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +import { TicketDetectionPreview } from './TicketDetectionPreview'; + +interface TicketUnassignedIssuesCardProps { + ticketId: string; + videoId?: string | null; +} + +export function TicketUnassignedIssuesCard({ + ticketId, + videoId, +}: TicketUnassignedIssuesCardProps) { + const [detectionCount, setDetectionCount] = useState(); + + if (!videoId) return null; + + return ( + + +
+
+ + + Unassigned Issues + + + Review detections awaiting assignment and discard invalid AI + results. + +
+ {detectionCount ?? 0} Unassigned +
+
+ +
+ +
+
+
+ ); +} diff --git a/src/app/(modules)/ticket/[ticketId]/page.tsx b/src/app/(modules)/ticket/[ticketId]/page.tsx index 48e2240..2f53c56 100644 --- a/src/app/(modules)/ticket/[ticketId]/page.tsx +++ b/src/app/(modules)/ticket/[ticketId]/page.tsx @@ -5,6 +5,8 @@ import { useParams, useRouter } from 'next/navigation'; import { ArrowLeft } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { PERMISSIONS } from '@/constants/permissions'; +import { PermissionGuard } from '@/guards'; import { TicketDetailHeader } from './components/TicketDetailHeader'; import { @@ -14,8 +16,8 @@ import { import { TicketAssignmentsCard } from './components/TicketAssignmentsCard'; import { TicketOverviewCard } from './components/TicketOverviewCard'; import { TicketStatusActions } from './components/TicketStatusActions'; -import { TicketDetectionPreviewCard } from './components/TicketDetectionPreviewCard'; import { TicketHistoryCard } from './components/TicketHistoryCard'; +import { TicketUnassignedIssuesCard } from './components/TicketUnassignedIssuesCard'; import { useTicketDetailEvents } from '../hooks/useTicketDetailEvents'; import { useTicketAssignmentsQuery, @@ -79,17 +81,19 @@ export default function TicketDetailPage() { {overview ? ( ) : null} - {activeAssignment ? ( - + {overview ? ( + + + ) : null}
diff --git a/src/app/(modules)/ticket/hooks/useTicketQueries.ts b/src/app/(modules)/ticket/hooks/useTicketQueries.ts index 156c706..395443a 100644 --- a/src/app/(modules)/ticket/hooks/useTicketQueries.ts +++ b/src/app/(modules)/ticket/hooks/useTicketQueries.ts @@ -113,12 +113,14 @@ export function useTicketAssignmentsQuery( export function useTicketDetectionsQuery( videoId: string | undefined, params: VideoDetectionsParams, + enabled = true, ) { const queryParams = useMemo( () => ({ skip: params.skip ?? 0, limit: params.limit ?? 1, assignment_id: params.assignment_id, + assignment_status: params.assignment_status, class_name: params.class_name, min_confidence: params.min_confidence, sort: params.sort ?? 'timestamp_asc', @@ -126,6 +128,7 @@ export function useTicketDetectionsQuery( }), [ params.assignment_id, + params.assignment_status, params.class_name, params.limit, params.min_confidence, @@ -139,7 +142,7 @@ export function useTicketDetectionsQuery( queryKey: ticketKeys.classDetections(videoId ?? '', queryParams), queryFn: () => videoService.getVideoDetections(videoId as string, queryParams), - enabled: Boolean(videoId), + enabled: Boolean(enabled && videoId), staleTime: Infinity, }); } @@ -154,6 +157,7 @@ export function useTicketClassDetectionsQuery( export function useTicketAssignmentDetectionsQuery( ticketId: string | undefined, params: TicketAssignmentDetectionsParams, + enabled = true, ) { const queryParams = useMemo( () => ({ @@ -172,7 +176,7 @@ export function useTicketAssignmentDetectionsQuery( ticketId as string, queryParams, ), - enabled: Boolean(ticketId), + enabled: Boolean(enabled && ticketId), staleTime: Infinity, }); } @@ -229,6 +233,9 @@ export function useDiscardDetectionMutation( }), queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }), queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }), + queryClient.invalidateQueries({ + queryKey: ticketKeys.assignmentDetectionLists(ticketId), + }), queryClient.invalidateQueries({ queryKey: ticketKeys.assignments(ticketId), }), diff --git a/src/services/api/video.service.ts b/src/services/api/video.service.ts index faf3077..62b30bf 100644 --- a/src/services/api/video.service.ts +++ b/src/services/api/video.service.ts @@ -74,6 +74,7 @@ export const videoService = { skip: params?.skip ?? 0, limit: params?.limit ?? 1, assignment_id: params?.assignment_id, + assignment_status: params?.assignment_status, 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 712a64c..bc3878b 100644 --- a/src/types/detection.ts +++ b/src/types/detection.ts @@ -75,10 +75,13 @@ export type DetectionResultLog = DetectionResultItem & { }; }; +export type AssignmentDetectionStatus = 'assigned' | 'unassigned'; + export type VideoDetectionsParams = { skip?: number; limit?: number; assignment_id?: number; + assignment_status?: AssignmentDetectionStatus; class_name?: string | string[]; proof_status?: DetectionProofStatus; min_confidence?: number; @@ -104,8 +107,6 @@ export type VideoDetectionsResponse = { items: DetectionResultItem[]; }; -export type AssignmentDetectionStatus = 'assigned' | 'unassigned'; - export type TicketAssignmentDetectionsParams = { assignment_status?: AssignmentDetectionStatus; class_name?: string | string[];