2026-03-29 17:30:04 +02:00
import React , { useState , useMemo } from 'react' ;
import { ExcelRow , COLUMNS } from '../types' ;
2026-04-07 10:45:28 +02:00
import { AlertTriangle , CheckCircle2 , ChevronDown , ChevronRight , Edit2 , Package , Boxes , Scale , Loader2 , RefreshCw , Layers , Link2 } from 'lucide-react' ;
2026-03-29 17:30:04 +02:00
import { cn } from '../lib/utils' ;
2026-03-29 18:26:20 +02:00
import { ConfirmModal } from './ConfirmModal' ;
2026-03-29 17:30:04 +02:00
interface DimensionsViewProps {
data : ExcelRow [];
headers : string [];
onEdit : ( index : number ) => void ;
onSaveRow : ( index : number , updatedRow : ExcelRow ) => void ;
2026-03-29 17:35:07 +02:00
onCaptureState : ( message : string ) => void ;
2026-03-29 17:30:04 +02:00
}
interface DimensionGroup {
key : string ;
innerDims : string ;
rows : { row : ExcelRow ; index : number }[];
isInconsistent : boolean ;
2026-04-07 10:45:28 +02:00
volume : number ;
2026-03-29 17:30:04 +02:00
discrepancies : {
outer : boolean ;
units : boolean ;
moq : boolean ;
};
}
2026-04-07 10:45:28 +02:00
interface NearDuplicateCluster {
groups : DimensionGroup [];
volumes : number [];
maxDiffPct : number ;
}
2026-03-29 17:35:07 +02:00
export function DimensionsView ({ data , headers , onEdit , onSaveRow , onCaptureState } : DimensionsViewProps ) {
2026-03-29 17:30:04 +02:00
const [ expandedGroups , setExpandedGroups ] = useState < Set < string >>( new Set ());
2026-04-07 10:45:28 +02:00
const [ expandedNearDuplicates , setExpandedNearDuplicates ] = useState < Set < number >>( new Set ());
2026-03-29 17:30:04 +02:00
const [ showOnlyInconsistent , setShowOnlyInconsistent ] = useState ( true );
2026-03-29 17:43:57 +02:00
const [ syncing , setSyncing ] = useState < { key : string , field : string } | null > ( null );
2026-03-29 18:26:20 +02:00
const [ pendingAction , setPendingAction ] = useState < {
group : DimensionGroup ,
sourceRow : ExcelRow ,
fieldType : 'outer' | 'units' | 'moq' | 'all'
} | null > ( null );
2026-04-08 16:10:14 +02:00
const [ clusterSelections , setClusterSelections ] = useState < Record < number , Set < number >>>({});
const [ clusterSyncTargets , setClusterSyncTargets ] = useState < Record < number , string >>({});
const [ pendingNearDupSync , setPendingNearDupSync ] = useState < {
clusterIndex : number ;
targetGroupKey : string ;
selectedIndices : number [];
} | null > ( null );
2026-03-29 17:30:04 +02:00
const groups = useMemo (() => {
const groupMap = new Map < string , { row : ExcelRow ; index : number } [] >();
data . forEach (( row , index ) => {
const iw = String ( row [ COLUMNS . INNER_W ] || '0' ). trim ();
const il = String ( row [ COLUMNS . INNER_L ] || '0' ). trim ();
const ih = String ( row [ COLUMNS . INNER_H ] || '0' ). trim ();
2026-04-07 10:37:16 +02:00
// Normalize key by sorting dimension values so different orderings
// (e.g. "29x10x15" vs "10x29x15") are treated as the same group
const sortedDims = [ parseFloat ( il ) || 0 , parseFloat ( iw ) || 0 , parseFloat ( ih ) || 0 ]
. sort (( a , b ) => a - b );
const key = sortedDims . join ( 'x' );
2026-03-29 17:30:04 +02:00
if ( ! groupMap . has ( key )) {
groupMap . set ( key , []);
}
groupMap . get ( key ) ! . push ({ row , index });
});
const result : DimensionGroup [] = [];
groupMap . forEach (( rows , key ) => {
2026-03-29 17:43:57 +02:00
// Skip empty/placeholder groups (0x0x0 or empty fields)
if ( key === '0x0x0' || key === 'xx' || key === 'x x' ) return ;
2026-03-29 17:30:04 +02:00
const first = rows [ 0 ]. row ;
const firstOuter = ` ${ first [ COLUMNS . OUTER_L ] } x ${ first [ COLUMNS . OUTER_W ] } x ${ first [ COLUMNS . OUTER_H ] } ` ;
const firstUnits = String ( first [ COLUMNS . UNITS_OUTER ]);
const firstMOQ = String ( first [ COLUMNS . MOQ ]);
let outerMatch = true ;
let unitsMatch = true ;
let moqMatch = true ;
rows . forEach (({ row }) => {
const outer = ` ${ row [ COLUMNS . OUTER_L ] } x ${ row [ COLUMNS . OUTER_W ] } x ${ row [ COLUMNS . OUTER_H ] } ` ;
const units = String ( row [ COLUMNS . UNITS_OUTER ]);
const moq = String ( row [ COLUMNS . MOQ ]);
if ( outer !== firstOuter ) outerMatch = false ;
if ( units !== firstUnits ) unitsMatch = false ;
if ( moq !== firstMOQ ) moqMatch = false ;
});
2026-04-07 10:45:28 +02:00
const parts = key . split ( 'x' ). map ( Number );
const volume = parts [ 0 ] * parts [ 1 ] * parts [ 2 ];
2026-03-29 17:30:04 +02:00
result . push ({
key ,
innerDims : key ,
rows ,
2026-04-07 10:45:28 +02:00
volume ,
2026-03-29 17:30:04 +02:00
isInconsistent : ! outerMatch || ! unitsMatch || ! moqMatch ,
discrepancies : {
outer : ! outerMatch ,
units : ! unitsMatch ,
moq : ! moqMatch
}
});
});
return result . sort (( a , b ) => ( b . isInconsistent ? 1 : 0 ) - ( a . isInconsistent ? 1 : 0 ));
}, [ data ]);
const filteredGroups = useMemo (() => {
return showOnlyInconsistent ? groups . filter ( g => g . isInconsistent ) : groups ;
}, [ groups , showOnlyInconsistent ]);
2026-04-07 10:45:28 +02:00
const nearDuplicateClusters = useMemo (() : NearDuplicateCluster [] => {
2026-04-07 10:56:58 +02:00
// Two groups are "similar" if every sorted dimension pair differs by < 1 cm absolute
2026-04-07 10:51:10 +02:00
// (keys are already sorted ascending, e.g. "15x20x29")
2026-04-07 10:56:58 +02:00
const MAX_DIFF_CM = 1 ;
2026-04-07 10:51:10 +02:00
2026-04-07 10:56:58 +02:00
const maxAbsDiff = ( keyA : string , keyB : string ) : number => {
2026-04-07 10:51:10 +02:00
const a = keyA . split ( 'x' ). map ( Number );
const b = keyB . split ( 'x' ). map ( Number );
2026-04-07 10:56:58 +02:00
return Math . max (... a . map (( v , i ) => Math . abs ( v - b [ i ])));
2026-04-07 10:51:10 +02:00
};
2026-04-07 10:45:28 +02:00
const validGroups = groups . filter ( g => g . volume > 0 );
const assignedKeys = new Set < string >();
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 ;
2026-04-07 10:56:58 +02:00
// b must be within 1 cm on every axis of every group already in the cluster
const isSimilar = cluster . every ( g => maxAbsDiff ( g . key , b . key ) < MAX_DIFF_CM );
2026-04-07 10:51:10 +02:00
if ( isSimilar ) {
2026-04-07 10:45:28 +02:00
cluster . push ( b );
assignedKeys . add ( b . key );
}
}
if ( cluster . length >= 2 ) {
const volumes = cluster . map ( g => g . volume );
2026-04-07 10:56:58 +02:00
// Max absolute diff (cm) across all pairs in the cluster
2026-04-07 10:51:10 +02:00
let maxDiffPct = 0 ;
for ( let x = 0 ; x < cluster . length ; x ++ ) {
for ( let y = x + 1 ; y < cluster . length ; y ++ ) {
2026-04-07 10:56:58 +02:00
maxDiffPct = Math . max ( maxDiffPct , maxAbsDiff ( cluster [ x ]. key , cluster [ y ]. key ));
2026-04-07 10:51:10 +02:00
}
}
2026-04-07 10:45:28 +02:00
clusters . push ({ groups : cluster , volumes , maxDiffPct });
}
}
return clusters ;
}, [ groups ]);
2026-03-29 17:30:04 +02:00
const toggleGroup = ( key : string ) => {
const next = new Set ( expandedGroups );
if ( next . has ( key )) {
next . delete ( key );
} else {
next . add ( key );
}
setExpandedGroups ( next );
};
2026-03-29 17:43:57 +02:00
const handleSyncField = async ( group : DimensionGroup , sourceRow : ExcelRow , fieldType : 'outer' | 'units' | 'moq' ) => {
2026-03-29 18:26:20 +02:00
setPendingAction ({ group , sourceRow , fieldType });
};
const handleFullSync = async ( group : DimensionGroup , sourceRow : ExcelRow ) => {
setPendingAction ({ group , sourceRow , fieldType : 'all' });
};
2026-04-08 16:10:14 +02:00
const executeNearDupSync = async () => {
if ( ! pendingNearDupSync ) return ;
const { clusterIndex , targetGroupKey , selectedIndices } = pendingNearDupSync ;
const cluster = nearDuplicateClusters [ clusterIndex ];
const targetGroup = cluster . groups . find ( g => g . key === targetGroupKey );
if ( ! targetGroup ) return ;
const sourceRow = targetGroup . rows [ 0 ]. row ;
const innerL = sourceRow [ COLUMNS . INNER_L ];
const innerW = sourceRow [ COLUMNS . INNER_W ];
const innerH = sourceRow [ COLUMNS . INNER_H ];
onCaptureState ( `Synced inner dimensions to ${ targetGroupKey } cm for ${ selectedIndices . length } products` );
setPendingNearDupSync ( null );
for ( const idx of selectedIndices ) {
const row = data [ idx ];
const updatedRow = [... row ];
updatedRow [ COLUMNS . INNER_L ] = innerL ;
updatedRow [ COLUMNS . INNER_W ] = innerW ;
updatedRow [ COLUMNS . INNER_H ] = innerH ;
await onSaveRow ( idx , updatedRow );
}
setClusterSelections ( prev => {
const next = { ... prev };
delete next [ clusterIndex ];
return next ;
});
};
2026-03-29 18:26:20 +02:00
const executeSync = async () => {
if ( ! pendingAction ) return ;
const { group , sourceRow , fieldType } = pendingAction ;
const fieldLabel = fieldType === 'outer' ? 'Outer Box Dimensions' : fieldType === 'units' ? 'Units per Outer' : fieldType === 'moq' ? 'MOQ' : 'Full Packaging Data' ;
2026-03-29 17:30:04 +02:00
2026-03-29 17:43:57 +02:00
onCaptureState ( `Bulk synced ${ fieldLabel } in group ${ group . innerDims } ` );
setSyncing ({ key : group.key , field : fieldType });
2026-03-29 18:26:20 +02:00
setPendingAction ( null );
2026-03-29 17:43:57 +02:00
2026-03-29 17:30:04 +02:00
try {
2026-03-29 17:41:03 +02:00
const outerL = sourceRow [ COLUMNS . OUTER_L ];
const outerW = sourceRow [ COLUMNS . OUTER_W ];
const outerH = sourceRow [ COLUMNS . OUTER_H ];
const unitsOuter = sourceRow [ COLUMNS . UNITS_OUTER ];
const moq = sourceRow [ COLUMNS . MOQ ];
2026-03-29 17:30:04 +02:00
for ( const { row , index } of group . rows ) {
2026-03-29 17:41:03 +02:00
if ( row === sourceRow ) continue ;
2026-03-29 17:43:57 +02:00
const updatedRow = [... row ];
if ( fieldType === 'outer' ) {
updatedRow [ COLUMNS . OUTER_L ] = outerL ;
updatedRow [ COLUMNS . OUTER_W ] = outerW ;
updatedRow [ COLUMNS . OUTER_H ] = outerH ;
} else if ( fieldType === 'units' ) {
updatedRow [ COLUMNS . UNITS_OUTER ] = unitsOuter ;
} else if ( fieldType === 'moq' ) {
updatedRow [ COLUMNS . MOQ ] = moq ;
2026-03-29 18:26:20 +02:00
} else if ( fieldType === 'all' ) {
updatedRow [ COLUMNS . OUTER_L ] = outerL ;
updatedRow [ COLUMNS . OUTER_W ] = outerW ;
updatedRow [ COLUMNS . OUTER_H ] = outerH ;
updatedRow [ COLUMNS . UNITS_OUTER ] = unitsOuter ;
updatedRow [ COLUMNS . MOQ ] = moq ;
2026-03-29 17:43:57 +02:00
}
await onSaveRow ( index , updatedRow );
}
} finally {
setSyncing ( null );
}
};
2026-03-29 17:41:03 +02:00
2026-03-29 17:30:04 +02:00
return (
< div className = "space-y-6" >
< div className = "flex items-center justify-between bg-slate-800/50 p-4 rounded-lg border border-slate-700" >
< div >
< h2 className = "text-xl font-semibold text-white flex items-center gap-2" >
< Boxes className = "text-blue-400" />
Dimension Consistency Check
</ h2 >
< p className = "text-sm text-slate-400 mt-1" >
Grouping products by Inner Box dimensions to find Packaging or MOQ discrepancies .
</ p >
</ div >
< div className = "flex items-center gap-4" >
< label className = "flex items-center gap-2 text-sm text-slate-300 cursor-pointer" >
< input
type = "checkbox"
checked = { showOnlyInconsistent }
onChange = { e => setShowOnlyInconsistent ( e . target . checked )}
className = "rounded border-slate-600 bg-slate-700 text-blue-600 focus:ring-blue-500"
/>
Show only inconsistent groups
</ label >
< div className = "text-xs text-slate-500 bg-slate-900 px-3 py-1.5 rounded-full border border-slate-700" >
{ groups . filter ( g => g . isInconsistent ). length } Inconsistencies found
</ div >
</ div >
</ div >
2026-04-07 10:45:28 +02:00
{ nearDuplicateClusters . length > 0 && (
< div className = "space-y-2" >
< div className = "flex items-center gap-2 px-1" >
< Link2 className = "w-4 h-4 text-violet-400" />
< span className = "text-sm font-semibold text-violet-300" > Possible data entry errors </ span >
< span className = "text-xs text-slate-500 bg-slate-900 px-2 py-0.5 rounded-full border border-slate-700" >
2026-04-07 10:56:58 +02:00
{ nearDuplicateClusters . length } cluster { nearDuplicateClusters . length !== 1 ? 's' : '' } with dimensions differing & lt ; 1 cm per axis
2026-04-07 10:45:28 +02:00
</ span >
</ div >
{ nearDuplicateClusters . map (( cluster , ci ) => (
< div key = { ci } className = "border border-violet-500/25 bg-violet-500/5 rounded-lg overflow-hidden" >
< button
onClick = {() => {
const next = new Set ( expandedNearDuplicates );
next . has ( ci ) ? next . delete ( ci ) : next . add ( ci );
setExpandedNearDuplicates ( next );
}}
className = "w-full flex items-center gap-4 p-3 hover:bg-violet-500/10 transition-colors text-left"
>
{ expandedNearDuplicates . has ( ci ) ? < ChevronDown className = "w-4 h-4 text-slate-500 shrink-0" /> : < ChevronRight className = "w-4 h-4 text-slate-500 shrink-0" />}
< div className = "flex items-center gap-3 flex-wrap" >
{ cluster . groups . map (( g , gi ) => (
< span key = { g . key } className = "font-mono text-xs text-violet-300 bg-violet-400/10 px-2 py-0.5 rounded" >
{ g . key } cm
< span className = "text-slate-500 ml-1" >({ cluster . volumes [ gi ]. toLocaleString ()} cm ³ )</ span >
</ span >
))}
< span className = "text-xs text-violet-400/70" >
2026-04-07 10:56:58 +02:00
— max diff { cluster . maxDiffPct . toFixed ( 1 )} cm
2026-04-07 10:45:28 +02:00
</ span >
</ div >
</ button >
{ expandedNearDuplicates . has ( ci ) && (
< div className = "border-t border-violet-500/20 px-4 py-3 space-y-2" >
{ cluster . groups . map (( g , gi ) => (
< div key = { g . key } className = "flex items-start gap-4 text-xs" >
< span className = "font-mono text-violet-300 w-32 shrink-0 pt-0.5" >{ g . key } cm </ span >
< div >
< span className = "text-slate-400" >{ cluster . volumes [ gi ]. toLocaleString ()} cm ³ </ span >
< span className = "text-slate-600 mx-2" > · </ span >
< span className = "text-slate-500" >{ g . rows . length } product { g . rows . length !== 1 ? 's' : '' } : </ span >
< span className = "text-slate-400" >
{ g . rows . slice ( 0 , 5 ). map ( r => r . row [ COLUMNS . ARTICLE_NO ]). join ( ', ' )}
{ g . rows . length > 5 && < span className = "text-slate-600" > + { g . rows . length - 5 } more </ span >}
</ span >
</ div >
</ div >
))}
</ div >
)}
</ div >
))}
</ div >
)}
2026-03-29 17:30:04 +02:00
< div className = "space-y-3" >
{ filteredGroups . map ( group => (
< div key = { group . key } className = { cn (
"border rounded-lg overflow-hidden transition-all" ,
group . isInconsistent ? "border-amber-500/30 bg-amber-500/5" : "border-slate-700 bg-slate-800/30"
)}>
< div className = "flex items-center justify-between bg-slate-800/20 pr-4" >
< button
onClick = {() => toggleGroup ( group . key )}
className = "flex-1 flex items-center gap-4 p-4 hover:bg-slate-700/30 transition-colors text-left"
>
{ expandedGroups . has ( group . key ) ? < ChevronDown className = "w-5 h-5 text-slate-500" /> : < ChevronRight className = "w-5 h-5 text-slate-500" />}
< div >
< div className = "flex items-center gap-2" >
< span className = "font-mono text-sm text-blue-400 bg-blue-400/10 px-2 py-0.5 rounded" >
Inner : { group . innerDims } cm
</ span >
{ group . isInconsistent ? (
< span className = "flex items-center gap-1 text-xs font-medium text-amber-500 bg-amber-500/10 px-2 py-0.5 rounded ring-1 ring-amber-500/20" >
< AlertTriangle className = "w-3 h-3" />
Inconsistent
</ span >
) : (
< span className = "flex items-center gap-1 text-xs font-medium text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded ring-1 ring-emerald-500/20" >
< CheckCircle2 className = "w-3 h-3" />
Consistent
</ span >
)}
</ div >
< div className = "text-xs text-slate-500 mt-1" >
{ group . rows . length } product { group . rows . length !== 1 ? 's' : '' } in this dimension group
</ div >
</ div >
</ button >
2026-03-29 17:41:03 +02:00
< div className = "flex items-center gap-6" >
2026-03-29 17:30:04 +02:00
< div className = "flex items-center gap-4 text-[10px] hidden md:flex" >
{ group . discrepancies . outer && (
< span className = "text-amber-400 flex items-center gap-1" >
< Package className = "w-3 h-3" /> Outer Dims vary
</ span >
)}
{ group . discrepancies . units && (
< span className = "text-amber-400 flex items-center gap-1" >
< Boxes className = "w-3 h-3" /> Units / Outer vary
</ span >
)}
{ group . discrepancies . moq && (
< span className = "text-amber-400 flex items-center gap-1" >
< Scale className = "w-3 h-3" /> MOQ varies
</ span >
)}
</ div >
2026-03-29 17:41:03 +02:00
{ group . isInconsistent && ! expandedGroups . has ( group . key ) && (
< div className = "text-[10px] font-bold text-blue-400 bg-blue-400/5 px-2 py-1 rounded border border-blue-400/20" >
2026-03-29 17:43:57 +02:00
OPEN TO SYNC FIELDS
2026-03-29 17:41:03 +02:00
</ div >
2026-03-29 17:30:04 +02:00
)}
</ div >
</ div >
{ expandedGroups . has ( group . key ) && (
< div className = "border-t border-slate-700 overflow-x-auto" >
< table className = "w-full text-xs text-left" >
< thead className = "bg-slate-900/50 text-slate-400 uppercase tracking-tight font-semibold" >
< tr >
< th className = "px-4 py-3" > Article No / Name </ th >
< th className = "px-4 py-3" > Inner Box ( L / W / H )</ th >
< th className = { cn ( "px-4 py-3" , group . discrepancies . outer && "text-amber-500" )}> Outer Box ( L / W / H )</ th >
< th className = { cn ( "px-4 py-3" , group . discrepancies . units && "text-amber-500" )}> Units / Outer </ th >
< th className = { cn ( "px-4 py-3" , group . discrepancies . moq && "text-amber-500" )}> MOQ </ th >
< th className = "px-4 py-3 text-right" > Actions </ th >
</ tr >
</ thead >
< tbody className = "divide-y divide-slate-700/50" >
{ group . rows . map (({ row , index }) => (
< tr key = { index } className = "hover:bg-slate-700/20 group transition-colors" >
< td className = "px-4 py-3" >
< div className = "font-medium text-slate-200" >{ row [ COLUMNS . ARTICLE_NO ]}</ div >
< div className = "text-[10px] text-slate-500 truncate max-w-[200px]" >{ row [ COLUMNS . ARTICLE_NAME ]}</ div >
</ td >
< td className = "px-4 py-3 text-slate-400 font-mono" >
2026-03-29 17:43:57 +02:00
{ row [ COLUMNS . INNER_L ] || '-' } × { row [ COLUMNS . INNER_W ] || '-' } × { row [ COLUMNS . INNER_H ] || '-' }
2026-03-29 17:30:04 +02:00
</ td >
< td className = { cn (
2026-03-29 17:54:22 +02:00
"px-4 py-3 font-mono" ,
2026-03-29 17:30:04 +02:00
group . discrepancies . outer ? "text-amber-300" : "text-slate-400"
)}>
2026-03-29 17:54:22 +02:00
< div className = "flex items-center gap-2 group/cell" >
< button
onClick = {() => handleSyncField ( group , row , 'outer' )}
disabled = { syncing ? . key === group . key }
title = "Apply these Outer Dims to all in group"
className = "p-1 hover:bg-emerald-600/20 text-slate-600 hover:text-emerald-400 rounded opacity-0 group-hover:opacity-100 transition-opacity"
>
{ syncing ? . key === group . key && syncing ? . field === 'outer' ? < Loader2 className = "w-3 h-3 animate-spin" /> : < Layers className = "w-3 h-3" />}
</ button >
< span >{ row [ COLUMNS . OUTER_L ] || '-' } × { row [ COLUMNS . OUTER_W ] || '-' } × { row [ COLUMNS . OUTER_H ] || '-' }</ span >
</ div >
2026-03-29 17:30:04 +02:00
</ td >
< td className = { cn (
2026-03-29 17:54:22 +02:00
"px-4 py-3" ,
2026-03-29 17:30:04 +02:00
group . discrepancies . units ? "text-amber-300 font-bold" : "text-slate-400"
)}>
2026-03-29 17:54:22 +02:00
< div className = "flex items-center gap-2 group/cell" >
< button
onClick = {() => handleSyncField ( group , row , 'units' )}
disabled = { syncing ? . key === group . key }
title = "Apply this Units/Outer to all in group"
className = "p-1 hover:bg-emerald-600/20 text-slate-600 hover:text-emerald-400 rounded opacity-0 group-hover:opacity-100 transition-opacity"
>
{ syncing ? . key === group . key && syncing ? . field === 'units' ? < Loader2 className = "w-3 h-3 animate-spin" /> : < Layers className = "w-3 h-3" />}
</ button >
< span >{ row [ COLUMNS . UNITS_OUTER ] || '-' }</ span >
</ div >
2026-03-29 17:30:04 +02:00
</ td >
< td className = { cn (
2026-03-29 17:54:22 +02:00
"px-4 py-3" ,
2026-03-29 17:30:04 +02:00
group . discrepancies . moq ? "text-amber-300 font-bold" : "text-slate-400"
)}>
2026-03-29 17:54:22 +02:00
< div className = "flex items-center gap-2 group/cell" >
< button
onClick = {() => handleSyncField ( group , row , 'moq' )}
disabled = { syncing ? . key === group . key }
title = "Apply this MOQ to all in group"
className = "p-1 hover:bg-emerald-600/20 text-slate-600 hover:text-emerald-400 rounded opacity-0 group-hover:opacity-100 transition-opacity"
>
{ syncing ? . key === group . key && syncing ? . field === 'moq' ? < Loader2 className = "w-3 h-3 animate-spin" /> : < Layers className = "w-3 h-3" />}
</ button >
< span >{ row [ COLUMNS . MOQ ] || '-' }</ span >
</ div >
2026-03-29 17:30:04 +02:00
</ td >
< td className = "px-4 py-3 text-right" >
2026-03-29 17:41:03 +02:00
< div className = "flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity" >
< button
2026-03-29 17:43:57 +02:00
onClick = {() => handleFullSync ( group , row )}
title = "FULL SYNC: Apply ALL packaging measures labels to all in group"
disabled = { syncing ? . key === group . key }
className = "p-1.5 hover:bg-blue-600/20 text-slate-500 hover:text-blue-400 rounded transition-all"
2026-03-29 17:41:03 +02:00
>
2026-03-29 17:43:57 +02:00
{ syncing ? . key === group . key && syncing ? . field === 'all' ? < Loader2 className = "w-4 h-4 animate-spin" /> : < RefreshCw className = "w-4 h-4" />}
2026-03-29 17:41:03 +02:00
</ button >
< button
onClick = {() => onEdit ( index )}
title = "Edit product"
2026-03-29 17:43:57 +02:00
className = "p-1.5 hover:bg-slate-600/20 text-slate-500 hover:text-slate-300 rounded transition-all"
2026-03-29 17:41:03 +02:00
>
< Edit2 className = "w-4 h-4" />
</ button >
</ div >
2026-03-29 17:30:04 +02:00
</ td >
</ tr >
))}
</ tbody >
</ table >
</ div >
)}
</ div >
))}
{ filteredGroups . length === 0 && (
< div className = "flex flex-col items-center justify-center py-20 bg-slate-800/20 border border-dashed border-slate-700 rounded-xl" >
< CheckCircle2 className = "w-12 h-12 text-emerald-500/50 mb-3" />
< h3 className = "text-slate-300 font-medium" > Clear of discrepancies </ h3 >
< p className = "text-slate-500 text-sm mt-1" >
{ showOnlyInconsistent ? "No inconsistent groups found." : "No dimension data available." }
</ p >
</ div >
)}
</ div >
2026-03-29 18:26:20 +02:00
< ConfirmModal
isOpen = { !! pendingAction }
onConfirm = { executeSync }
onCancel = {() => setPendingAction ( null )}
title = "Sync Group Data"
message = { `Are you sure you want to sync ${ pendingAction ? . fieldType === 'all' ? 'ALL packaging data' : pendingAction ? . fieldType } for the whole group using product ${ pendingAction ? . sourceRow [ COLUMNS . ARTICLE_NO ] } as the template?` }
type = "warning"
confirmText = "Sync Group"
/>
2026-03-29 17:30:04 +02:00
</ div >
);
}