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.
onAssignmentChange(assignment.id)}
>
- Lot {assignment.id}
+ Assignment {assignment.id}
);
})}
@@ -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.
-
void detectionsQuery.refetch()}
- >
+
Retry
) : !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({
]}
/>
-
- setIsDiscardDialogOpen(true)}
- >
-
- Discard detection
-
-
+ {isUnassignedPreview ? (
+ <>
+
+ setIsDiscardDialogOpen(true)}
+ >
+
+ Discard detection
+
+
- {
- 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[];