406 lines
11 KiB
TypeScript
406 lines
11 KiB
TypeScript
'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,
|
|
TicketAssignmentSummary,
|
|
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 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<DetectionCoordinatesParams>(
|
|
() => ({
|
|
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: extensionRequestKeys.lists(),
|
|
});
|
|
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 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'),
|
|
});
|
|
}
|