mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 13:35:25 +02:00
fix: commit all BC sync files missing from repo
This commit is contained in:
+282
-25
@@ -21,7 +21,7 @@ import { UndoToast } from './components/UndoToast';
|
||||
import { PendingValidationView } from './components/PendingValidationView';
|
||||
import { MissingDataView } from './components/MissingDataView';
|
||||
import { CosmeticItemsView } from './components/CosmeticItemsView';
|
||||
import { downloadBusinessCentralItemsExcel, updateCpnpNoInBC } from './services/businessCentral';
|
||||
import { downloadBusinessCentralItemsExcel, previewBusinessCentralSync, applyBusinessCentralSync, isPreviewTokenMismatchError } from './services/businessCentral';
|
||||
const FORCED_ZERO_STOCK_SKUS = new Set([
|
||||
'11631VC', '1237VC', '1238VC', '1652VC', '1653VC', '1684VC', '1688VC', '1717VC',
|
||||
'1718VC', '180VC', '1832VC', '2025VC', '2027VC', '2180VC', '2181VC', '2210VC',
|
||||
@@ -50,6 +50,36 @@ const FORCED_ZERO_STOCK_SKUS = new Set([
|
||||
'6471VC', '5658VCI', '2915244CLM', '30512912CLM', '2861262CLM', '41683MDRG', '41891MDRG'
|
||||
]);
|
||||
|
||||
const BC_SYNC_QUEUE_STORAGE_KEY = 'craze_bc_sync_queue';
|
||||
|
||||
type BcSyncStatus = 'queued' | 'previewed' | 'syncing' | 'synced' | 'failed';
|
||||
|
||||
interface BcSyncQueueEntry {
|
||||
rowIndex: number;
|
||||
originalData: ExcelRow;
|
||||
newData: ExcelRow;
|
||||
articleName: string;
|
||||
selected: boolean;
|
||||
status: BcSyncStatus;
|
||||
previewToken?: string;
|
||||
error?: string;
|
||||
source: 'save_all' | 'cosmetic_items' | 'manual';
|
||||
}
|
||||
|
||||
type BcSyncQueue = Record<string, BcSyncQueueEntry>;
|
||||
|
||||
function readStoredBcSyncQueue(): BcSyncQueue {
|
||||
try {
|
||||
const raw = localStorage.getItem(BC_SYNC_QUEUE_STORAGE_KEY);
|
||||
if (!raw) return {};
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
||||
return parsed as BcSyncQueue;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [session, setSession] = useState<AuthSession | null>(() => getStoredSession());
|
||||
|
||||
@@ -68,7 +98,9 @@ export default function App() {
|
||||
const [defaultLoadError, setDefaultLoadError] = useState<string | null>(null);
|
||||
const [rowStatuses, setRowStatuses] = useState<Record<string, string>>({});
|
||||
const [pendingRows, setPendingRows] = useState<Record<string, { rowIndex: number; originalData: ExcelRow; newData: ExcelRow; articleName: string }>>({});
|
||||
const [bcSyncQueue, setBcSyncQueue] = useState<BcSyncQueue>(() => readStoredBcSyncQueue());
|
||||
const [isSavingAll, setIsSavingAll] = useState(false);
|
||||
const [isSyncingBC, setIsSyncingBC] = useState(false);
|
||||
const [isDownloadingBCExcel, setIsDownloadingBCExcel] = useState(false);
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
|
||||
@@ -76,6 +108,14 @@ export default function App() {
|
||||
console.log('[App] session changed:', session ? 'logged in' : 'logged out');
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(BC_SYNC_QUEUE_STORAGE_KEY, JSON.stringify(bcSyncQueue));
|
||||
} catch {
|
||||
// Ignore storage quota or serialization errors.
|
||||
}
|
||||
}, [bcSyncQueue]);
|
||||
|
||||
const handleSignOut = () => {
|
||||
signOut();
|
||||
setSession(null);
|
||||
@@ -231,18 +271,31 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
||||
finalRow.push(null);
|
||||
}
|
||||
|
||||
// Merge logic: Prioritize internal control columns from syncedData or historyData
|
||||
// Use synced.data only if it has internal cols (>= 100), otherwise use hist
|
||||
const syncedHasInternalCols = synced?.data && (synced.data.length > 100 || Object.values(COLUMNS).some(idx => idx >= 100 && synced.data[idx] !== undefined && synced.data[idx] !== null));
|
||||
const sourceForInternal = syncedHasInternalCols ? synced!.data : (hist || synced?.data);
|
||||
|
||||
if (sourceForInternal) {
|
||||
// 1. ALWAYS restore Internal Control Columns (indices >= 100)
|
||||
// Merge logic:
|
||||
// - Use the Dropbox row as the base for master data and prices.
|
||||
// - Restore internal control columns from history when available.
|
||||
// This is critical because some synced rows still have empty internal
|
||||
// slots even when the change history already contains the corrected value.
|
||||
const mergedInternalSource = hist || synced?.data;
|
||||
|
||||
if (mergedInternalSource) {
|
||||
// 1. Restore internal control columns (indices >= 100)
|
||||
// These are the "TYPE", "Item to Logistic", "Checking", etc.
|
||||
// We use hardcoded indices from COLUMNS to ensure they stay at the end.
|
||||
// We use hardcoded indices from COLUMNS to keep them stable.
|
||||
Object.values(COLUMNS).forEach(idx => {
|
||||
if (idx >= 100 && sourceForInternal[idx] !== undefined && sourceForInternal[idx] !== null) {
|
||||
finalRow[idx] = sourceForInternal[idx];
|
||||
if (idx < 100) return;
|
||||
|
||||
const historyValue = hist?.[idx];
|
||||
const syncedValue = synced?.data?.[idx];
|
||||
const value =
|
||||
historyValue !== undefined && historyValue !== null && historyValue !== ''
|
||||
? historyValue
|
||||
: syncedValue !== undefined && syncedValue !== null && syncedValue !== ''
|
||||
? syncedValue
|
||||
: undefined;
|
||||
|
||||
if (value !== undefined) {
|
||||
finalRow[idx] = value;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -250,8 +303,9 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
||||
// This protects against the "Column Shift" bug where old saved indices might be wrong.
|
||||
if (synced?.status === 'pending' || synced?.status === 'edited') {
|
||||
editableColumns.forEach(idx => {
|
||||
if (idx < 100 && sourceForInternal[idx] !== undefined && sourceForInternal[idx] !== null) {
|
||||
finalRow[idx] = sourceForInternal[idx];
|
||||
const value = synced?.data?.[idx];
|
||||
if (idx < 100 && value !== undefined && value !== null) {
|
||||
finalRow[idx] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -529,6 +583,12 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
||||
});
|
||||
setPendingRows(prev => { const n = { ...prev }; delete n[articleNo]; return n; });
|
||||
setRowStatuses(prev => { const n = { ...prev }; delete n[articleNo]; return n; });
|
||||
setBcSyncQueue(prev => {
|
||||
if (!prev[articleNo]) return prev;
|
||||
const n = { ...prev };
|
||||
delete n[articleNo];
|
||||
return n;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveAll = async () => {
|
||||
@@ -544,28 +604,32 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
||||
let sessionIssue = false;
|
||||
|
||||
try {
|
||||
await Promise.all(entries.map(async ([articleNo, { newData, originalData, articleName }]) => {
|
||||
await Promise.all(entries.map(async ([articleNo, { rowIndex, newData, originalData, articleName }]) => {
|
||||
console.log('[handleSaveAll] Saving article:', articleNo);
|
||||
const result = await saveRowToSupabase(articleNo, newData);
|
||||
console.log('[handleSaveAll] Save result for', articleNo, ':', result);
|
||||
|
||||
if (result.success) {
|
||||
const cpnpValue = String(newData[COLUMNS.CPNP_NO] ?? '').trim();
|
||||
const cpnpChanged = cpnpValue !== String(originalData[COLUMNS.CPNP_NO] ?? '').trim();
|
||||
|
||||
const [histRes, bcResult] = await Promise.all([
|
||||
saveHistoryEntry(articleNo, articleName, originalData, newData, session?.user?.email || 'unknown'),
|
||||
cpnpValue && cpnpChanged ? updateCpnpNoInBC(articleNo, cpnpValue) : Promise.resolve({ success: true }),
|
||||
]);
|
||||
const histRes = await saveHistoryEntry(articleNo, articleName, originalData, newData, session?.user?.email || 'unknown');
|
||||
|
||||
if (!histRes.success) {
|
||||
console.warn(`[handleSaveAll] History save failed for ${articleNo}:`, histRes.error);
|
||||
}
|
||||
if (!bcResult.success) {
|
||||
console.warn(`[handleSaveAll] BC sync failed for ${articleNo}:`, (bcResult as any).error);
|
||||
}
|
||||
|
||||
setRowStatuses(prev => ({ ...prev, [articleNo]: 'saved' }));
|
||||
setBcSyncQueue(prev => ({
|
||||
...prev,
|
||||
[articleNo]: {
|
||||
rowIndex,
|
||||
originalData,
|
||||
newData,
|
||||
articleName,
|
||||
selected: true,
|
||||
status: 'queued',
|
||||
source: 'save_all',
|
||||
},
|
||||
}));
|
||||
|
||||
setRowStatuses(prev => ({ ...prev, [articleNo]: 'bc_pending' }));
|
||||
setPendingRows(prev => {
|
||||
const n = { ...prev };
|
||||
delete n[articleNo];
|
||||
@@ -598,6 +662,189 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
||||
}
|
||||
};
|
||||
|
||||
const handleQueueBcSync = (articleNo: string, rowIndex: number, originalData: ExcelRow, newData: ExcelRow, articleName: string) => {
|
||||
setBcSyncQueue(prev => ({
|
||||
...prev,
|
||||
[articleNo]: {
|
||||
rowIndex,
|
||||
originalData,
|
||||
newData,
|
||||
articleName,
|
||||
selected: true,
|
||||
status: 'queued',
|
||||
source: 'cosmetic_items',
|
||||
},
|
||||
}));
|
||||
setRowStatuses(prev => ({ ...prev, [articleNo]: 'bc_pending' }));
|
||||
};
|
||||
|
||||
const handleToggleBcQueueSelection = (articleNo: string) => {
|
||||
setBcSyncQueue(prev => {
|
||||
const item = prev[articleNo];
|
||||
if (!item) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[articleNo]: {
|
||||
...item,
|
||||
selected: !item.selected,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAllBcQueue = (selected: boolean) => {
|
||||
setBcSyncQueue(prev => {
|
||||
const next: BcSyncQueue = { ...prev };
|
||||
(Object.keys(next) as string[]).forEach(articleNo => {
|
||||
const item = next[articleNo];
|
||||
next[articleNo] = { ...item, selected };
|
||||
});
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const bcQueueEntries = useMemo<Array<[string, BcSyncQueueEntry]>>(
|
||||
() => Object.entries(bcSyncQueue) as Array<[string, BcSyncQueueEntry]>,
|
||||
[bcSyncQueue]
|
||||
);
|
||||
|
||||
const selectedBcQueueEntries = useMemo<Array<[string, BcSyncQueueEntry]>>(
|
||||
() => bcQueueEntries.filter(([, item]) => item.selected),
|
||||
[bcQueueEntries]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (appState.data.length === 0) return;
|
||||
const validArticles = new Set(appState.data.map(row => String(row[COLUMNS.ARTICLE_NO] ?? '').trim()).filter(Boolean));
|
||||
setBcSyncQueue(prev => {
|
||||
let changed = false;
|
||||
const next: BcSyncQueue = {};
|
||||
for (const [articleNo, item] of Object.entries(prev) as Array<[string, BcSyncQueueEntry]>) {
|
||||
if (!validArticles.has(articleNo)) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
next[articleNo] = item;
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
}, [appState.data]);
|
||||
|
||||
const handlePreviewSelectedBcSync = async () => {
|
||||
if (selectedBcQueueEntries.length === 0) return;
|
||||
setIsSyncingBC(true);
|
||||
try {
|
||||
const results = await Promise.all(selectedBcQueueEntries.map(async ([articleNo, item]) => {
|
||||
const preview = await previewBusinessCentralSync(appState.headers, item.newData);
|
||||
return [articleNo, preview] as const;
|
||||
}));
|
||||
|
||||
setBcSyncQueue(prev => {
|
||||
const next: BcSyncQueue = { ...prev };
|
||||
results.forEach(([articleNo, preview]) => {
|
||||
const item = next[articleNo];
|
||||
if (!item || !preview.success) {
|
||||
if (item) {
|
||||
next[articleNo] = {
|
||||
...item,
|
||||
status: 'failed',
|
||||
error: preview.error || 'Preview failed',
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
next[articleNo] = {
|
||||
...item,
|
||||
status: 'previewed',
|
||||
previewToken: preview.previewToken,
|
||||
error: undefined,
|
||||
};
|
||||
});
|
||||
return next;
|
||||
});
|
||||
} finally {
|
||||
setIsSyncingBC(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncSelectedBcSync = async () => {
|
||||
if (selectedBcQueueEntries.length === 0) return;
|
||||
setIsSyncingBC(true);
|
||||
try {
|
||||
const selectedArticles = new Set(selectedBcQueueEntries.map(([articleNo]) => articleNo));
|
||||
|
||||
setBcSyncQueue(prev => {
|
||||
const next: BcSyncQueue = { ...prev };
|
||||
(Object.entries(next) as Array<[string, BcSyncQueueEntry]>).forEach(([articleNo, item]) => {
|
||||
if (selectedArticles.has(articleNo)) {
|
||||
next[articleNo] = { ...item, status: 'syncing', error: undefined };
|
||||
}
|
||||
});
|
||||
return next;
|
||||
});
|
||||
|
||||
for (const [articleNo, item] of selectedBcQueueEntries) {
|
||||
const result = await applyBusinessCentralSync(appState.headers, item.newData, item.previewToken);
|
||||
if (!result.success) {
|
||||
if (isPreviewTokenMismatchError(result.error)) {
|
||||
const refreshedPreview = await previewBusinessCentralSync(appState.headers, item.newData);
|
||||
if (refreshedPreview.success) {
|
||||
setBcSyncQueue(prev => ({
|
||||
...prev,
|
||||
[articleNo]: {
|
||||
...prev[articleNo],
|
||||
status: 'previewed',
|
||||
previewToken: refreshedPreview.previewToken,
|
||||
error: undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
const retryResult = await applyBusinessCentralSync(appState.headers, item.newData, refreshedPreview.previewToken);
|
||||
if (retryResult.success) {
|
||||
setBcSyncQueue(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[articleNo];
|
||||
return next;
|
||||
});
|
||||
setRowStatuses(prev => ({ ...prev, [articleNo]: 'synced' }));
|
||||
continue;
|
||||
}
|
||||
|
||||
setBcSyncQueue(prev => ({
|
||||
...prev,
|
||||
[articleNo]: {
|
||||
...prev[articleNo],
|
||||
status: 'failed',
|
||||
error: retryResult.error || 'BC sync failed',
|
||||
},
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
setBcSyncQueue(prev => ({
|
||||
...prev,
|
||||
[articleNo]: {
|
||||
...prev[articleNo],
|
||||
status: 'failed',
|
||||
error: result.error || 'BC sync failed',
|
||||
},
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
|
||||
setBcSyncQueue(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[articleNo];
|
||||
return next;
|
||||
});
|
||||
setRowStatuses(prev => ({ ...prev, [articleNo]: 'synced' }));
|
||||
}
|
||||
} finally {
|
||||
setIsSyncingBC(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadBCExcel = async () => {
|
||||
setIsDownloadingBCExcel(true);
|
||||
try {
|
||||
@@ -616,6 +863,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
||||
data: JSON.parse(JSON.stringify(appState.data)), // Deep copy
|
||||
rowStatuses: { ...rowStatuses },
|
||||
pendingRows: { ...pendingRows },
|
||||
bcSyncQueue: JSON.parse(JSON.stringify(bcSyncQueue)) as BcSyncQueue,
|
||||
message
|
||||
};
|
||||
// Keep last 50 steps
|
||||
@@ -639,6 +887,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
||||
// Restore statuses and pending state
|
||||
if (lastAction.rowStatuses) setRowStatuses(lastAction.rowStatuses);
|
||||
if (lastAction.pendingRows) setPendingRows(lastAction.pendingRows);
|
||||
if (lastAction.bcSyncQueue) setBcSyncQueue(lastAction.bcSyncQueue);
|
||||
|
||||
setUndoHistory(remainingHistory);
|
||||
};
|
||||
@@ -709,6 +958,13 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
||||
onSaveAll={handleSaveAll}
|
||||
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 }]))}
|
||||
onToggleBcQueueSelection={handleToggleBcQueueSelection}
|
||||
onSelectAllBcQueue={handleSelectAllBcQueue}
|
||||
onPreviewSelectedBcSync={handlePreviewSelectedBcSync}
|
||||
onSyncSelectedBcSync={handleSyncSelectedBcSync}
|
||||
isSyncingBC={isSyncingBC}
|
||||
isDownloadingBCExcel={isDownloadingBCExcel}
|
||||
isMaximized={isMaximized}
|
||||
onToggleMaximize={() => setIsMaximized(true)}
|
||||
@@ -823,6 +1079,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
|
||||
onSaveRow={handleSaveRow}
|
||||
onCaptureState={captureState}
|
||||
rowStatuses={rowStatuses}
|
||||
onQueueBcSync={handleQueueBcSync}
|
||||
/>
|
||||
)}
|
||||
{activeModule === 'history' && (
|
||||
|
||||
Reference in New Issue
Block a user