diff --git a/scratch/check_item.js b/scratch/check_item.js
new file mode 100644
index 0000000..450be5c
--- /dev/null
+++ b/scratch/check_item.js
@@ -0,0 +1,18 @@
+import * as XLSX from 'xlsx';
+import * as fs from 'fs';
+
+const buf = fs.readFileSync('data (3).xlsx');
+const wb = XLSX.read(buf, {type: 'buffer'});
+const ws = wb.Sheets[wb.SheetNames[0]];
+const data = XLSX.utils.sheet_to_json(ws, {header: 1});
+
+const itemRow = data.find(r => String(r[0]) === '75432');
+if (itemRow) {
+ console.log('Row for 75432:');
+ console.log(`Index 0 (Article No): ${itemRow[0]}`);
+ console.log(`Index 31 (Units Inner): ${itemRow[31]}`);
+ console.log(`Index 32 (Units Outer): ${itemRow[32]}`);
+ console.log(`Index 33 (Units Pallet): ${itemRow[33]}`);
+} else {
+ console.log('Item 75432 not found');
+}
diff --git a/scratch/current_data.xlsx b/scratch/current_data.xlsx
new file mode 100644
index 0000000..7221bbb
Binary files /dev/null and b/scratch/current_data.xlsx differ
diff --git a/scratch/dump_headers.js b/scratch/dump_headers.js
new file mode 100644
index 0000000..7cc2b04
--- /dev/null
+++ b/scratch/dump_headers.js
@@ -0,0 +1,13 @@
+import * as XLSX from 'xlsx';
+import * as fs from 'fs';
+
+const buf = fs.readFileSync('data (3).xlsx');
+const wb = XLSX.read(buf, {type: 'buffer'});
+const ws = wb.Sheets[wb.SheetNames[0]];
+const data = XLSX.utils.sheet_to_json(ws, {header: 1});
+const headers = data[0];
+
+console.log('Headers:');
+headers.forEach((h, i) => {
+ console.log(`${i}: ${h}`);
+});
diff --git a/src/App.tsx b/src/App.tsx
index dee265d..619f126 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,6 +1,7 @@
import React, { useState, useMemo, useEffect } from 'react';
import * as XLSX from 'xlsx';
-import { AppState, ExcelRow, COLUMNS } from './types';
+import { AppState, ExcelRow, resolveColumnIndices, COLUMNS } from './types';
+import { ColumnsProvider } from './contexts/ColumnsContext';
import { Sidebar } from './components/Sidebar';
import { TopBar } from './components/TopBar';
import { ProductDescriptions } from './components/ProductDescriptions';
@@ -121,18 +122,19 @@ export default function App() {
console.log('Fetching synced data from Supabase...');
const syncedData = await getAllSyncedRows(session?.access_token);
+ const resolvedCols = resolveColumnIndices(headers);
const editableColumns = new Set([
- COLUMNS.CLASSIFICATION,
- COLUMNS.LONG_DE, COLUMNS.LONG_EN,
- COLUMNS.SHORT_DE, COLUMNS.SHORT_EN,
- COLUMNS.DETAILS_DE, COLUMNS.DETAILS_EN,
- COLUMNS.INNER_L, COLUMNS.INNER_W, COLUMNS.INNER_H,
- COLUMNS.OUTER_L, COLUMNS.OUTER_W, COLUMNS.OUTER_H,
- COLUMNS.UNITS_OUTER, COLUMNS.MOQ,
- COLUMNS.VERIFIED_DIMS,
- COLUMNS.VALIDATED_CHECK,
- COLUMNS.VALIDATED_NOTE,
- COLUMNS.PRODUCT_TYPE
+ resolvedCols.CLASSIFICATION,
+ resolvedCols.LONG_DE, resolvedCols.LONG_EN,
+ resolvedCols.SHORT_DE, resolvedCols.SHORT_EN,
+ resolvedCols.DETAILS_DE, resolvedCols.DETAILS_EN,
+ resolvedCols.INNER_L, resolvedCols.INNER_W, resolvedCols.INNER_H,
+ resolvedCols.OUTER_L, resolvedCols.OUTER_W, resolvedCols.OUTER_H,
+ resolvedCols.UNITS_OUTER, resolvedCols.MOQ,
+ resolvedCols.VERIFIED_DIMS,
+ resolvedCols.VALIDATED_CHECK,
+ resolvedCols.VALIDATED_NOTE,
+ resolvedCols.PRODUCT_TYPE
]);
headers.forEach((h: any, i: number) => {
@@ -142,7 +144,7 @@ export default function App() {
}
});
- const articleNoIdx = COLUMNS.ARTICLE_NO;
+ const articleNoIdx = resolvedCols.ARTICLE_NO;
const processedRows = rows.map(row => {
const articleNo = String(row[articleNoIdx]);
const synced = syncedData[articleNo];
@@ -530,7 +532,8 @@ export default function App() {
}
return (
-
+
+
)}
+
);
}
diff --git a/src/components/ArticleDetails.tsx b/src/components/ArticleDetails.tsx
index 6decc2e..d3510d0 100644
--- a/src/components/ArticleDetails.tsx
+++ b/src/components/ArticleDetails.tsx
@@ -1,5 +1,6 @@
import React, { useState, useMemo } from 'react';
-import { ExcelRow, COLUMNS } from '../types';
+import { ExcelRow } from '../types';
+import { useColumns } from '../contexts/ColumnsContext';
import { Search, Filter, Edit2, ChevronDown, ChevronUp, Info, Package, X } from 'lucide-react';
import { cn } from '../lib/utils';
import { ColumnFilterPopover } from './ColumnFilterPopover';
@@ -13,6 +14,7 @@ interface ArticleDetailsProps {
type TabType = 'all' | 'missingDetailsDE' | 'missingDetailsEN' | 'missingAnyDetails' | 'lowStock';
export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProps) {
+ const COLUMNS = useColumns();
const [activeTab, setActiveTab] = useState
('all');
const [search, setSearch] = useState('');
const [lineFilter, setLineFilter] = useState('');
diff --git a/src/components/DataCompleteness.tsx b/src/components/DataCompleteness.tsx
index b7e371f..a17518a 100644
--- a/src/components/DataCompleteness.tsx
+++ b/src/components/DataCompleteness.tsx
@@ -1,5 +1,6 @@
import React, { useMemo, useState } from 'react';
-import { ExcelRow, COLUMNS } from '../types';
+import { ExcelRow } from '../types';
+import { useColumns } from '../contexts/ColumnsContext';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { cn } from '../lib/utils';
@@ -9,6 +10,7 @@ interface DataCompletenessProps {
}
export function DataCompleteness({ data, headers }: DataCompletenessProps) {
+ const COLUMNS = useColumns();
const [sortCol, setSortCol] = useState('score');
const [sortDesc, setSortDesc] = useState(false);
const [page, setPage] = useState(1);
diff --git a/src/components/DimensionsView.tsx b/src/components/DimensionsView.tsx
index a65b788..0ef2143 100644
--- a/src/components/DimensionsView.tsx
+++ b/src/components/DimensionsView.tsx
@@ -1,5 +1,6 @@
import React, { useState, useMemo } from 'react';
-import { ExcelRow, COLUMNS } from '../types';
+import { ExcelRow } from '../types';
+import { useColumns } from '../contexts/ColumnsContext';
import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, RefreshCw, Layers, Link2, Search, Filter, X, Undo2 } from 'lucide-react';
import { cn } from '../lib/utils';
import { ConfirmModal } from './ConfirmModal';
@@ -35,6 +36,7 @@ interface NearDuplicateCluster {
}
export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureState, rowStatuses, onRevertRow }: DimensionsViewProps) {
+ const COLUMNS = useColumns();
const [expandedGroups, setExpandedGroups] = useState>(new Set());
const [expandedNearDuplicates, setExpandedNearDuplicates] = useState>(new Set());
const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(true);
diff --git a/src/components/EditPanel.tsx b/src/components/EditPanel.tsx
index d485870..0ecd21a 100644
--- a/src/components/EditPanel.tsx
+++ b/src/components/EditPanel.tsx
@@ -1,5 +1,6 @@
import React, { useState, useRef, useCallback } from 'react';
-import { ExcelRow, COLUMNS } from '../types';
+import { ExcelRow } from '../types';
+import { useColumns } from '../contexts/ColumnsContext';
import { X, Sparkles, Save, Loader2, Languages, Package, CheckCircle2, Mic, MicOff } from 'lucide-react';
import { generateGemini } from '../services/gemini';
import { cn } from '../lib/utils';
@@ -126,6 +127,7 @@ const FieldEditor = ({
);
export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: EditPanelProps) {
+ const COLUMNS = useColumns();
const [formData, setFormData] = useState({
longDe: row[COLUMNS.LONG_DE] || '',
longEn: row[COLUMNS.LONG_EN] || '',
diff --git a/src/components/HistoryView.tsx b/src/components/HistoryView.tsx
index cd2dff7..9a9c5e5 100644
--- a/src/components/HistoryView.tsx
+++ b/src/components/HistoryView.tsx
@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { History, RotateCcw, ChevronDown, ChevronRight, User, Calendar, Tag, Search, X, Edit2 } from 'lucide-react';
import { getHistory, deleteHistoryEntry, HistoryEntry } from '../lib/supabase';
-import { ExcelRow, COLUMNS } from '../types';
+import { ExcelRow } from '../types';
+import { useColumns } from '../contexts/ColumnsContext';
import { cn } from '../lib/utils';
interface HistoryViewProps {
@@ -13,6 +14,7 @@ interface HistoryViewProps {
}
export function HistoryView({ headers, data, onRevert, onEdit, sessionToken }: HistoryViewProps) {
+ const COLUMNS = useColumns();
const [history, setHistory] = useState([]);
const [loading, setLoading] = useState(true);
const [expandedId, setExpandedId] = useState(null);
diff --git a/src/components/MissingDataView.tsx b/src/components/MissingDataView.tsx
index 664686c..7ecb77b 100644
--- a/src/components/MissingDataView.tsx
+++ b/src/components/MissingDataView.tsx
@@ -1,5 +1,6 @@
import React, { useState, useMemo } from 'react';
-import { ExcelRow, COLUMNS } from '../types';
+import { ExcelRow } from '../types';
+import { useColumns } from '../contexts/ColumnsContext';
import { Search, ChevronDown, ChevronUp, X, Edit2, Save } from 'lucide-react';
import { cn } from '../lib/utils';
import { ColumnFilterPopover } from './ColumnFilterPopover';
@@ -65,6 +66,7 @@ interface EditingState {
}
export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: MissingDataViewProps) {
+ const COLUMNS = useColumns();
const [activeTab, setActiveTab] = useState('missingLaunch');
const [search, setSearch] = useState('');
const [sortCol, setSortCol] = useState(null);
diff --git a/src/components/PendingValidationView.tsx b/src/components/PendingValidationView.tsx
index 64b609a..91d14d6 100644
--- a/src/components/PendingValidationView.tsx
+++ b/src/components/PendingValidationView.tsx
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
-import { ExcelRow, COLUMNS } from '../types';
+import { ExcelRow } from '../types';
+import { useColumns } from '../contexts/ColumnsContext';
import { Clock, Undo2, Package, Box, DollarSign, FileText, Search, Filter, X } from 'lucide-react';
import { cn } from '../lib/utils';
@@ -12,6 +13,7 @@ interface PendingValidationViewProps {
}
export function PendingValidationView({ data, pendingRows, rowStatuses, onRevertRow, onEdit }: PendingValidationViewProps) {
+ const COLUMNS = useColumns();
const [search, setSearch] = useState('');
const pendingEntries = Object.entries(pendingRows);
diff --git a/src/components/PricingView.tsx b/src/components/PricingView.tsx
index 1794546..6b450cf 100644
--- a/src/components/PricingView.tsx
+++ b/src/components/PricingView.tsx
@@ -1,5 +1,6 @@
import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react';
-import { ExcelRow, COLUMNS } from '../types';
+import { ExcelRow } from '../types';
+import { useColumns } from '../contexts/ColumnsContext';
import {
AlertTriangle,
AlertCircle,
@@ -48,6 +49,7 @@ function findCol(headers: string[], ...keywords: string[]): number {
}
export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, rowStatuses }: PricingViewProps) {
+ const COLUMNS = useColumns();
const [filterMode, setFilterMode] = useState('all_errors');
const [search, setSearch] = useState('');
const [searchMode, setSearchMode] = useState<'all' | 'selected'>('all');
@@ -193,15 +195,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const nwIdx = findCol(headers, 'nw');
const gwIdx = findCol(headers, 'gw');
- // Units/Outer column — try dynamic detection first, fall back to hardcoded index
- const unitsOuterIdx = findCol(headers, 'units', 'outer') >= 0
- ? findCol(headers, 'units', 'outer')
- : findCol(headers, 'vpe') >= 0
- ? findCol(headers, 'vpe')
- : COLUMNS.UNITS_OUTER;
+ const unitsOuterIdx = COLUMNS.UNITS_OUTER;
return { uvpIdx, srpCols: srp, containerCols: container, nwIdx, gwIdx, unitsOuterIdx };
- }, [headers]);
+ }, [headers, COLUMNS]);
// ── Error analysis per row ────────────────────────────────────────────────
const analyzedRows = useMemo(() => {
diff --git a/src/components/ProductDescriptions.tsx b/src/components/ProductDescriptions.tsx
index e25f2ff..f01a3fa 100644
--- a/src/components/ProductDescriptions.tsx
+++ b/src/components/ProductDescriptions.tsx
@@ -1,5 +1,6 @@
import React, { useState, useMemo } from 'react';
-import { ExcelRow, COLUMNS } from '../types';
+import { ExcelRow } from '../types';
+import { useColumns } from '../contexts/ColumnsContext';
import { Search, Filter, Edit2, ChevronDown, ChevronUp, X } from 'lucide-react';
import { cn } from '../lib/utils';
import { ColumnFilterPopover } from './ColumnFilterPopover';
@@ -12,12 +13,11 @@ interface ProductDescriptionsProps {
rowStatuses: Record;
}
-type TabType = 'all' | 'missingLongDE' | 'missingLongEN' | 'missingShortDE' | 'missingShortEN' | 'complete' | 'incomplete';
-
-// Description columns that should only have Present/Missing filters
-const DESCRIPTION_COLUMNS = [COLUMNS.LONG_DE, COLUMNS.LONG_EN, COLUMNS.SHORT_DE, COLUMNS.SHORT_EN, COLUMNS.DETAILS_DE, COLUMNS.DETAILS_EN];
-
export function ProductDescriptions({ data, headers, asinColumnIndex, onEdit, rowStatuses }: ProductDescriptionsProps) {
+ const COLUMNS = useColumns();
+
+ // Description columns that should only have Present/Missing filters
+ const DESCRIPTION_COLUMNS = [COLUMNS.LONG_DE, COLUMNS.LONG_EN, COLUMNS.SHORT_DE, COLUMNS.SHORT_EN, COLUMNS.DETAILS_DE, COLUMNS.DETAILS_EN];
const [activeTab, setActiveTab] = useState('all');
const [search, setSearch] = useState('');
const [lineFilter, setLineFilter] = useState('');
diff --git a/src/contexts/ColumnsContext.tsx b/src/contexts/ColumnsContext.tsx
new file mode 100644
index 0000000..58b6352
--- /dev/null
+++ b/src/contexts/ColumnsContext.tsx
@@ -0,0 +1,23 @@
+import React, { createContext, useContext, useMemo } from 'react';
+import { COLUMNS, resolveColumnIndices } from '../types';
+
+type ColumnIndices = typeof COLUMNS;
+
+const ColumnsContext = createContext(COLUMNS);
+
+export function ColumnsProvider({ headers, children }: { headers: string[], children: React.ReactNode }) {
+ const resolved = useMemo(() => {
+ if (!headers || headers.length === 0) return COLUMNS;
+ return resolveColumnIndices(headers);
+ }, [headers]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useColumns() {
+ return useContext(ColumnsContext);
+}
diff --git a/src/types.ts b/src/types.ts
index 7689fe2..fdfe2ee 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -9,6 +9,7 @@ export interface AppState {
asinColumnIndex: number | null;
}
+// Canonical column indices (defaults)
export const COLUMNS = {
ARTICLE_NO: 0,
ARTICLE_NAME: 2,
@@ -16,28 +17,79 @@ export const COLUMNS = {
LICENSE: 8,
DETAILS_EN: 9,
DETAILS_DE: 10,
- BARCODE: 28,
- TARIFF_CODE: 58,
- COUNTRY_ORIGIN: 60,
- LONG_DE: 62,
- LONG_EN: 63,
- SHORT_DE: 64,
- SHORT_EN: 65,
- RECOMMENDED_AGE: 67,
+ 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: 14,
- MOQ: 27,
- UNITS_INNER: 31,
- UNITS_OUTER: 32,
- ASIN: 33,
- INNER_W: 42,
- INNER_L: 43,
- INNER_H: 44,
- OUTER_W: 47,
- OUTER_L: 48,
- OUTER_H: 49,
+ 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,
VERIFIED_DIMS: 100,
VALIDATED_CHECK: 101,
VALIDATED_NOTE: 102,
PRODUCT_TYPE: 103
-};
\ No newline at end of file
+};
+
+// Search patterns for dynamic detection
+export const COLUMN_PATTERNS: Record = {
+ ARTICLE_NO: ['article', 'no'],
+ ARTICLE_NAME: ['article', 'name'],
+ LINE: ['line'],
+ LICENSE: ['license'],
+ DETAILS_EN: ['details', 'english'],
+ DETAILS_DE: ['details', 'german'],
+ BARCODE: ['article', 'barcode'],
+ TARIFF_CODE: ['tariff', 'code'],
+ COUNTRY_ORIGIN: ['country', 'origin'],
+ LONG_DE: ['long', 'description', 'german'],
+ LONG_EN: ['long', 'description', 'english'],
+ SHORT_DE: ['short', 'description', 'german'],
+ SHORT_EN: ['short', 'description', 'english'],
+ RECOMMENDED_AGE: ['recommended', 'age'],
+ CLASSIFICATION: ['classification'],
+ ITEM_AVAILABLE: ['item', 'available'],
+ MOQ: ['moq'],
+ UNITS_INNER: ['units', 'inner'],
+ UNITS_OUTER: ['units', 'outer'],
+ ASIN: ['asin'],
+ INNER_W: ['inner', 'w'],
+ INNER_L: ['inner', 'l'],
+ INNER_H: ['inner', 'h'],
+ OUTER_W: ['outer', 'w'],
+ OUTER_L: ['outer', 'l'],
+ OUTER_H: ['outer', 'h'],
+ // Virtual/Extra columns stay hardcoded or managed elsewhere
+ VERIFIED_DIMS: ['verified'],
+ VALIDATED_CHECK: ['validated'],
+ VALIDATED_NOTE: ['note'],
+ PRODUCT_TYPE: ['product', 'type']
+};
+
+export function resolveColumnIndices(headers: string[]): typeof COLUMNS {
+ const resolved = { ...COLUMNS };
+ const h = headers.map(val => String(val || '').toLowerCase());
+
+ Object.entries(COLUMN_PATTERNS).forEach(([key, patterns]) => {
+ const idx = h.findIndex(headerText =>
+ patterns.every(p => headerText.includes(p.toLowerCase()))
+ );
+ if (idx >= 0) {
+ resolved[key as keyof typeof COLUMNS] = idx;
+ }
+ });
+
+ return resolved;
+}
\ No newline at end of file