'use client'; import { useEffect, useMemo, useState } from 'react'; import dynamic from 'next/dynamic'; import { AlertTriangle, MapPinned, RefreshCw } from 'lucide-react'; import { getDefectVisual } from '@/constants/defectVisualConfig'; import type { TicketAssignmentDetectionItem, TicketAssignmentDetectionsResponse, } from '@/types'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import type { AssignmentRangePoint } from './assignmentRange'; type AssignmentIssuesMapProps = { data?: TicketAssignmentDetectionsResponse; isLoading: boolean; isError: boolean; startPoint: AssignmentRangePoint | null; endPoint: AssignmentRangePoint | null; onRetry: () => void; }; type AssignmentMapItem = { id: number; class_name: string; display_name: string; latitude: number; longitude: number; sequence: number; }; type AssignmentMapClass = { class_name: string; display_name: string; }; type ClientAssignmentIssuesMapProps = { items: AssignmentMapItem[]; classes: AssignmentMapClass[]; selectedDetectionId: number | null; startPoint: AssignmentRangePoint | null; endPoint: AssignmentRangePoint | null; onSelectDetection: (detectionId: number) => void; }; function toMapItem( item: TicketAssignmentDetectionItem, sequence: number, ): AssignmentMapItem | null { const { latitude, longitude } = item.location; if ( typeof latitude !== 'number' || typeof longitude !== 'number' || !Number.isFinite(latitude) || !Number.isFinite(longitude) || latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180 ) { return null; } return { id: item.detection.id, class_name: item.detection.class_name, display_name: item.detection.display_name, latitude, longitude, sequence, }; } const ClientAssignmentIssuesMap = dynamic( async () => { const { CircleMarker, MapContainer, Polyline, Popup, TileLayer, Tooltip, useMap, } = await import('react-leaflet'); const { canvas } = await import('leaflet'); function FitMapToIssues({ items }: { items: AssignmentMapItem[] }) { const map = useMap(); useEffect(() => { if (items.length === 0) return; if (items.length === 1) { map.setView([items[0].latitude, items[0].longitude], 17); return; } map.fitBounds( items.map( (item) => [item.latitude, item.longitude] as [number, number], ), { padding: [32, 32], maxZoom: 17, }, ); }, [items, map]); return null; } return function ClientAssignmentIssuesMapInner({ items, classes, selectedDetectionId, startPoint, endPoint, onSelectDetection, }: ClientAssignmentIssuesMapProps) { const markerRenderer = useMemo(() => canvas({ tolerance: 12 }), []); const displayNames = new Map( classes.map((item) => [item.class_name, item.display_name]), ); const initialCenter: [number, number] = [ items[0].latitude, items[0].longitude, ]; const orderedItems = [...items].sort( (first, second) => first.sequence - second.sequence || first.id - second.id, ); const startIndex = orderedItems.findIndex( (item) => item.id === startPoint?.detectionId, ); const endIndex = orderedItems.findIndex( (item) => item.id === endPoint?.detectionId, ); const hasSelectedRange = startIndex >= 0 && endIndex >= 0; const rangeItems = hasSelectedRange ? orderedItems.slice( Math.min(startIndex, endIndex), Math.max(startIndex, endIndex) + 1, ) : []; const rangeDetectionIds = new Set(rangeItems.map((item) => item.id)); const rangePath = rangeItems.map( (item) => [item.latitude, item.longitude] as [number, number], ); return ( {rangePath.length >= 2 ? ( <> ) : null} {items.map((item) => { const visual = getDefectVisual(item.class_name); const IssueIcon = visual.icon; const isSelected = selectedDetectionId === item.id; const isStart = startPoint?.detectionId === item.id; const isEnd = endPoint?.detectionId === item.id; const rangeLabel = isStart ? 'Start' : isEnd ? 'End' : null; const rangeColor = isStart ? '#16a34a' : isEnd ? '#dc2626' : visual.boundingBoxColor; const isRangeIssue = isStart || isEnd; const isInsideRange = rangeDetectionIds.has(item.id); const displayName = displayNames.get(item.class_name) ?? item.display_name ?? item.class_name.replaceAll('_', ' '); return ( onSelectDetection(item.id), }} >

{displayName}

Detection #{item.id}

{rangeLabel ? ( {rangeLabel} ) : null}
Latitude
{item.latitude.toFixed(6)}
Longitude
{item.longitude.toFixed(6)}
{rangeLabel ? ( {rangeLabel} ) : null}
); })}
); }; }, { ssr: false, loading: () =>
, }, ); export function AssignmentIssuesMap({ data, isLoading, isError, startPoint, endPoint, onRetry, }: AssignmentIssuesMapProps) { const [selectedDetectionId, setSelectedDetectionId] = useState( null, ); const validItems = useMemo( () => (data?.items ?? []).flatMap((item, index) => { const mapItem = toMapItem(item, index); return mapItem ? [mapItem] : []; }), [data?.items], ); const classes = useMemo( () => Array.from( new Map( validItems.map((item) => [ item.class_name, { class_name: item.class_name, display_name: item.display_name, }, ]), ).values(), ), [validItems], ); const pageItemCount = data?.items.length ?? 0; useEffect(() => { if ( selectedDetectionId !== null && !validItems.some((item) => item.id === selectedDetectionId) ) { setSelectedDetectionId(null); } }, [selectedDetectionId, validItems]); return (
Issues on Map Select Start and End in the table to preview the covered range.
{data ? ( {validItems.length} mapped ) : null}
{isLoading ? (
) : isError ? (

Unable to load issue locations.

Check the connection and try again.

) : validItems.length === 0 ? (

No issue locations available.

Detections without valid coordinates cannot be placed on the map.

) : ( <>
Map legend {classes.map((item) => { const visual = getDefectVisual(item.class_name); const IssueIcon = visual.icon; return (
{item.display_name}
); })} {startPoint ? (
Start
) : null} {endPoint ? (
End
) : null} {startPoint && endPoint ? (
Coverage area
) : null}
{validItems.length} mapped from {pageItemCount} cached{' '} {pageItemCount === 1 ? 'issue' : 'issues'}
)} ); }