2026-03-29 17:30:04 +02:00
import React , { useState , useMemo } from 'react' ;
import { ExcelRow , COLUMNS } from '../types' ;
import { AlertTriangle , CheckCircle2 , ChevronDown , ChevronRight , Edit2 , Package , Boxes , Scale , Loader2 , Languages } from 'lucide-react' ;
import { cn } from '../lib/utils' ;
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 ;
discrepancies : {
outer : boolean ;
units : boolean ;
moq : boolean ;
};
}
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 ());
const [ showOnlyInconsistent , setShowOnlyInconsistent ] = useState ( true );
const [ syncing , setSyncing ] = useState < string | null >( null );
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 ();
const key = ` ${ il } x ${ iw } x ${ ih } ` ;
if ( ! groupMap . has ( key )) {
groupMap . set ( key , []);
}
groupMap . get ( key ) ! . push ({ row , index });
});
const result : DimensionGroup [] = [];
groupMap . forEach (( rows , key ) => {
if ( key === '0x0x0' || key === 'xx' ) return ; // Skip empty/placeholder groups
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 ;
});
result . push ({
key ,
innerDims : key ,
rows ,
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 ]);
const toggleGroup = ( key : string ) => {
const next = new Set ( expandedGroups );
if ( next . has ( key )) {
next . delete ( key );
} else {
next . add ( key );
}
setExpandedGroups ( next );
};
const syncGroup = async ( group : DimensionGroup ) => {
if ( ! window . confirm ( `This will apply the Packaging & MOQ values of product ${ group . rows [ 0 ]. row [ COLUMNS . ARTICLE_NO ] } to all ${ group . rows . length } products in this group. Continue?` )) {
return ;
}
2026-03-29 17:35:07 +02:00
onCaptureState ( `Synced ${ group . rows . length } products in ${ group . innerDims } group` );
2026-03-29 17:30:04 +02:00
setSyncing ( group . key );
try {
const first = group . rows [ 0 ]. row ;
const outerL = first [ COLUMNS . OUTER_L ];
const outerW = first [ COLUMNS . OUTER_W ];
const outerH = first [ COLUMNS . OUTER_H ];
const unitsOuter = first [ COLUMNS . UNITS_OUTER ];
const moq = first [ COLUMNS . MOQ ];
for ( const { row , index } of group . rows ) {
const updatedRow = [... row ];
updatedRow [ COLUMNS . OUTER_L ] = outerL ;
updatedRow [ COLUMNS . OUTER_W ] = outerW ;
updatedRow [ COLUMNS . OUTER_H ] = outerH ;
updatedRow [ COLUMNS . UNITS_OUTER ] = unitsOuter ;
updatedRow [ COLUMNS . MOQ ] = moq ;
await onSaveRow ( index , updatedRow );
}
} finally {
setSyncing ( null );
}
};
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 >
< 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 >
< div className = "flex items-center gap-4" >
< 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 >
{ group . isInconsistent && (
< button
onClick = {() => syncGroup ( group )}
disabled = { syncing === group . key }
className = "flex items-center gap-1.5 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-[10px] font-bold rounded-md transition-all shadow-lg shadow-blue-900/40 disabled:opacity-50"
>
{ syncing === group . key ? < Loader2 className = "w-3 h-3 animate-spin" /> : < Languages className = "w-3 h-3" />}
SYNC GROUP
</ button >
)}
</ 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" >
{ row [ COLUMNS . INNER_L ]} × { row [ COLUMNS . INNER_W ]} × { row [ COLUMNS . INNER_H ]}
</ td >
< td className = { cn (
"px-4 py-3 font-mono" ,
group . discrepancies . outer ? "text-amber-300" : "text-slate-400"
)}>
{ row [ COLUMNS . OUTER_L ]} × { row [ COLUMNS . OUTER_W ]} × { row [ COLUMNS . OUTER_H ]}
</ td >
< td className = { cn (
"px-4 py-3" ,
group . discrepancies . units ? "text-amber-300 font-bold" : "text-slate-400"
)}>
{ row [ COLUMNS . UNITS_OUTER ]}
</ td >
< td className = { cn (
"px-4 py-3" ,
group . discrepancies . moq ? "text-amber-300 font-bold" : "text-slate-400"
)}>
{ row [ COLUMNS . MOQ ]}
</ td >
< td className = "px-4 py-3 text-right" >
< button
onClick = {() => onEdit ( index )}
className = "p-1.5 hover:bg-blue-600/20 text-slate-500 hover:text-blue-400 rounded transition-all opacity-0 group-hover:opacity-100"
>
< Edit2 className = "w-4 h-4" />
</ button >
</ 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 >
</ div >
);
}