feat: add Missing Class / Launch tab to Article Details

Adds a new 'Missing Class / Launch' tab in Article Details that surfaces
items where Classification is empty or Launch Date is blank/01-01-1900.
The tab shows Classification, Launch Date, and Ready to Order columns for
quick reference. Column indices for Launch Date and Ready to Order are
resolved dynamically from the Excel headers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-04-12 15:02:52 +02:00
co-authored by Claude Sonnet 4.6
parent d1f1c9a467
commit fdf878a4f8
2 changed files with 80 additions and 29 deletions
+1
View File
@@ -548,6 +548,7 @@ export default function App() {
{activeModule === 'article_details' && (
<ArticleDetails
data={appState.data}
headers={appState.headers}
onEdit={(index) => setEditingRowIndex(index)}
rowStatuses={rowStatuses}
/>
+54 -4
View File
@@ -6,13 +6,23 @@ import { ColumnFilterPopover } from './ColumnFilterPopover';
interface ArticleDetailsProps {
data: ExcelRow[];
headers: string[];
onEdit: (index: number) => void;
rowStatuses: Record<string, string>;
}
type TabType = 'all' | 'missingDetailsDE' | 'missingDetailsEN' | 'missingAnyDetails' | 'lowStock';
type TabType = 'all' | 'missingDetailsDE' | 'missingDetailsEN' | 'missingAnyDetails' | 'lowStock' | 'missingClassOrLaunch';
export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProps) {
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) {
const [activeTab, setActiveTab] = useState<TabType>('all');
const [search, setSearch] = useState('');
const [lineFilter, setLineFilter] = useState('');
@@ -22,6 +32,12 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
const [page, setPage] = useState(1);
const [columnFilters, setColumnFilters] = useState<Record<number, string[]>>({});
const [openFilterCol, setOpenFilterCol] = useState<number | null>(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<Record<number, number>>({
[COLUMNS.ARTICLE_NO]: 100,
[COLUMNS.ARTICLE_NAME]: 200,
@@ -41,6 +57,11 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
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) {
@@ -156,6 +177,7 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
{ id: 'missingDetailsEN', label: 'No Details EN' },
{ id: 'missingAnyDetails', label: 'Missing Details' },
{ id: 'lowStock', label: 'Out of Stock' },
{ id: 'missingClassOrLaunch', label: 'Missing Class / Launch' },
];
return (
@@ -226,10 +248,15 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
{[
{ col: COLUMNS.ARTICLE_NO, label: 'SKU' },
{ col: COLUMNS.ARTICLE_NAME, label: 'Name' },
{ col: COLUMNS.CLASSIFICATION, label: 'Class' },
{ 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' },
]),
].map(({ col, label }) => (
<th
key={col}
@@ -302,13 +329,34 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
{row[COLUMNS.ARTICLE_NAME]}
</td>
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.CLASSIFICATION] }}>
{row[COLUMNS.CLASSIFICATION] ? (
<span className={cn(
"px-1.5 py-0.5 rounded-[4px] text-[10px] font-bold border",
String(row[COLUMNS.CLASSIFICATION]).includes('OOC') ? "bg-amber-500/10 text-amber-500 border-amber-500/20" : "bg-slate-700/50 text-slate-400 border-slate-600/50"
)}>
{row[COLUMNS.CLASSIFICATION] || '—'}
{row[COLUMNS.CLASSIFICATION]}
</span>
) : getBadge(null)}
</td>
{activeTab === 'missingClassOrLaunch' ? (
<>
{launchDateCol >= 0 && (
<td className="px-3 py-2 truncate">
{isEmptyLaunchDate(row[launchDateCol]) ? getBadge(null) : (
<span className="font-mono text-slate-300">{String(row[launchDateCol])}</span>
)}
</td>
)}
{readyToOrderCol >= 0 && (
<td className="px-3 py-2 truncate">
{row[readyToOrderCol] !== null && row[readyToOrderCol] !== undefined && row[readyToOrderCol] !== '' ? (
<span className="font-mono text-slate-300">{String(row[readyToOrderCol])}</span>
) : getBadge(null)}
</td>
)}
</>
) : (
<>
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.ITEM_AVAILABLE] }}>
<span className={cn(
"font-mono font-bold",
@@ -327,6 +375,8 @@ export function ArticleDetails({ data, onEdit, rowStatuses }: ArticleDetailsProp
<div className="truncate text-slate-400" title={row[COLUMNS.DETAILS_EN]}>{row[COLUMNS.DETAILS_EN]}</div>
) : getBadge(null)}
</td>
</>
)}
<td className="px-3 py-2 text-right">
<button
onClick={() => onEdit(index)}