feat(topbar): add Discard button to BC sync queue dropdown

This commit is contained in:
Christian Vidal Wolf
2026-05-15 13:52:47 +02:00
parent 6d664c26f1
commit 1d105e19ae
2 changed files with 63 additions and 6 deletions
+38 -3
View File
@@ -52,7 +52,7 @@ const FORCED_ZERO_STOCK_SKUS = new Set([
const BC_SYNC_QUEUE_STORAGE_KEY = 'craze_bc_sync_queue'; const BC_SYNC_QUEUE_STORAGE_KEY = 'craze_bc_sync_queue';
type BcSyncStatus = 'queued' | 'previewed' | 'syncing' | 'synced' | 'failed'; type BcSyncStatus = 'queued' | 'previewed' | 'preview_only' | 'syncing' | 'synced' | 'failed';
interface BcSyncQueueEntry { interface BcSyncQueueEntry {
rowIndex: number; rowIndex: number;
@@ -63,6 +63,7 @@ interface BcSyncQueueEntry {
status: BcSyncStatus; status: BcSyncStatus;
previewToken?: string; previewToken?: string;
error?: string; error?: string;
warning?: string;
source: 'save_all' | 'cosmetic_items' | 'manual'; source: 'save_all' | 'cosmetic_items' | 'manual';
} }
@@ -692,6 +693,19 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
}); });
}; };
const handleDiscardSelectedBcQueue = () => {
const selectedCount = Object.values(bcSyncQueue).filter(e => e.selected).length;
if (selectedCount === 0) return;
if (!window.confirm(`Discard ${selectedCount} pending BC sync(s)? They will be removed from the queue.`)) return;
setBcSyncQueue(prev => {
const next: BcSyncQueue = { ...prev };
(Object.keys(next) as string[]).forEach(articleNo => {
if (next[articleNo].selected) delete next[articleNo];
});
return next;
});
};
const handleSelectAllBcQueue = (selected: boolean) => { const handleSelectAllBcQueue = (selected: boolean) => {
setBcSyncQueue(prev => { setBcSyncQueue(prev => {
const next: BcSyncQueue = { ...prev }; const next: BcSyncQueue = { ...prev };
@@ -749,6 +763,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
...item, ...item,
status: 'failed', status: 'failed',
error: preview.error || 'Preview failed', error: preview.error || 'Preview failed',
warning: undefined,
}; };
} }
return; return;
@@ -759,6 +774,8 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
status: 'previewed', status: 'previewed',
previewToken: preview.previewToken, previewToken: preview.previewToken,
error: undefined, error: undefined,
selected: item.selected,
warning: undefined,
}; };
}); });
return next; return next;
@@ -785,7 +802,21 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
}); });
for (const [articleNo, item] of selectedBcQueueEntries) { for (const [articleNo, item] of selectedBcQueueEntries) {
const result = await applyBusinessCentralSync(appState.headers, item.newData, item.previewToken); const preview = await previewBusinessCentralSync(appState.headers, item.newData);
if (!preview.success) {
setBcSyncQueue(prev => ({
...prev,
[articleNo]: {
...prev[articleNo],
status: 'failed',
error: preview.error || 'Preview failed',
warning: undefined,
},
}));
continue;
}
const result = await applyBusinessCentralSync(appState.headers, item.newData, preview.previewToken);
if (!result.success) { if (!result.success) {
if (isPreviewTokenMismatchError(result.error)) { if (isPreviewTokenMismatchError(result.error)) {
const refreshedPreview = await previewBusinessCentralSync(appState.headers, item.newData); const refreshedPreview = await previewBusinessCentralSync(appState.headers, item.newData);
@@ -797,6 +828,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
status: 'previewed', status: 'previewed',
previewToken: refreshedPreview.previewToken, previewToken: refreshedPreview.previewToken,
error: undefined, error: undefined,
warning: undefined,
}, },
})); }));
@@ -817,6 +849,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
...prev[articleNo], ...prev[articleNo],
status: 'failed', status: 'failed',
error: retryResult.error || 'BC sync failed', error: retryResult.error || 'BC sync failed',
warning: retryResult.warning,
}, },
})); }));
continue; continue;
@@ -828,6 +861,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
...prev[articleNo], ...prev[articleNo],
status: 'failed', status: 'failed',
error: result.error || 'BC sync failed', error: result.error || 'BC sync failed',
warning: result.warning,
}, },
})); }));
continue; continue;
@@ -959,11 +993,12 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
onRevertRow={handleRevertRow} onRevertRow={handleRevertRow}
isSavingAll={isSavingAll} isSavingAll={isSavingAll}
bcQueueCount={Object.keys(bcSyncQueue).length} bcQueueCount={Object.keys(bcSyncQueue).length}
bcQueueEntries={Object.fromEntries((Object.entries(bcSyncQueue) as Array<[string, BcSyncQueueEntry]>).map(([k, v]) => [k, { articleName: v.articleName, selected: v.selected, status: v.status, error: v.error }]))} bcQueueEntries={Object.fromEntries((Object.entries(bcSyncQueue) as Array<[string, BcSyncQueueEntry]>).map(([k, v]) => [k, { articleName: v.articleName, selected: v.selected, status: v.status, error: v.error, warning: v.warning }]))}
onToggleBcQueueSelection={handleToggleBcQueueSelection} onToggleBcQueueSelection={handleToggleBcQueueSelection}
onSelectAllBcQueue={handleSelectAllBcQueue} onSelectAllBcQueue={handleSelectAllBcQueue}
onPreviewSelectedBcSync={handlePreviewSelectedBcSync} onPreviewSelectedBcSync={handlePreviewSelectedBcSync}
onSyncSelectedBcSync={handleSyncSelectedBcSync} onSyncSelectedBcSync={handleSyncSelectedBcSync}
onDiscardSelectedBcQueue={handleDiscardSelectedBcQueue}
isSyncingBC={isSyncingBC} isSyncingBC={isSyncingBC}
isDownloadingBCExcel={isDownloadingBCExcel} isDownloadingBCExcel={isDownloadingBCExcel}
isMaximized={isMaximized} isMaximized={isMaximized}
+25 -3
View File
@@ -22,24 +22,36 @@ interface TopBarProps {
onRevertRow: (articleNo: string) => void; onRevertRow: (articleNo: string) => void;
isSavingAll: boolean; isSavingAll: boolean;
bcQueueCount: number; bcQueueCount: number;
bcQueueEntries: Record<string, { articleName: string; selected: boolean; status: string; error?: string }>; bcQueueEntries: Record<string, { articleName: string; selected: boolean; status: string; error?: string; warning?: string }>;
onToggleBcQueueSelection: (articleNo: string) => void; onToggleBcQueueSelection: (articleNo: string) => void;
onSelectAllBcQueue: (selected: boolean) => void; onSelectAllBcQueue: (selected: boolean) => void;
onPreviewSelectedBcSync: () => Promise<void>; onPreviewSelectedBcSync: () => Promise<void>;
onSyncSelectedBcSync: () => Promise<void>; onSyncSelectedBcSync: () => Promise<void>;
onDiscardSelectedBcQueue: () => void;
isSyncingBC: boolean; isSyncingBC: boolean;
isDownloadingBCExcel: boolean; isDownloadingBCExcel: boolean;
isMaximized: boolean; isMaximized: boolean;
onToggleMaximize: () => void; onToggleMaximize: () => void;
} }
export function TopBar({ stats, activeModule, onExport, onDownloadBCExcel, onRefresh, hasData, hasUnsavedChanges, userEmail, onSignOut, canUndo, onUndo, undoMessage, undoSteps, pendingCount, pendingChanges, onSaveAll, onRevertRow, isSavingAll, bcQueueCount, bcQueueEntries, onToggleBcQueueSelection, onSelectAllBcQueue, onPreviewSelectedBcSync, onSyncSelectedBcSync, isSyncingBC, isDownloadingBCExcel, isMaximized, onToggleMaximize }: TopBarProps) { export function TopBar({ stats, activeModule, onExport, onDownloadBCExcel, onRefresh, hasData, hasUnsavedChanges, userEmail, onSignOut, canUndo, onUndo, undoMessage, undoSteps, pendingCount, pendingChanges, onSaveAll, onRevertRow, isSavingAll, bcQueueCount, bcQueueEntries, onToggleBcQueueSelection, onSelectAllBcQueue, onPreviewSelectedBcSync, onSyncSelectedBcSync, onDiscardSelectedBcQueue, isSyncingBC, isDownloadingBCExcel, isMaximized, onToggleMaximize }: TopBarProps) {
const [showPending, setShowPending] = useState(false); const [showPending, setShowPending] = useState(false);
const [showBcQueue, setShowBcQueue] = useState(false); const [showBcQueue, setShowBcQueue] = useState(false);
const pendingDropdownRef = useRef<HTMLDivElement>(null); const pendingDropdownRef = useRef<HTMLDivElement>(null);
const bcQueueDropdownRef = useRef<HTMLDivElement>(null); const bcQueueDropdownRef = useRef<HTMLDivElement>(null);
const selectedBcQueueCount = Object.values(bcQueueEntries).filter(entry => entry.selected).length; const selectedBcQueueCount = Object.values(bcQueueEntries).filter(entry => entry.selected).length;
const formatQueueStatus = (status: string) => {
const normalized = String(status || '').toLowerCase();
if (normalized === 'preview_only') return 'Preview only';
if (normalized === 'previewed') return 'Previewed';
if (normalized === 'syncing') return 'Syncing';
if (normalized === 'synced') return 'Synced';
if (normalized === 'failed') return 'Failed';
if (normalized === 'queued') return 'Queued';
return String(status || '');
};
useEffect(() => { useEffect(() => {
if (!showPending) return; if (!showPending) return;
const handler = (e: MouseEvent) => { const handler = (e: MouseEvent) => {
@@ -238,6 +250,13 @@ export function TopBar({ stats, activeModule, onExport, onDownloadBCExcel, onRef
> >
None None
</button> </button>
<button
onClick={onDiscardSelectedBcQueue}
disabled={isSyncingBC || selectedBcQueueCount === 0}
className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-red-900/50 text-red-400 border border-red-500/30 disabled:opacity-40 disabled:cursor-not-allowed"
>
Discard {selectedBcQueueCount > 0 ? `(${selectedBcQueueCount})` : ''}
</button>
<button <button
onClick={onPreviewSelectedBcSync} onClick={onPreviewSelectedBcSync}
disabled={isSyncingBC} disabled={isSyncingBC}
@@ -262,7 +281,10 @@ export function TopBar({ stats, activeModule, onExport, onDownloadBCExcel, onRef
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-xs font-mono text-slate-400">{articleNo}</p> <p className="text-xs font-mono text-slate-400">{articleNo}</p>
<p className="text-sm text-white truncate">{entry.articleName}</p> <p className="text-sm text-white truncate">{entry.articleName}</p>
<p className="text-[10px] text-slate-500 uppercase">{entry.status}{entry.error ? ` · ${entry.error}` : ''}</p> <p className="text-[10px] text-slate-500 uppercase">
{formatQueueStatus(entry.status)}
{entry.error ? ` · ${entry.error}` : ''}
</p>
</div> </div>
</div> </div>
))} ))}