import React, { useState, useMemo } from 'react'; import { ExcelRow, COLUMNS } from '../types'; import { Search, ChevronDown, ChevronUp, X, Edit2, Save } from 'lucide-react'; import { cn } from '../lib/utils'; import { ColumnFilterPopover } from './ColumnFilterPopover'; import { DateFilterPopover } from './DateFilterPopover'; interface MissingDataViewProps { data: ExcelRow[]; headers: string[]; onSaveRow: (rowIndex: number, updatedRow: ExcelRow) => void; onCaptureState: (message: string) => void; } function formatDateForInput(val: string): string { if (!val) return ''; // If it's already YYYY-MM-DD, return it if (/^\d{4}-\d{2}-\d{2}$/.test(val)) return val; // If it's DD/MM/YYYY, convert to YYYY-MM-DD const parts = val.split('/'); if (parts.length === 3) { return `${parts[2]}-${parts[1].padStart(2, '0')}-${parts[0].padStart(2, '0')}`; } return val; } function formatDateFromInput(val: string): string { // Always output YYYY-MM-DD as requested return val || ''; } function formatDateValue(val: any): string { if (val === null || val === undefined || val === '') return ''; if (typeof val === 'number') { if (val >= 25569 && val <= 60000) { const excelEpoch = new Date(1899, 11, 30); const date = new Date(excelEpoch.getTime() + val * 86400000); return date.toISOString().split('T')[0]; // Returns YYYY-MM-DD } return ''; } return formatDateForInput(String(val)); } function isEmptyOrEpoch(val: any): boolean { if (val === null || val === undefined || val === '') return true; if (typeof val === 'number') { if (val === 0 || val === 1) return true; if (val >= 25569 && val <= 60000) return false; return true; } const s = String(val).trim(); if (s === '' || s === '0' || s === '1') return true; if (s.endsWith('/1900')) return true; return false; } type TabType = 'missingLaunch'; interface EditingState { rowIndex: number; classification: string; launchDate: string; readyToOrder: string; } export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: MissingDataViewProps) { const [activeTab, setActiveTab] = useState('missingLaunch'); const [search, setSearch] = useState(''); const [sortCol, setSortCol] = useState(null); const [sortDesc, setSortDesc] = useState(false); const [page, setPage] = useState(1); const [editing, setEditing] = useState(null); const [columnFilters, setColumnFilters] = useState>({}); const [dateFilters, setDateFilters] = useState>({}); const [openFilter, setOpenFilter] = useState(null); const pageSize = 100; const launchDateCol = useMemo(() => { const idx = headers.findIndex(h => h.toLowerCase().includes('launch')); if (idx >= 0) return idx; return headers.findIndex(h => h.toLowerCase().includes('date')); }, [headers]); const readyToOrderCol = useMemo(() => { const idx = headers.findIndex(h => h.toLowerCase().includes('ready')); if (idx >= 0) return idx; return headers.findIndex(h => h.toLowerCase().includes('order')); }, [headers]); const launchHeader = launchDateCol >= 0 ? headers[launchDateCol] : 'Launch Date'; const readyHeader = readyToOrderCol >= 0 ? headers[readyToOrderCol] : 'Ready to Order'; const columns = [ { col: COLUMNS.ARTICLE_NO, label: 'SKU', width: 100 }, { col: COLUMNS.ARTICLE_NAME, label: 'Name', width: 220 }, { col: COLUMNS.CLASSIFICATION, label: 'Classification', width: 130 }, ...(launchDateCol >= 0 ? [{ col: launchDateCol, label: launchHeader, width: 130 }] : []), ...(readyToOrderCol >= 0 ? [{ col: readyToOrderCol, label: readyHeader, width: 130 }] : []), ]; const columnUniqueValues = useMemo(() => { const cols = columns.map(c => c.col); const result: Record> = {}; cols.forEach(col => result[col] = new Set()); data.forEach(row => { cols.forEach(col => { let val: any = row[col]; if (col === launchDateCol || col === readyToOrderCol) { val = formatDateValue(val) || String(val ?? ''); } else { val = String(val ?? ''); } if (val) result[col].add(val); }); }); return result; }, [data, columns, launchDateCol, readyToOrderCol]); const getUniqueValues = (col: number): string[] => { const values = columnUniqueValues[col]; if (!values) return []; return Array.from(values).sort() as string[]; }; const filteredData = useMemo(() => { let result = data.map((row, index) => ({ row, index })); if (activeTab === 'missingLaunch') { result = result.filter(r => { const val = launchDateCol >= 0 ? r.row[launchDateCol] : undefined; return isEmptyOrEpoch(val); }); } if (search) { const s = search.toLowerCase(); result = result.filter(r => String(r.row[COLUMNS.ARTICLE_NO] || '').toLowerCase().includes(s) || String(r.row[COLUMNS.ARTICLE_NAME] || '').toLowerCase().includes(s) ); } (Object.entries(columnFilters) as [string, string[]][]).forEach(([colIdx, filterValues]) => { if (!filterValues || filterValues.length === 0) return; const colIdxNum = parseInt(colIdx); if (colIdxNum === launchDateCol || colIdxNum === readyToOrderCol) return; result = result.filter(r => { const val: any = r.row[colIdxNum]; const displayVal = colIdxNum === launchDateCol || colIdxNum === readyToOrderCol ? formatDateValue(val) || String(val ?? '') : String(val ?? ''); return filterValues.includes(displayVal); }); }); (Object.entries(dateFilters) as [string, { start: string; end: string }][]).forEach(([colIdx, range]) => { if (!range.start && !range.end) return; const colIdxNum = parseInt(colIdx); result = result.filter(r => { const val = r.row[colIdxNum]; const dateStr = formatDateValue(val); if (!dateStr || !/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return false; const rowDate = new Date(dateStr); if (isNaN(rowDate.getTime())) return false; if (range.start) { const startDate = new Date(range.start); if (rowDate < startDate) return false; } if (range.end) { const endDate = new Date(range.end); if (rowDate > endDate) return false; } return true; }); }); 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, activeTab, search, sortCol, sortDesc, launchDateCol, columnFilters]); 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 openEdit = (rowIndex: number, row: ExcelRow) => { setEditing({ rowIndex, classification: String(row[COLUMNS.CLASSIFICATION] || ''), launchDate: launchDateCol >= 0 ? formatDateForInput(formatDateValue(row[launchDateCol]) || String(row[launchDateCol] ?? '')) : '', readyToOrder: readyToOrderCol >= 0 ? formatDateForInput(formatDateValue(row[readyToOrderCol]) || String(row[readyToOrderCol] ?? '')) : '', }); }; const handleSave = () => { if (!editing) return; const row = data[editing.rowIndex]; onCaptureState(`Updated product ${row[COLUMNS.ARTICLE_NO]}`); const newRow = [...row]; newRow[COLUMNS.CLASSIFICATION] = editing.classification; if (launchDateCol >= 0) newRow[launchDateCol] = formatDateFromInput(editing.launchDate); if (readyToOrderCol >= 0) newRow[readyToOrderCol] = formatDateFromInput(editing.readyToOrder); onSaveRow(editing.rowIndex, newRow); setEditing(null); }; const tabs: { id: TabType; label: string }[] = [ { id: 'missingLaunch', label: 'Missing Launch Date' }, ]; const editingRow = editing ? data[editing.rowIndex] : null; return (
{tabs.map(tab => ( ))}
{ 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 }) => { const selectedFilters = columnFilters[col] || []; const allValues = getUniqueValues(col); const filterCount = selectedFilters.length; return ( );})} {paginatedData.map(({ row, index }) => ( {launchDateCol >= 0 && ( )} {readyToOrderCol >= 0 && ( )} ))} {paginatedData.length === 0 && ( )}
handleSort(col)} > {label} {sortCol === col && ( sortDesc ? : )}
{col === launchDateCol || col === readyToOrderCol ? ( <> {openFilter === col && ( setDateFilters(prev => ({ ...prev, [col]: range }))} onClose={() => setOpenFilter(null)} /> )} ) : ( <> {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)} /> )} )}
{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}
{/* Focused edit panel */} {editing && editingRow && ( <>
setEditing(null)} />

Edit Fields

{editingRow[COLUMNS.ARTICLE_NO]} — {editingRow[COLUMNS.ARTICLE_NAME]}

{launchDateCol >= 0 && (
setEditing(prev => prev ? { ...prev, launchDate: e.target.value } : prev)} className={cn( "w-full bg-slate-900 border rounded-md px-3 py-2.5 text-sm text-white focus:outline-none focus:ring-1 transition-colors", editing.launchDate !== (formatDateValue(editingRow[launchDateCol]) || String(editingRow[launchDateCol] ?? '')) ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500 focus:ring-slate-500" )} />
)} {readyToOrderCol >= 0 && (
setEditing(prev => prev ? { ...prev, readyToOrder: e.target.value } : prev)} className={cn( "w-full bg-slate-900 border rounded-md px-3 py-2.5 text-sm text-white focus:outline-none focus:ring-1 transition-colors", editing.readyToOrder !== (formatDateValue(editingRow[readyToOrderCol]) || String(editingRow[readyToOrderCol] ?? '')) ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500 focus:ring-slate-500" )} />
)}
)}
); }