diff --git a/src/App.tsx b/src/App.tsx index 610742b..b5d82ed 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import { HistoryView } from './components/HistoryView'; import { UndoToast } from './components/UndoToast'; import { PendingValidationView } from './components/PendingValidationView'; import { MissingDataView } from './components/MissingDataView'; +import { CosmeticItemsView } from './components/CosmeticItemsView'; const FORCED_ZERO_STOCK_SKUS = new Set([ '11631VC', '1237VC', '1238VC', '1652VC', '1653VC', '1684VC', '1688VC', '1717VC', @@ -60,7 +61,7 @@ export default function App() { hasUnsavedChanges: false, asinColumnIndex: null }); - const [activeModule, setActiveModule] = useState<'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data'>('descriptions'); + const [activeModule, setActiveModule] = useState<'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data' | 'cosmetic_items'>('descriptions'); const [undoHistory, setUndoHistory] = useState<{ data: ExcelRow[], message: string }[]>([]); const [editingRowIndex, setEditingRowIndex] = useState(null); const [isLoadingDefault, setIsLoadingDefault] = useState(true); @@ -179,6 +180,7 @@ export default function App() { [COLUMNS.ITEM_TO_LOGISTIC]: 'Item to Logistic', [COLUMNS.ANNA_CHECK]: 'Anna Check', [COLUMNS.ANNA_NOTE]: 'Anna Note', + [COLUMNS.CPNP_NO]: 'CPNP No.', }; Object.entries(VIRTUAL_COLS).forEach(([idxStr, name]) => { const idx = Number(idxStr); @@ -204,7 +206,8 @@ export default function App() { resolvedCols.VALIDATED_CHECK, resolvedCols.VALIDATED_NOTE, resolvedCols.PRODUCT_TYPE, - resolvedCols.ITEM_TO_LOGISTIC + resolvedCols.ITEM_TO_LOGISTIC, + resolvedCols.CPNP_NO ]); headers.forEach((h: any, i: number) => { @@ -787,6 +790,15 @@ const articleNoIdx = resolvedCols.ARTICLE_NO; onCaptureState={captureState} /> )} + {activeModule === 'cosmetic_items' && ( + + )} {activeModule === 'history' && ( void; + onCaptureState: (message: string) => void; + rowStatuses: Record; +} + +export function CosmeticItemsView({ data, headers, onSaveRow, onCaptureState, rowStatuses }: CosmeticItemsViewProps) { + const COLUMNS = useColumns(); + const [search, setSearch] = usePersistentState('cosmeticItems-search', ''); + const [sortCol, setSortCol] = usePersistentState('cosmeticItems-sortCol', null); + const [sortDesc, setSortDesc] = usePersistentState('cosmeticItems-sortDesc', false); + const [page, setPage] = useState(1); + const [columnFilters, setColumnFilters] = usePersistentState>('cosmeticItems-columnFilters', {}); + const [openFilter, setOpenFilter] = useState(null); + const [editingCpnp, setEditingCpnp] = useState<{ rowIndex: number; value: string } | null>(null); + + const pageSize = 100; + + const columns = [ + { col: COLUMNS.ARTICLE_NO, label: 'SKU', width: 110 }, + { col: COLUMNS.ARTICLE_NAME, label: 'Name', width: 240 }, + { col: COLUMNS.LINE, label: 'Line', width: 120 }, + { col: COLUMNS.CLASSIFICATION, label: 'Classification', width: 130 }, + { col: COLUMNS.ITEM_AVAILABLE, label: 'Item Available', width: 120 }, + { col: COLUMNS.CPNP_NO, label: 'CPNP No.', width: 160 }, + ]; + + const columnUniqueValues = useMemo(() => { + const result: Record> = {}; + columns.forEach(c => { result[c.col] = new Set(); }); + + data.forEach(row => { + const lineVal = String(row[COLUMNS.LINE] ?? '').trim().toUpperCase(); + const isCosmeticLine = COSMETIC_LINES.some(l => lineVal === l); + if (!isCosmeticLine) return; + + columns.forEach(({ col }) => { + result[col].add(String(row[col] ?? '')); + }); + }); + + return result; + }, [data, COLUMNS]); + + const getUniqueValues = (col: number): string[] => + Array.from(columnUniqueValues[col] ?? []).sort(); + + const filteredData = useMemo(() => { + let result = data.map((row, index) => ({ row, index })); + + // Filter: only cosmetic lines + result = result.filter(({ row }) => { + const lineVal = String(row[COLUMNS.LINE] ?? '').trim().toUpperCase(); + return COSMETIC_LINES.some(l => lineVal === l); + }); + + // Filter: CPNP No. is empty + result = result.filter(({ row }) => { + const cpnp = row[COLUMNS.CPNP_NO]; + return cpnp === null || cpnp === undefined || String(cpnp).trim() === ''; + }); + + // Global search + if (search) { + const terms = search.toLowerCase().split(/\s+/).filter(Boolean); + result = result.filter(({ row }) => { + const articleNo = String(row[COLUMNS.ARTICLE_NO] || '').toLowerCase(); + const articleName = String(row[COLUMNS.ARTICLE_NAME] || '').toLowerCase(); + return terms.every(t => articleNo.includes(t) || articleName.includes(t)); + }); + } + + // Column filters + (Object.entries(columnFilters) as [string, string[]][]).forEach(([colIdx, filterValues]) => { + if (!filterValues || filterValues.length === 0) return; + const colNum = parseInt(colIdx); + result = result.filter(({ row }) => { + const val = String(row[colNum] ?? ''); + return filterValues.includes(val); + }); + }); + + // Sort + if (sortCol !== null) { + result.sort((a, b) => { + const valA = a.row[sortCol]; + const valB = b.row[sortCol]; + if (typeof valA === 'number' && typeof valB === 'number') { + return sortDesc ? valB - valA : valA - valB; + } + const sA = String(valA ?? ''); + const sB = String(valB ?? ''); + return sortDesc ? sB.localeCompare(sA) : sA.localeCompare(sB); + }); + } + + return result; + }, [data, search, sortCol, sortDesc, columnFilters, COLUMNS]); + + const paginatedData = useMemo(() => { + const start = (page - 1) * pageSize; + return filteredData.slice(start, start + pageSize); + }, [filteredData, page]); + + const totalPages = Math.ceil(filteredData.length / pageSize); + + const handleSort = (col: number) => { + if (sortCol === col) setSortDesc(d => !d); + else { setSortCol(col); setSortDesc(false); } + }; + + const startEditCpnp = (rowIndex: number, currentValue: any) => { + setEditingCpnp({ rowIndex, value: String(currentValue ?? '') }); + }; + + const saveCpnp = (rowIndex: number) => { + if (!editingCpnp || editingCpnp.rowIndex !== rowIndex) return; + const row = data[rowIndex]; + onCaptureState(`Updated CPNP No. for ${row[COLUMNS.ARTICLE_NO]}`); + const newRow = [...row]; + newRow[COLUMNS.CPNP_NO] = editingCpnp.value.trim(); + onSaveRow(rowIndex, newRow); + setEditingCpnp(null); + }; + + const cancelEditCpnp = () => setEditingCpnp(null); + + const lineBadgeColor = (line: string) => { + const l = String(line).toUpperCase(); + if (l === 'INKEE') return 'bg-pink-500/10 text-pink-400 border-pink-500/20'; + if (l === 'BATH FUN') return 'bg-cyan-500/10 text-cyan-400 border-cyan-500/20'; + if (l === 'TOP FASHION') return 'bg-purple-500/10 text-purple-400 border-purple-500/20'; + if (l === 'SENSES') return 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'; + return 'bg-slate-700/50 text-slate-400 border-slate-600/50'; + }; + + return ( +
+
+
+ + { setSearch(e.target.value); setPage(1); }} + className="w-full pl-9 pr-10 py-2 bg-slate-900/50 border border-slate-700 rounded-md text-sm text-white focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" + /> + {search && ( + + )} +
+
+ {filteredData.length} items + · + Lines: {COSMETIC_LINES.join(', ')} +
+
+ +
+
+ + + + {columns.map(({ col, label, width }) => { + const selectedFilters = columnFilters[col] || []; + const filterCount = selectedFilters.length; + const isCpnp = col === COLUMNS.CPNP_NO; + return ( + + ); + })} + + + + {paginatedData.map(({ row, index }) => { + const articleNo = String(row[COLUMNS.ARTICLE_NO] ?? ''); + const status = rowStatuses[articleNo]; + const isEditing = editingCpnp?.rowIndex === index; + + return ( + + + + + + + + + ); + })} + {paginatedData.length === 0 && ( + + + + )} + +
+
!isCpnp && handleSort(col)} + > + {label} + {!isCpnp && sortCol === col && ( + sortDesc ? : + )} + {isCpnp && ( + (editable) + )} +
+ {!isCpnp && ( +
+ + {openFilter === col && ( + setColumnFilters(prev => { + const current = prev[col] || []; + if (current.includes(val)) { + return { ...prev, [col]: current.filter(v => v !== val) }; + } + return { ...prev, [col]: [...current, val] }; + })} + onSelectAll={(vals) => setColumnFilters(prev => ({ ...prev, [col]: vals }))} + onClear={() => { setColumnFilters(prev => { const n = { ...prev }; delete n[col]; return n; }); }} + onClose={() => setOpenFilter(null)} + /> + )} +
+ )} +
+ {articleNo} + + {row[COLUMNS.ARTICLE_NAME]} + + + {row[COLUMNS.LINE]} + + + {row[COLUMNS.CLASSIFICATION] || } + + {row[COLUMNS.ITEM_AVAILABLE]} + + {isEditing ? ( +
+ setEditingCpnp(prev => prev ? { ...prev, value: e.target.value } : prev)} + onKeyDown={e => { + if (e.key === 'Enter') saveCpnp(index); + if (e.key === 'Escape') cancelEditCpnp(); + }} + className="flex-1 min-w-0 bg-slate-900 border border-indigo-500 rounded px-2 py-1 text-xs text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" + placeholder="Enter CPNP No." + /> + + +
+ ) : ( + + )} +
+ No cosmetic items pending CPNP registration. +
+
+ +
+
Showing {paginatedData.length} of {filteredData.length} items
+
+ + Page {page} of {totalPages || 1} + +
+
+
+
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index aac2595..b9a7741 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,17 +1,17 @@ import React from 'react'; -import { FileText, Table, Box, DollarSign, Package, Clock, History, AlertTriangle } from 'lucide-react'; +import { FileText, Table, Box, DollarSign, Package, Clock, History, AlertTriangle, Sparkles } from 'lucide-react'; import { cn } from '../lib/utils'; interface SidebarProps { activeModule: string; - setActiveModule: (m: 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data') => void; + setActiveModule: (m: 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data' | 'cosmetic_items') => void; userEmail: string; } export function Sidebar({ activeModule, setActiveModule, userEmail }: SidebarProps) { const isMasterUser = userEmail?.toLowerCase() === 'christian.vidal@craze-group.com'; - type ModuleId = 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data'; + type ModuleId = 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data' | 'cosmetic_items'; const navItems: { id: ModuleId; label: string; icon: React.ElementType }[] = [ { id: 'matrix', label: 'Matrix', icon: Table }, @@ -20,6 +20,7 @@ export function Sidebar({ activeModule, setActiveModule, userEmail }: SidebarPro { id: 'dimensions', label: 'Dimensions', icon: Box }, { id: 'pricing', label: 'Pricing & Units', icon: DollarSign }, { id: 'missing_data', label: 'Missing Data', icon: AlertTriangle }, + { id: 'cosmetic_items', label: 'Cosmetic Items', icon: Sparkles }, ...(isMasterUser ? [ { id: 'pending_validation' as ModuleId, label: 'Pending Validation', icon: Clock }, { id: 'history' as ModuleId, label: 'Change History', icon: History } diff --git a/src/types.ts b/src/types.ts index 92ea359..19fffd3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,7 +43,8 @@ export const COLUMNS = { PRODUCT_TYPE: 103, ITEM_TO_LOGISTIC: 104, ANNA_CHECK: 105, - ANNA_NOTE: 106 + ANNA_NOTE: 106, + CPNP_NO: 107 }; // Search patterns for dynamic detection @@ -81,6 +82,7 @@ export const COLUMN_PATTERNS: Record = { ITEM_TO_LOGISTIC: ['item', 'logistic'], ANNA_CHECK: ['anna', 'check'], ANNA_NOTE: ['anna', 'note'], + CPNP_NO: ['cpnp'], // Virtual/Extra columns stay hardcoded and are NOT auto-detected from headers // to prevent internal data from being shifted or overwritten by Excel column shifts. };