feat(tickets): add unassigned issue triage to ticket details

- embed detection previews within work assignments
- allow discarding only unassigned detections
- improve ticket detail labels, counts, and layout
This commit is contained in:
2026-08-07 15:31:47 +05:30
parent c8899c191e
commit 47697b4421
9 changed files with 177 additions and 74 deletions

View File

@@ -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 (
<div className="space-y-4">
@@ -165,16 +179,8 @@ export function TicketDetectionPreview({
className={cn('size-5', activeVisual.colorClassName)}
/>
</span>
<div className="min-w-0 space-y-1">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<h3>{activeDisplayName}</h3>
<Badge variant="secondary" className="rounded-lg">
{detectionCount} Detections
</Badge>
</div>
<p className="text-muted-foreground">
Review all detections in ascending timestamp order.
</p>
<div className="min-w-0">
<h3>{activeDisplayName}</h3>
</div>
</div>
@@ -215,28 +221,27 @@ export function TicketDetectionPreview({
</div>
</div>
{detectionsQuery.isLoading && !detectionsQuery.data ? (
{isInitialLoading && !activeDetection ? (
<TicketDetectionPreviewSkeleton />
) : detectionsQuery.isError ? (
) : isError ? (
<div className="rounded-lg border border-dashed border-destructive/40 bg-destructive/5 px-4 py-8 text-center">
<div className="flex flex-col items-center gap-2">
<AlertTriangle className="size-6 text-destructive" />
<p className="text-sm font-medium text-destructive">
Failed to load detection preview.
</p>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => void detectionsQuery.refetch()}
>
<Button type="button" variant="outline" size="sm" onClick={retry}>
Retry
</Button>
</div>
</div>
) : !activeDetection || !detectionResult ? (
<div className="rounded-lg border border-dashed bg-muted/20 px-4 py-10 text-center text-sm text-muted-foreground">
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.'}
</div>
) : (
<>
@@ -283,30 +288,34 @@ export function TicketDetectionPreview({
]}
/>
<div className="flex justify-end">
<Button
type="button"
variant="destructive"
onClick={() => setIsDiscardDialogOpen(true)}
>
<Trash2 />
Discard detection
</Button>
</div>
{isUnassignedPreview ? (
<>
<div className="flex justify-end">
<Button
type="button"
variant="destructive"
onClick={() => setIsDiscardDialogOpen(true)}
>
<Trash2 />
Discard detection
</Button>
</div>
<DetectionDiscardDialog
open={isDiscardDialogOpen}
detectionLabel={activeDetection.detection.display_name}
isPending={discardMutation.isPending}
onOpenChange={setIsDiscardDialogOpen}
onSubmit={async (payload) => {
await discardMutation.mutateAsync({
detectionId: activeDetection.detection.id,
payload,
});
setIsDiscardDialogOpen(false);
}}
/>
<DetectionDiscardDialog
open={isDiscardDialogOpen}
detectionLabel={activeDetection.detection.display_name}
isPending={discardMutation.isPending}
onOpenChange={setIsDiscardDialogOpen}
onSubmit={async (payload) => {
await discardMutation.mutateAsync({
detectionId: activeDetection.detection.id,
payload,
});
setIsDiscardDialogOpen(false);
}}
/>
</>
) : null}
</>
)}
</div>