feat(bc): sync categorization code

This commit is contained in:
Christian Vidal Wolf
2026-07-02 15:49:52 +02:00
parent 9e19a8cace
commit 633f523fc8
7 changed files with 146 additions and 25 deletions
+64 -5
View File
@@ -59,6 +59,16 @@ const LEGACY_CPNP_INDEX = 77;
const DROPBOX_PROXY_URL = '/api/dropbox-proxy';
const DROPBOX_FILE_URL = '/dropbox-file/scl/fi/usa8me7ywgylrij2bt6hj/Data-Matrix.xlsx?rlkey=tsec8csrhye54u1fdvk15ped1&st=qbxxs4cn&dl=0';
function normalizeImportedHeaders(headers: any[]): string[] {
const hasCategorizationCode = headers.some(header =>
String(header ?? '').toLowerCase().trim().replace(/[\s_-]+/g, '') === 'categorizationcode'
);
return headers.map(header => {
const label = String(header ?? '');
return !hasCategorizationCode && label.trim().toUpperCase() === 'TYPE' ? 'CategorizationCode' : label;
});
}
type BcSyncStatus = 'queued' | 'previewed' | 'preview_only' | 'syncing' | 'synced' | 'failed';
interface BcSyncQueueEntry {
@@ -249,7 +259,7 @@ export default function App() {
}
if (rows.length > 0) {
const headers = allData.slice(0, 1)[0];
const headers = normalizeImportedHeaders(allData.slice(0, 1)[0]);
console.log('Syncing Excel data to Supabase...');
const syncRes = await fetch('/api/dropbox-sync', {
@@ -273,14 +283,14 @@ export default function App() {
console.log('Supabase synced rows:', Object.keys(syncedData).length, '| History rows:', Object.keys(historyData).length);
// Extend headers with virtual columns if the Excel is shorter than the saved data.
// COLUMNS hardcoded indices (PRODUCT_TYPE=103, ITEM_TO_LOGISTIC=104, etc.) are used
// COLUMNS hardcoded indices (CATEGORIZATION_CODE=103, ITEM_TO_LOGISTIC=104, etc.) are used
// by older saved rows — ensure headers covers them so the merge and render work.
const extendedHeaders = [...headers];
const VIRTUAL_COLS: Record<number, string> = {
[COLUMNS.VERIFIED_DIMS]: 'Verified Dims',
[COLUMNS.VALIDATED_CHECK]: 'Validated',
[COLUMNS.VALIDATED_NOTE]: 'Note',
[COLUMNS.PRODUCT_TYPE]: 'TYPE',
[COLUMNS.CATEGORIZATION_CODE]: 'CategorizationCode',
[COLUMNS.ITEM_TO_LOGISTIC]: 'Item to Logistic',
[COLUMNS.ANNA_CHECK]: 'Anna Check',
[COLUMNS.ANNA_NOTE]: 'Anna Note',
@@ -448,7 +458,7 @@ export default function App() {
const data = XLSX.utils.sheet_to_json<any[]>(ws, { header: 1 });
if (data.length > 0) {
const headers = data[0];
const headers = normalizeImportedHeaders(data[0]);
const rawRows = data.slice(1);
const resolvedCols = resolveColumnIndices(headers);
@@ -733,6 +743,18 @@ export default function App() {
[bcQueueEntries]
);
const bcQueueDisplayEntries = useMemo(() => {
const queueColumns = resolveColumnIndices(appState.headers);
return Object.fromEntries((Object.entries(bcSyncQueue) as Array<[string, BcSyncQueueEntry]>).map(([articleNo, entry]) => [articleNo, {
articleName: entry.articleName,
selected: entry.selected,
status: entry.status,
error: entry.error,
warning: entry.warning,
categorizationCode: String(entry.newData[queueColumns.CATEGORIZATION_CODE] ?? '').trim(),
}]));
}, [bcSyncQueue, appState.headers]);
useEffect(() => {
if (appState.data.length === 0) return;
const validArticles = new Set(appState.data.map(row => String(row[COLUMNS.ARTICLE_NO] ?? '').trim()).filter(Boolean));
@@ -794,12 +816,26 @@ export default function App() {
const handleSyncSelectedBcSync = async () => {
if (bcQueueEntries.length === 0) return;
setIsSyncingBC(true);
const summary = {
synced: [] as string[],
failed: [] as string[],
skippedEmptyCategorization: [] as string[],
};
try {
const syncColumns = resolveColumnIndices(appState.headers);
const entriesToSync = [...bcQueueEntries].sort((a, b) => {
const rowDelta = a[1].rowIndex - b[1].rowIndex;
if (rowDelta !== 0) return rowDelta;
return a[0].localeCompare(b[0]);
});
const plannedCategorizationUpdates = entriesToSync
.map(([articleNo, item]) => ({
articleNo,
categorizationCode: String(item.newData[syncColumns.CATEGORIZATION_CODE] ?? '').trim(),
}))
.filter(item => item.categorizationCode);
console.table(plannedCategorizationUpdates);
setBcSyncQueue(prev => {
const next: BcSyncQueue = { ...prev };
@@ -812,6 +848,14 @@ export default function App() {
});
for (const [articleNo, item] of entriesToSync) {
const categorizationCode = String(item.newData[syncColumns.CATEGORIZATION_CODE] ?? '').trim();
if (!categorizationCode) {
console.info(`[BC sync] ${articleNo}: CategorizationCode is empty, so categorizationCode will not be sent.`);
summary.skippedEmptyCategorization.push(articleNo);
} else {
console.info(`[BC sync] ${articleNo}: will send categorizationCode=${categorizationCode}`);
}
const preview = await previewBusinessCentralSync(appState.headers, item.newData);
if (!preview.success) {
setBcSyncQueue(prev => ({
@@ -823,6 +867,7 @@ export default function App() {
warning: undefined,
},
}));
summary.failed.push(`${articleNo}: ${preview.error || 'Preview failed'}`);
continue;
}
@@ -850,6 +895,7 @@ export default function App() {
return next;
});
setRowStatuses(prev => ({ ...prev, [articleNo]: 'synced' }));
summary.synced.push(articleNo);
continue;
}
@@ -862,6 +908,7 @@ export default function App() {
warning: retryResult.warning,
},
}));
summary.failed.push(`${articleNo}: ${retryResult.error || 'BC sync failed'}`);
continue;
}
}
@@ -874,6 +921,7 @@ export default function App() {
warning: result.warning,
},
}));
summary.failed.push(`${articleNo}: ${result.error || 'BC sync failed'}`);
continue;
}
@@ -883,9 +931,20 @@ export default function App() {
return next;
});
setRowStatuses(prev => ({ ...prev, [articleNo]: 'synced' }));
summary.synced.push(articleNo);
}
} finally {
setIsSyncingBC(false);
const lines = [
`Business Central sync finished.`,
`Synced: ${summary.synced.length}`,
`Failed: ${summary.failed.length}`,
`Empty CategorizationCode skipped: ${summary.skippedEmptyCategorization.length}`,
];
if (summary.failed.length > 0) {
lines.push('', 'Failures:', ...summary.failed.slice(0, 10));
}
alert(lines.join('\n'));
}
};
@@ -987,7 +1046,7 @@ export default function App() {
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, warning: v.warning }]))}
bcQueueEntries={bcQueueDisplayEntries}
onToggleBcQueueSelection={handleToggleBcQueueSelection}
onSelectAllBcQueue={handleSelectAllBcQueue}
onPreviewSelectedBcSync={handlePreviewSelectedBcSync}