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