From 7eeccdaf8c11326b9ca2c487d3a99bc00e2d1a4a Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Sun, 12 Apr 2026 15:11:57 +0200 Subject: [PATCH] feat: add Missing Data module as new sidebar entry Replaces the tab approach with a dedicated 'Missing Data' module in the sidebar. The module has two sub-tabs: 'Missing Classification' and 'Missing Launch Date'. Shows SKU, Name, Classification, Launch Date and Ready to Order columns. Includes a local Excel serial date formatter so dates that slipped through the App.tsx pre-processing are rendered correctly instead of showing as raw numbers. Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 10 +- src/components/ArticleDetails.tsx | 126 ++++---------- src/components/MissingDataView.tsx | 267 +++++++++++++++++++++++++++++ src/components/Sidebar.tsx | 15 +- 4 files changed, 322 insertions(+), 96 deletions(-) create mode 100644 src/components/MissingDataView.tsx diff --git a/src/App.tsx b/src/App.tsx index eac919b..959c83d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import { ArticleDetails } from './components/ArticleDetails'; import { HistoryView } from './components/HistoryView'; import { UndoToast } from './components/UndoToast'; import { PendingValidationView } from './components/PendingValidationView'; +import { MissingDataView } from './components/MissingDataView'; export default function App() { const [session, setSession] = useState(() => getStoredSession()); @@ -27,7 +28,7 @@ export default function App() { hasUnsavedChanges: false, asinColumnIndex: null }); - const [activeModule, setActiveModule] = useState<'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history'>('descriptions'); + const [activeModule, setActiveModule] = useState<'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data'>('descriptions'); const [undoHistory, setUndoHistory] = useState<{ data: ExcelRow[], message: string }[]>([]); const [editingRowIndex, setEditingRowIndex] = useState(null); const [isLoadingDefault, setIsLoadingDefault] = useState(true); @@ -548,7 +549,6 @@ export default function App() { {activeModule === 'article_details' && ( setEditingRowIndex(index)} rowStatuses={rowStatuses} /> @@ -562,6 +562,12 @@ export default function App() { onEdit={(index) => setEditingRowIndex(index)} /> )} + {activeModule === 'missing_data' && ( + + )} {activeModule === 'history' && ( void; rowStatuses: Record; } -type TabType = 'all' | 'missingDetailsDE' | 'missingDetailsEN' | 'missingAnyDetails' | 'lowStock' | 'missingClassOrLaunch'; +type TabType = 'all' | 'missingDetailsDE' | 'missingDetailsEN' | 'missingAnyDetails' | 'lowStock'; -function isEmptyLaunchDate(val: any): boolean { - if (val === null || val === undefined || val === '') return true; - const s = String(val).trim(); - if (s === '' || s === '0' || s === '1') return true; - // "00/01/1900" or "01/01/1900" variants - if (s.endsWith('/1900')) return true; - return false; -} - -export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDetailsProps) { +export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProps) { const [activeTab, setActiveTab] = useState('all'); const [search, setSearch] = useState(''); const [lineFilter, setLineFilter] = useState(''); @@ -32,12 +22,6 @@ export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDe const [page, setPage] = useState(1); const [columnFilters, setColumnFilters] = useState>({}); const [openFilterCol, setOpenFilterCol] = useState(null); - - const launchDateCol = useMemo(() => - headers.findIndex(h => h.toLowerCase().includes('launch')), [headers]); - const readyToOrderCol = useMemo(() => - headers.findIndex(h => h.toLowerCase().includes('ready')), [headers]); - const [columnWidths, setColumnWidths] = useState>({ [COLUMNS.ARTICLE_NO]: 100, [COLUMNS.ARTICLE_NAME]: 200, @@ -57,16 +41,11 @@ export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDe if (activeTab === 'missingDetailsEN') result = result.filter(r => !r.row[COLUMNS.DETAILS_EN]); if (activeTab === 'missingAnyDetails') result = result.filter(r => !r.row[COLUMNS.DETAILS_DE] || !r.row[COLUMNS.DETAILS_EN]); if (activeTab === 'lowStock') result = result.filter(r => Number(r.row[COLUMNS.ITEM_AVAILABLE] || 0) <= 0); - if (activeTab === 'missingClassOrLaunch') result = result.filter(r => { - const missingClass = !r.row[COLUMNS.CLASSIFICATION] || String(r.row[COLUMNS.CLASSIFICATION]).trim() === ''; - const missingLaunch = launchDateCol >= 0 ? isEmptyLaunchDate(r.row[launchDateCol]) : false; - return missingClass || missingLaunch; - }); // Search filter if (search) { const s = search.toLowerCase(); - result = result.filter(r => + result = result.filter(r => String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) || String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) ); @@ -88,11 +67,11 @@ export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDe 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); @@ -160,7 +139,7 @@ export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDe const getBadge = (val: any, type: 'success' | 'warning' | 'error' | 'info' = 'info') => { if (!val) return Empty; - + const styles = { success: "bg-green-500/10 text-green-400 border-green-500/20", warning: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20", @@ -177,7 +156,6 @@ export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDe { id: 'missingDetailsEN', label: 'No Details EN' }, { id: 'missingAnyDetails', label: 'Missing Details' }, { id: 'lowStock', label: 'Out of Stock' }, - { id: 'missingClassOrLaunch', label: 'Missing Class / Launch' }, ]; return ( @@ -189,8 +167,8 @@ export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDe onClick={() => { setActiveTab(tab.id); setPage(1); }} className={cn( "px-4 py-2 rounded-md text-sm font-medium transition-colors", - activeTab === tab.id - ? "bg-indigo-600 text-white shadow-md" + activeTab === tab.id + ? "bg-indigo-600 text-white shadow-md" : "bg-slate-800 text-slate-400 hover:bg-slate-700 hover:text-white" )} > @@ -248,18 +226,13 @@ export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDe {[ { col: COLUMNS.ARTICLE_NO, label: 'SKU' }, { col: COLUMNS.ARTICLE_NAME, label: 'Name' }, - { col: COLUMNS.CLASSIFICATION, label: 'Classification' }, - ...(activeTab === 'missingClassOrLaunch' ? [ - ...(launchDateCol >= 0 ? [{ col: launchDateCol, label: headers[launchDateCol] || 'Launch Date' }] : []), - ...(readyToOrderCol >= 0 ? [{ col: readyToOrderCol, label: headers[readyToOrderCol] || 'Ready to Order' }] : []), - ] : [ - { col: COLUMNS.ITEM_AVAILABLE, label: 'Stock' }, - { col: COLUMNS.DETAILS_DE, label: 'Details DE' }, - { col: COLUMNS.DETAILS_EN, label: 'Details EN' }, - ]), + { col: COLUMNS.CLASSIFICATION, label: 'Class' }, + { col: COLUMNS.ITEM_AVAILABLE, label: 'Stock' }, + { col: COLUMNS.DETAILS_DE, label: 'Details DE' }, + { col: COLUMNS.DETAILS_EN, label: 'Details EN' }, ].map(({ col, label }) => ( - @@ -329,54 +302,31 @@ export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDe {row[COLUMNS.ARTICLE_NAME]} - {row[COLUMNS.CLASSIFICATION] ? ( - - {row[COLUMNS.CLASSIFICATION]} - + + {row[COLUMNS.CLASSIFICATION] || '—'} + + + + + {row[COLUMNS.ITEM_AVAILABLE] || 0} + + + + {row[COLUMNS.DETAILS_DE] ? ( +
{row[COLUMNS.DETAILS_DE]}
+ ) : getBadge(null)} + + + {row[COLUMNS.DETAILS_EN] ? ( +
{row[COLUMNS.DETAILS_EN]}
) : getBadge(null)} - {activeTab === 'missingClassOrLaunch' ? ( - <> - {launchDateCol >= 0 && ( - - {isEmptyLaunchDate(row[launchDateCol]) ? getBadge(null) : ( - {String(row[launchDateCol])} - )} - - )} - {readyToOrderCol >= 0 && ( - - {row[readyToOrderCol] !== null && row[readyToOrderCol] !== undefined && row[readyToOrderCol] !== '' ? ( - {String(row[readyToOrderCol])} - ) : getBadge(null)} - - )} - - ) : ( - <> - - - {row[COLUMNS.ITEM_AVAILABLE] || 0} - - - - {row[COLUMNS.DETAILS_DE] ? ( -
{row[COLUMNS.DETAILS_DE]}
- ) : getBadge(null)} - - - {row[COLUMNS.DETAILS_EN] ? ( -
{row[COLUMNS.DETAILS_EN]}
- ) : getBadge(null)} - - - )} + ))} + + +
+
+ + { 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 +
+
+ +
+
+ + + + {columns.map(({ col, label, width }) => ( + + ))} + + + + {paginatedData.map(({ row, index }) => ( + + + + + {launchDateCol >= 0 && ( + + )} + {readyToOrderCol >= 0 && ( + + )} + + ))} + {paginatedData.length === 0 && ( + + + + )} + +
handleSort(col)} + > + + {label} + {sortCol === col && ( + sortDesc ? : + )} + +
+ {row[COLUMNS.ARTICLE_NO]} + + {row[COLUMNS.ARTICLE_NAME]} + + {row[COLUMNS.CLASSIFICATION] && String(row[COLUMNS.CLASSIFICATION]).trim() !== '' ? ( + + {row[COLUMNS.CLASSIFICATION]} + + ) : ( + Empty + )} + + {isEmptyOrEpoch(row[launchDateCol]) ? ( + Empty + ) : ( + formatDateValue(row[launchDateCol]) || String(row[launchDateCol]) + )} + + {row[readyToOrderCol] !== null && row[readyToOrderCol] !== undefined && row[readyToOrderCol] !== '' ? ( + formatDateValue(row[readyToOrderCol]) || String(row[readyToOrderCol]) + ) : ( + + )} +
+ No items found. +
+
+ +
+
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 f3a6b68..aac2595 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,27 +1,30 @@ import React from 'react'; -import { FileText, Table, Box, DollarSign, Package, Clock, History } from 'lucide-react'; +import { FileText, Table, Box, DollarSign, Package, Clock, History, AlertTriangle } from 'lucide-react'; import { cn } from '../lib/utils'; interface SidebarProps { activeModule: string; - setActiveModule: (m: 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history') => void; + setActiveModule: (m: 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data') => void; userEmail: string; } export function Sidebar({ activeModule, setActiveModule, userEmail }: SidebarProps) { const isMasterUser = userEmail?.toLowerCase() === 'christian.vidal@craze-group.com'; - const navItems = [ + type ModuleId = 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data'; + + const navItems: { id: ModuleId; label: string; icon: React.ElementType }[] = [ { id: 'matrix', label: 'Matrix', icon: Table }, { id: 'descriptions', label: 'Product Descriptions', icon: FileText }, { id: 'article_details', label: 'Article Details', icon: Package }, { id: 'dimensions', label: 'Dimensions', icon: Box }, { id: 'pricing', label: 'Pricing & Units', icon: DollarSign }, + { id: 'missing_data', label: 'Missing Data', icon: AlertTriangle }, ...(isMasterUser ? [ - { id: 'pending_validation', label: 'Pending Validation', icon: Clock }, - { id: 'history', label: 'Change History', icon: History } + { id: 'pending_validation' as ModuleId, label: 'Pending Validation', icon: Clock }, + { id: 'history' as ModuleId, label: 'Change History', icon: History } ] : []) - ] as const; + ]; return (