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'; } from '../../hooks/useTicketQueries';
import { DetectionDiscardDialog } from './DetectionDiscardDialog'; import { DetectionDiscardDialog } from './DetectionDiscardDialog';
interface TicketClassDetectionPreviewProps { interface TicketDetectionPreviewProps {
ticketId: string; ticketId: string;
videoId?: string | null; videoId?: string | null;
assignmentId: number; assignmentId: number;
@@ -52,7 +52,7 @@ function DetectionMetadataBar({
); );
} }
function TicketClassDetectionPreviewSkeleton() { function TicketDetectionPreviewSkeleton() {
return ( return (
<> <>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2"> <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, ticketId,
videoId, videoId,
assignmentId, assignmentId,
}: TicketClassDetectionPreviewProps) { }: TicketDetectionPreviewProps) {
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false); const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
@@ -130,7 +130,6 @@ export function TicketClassDetectionPreview({
const discardMutation = useDiscardDetectionMutation( const discardMutation = useDiscardDetectionMutation(
ticketId, ticketId,
videoId ?? undefined, videoId ?? undefined,
activeDetection?.detection.class_name,
); );
useEffect(() => { useEffect(() => {
@@ -217,7 +216,7 @@ export function TicketClassDetectionPreview({
</div> </div>
{detectionsQuery.isLoading && !detectionsQuery.data ? ( {detectionsQuery.isLoading && !detectionsQuery.data ? (
<TicketClassDetectionPreviewSkeleton /> <TicketDetectionPreviewSkeleton />
) : detectionsQuery.isError ? ( ) : detectionsQuery.isError ? (
<div className="rounded-lg border border-dashed border-destructive/40 bg-destructive/5 px-4 py-8 text-center"> <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"> <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 { PERMISSIONS } from '@/constants/permissions';
import { PermissionGuard } from '@/guards'; import { PermissionGuard } from '@/guards';
import type { TicketAssignmentSummary, TicketDetail } from '@/types'; import type { TicketAssignmentSummary } from '@/types';
import { NoTicketAction } from './actions/NoTicketAction'; import { NoTicketAction } from './actions/NoTicketAction';
import { RequestExtensionAction } from './actions/RequestExtensionAction'; import { RequestExtensionAction } from './actions/RequestExtensionAction';
import { ReviewExtensionRequestAction } from './actions/ReviewExtensionRequestAction'; import { ReviewExtensionRequestAction } from './actions/ReviewExtensionRequestAction';
interface TicketStatusActionsProps { interface TicketStatusActionsProps {
ticket: TicketDetail; ticketId: string;
assignment?: TicketAssignmentSummary; assignment?: TicketAssignmentSummary;
} }
function getTicketAction( function getTicketAction(
ticket: TicketDetail, ticketId: string,
assignment?: TicketAssignmentSummary, assignment?: TicketAssignmentSummary,
) { ) {
if (assignment && !assignment.is_complete) { if (assignment && !assignment.is_complete) {
@@ -27,7 +27,7 @@ function getTicketAction(
fallback={<NoTicketAction />} fallback={<NoTicketAction />}
> >
<RequestExtensionAction <RequestExtensionAction
ticket={ticket} ticketId={ticketId}
assignmentId={assignment.id} assignmentId={assignment.id}
dueAt={assignment.due_at} dueAt={assignment.due_at}
/> />
@@ -35,7 +35,7 @@ function getTicketAction(
} }
> >
<ReviewExtensionRequestAction <ReviewExtensionRequestAction
ticket={ticket} ticketId={ticketId}
assignmentId={assignment.id} assignmentId={assignment.id}
/> />
</PermissionGuard> </PermissionGuard>
@@ -46,10 +46,10 @@ function getTicketAction(
} }
export function TicketStatusActions({ export function TicketStatusActions({
ticket, ticketId,
assignment, assignment,
}: TicketStatusActionsProps) { }: TicketStatusActionsProps) {
const action = getTicketAction(ticket, assignment); const action = getTicketAction(ticketId, assignment);
return action ? <div className="space-y-3">{action}</div> : null; 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 { Clock3 } from 'lucide-react';
import type { TicketDetail } from '@/types';
import { TicketActionCard } from './TicketActionCard'; import { TicketActionCard } from './TicketActionCard';
import { TicketExtensionRequestPanel } from './TicketExtensionRequestPanel'; import { TicketExtensionRequestPanel } from './TicketExtensionRequestPanel';
export function RequestExtensionAction({ export function RequestExtensionAction({
ticket, ticketId,
assignmentId, assignmentId,
dueAt, dueAt,
}: { }: {
ticket: TicketDetail; ticketId: string;
assignmentId: number; assignmentId: number;
dueAt: string | null; dueAt: string | null;
}) { }) {
return ( return (
<TicketActionCard icon={Clock3} title="Request Extension"> <TicketActionCard icon={Clock3} title="Request Extension">
<TicketExtensionRequestPanel <TicketExtensionRequestPanel
ticket={ticket} ticketId={ticketId}
assignmentId={assignmentId} assignmentId={assignmentId}
dueAt={dueAt} dueAt={dueAt}
/> />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,8 +66,6 @@ export const API_ROUTES = {
BASE: '/biz/api/v1/tickets', BASE: '/biz/api/v1/tickets',
DETAIL: (id: string) => `/biz/api/v1/tickets/${id}`, DETAIL: (id: string) => `/biz/api/v1/tickets/${id}`,
TIMELINE: (id: string) => `/biz/api/v1/tickets/${id}/timeline`, 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`, DEFECT_CLASSES: (id: string) => `/biz/api/v1/tickets/${id}/defect-classes`,
ASSIGNMENTS: (id: string) => `/biz/api/v1/tickets/${id}/assignments`, ASSIGNMENTS: (id: string) => `/biz/api/v1/tickets/${id}/assignments`,
REQUEST_EXTENSION: (ticketId: string, assignmentId: number) => REQUEST_EXTENSION: (ticketId: string, assignmentId: number) =>

View File

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

View File

@@ -1,5 +1,3 @@
import type { TicketDefectClassTicketStatus } from './defect-class';
import type { TicketStatus } from './status';
import type { TicketAssignmentCriteria } from './actions'; import type { TicketAssignmentCriteria } from './actions';
export interface TicketActor { export interface TicketActor {
@@ -9,15 +7,6 @@ export interface TicketActor {
avatar_url?: string | null; 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 { export interface TicketVideo {
id: string; id: string;
name: string | null; name: string | null;
@@ -26,40 +15,6 @@ export interface TicketVideo {
thumbnail: string | null; 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 { export interface TicketTimestamps {
created_at: string; created_at: string;
updated_at: string; updated_at: string;
@@ -137,24 +92,3 @@ export interface TicketOverviewDetail {
assignments?: TicketAssignmentSummary[]; assignments?: TicketAssignmentSummary[];
timestamps?: TicketTimestamps | null; 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'; import type { TicketStatus } from './status';
export interface SseTokenResponse { export interface SseTokenResponse {
@@ -22,7 +21,7 @@ export type TicketTableStatusEvent = {
message?: string; message?: string;
}; };
export type TicketDetailStatusEvent = Partial<TicketDetail> & { export type TicketDetailStatusEvent = {
type: 'ticket_status' | 'heartbeat'; type: 'ticket_status' | 'heartbeat';
kind?: 'snapshot' | 'transitioned'; kind?: 'snapshot' | 'transitioned';
id?: string; id?: string;