'use client'; import { useMemo } from 'react'; import { useInfiniteQuery, useMutation, useQuery, useQueryClient, } from '@tanstack/react-query'; import { toast } from 'sonner'; import { detectionService, ticketService, videoService } from '@/services/api'; import type { AssignTicketPayload, CloseTicketPayload, DetectionCoordinatesParams, DiscardDetectionPayload, RequestTicketExtensionPayload, ReviewDetectionRepairProofPayload, ReviewTicketExtensionPayload, TicketAssignmentSummary, TicketListParams, DetectionProofStatus, VideoDetectionsParams, } from '@/types'; import { ticketKeys } from '../queries/ticketKeys'; const TICKET_TABLE_REFRESH_MS = 5 * 60 * 1000; interface UseTicketsQueryParams { skip: number; limit: number; chainageId?: string; } function buildTicketListParams({ skip, limit, chainageId, }: UseTicketsQueryParams): TicketListParams { return { skip, limit, chainage_id: chainageId || undefined, }; } export function useTicketsQuery(params: UseTicketsQueryParams) { const { skip, limit, chainageId } = params; const listParams = useMemo( () => buildTicketListParams({ skip, limit, chainageId }), [chainageId, limit, skip], ); return useQuery({ queryKey: ticketKeys.list(listParams), queryFn: () => ticketService.getTickets(listParams), staleTime: TICKET_TABLE_REFRESH_MS, refetchInterval: TICKET_TABLE_REFRESH_MS, }); } export function useTicketOverviewQuery(ticketId: string | undefined) { return useQuery({ queryKey: ticketKeys.overview(ticketId ?? ''), queryFn: () => ticketService.getTicketOverview(ticketId as string), enabled: Boolean(ticketId), }); } export function useTicketTimelineQuery(ticketId: string | undefined) { return useQuery({ queryKey: ticketKeys.timeline(ticketId ?? ''), queryFn: () => ticketService.getTicketTimeline(ticketId as string), enabled: Boolean(ticketId), }); } export function useTicketDefectClassesQuery(ticketId: string | undefined) { return useQuery({ queryKey: ticketKeys.defectClasses(ticketId ?? ''), queryFn: () => ticketService.getTicketDefectClasses(ticketId as string), enabled: Boolean(ticketId), }); } export function useTicketAssignmentsQuery( ticketId: string | undefined, initialAssignments?: TicketAssignmentSummary[], ) { return useQuery({ queryKey: ticketKeys.assignments(ticketId ?? ''), queryFn: () => ticketService.getTicketAssignments(ticketId as string), enabled: Boolean(ticketId), initialData: initialAssignments !== undefined ? { items: initialAssignments, total: initialAssignments.length, } : undefined, staleTime: 30 * 1000, }); } export function useTicketDetectionsQuery( videoId: string | undefined, params: VideoDetectionsParams, ) { const queryParams = useMemo( () => ({ skip: params.skip ?? 0, limit: params.limit ?? 1, assignment_id: params.assignment_id, class_name: params.class_name, min_confidence: params.min_confidence, sort: params.sort ?? 'timestamp_asc', search: params.search, }), [ params.assignment_id, params.class_name, params.limit, params.min_confidence, params.search, params.skip, params.sort, ], ); return useQuery({ queryKey: ticketKeys.classDetections(videoId ?? '', queryParams), queryFn: () => videoService.getVideoDetections(videoId as string, queryParams), enabled: Boolean(videoId), }); } export function useDetectionCoordinatesQuery( videoId: string | undefined, params: DetectionCoordinatesParams, ) { const queryParams = useMemo( () => ({ limit: params.limit ?? 50, class_name: params.class_name, assignment_status: params.assignment_status, search: params.search, }), [params.assignment_status, params.class_name, params.limit, params.search], ); return useInfiniteQuery({ queryKey: ticketKeys.detectionCoordinates(videoId ?? '', queryParams), queryFn: ({ pageParam }) => detectionService.getCoordinates(videoId as string, { ...queryParams, skip: pageParam, }), initialPageParam: 0, getNextPageParam: (lastPage, allPages) => { if (lastPage.items.length === 0) return undefined; const loadedCount = allPages.reduce( (total, page) => total + page.items.length, 0, ); return loadedCount < lastPage.total ? loadedCount : undefined; }, enabled: Boolean(videoId), staleTime: 60 * 1000, }); } export function useTicketClassDetectionsQuery( videoId: string | undefined, params: VideoDetectionsParams, ) { return useTicketDetectionsQuery(videoId, params); } export function useTicketReviewDetectionsQuery( videoId: string | undefined, proofStatus?: DetectionProofStatus, ) { const pageSize = 20; return useInfiniteQuery({ queryKey: ticketKeys.reviewDetections(videoId ?? '', proofStatus), queryFn: ({ pageParam }) => videoService.getVideoDetections(videoId as string, { proof_status: proofStatus, sort: 'timestamp_asc', skip: pageParam, limit: pageSize, }), initialPageParam: 0, getNextPageParam: (lastPage) => { const nextSkip = lastPage.skip + lastPage.items.length; return nextSkip < lastPage.total ? nextSkip : undefined; }, enabled: Boolean(videoId), }); } export function useDiscardDetectionMutation( ticketId: string, videoId: string | undefined, ) { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ detectionId, payload, }: { detectionId: number; payload: DiscardDetectionPayload; }) => detectionService.discardDetection(detectionId, payload), onSuccess: async () => { toast.success('Detection discarded'); await Promise.all([ videoId ? queryClient.invalidateQueries({ queryKey: ticketKeys.classDetectionLists(videoId), }) : Promise.resolve(), queryClient.invalidateQueries({ queryKey: ticketKeys.overview(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.defectClasses(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }), queryClient.invalidateQueries({ queryKey: ticketKeys.assignments(ticketId), }), ]); }, onError: () => toast.error('Failed to discard detection'), }); } export function useReviewDetectionRepairProofMutation({ ticketId, videoId, }: { ticketId: string; videoId: string | undefined; }) { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ detectionId, payload, }: { detectionId: number; payload: ReviewDetectionRepairProofPayload; }) => detectionService.reviewRepairProof(ticketId, detectionId, payload), onSuccess: async (_repairProof, variables) => { toast.success( variables.payload.action === 'approve' ? 'Repair proof approved' : 'Repair proof rejected', ); await Promise.all([ videoId ? queryClient.invalidateQueries({ queryKey: ticketKeys.classDetectionLists(videoId), }) : Promise.resolve(), queryClient.invalidateQueries({ queryKey: ticketKeys.defectClasses(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.overview(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.timeline(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }), ]); }, onError: () => toast.error('Failed to review repair proof'), }); } export function useTicketExtensionRequestQuery( ticketId: string | undefined, assignmentId: number | undefined, ) { return useQuery({ queryKey: ticketKeys.extensionRequest(ticketId ?? '', assignmentId), queryFn: () => ticketService.getTicketExtensionRequest( ticketId as string, assignmentId as number, ), enabled: Boolean(ticketId && assignmentId), }); } export function useAssignableTicketUsersQuery(enabled: boolean) { return useQuery({ queryKey: ticketKeys.assignableUsers(), queryFn: () => ticketService.getAssignableUsers(), enabled, }); } export function useAssignTicketMutation(ticketId: string, videoId?: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: (payload: AssignTicketPayload) => ticketService.assignTicket(ticketId, payload), onSuccess: async () => { toast.success('Ticket assigned'); await Promise.all([ queryClient.invalidateQueries({ queryKey: ticketKeys.overview(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.timeline(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }), ...(videoId ? [ queryClient.invalidateQueries({ queryKey: ticketKeys.classDetectionLists(videoId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.detectionCoordinateLists(videoId), }), ] : []), ]); }, onError: () => toast.error('Failed to assign ticket'), }); } export function useRequestTicketExtensionMutation( ticketId: string, assignmentId: number | undefined, ) { const queryClient = useQueryClient(); return useMutation({ mutationFn: (payload: RequestTicketExtensionPayload) => { if (assignmentId === undefined) { throw new Error('No active assignment is available for this ticket.'); } return ticketService.requestTicketExtension( ticketId, assignmentId, payload, ); }, onSuccess: () => { toast.success('Extension request submitted'); queryClient.invalidateQueries({ queryKey: ticketKeys.extensionRequest(ticketId, assignmentId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.assignments(ticketId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.timeline(ticketId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }); }, onError: () => toast.error('Failed to request an extension'), }); } export function useReviewTicketExtensionMutation( ticketId: string, assignmentId: number, ) { const queryClient = useQueryClient(); return useMutation({ mutationFn: (payload: ReviewTicketExtensionPayload) => ticketService.reviewTicketExtension(ticketId, assignmentId, payload), onSuccess: (_response, payload) => { toast.success( payload.action === 'approve' ? 'Extension request approved' : 'Extension request rejected', ); queryClient.invalidateQueries({ queryKey: ticketKeys.extensionRequest(ticketId, assignmentId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.assignments(ticketId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.timeline(ticketId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }); }, 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: () => { toast.success('Ticket closed'); queryClient.invalidateQueries({ queryKey: ticketKeys.overview(ticketId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.assignments(ticketId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.timeline(ticketId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }); }, onError: () => toast.error('Failed to close ticket'), }); }