chore: class location change
This commit is contained in:
@@ -2,22 +2,27 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { AlertTriangle, Component } from 'lucide-react';
|
import { CalendarCheck2, Component, FileVideo, ListChecks } from 'lucide-react';
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { DEFECT_CLASS_STATUS_CONFIG } from '@/constants/defectClassStatus';
|
import { DEFECT_CLASS_STATUS_CONFIG } from '@/constants/defectClassStatus';
|
||||||
|
import { getDefectVisual } from '@/constants/defectVisualConfig';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { TicketOverviewDetail } from '@/types';
|
||||||
|
import { formatDate } from '@/utils/date';
|
||||||
|
|
||||||
import { useTicketDefectClassesQuery } from '../../hooks/useTicketQueries';
|
import { useTicketDefectClassesQuery } from '../../hooks/useTicketQueries';
|
||||||
|
|
||||||
interface TicketDefectClassTabsProps {
|
interface TicketDefectClassTabsProps {
|
||||||
ticketId: string;
|
ticketId: string;
|
||||||
|
ticket?: TicketOverviewDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TicketDefectClassTabs({
|
export function TicketDefectClassTabs({
|
||||||
ticketId,
|
ticketId,
|
||||||
|
ticket,
|
||||||
}: TicketDefectClassTabsProps) {
|
}: TicketDefectClassTabsProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -56,20 +61,27 @@ export function TicketDefectClassTabs({
|
|||||||
setSelectedClass(className);
|
setSelectedClass(className);
|
||||||
router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false });
|
router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false });
|
||||||
};
|
};
|
||||||
|
const selectedIssue = ticket?.ai_result.detections_by_class.find(
|
||||||
|
(item) => item.class_name === selectedClass,
|
||||||
|
);
|
||||||
|
const selectedIssueVisual = getDefectVisual(selectedIssue?.class_name ?? '');
|
||||||
|
const SelectedIssueIcon = selectedIssueVisual.icon;
|
||||||
|
const completedAt = ticket?.ai_result.completed_at;
|
||||||
|
const videoName = ticket?.video?.name;
|
||||||
|
|
||||||
if (defectClassesQuery.isLoading) {
|
if (defectClassesQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardContent className="space-y-4 p-5">
|
||||||
<div className="h-5 w-24 animate-pulse rounded bg-muted" />
|
<div className="h-5 w-24 animate-pulse rounded bg-muted" />
|
||||||
</CardHeader>
|
<div className="flex flex-col gap-2">
|
||||||
<CardContent className="flex flex-col gap-2">
|
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="h-11 w-full animate-pulse rounded-lg bg-muted"
|
className="h-11 w-full animate-pulse rounded-lg bg-muted"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -87,19 +99,19 @@ export function TicketDefectClassTabs({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardContent className="grid gap-0 p-0 lg:grid-cols-[minmax(220px,280px)_1fr] lg:divide-x lg:divide-border">
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<div className="space-y-4 p-5">
|
||||||
<AlertTriangle className="size-5 shrink-0 text-primary" />
|
<div className="flex items-center gap-2">
|
||||||
<span>Issues</span>
|
<ListChecks className="size-5 shrink-0 text-primary" />
|
||||||
|
<h2 className="text-base font-semibold">Detected Issues</h2>
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="min-w-5 min-h-5 justify-center rounded-full"
|
className="min-w-5 min-h-5 justify-center rounded-full"
|
||||||
>
|
>
|
||||||
{defectClasses.length}
|
{defectClasses.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</CardTitle>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<TabsList className="flex h-auto w-full flex-col items-stretch gap-2 bg-transparent p-0">
|
<TabsList className="flex h-auto w-full flex-col items-stretch gap-2 bg-transparent p-0">
|
||||||
{defectClasses.map((item) => {
|
{defectClasses.map((item) => {
|
||||||
const statusConfig =
|
const statusConfig =
|
||||||
@@ -130,6 +142,93 @@ export function TicketDefectClassTabs({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 p-5">
|
||||||
|
{selectedIssue ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'flex size-10 shrink-0 items-center justify-center rounded-full',
|
||||||
|
selectedIssueVisual.cardClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SelectedIssueIcon
|
||||||
|
className={cn(
|
||||||
|
'size-5',
|
||||||
|
selectedIssueVisual.colorClassName,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
|
<h3 className="truncate text-lg font-semibold text-foreground">
|
||||||
|
{selectedIssue.display_name}
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 rounded-full border-0 px-2.5 py-1 text-xs font-semibold',
|
||||||
|
selectedIssueVisual.cardClassName,
|
||||||
|
selectedIssueVisual.colorClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedIssue.count} Detections
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
|
Current Issue
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-semibold text-foreground">
|
||||||
|
{selectedIssue.display_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
|
Current Issue Count
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-semibold text-violet-600">
|
||||||
|
{selectedIssue.count}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{completedAt ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||||
|
<CalendarCheck2 className="size-4" />
|
||||||
|
<span>AI Completed At</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-semibold text-foreground">
|
||||||
|
{formatDate(completedAt)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{videoName ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||||
|
<FileVideo className="size-4" />
|
||||||
|
<span>Video</span>
|
||||||
|
</div>
|
||||||
|
<p className="truncate text-sm font-semibold text-foreground">
|
||||||
|
{videoName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex min-h-48 items-center justify-center text-center">
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
|
Select an issue to view details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function TicketDetailHeader({
|
|||||||
ticket: TicketOverviewDetail;
|
ticket: TicketOverviewDetail;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const ticketLabel = ticket.ticket_name;
|
const ticketLabel = ticket.ticket_name || ticket.id;
|
||||||
|
|
||||||
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">
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
CalendarDays,
|
CalendarDays,
|
||||||
Clock3,
|
Clock3,
|
||||||
Gauge,
|
Gauge,
|
||||||
LayoutDashboard,
|
|
||||||
MapPin,
|
MapPin,
|
||||||
Monitor,
|
Monitor,
|
||||||
UserRound,
|
UserRound,
|
||||||
@@ -62,13 +61,11 @@ function SummaryCard({
|
|||||||
value,
|
value,
|
||||||
colorClassName,
|
colorClassName,
|
||||||
cardClassName,
|
cardClassName,
|
||||||
icon: Icon,
|
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
colorClassName: string;
|
colorClassName: string;
|
||||||
cardClassName: string;
|
cardClassName: string;
|
||||||
icon?: IconComponent;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -103,81 +100,70 @@ function VideoMetric({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OverviewStat({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
valueClassName,
|
|
||||||
icon: Icon,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: React.ReactNode;
|
|
||||||
valueClassName?: string;
|
|
||||||
icon: IconComponent;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-3 rounded-xl bg-muted/40 p-4 transition-colors hover:bg-muted/60">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<MetaLabel>{label}</MetaLabel>
|
|
||||||
<p
|
|
||||||
className={cn(
|
|
||||||
'mt-2 truncate text-lg font-bold tracking-tight text-foreground',
|
|
||||||
valueClassName,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Icon className={cn('size-5 shrink-0 opacity-80', valueClassName)} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TicketOverviewCard({
|
export function TicketOverviewCard({
|
||||||
ticket,
|
ticket,
|
||||||
defectClass,
|
|
||||||
}: {
|
}: {
|
||||||
ticket: TicketOverviewDetail;
|
ticket: TicketOverviewDetail;
|
||||||
defectClass?: string;
|
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const currentIssue =
|
const videoMetadata = ticket.video_metadata;
|
||||||
ticket.ai_result.detections_by_class.find(
|
const duration =
|
||||||
(item) => item.class_name === defectClass,
|
typeof videoMetadata?.duration_seconds === 'number'
|
||||||
) ?? ticket.ai_result.detections_by_class[0];
|
? `${Math.floor(videoMetadata.duration_seconds / 60)}:${Math.floor(
|
||||||
const currentVisual = getDefectVisual(currentIssue?.class_name ?? '');
|
videoMetadata.duration_seconds % 60,
|
||||||
const minutes = Math.floor(ticket.video_metadata.duration_seconds / 60);
|
)
|
||||||
const seconds = Math.floor(ticket.video_metadata.duration_seconds % 60);
|
.toString()
|
||||||
const duration = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
.padStart(2, '0')}`
|
||||||
|
: null;
|
||||||
const chainageName =
|
const chainageName =
|
||||||
ticket.location?.chainage?.name || ticket.chainage_name || null;
|
ticket.location?.chainage?.name || ticket.chainage_name || null;
|
||||||
const packageName = ticket.location?.package?.name || null;
|
const packageName = ticket.location?.package?.name || null;
|
||||||
const locationLabel =
|
const locationLabel =
|
||||||
[chainageName, packageName].filter(Boolean).join(', ') || '—';
|
[chainageName, packageName].filter(Boolean).join(', ') || null;
|
||||||
|
const uploadedOn =
|
||||||
|
ticket.timestamps?.created_at || ticket.ai_result.completed_at || null;
|
||||||
|
const uploaderName = ticket.uploader?.name || null;
|
||||||
|
const analysisVideoId = ticket.video_id || ticket.video?.id || null;
|
||||||
|
const videoName = ticket.video?.name || null;
|
||||||
|
const hasVideoMetrics = Boolean(
|
||||||
|
videoName ||
|
||||||
|
videoMetadata?.fps ||
|
||||||
|
duration ||
|
||||||
|
videoMetadata?.resolution?.label,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
|
{uploadedOn || uploaderName || locationLabel ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="grid gap-4 py-4 sm:grid-cols-3 sm:divide-x">
|
<CardContent className="grid gap-4 py-4 sm:grid-cols-3 sm:divide-x">
|
||||||
|
{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" />
|
||||||
<OverviewField label="Uploaded On">
|
<OverviewField label="Uploaded On">
|
||||||
<MetaValue>{formatDate(ticket.timestamps.created_at)}</MetaValue>
|
<MetaValue>{formatDate(uploadedOn)}</MetaValue>
|
||||||
</OverviewField>
|
</OverviewField>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
{uploaderName ? (
|
||||||
<div className="flex items-center gap-3 sm:px-4">
|
<div className="flex items-center gap-3 sm:px-4">
|
||||||
<UserRound className="size-5 shrink-0 text-muted-foreground" />
|
<UserRound className="size-5 shrink-0 text-muted-foreground" />
|
||||||
<OverviewField label="Uploaded By">
|
<OverviewField label="Uploaded By">
|
||||||
<MetaValue>{ticket.uploader.name}</MetaValue>
|
<MetaValue>{uploaderName}</MetaValue>
|
||||||
</OverviewField>
|
</OverviewField>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
{locationLabel ? (
|
||||||
<div className="flex items-center gap-3 sm:pl-4">
|
<div className="flex items-center gap-3 sm:pl-4">
|
||||||
<MapPin className="size-5 shrink-0 text-muted-foreground" />
|
<MapPin className="size-5 shrink-0 text-muted-foreground" />
|
||||||
<OverviewField label="Location">
|
<OverviewField label="Location">
|
||||||
<MetaValue>{locationLabel}</MetaValue>
|
<MetaValue>{locationLabel}</MetaValue>
|
||||||
</OverviewField>
|
</OverviewField>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -185,14 +171,14 @@ export function TicketOverviewCard({
|
|||||||
<Activity className="size-5 text-primary" />
|
<Activity className="size-5 text-primary" />
|
||||||
AI Detection Summary
|
AI Detection Summary
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{ticket.video_id ? (
|
{analysisVideoId ? (
|
||||||
<CardAction>
|
<CardAction>
|
||||||
<PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}>
|
<PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-primary hover:bg-primary/10 hover:text-primary"
|
className="text-primary hover:bg-primary/10 hover:text-primary"
|
||||||
onClick={() => router.push(`/results/${ticket.video_id}`)}
|
onClick={() => router.push(`/results/${analysisVideoId}`)}
|
||||||
>
|
>
|
||||||
View Analysis
|
View Analysis
|
||||||
<ArrowRight className="size-3.5" />
|
<ArrowRight className="size-3.5" />
|
||||||
@@ -216,53 +202,31 @@ export function TicketOverviewCard({
|
|||||||
key={item.class_name}
|
key={item.class_name}
|
||||||
label={item.display_name}
|
label={item.display_name}
|
||||||
value={item.count}
|
value={item.count}
|
||||||
icon={visual.icon}
|
|
||||||
colorClassName={visual.colorClassName}
|
colorClassName={visual.colorClassName}
|
||||||
cardClassName={visual.cardClassName}
|
cardClassName={visual.cardClassName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{hasVideoMetrics ? (
|
||||||
<div className="grid gap-3 rounded-lg bg-muted/35 px-4 py-3 text-sm sm:grid-cols-3">
|
<div className="grid gap-3 rounded-lg bg-muted/35 px-4 py-3 text-sm sm:grid-cols-3">
|
||||||
<VideoMetric
|
{videoName ? (
|
||||||
icon={Gauge}
|
<VideoMetric icon={Monitor} label={videoName} />
|
||||||
label={`${ticket.video_metadata.fps} FPS`}
|
) : null}
|
||||||
/>
|
{videoMetadata?.fps ? (
|
||||||
|
<VideoMetric icon={Gauge} label={`${videoMetadata.fps} FPS`} />
|
||||||
|
) : null}
|
||||||
|
{duration ? (
|
||||||
<VideoMetric icon={Clock3} label={`${duration} Duration`} />
|
<VideoMetric icon={Clock3} label={`${duration} Duration`} />
|
||||||
|
) : null}
|
||||||
|
{videoMetadata?.resolution?.label ? (
|
||||||
<VideoMetric
|
<VideoMetric
|
||||||
icon={Monitor}
|
icon={Monitor}
|
||||||
label={`${ticket.video_metadata.resolution.label} Resolution`}
|
label={`${videoMetadata.resolution.label} Resolution`}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
) : null}
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<LayoutDashboard className="size-5 text-primary" />
|
|
||||||
Ticket Overview
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid gap-3 md:grid-cols-3">
|
|
||||||
<OverviewStat
|
|
||||||
icon={currentVisual.icon}
|
|
||||||
label="Current Issue"
|
|
||||||
value={currentIssue?.display_name ?? '-'}
|
|
||||||
valueClassName={currentVisual.colorClassName}
|
|
||||||
/>
|
|
||||||
<OverviewStat
|
|
||||||
icon={Activity}
|
|
||||||
valueClassName="text-violet-600"
|
|
||||||
label="Current Issue Count"
|
|
||||||
value={currentIssue?.count ?? 0}
|
|
||||||
/>
|
|
||||||
<OverviewStat
|
|
||||||
icon={LayoutDashboard}
|
|
||||||
valueClassName="text-violet-600"
|
|
||||||
label="Total AI Detections"
|
|
||||||
value={ticket.ai_result.detection_count}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,10 +68,11 @@ export default function TicketDetailPage() {
|
|||||||
<div className="grid gap-5 xl:grid-cols-[minmax(0,7fr)_minmax(0,3fr)]">
|
<div className="grid gap-5 xl:grid-cols-[minmax(0,7fr)_minmax(0,3fr)]">
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{overview ? (
|
{overview ? (
|
||||||
<TicketOverviewCard ticket={overview} defectClass={defectClass} />
|
<TicketOverviewCard ticket={overview} />
|
||||||
) : (
|
) : (
|
||||||
<TicketOverviewCardSkeleton />
|
<TicketOverviewCardSkeleton />
|
||||||
)}
|
)}
|
||||||
|
<TicketDefectClassTabs ticketId={ticketId} ticket={overview} />
|
||||||
{isClassDetailLoading ? <TicketClassContentSkeleton /> : null}
|
{isClassDetailLoading ? <TicketClassContentSkeleton /> : null}
|
||||||
{!isClassDetailLoading && classDetail ? (
|
{!isClassDetailLoading && classDetail ? (
|
||||||
<TicketHistoryCard ticket={classDetail} />
|
<TicketHistoryCard ticket={classDetail} />
|
||||||
@@ -79,7 +80,6 @@ export default function TicketDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3 self-start">
|
<div className="space-y-3 self-start">
|
||||||
<TicketDefectClassTabs ticketId={ticketId} />
|
|
||||||
{isClassDetailLoading ? <TicketClassActionsSkeleton /> : null}
|
{isClassDetailLoading ? <TicketClassActionsSkeleton /> : null}
|
||||||
{!isClassDetailLoading && classDetail ? (
|
{!isClassDetailLoading && classDetail ? (
|
||||||
<TicketStatusActions ticket={classDetail} />
|
<TicketStatusActions ticket={classDetail} />
|
||||||
|
|||||||
@@ -116,24 +116,25 @@ export interface TicketDetectionClassCount {
|
|||||||
|
|
||||||
export interface TicketOverviewDetail {
|
export interface TicketOverviewDetail {
|
||||||
id: string;
|
id: string;
|
||||||
ticket_name: string;
|
ticket_name?: string | null;
|
||||||
video_id: string;
|
video_id?: string | null;
|
||||||
chainage_id: string;
|
video?: TicketVideo | null;
|
||||||
chainage_name: string;
|
chainage_id?: string | null;
|
||||||
|
chainage_name?: string | null;
|
||||||
location: TicketLocation | null;
|
location: TicketLocation | null;
|
||||||
video_metadata: TicketVideoMetadata;
|
video_metadata?: TicketVideoMetadata | null;
|
||||||
uploader: {
|
uploader?: {
|
||||||
user_id: number;
|
user_id: number;
|
||||||
name: string;
|
name: string | null;
|
||||||
email: string;
|
email: string | null;
|
||||||
};
|
} | null;
|
||||||
ai_result: {
|
ai_result: {
|
||||||
detection_count: number;
|
detection_count: number;
|
||||||
completed_at: string;
|
completed_at: string;
|
||||||
available_defect_classes: string[];
|
available_defect_classes: string[];
|
||||||
detections_by_class: TicketDetectionClassCount[];
|
detections_by_class: TicketDetectionClassCount[];
|
||||||
};
|
};
|
||||||
timestamps: TicketTimestamps;
|
timestamps?: TicketTimestamps | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketDetail {
|
export interface TicketDetail {
|
||||||
|
|||||||
Reference in New Issue
Block a user