Add Launch Date Inconsistencies and Upcoming Launch Date tabs

- Add Launch Date Inconsistencies tab: shows when Ready to Order > Launch Date
- Add Upcoming Launch Date tab: alerts when launch is within 180 days (yellow) or 120 days (red)
- Show days until launch countdown
This commit is contained in:
Christian Vidal Wolf
2026-04-20 18:48:30 +02:00
parent db696d2816
commit 66a42ad1a0
+92 -3
View File
@@ -96,6 +96,7 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
{ col: COLUMNS.CLASSIFICATION, label: 'Classification', width: 130 },
...(launchDateCol >= 0 ? [{ col: launchDateCol, label: launchHeader, width: 130 }] : []),
...(readyToOrderCol >= 0 ? [{ col: readyToOrderCol, label: readyHeader, width: 130 }] : []),
...((activeTab === 'launchInconsistent' || activeTab === 'upcomingLaunch') ? [{ col: -1, label: 'Days Until Launch', width: 120 }] : []),
];
const columnUniqueValues = useMemo(() => {
@@ -124,7 +125,10 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
};
const filteredData = useMemo(() => {
let result = data.map((row, index) => ({ row, index }));
let result = data.map((row, index) => ({ row, index, status: 'ok' as string, daysUntil: 0 }));
const today = new Date();
today.setHours(0, 0, 0, 0);
if (activeTab === 'missingLaunch') {
result = result.filter(r => {
@@ -133,6 +137,60 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
});
}
if (activeTab === 'launchInconsistent' || activeTab === 'upcomingLaunch') {
result = result.map(r => {
const launchVal = launchDateCol >= 0 ? r.row[launchDateCol] : undefined;
const readyVal = readyToOrderCol >= 0 ? r.row[readyToOrderCol] : undefined;
const launchDate = formatDateValue(launchVal);
const readyDate = formatDateValue(readyVal);
let daysUntil = 0;
let status = 'ok';
if (launchDate && /^\d{4}-\d{2}-\d{2}$/.test(launchDate)) {
const launchD = new Date(launchDate);
const diffTime = launchD.getTime() - today.getTime();
daysUntil = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (activeTab === 'launchInconsistent') {
if (readyDate && /^\d{4}-\d{2}-\d{2}$/.test(readyDate)) {
const readyD = new Date(readyDate);
if (readyD > launchD) {
status = 'error_ready_after_launch';
} else {
status = 'ok';
}
} else {
status = 'ok';
}
} else if (activeTab === 'upcomingLaunch') {
if (daysUntil <= 0) {
status = 'past';
} else if (daysUntil <= 120) {
status = 'critical';
} else if (daysUntil <= 180) {
status = 'warning';
} else {
status = 'ok';
}
}
} else {
status = 'missing_launch';
}
return { ...r, status, daysUntil };
});
if (activeTab === 'launchInconsistent') {
result = result.filter(r =>
r.status === 'error_ready_after_launch' || (r.status === 'ok' && r.daysUntil > 0 && isEmptyOrEpoch(launchDateCol >= 0 ? r.row[readyToOrderCol] : undefined))
);
}
if (activeTab === 'upcomingLaunch') {
result = result.filter(r => r.status === 'warning' || r.status === 'critical' || r.status === 'past');
}
}
if (search) {
const s = search.toLowerCase();
result = result.filter(r =>
@@ -228,6 +286,8 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
const tabs: { id: TabType; label: string }[] = [
{ id: 'missingLaunch', label: 'Missing Launch Date' },
{ id: 'launchInconsistent', label: 'Launch Date Inconsistencies' },
{ id: 'upcomingLaunch', label: 'Upcoming Launch Date' },
];
const editingRow = editing ? data[editing.rowIndex] : null;
@@ -363,8 +423,14 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
</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">
{paginatedData.map(({ row, index, status, daysUntil }) => (
<tr key={index} className={cn(
"hover:bg-slate-700/20 transition-colors",
activeTab === 'launchInconsistent' && status === 'error_ready_after_launch' && "bg-red-500/10",
activeTab === 'upcomingLaunch' && status === 'warning' && "bg-yellow-500/10",
activeTab === 'upcomingLaunch' && status === 'critical' && "bg-red-500/10",
activeTab === 'upcomingLaunch' && status === 'past' && "bg-red-600/20"
)}>
<td className="px-3 py-2 font-mono text-indigo-400 truncate" style={{ width: 100 }}>
{row[COLUMNS.ARTICLE_NO]}
</td>
@@ -403,6 +469,29 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
)}
</td>
)}
{(activeTab === 'launchInconsistent' || activeTab === 'upcomingLaunch') && (
<td className="px-3 py-2 font-mono" style={{ width: 120 }}>
{status === 'error_ready_after_launch' ? (
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-red-500/20 text-red-400 border border-red-500/40">
Ready &gt; Launch
</span>
) : status === 'warning' ? (
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-yellow-500/20 text-yellow-400 border border-yellow-500/40">
{daysUntil} days
</span>
) : status === 'critical' ? (
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-red-500/20 text-red-400 border border-red-500/40">
{daysUntil} days
</span>
) : status === 'past' ? (
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-red-600/30 text-red-300 border border-red-500/50">
Past
</span>
) : (
<span className="text-slate-500"></span>
)}
</td>
)}
<td className="px-3 py-2 text-right" style={{ width: 60 }}>
<button
onClick={() => openEdit(index, row)}