164 lines
5.7 KiB
TypeScript
164 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
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 { 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 { useTicketDefectClassesQuery } from '../../hooks/useTicketQueries';
|
|
import { TicketClassDetectionPreview } from './TicketClassDetectionPreview';
|
|
|
|
interface TicketDefectClassTabsProps {
|
|
ticketId: string;
|
|
ticket?: TicketOverviewDetail;
|
|
}
|
|
|
|
export function TicketDefectClassTabs({
|
|
ticketId,
|
|
ticket,
|
|
}: TicketDefectClassTabsProps) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
const defectClassParam = searchParams.get('defect_class');
|
|
const defectClassesQuery = useTicketDefectClassesQuery(ticketId);
|
|
const defectClasses = useMemo(
|
|
() => defectClassesQuery.data?.defect_classes ?? [],
|
|
[defectClassesQuery.data?.defect_classes],
|
|
);
|
|
const [selectedClass, setSelectedClass] = useState<string>();
|
|
|
|
useEffect(() => {
|
|
if (!defectClasses.length) return;
|
|
|
|
const hasUrlClass = defectClasses.some(
|
|
(item) => item.class_name === defectClassParam,
|
|
);
|
|
|
|
if (hasUrlClass) {
|
|
setSelectedClass(defectClassParam ?? undefined);
|
|
return;
|
|
}
|
|
|
|
const firstClass = defectClasses[0].class_name;
|
|
const nextParams = new URLSearchParams(searchParams.toString());
|
|
nextParams.set('defect_class', firstClass);
|
|
setSelectedClass(firstClass);
|
|
router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false });
|
|
}, [defectClassParam, defectClasses, 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;
|
|
|
|
if (defectClassesQuery.isLoading) {
|
|
return (
|
|
<Card className="w-full">
|
|
<CardContent className="space-y-4 p-5">
|
|
<div className="h-5 w-24 animate-pulse rounded 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 (
|
|
<Tabs
|
|
value={selectedClass}
|
|
onValueChange={handleClassChange}
|
|
orientation="vertical"
|
|
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">
|
|
<div className="space-y-4 p-5">
|
|
<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 border-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 gap-1.5 rounded-full border-0 px-2.5 py-1 text-xs font-medium',
|
|
statusConfig.badgeClassName,
|
|
)}
|
|
>
|
|
<Component className="size-3" />
|
|
{statusConfig.label}
|
|
</Badge>
|
|
</TabsTrigger>
|
|
);
|
|
})}
|
|
</TabsList>
|
|
</div>
|
|
|
|
<div className="min-w-0 p-5">
|
|
<TicketClassDetectionPreview
|
|
videoId={previewVideoId}
|
|
defectClassName={selectedClass}
|
|
displayName={previewDisplayName}
|
|
totalCount={selectedIssue?.count}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</Tabs>
|
|
);
|
|
}
|