Add date range filter with calendar picker for Launch Date and Ready to Order

This commit is contained in:
Christian Vidal Wolf
2026-04-14 17:13:47 +02:00
parent 78df66be24
commit a1402316e0
2 changed files with 166 additions and 25 deletions
+81 -25
View File
@@ -3,6 +3,7 @@ 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[];
@@ -72,6 +73,7 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
const [page, setPage] = useState(1);
const [editing, setEditing] = useState<EditingState | null>(null);
const [columnFilters, setColumnFilters] = useState<Record<number, string[]>>({});
const [dateFilters, setDateFilters] = useState<Record<number, { start: string; end: string }>>({});
const [openFilter, setOpenFilter] = useState<number | null>(null);
const pageSize = 100;
@@ -137,6 +139,7 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
(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
@@ -146,6 +149,29 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
});
});
(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) || String(val ?? '');
if (!dateStr) return false;
const parts = dateStr.split('/');
if (parts.length !== 3) return false;
const rowDate = new Date(`${parts[2]}-${parts[1]}-${parts[0]}`);
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];
@@ -269,31 +295,61 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
)}
</div>
<div className="mt-1 relative">
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === col ? null : col); }}
className={cn(
"w-full flex items-center justify-between px-1.5 py-0.5 bg-slate-800 border rounded text-[10px] transition-colors",
filterCount > 0 ? "border-indigo-500 text-white" : "border-slate-600 text-slate-400 hover:border-slate-500"
)}
>
<span className="truncate">{filterCount > 0 ? `${filterCount} selected` : 'Filter...'}</span>
<ChevronDown className={cn("w-3 h-3 transition-transform", openFilter === col && "rotate-180")} />
</button>
{openFilter === col && (
<ColumnFilterPopover
uniqueValues={allValues}
selectedValues={selectedFilters}
onToggle={(val) => 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)}
/>
{col === launchDateCol || col === readyToOrderCol ? (
<>
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === col ? null : col); }}
className={cn(
"w-full flex items-center justify-between px-1.5 py-0.5 bg-slate-800 border rounded text-[10px] transition-colors",
dateFilters[col]?.start || dateFilters[col]?.end
? "border-indigo-500 text-white"
: "border-slate-600 text-slate-400 hover:border-slate-500"
)}
>
<span className="truncate">
{dateFilters[col]?.start || dateFilters[col]?.end
? `${dateFilters[col].start ? dateFilters[col].start : '...'} - ${dateFilters[col].end ? dateFilters[col].end : '...'}`
: 'Filter...'}
</span>
<ChevronDown className={cn("w-3 h-3 transition-transform", openFilter === col && "rotate-180")} />
</button>
{openFilter === col && (
<DateFilterPopover
selectedRange={dateFilters[col] || { start: '', end: '' }}
onRangeChange={(range) => setDateFilters(prev => ({ ...prev, [col]: range }))}
onClose={() => setOpenFilter(null)}
/>
)}
</>
) : (
<>
<button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === col ? null : col); }}
className={cn(
"w-full flex items-center justify-between px-1.5 py-0.5 bg-slate-800 border rounded text-[10px] transition-colors",
filterCount > 0 ? "border-indigo-500 text-white" : "border-slate-600 text-slate-400 hover:border-slate-500"
)}
>
<span className="truncate">{filterCount > 0 ? `${filterCount} selected` : 'Filter...'}</span>
<ChevronDown className={cn("w-3 h-3 transition-transform", openFilter === col && "rotate-180")} />
</button>
{openFilter === col && (
<ColumnFilterPopover
uniqueValues={allValues}
selectedValues={selectedFilters}
onToggle={(val) => 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)}
/>
)}
</>
)}
</div>
</th>