From b1174c88257d249206baf3e6bf8c2941b2f24468 Mon Sep 17 00:00:00 2001 From: "santasri.pachhal" Date: Wed, 1 Jul 2026 16:13:42 +0530 Subject: [PATCH] feat(ticket): split overview/class detail queries and add loading skeletons --- src/app/(modules)/layout.tsx | 2 +- .../components/TicketClassDetailSkeleton.tsx | 108 ++++++++++++++++ .../components/TicketDefectClassTabs.tsx | 23 ++-- .../components/TicketDetailHeader.tsx | 8 +- .../components/TicketDetailSkeleton.tsx | 115 ------------------ .../components/TicketOverviewCard.tsx | 14 +-- .../components/TicketOverviewSkeleton.tsx | 41 +++++++ .../components/TicketStatusActions.tsx | 13 +- src/app/(modules)/ticket/[ticketId]/page.tsx | 59 ++++++--- .../ticket/hooks/useTicketDetailEvents.ts | 9 +- .../ticket/hooks/useTicketQueries.ts | 29 +++-- .../(modules)/ticket/queries/ticketKeys.ts | 8 +- src/app/globals.css | 14 +++ src/services/api/ticket.service.ts | 22 ++-- src/types/ticket/detail.ts | 19 +++ 15 files changed, 305 insertions(+), 179 deletions(-) create mode 100644 src/app/(modules)/ticket/[ticketId]/components/TicketClassDetailSkeleton.tsx delete mode 100644 src/app/(modules)/ticket/[ticketId]/components/TicketDetailSkeleton.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/TicketOverviewSkeleton.tsx diff --git a/src/app/(modules)/layout.tsx b/src/app/(modules)/layout.tsx index bf76415..4b5b645 100644 --- a/src/app/(modules)/layout.tsx +++ b/src/app/(modules)/layout.tsx @@ -16,7 +16,7 @@ const ModulesLayout = ({
-
+
diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketClassDetailSkeleton.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketClassDetailSkeleton.tsx new file mode 100644 index 0000000..65bc990 --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketClassDetailSkeleton.tsx @@ -0,0 +1,108 @@ +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; + +export function TicketClassContentSkeleton() { + return ( +
+ + + + + + +
+ {Array.from({ length: 2 }).map((_, index) => ( +
+ + +
+ ))} +
+
+ +
+ + +
+
+
+
+ + + + + + + + + + + + + + + + +
+ {Array.from({ length: 5 }).map((_, index) => ( +
+ + +
+ ))} +
+
+
+ + + + + + + {Array.from({ length: 3 }).map((_, index) => ( +
+ {index < 2 ? ( + + ) : null} + +
+
+ + +
+ + +
+
+ ))} +
+
+
+ ); +} + +export function TicketClassActionsSkeleton() { + return ( + + +
+ + +
+
+ +
+
+ + +
+
+ + +
+ +
+
+
+ ); +} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx index 95e984c..596a85c 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketDefectClassTabs.tsx @@ -37,15 +37,17 @@ export function TicketDefectClassTabs({ (item) => item.class_name === defectClassParam, ); - setSelectedClass((current) => { - if (hasUrlClass) return defectClassParam ?? undefined; + if (hasUrlClass) { + setSelectedClass(defectClassParam ?? undefined); + return; + } - return current && - defectClasses.some((item) => item.class_name === current) - ? current - : defectClasses[0].class_name; - }); - }, [defectClassParam, defectClasses]); + const firstClass = defectClasses[0].class_name; + const nextParams = new URLSearchParams(searchParams.toString()); + nextParams.set('defect_class', firstClass); + setSelectedClass(firstClass); + router.replace(`${pathname}?${nextParams.toString()}`, { scroll: false }); + }, [defectClassParam, defectClasses, pathname, router, searchParams]); const handleClassChange = (className: string) => { const nextParams = new URLSearchParams(searchParams.toString()); @@ -88,7 +90,10 @@ export function TicketDefectClassTabs({ Issues - + {defectClasses.length} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx index f83a066..a41ddc3 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx @@ -7,9 +7,13 @@ import { Button } from '@/components/ui/button'; import { SparkleButton } from '@/components/ui/sparkle-button'; import { PERMISSIONS } from '@/constants/permissions'; import { PermissionGuard } from '@/guards'; -import type { TicketDetail } from '@/types'; +import type { TicketOverviewDetail } from '@/types'; -export function TicketDetailHeader({ ticket }: { ticket: TicketDetail }) { +export function TicketDetailHeader({ + ticket, +}: { + ticket: TicketOverviewDetail; +}) { const router = useRouter(); const ticketLabel = ticket.ticket_name; diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketDetailSkeleton.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketDetailSkeleton.tsx deleted file mode 100644 index 08eae16..0000000 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketDetailSkeleton.tsx +++ /dev/null @@ -1,115 +0,0 @@ -'use client'; - -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { Skeleton } from '@/components/ui/skeleton'; - -function OverviewSkeletonField() { - return ( -
- - -
- ); -} - -function TimelineSkeletonItem({ isLast = false }: { isLast?: boolean }) { - return ( -
- {!isLast ? ( - - ) : null} - -
-
- - -
- - -
-
- ); -} - -export function TicketDetailSkeleton() { - return ( -
-
-
-
- - -
- -
- -
- - -
-
- -
-
- - - - - - {Array.from({ length: 8 }).map((_, index) => ( - - ))} - - - -
- - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - -
- - - - - - - - - - - -
-
- ); -} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx index 25f87fe..9176db2 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx @@ -1,7 +1,7 @@ 'use client'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import type { TicketActor, TicketDetail } from '@/types'; +import type { TicketActor, TicketOverviewDetail } from '@/types'; function formatCreatedDate(value: string | null | undefined) { if (!value) return '-'; @@ -74,9 +74,11 @@ function OverviewField({ ); } -export function TicketOverviewCard({ ticket }: { ticket: TicketDetail }) { - const detectionCount = ticket.ai_result?.detection_count; - +export function TicketOverviewCard({ + ticket, +}: { + ticket: TicketOverviewDetail; +}) { return ( @@ -85,9 +87,7 @@ export function TicketOverviewCard({ ticket }: { ticket: TicketDetail }) { - - {detectionCount != null ? `${detectionCount} Occurrences` : '-'} - + {ticket.ai_result.detection_count} Occurrences diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewSkeleton.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewSkeleton.tsx new file mode 100644 index 0000000..08c1c4b --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewSkeleton.tsx @@ -0,0 +1,41 @@ +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; + +function OverviewSkeletonField() { + return ( +
+ + +
+ ); +} + +export function TicketOverviewHeaderSkeleton() { + return ( +
+
+ + +
+ +
+ +
+
+ ); +} + +export function TicketOverviewCardSkeleton() { + return ( + + + + + + {Array.from({ length: 4 }).map((_, index) => ( + + ))} + + + ); +} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx index 0d70cb3..a7a2cba 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx @@ -9,7 +9,6 @@ import { ClosedTicketAction } from './actions/ClosedTicketAction'; import { NoTicketAction } from './actions/NoTicketAction'; import { ReviewRepairAction } from './actions/ReviewRepairAction'; import { SubmitRepairAction } from './actions/SubmitRepairAction'; -import { TicketDefectClassTabs } from './TicketDefectClassTabs'; interface TicketStatusActionsProps { ticket: TicketDetail; @@ -49,7 +48,10 @@ function getTicketAction(ticket: TicketDetail) { ); } - if (ticket.assignment_status === 'approved' || ticket.assignment_status === 'rejected') { + if ( + ticket.assignment_status === 'approved' || + ticket.assignment_status === 'rejected' + ) { return ; } @@ -57,10 +59,5 @@ function getTicketAction(ticket: TicketDetail) { } export function TicketStatusActions({ ticket }: TicketStatusActionsProps) { - return ( -
- - {getTicketAction(ticket)} -
- ); + return getTicketAction(ticket); } diff --git a/src/app/(modules)/ticket/[ticketId]/page.tsx b/src/app/(modules)/ticket/[ticketId]/page.tsx index d469af2..60b8bc5 100644 --- a/src/app/(modules)/ticket/[ticketId]/page.tsx +++ b/src/app/(modules)/ticket/[ticketId]/page.tsx @@ -6,14 +6,25 @@ import { ArrowLeft } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { TicketDetailHeader } from './components/TicketDetailHeader'; -import { TicketDetailSkeleton } from './components/TicketDetailSkeleton'; +import { + TicketOverviewCardSkeleton, + TicketOverviewHeaderSkeleton, +} from './components/TicketOverviewSkeleton'; +import { + TicketClassActionsSkeleton, + TicketClassContentSkeleton, +} from './components/TicketClassDetailSkeleton'; import { TicketHistoryCard } from './components/TicketHistoryCard'; import { TicketOverviewCard } from './components/TicketOverviewCard'; import { TicketRepairReportCard } from './components/TicketRepairReportCard'; import { TicketStatusActions } from './components/TicketStatusActions'; +import { TicketDefectClassTabs } from './components/TicketDefectClassTabs'; import { useTicketDetailEvents } from '../hooks/useTicketDetailEvents'; -import { useTicketDetailQuery } from '../hooks/useTicketQueries'; +import { + useTicketClassDetailQuery, + useTicketOverviewQuery, +} from '../hooks/useTicketQueries'; export default function TicketDetailPage() { const router = useRouter(); @@ -21,16 +32,17 @@ export default function TicketDetailPage() { const { ticketId } = useParams() as { ticketId: string }; const defectClass = searchParams.get('defect_class') ?? undefined; - const ticketQuery = useTicketDetailQuery(ticketId, defectClass); - const ticket = ticketQuery.data; + const overviewQuery = useTicketOverviewQuery(ticketId); + const classDetailQuery = useTicketClassDetailQuery(ticketId, defectClass); + const overview = overviewQuery.data; + const classDetail = classDetailQuery.data; + const isClassDetailLoading = Boolean( + defectClass && classDetailQuery.isLoading, + ); useTicketDetailEvents(ticketId, defectClass, Boolean(ticketId)); - if (ticketQuery.isLoading) { - return ; - } - - if (ticketQuery.isError || !ticket) { + if (overviewQuery.isError) { return (

Failed to load ticket.

@@ -48,17 +60,34 @@ export default function TicketDetailPage() { return (
- + {overview ? ( + + ) : ( + + )}
- - - + {overview ? ( + + ) : ( + + )} + {isClassDetailLoading ? : null} + {!isClassDetailLoading && classDetail ? ( + <> + + + + ) : null}
-
- +
+ + {isClassDetailLoading ? : null} + {!isClassDetailLoading && classDetail ? ( + + ) : null}
diff --git a/src/app/(modules)/ticket/hooks/useTicketDetailEvents.ts b/src/app/(modules)/ticket/hooks/useTicketDetailEvents.ts index 52078b9..978f4a6 100644 --- a/src/app/(modules)/ticket/hooks/useTicketDetailEvents.ts +++ b/src/app/(modules)/ticket/hooks/useTicketDetailEvents.ts @@ -31,9 +31,12 @@ export function useTicketDetailEvents( const refetchTicket = useCallback(() => { if (!ticketId) return; - queryClient.refetchQueries({ - queryKey: ticketKeys.detail(ticketId, defectClassRef.current), - }); + queryClient.refetchQueries({ queryKey: ticketKeys.overview(ticketId) }); + if (defectClassRef.current) { + queryClient.refetchQueries({ + queryKey: ticketKeys.classDetail(ticketId, defectClassRef.current), + }); + } queryClient.invalidateQueries({ queryKey: ticketKeys.defectClasses(ticketId), }); diff --git a/src/app/(modules)/ticket/hooks/useTicketQueries.ts b/src/app/(modules)/ticket/hooks/useTicketQueries.ts index 9f1b8f0..5a2ad67 100644 --- a/src/app/(modules)/ticket/hooks/useTicketQueries.ts +++ b/src/app/(modules)/ticket/hooks/useTicketQueries.ts @@ -52,15 +52,26 @@ export function useTicketsQuery(params: UseTicketsQueryParams) { }); } -export function useTicketDetailQuery( +export function useTicketOverviewQuery(ticketId: string | undefined) { + return useQuery({ + queryKey: ticketKeys.overview(ticketId ?? ''), + queryFn: () => ticketService.getTicketOverview(ticketId as string), + enabled: Boolean(ticketId), + }); +} + +export function useTicketClassDetailQuery( ticketId: string | undefined, defectClass: string | undefined, ) { return useQuery({ - queryKey: ticketKeys.detail(ticketId ?? '', defectClass), + queryKey: ticketKeys.classDetail(ticketId ?? '', defectClass ?? ''), queryFn: () => - ticketService.getTicketDetail(ticketId as string, defectClass), - enabled: Boolean(ticketId), + ticketService.getTicketClassDetail( + ticketId as string, + defectClass as string, + ), + enabled: Boolean(ticketId && defectClass), }); } @@ -102,7 +113,7 @@ export function useAssignTicketMutation(ticketId: string) { onSuccess: (ticket, payload) => { toast.success('Ticket assigned'); queryClient.setQueryData( - ticketKeys.detail(ticketId, payload.defect_class), + ticketKeys.classDetail(ticketId, payload.defect_class), (current) => mergeTicketDetailResponse(current, ticket), ); queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }); @@ -120,7 +131,7 @@ export function useSubmitRepairMutation(ticketId: string) { onSuccess: (ticket) => { toast.success('Repair submitted'); queryClient.setQueryData( - ticketKeys.detail(ticketId), + ticketKeys.classDetail(ticketId, ticket.defect_class), (current) => mergeTicketDetailResponse(current, ticket), ); queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }); @@ -138,7 +149,7 @@ export function useSubmitRepairUploadMutation(ticketId: string) { onSuccess: (ticket, payload) => { toast.success('Repair evidence uploaded'); queryClient.setQueryData( - ticketKeys.detail(ticketId, payload.defect_class), + ticketKeys.classDetail(ticketId, payload.defect_class), (current) => mergeTicketDetailResponse(current, ticket), ); queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }); @@ -158,7 +169,7 @@ export function useReviewRepairMutation(ticketId: string) { payload.action === 'approve' ? 'Repair approved' : 'Repair rejected', ); queryClient.setQueryData( - ticketKeys.detail(ticketId, payload.defect_class), + ticketKeys.classDetail(ticketId, payload.defect_class), (current) => mergeTicketDetailResponse(current, ticket), ); queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }); @@ -176,7 +187,7 @@ export function useCloseTicketMutation(ticketId: string) { onSuccess: (ticket, payload) => { toast.success('Ticket closed'); queryClient.setQueryData( - ticketKeys.detail(ticketId, payload.defect_class), + ticketKeys.classDetail(ticketId, payload.defect_class), (current) => mergeTicketDetailResponse(current, ticket), ); queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }); diff --git a/src/app/(modules)/ticket/queries/ticketKeys.ts b/src/app/(modules)/ticket/queries/ticketKeys.ts index 7d33da3..25c824f 100644 --- a/src/app/(modules)/ticket/queries/ticketKeys.ts +++ b/src/app/(modules)/ticket/queries/ticketKeys.ts @@ -5,9 +5,11 @@ export const ticketKeys = { lists: () => [...ticketKeys.all, 'list'] as const, list: (params: TicketListParams) => [...ticketKeys.lists(), params] as const, details: () => [...ticketKeys.all, 'detail'] as const, - detail: (ticketId: string, defectClass?: string) => - [...ticketKeys.details(), ticketId, defectClass] as const, + overview: (ticketId: string) => + [...ticketKeys.details(), ticketId, 'overview'] as const, + classDetail: (ticketId: string, defectClass: string) => + [...ticketKeys.details(), ticketId, 'class', defectClass] as const, defectClasses: (ticketId: string) => - [...ticketKeys.detail(ticketId), 'defect-classes'] as const, + [...ticketKeys.details(), ticketId, 'defect-classes'] as const, assignableUsers: () => [...ticketKeys.all, 'assignable-users'] as const, }; diff --git a/src/app/globals.css b/src/app/globals.css index fd7fbe2..1fe6995 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -122,6 +122,20 @@ } @layer base { + html { + scrollbar-gutter: stable; + } + + html:has(body[data-scroll-locked]) { + scrollbar-gutter: auto; + } + + @supports not (scrollbar-gutter: stable) { + html { + overflow-y: scroll; + } + } + * { @apply border-border outline-none; } diff --git a/src/services/api/ticket.service.ts b/src/services/api/ticket.service.ts index 35b8418..7e8fafa 100644 --- a/src/services/api/ticket.service.ts +++ b/src/services/api/ticket.service.ts @@ -10,6 +10,7 @@ import type { SubmitRepairPayload, SubmitRepairUploadPayload, TicketDetail, + TicketOverviewDetail, TicketDefectClassesResponse, TicketListParams, TicketListResponse, @@ -32,15 +33,22 @@ export const ticketService = { return response.data; }, - getTicketDetail: async ( + getTicketOverview: async ( ticketId: string, - defectClass?: string, - ): Promise => { - const path = defectClass - ? API_ROUTES.TICKETS.CLASS_DETAIL(ticketId, defectClass) - : API_ROUTES.TICKETS.DETAIL(ticketId); + ): Promise => { + const response = await axiosClient.get( + API_ROUTES.TICKETS.DETAIL(ticketId), + ); + return response.data; + }, - const response = await axiosClient.get(path); + getTicketClassDetail: async ( + ticketId: string, + defectClass: string, + ): Promise => { + const response = await axiosClient.get( + API_ROUTES.TICKETS.CLASS_DETAIL(ticketId, defectClass), + ); return response.data; }, diff --git a/src/types/ticket/detail.ts b/src/types/ticket/detail.ts index c0d4394..c205f13 100644 --- a/src/types/ticket/detail.ts +++ b/src/types/ticket/detail.ts @@ -65,6 +65,25 @@ export interface TicketTimestamps { updated_at: string; } +export interface TicketOverviewDetail { + id: string; + ticket_name: string; + video_id: string; + chainage_id: string; + chainage_name: string; + uploader: { + user_id: number; + name: string; + email: string; + }; + ai_result: { + detection_count: number; + completed_at: string; + available_defect_classes: string[]; + }; + timestamps: TicketTimestamps; +} + export interface TicketDetail { id: string; ticket_name?: string;