feat(ticket): support criteria-based global ticket assignment
This commit is contained in:
@@ -19,12 +19,19 @@ import {
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
|
||||
import type { AssignmentRangePoint } from './assignmentRange';
|
||||
import { AssignmentRangeActions } from './AssignmentRangeActions';
|
||||
|
||||
type AssignmentIssuesMapProps = {
|
||||
data?: DetectionCoordinatesResponse;
|
||||
isLoading: boolean;
|
||||
isFetchingMore: boolean;
|
||||
isError: boolean;
|
||||
hasMore: boolean;
|
||||
startPoint: AssignmentRangePoint | null;
|
||||
endPoint: AssignmentRangePoint | null;
|
||||
onSetStart: (point: AssignmentRangePoint) => void;
|
||||
onSetEnd: (point: AssignmentRangePoint) => void;
|
||||
onLoadMore: () => void;
|
||||
onRetry: () => void;
|
||||
};
|
||||
@@ -33,7 +40,11 @@ type ClientAssignmentIssuesMapProps = {
|
||||
items: DetectionCoordinateItem[];
|
||||
classes: DetectionCoordinateClass[];
|
||||
selectedDetectionId: number | null;
|
||||
startPoint: AssignmentRangePoint | null;
|
||||
endPoint: AssignmentRangePoint | null;
|
||||
onSelectDetection: (detectionId: number) => void;
|
||||
onSetStart: (point: AssignmentRangePoint) => void;
|
||||
onSetEnd: (point: AssignmentRangePoint) => void;
|
||||
};
|
||||
|
||||
function isValidCoordinate(item: DetectionCoordinateItem) {
|
||||
@@ -49,8 +60,9 @@ function isValidCoordinate(item: DetectionCoordinateItem) {
|
||||
|
||||
const ClientAssignmentIssuesMap = dynamic<ClientAssignmentIssuesMapProps>(
|
||||
async () => {
|
||||
const { CircleMarker, MapContainer, Popup, TileLayer, useMap } =
|
||||
const { CircleMarker, MapContainer, Popup, TileLayer, Tooltip, useMap } =
|
||||
await import('react-leaflet');
|
||||
const { canvas } = await import('leaflet');
|
||||
|
||||
function FitMapToIssues({ items }: { items: DetectionCoordinateItem[] }) {
|
||||
const map = useMap();
|
||||
@@ -81,8 +93,13 @@ const ClientAssignmentIssuesMap = dynamic<ClientAssignmentIssuesMapProps>(
|
||||
items,
|
||||
classes,
|
||||
selectedDetectionId,
|
||||
startPoint,
|
||||
endPoint,
|
||||
onSelectDetection,
|
||||
onSetStart,
|
||||
onSetEnd,
|
||||
}: ClientAssignmentIssuesMapProps) {
|
||||
const markerRenderer = useMemo(() => canvas({ tolerance: 12 }), []);
|
||||
const displayNames = new Map(
|
||||
classes.map((item) => [item.class_name, item.display_name]),
|
||||
);
|
||||
@@ -108,30 +125,48 @@ const ClientAssignmentIssuesMap = dynamic<ClientAssignmentIssuesMapProps>(
|
||||
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 ? 'A' : isEnd ? 'B' : null;
|
||||
const rangeColor = isStart
|
||||
? '#16a34a'
|
||||
: isEnd
|
||||
? '#dc2626'
|
||||
: visual.boundingBoxColor;
|
||||
const isRangeIssue = isStart || isEnd;
|
||||
const displayName =
|
||||
displayNames.get(item.class_name) ??
|
||||
item.class_name.replaceAll('_', ' ');
|
||||
const point: AssignmentRangePoint = {
|
||||
detectionId: item.id,
|
||||
latitude: item.latitude,
|
||||
longitude: item.longitude,
|
||||
};
|
||||
|
||||
return (
|
||||
<CircleMarker
|
||||
key={item.id}
|
||||
center={[item.latitude, item.longitude]}
|
||||
radius={isSelected ? 11 : 8}
|
||||
renderer={markerRenderer}
|
||||
radius={isSelected || isRangeIssue ? 11 : 8}
|
||||
pathOptions={{
|
||||
color: isSelected ? '#ffffff' : visual.boundingBoxColor,
|
||||
fillColor: visual.boundingBoxColor,
|
||||
fillOpacity: isSelected ? 1 : 0.82,
|
||||
weight: isSelected ? 4 : 2,
|
||||
color:
|
||||
isSelected || isRangeIssue
|
||||
? '#ffffff'
|
||||
: visual.boundingBoxColor,
|
||||
fillColor: rangeColor,
|
||||
fillOpacity: isSelected || isRangeIssue ? 1 : 0.82,
|
||||
weight: isSelected || isRangeIssue ? 4 : 2,
|
||||
}}
|
||||
eventHandlers={{
|
||||
click: () => onSelectDetection(item.id),
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<div className="min-w-52 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Popup minWidth={240} maxWidth={280}>
|
||||
<div className="w-60">
|
||||
<div className="flex items-start gap-3 border-b pb-3 pr-4">
|
||||
<span
|
||||
className="flex size-8 items-center justify-center rounded-full"
|
||||
className="flex size-9 shrink-0 items-center justify-center rounded-full"
|
||||
style={{
|
||||
backgroundColor: `${visual.boundingBoxColor}1f`,
|
||||
color: visual.boundingBoxColor,
|
||||
@@ -139,32 +174,63 @@ const ClientAssignmentIssuesMap = dynamic<ClientAssignmentIssuesMapProps>(
|
||||
>
|
||||
<IssueIcon className="size-4" />
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-semibold capitalize">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="!m-0 truncate text-sm font-semibold leading-5">
|
||||
{displayName}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="!m-0 text-xs leading-4 text-muted-foreground">
|
||||
Detection #{item.id}
|
||||
</p>
|
||||
</div>
|
||||
{rangeLabel ? (
|
||||
<span
|
||||
className="flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white"
|
||||
style={{ backgroundColor: rangeColor }}
|
||||
aria-label={isStart ? 'Start issue' : 'End issue'}
|
||||
>
|
||||
{rangeLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
|
||||
<dt className="text-muted-foreground">Confidence</dt>
|
||||
<dd className="text-right font-medium">
|
||||
{Math.round(item.confidence * 100)}%
|
||||
</dd>
|
||||
<dt className="text-muted-foreground">Latitude</dt>
|
||||
<dd className="text-right font-mono text-xs">
|
||||
{item.latitude.toFixed(6)}
|
||||
</dd>
|
||||
<dt className="text-muted-foreground">Longitude</dt>
|
||||
<dd className="text-right font-mono text-xs">
|
||||
{item.longitude.toFixed(6)}
|
||||
</dd>
|
||||
<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">
|
||||
{item.latitude.toFixed(6)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<dt className="text-muted-foreground">Longitude</dt>
|
||||
<dd className="!m-0 font-mono text-xs tabular-nums">
|
||||
{item.longitude.toFixed(6)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="border-t pt-3">
|
||||
<AssignmentRangeActions
|
||||
point={point}
|
||||
startPoint={startPoint}
|
||||
endPoint={endPoint}
|
||||
onSetStart={onSetStart}
|
||||
onSetEnd={onSetEnd}
|
||||
stretch
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
{rangeLabel ? (
|
||||
<Tooltip permanent direction="center" opacity={1}>
|
||||
{rangeLabel}
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</CircleMarker>
|
||||
);
|
||||
})}
|
||||
@@ -184,6 +250,10 @@ export function AssignmentIssuesMap({
|
||||
isFetchingMore,
|
||||
isError,
|
||||
hasMore,
|
||||
startPoint,
|
||||
endPoint,
|
||||
onSetStart,
|
||||
onSetEnd,
|
||||
onLoadMore,
|
||||
onRetry,
|
||||
}: AssignmentIssuesMapProps) {
|
||||
@@ -217,7 +287,7 @@ export function AssignmentIssuesMap({
|
||||
<div>
|
||||
<CardTitle className="text-base">Issues on Map</CardTitle>
|
||||
<CardDescription>
|
||||
Click a map point to view the issue details.
|
||||
Click a marker, then choose Start or End.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
@@ -261,7 +331,11 @@ export function AssignmentIssuesMap({
|
||||
items={validItems}
|
||||
classes={data?.classes ?? []}
|
||||
selectedDetectionId={selectedDetectionId}
|
||||
startPoint={startPoint}
|
||||
endPoint={endPoint}
|
||||
onSelectDetection={setSelectedDetectionId}
|
||||
onSetStart={onSetStart}
|
||||
onSetEnd={onSetEnd}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user