refactor(ticket): make details lot-based and remove class-detail flow

This commit is contained in:
2026-07-31 12:49:38 +05:30
parent 44d3be5d27
commit 2caeae7c91
20 changed files with 84 additions and 460 deletions

View File

@@ -1,28 +0,0 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
export function TicketClassActionsSkeleton() {
return (
<Card aria-label="Loading defect class actions">
<CardHeader>
<div className="flex items-center gap-2">
<Skeleton className="size-4 rounded-lg" />
<Skeleton className="h-5 w-32" />
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-12" />
<Skeleton className="h-20 w-full" />
</div>
<Skeleton className="h-10 w-full" />
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,69 +0,0 @@
'use client';
import { useEffect } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { Card, CardContent } from '@/components/ui/card';
import type { TicketOverviewDetail } from '@/types';
import { useTicketDefectClassesQuery } from '../../hooks/useTicketQueries';
import { TicketClassDetectionPreview } from './TicketClassDetectionPreview';
interface TicketDefectClassTabsProps {
ticketId: string;
ticket?: TicketOverviewDetail;
assignmentId: number;
}
export function TicketDefectClassTabs({
ticketId,
ticket,
assignmentId,
}: TicketDefectClassTabsProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const defectClassParam = searchParams.get('defect_class');
const defectClassesQuery = useTicketDefectClassesQuery(ticketId);
useEffect(() => {
const defectClasses = defectClassesQuery.data?.defect_classes ?? [];
if (!defectClasses.length) return;
const hasUrlClass = defectClasses.some(
(item) => item.class_name === defectClassParam,
);
if (hasUrlClass) return;
const firstClass = defectClasses[0].class_name;
const nextParams = new URLSearchParams(searchParams.toString());
nextParams.set('defect_class', firstClass);
router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false });
}, [
defectClassParam,
defectClassesQuery.data?.defect_classes,
pathname,
router,
searchParams,
]);
const previewVideoId = ticket?.video_id ?? defectClassesQuery.data?.video_id;
if (!previewVideoId) return null;
return (
<Card className="w-full">
<CardContent className="p-5">
<div className="min-w-0">
<TicketClassDetectionPreview
ticketId={ticketId}
videoId={previewVideoId}
assignmentId={assignmentId}
/>
</div>
</CardContent>
</Card>
);
}

View File

@@ -17,7 +17,7 @@ import {
} from '../../hooks/useTicketQueries';
import { DetectionDiscardDialog } from './DetectionDiscardDialog';
interface TicketClassDetectionPreviewProps {
interface TicketDetectionPreviewProps {
ticketId: string;
videoId?: string | null;
assignmentId: number;
@@ -52,7 +52,7 @@ function DetectionMetadataBar({
);
}
function TicketClassDetectionPreviewSkeleton() {
function TicketDetectionPreviewSkeleton() {
return (
<>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
@@ -91,11 +91,11 @@ function TicketClassDetectionPreviewSkeleton() {
);
}
export function TicketClassDetectionPreview({
export function TicketDetectionPreview({
ticketId,
videoId,
assignmentId,
}: TicketClassDetectionPreviewProps) {
}: TicketDetectionPreviewProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
@@ -130,7 +130,6 @@ export function TicketClassDetectionPreview({
const discardMutation = useDiscardDetectionMutation(
ticketId,
videoId ?? undefined,
activeDetection?.detection.class_name,
);
useEffect(() => {
@@ -217,7 +216,7 @@ export function TicketClassDetectionPreview({
</div>
{detectionsQuery.isLoading && !detectionsQuery.data ? (
<TicketClassDetectionPreviewSkeleton />
<TicketDetectionPreviewSkeleton />
) : detectionsQuery.isError ? (
<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">

View File

@@ -0,0 +1,33 @@
'use client';
import { Card, CardContent } from '@/components/ui/card';
import { TicketDetectionPreview } from './TicketDetectionPreview';
interface TicketDetectionPreviewCardProps {
ticketId: string;
videoId?: string | null;
assignmentId: number;
}
export function TicketDetectionPreviewCard({
ticketId,
videoId,
assignmentId,
}: TicketDetectionPreviewCardProps) {
if (!videoId) return null;
return (
<Card className="w-full">
<CardContent className="p-5">
<div className="min-w-0">
<TicketDetectionPreview
ticketId={ticketId}
videoId={videoId}
assignmentId={assignmentId}
/>
</div>
</CardContent>
</Card>
);
}

View File

@@ -2,19 +2,19 @@
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionGuard } from '@/guards';
import type { TicketAssignmentSummary, TicketDetail } from '@/types';
import type { TicketAssignmentSummary } from '@/types';
import { NoTicketAction } from './actions/NoTicketAction';
import { RequestExtensionAction } from './actions/RequestExtensionAction';
import { ReviewExtensionRequestAction } from './actions/ReviewExtensionRequestAction';
interface TicketStatusActionsProps {
ticket: TicketDetail;
ticketId: string;
assignment?: TicketAssignmentSummary;
}
function getTicketAction(
ticket: TicketDetail,
ticketId: string,
assignment?: TicketAssignmentSummary,
) {
if (assignment && !assignment.is_complete) {
@@ -27,7 +27,7 @@ function getTicketAction(
fallback={<NoTicketAction />}
>
<RequestExtensionAction
ticket={ticket}
ticketId={ticketId}
assignmentId={assignment.id}
dueAt={assignment.due_at}
/>
@@ -35,7 +35,7 @@ function getTicketAction(
}
>
<ReviewExtensionRequestAction
ticket={ticket}
ticketId={ticketId}
assignmentId={assignment.id}
/>
</PermissionGuard>
@@ -46,10 +46,10 @@ function getTicketAction(
}
export function TicketStatusActions({
ticket,
ticketId,
assignment,
}: TicketStatusActionsProps) {
const action = getTicketAction(ticket, assignment);
const action = getTicketAction(ticketId, assignment);
return action ? <div className="space-y-3">{action}</div> : null;
}

View File

@@ -1,61 +0,0 @@
'use client';
import { CalendarClock, UserCheck } from 'lucide-react';
import { PersonInfo } from '@/components/person-avatar';
import { Badge } from '@/components/ui/badge';
import type { TicketDetail } from '@/types';
import { formatDate } from '@/utils/date';
import { TicketActionCard } from './TicketActionCard';
function AssignmentField({ label, value }: { label: string; value: string }) {
return (
<div className="min-w-0 space-y-1">
<p className="text-xs text-muted-foreground">{label}</p>
<p className="truncate text-sm font-medium text-foreground">{value}</p>
</div>
);
}
export function AssignedContractorAction({ ticket }: { ticket: TicketDetail }) {
const worker = ticket.worker;
return (
<TicketActionCard icon={UserCheck} title="Assigned Contractor">
{worker ? (
<div className="space-y-5">
<div className="flex items-center justify-between gap-3">
<PersonInfo person={worker} size="lg" />
<Badge>
Assigned
</Badge>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-5 border-t pt-4">
<AssignmentField label="Role" value={worker.role?.name ?? '-'} />
<AssignmentField
label="Assigned By"
value={worker.assigned_by_name ?? '-'}
/>
<AssignmentField
label="Assigned On"
value={formatDate(worker.assigned_at)}
/>
<div className="min-w-0 space-y-1">
<p className="text-xs text-muted-foreground">Due Date</p>
<p className="flex items-center gap-1.5 text-sm font-medium text-foreground">
<CalendarClock className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate">{formatDate(worker.due_at)}</span>
</p>
</div>
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">
Assignment details are not available.
</p>
)}
</TicketActionCard>
);
}

View File

@@ -1,107 +0,0 @@
'use client';
import type { ReactNode } from 'react';
import { Lock, MessageSquare } from 'lucide-react';
import { PersonInfo } from '@/components/person-avatar';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { cn } from '@/lib/utils';
import type { TicketDetail } from '@/types';
import { formatDateOnly } from '@/utils/date';
function MetaField({
label,
value,
valueClassName,
}: {
label: string;
value: ReactNode;
valueClassName?: string;
}) {
return (
<div className="space-y-1">
<p className="text-xs font-medium tracking-wide text-muted-foreground">
{label}
</p>
<div
className={cn('text-sm font-semibold text-foreground', valueClassName)}
>
{value}
</div>
</div>
);
}
function FinalCommentsPanel({
comment,
}: {
comment: string | null | undefined;
}) {
if (!comment?.trim()) {
return (
<div className="flex min-h-28 flex-col items-center justify-center rounded-lg border border-dashed border-border bg-muted/10 px-4 py-6 text-center">
<MessageSquare className="mb-2 size-5 text-muted-foreground" />
<p className="text-sm text-muted-foreground">No comments added yet</p>
</div>
);
}
return (
<div className="rounded-lg border bg-muted/20 px-4 py-3">
<p className="text-sm leading-relaxed text-foreground">
{comment.trim()}
</p>
</div>
);
}
export function ClosedTicketAction({ ticket }: { ticket: TicketDetail }) {
const reviewer = ticket.reviewer;
const reviewedAt = reviewer?.reviewed_at ?? ticket.timestamps.updated_at;
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Resolution Details</CardTitle>
<CardDescription>
Review record for {ticket.ticket_name}
</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
<div className="space-y-4 rounded-lg border bg-muted/20 p-4">
<MetaField
label="Reviewed By"
value={<PersonInfo person={reviewer} size="lg" />}
/>
<MetaField label="Reviewed Date" value={formatDateOnly(reviewedAt)} />
</div>
<div className="space-y-2">
<p className="text-xs font-medium tracking-wide text-muted-foreground">
Review Comment
</p>
<FinalCommentsPanel comment={reviewer?.review_comment} />
</div>
<Button
type="button"
variant="secondary"
className="w-full text-muted-foreground"
disabled
>
<Lock className="size-4" />
Ticket Finalized
</Button>
</CardContent>
</Card>
);
}

View File

@@ -1,23 +1,21 @@
import { Clock3 } from 'lucide-react';
import type { TicketDetail } from '@/types';
import { TicketActionCard } from './TicketActionCard';
import { TicketExtensionRequestPanel } from './TicketExtensionRequestPanel';
export function RequestExtensionAction({
ticket,
ticketId,
assignmentId,
dueAt,
}: {
ticket: TicketDetail;
ticketId: string;
assignmentId: number;
dueAt: string | null;
}) {
return (
<TicketActionCard icon={Clock3} title="Request Extension">
<TicketExtensionRequestPanel
ticket={ticket}
ticketId={ticketId}
assignmentId={assignmentId}
dueAt={dueAt}
/>

View File

@@ -8,20 +8,19 @@ import { DatePickerSimple } from '@/components/form/DatePickerSimple';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import type { TicketDetail } from '@/types';
import { useRequestTicketExtensionMutation } from '../../../hooks/useTicketQueries';
const MAX_REASON_LENGTH = 300;
interface RequestExtensionFormProps {
ticket: TicketDetail;
ticketId: string;
assignmentId: number;
currentDueAt: string | null;
}
export function RequestExtensionForm({
ticket,
ticketId,
assignmentId,
currentDueAt,
}: RequestExtensionFormProps) {
@@ -42,9 +41,8 @@ export function RequestExtensionForm({
const [requestedDueDate, setRequestedDueDate] = useState<Date>();
const [reason, setReason] = useState('');
const requestExtensionMutation = useRequestTicketExtensionMutation(
ticket.id,
ticketId,
assignmentId,
ticket.defect_class,
);
const isPending = requestExtensionMutation.isPending;
const isRequestedDateValid = Boolean(

View File

@@ -24,7 +24,6 @@ import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import type {
TicketDetail,
TicketExtensionRequest,
TicketExtensionRequestsResponse,
} from '@/types';
@@ -86,11 +85,11 @@ function Requester({ request }: { request: TicketExtensionRequest }) {
}
function ExtensionReviewForm({
ticket,
ticketId,
assignmentId,
request,
}: {
ticket: TicketDetail;
ticketId: string;
assignmentId: number;
request: TicketExtensionRequest;
}) {
@@ -102,9 +101,8 @@ function ExtensionReviewForm({
const [isRejectConfirmationOpen, setIsRejectConfirmationOpen] =
useState(false);
const reviewMutation = useReviewTicketExtensionMutation(
ticket.id,
ticketId,
assignmentId,
ticket.defect_class,
);
const extensionDays = getExtensionDays(request);
@@ -274,14 +272,14 @@ function ExtensionReviewForm({
}
export function ReviewExtensionRequestAction({
ticket,
ticketId,
assignmentId,
}: {
ticket: TicketDetail;
ticketId: string;
assignmentId: number;
}) {
const extensionRequestQuery = useTicketExtensionRequestQuery(
ticket.id,
ticketId,
assignmentId,
);
@@ -308,7 +306,7 @@ export function ReviewExtensionRequestAction({
return (
<ExtensionReviewForm
ticket={ticket}
ticketId={ticketId}
assignmentId={assignmentId}
request={pendingRequest}
/>

View File

@@ -14,7 +14,6 @@ import {
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import type {
TicketDetail,
TicketExtensionRequest,
TicketExtensionRequestsResponse,
} from '@/types';
@@ -186,16 +185,16 @@ function ExtensionRequestDetails({
}
export function TicketExtensionRequestPanel({
ticket,
ticketId,
assignmentId,
dueAt,
}: {
ticket: TicketDetail;
ticketId: string;
assignmentId: number;
dueAt: string | null;
}) {
const extensionRequestQuery = useTicketExtensionRequestQuery(
ticket.id,
ticketId,
assignmentId,
);
@@ -234,7 +233,7 @@ export function TicketExtensionRequestPanel({
if (!latestRequest) {
return (
<RequestExtensionForm
ticket={ticket}
ticketId={ticketId}
assignmentId={assignmentId}
currentDueAt={dueAt}
/>

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { useParams, useRouter } from 'next/navigation';
import { ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -11,24 +11,20 @@ import {
TicketOverviewCardSkeleton,
TicketOverviewHeaderSkeleton,
} from './components/TicketOverviewSkeleton';
import { TicketClassActionsSkeleton } from './components/TicketClassDetailSkeleton';
import { TicketAssignmentsCard } from './components/TicketAssignmentsCard';
import { TicketOverviewCard } from './components/TicketOverviewCard';
import { TicketStatusActions } from './components/TicketStatusActions';
import { TicketDefectClassTabs } from './components/TicketDefectClassTabs';
import { TicketDetectionPreviewCard } from './components/TicketDetectionPreviewCard';
import { TicketHistoryCard } from './components/TicketHistoryCard';
import { useTicketDetailEvents } from '../hooks/useTicketDetailEvents';
import {
useTicketAssignmentsQuery,
useTicketClassDetailQuery,
useTicketOverviewQuery,
} from '../hooks/useTicketQueries';
export default function TicketDetailPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { ticketId } = useParams() as { ticketId: string };
const defectClass = searchParams.get('defect_class') ?? undefined;
const [selectedAssignmentId, setSelectedAssignmentId] = useState<number>();
const overviewQuery = useTicketOverviewQuery(ticketId);
@@ -37,8 +33,6 @@ export default function TicketDetailPage() {
ticketId,
overview?.assignments,
);
const classDetailQuery = useTicketClassDetailQuery(ticketId, defectClass);
const classDetail = classDetailQuery.data;
const assignments = (assignmentsQuery.data?.items ?? []).filter(
(assignment) => assignment.item_count > 0,
);
@@ -46,11 +40,8 @@ export default function TicketDetailPage() {
assignments.find((assignment) => assignment.id === selectedAssignmentId) ??
assignments[0];
const activeAssignmentId = activeAssignment?.id;
const isClassDetailLoading = Boolean(
defectClass && classDetailQuery.isLoading,
);
useTicketDetailEvents(ticketId, defectClass, Boolean(ticketId));
useTicketDetailEvents(ticketId, Boolean(ticketId));
if (overviewQuery.isError) {
return (
@@ -94,9 +85,9 @@ export default function TicketDetailPage() {
/>
) : null}
{activeAssignment ? (
<TicketDefectClassTabs
<TicketDetectionPreviewCard
ticketId={ticketId}
ticket={overview}
videoId={overview?.video_id ?? overview?.video?.id}
assignmentId={activeAssignment.id}
/>
) : null}
@@ -104,12 +95,9 @@ export default function TicketDetailPage() {
</div>
<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">
{activeAssignment && isClassDetailLoading ? (
<TicketClassActionsSkeleton />
) : null}
{activeAssignment && !isClassDetailLoading && classDetail ? (
{activeAssignment ? (
<TicketStatusActions
ticket={classDetail}
ticketId={ticketId}
assignment={activeAssignment}
/>
) : null}

View File

@@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { API_ROUTES } from '@/constants/apiRoutes';
@@ -18,15 +18,9 @@ function isHeartbeatEvent(event: { type?: string } | null | undefined) {
export function useTicketDetailEvents(
ticketId: string | undefined,
defectClass: string | undefined,
enabled = true,
): void {
const queryClient = useQueryClient();
const defectClassRef = useRef(defectClass);
useEffect(() => {
defectClassRef.current = defectClass;
}, [defectClass]);
const refetchTicket = useCallback(() => {
if (!ticketId) return;
@@ -38,11 +32,6 @@ export function useTicketDetailEvents(
queryClient.refetchQueries({
queryKey: ticketKeys.timeline(ticketId),
});
if (defectClassRef.current) {
queryClient.refetchQueries({
queryKey: ticketKeys.classDetail(ticketId, defectClassRef.current),
});
}
queryClient.invalidateQueries({
queryKey: ticketKeys.defectClasses(ticketId),
});

View File

@@ -18,7 +18,6 @@ import type {
RequestTicketExtensionPayload,
ReviewDetectionRepairProofPayload,
ReviewTicketExtensionPayload,
TicketDetail,
TicketAssignmentSummary,
TicketListParams,
DetectionProofStatus,
@@ -78,21 +77,6 @@ export function useTicketTimelineQuery(ticketId: string | undefined) {
});
}
export function useTicketClassDetailQuery(
ticketId: string | undefined,
defectClass: string | undefined,
) {
return useQuery({
queryKey: ticketKeys.classDetail(ticketId ?? '', defectClass ?? ''),
queryFn: () =>
ticketService.getTicketClassDetail(
ticketId as string,
defectClass as string,
),
enabled: Boolean(ticketId && defectClass),
});
}
export function useTicketDefectClassesQuery(ticketId: string | undefined) {
return useQuery({
queryKey: ticketKeys.defectClasses(ticketId ?? ''),
@@ -224,7 +208,6 @@ export function useTicketReviewDetectionsQuery(
export function useDiscardDetectionMutation(
ticketId: string,
videoId: string | undefined,
defectClass?: string,
) {
const queryClient = useQueryClient();
@@ -248,13 +231,6 @@ export function useDiscardDetectionMutation(
queryClient.invalidateQueries({
queryKey: ticketKeys.overview(ticketId),
}),
...(defectClass
? [
queryClient.invalidateQueries({
queryKey: ticketKeys.classDetail(ticketId, defectClass),
}),
]
: []),
queryClient.invalidateQueries({
queryKey: ticketKeys.defectClasses(ticketId),
}),
@@ -372,7 +348,6 @@ export function useAssignTicketMutation(ticketId: string, videoId?: string) {
export function useRequestTicketExtensionMutation(
ticketId: string,
assignmentId: number | undefined,
defectClass: string,
) {
const queryClient = useQueryClient();
@@ -392,9 +367,6 @@ export function useRequestTicketExtensionMutation(
queryClient.invalidateQueries({
queryKey: ticketKeys.extensionRequest(ticketId, assignmentId),
});
queryClient.invalidateQueries({
queryKey: ticketKeys.classDetail(ticketId, defectClass),
});
queryClient.invalidateQueries({
queryKey: ticketKeys.assignments(ticketId),
});
@@ -409,7 +381,6 @@ export function useRequestTicketExtensionMutation(
export function useReviewTicketExtensionMutation(
ticketId: string,
assignmentId: number,
defectClass: string,
) {
const queryClient = useQueryClient();
@@ -425,9 +396,6 @@ export function useReviewTicketExtensionMutation(
queryClient.invalidateQueries({
queryKey: ticketKeys.extensionRequest(ticketId, assignmentId),
});
queryClient.invalidateQueries({
queryKey: ticketKeys.classDetail(ticketId, defectClass),
});
queryClient.invalidateQueries({
queryKey: ticketKeys.assignments(ticketId),
});
@@ -439,18 +407,21 @@ export function useReviewTicketExtensionMutation(
onError: () => toast.error('Failed to review extension request'),
});
}
export function useCloseTicketMutation(ticketId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: CloseTicketPayload) =>
ticketService.closeTicket(ticketId, payload),
onSuccess: (ticket, payload) => {
onSuccess: () => {
toast.success('Ticket closed');
queryClient.setQueryData<TicketDetail>(
ticketKeys.classDetail(ticketId, payload.defect_class),
ticket,
);
queryClient.invalidateQueries({
queryKey: ticketKeys.overview(ticketId),
});
queryClient.invalidateQueries({
queryKey: ticketKeys.assignments(ticketId),
});
queryClient.invalidateQueries({
queryKey: ticketKeys.timeline(ticketId),
});

View File

@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import { PageHeader } from '@/components/page-header';
import { Ticket } from 'lucide-react';
import type { TicketListItem } from '@/types';
import { ROUTES } from '@/utils/routes';
import { TicketFilters } from './components/TicketFilters';
import { TicketTable } from './components/TicketTable';
@@ -37,11 +38,7 @@ export default function TicketPage() {
const handleView = useCallback(
(ticket: TicketListItem) => {
const defectClassQuery = ticket.default_defect_class
? `?defect_class=${encodeURIComponent(ticket.default_defect_class)}`
: '';
router.push(`/ticket/${ticket.id}${defectClassQuery}`);
router.push(ROUTES.TICKET_DETAIL(ticket.id));
},
[router],
);

View File

@@ -13,8 +13,6 @@ export const ticketKeys = {
[...ticketKeys.details(), ticketId, 'overview'] as const,
timeline: (ticketId: string) =>
[...ticketKeys.details(), ticketId, 'timeline'] as const,
classDetail: (ticketId: string, defectClass: string) =>
[...ticketKeys.details(), ticketId, 'class', defectClass] as const,
classDetectionLists: (videoId: string) =>
[...ticketKeys.details(), 'video', videoId, 'detections'] as const,
classDetections: (videoId: string, params: VideoDetectionsParams) =>

View File

@@ -66,8 +66,6 @@ export const API_ROUTES = {
BASE: '/biz/api/v1/tickets',
DETAIL: (id: string) => `/biz/api/v1/tickets/${id}`,
TIMELINE: (id: string) => `/biz/api/v1/tickets/${id}/timeline`,
CLASS_DETAIL: (id: string, defectClass: string) =>
`/biz/api/v1/tickets/${id}/class-detail?defect_class=${defectClass}`,
DEFECT_CLASSES: (id: string) => `/biz/api/v1/tickets/${id}/defect-classes`,
ASSIGNMENTS: (id: string) => `/biz/api/v1/tickets/${id}/assignments`,
REQUEST_EXTENSION: (ticketId: string, assignmentId: number) =>

View File

@@ -8,7 +8,6 @@ import type {
RequestTicketExtensionPayload,
ReviewTicketExtensionPayload,
SseTokenResponse,
TicketDetail,
TicketAssignmentSummary,
TicketAssignmentsResponse,
TicketExtensionRequestsResponse,
@@ -62,16 +61,6 @@ export const ticketService = {
return response.data;
},
getTicketClassDetail: async (
ticketId: string,
defectClass: string,
): Promise<TicketDetail> => {
const response = await axiosClient.get<TicketDetail>(
API_ROUTES.TICKETS.CLASS_DETAIL(ticketId, defectClass),
);
return response.data;
},
getTicketDefectClasses: async (
ticketId: string,
): Promise<TicketDefectClassesResponse> => {
@@ -166,11 +155,12 @@ export const ticketService = {
);
return response.data;
},
closeTicket: async (
ticketId: string,
payload: CloseTicketPayload,
): Promise<TicketDetail> => {
const response = await axiosClient.post<TicketDetail>(
): Promise<unknown> => {
const response = await axiosClient.post<unknown>(
API_ROUTES.TICKETS.CLOSE(ticketId),
payload,
);

View File

@@ -1,5 +1,3 @@
import type { TicketDefectClassTicketStatus } from './defect-class';
import type { TicketStatus } from './status';
import type { TicketAssignmentCriteria } from './actions';
export interface TicketActor {
@@ -9,15 +7,6 @@ export interface TicketActor {
avatar_url?: string | null;
}
export interface TicketStatusMetadata {
reason: string | null;
label: string | null;
message: string | null;
detection_count?: number | null;
requires_action?: boolean;
review_comment?: string | null;
}
export interface TicketVideo {
id: string;
name: string | null;
@@ -26,40 +15,6 @@ export interface TicketVideo {
thumbnail: string | null;
}
export interface TicketWorker extends TicketActor {
assignment_id: number;
assigned_at: string | null;
due_at: string | null;
assigned_by_user_id: number | null;
assigned_by_name: string | null;
role: {
id: number;
name: string;
} | null;
}
export interface TicketRepair {
notes: string | null;
proof_video_url: string | null;
proof_image_urls: string[];
submitted_at: string | null;
}
export interface TicketReviewer extends TicketActor {
review_comment: string | null;
reviewed_at: string | null;
}
export interface TicketAiResult {
detection_count: number;
available_defect_classes: string[];
}
export interface TicketTenant {
root_tenant_id: number;
organization_id: number;
}
export interface TicketTimestamps {
created_at: string;
updated_at: string;
@@ -137,24 +92,3 @@ export interface TicketOverviewDetail {
assignments?: TicketAssignmentSummary[];
timestamps?: TicketTimestamps | null;
}
export interface TicketDetail {
id: string;
ticket_name?: string;
video_id: string;
chainage_id: string | null;
chainage_name: string | null;
status: TicketStatus;
defect_class: string;
defect_display_name: string;
assignment_status: TicketDefectClassTicketStatus;
status_metadata: TicketStatusMetadata | null;
video: TicketVideo | null;
uploader: TicketActor | null;
worker: TicketWorker | null;
repair: TicketRepair | null;
reviewer: TicketReviewer | null;
ai_result: TicketAiResult | null;
tenant: TicketTenant | null;
timestamps: TicketTimestamps;
}

View File

@@ -1,4 +1,3 @@
import type { TicketDetail } from './detail';
import type { TicketStatus } from './status';
export interface SseTokenResponse {
@@ -22,7 +21,7 @@ export type TicketTableStatusEvent = {
message?: string;
};
export type TicketDetailStatusEvent = Partial<TicketDetail> & {
export type TicketDetailStatusEvent = {
type: 'ticket_status' | 'heartbeat';
kind?: 'snapshot' | 'transitioned';
id?: string;