feat(ticket): scope issue preview to selected assignment lot

This commit is contained in:
2026-07-29 15:44:19 +05:30
parent 7ffe3f0708
commit 5e66b70611
8 changed files with 166 additions and 178 deletions

View File

@@ -13,11 +13,11 @@ const ModulesLayout = ({
<AuthGuard> <AuthGuard>
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar />
<main className="flex flex-1 h-screen w-full flex-col overflow-hidden"> <main className="flex h-screen min-w-0 flex-1 flex-col overflow-hidden">
<div className="scroll-stable flex-1 overflow-auto"> <div className="scroll-stable min-w-0 flex-1 overflow-auto">
<div className="mx-auto flex min-h-full w-full max-w-380 flex-col"> <div className="mx-auto flex min-h-full w-full min-w-0 max-w-380 flex-col">
<ModuleShellHeader /> <ModuleShellHeader />
<div className="flex-1 p-6">{children}</div> <div className="min-w-0 flex-1 p-6">{children}</div>
</div> </div>
</div> </div>
</main> </main>

View File

@@ -1,5 +1,6 @@
'use client'; 'use client';
import { useEffect } from 'react';
import { BriefcaseBusiness, CalendarClock, ListChecks } from 'lucide-react'; import { BriefcaseBusiness, CalendarClock, ListChecks } from 'lucide-react';
import { PersonInfo } from '@/components/person-avatar'; import { PersonInfo } from '@/components/person-avatar';
@@ -14,6 +15,7 @@ import {
} from '@/components/ui/card'; } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { getDefectVisual } from '@/constants/defectVisualConfig'; import { getDefectVisual } from '@/constants/defectVisualConfig';
import { cn } from '@/lib/utils';
import type { TicketActor, TicketAssignmentSummary } from '@/types'; import type { TicketActor, TicketAssignmentSummary } from '@/types';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
@@ -22,6 +24,8 @@ import { useTicketAssignmentsQuery } from '../../hooks/useTicketQueries';
interface TicketAssignmentsCardProps { interface TicketAssignmentsCardProps {
ticketId: string; ticketId: string;
initialAssignments?: TicketAssignmentSummary[]; initialAssignments?: TicketAssignmentSummary[];
selectedAssignmentId?: number;
onAssignmentChange: (assignmentId: number) => void;
} }
function formatLabel(value: string) { function formatLabel(value: string) {
@@ -60,22 +64,22 @@ function AssignmentProgress({
const progress = total > 0 ? Math.min((approved / total) * 100, 100) : 0; const progress = total > 0 ? Math.min((approved / total) * 100, 100) : 0;
return ( return (
<div className="space-y-2"> <div className="min-w-0 space-y-2">
<div className="flex items-center justify-between gap-3 text-xs"> <div className="flex min-w-0 flex-col gap-1 text-xs sm:flex-row sm:items-center sm:justify-between sm:gap-3">
<span className="font-medium text-foreground">Repair progress</span> <span className="font-medium text-foreground">Repair progress</span>
<span className="tabular-nums text-muted-foreground"> <span className="tabular-nums text-muted-foreground">
{approved} of {total} approved {approved} of {total} approved
</span> </span>
</div> </div>
<Progress value={assignment.is_complete ? 100 : progress} /> <Progress value={assignment.is_complete ? 100 : progress} />
<div className="grid grid-cols-3 gap-2 text-xs"> <div className="flex min-w-0 flex-wrap gap-x-4 gap-y-1 text-xs">
<span className="text-emerald-600"> <span className="whitespace-nowrap text-emerald-600">
{assignment.approved_detections} approved {assignment.approved_detections} approved
</span> </span>
<span className="text-amber-600"> <span className="whitespace-nowrap text-amber-600">
{assignment.pending_detections} pending {assignment.pending_detections} pending
</span> </span>
<span className="text-muted-foreground"> <span className="whitespace-nowrap text-muted-foreground">
{assignment.not_submitted_detections} not submitted {assignment.not_submitted_detections} not submitted
</span> </span>
</div> </div>
@@ -102,7 +106,7 @@ function AssignmentLot({
}; };
return ( return (
<div className="space-y-4 rounded-lg border 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} Lot {assignment.id}
@@ -110,7 +114,7 @@ function AssignmentLot({
<PersonInfo person={worker} /> <PersonInfo person={worker} />
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex min-w-0 max-w-full flex-wrap items-center gap-2">
{classNames.map((className) => ( {classNames.map((className) => (
<AssignmentClassBadge key={className} className={className} /> <AssignmentClassBadge key={className} className={className} />
))} ))}
@@ -120,7 +124,7 @@ function AssignmentLot({
{assignment.item_count === 1 ? 'issue' : 'issues'} {assignment.item_count === 1 ? 'issue' : 'issues'}
</span> </span>
{assignment.due_at ? ( {assignment.due_at ? (
<span className="flex items-center gap-1.5 text-xs text-muted-foreground"> <span className="flex min-w-0 max-w-full items-center gap-1.5 text-xs text-muted-foreground">
<CalendarClock className="size-3.5" /> <CalendarClock className="size-3.5" />
Due {formatDate(assignment.due_at)} Due {formatDate(assignment.due_at)}
</span> </span>
@@ -135,18 +139,36 @@ function AssignmentLot({
export function TicketAssignmentsCard({ export function TicketAssignmentsCard({
ticketId, ticketId,
initialAssignments, initialAssignments,
selectedAssignmentId,
onAssignmentChange,
}: TicketAssignmentsCardProps) { }: TicketAssignmentsCardProps) {
const assignmentsQuery = useTicketAssignmentsQuery( const assignmentsQuery = useTicketAssignmentsQuery(
ticketId, ticketId,
initialAssignments, initialAssignments,
); );
const assignments = assignmentsQuery.data?.items ?? []; const assignments = assignmentsQuery.data?.items ?? [];
const selectedAssignment =
assignments.find((assignment) => assignment.id === selectedAssignmentId) ??
assignments[0];
const activeAssignmentId = selectedAssignment
? String(selectedAssignment.id)
: '';
const resolvedAssignmentId = selectedAssignment?.id;
useEffect(() => {
if (
resolvedAssignmentId !== undefined &&
resolvedAssignmentId !== selectedAssignmentId
) {
onAssignmentChange(resolvedAssignmentId);
}
}, [onAssignmentChange, resolvedAssignmentId, selectedAssignmentId]);
return ( return (
<Card> <Card className="min-w-0 max-w-full">
<CardHeader> <CardHeader className="min-w-0 px-3 sm:px-6">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div> <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 Assignment lots
@@ -160,7 +182,7 @@ export function TicketAssignmentsCard({
) : null} ) : null}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="min-w-0 space-y-3 overflow-hidden px-3 sm:px-6">
{assignmentsQuery.isLoading ? ( {assignmentsQuery.isLoading ? (
Array.from({ length: 2 }).map((_, index) => ( Array.from({ length: 2 }).map((_, index) => (
<div <div
@@ -183,10 +205,54 @@ export function TicketAssignmentsCard({
Try again Try again
</Button> </Button>
</div> </div>
) : assignments.length ? ( ) : selectedAssignment ? (
assignments.map((assignment) => ( <div className="min-w-0 space-y-3">
<AssignmentLot key={assignment.id} assignment={assignment} /> <nav
)) aria-label="Assignment lots"
className="w-full max-w-full overflow-x-auto overflow-y-hidden overscroll-x-contain touch-pan-x"
>
<div
role="tablist"
aria-orientation="horizontal"
className="flex w-max min-w-full border-b"
>
{assignments.map((assignment) => {
const assignmentId = String(assignment.id);
const isActive = assignmentId === activeAssignmentId;
return (
<button
key={assignment.id}
id={`assignment-tab-${assignmentId}`}
type="button"
role="tab"
aria-selected={isActive}
aria-controls={`assignment-panel-${assignmentId}`}
tabIndex={isActive ? 0 : -1}
className={cn(
'relative shrink-0 px-3 py-2.5 text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',
isActive
? 'text-foreground after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:bg-foreground'
: 'text-muted-foreground hover:text-foreground',
)}
onClick={() => onAssignmentChange(assignment.id)}
>
Lot {assignment.id}
</button>
);
})}
</div>
</nav>
<div
id={`assignment-panel-${activeAssignmentId}`}
role="tabpanel"
aria-labelledby={`assignment-tab-${activeAssignmentId}`}
className="min-w-0"
>
<AssignmentLot assignment={selectedAssignment} />
</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 assignments yet</p>

View File

@@ -13,15 +13,14 @@ import { cn } from '@/lib/utils';
import { import {
useDiscardDetectionMutation, useDiscardDetectionMutation,
useTicketClassDetectionsQuery, useTicketDetectionsQuery,
} from '../../hooks/useTicketQueries'; } from '../../hooks/useTicketQueries';
import { DetectionDiscardDialog } from './DetectionDiscardDialog'; import { DetectionDiscardDialog } from './DetectionDiscardDialog';
interface TicketClassDetectionPreviewProps { interface TicketClassDetectionPreviewProps {
ticketId: string; ticketId: string;
videoId?: string | null; videoId?: string | null;
defectClassName: string; assignmentId?: number;
displayName: string;
totalCount?: number; totalCount?: number;
} }
@@ -96,41 +95,45 @@ function TicketClassDetectionPreviewSkeleton() {
export function TicketClassDetectionPreview({ export function TicketClassDetectionPreview({
ticketId, ticketId,
videoId, videoId,
defectClassName, assignmentId,
displayName,
totalCount, totalCount,
}: TicketClassDetectionPreviewProps) { }: TicketClassDetectionPreviewProps) {
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false); const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
const visual = getDefectVisual(defectClassName);
const IssueIcon = visual.icon;
useEffect(() => { useEffect(() => {
setCurrentIndex(0); setCurrentIndex(0);
}, [defectClassName]); }, [assignmentId, videoId]);
const queryParams = useMemo( const queryParams = useMemo(
() => ({ () => ({
class_name: defectClassName,
skip: currentIndex, skip: currentIndex,
limit: 1, limit: 1,
assignment_id: assignmentId,
sort: 'timestamp_asc', sort: 'timestamp_asc',
}), }),
[currentIndex, defectClassName], [assignmentId, currentIndex],
); );
const detectionsQuery = useTicketClassDetectionsQuery( const detectionsQuery = useTicketDetectionsQuery(
videoId ?? undefined, videoId ?? undefined,
queryParams, queryParams,
); );
const detectionResult = detectionsQuery.data;
const activeDetection = detectionResult?.items[0];
const detectionCount =
detectionResult?.total ?? (assignmentId ? 0 : (totalCount ?? 0));
const activeVisual = getDefectVisual(
activeDetection?.detection.class_name ?? '',
);
const ActiveIssueIcon = activeVisual.icon;
const activeDisplayName =
activeDetection?.detection.display_name ?? 'Detection';
const discardMutation = useDiscardDetectionMutation( const discardMutation = useDiscardDetectionMutation(
ticketId, ticketId,
videoId ?? undefined, videoId ?? undefined,
defectClassName, activeDetection?.detection.class_name,
); );
const detectionResult = detectionsQuery.data;
const activeDetection = detectionResult?.items[0];
const detectionCount = detectionResult?.total ?? totalCount ?? 0;
useEffect(() => { useEffect(() => {
if (detectionCount === 0 && currentIndex !== 0) { if (detectionCount === 0 && currentIndex !== 0) {
@@ -153,27 +156,22 @@ export function TicketClassDetectionPreview({
<span <span
className={cn( className={cn(
'flex size-10 shrink-0 self-start items-center justify-center rounded-lg', 'flex size-10 shrink-0 self-start items-center justify-center rounded-lg',
visual.cardClassName, activeVisual.cardClassName,
)} )}
> >
<IssueIcon className={cn('size-5', visual.colorClassName)} /> <ActiveIssueIcon
className={cn('size-5', activeVisual.colorClassName)}
/>
</span> </span>
<div className="min-w-0 space-y-1"> <div className="min-w-0 space-y-1">
<div className="flex min-w-0 flex-wrap items-center gap-2"> <div className="flex min-w-0 flex-wrap items-center gap-2">
<h3>{displayName}</h3> <h3>{activeDisplayName}</h3>
<Badge <Badge variant="secondary" className="rounded-lg">
variant="secondary"
className={cn(
'rounded-lg',
visual.cardClassName,
visual.colorClassName,
)}
>
{detectionCount} Detections {detectionCount} Detections
</Badge> </Badge>
</div> </div>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Review detections in ascending timestamp order. Review all detections in ascending timestamp order.
</p> </p>
</div> </div>
</div> </div>
@@ -258,7 +256,7 @@ export function TicketClassDetectionPreview({
className="min-w-0" className="min-w-0"
latitude={activeDetection.location.latitude} latitude={activeDetection.location.latitude}
longitude={activeDetection.location.longitude} longitude={activeDetection.location.longitude}
label={`${displayName} - Frame ${activeDetection.frame.number}`} label={`${activeDetection.detection.display_name} - Frame ${activeDetection.frame.number}`}
title="Location on Map" title="Location on Map"
variant="plain" variant="plain"
aspectRatio="16 / 9" aspectRatio="16 / 9"

View File

@@ -1,14 +1,9 @@
'use client'; 'use client';
import { useEffect, useMemo, useState } from 'react'; import { useEffect } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { Component, ListChecks } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { DEFECT_CLASS_STATUS_CONFIG } from '@/constants/defectClassStatus';
import { cn } from '@/lib/utils';
import type { TicketOverviewDetail } from '@/types'; import type { TicketOverviewDetail } from '@/types';
import { useTicketDefectClassesQuery } from '../../hooks/useTicketQueries'; import { useTicketDefectClassesQuery } from '../../hooks/useTicketQueries';
@@ -17,148 +12,59 @@ import { TicketClassDetectionPreview } from './TicketClassDetectionPreview';
interface TicketDefectClassTabsProps { interface TicketDefectClassTabsProps {
ticketId: string; ticketId: string;
ticket?: TicketOverviewDetail; ticket?: TicketOverviewDetail;
assignmentId?: number;
} }
export function TicketDefectClassTabs({ export function TicketDefectClassTabs({
ticketId, ticketId,
ticket, ticket,
assignmentId,
}: TicketDefectClassTabsProps) { }: TicketDefectClassTabsProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const defectClassParam = searchParams.get('defect_class'); const defectClassParam = searchParams.get('defect_class');
const defectClassesQuery = useTicketDefectClassesQuery(ticketId); const defectClassesQuery = useTicketDefectClassesQuery(ticketId);
const defectClasses = useMemo(
() => defectClassesQuery.data?.defect_classes ?? [],
[defectClassesQuery.data?.defect_classes],
);
const [selectedClass, setSelectedClass] = useState<string>();
useEffect(() => { useEffect(() => {
const defectClasses = defectClassesQuery.data?.defect_classes ?? [];
if (!defectClasses.length) return; if (!defectClasses.length) return;
const hasUrlClass = defectClasses.some( const hasUrlClass = defectClasses.some(
(item) => item.class_name === defectClassParam, (item) => item.class_name === defectClassParam,
); );
if (hasUrlClass) { if (hasUrlClass) return;
setSelectedClass(defectClassParam ?? undefined);
return;
}
const firstClass = defectClasses[0].class_name; const firstClass = defectClasses[0].class_name;
const nextParams = new URLSearchParams(searchParams.toString()); const nextParams = new URLSearchParams(searchParams.toString());
nextParams.set('defect_class', firstClass); nextParams.set('defect_class', firstClass);
setSelectedClass(firstClass);
router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false }); router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false });
}, [defectClassParam, defectClasses, pathname, router, searchParams]); }, [
defectClassParam,
defectClassesQuery.data?.defect_classes,
pathname,
router,
searchParams,
]);
const handleClassChange = (className: string) => {
const nextParams = new URLSearchParams(searchParams.toString());
nextParams.set('defect_class', className);
setSelectedClass(className);
router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false });
};
const selectedIssue = ticket?.ai_result.detections_by_class.find(
(item) => item.class_name === selectedClass,
);
const selectedClassItem = defectClasses.find(
(item) => item.class_name === selectedClass,
);
const previewDisplayName =
selectedIssue?.display_name ??
selectedClassItem?.display_name ??
selectedClass ??
'Detection';
const previewVideoId = ticket?.video_id ?? defectClassesQuery.data?.video_id; const previewVideoId = ticket?.video_id ?? defectClassesQuery.data?.video_id;
if (defectClassesQuery.isLoading) { if (!previewVideoId) return null;
return (
<Card className="w-full rounded-lg">
<CardContent className="space-y-4 p-5">
<div className="h-5 w-24 animate-pulse rounded-lg bg-muted" />
<div className="flex flex-col gap-2">
{Array.from({ length: 3 }).map((_, index) => (
<div
key={index}
className="h-11 w-full animate-pulse rounded-lg bg-muted"
/>
))}
</div>
</CardContent>
</Card>
);
}
if (defectClassesQuery.isError || !defectClasses.length || !selectedClass) {
return null;
}
return ( return (
<Tabs
value={selectedClass}
onValueChange={handleClassChange}
orientation="vertical"
className="w-full"
>
<Card className="w-full"> <Card className="w-full">
<CardContent className="grid gap-0 p-0 lg:grid-cols-[minmax(220px,280px)_1fr] lg:divide-x lg:divide-border"> <CardContent className="p-5">
<div className="space-y-4 px-5"> <div className="min-w-0">
<div className="flex items-center gap-2">
<ListChecks className="size-5 shrink-0 text-primary" />
<h2 className="text-base font-semibold">Detected Issues</h2>
<Badge
variant="secondary"
className="min-w-5 min-h-5 justify-center rounded-full"
>
{defectClasses.length}
</Badge>
</div>
<TabsList className="flex h-auto w-full flex-col items-stretch gap-2 bg-transparent p-0">
{defectClasses.map((item) => {
const statusConfig =
DEFECT_CLASS_STATUS_CONFIG[item.assignment_status];
return (
<TabsTrigger
key={item.class_name}
value={item.class_name}
className={cn(
'group h-auto w-full flex-none items-center justify-between! gap-2 rounded-lg border bg-muted/30 px-3 py-2.5 text-sm text-muted-foreground shadow-none after:hidden',
'data-[state=active]:bg-muted data-[state=active]:text-foreground',
)}
>
<span className="min-w-0 truncate text-left font-medium">
{item.display_name}
</span>
<Badge
className={cn(
'shrink-0 rounded-full',
statusConfig.badgeClassName,
)}
>
<Component className="size-3" />
{statusConfig.label}
</Badge>
</TabsTrigger>
);
})}
</TabsList>
</div>
<div className="min-w-0 px-5">
<TicketClassDetectionPreview <TicketClassDetectionPreview
ticketId={ticketId} ticketId={ticketId}
videoId={previewVideoId} videoId={previewVideoId}
defectClassName={selectedClass} assignmentId={assignmentId}
displayName={previewDisplayName} totalCount={ticket?.ai_result.detection_count}
totalCount={selectedIssue?.count}
/> />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</Tabs>
); );
} }

View File

@@ -1,5 +1,6 @@
'use client'; 'use client';
import { useState } from 'react';
import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
@@ -31,11 +32,14 @@ export default function TicketDetailPage() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { ticketId } = useParams() as { ticketId: string }; const { ticketId } = useParams() as { ticketId: string };
const defectClass = searchParams.get('defect_class') ?? undefined; const defectClass = searchParams.get('defect_class') ?? undefined;
const [selectedAssignmentId, setSelectedAssignmentId] = useState<number>();
const overviewQuery = useTicketOverviewQuery(ticketId); const overviewQuery = useTicketOverviewQuery(ticketId);
const classDetailQuery = useTicketClassDetailQuery(ticketId, defectClass); const classDetailQuery = useTicketClassDetailQuery(ticketId, defectClass);
const overview = overviewQuery.data; const overview = overviewQuery.data;
const classDetail = classDetailQuery.data; const classDetail = classDetailQuery.data;
const activeAssignmentId =
selectedAssignmentId ?? overview?.assignments?.[0]?.id;
const isClassDetailLoading = Boolean( const isClassDetailLoading = Boolean(
defectClass && classDetailQuery.isLoading, defectClass && classDetailQuery.isLoading,
); );
@@ -59,7 +63,7 @@ export default function TicketDetailPage() {
} }
return ( return (
<main className="relative z-10 space-y-5 xl:flex xl:h-[calc(100vh-6.5rem)] xl:min-h-0 xl:flex-col xl:gap-5 xl:space-y-0 xl:overflow-hidden"> <main className="relative z-10 min-w-0 max-w-full space-y-5 xl:flex xl:h-[calc(100vh-6.5rem)] xl:min-h-0 xl:flex-col xl:gap-5 xl:space-y-0 xl:overflow-hidden">
<div className="xl:shrink-0"> <div className="xl:shrink-0">
{overview ? ( {overview ? (
<TicketDetailHeader ticket={overview} /> <TicketDetailHeader ticket={overview} />
@@ -68,8 +72,8 @@ export default function TicketDetailPage() {
)} )}
</div> </div>
<div className="grid items-start gap-5 xl:min-h-0 xl:flex-1 xl:grid-cols-[minmax(0,7fr)_minmax(0,3fr)] xl:items-stretch xl:overflow-hidden"> <div className="grid min-w-0 items-start gap-5 xl:min-h-0 xl:flex-1 xl:grid-cols-[minmax(0,7fr)_minmax(0,3fr)] xl:items-stretch xl:overflow-hidden">
<div className="space-y-5 xl:min-h-0 xl:overflow-y-auto xl:overscroll-contain xl:pr-2"> <div className="min-w-0 space-y-5 xl:min-h-0 xl:overflow-y-auto xl:overscroll-contain xl:pr-2">
{overview ? ( {overview ? (
<TicketOverviewCard ticket={overview} /> <TicketOverviewCard ticket={overview} />
) : ( ) : (
@@ -79,16 +83,22 @@ export default function TicketDetailPage() {
<TicketAssignmentsCard <TicketAssignmentsCard
ticketId={ticketId} ticketId={ticketId}
initialAssignments={overview.assignments} initialAssignments={overview.assignments}
selectedAssignmentId={activeAssignmentId}
onAssignmentChange={setSelectedAssignmentId}
/> />
) : null} ) : null}
<TicketDefectClassTabs ticketId={ticketId} ticket={overview} /> <TicketDefectClassTabs
ticketId={ticketId}
ticket={overview}
assignmentId={activeAssignmentId}
/>
{isClassDetailLoading ? <TicketClassContentSkeleton /> : null} {isClassDetailLoading ? <TicketClassContentSkeleton /> : null}
{!isClassDetailLoading && classDetail ? ( {!isClassDetailLoading && classDetail ? (
<TicketHistoryCard ticket={classDetail} /> <TicketHistoryCard ticket={classDetail} />
) : null} ) : null}
</div> </div>
<div className="space-y-3 self-start xl:min-h-0 xl:self-stretch xl:overflow-y-auto xl:overscroll-contain xl:pr-2"> <div className="min-w-0 space-y-3 self-start xl:min-h-0 xl:self-stretch xl:overflow-y-auto xl:overscroll-contain xl:pr-2">
{isClassDetailLoading ? <TicketClassActionsSkeleton /> : null} {isClassDetailLoading ? <TicketClassActionsSkeleton /> : null}
{!isClassDetailLoading && classDetail ? ( {!isClassDetailLoading && classDetail ? (
<TicketStatusActions ticket={classDetail} /> <TicketStatusActions ticket={classDetail} />

View File

@@ -120,12 +120,14 @@ export function useTicketDetectionsQuery(
() => ({ () => ({
skip: params.skip ?? 0, skip: params.skip ?? 0,
limit: params.limit ?? 1, limit: params.limit ?? 1,
assignment_id: params.assignment_id,
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',
search: params.search, search: params.search,
}), }),
[ [
params.assignment_id,
params.class_name, params.class_name,
params.limit, params.limit,
params.min_confidence, params.min_confidence,
@@ -220,7 +222,7 @@ export function useTicketClassReviewDetectionsQuery(
export function useDiscardDetectionMutation( export function useDiscardDetectionMutation(
ticketId: string, ticketId: string,
videoId: string | undefined, videoId: string | undefined,
defectClass: string, defectClass?: string,
) { ) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -244,9 +246,13 @@ export function useDiscardDetectionMutation(
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ticketKeys.overview(ticketId), queryKey: ticketKeys.overview(ticketId),
}), }),
...(defectClass
? [
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ticketKeys.classDetail(ticketId, defectClass), queryKey: ticketKeys.classDetail(ticketId, defectClass),
}), }),
]
: []),
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ticketKeys.defectClasses(ticketId), queryKey: ticketKeys.defectClasses(ticketId),
}), }),

View File

@@ -73,6 +73,7 @@ export const videoService = {
params: { params: {
skip: params?.skip ?? 0, skip: params?.skip ?? 0,
limit: params?.limit ?? 1, limit: params?.limit ?? 1,
assignment_id: params?.assignment_id,
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,

View File

@@ -110,6 +110,7 @@ export type DetectionResultLog = DetectionResultItem & {
export type VideoDetectionsParams = { export type VideoDetectionsParams = {
skip?: number; skip?: number;
limit?: number; limit?: number;
assignment_id?: number;
class_name?: string | string[]; class_name?: string | string[];
proof_status?: DetectionProofStatus; proof_status?: DetectionProofStatus;
min_confidence?: number; min_confidence?: number;