refactor(ticket): make repair review ticket-level

This commit is contained in:
2026-07-30 20:29:49 +05:30
parent 1cbad4074c
commit 58098af0fe
13 changed files with 25 additions and 56 deletions

View File

@@ -6,17 +6,15 @@ import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { PERMISSIONS } from '@/constants/permissions'; import { PERMISSIONS } from '@/constants/permissions';
import { PermissionGuard } from '@/guards'; import { PermissionGuard } from '@/guards';
import type { TicketDetail, TicketOverviewDetail } from '@/types'; import type { TicketOverviewDetail } from '@/types';
import { AssignTicketAction } from './actions/AssignTicketAction'; import { AssignTicketAction } from './actions/AssignTicketAction';
import { OpenRepairReviewAction } from './actions/OpenRepairReviewAction'; import { OpenRepairReviewAction } from './actions/OpenRepairReviewAction';
export function TicketDetailHeader({ export function TicketDetailHeader({
ticket, ticket,
classDetail,
}: { }: {
ticket: TicketOverviewDetail; ticket: TicketOverviewDetail;
classDetail?: TicketDetail;
}) { }) {
const router = useRouter(); const router = useRouter();
const ticketLabel = ticket.ticket_name || ticket.id; const ticketLabel = ticket.ticket_name || ticket.id;
@@ -29,11 +27,9 @@ export function TicketDetailHeader({
</div> </div>
<div className="flex shrink-0 items-center gap-2"> <div className="flex shrink-0 items-center gap-2">
{classDetail ? (
<PermissionGuard permissions={PERMISSIONS.TICKET.REVIEW}> <PermissionGuard permissions={PERMISSIONS.TICKET.REVIEW}>
<OpenRepairReviewAction ticket={classDetail} /> <OpenRepairReviewAction ticketId={ticket.id} />
</PermissionGuard> </PermissionGuard>
) : null}
<PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}> <PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}>
<AssignTicketAction ticketId={ticket.id} /> <AssignTicketAction ticketId={ticket.id} />
</PermissionGuard> </PermissionGuard>

View File

@@ -4,18 +4,15 @@ import { useRouter } from 'next/navigation';
import { ClipboardCheck } from 'lucide-react'; import { ClipboardCheck } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import type { TicketDetail } from '@/types';
import { ROUTES } from '@/utils/routes'; import { ROUTES } from '@/utils/routes';
export function OpenRepairReviewAction({ ticket }: { ticket: TicketDetail }) { export function OpenRepairReviewAction({ ticketId }: { ticketId: string }) {
const router = useRouter(); const router = useRouter();
return ( return (
<Button <Button
type="button" type="button"
onClick={() => onClick={() => router.push(ROUTES.TICKET_REVIEW(ticketId))}
router.push(ROUTES.TICKET_CLASS_REVIEW(ticket.id, ticket.defect_class))
}
> >
<ClipboardCheck /> <ClipboardCheck />
Review Repairs Review Repairs

View File

@@ -73,7 +73,7 @@ export default function TicketDetailPage() {
<main className="relative z-10 min-w-0 max-w-full space-y-5 xl:flex xl:h-[calc(100vh-6.5rem)] xl:min-h-0 xl:flex-col xl:gap-5 xl:space-y-0 xl:overflow-hidden"> <main className="relative z-10 min-w-0 max-w-full space-y-5 xl:flex xl:h-[calc(100vh-6.5rem)] xl:min-h-0 xl:flex-col xl:gap-5 xl:space-y-0 xl:overflow-hidden">
<div className="xl:shrink-0"> <div className="xl:shrink-0">
{overview ? ( {overview ? (
<TicketDetailHeader ticket={overview} classDetail={classDetail} /> <TicketDetailHeader ticket={overview} />
) : ( ) : (
<TicketOverviewHeaderSkeleton /> <TicketOverviewHeaderSkeleton />
)} )}

View File

@@ -8,7 +8,7 @@ import { cn } from '@/lib/utils';
import type { DetectionResultItem } from '@/types'; import type { DetectionResultItem } from '@/types';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { RepairProofGallery } from '../../../components/repair/RepairProofGallery'; import { RepairProofGallery } from '../../components/repair/RepairProofGallery';
import { formatVideoTimestamp, type IssueReviewStatus } from './reviewTypes'; import { formatVideoTimestamp, type IssueReviewStatus } from './reviewTypes';
const REVIEW_STATUS_LABEL: Record<IssueReviewStatus, string> = { const REVIEW_STATUS_LABEL: Record<IssueReviewStatus, string> = {

View File

@@ -79,8 +79,6 @@ export function IssueReviewQueue({
onLoadMore, onLoadMore,
}: IssueReviewQueueProps) { }: IssueReviewQueueProps) {
const loadMoreRef = useRef<HTMLDivElement>(null); const loadMoreRef = useRef<HTMLDivElement>(null);
const defectLabel = detections[0]?.detection.display_name ?? 'Detected';
useEffect(() => { useEffect(() => {
const target = loadMoreRef.current; const target = loadMoreRef.current;
if (!target || !hasNextPage || isFetchingNextPage) return; if (!target || !hasNextPage || isFetchingNextPage) return;
@@ -104,7 +102,7 @@ export function IssueReviewQueue({
<CardHeader className="border-b"> <CardHeader className="border-b">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<ListChecks className="size-5 text-primary" /> <ListChecks className="size-5 text-primary" />
{defectLabel} Issues Repair Issues
<Badge variant="secondary" className="ml-auto rounded-full"> <Badge variant="secondary" className="ml-auto rounded-full">
{totalCount} {totalCount}
</Badge> </Badge>
@@ -115,6 +113,7 @@ export function IssueReviewQueue({
<div className="space-y-2 p-1"> <div className="space-y-2 p-1">
{detections.map((detection, index) => { {detections.map((detection, index) => {
const issueId = getIssueId(detection); const issueId = getIssueId(detection);
const defectLabel = detection.detection.display_name;
const status = getIssueReviewStatus(detection); const status = getIssueReviewStatus(detection);
const config = STATUS_CONFIG[status]; const config = STATUS_CONFIG[status];
const StatusIcon = config.icon; const StatusIcon = config.icon;

View File

@@ -18,9 +18,9 @@ import type { ReviewDetectionRepairProofPayload } from '@/types';
import { import {
useReviewDetectionRepairProofMutation, useReviewDetectionRepairProofMutation,
useTicketClassReviewDetectionsQuery, useTicketReviewDetectionsQuery,
useTicketOverviewQuery, useTicketOverviewQuery,
} from '../../../hooks/useTicketQueries'; } from '../../hooks/useTicketQueries';
import { IssueEvidencePanel } from './components/IssueEvidencePanel'; import { IssueEvidencePanel } from './components/IssueEvidencePanel';
import { IssueReviewAction } from './components/IssueReviewAction'; import { IssueReviewAction } from './components/IssueReviewAction';
import { IssueReviewHeader } from './components/IssueReviewHeader'; import { IssueReviewHeader } from './components/IssueReviewHeader';
@@ -39,14 +39,11 @@ const PROOF_STATUS_FILTERS: ProofStatusFilter[] = [
'rejected', 'rejected',
]; ];
export default function TicketClassReviewPage() { export default function TicketReviewPage() {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { ticketId, defectClass } = useParams() as { const { ticketId } = useParams() as { ticketId: string };
ticketId: string;
defectClass: string;
};
const proofStatusParam = searchParams.get('proof_status'); const proofStatusParam = searchParams.get('proof_status');
const proofStatus: ProofStatusFilter = PROOF_STATUS_FILTERS.includes( const proofStatus: ProofStatusFilter = PROOF_STATUS_FILTERS.includes(
proofStatusParam as ProofStatusFilter, proofStatusParam as ProofStatusFilter,
@@ -57,9 +54,8 @@ export default function TicketClassReviewPage() {
const overviewQuery = useTicketOverviewQuery(ticketId); const overviewQuery = useTicketOverviewQuery(ticketId);
const ticket = overviewQuery.data; const ticket = overviewQuery.data;
const videoId = ticket?.video_id ?? ticket?.video?.id ?? undefined; const videoId = ticket?.video_id ?? ticket?.video?.id ?? undefined;
const detectionsQuery = useTicketClassReviewDetectionsQuery( const detectionsQuery = useTicketReviewDetectionsQuery(
videoId, videoId,
defectClass,
proofStatus === 'all' ? 'submitted' : proofStatus, proofStatus === 'all' ? 'submitted' : proofStatus,
); );
const detectionResult = detectionsQuery.data?.pages[0]; const detectionResult = detectionsQuery.data?.pages[0];
@@ -72,7 +68,6 @@ export default function TicketClassReviewPage() {
const reviewMutation = useReviewDetectionRepairProofMutation({ const reviewMutation = useReviewDetectionRepairProofMutation({
ticketId, ticketId,
videoId, videoId,
defectClass,
}); });
const selectIssue = useCallback( const selectIssue = useCallback(
@@ -171,8 +166,7 @@ export default function TicketClassReviewPage() {
}, [detectionsQuery]); }, [detectionsQuery]);
const backToTicket = () => { const backToTicket = () => {
const params = new URLSearchParams({ defect_class: defectClass }); router.push(ROUTES.TICKET_DETAIL(ticketId));
router.push(`${ROUTES.TICKET_DETAIL(ticketId)}?${params.toString()}`);
}; };
if (overviewQuery.isLoading || (videoId && detectionsQuery.isLoading)) { if (overviewQuery.isLoading || (videoId && detectionsQuery.isLoading)) {
@@ -206,7 +200,7 @@ export default function TicketClassReviewPage() {
<CardContent className="flex min-h-80 flex-col items-center justify-center gap-3 text-center"> <CardContent className="flex min-h-80 flex-col items-center justify-center gap-3 text-center">
<AlertTriangle className="size-8 text-destructive" /> <AlertTriangle className="size-8 text-destructive" />
<div> <div>
<p className="font-semibold">Failed to load class detections.</p> <p className="font-semibold">Failed to load detections.</p>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-sm text-muted-foreground">
The review screen uses the detection API for its queue and The review screen uses the detection API for its queue and
evidence. evidence.
@@ -233,7 +227,7 @@ export default function TicketClassReviewPage() {
<div className="space-y-5"> <div className="space-y-5">
<IssueReviewHeader <IssueReviewHeader
ticketLabel={ticket.ticket_name || ticket.id} ticketLabel={ticket.ticket_name || ticket.id}
defectLabel={defectClass} defectLabel="Repair"
currentIndex={0} currentIndex={0}
total={0} total={0}
proofStatus={proofStatus} proofStatus={proofStatus}
@@ -248,7 +242,7 @@ export default function TicketClassReviewPage() {
<div> <div>
<p className="font-semibold">No detections found.</p> <p className="font-semibold">No detections found.</p>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-sm text-muted-foreground">
There are no {defectClass} issues available for review. There are no issues available for review.
</p> </p>
</div> </div>
</CardContent> </CardContent>

View File

@@ -189,22 +189,16 @@ export function useTicketClassDetectionsQuery(
return useTicketDetectionsQuery(videoId, params); return useTicketDetectionsQuery(videoId, params);
} }
export function useTicketClassReviewDetectionsQuery( export function useTicketReviewDetectionsQuery(
videoId: string | undefined, videoId: string | undefined,
defectClass: string | undefined,
proofStatus?: DetectionProofStatus, proofStatus?: DetectionProofStatus,
) { ) {
const pageSize = 20; const pageSize = 20;
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: ticketKeys.classReviewDetections( queryKey: ticketKeys.reviewDetections(videoId ?? '', proofStatus),
videoId ?? '',
defectClass ?? '',
proofStatus,
),
queryFn: ({ pageParam }) => queryFn: ({ pageParam }) =>
videoService.getVideoDetections(videoId as string, { videoService.getVideoDetections(videoId as string, {
class_name: defectClass,
proof_status: proofStatus, proof_status: proofStatus,
sort: 'timestamp_asc', sort: 'timestamp_asc',
skip: pageParam, skip: pageParam,
@@ -215,7 +209,7 @@ export function useTicketClassReviewDetectionsQuery(
const nextSkip = lastPage.skip + lastPage.items.length; const nextSkip = lastPage.skip + lastPage.items.length;
return nextSkip < lastPage.total ? nextSkip : undefined; return nextSkip < lastPage.total ? nextSkip : undefined;
}, },
enabled: Boolean(videoId && defectClass), enabled: Boolean(videoId),
}); });
} }
@@ -269,11 +263,9 @@ export function useDiscardDetectionMutation(
export function useReviewDetectionRepairProofMutation({ export function useReviewDetectionRepairProofMutation({
ticketId, ticketId,
videoId, videoId,
defectClass,
}: { }: {
ticketId: string; ticketId: string;
videoId: string | undefined; videoId: string | undefined;
defectClass: string;
}) { }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -298,9 +290,6 @@ export function useReviewDetectionRepairProofMutation({
queryKey: ticketKeys.classDetectionLists(videoId), queryKey: ticketKeys.classDetectionLists(videoId),
}) })
: Promise.resolve(), : Promise.resolve(),
queryClient.invalidateQueries({
queryKey: ticketKeys.classDetail(ticketId, defectClass),
}),
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ticketKeys.defectClasses(ticketId), queryKey: ticketKeys.defectClasses(ticketId),
}), }),

View File

@@ -26,15 +26,10 @@ export const ticketKeys = {
] as const, ] as const,
detectionCoordinates: (videoId: string, params: DetectionCoordinatesParams) => detectionCoordinates: (videoId: string, params: DetectionCoordinatesParams) =>
[...ticketKeys.detectionCoordinateLists(videoId), params] as const, [...ticketKeys.detectionCoordinateLists(videoId), params] as const,
classReviewDetections: ( reviewDetections: (videoId: string, proofStatus?: string) =>
videoId: string,
defectClass: string,
proofStatus?: string,
) =>
[ [
...ticketKeys.classDetectionLists(videoId), ...ticketKeys.classDetectionLists(videoId),
'class-review', 'review',
defectClass,
proofStatus ?? 'all', proofStatus ?? 'all',
] as const, ] as const,
defectClasses: (ticketId: string) => defectClasses: (ticketId: string) =>

View File

@@ -17,6 +17,5 @@ export const ROUTES = {
TICKET: '/ticket', TICKET: '/ticket',
TICKET_DETAIL: (ticketId: string) => `/ticket/${ticketId}`, TICKET_DETAIL: (ticketId: string) => `/ticket/${ticketId}`,
TICKET_ASSIGN: (ticketId: string) => `/ticket/${ticketId}/assign`, TICKET_ASSIGN: (ticketId: string) => `/ticket/${ticketId}/assign`,
TICKET_CLASS_REVIEW: (ticketId: string, defectClass: string) => TICKET_REVIEW: (ticketId: string) => `/ticket/${ticketId}/review`,
`/ticket/${ticketId}/${defectClass}/review`,
} as const; } as const;