feat(ticket): use assignment detections API for unassigned issues
This commit is contained in:
@@ -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<Date>();
|
||||
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);
|
||||
|
||||
@@ -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<ClientAssignmentIssuesMapProps>(
|
||||
</div>
|
||||
|
||||
<dl className="!m-0 space-y-2 py-3 text-sm">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<dt className="text-muted-foreground">Confidence</dt>
|
||||
<dd className="!m-0 font-medium tabular-nums">
|
||||
{Math.round(item.confidence * 100)}%
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<dt className="text-muted-foreground">Latitude</dt>
|
||||
<dd className="!m-0 font-mono text-xs tabular-nums">
|
||||
@@ -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],
|
||||
|
||||
@@ -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<string, VideoDetectionsResponse>
|
||||
Record<string, TicketAssignmentDetectionsResponse>
|
||||
>({});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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<DetectionResultItem>[] {
|
||||
}: UseIssueColumnsOptions): ColumnDef<TicketAssignmentDetectionItem>[] {
|
||||
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',
|
||||
|
||||
@@ -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<DetectionResultItem>[];
|
||||
issues: DetectionResultItem[];
|
||||
columns: ColumnDef<TicketAssignmentDetectionItem>[];
|
||||
issues: TicketAssignmentDetectionItem[];
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
toolbar: ReactNode;
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid min-w-0 items-start gap-5 xl:min-h-0 xl:flex-1 xl:grid-cols-[minmax(0,1fr)_360px] xl:items-stretch xl:overflow-hidden">
|
||||
<div className="min-w-0 xl:h-full xl:min-h-0 xl:overflow-y-auto xl:overscroll-contain xl:pr-2">
|
||||
<ChooseIssuesSection
|
||||
videoId={videoId}
|
||||
ticketId={ticketId}
|
||||
issueTypes={overview.ai_result.detections_by_class}
|
||||
selectedIssueTypes={selectedIssueTypes}
|
||||
cacheRevision={assignmentRevision}
|
||||
@@ -146,7 +145,6 @@ export default function TicketAssignmentPage() {
|
||||
>
|
||||
<AssignTicketForm
|
||||
ticketId={ticketId}
|
||||
videoId={videoId}
|
||||
selectedClassNames={selectedIssueTypes}
|
||||
startPoint={startPoint}
|
||||
endPoint={endPoint}
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
RequestTicketExtensionPayload,
|
||||
ReviewDetectionRepairProofPayload,
|
||||
TicketAssignmentSummary,
|
||||
TicketAssignmentDetectionsParams,
|
||||
TicketListParams,
|
||||
DetectionProofStatus,
|
||||
VideoDetectionsParams,
|
||||
@@ -136,6 +137,32 @@ export function useTicketClassDetectionsQuery(
|
||||
return useTicketDetectionsQuery(videoId, params);
|
||||
}
|
||||
|
||||
export function useTicketAssignmentDetectionsQuery(
|
||||
ticketId: string | undefined,
|
||||
params: TicketAssignmentDetectionsParams,
|
||||
) {
|
||||
const queryParams = useMemo(
|
||||
() => ({
|
||||
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'),
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user