refactor(ticket): make repair review ticket-level
This commit is contained in:
@@ -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 ticketId={ticket.id} />
|
||||||
<OpenRepairReviewAction ticket={classDetail} />
|
</PermissionGuard>
|
||||||
</PermissionGuard>
|
|
||||||
) : null}
|
|
||||||
<PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}>
|
<PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}>
|
||||||
<AssignTicketAction ticketId={ticket.id} />
|
<AssignTicketAction ticketId={ticket.id} />
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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> = {
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
@@ -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),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user