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
+85
View File
@@ -0,0 +1,85 @@
import React, { useState } from 'react';
import { Calendar } from 'lucide-react';
import { cn } from '../lib/utils';
interface DateFilterPopoverProps {
selectedRange: { start: string; end: string };
onRangeChange: (range: { start: string; end: string }) => void;
onClose: () => void;
}
export function DateFilterPopover({ selectedRange, onRangeChange, onClose }: DateFilterPopoverProps) {
const [startDate, setStartDate] = useState(selectedRange.start);
const [endDate, setEndDate] = useState(selectedRange.end);
const handleApply = () => {
onRangeChange({ start: startDate, end: endDate });
onClose();
};
const handleClear = () => {
setStartDate('');
setEndDate('');
onRangeChange({ start: '', end: '' });
onClose();
};
const hasValue = startDate || endDate;
return (
<div
className="absolute top-full left-0 mt-1 w-72 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl z-50 p-4 flex flex-col gap-4 animate-in fade-in zoom-in-95 duration-100"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-2 text-xs font-medium text-slate-400 uppercase tracking-wider">
<Calendar className="w-4 h-4" />
Filter by Date Range
</div>
<div className="space-y-3">
<div className="flex flex-col gap-1.5">
<label className="text-[10px] text-slate-500 uppercase tracking-wider">From</label>
<input
type="date"
value={startDate}
onChange={e => setStartDate(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-[10px] text-slate-500 uppercase tracking-wider">To</label>
<input
type="date"
value={endDate}
onChange={e => setEndDate(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs text-white focus:outline-none focus:border-blue-500"
/>
</div>
</div>
<div className="flex items-center justify-between pt-3 border-t border-slate-700">
<button
onClick={handleClear}
className="text-[10px] font-bold text-slate-400 hover:text-white transition-colors uppercase tracking-tight"
>
Clear
</button>
<div className="flex items-center gap-2">
<button
onClick={onClose}
className="px-3 py-1.5 text-[10px] font-medium text-slate-300 hover:text-white hover:bg-slate-700 rounded transition-colors"
>
Cancel
</button>
<button
onClick={handleApply}
className="px-4 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-[10px] font-black rounded transition-colors shadow-lg"
>
Apply
</button>
</div>
</div>
</div>
);
}
+56
View File
@@ -3,6 +3,7 @@ import { ExcelRow, COLUMNS } from '../types';
import { Search, ChevronDown, ChevronUp, X, Edit2, Save } from 'lucide-react'; import { Search, ChevronDown, ChevronUp, X, Edit2, Save } from 'lucide-react';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { ColumnFilterPopover } from './ColumnFilterPopover'; import { ColumnFilterPopover } from './ColumnFilterPopover';
import { DateFilterPopover } from './DateFilterPopover';
interface MissingDataViewProps { interface MissingDataViewProps {
data: ExcelRow[]; data: ExcelRow[];
@@ -72,6 +73,7 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [editing, setEditing] = useState<EditingState | null>(null); const [editing, setEditing] = useState<EditingState | null>(null);
const [columnFilters, setColumnFilters] = useState<Record<number, string[]>>({}); 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 [openFilter, setOpenFilter] = useState<number | null>(null);
const pageSize = 100; const pageSize = 100;
@@ -137,6 +139,7 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
(Object.entries(columnFilters) as [string, string[]][]).forEach(([colIdx, filterValues]) => { (Object.entries(columnFilters) as [string, string[]][]).forEach(([colIdx, filterValues]) => {
if (!filterValues || filterValues.length === 0) return; if (!filterValues || filterValues.length === 0) return;
const colIdxNum = parseInt(colIdx); const colIdxNum = parseInt(colIdx);
if (colIdxNum === launchDateCol || colIdxNum === readyToOrderCol) return;
result = result.filter(r => { result = result.filter(r => {
const val: any = r.row[colIdxNum]; const val: any = r.row[colIdxNum];
const displayVal = colIdxNum === launchDateCol || colIdxNum === readyToOrderCol 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) { if (sortCol !== null) {
result.sort((a, b) => { result.sort((a, b) => {
const valA = a.row[sortCol]; const valA = a.row[sortCol];
@@ -269,6 +295,34 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
)} )}
</div> </div>
<div className="mt-1 relative"> <div className="mt-1 relative">
{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 <button
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === col ? null : col); }} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === col ? null : col); }}
className={cn( className={cn(
@@ -295,6 +349,8 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
onClose={() => setOpenFilter(null)} onClose={() => setOpenFilter(null)}
/> />
)} )}
</>
)}
</div> </div>
</th> </th>
);})} );})}