diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx
index a0a9745..52b98da 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx
@@ -2,22 +2,27 @@
import { useEffect, useMemo, useState } from 'react';
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 { Card, CardContent, CardHeader, CardTitle } 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 { getDefectVisual } from '@/constants/defectVisualConfig';
import { cn } from '@/lib/utils';
+import type { TicketOverviewDetail } from '@/types';
+import { formatDate } from '@/utils/date';
import { useTicketDefectClassesQuery } from '../../hooks/useTicketQueries';
interface TicketDefectClassTabsProps {
ticketId: string;
+ ticket?: TicketOverviewDetail;
}
export function TicketDefectClassTabs({
ticketId,
+ ticket,
}: TicketDefectClassTabsProps) {
const router = useRouter();
const pathname = usePathname();
@@ -56,20 +61,27 @@ export function TicketDefectClassTabs({
setSelectedClass(className);
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) {
return (
-
+
-
-
+
{Array.from({ length: 3 }).map((_, index) => (
))}
+
);
@@ -87,49 +99,136 @@ export function TicketDefectClassTabs({
className="w-full"
>
-
-
-
- Issues
-
- {defectClasses.length}
-
-
-
-
-
- {defectClasses.map((item) => {
- const statusConfig =
- DEFECT_CLASS_STATUS_CONFIG[item.assignment_status];
+
+
+
+
+
Detected Issues
+
+ {defectClasses.length}
+
+
- return (
-
-
- {item.display_name}
-
-
+ {defectClasses.map((item) => {
+ const statusConfig =
+ DEFECT_CLASS_STATUS_CONFIG[item.assignment_status];
+
+ return (
+
-
- {statusConfig.label}
-
-
- );
- })}
-
+
+ {item.display_name}
+
+
+
+ {statusConfig.label}
+
+
+ );
+ })}
+
+
+
+
+ {selectedIssue ? (
+
+
+
+
+
+
+
+ {selectedIssue.display_name}
+
+
+ {selectedIssue.count} Detections
+
+
+
+
+
+
+
+
+ Current Issue
+
+
+ {selectedIssue.display_name}
+
+
+
+
+ Current Issue Count
+
+
+ {selectedIssue.count}
+
+
+ {completedAt ? (
+
+
+
+ AI Completed At
+
+
+ {formatDate(completedAt)}
+
+
+ ) : null}
+ {videoName ? (
+
+
+
+ Video
+
+
+ {videoName}
+
+
+ ) : null}
+
+
+
+ ) : (
+
+
+ Select an issue to view details.
+
+
+ )}
+
diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx
index 082c1cc..b3a40b3 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx
@@ -12,7 +12,7 @@ export function TicketDetailHeader({
ticket: TicketOverviewDetail;
}) {
const router = useRouter();
- const ticketLabel = ticket.ticket_name;
+ const ticketLabel = ticket.ticket_name || ticket.id;
return (
diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx
index 5ad1115..aed7a9d 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx
@@ -6,7 +6,6 @@ import {
CalendarDays,
Clock3,
Gauge,
- LayoutDashboard,
MapPin,
Monitor,
UserRound,
@@ -62,13 +61,11 @@ function SummaryCard({
value,
colorClassName,
cardClassName,
- icon: Icon,
}: {
label: string;
value: number;
colorClassName: string;
cardClassName: string;
- icon?: IconComponent;
}) {
return (
-
-
{label}
-
- {value}
-
-
-
-
- );
-}
-
export function TicketOverviewCard({
ticket,
- defectClass,
}: {
ticket: TicketOverviewDetail;
- defectClass?: string;
}) {
const router = useRouter();
- const currentIssue =
- ticket.ai_result.detections_by_class.find(
- (item) => item.class_name === defectClass,
- ) ?? ticket.ai_result.detections_by_class[0];
- const currentVisual = getDefectVisual(currentIssue?.class_name ?? '');
- const minutes = Math.floor(ticket.video_metadata.duration_seconds / 60);
- const seconds = Math.floor(ticket.video_metadata.duration_seconds % 60);
- const duration = `${minutes}:${seconds.toString().padStart(2, '0')}`;
+ const videoMetadata = ticket.video_metadata;
+ const duration =
+ typeof videoMetadata?.duration_seconds === 'number'
+ ? `${Math.floor(videoMetadata.duration_seconds / 60)}:${Math.floor(
+ videoMetadata.duration_seconds % 60,
+ )
+ .toString()
+ .padStart(2, '0')}`
+ : null;
const chainageName =
ticket.location?.chainage?.name || ticket.chainage_name || null;
const packageName = ticket.location?.package?.name || null;
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 (
-
-
-
-
-
- {formatDate(ticket.timestamps.created_at)}
-
-
-
-
-
- {ticket.uploader.name}
-
-
-
-
-
- {locationLabel}
-
-
-
-
+ {uploadedOn || uploaderName || locationLabel ? (
+
+
+ {uploadedOn ? (
+
+
+
+ {formatDate(uploadedOn)}
+
+
+ ) : null}
+ {uploaderName ? (
+
+
+
+ {uploaderName}
+
+
+ ) : null}
+ {locationLabel ? (
+
+
+
+ {locationLabel}
+
+
+ ) : null}
+
+
+ ) : null}
@@ -185,14 +171,14 @@ export function TicketOverviewCard({
AI Detection Summary
- {ticket.video_id ? (
+ {analysisVideoId ? (
-
-
-
-
-
-
-
-
-
-
-
-
- Ticket Overview
-
-
-
-
-
-
+ {hasVideoMetrics ? (
+
+ {videoName ? (
+
+ ) : null}
+ {videoMetadata?.fps ? (
+
+ ) : null}
+ {duration ? (
+
+ ) : null}
+ {videoMetadata?.resolution?.label ? (
+
+ ) : null}
+
+ ) : null}
diff --git a/src/app/(modules)/ticket/[ticketId]/page.tsx b/src/app/(modules)/ticket/[ticketId]/page.tsx
index 6b3aeef..156129b 100644
--- a/src/app/(modules)/ticket/[ticketId]/page.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/page.tsx
@@ -68,10 +68,11 @@ export default function TicketDetailPage() {
{overview ? (
-
+
) : (
)}
+
{isClassDetailLoading ? : null}
{!isClassDetailLoading && classDetail ? (
@@ -79,7 +80,6 @@ export default function TicketDetailPage() {
-
{isClassDetailLoading ? : null}
{!isClassDetailLoading && classDetail ? (
diff --git a/src/types/ticket/detail.ts b/src/types/ticket/detail.ts
index 491aa59..9595d6e 100644
--- a/src/types/ticket/detail.ts
+++ b/src/types/ticket/detail.ts
@@ -116,24 +116,25 @@ export interface TicketDetectionClassCount {
export interface TicketOverviewDetail {
id: string;
- ticket_name: string;
- video_id: string;
- chainage_id: string;
- chainage_name: string;
+ ticket_name?: string | null;
+ video_id?: string | null;
+ video?: TicketVideo | null;
+ chainage_id?: string | null;
+ chainage_name?: string | null;
location: TicketLocation | null;
- video_metadata: TicketVideoMetadata;
- uploader: {
+ video_metadata?: TicketVideoMetadata | null;
+ uploader?: {
user_id: number;
- name: string;
- email: string;
- };
+ name: string | null;
+ email: string | null;
+ } | null;
ai_result: {
detection_count: number;
completed_at: string;
available_defect_classes: string[];
detections_by_class: TicketDetectionClassCount[];
};
- timestamps: TicketTimestamps;
+ timestamps?: TicketTimestamps | null;
}
export interface TicketDetail {