feat(ticket): add issue-by-issue repair review page

This commit is contained in:
2026-07-22 14:46:34 +05:30
parent 76c87811d8
commit e94da54aa9
19 changed files with 1262 additions and 109 deletions

View File

@@ -1,7 +1,12 @@
'use client';
import { useMemo } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { toast } from 'sonner';
import { detectionService, ticketService, videoService } from '@/services/api';
@@ -10,12 +15,13 @@ import type {
CloseTicketPayload,
DiscardDetectionPayload,
RequestTicketExtensionPayload,
ReviewDetectionRepairProofPayload,
ReviewTicketExtensionPayload,
ReviewRepairPayload,
SubmitRepairPayload,
SubmitRepairUploadPayload,
TicketDetail,
TicketListParams,
DetectionProofStatus,
VideoDetectionsParams,
} from '@/types';
@@ -118,6 +124,36 @@ export function useTicketClassDetectionsQuery(
});
}
export function useTicketClassReviewDetectionsQuery(
videoId: string | undefined,
defectClass: string | undefined,
proofStatus?: DetectionProofStatus,
) {
const pageSize = 20;
return useInfiniteQuery({
queryKey: ticketKeys.classReviewDetections(
videoId ?? '',
defectClass ?? '',
proofStatus,
),
queryFn: ({ pageParam }) =>
videoService.getVideoDetections(videoId as string, {
class_name: defectClass,
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 && defectClass),
});
}
export function useDiscardDetectionMutation(
ticketId: string,
videoId: string | undefined,
@@ -158,6 +194,54 @@ export function useDiscardDetectionMutation(
});
}
export function useReviewDetectionRepairProofMutation({
ticketId,
videoId,
defectClass,
}: {
ticketId: string;
videoId: string | undefined;
defectClass: string;
}) {
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.classDetail(ticketId, defectClass),
}),
queryClient.invalidateQueries({
queryKey: ticketKeys.defectClasses(ticketId),
}),
queryClient.invalidateQueries({
queryKey: ticketKeys.overview(ticketId),
}),
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }),
]);
},
onError: () => toast.error('Failed to review repair proof'),
});
}
export function useTicketExtensionRequestQuery(
ticketId: string | undefined,
assignmentId: number | undefined,
@@ -306,26 +390,6 @@ export function useReviewTicketExtensionMutation(
onError: () => toast.error('Failed to review extension request'),
});
}
export function useReviewRepairMutation(ticketId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: ReviewRepairPayload) =>
ticketService.reviewRepair(ticketId, payload),
onSuccess: (ticket, payload) => {
toast.success(
payload.action === 'approve' ? 'Repair approved' : 'Repair rejected',
);
queryClient.setQueryData<TicketDetail>(
ticketKeys.classDetail(ticketId, payload.defect_class),
(current) => mergeTicketDetailResponse(current, ticket),
);
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
},
onError: () => toast.error('Failed to review repair'),
});
}
export function useCloseTicketMutation(ticketId: string) {
const queryClient = useQueryClient();