mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 12:35:25 +02:00
feat(topbar): add Discard button to BC sync queue dropdown
This commit is contained in:
+38
-3
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user