2026-05-15 10:51:01 +02:00
import React , { useState , useEffect , useMemo } from 'react' ;
import { History , RotateCcw , ChevronDown , ChevronRight , User , Calendar , Tag , Search , X , Edit2 , Maximize2 , Check , Loader2 , CloudUpload } from 'lucide-react' ;
2026-04-10 09:47:35 +02:00
import { getHistory , deleteHistoryEntry , HistoryEntry } from '../lib/supabase' ;
2026-04-22 16:52:59 +02:00
import { ExcelRow } from '../types' ;
import { useColumns } from '../contexts/ColumnsContext' ;
2026-04-09 09:24:17 +02:00
import { cn } from '../lib/utils' ;
2026-04-24 14:19:09 +02:00
import { usePersistentState } from '../contexts/FilterContext' ;
2026-05-15 10:51:01 +02:00
import { previewBusinessCentralSync , applyBusinessCentralSync , isPreviewTokenMismatchError } from '../services/businessCentral' ;
import { SyncStatusPill } from './SyncStatusPill' ;
2026-04-09 09:24:17 +02:00
interface HistoryViewProps {
headers : string [];
2026-04-10 09:42:04 +02:00
data : ExcelRow [];
2026-04-23 18:58:52 +02:00
onRevert : ( articleNo : string , oldData : ExcelRow , historyId? : string ) => void ;
2026-04-10 12:46:26 +02:00
onEdit ?: ( rowIndex : number ) => void ;
2026-04-09 09:24:17 +02:00
}
2026-05-15 13:45:44 +02:00
type HistorySyncStatus = 'bc_pending' | 'previewed' | 'preview_only' | 'syncing' | 'synced' | 'failed' ;
2026-05-15 10:51:01 +02:00
interface HistorySyncRecord {
selected : boolean ;
status : HistorySyncStatus ;
previewToken? : string ;
error? : string ;
2026-05-15 13:45:44 +02:00
warning? : string ;
2026-05-15 10:51:01 +02:00
}
type HistorySyncMap = Record < string , HistorySyncRecord >;
function readStoredHistorySyncMap () : HistorySyncMap {
try {
const raw = localStorage . getItem ( 'history-bcSync' );
if ( ! raw ) return {};
const parsed = JSON . parse ( raw ) as unknown ;
if ( ! parsed || typeof parsed !== 'object' || Array . isArray ( parsed )) return {};
return parsed as HistorySyncMap ;
} catch {
return {};
}
}
2026-04-23 12:37:04 +02:00
export function HistoryView ({ headers , data , onRevert , onEdit } : HistoryViewProps ) {
2026-04-22 16:52:59 +02:00
const COLUMNS = useColumns ();
2026-04-09 09:24:17 +02:00
const [ history , setHistory ] = useState < HistoryEntry [] >([]);
const [ loading , setLoading ] = useState ( true );
const [ expandedId , setExpandedId ] = useState < string | null >( null );
2026-04-24 16:09:49 +02:00
const [ search , setSearch ] = usePersistentState ( 'history-search' , '' );
2026-05-14 20:15:34 +02:00
const [ userFilter , setUserFilter ] = usePersistentState ( 'history-userFilter' , '' );
2026-05-15 13:45:44 +02:00
const [ statusFilter , setStatusFilter ] = usePersistentState < 'all' | 'bc_pending' | 'previewed' | 'preview_only' | 'synced' | 'failed' > ( 'history-statusFilter' , 'all' );
2026-05-15 10:51:01 +02:00
const [ bcHistorySync , setBcHistorySync ] = useState < HistorySyncMap >(() => readStoredHistorySyncMap ());
const [ bcSyncBusy , setBcSyncBusy ] = useState ( false );
2026-04-23 14:53:43 +02:00
const [ isFullscreen , setIsFullscreen ] = useState ( false );
2026-04-09 09:24:17 +02:00
useEffect (() => {
loadHistory ();
2026-04-23 18:47:32 +02:00
}, []);
useEffect (() => {
if ( data . length > 0 ) {
loadHistory ();
}
2026-04-23 18:43:18 +02:00
}, [ data ]);
2026-04-09 09:24:17 +02:00
2026-05-15 10:51:01 +02:00
useEffect (() => {
try {
localStorage . setItem ( 'history-bcSync' , JSON . stringify ( bcHistorySync ));
} catch {
// Ignore storage quota or serialization errors.
}
}, [ bcHistorySync ]);
2026-04-09 09:24:17 +02:00
const loadHistory = async () => {
setLoading ( true );
2026-04-23 18:51:07 +02:00
const historyData = await getHistory ();
setHistory ( historyData );
2026-04-09 09:24:17 +02:00
setLoading ( false );
};
2026-05-15 10:51:01 +02:00
useEffect (() => {
if ( history . length === 0 ) return ;
const validKeys = new Set ( history . map ( entry => String ( entry . id || ` ${ entry . product_id } - ${ entry . changed_at } ` )));
setBcHistorySync ( prev => {
let changed = false ;
const next : Record < string , HistorySyncRecord > = {};
for ( const [ key , value ] of Object . entries ( prev )) {
if ( ! validKeys . has ( key )) {
changed = true ;
continue ;
}
next [ key ] = value as HistorySyncRecord ;
}
return changed ? next : prev ;
});
}, [ history ]);
2026-04-10 09:47:35 +02:00
const handleRevert = ( entry : HistoryEntry ) => {
if ( window . confirm ( `Are you sure you want to revert changes for ${ entry . article_name } ?` )) {
const currentRow = data . find ( r => String ( r [ 0 ]) === entry . product_id );
if ( currentRow && JSON . stringify ( currentRow ) === JSON . stringify ( entry . old_data )) {
if ( ! window . confirm ( "Reverting will restore original data. No actual changes will be made. Continue?" )) {
return ;
}
}
onRevert ( entry . product_id , entry . old_data , entry . id );
}
};
2026-04-09 09:24:17 +02:00
const getChangedFields = ( oldData : ExcelRow , newData : ExcelRow ) => {
const changes : { header : string ; old : any ; new : any ; index : number }[] = [];
const maxLen = Math . max ( oldData . length , newData . length );
for ( let i = 0 ; i < maxLen ; i ++ ) {
if ( oldData [ i ] !== newData [ i ]) {
changes . push ({
header : headers [ i ] || `Col ${ i } ` ,
old : oldData [ i ],
new : newData [ i ],
index : i
});
}
}
return changes ;
};
const formatDate = ( dateStr : string ) => {
const date = new Date ( dateStr );
return new Intl . DateTimeFormat ( 'en-GB' , {
day : '2-digit' ,
month : '2-digit' ,
year : 'numeric' ,
hour : '2-digit' ,
minute : '2-digit' ,
second : '2-digit'
}). format ( date );
};
2026-05-14 20:15:34 +02:00
const uniqueUsers = Array . from ( new Set ( history . map ( e => e . changed_by ))). sort ();
2026-05-15 10:51:01 +02:00
const getEntryKey = ( entry : HistoryEntry ) => String ( entry . id || ` ${ entry . product_id } - ${ entry . changed_at } ` );
const getEntryStatus = ( entry : HistoryEntry ) : HistorySyncStatus => {
return bcHistorySync [ getEntryKey ( entry )] ? . status || 'bc_pending' ;
};
const getEntrySelected = ( entry : HistoryEntry ) : boolean => {
return bcHistorySync [ getEntryKey ( entry )] ? . selected || false ;
};
// history is asc (oldest first) so index+1 = chronological #
// display newest first by reversing for render only
const filteredHistory = [... history ]. reverse (). filter ( entry => {
2026-05-14 20:15:34 +02:00
if ( userFilter && entry . changed_by !== userFilter ) return false ;
2026-05-15 10:51:01 +02:00
const status = getEntryStatus ( entry );
if ( statusFilter !== 'all' && status !== statusFilter ) return false ;
2026-04-24 12:20:14 +02:00
const terms = search . toLowerCase (). split ( /\s+/ ). filter ( Boolean );
if ( terms . length === 0 ) return true ;
const searchableText = ` ${ entry . article_name } ${ entry . product_id } ${ entry . changed_by } ` . toLowerCase ();
return terms . every ( term => searchableText . includes ( term ));
});
2026-04-09 09:24:17 +02:00
2026-05-15 10:51:01 +02:00
const selectedEntries = useMemo (
() => history . filter ( entry => getEntrySelected ( entry )),
[ history , bcHistorySync ]
);
const statusCounts = useMemo (() => {
2026-05-15 13:45:44 +02:00
const counts = { bc_pending : 0 , previewed : 0 , preview_only : 0 , synced : 0 , failed : 0 };
2026-05-15 10:51:01 +02:00
history . forEach ( entry => {
const status = getEntryStatus ( entry );
if ( status in counts ) counts [ status as keyof typeof counts ] += 1 ;
});
return counts ;
}, [ history , bcHistorySync ]);
const updateEntry = ( entry : HistoryEntry , patch : Partial < HistorySyncRecord >) => {
const key = getEntryKey ( entry );
setBcHistorySync ( prev => ({
... prev ,
[ key ] : {
selected : prev [ key ] ? . selected ?? false ,
status : prev [ key ] ? . status ?? 'bc_pending' ,
previewToken : prev [ key ] ? . previewToken ,
error : prev [ key ] ? . error ,
... prev [ key ],
... patch ,
},
}));
};
const selectAllVisible = ( selected : boolean ) => {
setBcHistorySync ( prev => {
const next = { ... prev };
filteredHistory . forEach ( entry => {
const key = getEntryKey ( entry );
next [ key ] = {
selected ,
status : next [ key ] ? . status ?? 'bc_pending' ,
previewToken : next [ key ] ? . previewToken ,
error : next [ key ] ? . error ,
2026-05-15 13:45:44 +02:00
warning : next [ key ] ? . warning ,
2026-05-15 10:51:01 +02:00
};
});
return next ;
});
};
2026-05-15 13:45:44 +02:00
const discardSelected = () => {
if ( selectedEntries . length === 0 ) return ;
if ( ! window . confirm ( `Discard ${ selectedEntries . length } pending sync(s)? They will be reset to "Pending BC" and will not be synced to Business Central.` )) return ;
setBcHistorySync ( prev => {
const next = { ... prev };
selectedEntries . forEach ( entry => {
const key = getEntryKey ( entry );
next [ key ] = { selected : false , status : 'bc_pending' };
});
return next ;
});
};
2026-05-15 10:51:01 +02:00
const previewSelected = async () => {
if ( selectedEntries . length === 0 ) return ;
setBcSyncBusy ( true );
try {
for ( const entry of [... selectedEntries ]. sort (( a , b ) => {
const timeDelta = new Date ( a . changed_at ). getTime () - new Date ( b . changed_at ). getTime ();
if ( timeDelta !== 0 ) return timeDelta ;
return String ( a . id || '' ). localeCompare ( String ( b . id || '' ));
})) {
const preview = await previewBusinessCentralSync ( headers , entry . new_data );
if ( ! preview . success ) {
2026-05-15 13:45:44 +02:00
updateEntry ( entry , { status : 'failed' , error : preview.error || 'Preview failed' , warning : undefined , selected : true });
2026-05-15 10:51:01 +02:00
continue ;
}
updateEntry ( entry , {
status : 'previewed' ,
previewToken : preview.previewToken ,
error : undefined ,
2026-05-15 13:45:44 +02:00
warning : undefined ,
2026-05-15 10:51:01 +02:00
selected : true ,
});
}
} finally {
setBcSyncBusy ( false );
}
};
const syncSelected = async () => {
if ( selectedEntries . length === 0 ) return ;
setBcSyncBusy ( true );
try {
const orderedEntries = [... selectedEntries ]. sort (( a , b ) => {
const timeDelta = new Date ( a . changed_at ). getTime () - new Date ( b . changed_at ). getTime ();
if ( timeDelta !== 0 ) return timeDelta ;
return String ( a . id || '' ). localeCompare ( String ( b . id || '' ));
});
for ( const entry of orderedEntries ) {
updateEntry ( entry , { status : 'syncing' , error : undefined , selected : true });
const key = getEntryKey ( entry );
const currentRecord = bcHistorySync [ key ];
let previewToken = currentRecord ? . previewToken ;
if ( ! previewToken ) {
const preview = await previewBusinessCentralSync ( headers , entry . new_data );
if ( ! preview . success ) {
2026-05-15 13:45:44 +02:00
updateEntry ( entry , { status : 'failed' , error : preview.error || 'Preview failed' , warning : undefined , selected : true });
2026-05-15 10:51:01 +02:00
continue ;
}
previewToken = preview . previewToken ;
2026-05-15 13:45:44 +02:00
updateEntry ( entry , { status : 'previewed' , previewToken , error : undefined , warning : undefined , selected : true });
2026-05-15 10:51:01 +02:00
}
const apply = await applyBusinessCentralSync ( headers , entry . new_data , previewToken );
if ( ! apply . success ) {
if ( isPreviewTokenMismatchError ( apply . error )) {
const refreshedPreview = await previewBusinessCentralSync ( headers , entry . new_data );
if ( refreshedPreview . success ) {
updateEntry ( entry , {
status : 'previewed' ,
previewToken : refreshedPreview.previewToken ,
error : undefined ,
2026-05-15 13:45:44 +02:00
warning : undefined ,
2026-05-15 10:51:01 +02:00
selected : true ,
});
const retryApply = await applyBusinessCentralSync ( headers , entry . new_data , refreshedPreview . previewToken );
if ( retryApply . success ) {
updateEntry ( entry , {
status : 'synced' ,
error : undefined ,
2026-05-15 13:45:44 +02:00
warning : retryApply.warning ,
2026-05-15 10:51:01 +02:00
selected : false ,
previewToken : retryApply.previewToken || refreshedPreview . previewToken ,
});
continue ;
}
2026-05-15 13:45:44 +02:00
updateEntry ( entry , { status : 'failed' , error : retryApply.error || 'BC sync failed' , warning : retryApply.warning , selected : true });
2026-05-15 10:51:01 +02:00
continue ;
}
}
2026-05-15 13:45:44 +02:00
updateEntry ( entry , { status : 'failed' , error : apply.error || 'BC sync failed' , warning : apply.warning , selected : true });
2026-05-15 10:51:01 +02:00
continue ;
}
updateEntry ( entry , {
status : 'synced' ,
error : undefined ,
2026-05-15 13:45:44 +02:00
warning : apply.warning ,
2026-05-15 10:51:01 +02:00
selected : false ,
previewToken : apply.previewToken || previewToken ,
});
}
} finally {
setBcSyncBusy ( false );
}
};
2026-04-09 09:24:17 +02:00
if ( loading ) {
return (
< div className = "flex flex-col items-center justify-center h-full text-slate-400" >
< div className = "animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4" ></ div >
< p className = "text-lg" > Fetching history ...</ p >
</ div >
);
}
return (
2026-04-23 14:53:43 +02:00
< div className = { isFullscreen ? "fixed inset-0 z-40 bg-[#041021] p-6 overflow-auto" : "space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500" }>
{ isFullscreen && (
< button
onClick = {() => setIsFullscreen ( false )}
className = "fixed top-4 right-4 z-50 p-2 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-md transition-colors border border-slate-700"
>
< X className = "w-5 h-5" />
</ button >
)}
2026-04-09 09:24:17 +02:00
< div className = "flex items-center justify-between" >
< div >
< h1 className = "text-2xl font-bold text-white flex items-center gap-3" >
< History className = "w-8 h-8 text-blue-500" />
Change History
</ h1 >
2026-05-14 18:33:07 +02:00
< p className = "text-slate-400 mt-1" >
{ history . length > 0 ? (
<>< span className = "text-white font-semibold" >{ history . length }</ span > total records { filteredHistory . length !== history . length && <> & mdash ; showing < span className = "text-white font-semibold" >{ filteredHistory . length }</ span ></>}</>
) : 'Review and revert any changes made to products.' }
</ p >
2026-04-09 09:24:17 +02:00
</ div >
< div className = "flex items-center gap-4" >
2026-05-14 20:15:34 +02:00
< div className = "relative" >
< User className = "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" />
< select
value = { userFilter }
onChange = { e => setUserFilter ( e . target . value )}
className = { cn (
"pl-9 pr-8 py-2 bg-slate-800 border rounded-md text-sm focus:outline-none focus:border-blue-500 transition-colors w-52 appearance-none" ,
userFilter ? "border-blue-500 text-white" : "border-slate-700 text-slate-400"
)}
>
< option value = "" > All users </ option >
{ uniqueUsers . map ( u => (
< option key = { u } value = { u }>{ u }</ option >
))}
</ select >
{ userFilter && (
< button
onClick = {() => setUserFilter ( '' )}
className = "absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white"
>
< X className = "w-3.5 h-3.5" />
</ button >
)}
</ div >
2026-04-09 09:24:17 +02:00
< div className = "relative" >
< Tag className = "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
2026-05-14 20:15:34 +02:00
< input
2026-04-09 09:24:17 +02:00
type = "text"
placeholder = "Search history..."
value = { search }
onChange = {( e ) => setSearch ( e . target . value )}
2026-04-10 12:39:31 +02:00
className = "pl-10 pr-10 py-2 bg-slate-800 border border-slate-700 rounded-md text-sm text-slate-200 focus:outline-none focus:border-blue-500 transition-colors w-64"
2026-04-09 09:24:17 +02:00
/>
2026-04-10 12:39:31 +02:00
{ search && (
< button
onClick = {() => setSearch ( '' )}
className = "absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white"
>
< X className = "w-4 h-4" />
</ button >
)}
2026-04-09 09:24:17 +02:00
</ div >
< button
onClick = { loadHistory }
className = "px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-md transition-colors flex items-center gap-2 border border-slate-700"
>
< RotateCcw className = "w-4 h-4" />
Refresh
</ button >
2026-04-23 14:53:43 +02:00
< button
onClick = {() => setIsFullscreen ( ! isFullscreen )}
className = "p-2 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-md transition-colors border border-slate-700"
title = { isFullscreen ? "Exit fullscreen" : "Enter fullscreen" }
>
< Maximize2 className = "w-5 h-5" />
</ button >
2026-04-09 09:24:17 +02:00
</ div >
</ div >
2026-05-15 10:51:01 +02:00
< div className = "bg-slate-900/70 border border-slate-800 rounded-xl p-4 flex flex-wrap items-center gap-3" >
< div className = "flex items-center gap-2 text-xs text-slate-400" >
< span className = "font-semibold text-slate-200" >{ selectedEntries . length }</ span > selected
< span className = "text-slate-600" > · </ span >
< span >{ statusCounts . bc_pending } pending </ span >
< span >{ statusCounts . previewed } previewed </ span >
2026-05-15 13:45:44 +02:00
< span >{ statusCounts . preview_only } preview only </ span >
2026-05-15 10:51:01 +02:00
< span >{ statusCounts . synced } synced </ span >
< span >{ statusCounts . failed } failed </ span >
</ div >
< div className = "ml-auto flex items-center gap-2 flex-wrap" >
< select
value = { statusFilter }
onChange = { e => setStatusFilter ( e . target . value as any )}
className = "bg-slate-800 border border-slate-700 rounded-md px-3 py-2 text-xs text-slate-200 focus:outline-none focus:border-blue-500"
>
< option value = "all" > All statuses </ option >
< option value = "bc_pending" > Pending BC </ option >
< option value = "previewed" > Previewed </ option >
2026-05-15 13:45:44 +02:00
< option value = "preview_only" > Preview only </ option >
2026-05-15 10:51:01 +02:00
< option value = "synced" > Synced BC </ option >
< option value = "failed" > Failed </ option >
</ select >
< button
onClick = {() => selectAllVisible ( true )}
className = "px-3 py-2 rounded-md bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs border border-slate-700"
>
Select all visible
</ button >
< button
onClick = {() => selectAllVisible ( false )}
className = "px-3 py-2 rounded-md bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs border border-slate-700"
>
Clear selection
</ button >
2026-05-15 13:45:44 +02:00
< button
onClick = { discardSelected }
disabled = { selectedEntries . length === 0 }
className = { cn (
"px-3 py-2 rounded-md text-xs border transition-colors" ,
selectedEntries . length === 0
? "bg-slate-700 text-slate-500 border-slate-600 cursor-not-allowed"
: "bg-slate-800 hover:bg-red-900/40 text-red-400 border-red-500/30 hover:border-red-500/60"
)}
>
Discard selected
</ button >
2026-05-15 10:51:01 +02:00
< button
onClick = { previewSelected }
disabled = { bcSyncBusy || selectedEntries . length === 0 }
className = { cn (
"inline-flex items-center gap-2 px-4 py-2 rounded-md text-xs font-semibold border transition-colors" ,
bcSyncBusy || selectedEntries . length === 0
? "bg-slate-700 text-slate-400 border-slate-600 cursor-not-allowed"
: "bg-indigo-600 hover:bg-indigo-500 text-white border-indigo-500/30"
)}
>
{ bcSyncBusy ? < Loader2 className = "w-3.5 h-3.5 animate-spin" /> : < Search className = "w-3.5 h-3.5" />}
Preview selected
</ button >
< button
onClick = { syncSelected }
disabled = { bcSyncBusy || selectedEntries . length === 0 }
className = { cn (
"inline-flex items-center gap-2 px-4 py-2 rounded-md text-xs font-semibold border transition-colors" ,
bcSyncBusy || selectedEntries . length === 0
? "bg-slate-700 text-slate-400 border-slate-600 cursor-not-allowed"
: "bg-emerald-600 hover:bg-emerald-500 text-white border-emerald-500/30"
)}
>
{ bcSyncBusy ? < Loader2 className = "w-3.5 h-3.5 animate-spin" /> : < CloudUpload className = "w-3.5 h-3.5" />}
Sync selected to BC
</ button >
</ div >
</ div >
2026-04-09 09:24:17 +02:00
< div className = "bg-[#0a1628] border border-slate-800 rounded-xl overflow-hidden shadow-2xl" >
{ filteredHistory . length === 0 ? (
< div className = "p-12 text-center" >
< History className = "w-12 h-12 text-slate-700 mx-auto mb-4" />
< p className = "text-slate-500 text-lg" > No history records found .</ p >
< p className = "text-slate-600 text-sm mt-1" >
{ search ? "Try adjusting your search filters." : "Changes are recorded when you 'Save All' pending modifications." }
</ p >
</ div >
) : (
< div className = "divide-y divide-slate-800" >
2026-05-14 18:33:07 +02:00
{ filteredHistory . map (( entry , filteredIdx ) => {
2026-04-09 09:24:17 +02:00
const isExpanded = expandedId === entry . id ;
const changes = getChangedFields ( entry . old_data , entry . new_data );
2026-05-15 10:51:01 +02:00
const globalNumber = history . indexOf ( entry ) + 1 ;
const status = getEntryStatus ( entry );
const selected = getEntrySelected ( entry );
2026-05-14 18:33:07 +02:00
2026-04-09 09:24:17 +02:00
return (
< div key = { entry . id } className = { cn (
"transition-colors" ,
isExpanded ? "bg-blue-600/5" : "hover:bg-slate-800/30"
)}>
{ /* Summary Row */ }
2026-05-14 18:33:07 +02:00
< div
2026-04-09 09:24:17 +02:00
className = "p-4 flex items-center gap-4 cursor-pointer"
onClick = {() => setExpandedId ( isExpanded ? null : entry . id )}
>
2026-05-15 10:51:01 +02:00
< input
type = "checkbox"
checked = { selected }
onClick = { e => e . stopPropagation ()}
onChange = {() => updateEntry ( entry , { selected : ! selected })}
className = "accent-blue-500"
/>
2026-04-09 09:24:17 +02:00
{ isExpanded ? < ChevronDown className = "w-5 h-5 text-slate-500" /> : < ChevronRight className = "w-5 h-5 text-slate-500" />}
2026-05-14 18:33:07 +02:00
< div className = "w-8 text-right shrink-0" >
< span className = "text-xs font-mono text-slate-500" > # { globalNumber }</ span >
</ div >
2026-04-09 09:24:17 +02:00
< div className = "flex-1 grid grid-cols-4 gap-4 items-center" >
< div className = "flex items-center gap-3" >
< div className = "w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center text-blue-500 font-bold shrink-0" >
{ entry . product_id . slice ( 0 , 2 ). toUpperCase ()}
</ div >
< div className = "min-w-0" >
< div className = "font-semibold text-slate-200 truncate" >{ entry . article_name }</ div >
< div className = "text-xs text-slate-500 font-mono" > ID : { entry . product_id }</ div >
</ div >
</ div >
< div className = "flex items-center gap-2 text-slate-400" >
< User className = "w-4 h-4" />
< span className = "text-sm truncate" >{ entry . changed_by }</ span >
</ div >
< div className = "flex items-center gap-2 text-slate-400" >
< Calendar className = "w-4 h-4" />
< span className = "text-sm" >{ formatDate ( entry . changed_at )}</ span >
</ div >
2026-05-15 10:51:01 +02:00
< div className = "flex flex-col items-end gap-2 text-sm" >
< div className = "flex items-center justify-end gap-3 flex-wrap" >
< SyncStatusPill status = { status } />
< span className = "px-2.5 py-1 rounded-full bg-blue-500/10 text-blue-400 font-medium" >
{ changes . length } { changes . length === 1 ? 'change' : 'changes' }
</ span >
< button
onClick = {( e ) => {
e . stopPropagation ();
if ( onEdit ) {
const idx = data . findIndex ( r => String ( r [ COLUMNS . ARTICLE_NO ]) === entry . product_id );
if ( idx !== - 1 ) onEdit ( idx );
}
}}
className = "flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-colors border border-blue-500/20"
>
< Edit2 className = "w-4 h-4" />
Edit
</ button >
< button
onClick = {( e ) => {
e . stopPropagation ();
handleRevert ( entry );
}}
className = "flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-orange-500/10 text-orange-400 hover:bg-orange-500/20 transition-colors border border-orange-500/20"
>
< RotateCcw className = "w-4 h-4" />
Revert
</ button >
</ div >
{ status === 'failed' && bcHistorySync [ getEntryKey ( entry )] ? . error && (
< div className = "max-w-[40rem] rounded-md border border-red-500/20 bg-red-500/10 px-3 py-2 text-xs text-red-200" >
< span className = "font-semibold" > BC error : </ span > { bcHistorySync [ getEntryKey ( entry )]. error }
</ div >
)}
2026-04-09 09:24:17 +02:00
</ div >
</ div >
</ div >
{ /* Details View */ }
{ isExpanded && (
< div className = "px-14 pb-6 pt-2 animate-in slide-in-from-top-2 duration-300" >
< div className = "bg-[#040d1a] border border-slate-700/50 rounded-lg overflow-hidden" >
< table className = "w-full text-sm" >
< thead >
< tr className = "bg-slate-800/50 text-slate-400 text-left" >
2026-05-14 18:33:07 +02:00
< th className = "px-4 py-2 font-medium w-8" > # </ th >
< th className = "px-4 py-2 font-medium" > Column </ th >
2026-04-09 09:24:17 +02:00
< th className = "px-4 py-2 font-medium" > Original Value </ th >
< th className = "px-4 py-2 font-medium" > New Value </ th >
</ tr >
</ thead >
< tbody className = "divide-y divide-slate-800" >
{ changes . map (( change , idx ) => (
< tr key = { idx } className = "hover:bg-slate-700/20" >
2026-05-14 18:33:07 +02:00
< td className = "px-4 py-2 text-slate-600 text-xs font-mono" >{ idx + 1 }</ td >
2026-04-09 09:24:17 +02:00
< td className = "px-4 py-2 text-slate-300 font-medium whitespace-nowrap" >
{ change . header }
</ td >
< td className = "px-4 py-2" >
< span className = "text-red-400/80 line-through decoration-red-500/50" >
{ String ( change . old ?? '-' )}
</ span >
</ td >
< td className = "px-4 py-2" >
< span className = "text-emerald-400 font-medium" >
{ String ( change . new ?? '-' )}
</ span >
</ td >
</ tr >
))}
</ tbody >
</ table >
</ div >
< div className = "mt-4 flex items-center justify-between px-2" >
< div className = "text-xs text-slate-500 flex items-center gap-1" >
< Tag className = "w-3 h-3" />
Row Index Reference : { entry . product_id }
</ div >
< p className = "text-xs text-slate-500 italic" >
Reverting will move this record to 'Pending Validation' for final approval before re - syncing .
</ p >
</ div >
</ div >
)}
</ div >
);
})}
</ div >
)}
</ div >
</ div >
);
}