From 0d50fce790c3ef97347023c5966ddeddb4a4ffaf Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Wed, 8 Jul 2026 13:28:47 +0200 Subject: [PATCH] feat(matrix): support new matrix layout with 4 added columns - Update default column indices to July 2026 layout (EmpCO_Compliant, Comments, Categorisation Code, Ingredients added; PM Classification removed; downstream columns shifted) - Seed internal Categorisation Code from the new Excel column while keeping app-side edits as the source of truth - Guard legacy CPNP index-77 fallbacks: only trusted when the current layout still holds CPNP there (new layout stores Item To Root Units) - Use resolved column index for forced zero stock instead of static one Co-Authored-By: Claude Fable 5 --- src/App.tsx | 54 ++++++++++++++++++++++++++++++++++++++++----- src/lib/supabase.ts | 12 ++++++---- src/types.ts | 47 +++++++++++++++++++++------------------ 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cf148d0..3d6dfbb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -276,10 +276,26 @@ export default function App() { console.warn('Supabase sync failed:', syncRes.status, errorText); } + // Layout detection: older matrix files carried CPNP No. at index 77 + // (LEGACY_CPNP_INDEX); the July 2026 layout stores "Item To Root Units" + // there, so legacy CPNP fallbacks must be disabled for the new layout. + const legacyCpnpIndexIsCpnp = String(headers[LEGACY_CPNP_INDEX] ?? '') + .toLowerCase() + .includes('cpnp'); + // The new layout also ships Categorisation Code as a real Excel column + // (British spelling). It seeds the internal column below; app-side edits + // stored in the internal column keep precedence. + const normalizeHeaderKey = (h: unknown) => + String(h ?? '').toLowerCase().replace(/[\s_-]+/g, ''); + const excelCategorisationIdx = headers.findIndex(h => { + const key = normalizeHeaderKey(h); + return key === 'categorisationcode' || key === 'categorizationcode'; + }); + console.log('Fetching synced data from Supabase...'); const syncedData = await getAllSyncedRows(); console.log('Fetching history data from Supabase for merge...'); - const historyData = await getHistoryDataForMerge(); + const historyData = await getHistoryDataForMerge(legacyCpnpIndexIsCpnp); 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. @@ -354,12 +370,15 @@ export default function App() { }); // Older cosmetic edits stored CPNP in index 77 before the dedicated - // internal CPNP column was stabilized at index 107. + // internal CPNP column was stabilized at index 107. Only trust that + // index when the current layout still holds CPNP there — in the new + // layout index 77 is "Item To Root Units". const legacyCpnp = hist?.[COLUMNS.CPNP_NO] ?? synced?.data?.[COLUMNS.CPNP_NO] ?? - hist?.[LEGACY_CPNP_INDEX] ?? - synced?.data?.[LEGACY_CPNP_INDEX]; + (legacyCpnpIndexIsCpnp + ? hist?.[LEGACY_CPNP_INDEX] ?? synced?.data?.[LEGACY_CPNP_INDEX] + : undefined); if (legacyCpnp !== undefined && legacyCpnp !== null && String(legacyCpnp).trim() !== '') { finalRow[COLUMNS.CPNP_NO] = legacyCpnp; } @@ -370,9 +389,17 @@ export default function App() { } } + // Categorisation Code: Excel value is the base, internal app edits win + if (excelCategorisationIdx >= 0 && excelCategorisationIdx !== COLUMNS.CATEGORIZATION_CODE) { + const internalCat = finalRow[COLUMNS.CATEGORIZATION_CODE]; + if (internalCat === undefined || internalCat === null || String(internalCat).trim() === '') { + finalRow[COLUMNS.CATEGORIZATION_CODE] = row[excelCategorisationIdx]; + } + } + // Force stock 0 for specific SKUs (User request) if (FORCED_ZERO_STOCK_SKUS.has(articleNo)) { - finalRow[COLUMNS.ITEM_AVAILABLE] = 0; + finalRow[resolvedCols.ITEM_AVAILABLE] = 0; } return finalRow.map((val: any, idx: number) => { @@ -820,6 +847,7 @@ export default function App() { synced: [] as string[], failed: [] as string[], skippedEmptyCategorization: [] as string[], + warnings: [] as string[], }; try { const syncColumns = resolveColumnIndices(appState.headers); @@ -896,6 +924,9 @@ export default function App() { }); setRowStatuses(prev => ({ ...prev, [articleNo]: 'synced' })); summary.synced.push(articleNo); + if (retryResult.warning) { + summary.warnings.push(`${articleNo}: ${retryResult.warning}`); + } continue; } @@ -932,15 +963,26 @@ export default function App() { }); setRowStatuses(prev => ({ ...prev, [articleNo]: 'synced' })); summary.synced.push(articleNo); + if (result.warning) { + summary.warnings.push(`${articleNo}: ${result.warning}`); + } } } finally { setIsSyncingBC(false); + const summaryTitle = summary.failed.length > 0 + ? 'Business Central sync finished with errors.' + : summary.warnings.length > 0 + ? 'Business Central sync finished with warnings.' + : 'Business Central sync finished.'; const lines = [ - `Business Central sync finished.`, + summaryTitle, `Synced: ${summary.synced.length}`, `Failed: ${summary.failed.length}`, `Empty CategorizationCode skipped: ${summary.skippedEmptyCategorization.length}`, ]; + if (summary.warnings.length > 0) { + lines.push('', 'Warnings (sync completed):', ...summary.warnings.slice(0, 10)); + } if (summary.failed.length > 0) { lines.push('', 'Failures:', ...summary.failed.slice(0, 10)); } diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 5d75d62..b00958d 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -299,7 +299,7 @@ export async function getHistory(): Promise { } } -export async function getHistoryDataForMerge(): Promise> { +export async function getHistoryDataForMerge(includeLegacyCpnpIndex = true): Promise> { try { const PAGE_SIZE = 1000; const entries: HistoryEntry[] = []; @@ -348,15 +348,19 @@ export async function getHistoryDataForMerge(): Promise // CPNP values were saved through multiple code paths over time. // Preserve the latest non-empty value even when a given history row - // does not surface it as a changed index. + // does not surface it as a changed index. Index 77 is only a CPNP + // location in the pre-July-2026 layout (includeLegacyCpnpIndex); + // in the new layout it holds "Item To Root Units". const latestCpnp = entry.new_data?.[COLUMNS.CPNP_NO] ?? - entry.new_data?.[LEGACY_CPNP_INDEX] ?? + (includeLegacyCpnpIndex ? entry.new_data?.[LEGACY_CPNP_INDEX] : undefined) ?? entry.old_data?.[COLUMNS.CPNP_NO]; const fallbackCpnp = latestCpnp !== undefined && latestCpnp !== null && latestCpnp !== '' ? latestCpnp - : entry.old_data?.[LEGACY_CPNP_INDEX]; + : includeLegacyCpnpIndex + ? entry.old_data?.[LEGACY_CPNP_INDEX] + : undefined; if (fallbackCpnp !== undefined && fallbackCpnp !== null && fallbackCpnp !== '') { target[COLUMNS.CPNP_NO] = fallbackCpnp; diff --git a/src/types.ts b/src/types.ts index 5c282ae..9a00bb7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,33 +10,36 @@ export interface AppState { } // Canonical column indices (defaults) +// Aligned to the matrix layout introduced in July 2026, which added +// EmpCO_Compliant (9), Comments (10), Categorisation Code (11) and +// Ingredients (80), and removed PM Classification. export const COLUMNS = { ARTICLE_NO: 0, ARTICLE_NAME: 2, LINE: 7, LICENSE: 8, - DETAILS_EN: 9, - DETAILS_DE: 10, - BARCODE: 29, - TARIFF_CODE: 60, - COUNTRY_ORIGIN: 62, - LONG_DE: 64, - LONG_EN: 65, - SHORT_DE: 66, - SHORT_EN: 67, - RECOMMENDED_AGE: 70, - CLASSIFICATION: 11, - ITEM_AVAILABLE: 16, - MOQ: 28, - UNITS_INNER: 32, - UNITS_OUTER: 33, - ASIN: 76, - INNER_W: 43, - INNER_L: 44, - INNER_H: 45, - OUTER_W: 48, - OUTER_L: 49, - OUTER_H: 50, + DETAILS_EN: 12, + DETAILS_DE: 13, + BARCODE: 31, + TARIFF_CODE: 62, + COUNTRY_ORIGIN: 64, + LONG_DE: 65, + LONG_EN: 66, + SHORT_DE: 67, + SHORT_EN: 68, + RECOMMENDED_AGE: 71, + CLASSIFICATION: 14, + ITEM_AVAILABLE: 18, + MOQ: 30, + UNITS_INNER: 34, + UNITS_OUTER: 35, + ASIN: 78, + INNER_W: 45, + INNER_L: 46, + INNER_H: 47, + OUTER_W: 50, + OUTER_L: 51, + OUTER_H: 52, VERIFIED_DIMS: 100, VALIDATED_CHECK: 101, VALIDATED_NOTE: 102,