'use client'; import { useMemo } from 'react'; import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient, } from '@tanstack/react-query'; import { toast } from 'sonner'; import { detectionService, ticketService, videoService } from '@/services/api'; import type { AssignTicketPayload, CloseTicketPayload, DiscardDetectionPayload, RequestTicketExtensionPayload, ReviewDetectionRepairProofPayload, TicketAssignmentSummary, TicketAssignmentDetectionsParams, TicketListParams, DetectionProofStatus, VideoDetectionsParams, } from '@/types'; import { ticketKeys } from '../queries/ticketKeys'; import { extensionRequestKeys } from '../../extension-requests/queries/extensionRequestKeys'; const TICKET_TABLE_REFRESH_MS = 5 * 60 * 1000; interface TicketDetectionsQueryOptions { enabled?: boolean; keepPreviousPage?: boolean; } 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 useTicketSummaryQuery(chainageId?: string) { const summaryParams = useMemo( () => ({ chainage_id: chainageId || undefined }), [chainageId], ); return useQuery({ queryKey: ticketKeys.summary(summaryParams), queryFn: () => ticketService.getTicketSummary(summaryParams), 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 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, options: TicketDetectionsQueryOptions = {}, ) { const { enabled = true, keepPreviousPage = false } = options; const queryParams = useMemo( () => ({ skip: params.skip ?? 0, limit: params.limit ?? 1, assignment_id: params.assignment_id, assignment_status: params.assignment_status, class_name: params.class_name, min_confidence: params.min_confidence, sort: params.sort ?? 'timestamp_asc', search: params.search, }), [ params.assignment_id, params.assignment_status, 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(enabled && videoId), placeholderData: keepPreviousPage ? keepPreviousData : undefined, staleTime: Infinity, }); } export function useTicketClassDetectionsQuery( videoId: string | undefined, params: VideoDetectionsParams, ) { return useTicketDetectionsQuery(videoId, params); } export function useTicketAssignmentDetectionsQuery( ticketId: string | undefined, params: TicketAssignmentDetectionsParams, enabled = true, ) { const queryParams = useMemo( () => ({ assignment_status: params.assignment_status ?? 'unassigned', class_name: params.class_name, skip: params.skip ?? 0, limit: params.limit ?? 500, }), [params.assignment_status, params.class_name, params.limit, params.skip], ); return useQuery({ queryKey: ticketKeys.assignmentDetections(ticketId ?? '', queryParams), queryFn: () => ticketService.getTicketAssignmentDetections( ticketId as string, queryParams, ), enabled: Boolean(enabled && ticketId), staleTime: Infinity, }); } 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.lists() }), queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }), queryClient.invalidateQueries({ queryKey: ticketKeys.assignmentDetectionLists(ticketId), }), 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.overview(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.timeline(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }), queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }), ]); }, 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) { 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() }), queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }), queryClient.invalidateQueries({ queryKey: ticketKeys.assignmentDetectionLists(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.assignments(ticketId), }), ]); }, 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: extensionRequestKeys.lists(), }); queryClient.invalidateQueries({ queryKey: ticketKeys.assignments(ticketId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.timeline(ticketId), }); queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }); queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }); }, onError: () => toast.error('Failed to request an extension'), }); } 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() }); queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }); }, onError: () => toast.error('Failed to close ticket'), }); }