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:
@@ -20,9 +20,11 @@ import type { TicketActor, TicketAssignmentSummary } from '@/types';
|
|||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
|
|
||||||
import { useTicketAssignmentsQuery } from '../../hooks/useTicketQueries';
|
import { useTicketAssignmentsQuery } from '../../hooks/useTicketQueries';
|
||||||
|
import { TicketDetectionPreview } from './TicketDetectionPreview';
|
||||||
|
|
||||||
interface TicketAssignmentsCardProps {
|
interface TicketAssignmentsCardProps {
|
||||||
ticketId: string;
|
ticketId: string;
|
||||||
|
videoId?: string | null;
|
||||||
initialAssignments?: TicketAssignmentSummary[];
|
initialAssignments?: TicketAssignmentSummary[];
|
||||||
selectedAssignmentId?: number;
|
selectedAssignmentId?: number;
|
||||||
onAssignmentChange: (assignmentId: number) => void;
|
onAssignmentChange: (assignmentId: number) => void;
|
||||||
@@ -87,7 +89,7 @@ function AssignmentProgress({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssignmentLot({
|
function AssignmentOverview({
|
||||||
assignment,
|
assignment,
|
||||||
}: {
|
}: {
|
||||||
assignment: TicketAssignmentSummary;
|
assignment: TicketAssignmentSummary;
|
||||||
@@ -109,7 +111,7 @@ function AssignmentLot({
|
|||||||
<div className="min-w-0 space-y-4 rounded-lg border p-3 sm:p-4">
|
<div className="min-w-0 space-y-4 rounded-lg border p-3 sm:p-4">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="mb-2 text-xs font-medium text-muted-foreground">
|
<p className="mb-2 text-xs font-medium text-muted-foreground">
|
||||||
Lot {assignment.id}
|
Assignment {assignment.id}
|
||||||
</p>
|
</p>
|
||||||
<PersonInfo person={worker} />
|
<PersonInfo person={worker} />
|
||||||
</div>
|
</div>
|
||||||
@@ -138,6 +140,7 @@ function AssignmentLot({
|
|||||||
|
|
||||||
export function TicketAssignmentsCard({
|
export function TicketAssignmentsCard({
|
||||||
ticketId,
|
ticketId,
|
||||||
|
videoId,
|
||||||
initialAssignments,
|
initialAssignments,
|
||||||
selectedAssignmentId,
|
selectedAssignmentId,
|
||||||
onAssignmentChange,
|
onAssignmentChange,
|
||||||
@@ -173,14 +176,17 @@ export function TicketAssignmentsCard({
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<BriefcaseBusiness className="size-5 text-primary" />
|
<BriefcaseBusiness className="size-5 text-primary" />
|
||||||
Assignment lots
|
Work Assignments
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="mt-1">
|
<CardDescription className="mt-1">
|
||||||
Live ownership, criteria, and repair progress for this ticket.
|
Issues grouped by assignee, scope, and repair progress.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{assignmentsQuery.data ? (
|
{assignmentsQuery.data ? (
|
||||||
<Badge variant="default">{assignments.length}</Badge>
|
<Badge variant="default">
|
||||||
|
{assignments.length}{' '}
|
||||||
|
{assignments.length === 1 ? 'Assignment' : 'Assignments'}
|
||||||
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -195,7 +201,7 @@ export function TicketAssignmentsCard({
|
|||||||
) : assignmentsQuery.isError ? (
|
) : assignmentsQuery.isError ? (
|
||||||
<div className="rounded-lg border border-dashed p-6 text-center">
|
<div className="rounded-lg border border-dashed p-6 text-center">
|
||||||
<p className="text-sm font-medium">
|
<p className="text-sm font-medium">
|
||||||
Unable to load assignment lots.
|
Unable to load work assignments.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -210,7 +216,7 @@ export function TicketAssignmentsCard({
|
|||||||
) : selectedAssignment ? (
|
) : selectedAssignment ? (
|
||||||
<div className="min-w-0 space-y-3">
|
<div className="min-w-0 space-y-3">
|
||||||
<nav
|
<nav
|
||||||
aria-label="Assignment lots"
|
aria-label="Work assignments"
|
||||||
className="w-full max-w-full overflow-x-auto overflow-y-hidden overscroll-x-contain touch-pan-x"
|
className="w-full max-w-full overflow-x-auto overflow-y-hidden overscroll-x-contain touch-pan-x"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -239,7 +245,7 @@ export function TicketAssignmentsCard({
|
|||||||
)}
|
)}
|
||||||
onClick={() => onAssignmentChange(assignment.id)}
|
onClick={() => onAssignmentChange(assignment.id)}
|
||||||
>
|
>
|
||||||
Lot {assignment.id}
|
Assignment {assignment.id}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -252,14 +258,23 @@ export function TicketAssignmentsCard({
|
|||||||
aria-labelledby={`assignment-tab-${activeAssignmentId}`}
|
aria-labelledby={`assignment-tab-${activeAssignmentId}`}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
>
|
>
|
||||||
<AssignmentLot assignment={selectedAssignment} />
|
<AssignmentOverview assignment={selectedAssignment} />
|
||||||
|
{videoId ? (
|
||||||
|
<div className={'mt-4 border-t pt-4'}>
|
||||||
|
<TicketDetectionPreview
|
||||||
|
ticketId={ticketId}
|
||||||
|
videoId={videoId}
|
||||||
|
assignmentId={selectedAssignment.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border border-dashed px-4 py-8 text-center">
|
<div className="rounded-lg border border-dashed px-4 py-8 text-center">
|
||||||
<p className="text-sm font-medium">No assignments yet</p>
|
<p className="text-sm font-medium">No work assignments yet</p>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Assignment lots will appear here after work is allocated.
|
Work assignments will appear here after issues are allocated.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
import { PermissionGuard } from '@/guards';
|
import { PermissionGuard } from '@/guards';
|
||||||
@@ -17,7 +18,7 @@ export function TicketDetailHeader({
|
|||||||
ticket: TicketOverviewDetail;
|
ticket: TicketOverviewDetail;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const ticketLabel = ticket.ticket_name || ticket.id;
|
const ticketName = ticket.ticket_name || 'Unnamed ticket';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
@@ -32,11 +33,17 @@ export function TicketDetailHeader({
|
|||||||
>
|
>
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="min-w-0 space-y-1">
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
Ticket Detail
|
Ticket Detail
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs">Ticket: {ticketLabel}</p>
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="max-w-full font-mono text-xs"
|
||||||
|
title={ticketName}
|
||||||
|
>
|
||||||
|
<span className="truncate">{ticketName}</span>
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { AlertTriangle, ChevronLeft, ChevronRight, Trash2 } from 'lucide-react';
|
import { AlertTriangle, ChevronLeft, ChevronRight, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
import DetectionLocationMap from '@/components/map/detectionLocationMap';
|
import DetectionLocationMap from '@/components/map/detectionLocationMap';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import AnnotatedDetectionImage from '@/components/video/annotatedDetectionImage';
|
import AnnotatedDetectionImage from '@/components/video/annotatedDetectionImage';
|
||||||
@@ -20,7 +19,9 @@ import { DetectionDiscardDialog } from './DetectionDiscardDialog';
|
|||||||
interface TicketDetectionPreviewProps {
|
interface TicketDetectionPreviewProps {
|
||||||
ticketId: string;
|
ticketId: string;
|
||||||
videoId?: string | null;
|
videoId?: string | null;
|
||||||
assignmentId: number;
|
assignmentId?: number;
|
||||||
|
assignmentStatus?: 'unassigned';
|
||||||
|
onDetectionCountChange?: (count: number | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetectionMetadataBar({
|
function DetectionMetadataBar({
|
||||||
@@ -95,22 +96,28 @@ export function TicketDetectionPreview({
|
|||||||
ticketId,
|
ticketId,
|
||||||
videoId,
|
videoId,
|
||||||
assignmentId,
|
assignmentId,
|
||||||
|
assignmentStatus,
|
||||||
|
onDetectionCountChange,
|
||||||
}: TicketDetectionPreviewProps) {
|
}: TicketDetectionPreviewProps) {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
|
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
|
||||||
|
const isUnassignedPreview = assignmentStatus === 'unassigned';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentIndex(0);
|
setCurrentIndex(0);
|
||||||
}, [assignmentId, videoId]);
|
}, [assignmentId, assignmentStatus, videoId]);
|
||||||
|
|
||||||
const queryParams = useMemo(
|
const queryParams = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
skip: currentIndex,
|
skip: currentIndex,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
assignment_id: assignmentId,
|
assignment_id: isUnassignedPreview ? undefined : assignmentId,
|
||||||
|
assignment_status: isUnassignedPreview
|
||||||
|
? ('unassigned' as const)
|
||||||
|
: undefined,
|
||||||
sort: 'timestamp_asc',
|
sort: 'timestamp_asc',
|
||||||
}),
|
}),
|
||||||
[assignmentId, currentIndex],
|
[assignmentId, currentIndex, isUnassignedPreview],
|
||||||
);
|
);
|
||||||
|
|
||||||
const detectionsQuery = useTicketDetectionsQuery(
|
const detectionsQuery = useTicketDetectionsQuery(
|
||||||
@@ -120,7 +127,7 @@ export function TicketDetectionPreview({
|
|||||||
const detectionResult = detectionsQuery.data;
|
const detectionResult = detectionsQuery.data;
|
||||||
const activeDetection = detectionResult?.items[0];
|
const activeDetection = detectionResult?.items[0];
|
||||||
const confirmedDetectionCount = detectionResult?.total;
|
const confirmedDetectionCount = detectionResult?.total;
|
||||||
const detectionCount = detectionResult?.total ?? 0;
|
const detectionCount = confirmedDetectionCount ?? 0;
|
||||||
const activeVisual = getDefectVisual(
|
const activeVisual = getDefectVisual(
|
||||||
activeDetection?.detection.class_name ?? '',
|
activeDetection?.detection.class_name ?? '',
|
||||||
);
|
);
|
||||||
@@ -148,8 +155,15 @@ export function TicketDetectionPreview({
|
|||||||
}
|
}
|
||||||
}, [confirmedDetectionCount, currentIndex]);
|
}, [confirmedDetectionCount, currentIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDetectionCountChange?.(confirmedDetectionCount);
|
||||||
|
}, [confirmedDetectionCount, onDetectionCountChange]);
|
||||||
|
|
||||||
const isNavigationDisabled =
|
const isNavigationDisabled =
|
||||||
detectionCount === 0 || detectionsQuery.isLoading;
|
detectionCount === 0 || detectionsQuery.isLoading;
|
||||||
|
const isInitialLoading = detectionsQuery.isLoading;
|
||||||
|
const isError = detectionsQuery.isError;
|
||||||
|
const retry = () => void detectionsQuery.refetch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -165,16 +179,8 @@ export function TicketDetectionPreview({
|
|||||||
className={cn('size-5', activeVisual.colorClassName)}
|
className={cn('size-5', activeVisual.colorClassName)}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 space-y-1">
|
<div className="min-w-0">
|
||||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
||||||
<h3>{activeDisplayName}</h3>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -215,28 +221,27 @@ export function TicketDetectionPreview({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{detectionsQuery.isLoading && !detectionsQuery.data ? (
|
{isInitialLoading && !activeDetection ? (
|
||||||
<TicketDetectionPreviewSkeleton />
|
<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="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">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<AlertTriangle className="size-6 text-destructive" />
|
<AlertTriangle className="size-6 text-destructive" />
|
||||||
<p className="text-sm font-medium text-destructive">
|
<p className="text-sm font-medium text-destructive">
|
||||||
Failed to load detection preview.
|
Failed to load detection preview.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button type="button" variant="outline" size="sm" onClick={retry}>
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => void detectionsQuery.refetch()}
|
|
||||||
>
|
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : !activeDetection || !detectionResult ? (
|
) : !activeDetection || !detectionResult ? (
|
||||||
<div className="rounded-lg border border-dashed bg-muted/20 px-4 py-10 text-center text-sm text-muted-foreground">
|
<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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -283,6 +288,8 @@ export function TicketDetectionPreview({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{isUnassignedPreview ? (
|
||||||
|
<>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -308,6 +315,8 @@ export function TicketDetectionPreview({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export function TicketOverviewCard({
|
|||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{uploadedOn || uploaderName || locationLabel ? (
|
{uploadedOn || uploaderName || locationLabel ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="grid gap-4 py-4 sm:grid-cols-3 sm:divide-x">
|
<CardContent className="grid gap-4 sm:grid-cols-3 sm:divide-x">
|
||||||
{uploadedOn ? (
|
{uploadedOn ? (
|
||||||
<div className="flex items-center gap-3 sm:pr-4">
|
<div className="flex items-center gap-3 sm:pr-4">
|
||||||
<CalendarDays className="size-5 shrink-0 text-muted-foreground" />
|
<CalendarDays className="size-5 shrink-0 text-muted-foreground" />
|
||||||
|
|||||||
@@ -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<number>();
|
||||||
|
|
||||||
|
if (!videoId) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={'w-full'}>
|
||||||
|
<CardHeader className="min-w-0 px-3 sm:px-6">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<ListChecks className="size-5 text-primary" />
|
||||||
|
Unassigned Issues
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="mt-1">
|
||||||
|
Review detections awaiting assignment and discard invalid AI
|
||||||
|
results.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Badge variant={'default'}>{detectionCount ?? 0} Unassigned</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className={'p-5'}>
|
||||||
|
<div className={'min-w-0'}>
|
||||||
|
<TicketDetectionPreview
|
||||||
|
ticketId={ticketId}
|
||||||
|
videoId={videoId}
|
||||||
|
assignmentStatus={'unassigned'}
|
||||||
|
onDetectionCountChange={setDetectionCount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import { useParams, useRouter } from 'next/navigation';
|
|||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
|
import { PermissionGuard } from '@/guards';
|
||||||
|
|
||||||
import { TicketDetailHeader } from './components/TicketDetailHeader';
|
import { TicketDetailHeader } from './components/TicketDetailHeader';
|
||||||
import {
|
import {
|
||||||
@@ -14,8 +16,8 @@ import {
|
|||||||
import { TicketAssignmentsCard } from './components/TicketAssignmentsCard';
|
import { TicketAssignmentsCard } from './components/TicketAssignmentsCard';
|
||||||
import { TicketOverviewCard } from './components/TicketOverviewCard';
|
import { TicketOverviewCard } from './components/TicketOverviewCard';
|
||||||
import { TicketStatusActions } from './components/TicketStatusActions';
|
import { TicketStatusActions } from './components/TicketStatusActions';
|
||||||
import { TicketDetectionPreviewCard } from './components/TicketDetectionPreviewCard';
|
|
||||||
import { TicketHistoryCard } from './components/TicketHistoryCard';
|
import { TicketHistoryCard } from './components/TicketHistoryCard';
|
||||||
|
import { TicketUnassignedIssuesCard } from './components/TicketUnassignedIssuesCard';
|
||||||
import { useTicketDetailEvents } from '../hooks/useTicketDetailEvents';
|
import { useTicketDetailEvents } from '../hooks/useTicketDetailEvents';
|
||||||
import {
|
import {
|
||||||
useTicketAssignmentsQuery,
|
useTicketAssignmentsQuery,
|
||||||
@@ -79,17 +81,19 @@ export default function TicketDetailPage() {
|
|||||||
{overview ? (
|
{overview ? (
|
||||||
<TicketAssignmentsCard
|
<TicketAssignmentsCard
|
||||||
ticketId={ticketId}
|
ticketId={ticketId}
|
||||||
|
videoId={overview.video_id ?? overview.video?.id}
|
||||||
initialAssignments={overview.assignments}
|
initialAssignments={overview.assignments}
|
||||||
selectedAssignmentId={activeAssignmentId}
|
selectedAssignmentId={activeAssignmentId}
|
||||||
onAssignmentChange={setSelectedAssignmentId}
|
onAssignmentChange={setSelectedAssignmentId}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{activeAssignment ? (
|
{overview ? (
|
||||||
<TicketDetectionPreviewCard
|
<PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}>
|
||||||
|
<TicketUnassignedIssuesCard
|
||||||
ticketId={ticketId}
|
ticketId={ticketId}
|
||||||
videoId={overview?.video_id ?? overview?.video?.id}
|
videoId={overview.video_id ?? overview.video?.id}
|
||||||
assignmentId={activeAssignment.id}
|
|
||||||
/>
|
/>
|
||||||
|
</PermissionGuard>
|
||||||
) : null}
|
) : null}
|
||||||
<TicketHistoryCard ticketId={ticketId} />
|
<TicketHistoryCard ticketId={ticketId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -113,12 +113,14 @@ export function useTicketAssignmentsQuery(
|
|||||||
export function useTicketDetectionsQuery(
|
export function useTicketDetectionsQuery(
|
||||||
videoId: string | undefined,
|
videoId: string | undefined,
|
||||||
params: VideoDetectionsParams,
|
params: VideoDetectionsParams,
|
||||||
|
enabled = true,
|
||||||
) {
|
) {
|
||||||
const queryParams = useMemo(
|
const queryParams = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
skip: params.skip ?? 0,
|
skip: params.skip ?? 0,
|
||||||
limit: params.limit ?? 1,
|
limit: params.limit ?? 1,
|
||||||
assignment_id: params.assignment_id,
|
assignment_id: params.assignment_id,
|
||||||
|
assignment_status: params.assignment_status,
|
||||||
class_name: params.class_name,
|
class_name: params.class_name,
|
||||||
min_confidence: params.min_confidence,
|
min_confidence: params.min_confidence,
|
||||||
sort: params.sort ?? 'timestamp_asc',
|
sort: params.sort ?? 'timestamp_asc',
|
||||||
@@ -126,6 +128,7 @@ export function useTicketDetectionsQuery(
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
params.assignment_id,
|
params.assignment_id,
|
||||||
|
params.assignment_status,
|
||||||
params.class_name,
|
params.class_name,
|
||||||
params.limit,
|
params.limit,
|
||||||
params.min_confidence,
|
params.min_confidence,
|
||||||
@@ -139,7 +142,7 @@ export function useTicketDetectionsQuery(
|
|||||||
queryKey: ticketKeys.classDetections(videoId ?? '', queryParams),
|
queryKey: ticketKeys.classDetections(videoId ?? '', queryParams),
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
videoService.getVideoDetections(videoId as string, queryParams),
|
videoService.getVideoDetections(videoId as string, queryParams),
|
||||||
enabled: Boolean(videoId),
|
enabled: Boolean(enabled && videoId),
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -154,6 +157,7 @@ export function useTicketClassDetectionsQuery(
|
|||||||
export function useTicketAssignmentDetectionsQuery(
|
export function useTicketAssignmentDetectionsQuery(
|
||||||
ticketId: string | undefined,
|
ticketId: string | undefined,
|
||||||
params: TicketAssignmentDetectionsParams,
|
params: TicketAssignmentDetectionsParams,
|
||||||
|
enabled = true,
|
||||||
) {
|
) {
|
||||||
const queryParams = useMemo(
|
const queryParams = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -172,7 +176,7 @@ export function useTicketAssignmentDetectionsQuery(
|
|||||||
ticketId as string,
|
ticketId as string,
|
||||||
queryParams,
|
queryParams,
|
||||||
),
|
),
|
||||||
enabled: Boolean(ticketId),
|
enabled: Boolean(enabled && ticketId),
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -229,6 +233,9 @@ export function useDiscardDetectionMutation(
|
|||||||
}),
|
}),
|
||||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }),
|
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }),
|
||||||
queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }),
|
queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }),
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ticketKeys.assignmentDetectionLists(ticketId),
|
||||||
|
}),
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.assignments(ticketId),
|
queryKey: ticketKeys.assignments(ticketId),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const videoService = {
|
|||||||
skip: params?.skip ?? 0,
|
skip: params?.skip ?? 0,
|
||||||
limit: params?.limit ?? 1,
|
limit: params?.limit ?? 1,
|
||||||
assignment_id: params?.assignment_id,
|
assignment_id: params?.assignment_id,
|
||||||
|
assignment_status: params?.assignment_status,
|
||||||
class_name: params?.class_name,
|
class_name: params?.class_name,
|
||||||
proof_status: params?.proof_status,
|
proof_status: params?.proof_status,
|
||||||
min_confidence: params?.min_confidence,
|
min_confidence: params?.min_confidence,
|
||||||
|
|||||||
@@ -75,10 +75,13 @@ export type DetectionResultLog = DetectionResultItem & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AssignmentDetectionStatus = 'assigned' | 'unassigned';
|
||||||
|
|
||||||
export type VideoDetectionsParams = {
|
export type VideoDetectionsParams = {
|
||||||
skip?: number;
|
skip?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
assignment_id?: number;
|
assignment_id?: number;
|
||||||
|
assignment_status?: AssignmentDetectionStatus;
|
||||||
class_name?: string | string[];
|
class_name?: string | string[];
|
||||||
proof_status?: DetectionProofStatus;
|
proof_status?: DetectionProofStatus;
|
||||||
min_confidence?: number;
|
min_confidence?: number;
|
||||||
@@ -104,8 +107,6 @@ export type VideoDetectionsResponse = {
|
|||||||
items: DetectionResultItem[];
|
items: DetectionResultItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AssignmentDetectionStatus = 'assigned' | 'unassigned';
|
|
||||||
|
|
||||||
export type TicketAssignmentDetectionsParams = {
|
export type TicketAssignmentDetectionsParams = {
|
||||||
assignment_status?: AssignmentDetectionStatus;
|
assignment_status?: AssignmentDetectionStatus;
|
||||||
class_name?: string | string[];
|
class_name?: string | string[];
|
||||||
|
|||||||
Reference in New Issue
Block a user