diff --git a/src/components/DimensionsView.tsx b/src/components/DimensionsView.tsx index 6b26b3c..524ca27 100644 --- a/src/components/DimensionsView.tsx +++ b/src/components/DimensionsView.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo } from 'react'; import { ExcelRow, COLUMNS } from '../types'; -import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, RefreshCw, Layers } from 'lucide-react'; +import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, RefreshCw, Layers, Link2 } from 'lucide-react'; import { cn } from '../lib/utils'; import { ConfirmModal } from './ConfirmModal'; @@ -17,6 +17,7 @@ interface DimensionGroup { innerDims: string; rows: { row: ExcelRow; index: number }[]; isInconsistent: boolean; + volume: number; discrepancies: { outer: boolean; units: boolean; @@ -24,8 +25,15 @@ interface DimensionGroup { }; } +interface NearDuplicateCluster { + groups: DimensionGroup[]; + volumes: number[]; + maxDiffPct: number; +} + export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureState }: DimensionsViewProps) { const [expandedGroups, setExpandedGroups] = useState>(new Set()); + const [expandedNearDuplicates, setExpandedNearDuplicates] = useState>(new Set()); const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(true); const [syncing, setSyncing] = useState<{ key: string, field: string } | null>(null); const [pendingAction, setPendingAction] = useState<{ @@ -77,10 +85,14 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat if (moq !== firstMOQ) moqMatch = false; }); + const parts = key.split('x').map(Number); + const volume = parts[0] * parts[1] * parts[2]; + result.push({ key, innerDims: key, rows, + volume, isInconsistent: !outerMatch || !unitsMatch || !moqMatch, discrepancies: { outer: !outerMatch, @@ -97,6 +109,46 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat return showOnlyInconsistent ? groups.filter(g => g.isInconsistent) : groups; }, [groups, showOnlyInconsistent]); + const nearDuplicateClusters = useMemo((): NearDuplicateCluster[] => { + const validGroups = groups.filter(g => g.volume > 0); + const assignedKeys = new Set(); + const clusters: NearDuplicateCluster[] = []; + + for (let i = 0; i < validGroups.length; i++) { + const a = validGroups[i]; + if (assignedKeys.has(a.key)) continue; + + const cluster: DimensionGroup[] = [a]; + assignedKeys.add(a.key); + + for (let j = i + 1; j < validGroups.length; j++) { + const b = validGroups[j]; + if (assignedKeys.has(b.key)) continue; + + // Check if b is within 5% volume of every group already in the cluster + const isNear = cluster.every(g => { + const maxVol = Math.max(g.volume, b.volume); + return Math.abs(g.volume - b.volume) / maxVol <= 0.05; + }); + + if (isNear) { + cluster.push(b); + assignedKeys.add(b.key); + } + } + + if (cluster.length >= 2) { + const volumes = cluster.map(g => g.volume); + const maxVol = Math.max(...volumes); + const minVol = Math.min(...volumes); + const maxDiffPct = ((maxVol - minVol) / maxVol) * 100; + clusters.push({ groups: cluster, volumes, maxDiffPct }); + } + } + + return clusters; + }, [groups]); + const toggleGroup = (key: string) => { const next = new Set(expandedGroups); if (next.has(key)) { @@ -186,6 +238,61 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat + {nearDuplicateClusters.length > 0 && ( +
+
+ + Possible data entry errors + + {nearDuplicateClusters.length} cluster{nearDuplicateClusters.length !== 1 ? 's' : ''} within 5% volume + +
+ {nearDuplicateClusters.map((cluster, ci) => ( +
+ + {expandedNearDuplicates.has(ci) && ( +
+ {cluster.groups.map((g, gi) => ( +
+ {g.key} cm +
+ {cluster.volumes[gi].toLocaleString()} cm³ + · + {g.rows.length} product{g.rows.length !== 1 ? 's' : ''}: + + {g.rows.slice(0, 5).map(r => r.row[COLUMNS.ARTICLE_NO]).join(', ')} + {g.rows.length > 5 && +{g.rows.length - 5} more} + +
+
+ ))} +
+ )} +
+ ))} +
+ )} +
{filteredGroups.map(group => (