mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 13:45:24 +02:00
Add date range filter with calendar picker for Launch Date and Ready to Order
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,31 +295,61 @@ export function MissingDataView({ data, headers, onSaveRow, onCaptureState }: Mi
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 relative">
|
<div className="mt-1 relative">
|
||||||
<button
|
{col === launchDateCol || col === readyToOrderCol ? (
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === col ? null : col); }}
|
<>
|
||||||
className={cn(
|
<button
|
||||||
"w-full flex items-center justify-between px-1.5 py-0.5 bg-slate-800 border rounded text-[10px] transition-colors",
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === col ? null : col); }}
|
||||||
filterCount > 0 ? "border-indigo-500 text-white" : "border-slate-600 text-slate-400 hover:border-slate-500"
|
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
|
||||||
<span className="truncate">{filterCount > 0 ? `${filterCount} selected` : 'Filter...'}</span>
|
? "border-indigo-500 text-white"
|
||||||
<ChevronDown className={cn("w-3 h-3 transition-transform", openFilter === col && "rotate-180")} />
|
: "border-slate-600 text-slate-400 hover:border-slate-500"
|
||||||
</button>
|
)}
|
||||||
{openFilter === col && (
|
>
|
||||||
<ColumnFilterPopover
|
<span className="truncate">
|
||||||
uniqueValues={allValues}
|
{dateFilters[col]?.start || dateFilters[col]?.end
|
||||||
selectedValues={selectedFilters}
|
? `${dateFilters[col].start ? dateFilters[col].start : '...'} - ${dateFilters[col].end ? dateFilters[col].end : '...'}`
|
||||||
onToggle={(val) => setColumnFilters(prev => {
|
: 'Filter...'}
|
||||||
const current = prev[col] || [];
|
</span>
|
||||||
if (current.includes(val)) {
|
<ChevronDown className={cn("w-3 h-3 transition-transform", openFilter === col && "rotate-180")} />
|
||||||
return { ...prev, [col]: current.filter(v => v !== val) };
|
</button>
|
||||||
}
|
{openFilter === col && (
|
||||||
return { ...prev, [col]: [...current, val] };
|
<DateFilterPopover
|
||||||
})}
|
selectedRange={dateFilters[col] || { start: '', end: '' }}
|
||||||
onSelectAll={(vals) => setColumnFilters(prev => ({ ...prev, [col]: vals }))}
|
onRangeChange={(range) => setDateFilters(prev => ({ ...prev, [col]: range }))}
|
||||||
onClear={() => { setColumnFilters(prev => { const n = { ...prev }; delete n[col]; return n; }); }}
|
onClose={() => setOpenFilter(null)}
|
||||||
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>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
Reference in New Issue
Block a user