mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 13:55:23 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
fdf878a4f8
commit
7eeccdaf8c
+8
-2
@@ -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<AuthSession | null>(() => 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<number | null>(null);
|
||||
const [isLoadingDefault, setIsLoadingDefault] = useState(true);
|
||||
@@ -548,7 +549,6 @@ export default function App() {
|
||||
{activeModule === 'article_details' && (
|
||||
<ArticleDetails
|
||||
data={appState.data}
|
||||
headers={appState.headers}
|
||||
onEdit={(index) => setEditingRowIndex(index)}
|
||||
rowStatuses={rowStatuses}
|
||||
/>
|
||||
@@ -562,6 +562,12 @@ export default function App() {
|
||||
onEdit={(index) => setEditingRowIndex(index)}
|
||||
/>
|
||||
)}
|
||||
{activeModule === 'missing_data' && (
|
||||
<MissingDataView
|
||||
data={appState.data}
|
||||
headers={appState.headers}
|
||||
/>
|
||||
)}
|
||||
{activeModule === 'history' && (
|
||||
<HistoryView
|
||||
headers={appState.headers}
|
||||
|
||||
@@ -6,23 +6,13 @@ 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' | '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<TabType>('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<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,
|
||||
@@ -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 <span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-red-500/10 text-red-400 border border-red-500/20">Empty</span>;
|
||||
|
||||
|
||||
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 }) => (
|
||||
<th
|
||||
key={col}
|
||||
<th
|
||||
key={col}
|
||||
className="px-3 py-3 font-medium transition-colors select-none group relative border-r border-slate-700/30"
|
||||
style={{ width: columnWidths[col] || 'auto', minWidth: columnWidths[col] || 'auto' }}
|
||||
>
|
||||
@@ -329,54 +302,31 @@ export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDe
|
||||
{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]}
|
||||
</span>
|
||||
<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] || '—'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.ITEM_AVAILABLE] }}>
|
||||
<span className={cn(
|
||||
"font-mono font-bold",
|
||||
Number(row[COLUMNS.ITEM_AVAILABLE] || 0) <= 0 ? "text-red-400" : "text-emerald-400"
|
||||
)}>
|
||||
{row[COLUMNS.ITEM_AVAILABLE] || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.DETAILS_DE] }}>
|
||||
{row[COLUMNS.DETAILS_DE] ? (
|
||||
<div className="truncate text-slate-400" title={row[COLUMNS.DETAILS_DE]}>{row[COLUMNS.DETAILS_DE]}</div>
|
||||
) : getBadge(null)}
|
||||
</td>
|
||||
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.DETAILS_EN] }}>
|
||||
{row[COLUMNS.DETAILS_EN] ? (
|
||||
<div className="truncate text-slate-400" title={row[COLUMNS.DETAILS_EN]}>{row[COLUMNS.DETAILS_EN]}</div>
|
||||
) : 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",
|
||||
Number(row[COLUMNS.ITEM_AVAILABLE] || 0) <= 0 ? "text-red-400" : "text-emerald-400"
|
||||
)}>
|
||||
{row[COLUMNS.ITEM_AVAILABLE] || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.DETAILS_DE] }}>
|
||||
{row[COLUMNS.DETAILS_DE] ? (
|
||||
<div className="truncate text-slate-400" title={row[COLUMNS.DETAILS_DE]}>{row[COLUMNS.DETAILS_DE]}</div>
|
||||
) : getBadge(null)}
|
||||
</td>
|
||||
<td className="px-3 py-2 truncate" style={{ width: columnWidths[COLUMNS.DETAILS_EN] }}>
|
||||
{row[COLUMNS.DETAILS_EN] ? (
|
||||
<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)}
|
||||
@@ -398,7 +348,7 @@ export function ArticleDetails({ data, headers, onEdit, rowStatuses }: ArticleDe
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-slate-900 border-t border-slate-700 p-4 flex items-center justify-between text-xs text-slate-500">
|
||||
<div>Showing {paginatedData.length} of {filteredData.length} articles</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { ExcelRow, COLUMNS } from '../types';
|
||||
import { Search, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
interface MissingDataViewProps {
|
||||
data: ExcelRow[];
|
||||
headers: string[];
|
||||
}
|
||||
|
||||
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.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return 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) {
|
||||
// valid date range — not empty
|
||||
return false;
|
||||
}
|
||||
// number outside date range — treat as empty
|
||||
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 = 'missingClass' | 'missingLaunch';
|
||||
|
||||
export function MissingDataView({ data, headers }: MissingDataViewProps) {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('missingClass');
|
||||
const [search, setSearch] = useState('');
|
||||
const [sortCol, setSortCol] = useState<number | null>(null);
|
||||
const [sortDesc, setSortDesc] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 100;
|
||||
|
||||
const launchDateCol = useMemo(() =>
|
||||
headers.findIndex(h => h.toLowerCase().includes('launch')), [headers]);
|
||||
const readyToOrderCol = useMemo(() =>
|
||||
headers.findIndex(h => h.toLowerCase().includes('ready')), [headers]);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
let result = data.map((row, index) => ({ row, index }));
|
||||
|
||||
if (activeTab === 'missingClass') {
|
||||
result = result.filter(r => {
|
||||
const val = r.row[COLUMNS.CLASSIFICATION];
|
||||
return !val || String(val).trim() === '';
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
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 tabs: { id: TabType; label: string }[] = [
|
||||
{ id: 'missingClass', label: 'Missing Classification' },
|
||||
{ id: 'missingLaunch', label: 'Missing Launch Date' },
|
||||
];
|
||||
|
||||
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 }] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
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"
|
||||
: "bg-slate-800 text-slate-400 hover:bg-slate-700 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 mb-6 bg-slate-800/50 p-4 rounded-xl border border-slate-700/50">
|
||||
<div className="flex-1 min-w-[200px] relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search SKU or Name..."
|
||||
value={search}
|
||||
onChange={e => { 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 && (
|
||||
<button
|
||||
onClick={() => { setSearch(''); setPage(1); }}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center text-xs text-slate-500">
|
||||
{filteredData.length} items
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 bg-slate-800 rounded-xl border border-slate-700 shadow-xl overflow-hidden flex flex-col">
|
||||
<div className="overflow-x-auto flex-1">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead className="bg-slate-900/80 text-slate-400 sticky top-0 z-10">
|
||||
<tr>
|
||||
{columns.map(({ col, label, width }) => (
|
||||
<th
|
||||
key={col}
|
||||
style={{ width, minWidth: width }}
|
||||
className="px-3 py-3 font-medium border-r border-slate-700/30 select-none cursor-pointer hover:text-white"
|
||||
onClick={() => handleSort(col)}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
{label}
|
||||
{sortCol === col && (
|
||||
sortDesc ? <ChevronDown className="w-3 h-3" /> : <ChevronUp className="w-3 h-3" />
|
||||
)}
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-700/30">
|
||||
{paginatedData.map(({ row, index }) => (
|
||||
<tr key={index} className="hover:bg-slate-700/20 transition-colors">
|
||||
<td className="px-3 py-2 font-mono text-indigo-400 truncate" style={{ width: 100 }}>
|
||||
{row[COLUMNS.ARTICLE_NO]}
|
||||
</td>
|
||||
<td className="px-3 py-2 font-medium text-slate-200 truncate" style={{ width: 220 }} title={row[COLUMNS.ARTICLE_NAME]}>
|
||||
{row[COLUMNS.ARTICLE_NAME]}
|
||||
</td>
|
||||
<td className="px-3 py-2 truncate" style={{ width: 130 }}>
|
||||
{row[COLUMNS.CLASSIFICATION] && String(row[COLUMNS.CLASSIFICATION]).trim() !== '' ? (
|
||||
<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]}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-red-500/10 text-red-400 border border-red-500/20">Empty</span>
|
||||
)}
|
||||
</td>
|
||||
{launchDateCol >= 0 && (
|
||||
<td className="px-3 py-2 truncate font-mono text-slate-300" style={{ width: 130 }}>
|
||||
{isEmptyOrEpoch(row[launchDateCol]) ? (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-red-500/10 text-red-400 border border-red-500/20">Empty</span>
|
||||
) : (
|
||||
formatDateValue(row[launchDateCol]) || String(row[launchDateCol])
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
{readyToOrderCol >= 0 && (
|
||||
<td className="px-3 py-2 truncate font-mono text-slate-300" style={{ width: 130 }}>
|
||||
{row[readyToOrderCol] !== null && row[readyToOrderCol] !== undefined && row[readyToOrderCol] !== '' ? (
|
||||
formatDateValue(row[readyToOrderCol]) || String(row[readyToOrderCol])
|
||||
) : (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-slate-700/50 text-slate-500 border border-slate-600/50">—</span>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
{paginatedData.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-4 py-8 text-center text-slate-500">
|
||||
No items found.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900 border-t border-slate-700 p-4 flex items-center justify-between text-xs text-slate-500">
|
||||
<div>Showing {paginatedData.length} of {filteredData.length} items</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
disabled={page === 1}
|
||||
onClick={() => setPage(p => p - 1)}
|
||||
className="px-2 py-1 rounded bg-slate-800 hover:bg-slate-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span className="text-slate-300">Page {page} of {totalPages || 1}</span>
|
||||
<button
|
||||
disabled={page === totalPages || totalPages === 0}
|
||||
onClick={() => setPage(p => p + 1)}
|
||||
className="px-2 py-1 rounded bg-slate-800 hover:bg-slate-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<aside className="w-60 bg-slate-800 border-r border-slate-700 flex flex-col shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user