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

View File

@@ -4,18 +4,15 @@ import { useRouter } from 'next/navigation';
import { ClipboardCheck } from 'lucide-react';
import { Button } from '@/components/ui/button';
import type { TicketDetail } from '@/types';
import { ROUTES } from '@/utils/routes';
export function OpenRepairReviewAction({ ticket }: { ticket: TicketDetail }) {
export function OpenRepairReviewAction({ ticketId }: { ticketId: string }) {
const router = useRouter();
return (
<Button
type="button"
onClick={() =>
router.push(ROUTES.TICKET_CLASS_REVIEW(ticket.id, ticket.defect_class))
}
onClick={() => router.push(ROUTES.TICKET_REVIEW(ticketId))}
>
<ClipboardCheck />
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">
<div className="xl:shrink-0">
{overview ? (
<TicketDetailHeader ticket={overview} classDetail={classDetail} />
<TicketDetailHeader ticket={overview} />
) : (
<TicketOverviewHeaderSkeleton />
)}

View File

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

View File

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

View File

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

View File

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

View File

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