From 3db2bc0daedc43fb845d09e490e642f189bd2722 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Sun, 29 Mar 2026 17:30:04 +0200 Subject: [PATCH] Add Dimensions tab for packaging and MOQ consistency check --- src/App.tsx | 11 +- src/components/DimensionsView.tsx | 292 ++++++++++++++++++++++++++++++ src/components/EditPanel.tsx | 69 ++++++- src/components/Sidebar.tsx | 5 +- src/types.ts | 11 +- 5 files changed, 382 insertions(+), 6 deletions(-) create mode 100644 src/components/DimensionsView.tsx diff --git a/src/App.tsx b/src/App.tsx index 2fb0cb1..c4d9745 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { EditPanel } from './components/EditPanel'; import { getAllSyncedRows, saveRowToSupabase } from './lib/supabase'; import { getStoredSession, signOut, type AuthSession } from './lib/auth'; import { LoginPage } from './components/LoginPage'; +import { DimensionsView } from './components/DimensionsView'; export default function App() { const [session, setSession] = useState(() => getStoredSession()); @@ -29,7 +30,7 @@ export default function App() { fileDate: null, hasUnsavedChanges: false }); - const [activeModule, setActiveModule] = useState<'descriptions' | 'matrix'>('descriptions'); + const [activeModule, setActiveModule] = useState<'descriptions' | 'matrix' | 'dimensions'>('descriptions'); const [editingRowIndex, setEditingRowIndex] = useState(null); const [isLoadingDefault, setIsLoadingDefault] = useState(true); const [defaultLoadError, setDefaultLoadError] = useState(null); @@ -322,6 +323,14 @@ export default function App() { )} + {activeModule === 'dimensions' && ( + setEditingRowIndex(index)} + onSaveRow={handleSaveRow} + /> + )} )} diff --git a/src/components/DimensionsView.tsx b/src/components/DimensionsView.tsx new file mode 100644 index 0000000..5c77b84 --- /dev/null +++ b/src/components/DimensionsView.tsx @@ -0,0 +1,292 @@ +import React, { useState, useMemo } from 'react'; +import { ExcelRow, COLUMNS } from '../types'; +import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, Languages } from 'lucide-react'; +import { cn } from '../lib/utils'; + +interface DimensionsViewProps { + data: ExcelRow[]; + headers: string[]; + onEdit: (index: number) => void; + onSaveRow: (index: number, updatedRow: ExcelRow) => void; +} + +interface DimensionGroup { + key: string; + innerDims: string; + rows: { row: ExcelRow; index: number }[]; + isInconsistent: boolean; + discrepancies: { + outer: boolean; + units: boolean; + moq: boolean; + }; +} + +export function DimensionsView({ data, headers, onEdit, onSaveRow }: DimensionsViewProps) { + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(true); + const [syncing, setSyncing] = useState(null); + + const groups = useMemo(() => { + const groupMap = new Map(); + + data.forEach((row, index) => { + const iw = String(row[COLUMNS.INNER_W] || '0').trim(); + const il = String(row[COLUMNS.INNER_L] || '0').trim(); + const ih = String(row[COLUMNS.INNER_H] || '0').trim(); + + const key = `${il}x${iw}x${ih}`; + if (!groupMap.has(key)) { + groupMap.set(key, []); + } + groupMap.get(key)!.push({ row, index }); + }); + + const result: DimensionGroup[] = []; + groupMap.forEach((rows, key) => { + if (key === '0x0x0' || key === 'xx') return; // Skip empty/placeholder groups + + const first = rows[0].row; + const firstOuter = `${first[COLUMNS.OUTER_L]}x${first[COLUMNS.OUTER_W]}x${first[COLUMNS.OUTER_H]}`; + const firstUnits = String(first[COLUMNS.UNITS_OUTER]); + const firstMOQ = String(first[COLUMNS.MOQ]); + + let outerMatch = true; + let unitsMatch = true; + let moqMatch = true; + + rows.forEach(({ row }) => { + const outer = `${row[COLUMNS.OUTER_L]}x${row[COLUMNS.OUTER_W]}x${row[COLUMNS.OUTER_H]}`; + const units = String(row[COLUMNS.UNITS_OUTER]); + const moq = String(row[COLUMNS.MOQ]); + + if (outer !== firstOuter) outerMatch = false; + if (units !== firstUnits) unitsMatch = false; + if (moq !== firstMOQ) moqMatch = false; + }); + + result.push({ + key, + innerDims: key, + rows, + isInconsistent: !outerMatch || !unitsMatch || !moqMatch, + discrepancies: { + outer: !outerMatch, + units: !unitsMatch, + moq: !moqMatch + } + }); + }); + + return result.sort((a, b) => (b.isInconsistent ? 1 : 0) - (a.isInconsistent ? 1 : 0)); + }, [data]); + + const filteredGroups = useMemo(() => { + return showOnlyInconsistent ? groups.filter(g => g.isInconsistent) : groups; + }, [groups, showOnlyInconsistent]); + + const toggleGroup = (key: string) => { + const next = new Set(expandedGroups); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + setExpandedGroups(next); + }; + + const syncGroup = async (group: DimensionGroup) => { + if (!window.confirm(`This will apply the Packaging & MOQ values of product ${group.rows[0].row[COLUMNS.ARTICLE_NO]} to all ${group.rows.length} products in this group. Continue?`)) { + return; + } + + setSyncing(group.key); + try { + const first = group.rows[0].row; + const outerL = first[COLUMNS.OUTER_L]; + const outerW = first[COLUMNS.OUTER_W]; + const outerH = first[COLUMNS.OUTER_H]; + const unitsOuter = first[COLUMNS.UNITS_OUTER]; + const moq = first[COLUMNS.MOQ]; + + for (const { row, index } of group.rows) { + const updatedRow = [...row]; + updatedRow[COLUMNS.OUTER_L] = outerL; + updatedRow[COLUMNS.OUTER_W] = outerW; + updatedRow[COLUMNS.OUTER_H] = outerH; + updatedRow[COLUMNS.UNITS_OUTER] = unitsOuter; + updatedRow[COLUMNS.MOQ] = moq; + + await onSaveRow(index, updatedRow); + } + } finally { + setSyncing(null); + } + }; + + return ( +
+
+
+

+ + Dimension Consistency Check +

+

+ Grouping products by Inner Box dimensions to find Packaging or MOQ discrepancies. +

+
+
+ +
+ {groups.filter(g => g.isInconsistent).length} Inconsistencies found +
+
+
+ +
+ {filteredGroups.map(group => ( +
+
+ + +
+
+ {group.discrepancies.outer && ( + + Outer Dims vary + + )} + {group.discrepancies.units && ( + + Units/Outer vary + + )} + {group.discrepancies.moq && ( + + MOQ varies + + )} +
+ + {group.isInconsistent && ( + + )} +
+
+ + {expandedGroups.has(group.key) && ( +
+ + + + + + + + + + + + + {group.rows.map(({ row, index }) => ( + + + + + + + + + ))} + +
Article No / NameInner Box (L/W/H)Outer Box (L/W/H)Units/OuterMOQActions
+
{row[COLUMNS.ARTICLE_NO]}
+
{row[COLUMNS.ARTICLE_NAME]}
+
+ {row[COLUMNS.INNER_L]} × {row[COLUMNS.INNER_W]} × {row[COLUMNS.INNER_H]} + + {row[COLUMNS.OUTER_L]} × {row[COLUMNS.OUTER_W]} × {row[COLUMNS.OUTER_H]} + + {row[COLUMNS.UNITS_OUTER]} + + {row[COLUMNS.MOQ]} + + +
+
+ )} +
+ ))} + + {filteredGroups.length === 0 && ( +
+ +

Clear of discrepancies

+

+ {showOnlyInconsistent ? "No inconsistent groups found." : "No dimension data available."} +

+
+ )} +
+
+ ); +} diff --git a/src/components/EditPanel.tsx b/src/components/EditPanel.tsx index 4f3b4b2..b76c401 100644 --- a/src/components/EditPanel.tsx +++ b/src/components/EditPanel.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { ExcelRow, COLUMNS } from '../types'; -import { X, Sparkles, Save, Loader2, Languages } from 'lucide-react'; +import { X, Sparkles, Save, Loader2, Languages, Package } from 'lucide-react'; import { generateGemini } from '../services/gemini'; import { cn } from '../lib/utils'; @@ -17,17 +17,33 @@ export function EditPanel({ row, rowIndex, onSave, onClose }: EditPanelProps) { longEn: row[COLUMNS.LONG_EN] || '', shortDe: row[COLUMNS.SHORT_DE] || '', shortEn: row[COLUMNS.SHORT_EN] || '', + innerW: row[COLUMNS.INNER_W] || '', + innerL: row[COLUMNS.INNER_L] || '', + innerH: row[COLUMNS.INNER_H] || '', + outerW: row[COLUMNS.OUTER_W] || '', + outerL: row[COLUMNS.OUTER_L] || '', + outerH: row[COLUMNS.OUTER_H] || '', + unitsOuter: row[COLUMNS.UNITS_OUTER] || '', + moq: row[COLUMNS.MOQ] || '', }); const [loadingField, setLoadingField] = useState(null); const [error, setError] = useState(null); const isModified = (field: keyof typeof formData) => { - const colMap = { + const colMap: Record = { longDe: COLUMNS.LONG_DE, longEn: COLUMNS.LONG_EN, shortDe: COLUMNS.SHORT_DE, shortEn: COLUMNS.SHORT_EN, + innerW: COLUMNS.INNER_W, + innerL: COLUMNS.INNER_L, + innerH: COLUMNS.INNER_H, + outerW: COLUMNS.OUTER_W, + outerL: COLUMNS.OUTER_L, + outerH: COLUMNS.OUTER_H, + unitsOuter: COLUMNS.UNITS_OUTER, + moq: COLUMNS.MOQ, }; return formData[field] !== (row[colMap[field]] || ''); }; @@ -89,9 +105,33 @@ export function EditPanel({ row, rowIndex, onSave, onClose }: EditPanelProps) { newRow[COLUMNS.LONG_EN] = formData.longEn; newRow[COLUMNS.SHORT_DE] = formData.shortDe; newRow[COLUMNS.SHORT_EN] = formData.shortEn; + newRow[COLUMNS.INNER_W] = formData.innerW; + newRow[COLUMNS.INNER_L] = formData.innerL; + newRow[COLUMNS.INNER_H] = formData.innerH; + newRow[COLUMNS.OUTER_W] = formData.outerW; + newRow[COLUMNS.OUTER_L] = formData.outerL; + newRow[COLUMNS.OUTER_H] = formData.outerH; + newRow[COLUMNS.UNITS_OUTER] = formData.unitsOuter; + newRow[COLUMNS.MOQ] = formData.moq; onSave(rowIndex, newRow); }; + const DimensionInput = ({ label, field, placeholder }: { label: string, field: keyof typeof formData, placeholder?: string }) => ( +
+ + setFormData(prev => ({ ...prev, [field]: e.target.value }))} + placeholder={placeholder} + className={cn( + "w-full bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors", + isModified(field) ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500" + )} + /> +
+ ); + const FieldEditor = ({ title, field }: { title: string, field: keyof typeof formData }) => (
@@ -167,6 +207,31 @@ export function EditPanel({ row, rowIndex, onSave, onClose }: EditPanelProps) {
+
+

+ Dimensions & Packaging +

+ +
+
INNER BOX (L × W × H) cm
+ + + +
+ +
+
OUTER BOX (L × W × H) cm
+ + + +
+ +
+ + +
+
+ diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 61b565a..ea27642 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,16 +1,17 @@ import React from 'react'; -import { FileSpreadsheet, FileText, CheckSquare, Table } from 'lucide-react'; +import { FileSpreadsheet, FileText, CheckSquare, Table, Box } from 'lucide-react'; import { cn } from '../lib/utils'; interface SidebarProps { activeModule: string; - setActiveModule: (m: 'descriptions' | 'completeness' | 'matrix') => void; + setActiveModule: (m: 'descriptions' | 'completeness' | 'matrix' | 'dimensions') => void; } export function Sidebar({ activeModule, setActiveModule }: SidebarProps) { const navItems = [ { id: 'matrix', label: 'Matrix', icon: Table }, { id: 'descriptions', label: 'Product Descriptions', icon: FileText }, + { id: 'dimensions', label: 'Dimensions', icon: Box }, ] as const; return ( diff --git a/src/types.ts b/src/types.ts index 8f466f1..6b66776 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,5 +24,14 @@ export const COLUMNS = { SHORT_EN: 65, RECOMMENDED_AGE: 67, CLASSIFICATION: 11, // Column L (index 11) - ITEM_AVAILABLE: 14 // Column O (index 14) + ITEM_AVAILABLE: 14, // Column O (index 14) + MOQ: 27, + UNITS_INNER: 31, + UNITS_OUTER: 32, + INNER_W: 42, + INNER_L: 43, + INNER_H: 44, + OUTER_W: 47, + OUTER_L: 48, + OUTER_H: 49 };