diff --git a/src/app/(modules)/ticket/[ticketId]/assign/components/AssignTicketForm.tsx b/src/app/(modules)/ticket/[ticketId]/assign/components/AssignTicketForm.tsx index 9209a03..5facaf6 100644 --- a/src/app/(modules)/ticket/[ticketId]/assign/components/AssignTicketForm.tsx +++ b/src/app/(modules)/ticket/[ticketId]/assign/components/AssignTicketForm.tsx @@ -28,7 +28,6 @@ import { interface AssignTicketFormProps { ticketId: string; - videoId?: string; selectedClassNames: string[]; startPoint: AssignmentRangePoint | null; endPoint: AssignmentRangePoint | null; @@ -37,7 +36,6 @@ interface AssignTicketFormProps { export function AssignTicketForm({ ticketId, - videoId, selectedClassNames, startPoint, endPoint, @@ -49,7 +47,7 @@ export function AssignTicketForm({ const [dueDate, setDueDate] = useState(); const [assignNote, setAssignNote] = useState(''); - const assignMutation = useAssignTicketMutation(ticketId, videoId); + const assignMutation = useAssignTicketMutation(ticketId); const today = useMemo(() => { const date = new Date(); date.setHours(0, 0, 0, 0); diff --git a/src/app/(modules)/ticket/[ticketId]/assign/components/AssignmentIssuesMap.tsx b/src/app/(modules)/ticket/[ticketId]/assign/components/AssignmentIssuesMap.tsx index 7566bac..ce14820 100644 --- a/src/app/(modules)/ticket/[ticketId]/assign/components/AssignmentIssuesMap.tsx +++ b/src/app/(modules)/ticket/[ticketId]/assign/components/AssignmentIssuesMap.tsx @@ -5,7 +5,10 @@ import dynamic from 'next/dynamic'; import { AlertTriangle, MapPinned, RefreshCw } from 'lucide-react'; import { getDefectVisual } from '@/constants/defectVisualConfig'; -import type { DetectionResultItem, VideoDetectionsResponse } from '@/types'; +import type { + TicketAssignmentDetectionItem, + TicketAssignmentDetectionsResponse, +} from '@/types'; import { Button } from '@/components/ui/button'; import { Card, @@ -18,7 +21,7 @@ import { import type { AssignmentRangePoint } from './assignmentRange'; type AssignmentIssuesMapProps = { - data?: VideoDetectionsResponse; + data?: TicketAssignmentDetectionsResponse; isLoading: boolean; isError: boolean; startPoint: AssignmentRangePoint | null; @@ -32,7 +35,6 @@ type AssignmentMapItem = { display_name: string; latitude: number; longitude: number; - confidence: number; sequence: number; }; @@ -50,7 +52,10 @@ type ClientAssignmentIssuesMapProps = { onSelectDetection: (detectionId: number) => void; }; -function toMapItem(item: DetectionResultItem): AssignmentMapItem | null { +function toMapItem( + item: TicketAssignmentDetectionItem, + sequence: number, +): AssignmentMapItem | null { const { latitude, longitude } = item.location; if ( typeof latitude !== 'number' || @@ -71,8 +76,7 @@ function toMapItem(item: DetectionResultItem): AssignmentMapItem | null { display_name: item.detection.display_name, latitude, longitude, - confidence: item.detection.confidence, - sequence: item.frame.timestamp_seconds, + sequence, }; } @@ -263,12 +267,6 @@ const ClientAssignmentIssuesMap = dynamic(
-
-
Confidence
-
- {Math.round(item.confidence * 100)}% -
-
Latitude
@@ -321,8 +319,8 @@ export function AssignmentIssuesMap({ ); const validItems = useMemo( () => - (data?.items ?? []).flatMap((item) => { - const mapItem = toMapItem(item); + (data?.items ?? []).flatMap((item, index) => { + const mapItem = toMapItem(item, index); return mapItem ? [mapItem] : []; }), [data?.items], diff --git a/src/app/(modules)/ticket/[ticketId]/assign/components/ChooseIssuesSection.tsx b/src/app/(modules)/ticket/[ticketId]/assign/components/ChooseIssuesSection.tsx index 7be4c15..588f482 100644 --- a/src/app/(modules)/ticket/[ticketId]/assign/components/ChooseIssuesSection.tsx +++ b/src/app/(modules)/ticket/[ticketId]/assign/components/ChooseIssuesSection.tsx @@ -7,9 +7,9 @@ import { MultiSelectPopover } from '@/components/form/MultiSelectPopover'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import type { VideoDetectionsResponse } from '@/types'; +import type { TicketAssignmentDetectionsResponse } from '@/types'; -import { useTicketDetectionsQuery } from '../../../hooks/useTicketQueries'; +import { useTicketAssignmentDetectionsQuery } from '../../../hooks/useTicketQueries'; import { formatCompactRangeCoordinate, formatRangeCoordinate, @@ -28,7 +28,7 @@ interface IssueTypeOption { } interface ChooseIssuesSectionProps { - videoId?: string; + ticketId: string; issueTypes: IssueTypeOption[]; selectedIssueTypes: string[]; cacheRevision: number; @@ -40,7 +40,7 @@ interface ChooseIssuesSectionProps { } export function ChooseIssuesSection({ - videoId, + ticketId, issueTypes, selectedIssueTypes, cacheRevision, @@ -101,22 +101,25 @@ export function ChooseIssuesSection({ const queryParams = useMemo( () => ({ + assignment_status: 'unassigned' as const, skip, limit, class_name: selectedIssueTypes.length > 0 ? selectedIssueTypes : undefined, - sort: 'timestamp_asc', }), [limit, selectedIssueTypes, skip], ); - const detectionsQuery = useTicketDetectionsQuery(videoId, queryParams); + const detectionsQuery = useTicketAssignmentDetectionsQuery( + ticketId, + queryParams, + ); const result = detectionsQuery.data; const mapCacheKey = useMemo( - () => JSON.stringify([videoId ?? '', selectedIssueTypes, cacheRevision]), - [cacheRevision, selectedIssueTypes, videoId], + () => JSON.stringify([ticketId, selectedIssueTypes, cacheRevision]), + [cacheRevision, selectedIssueTypes, ticketId], ); const [mapCache, setMapCache] = useState< - Record + Record >({}); useEffect(() => { diff --git a/src/app/(modules)/ticket/[ticketId]/assign/components/IssueColumns.tsx b/src/app/(modules)/ticket/[ticketId]/assign/components/IssueColumns.tsx index c625a6b..2c66e83 100644 --- a/src/app/(modules)/ticket/[ticketId]/assign/components/IssueColumns.tsx +++ b/src/app/(modules)/ticket/[ticketId]/assign/components/IssueColumns.tsx @@ -5,17 +5,11 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Badge } from '@/components/ui/badge'; import { getDefectVisual } from '@/constants/defectVisualConfig'; -import type { DetectionResultItem } from '@/types'; +import type { TicketAssignmentDetectionItem } from '@/types'; import type { AssignmentRangePoint } from './assignmentRange'; import { AssignmentRangeActions } from './AssignmentRangeActions'; -function formatTimestamp(seconds: number) { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = Math.floor(seconds % 60); - return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; -} - interface UseIssueColumnsOptions { startPoint: AssignmentRangePoint | null; endPoint: AssignmentRangePoint | null; @@ -28,7 +22,7 @@ export function useIssueColumns({ endPoint, onSetStart, onSetEnd, -}: UseIssueColumnsOptions): ColumnDef[] { +}: UseIssueColumnsOptions): ColumnDef[] { return useMemo( () => [ { @@ -89,13 +83,6 @@ export function useIssueColumns({ ); }, }, - { - id: 'timestamp', - header: 'Timestamp', - size: 130, - cell: ({ row }) => - formatTimestamp(row.original.frame.timestamp_seconds), - }, { id: 'range_actions', header: 'Set range', diff --git a/src/app/(modules)/ticket/[ticketId]/assign/components/IssueTable.tsx b/src/app/(modules)/ticket/[ticketId]/assign/components/IssueTable.tsx index 3920e01..011cd1a 100644 --- a/src/app/(modules)/ticket/[ticketId]/assign/components/IssueTable.tsx +++ b/src/app/(modules)/ticket/[ticketId]/assign/components/IssueTable.tsx @@ -4,11 +4,11 @@ import type { ReactNode } from 'react'; import type { ColumnDef } from '@tanstack/react-table'; import { DataTable } from '@/components/data-table'; -import type { DetectionResultItem } from '@/types'; +import type { TicketAssignmentDetectionItem } from '@/types'; interface IssueTableProps { - columns: ColumnDef[]; - issues: DetectionResultItem[]; + columns: ColumnDef[]; + issues: TicketAssignmentDetectionItem[]; isLoading: boolean; isError: boolean; toolbar: ReactNode; diff --git a/src/app/(modules)/ticket/[ticketId]/assign/page.tsx b/src/app/(modules)/ticket/[ticketId]/assign/page.tsx index 03af3c3..6e44fdb 100644 --- a/src/app/(modules)/ticket/[ticketId]/assign/page.tsx +++ b/src/app/(modules)/ticket/[ticketId]/assign/page.tsx @@ -45,7 +45,6 @@ export default function TicketAssignmentPage() { const [assignmentRevision, setAssignmentRevision] = useState(0); const overviewQuery = useTicketOverviewQuery(ticketId); const overview = overviewQuery.data; - const videoId = overview?.video_id ?? overview?.video?.id ?? undefined; const ticketDetailUrl = ROUTES.TICKET_DETAIL(ticketId); const backToTicket = () => router.push(ticketDetailUrl); @@ -107,7 +106,7 @@ export default function TicketAssignmentPage() {
({ + assignment_status: params.assignment_status ?? 'unassigned', + class_name: params.class_name, + skip: params.skip ?? 0, + limit: params.limit ?? 500, + }), + [params.assignment_status, params.class_name, params.limit, params.skip], + ); + + return useQuery({ + queryKey: ticketKeys.assignmentDetections(ticketId ?? '', queryParams), + queryFn: () => + ticketService.getTicketAssignmentDetections( + ticketId as string, + queryParams, + ), + enabled: Boolean(ticketId), + staleTime: Infinity, + }); +} + export function useTicketReviewDetectionsQuery( videoId: string | undefined, proofStatus?: DetectionProofStatus, @@ -262,7 +289,7 @@ export function useAssignableTicketUsersQuery(enabled: boolean) { }); } -export function useAssignTicketMutation(ticketId: string, videoId?: string) { +export function useAssignTicketMutation(ticketId: string) { const queryClient = useQueryClient(); return useMutation({ @@ -278,13 +305,12 @@ export function useAssignTicketMutation(ticketId: string, videoId?: string) { queryKey: ticketKeys.timeline(ticketId), }), queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }), - ...(videoId - ? [ - queryClient.invalidateQueries({ - queryKey: ticketKeys.classDetectionLists(videoId), - }), - ] - : []), + queryClient.invalidateQueries({ + queryKey: ticketKeys.assignmentDetectionLists(ticketId), + }), + queryClient.invalidateQueries({ + queryKey: ticketKeys.assignments(ticketId), + }), ]); }, onError: () => toast.error('Failed to assign ticket'), diff --git a/src/app/(modules)/ticket/queries/ticketKeys.ts b/src/app/(modules)/ticket/queries/ticketKeys.ts index c0e1715..4590fc7 100644 --- a/src/app/(modules)/ticket/queries/ticketKeys.ts +++ b/src/app/(modules)/ticket/queries/ticketKeys.ts @@ -1,4 +1,8 @@ -import type { TicketListParams, VideoDetectionsParams } from '@/types'; +import type { + TicketAssignmentDetectionsParams, + TicketListParams, + VideoDetectionsParams, +} from '@/types'; export const ticketKeys = { all: ['tickets'] as const, @@ -19,6 +23,12 @@ export const ticketKeys = { 'review', proofStatus ?? 'all', ] as const, + assignmentDetectionLists: (ticketId: string) => + [...ticketKeys.details(), ticketId, 'assignment-detections'] as const, + assignmentDetections: ( + ticketId: string, + params: TicketAssignmentDetectionsParams, + ) => [...ticketKeys.assignmentDetectionLists(ticketId), params] as const, assignments: (ticketId: string) => [...ticketKeys.details(), ticketId, 'assignments'] as const, extensionRequest: (ticketId: string, assignmentId: number | undefined) => diff --git a/src/constants/apiRoutes.ts b/src/constants/apiRoutes.ts index c2f1685..7fbaaed 100644 --- a/src/constants/apiRoutes.ts +++ b/src/constants/apiRoutes.ts @@ -64,6 +64,8 @@ export const API_ROUTES = { TICKETS: { BASE: '/biz/api/v1/tickets', DETAIL: (id: string) => `/biz/api/v1/tickets/${id}`, + ASSIGNMENT_DETECTIONS: (id: string) => + `/biz/api/v1/tickets/${id}/assignment-detections`, TIMELINE: (id: string) => `/biz/api/v1/tickets/${id}/timeline`, ASSIGNMENTS: (id: string) => `/biz/api/v1/tickets/${id}/assignments`, REQUEST_EXTENSION: (ticketId: string, assignmentId: number) => diff --git a/src/services/api/ticket.service.ts b/src/services/api/ticket.service.ts index c17acec..e9df4b2 100644 --- a/src/services/api/ticket.service.ts +++ b/src/services/api/ticket.service.ts @@ -9,6 +9,8 @@ import type { ReviewTicketExtensionPayload, SseTokenResponse, TicketAssignmentSummary, + TicketAssignmentDetectionsParams, + TicketAssignmentDetectionsResponse, TicketAssignmentsResponse, TicketExtensionRequestsResponse, TicketExtensionRequestsParams, @@ -52,6 +54,27 @@ export const ticketService = { return response.data; }, + getTicketAssignmentDetections: async ( + ticketId: string, + params?: TicketAssignmentDetectionsParams, + ): Promise => { + const response = await axiosClient.get( + API_ROUTES.TICKETS.ASSIGNMENT_DETECTIONS(ticketId), + { + params: { + assignment_status: params?.assignment_status ?? 'unassigned', + class_name: params?.class_name, + skip: params?.skip ?? 0, + limit: params?.limit ?? 500, + }, + paramsSerializer: { + indexes: null, + }, + }, + ); + return response.data; + }, + getTicketTimeline: async ( ticketId: string, ): Promise => { diff --git a/src/types/detection.ts b/src/types/detection.ts index aef1644..712a64c 100644 --- a/src/types/detection.ts +++ b/src/types/detection.ts @@ -104,6 +104,37 @@ export type VideoDetectionsResponse = { items: DetectionResultItem[]; }; +export type AssignmentDetectionStatus = 'assigned' | 'unassigned'; + +export type TicketAssignmentDetectionsParams = { + assignment_status?: AssignmentDetectionStatus; + class_name?: string | string[]; + skip?: number; + limit?: number; +}; + +export type TicketAssignmentDetectionItem = { + id: string; + detection: { + id: number; + class_name: string; + display_name: string; + }; + location: { + latitude: number; + longitude: number; + }; +}; + +export type TicketAssignmentDetectionsResponse = { + ticket_id: string; + assignment_status: AssignmentDetectionStatus; + items: TicketAssignmentDetectionItem[]; + total: number; + skip: number; + limit: number; +}; + export type DetectionDiscardReason = | 'not_a_defect' | 'wrong_class'