diff --git a/src/App.tsx b/src/App.tsx index c12b53a..d7b1bee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,7 +52,7 @@ const FORCED_ZERO_STOCK_SKUS = new Set([ 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 { rowIndex: number; @@ -63,6 +63,7 @@ interface BcSyncQueueEntry { status: BcSyncStatus; previewToken?: string; error?: string; + warning?: string; 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) => { setBcSyncQueue(prev => { const next: BcSyncQueue = { ...prev }; @@ -749,6 +763,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; ...item, status: 'failed', error: preview.error || 'Preview failed', + warning: undefined, }; } return; @@ -759,6 +774,8 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; status: 'previewed', previewToken: preview.previewToken, error: undefined, + selected: item.selected, + warning: undefined, }; }); return next; @@ -785,7 +802,21 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; }); 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 (isPreviewTokenMismatchError(result.error)) { const refreshedPreview = await previewBusinessCentralSync(appState.headers, item.newData); @@ -797,6 +828,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; status: 'previewed', previewToken: refreshedPreview.previewToken, error: undefined, + warning: undefined, }, })); @@ -817,6 +849,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; ...prev[articleNo], status: 'failed', error: retryResult.error || 'BC sync failed', + warning: retryResult.warning, }, })); continue; @@ -828,6 +861,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; ...prev[articleNo], status: 'failed', error: result.error || 'BC sync failed', + warning: result.warning, }, })); continue; @@ -959,11 +993,12 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; onRevertRow={handleRevertRow} isSavingAll={isSavingAll} 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} onSelectAllBcQueue={handleSelectAllBcQueue} onPreviewSelectedBcSync={handlePreviewSelectedBcSync} onSyncSelectedBcSync={handleSyncSelectedBcSync} + onDiscardSelectedBcQueue={handleDiscardSelectedBcQueue} isSyncingBC={isSyncingBC} isDownloadingBCExcel={isDownloadingBCExcel} isMaximized={isMaximized} diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index 27133ab..73c4e13 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -22,24 +22,36 @@ interface TopBarProps { onRevertRow: (articleNo: string) => void; isSavingAll: boolean; bcQueueCount: number; - bcQueueEntries: Record; + bcQueueEntries: Record; onToggleBcQueueSelection: (articleNo: string) => void; onSelectAllBcQueue: (selected: boolean) => void; onPreviewSelectedBcSync: () => Promise; onSyncSelectedBcSync: () => Promise; + onDiscardSelectedBcQueue: () => void; isSyncingBC: boolean; isDownloadingBCExcel: boolean; isMaximized: boolean; 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 [showBcQueue, setShowBcQueue] = useState(false); const pendingDropdownRef = useRef(null); const bcQueueDropdownRef = useRef(null); 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(() => { if (!showPending) return; const handler = (e: MouseEvent) => { @@ -238,6 +250,13 @@ export function TopBar({ stats, activeModule, onExport, onDownloadBCExcel, onRef > None +