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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user