182 lines
8.8 KiB
TypeScript
182 lines
8.8 KiB
TypeScript
import React, { useState, useRef } from 'react';
|
|
import { Calendar, RefreshCw, ChevronDown } from 'lucide-react';
|
|
import type { DateRange } from '../types';
|
|
import { endOfLocalDay, formatDateParam, parseLocalDateInput, rangeForDay, rangeForLastDays, rangeForPreviousDay, startOfLocalDay } from '../dateRanges';
|
|
|
|
interface DateRangePickerProps {
|
|
dateRange: DateRange;
|
|
onChange: (range: DateRange) => void;
|
|
refreshInterval?: number;
|
|
setRefreshInterval?: (interval: number) => void;
|
|
onManualRefresh?: () => void;
|
|
}
|
|
|
|
const PRESETS = [
|
|
{ label: 'Hoje', getRange: () => rangeForDay(new Date()) },
|
|
{ label: 'Ontem', getRange: () => rangeForPreviousDay() },
|
|
{ label: 'Últimos 7 dias', getRange: () => rangeForLastDays(7) },
|
|
{ label: 'Últimos 30 dias', getRange: () => rangeForLastDays(30) },
|
|
{ label: 'Este Mês', getRange: () => { const end = endOfLocalDay(new Date()); const start = startOfLocalDay(new Date(end.getFullYear(), end.getMonth(), 1)); return { start, end }; } },
|
|
{ label: 'Mês Passado', getRange: () => { const d = new Date(); const start = new Date(d.getFullYear(), d.getMonth() - 1, 1); const end = new Date(d.getFullYear(), d.getMonth(), 0, 23, 59, 59, 999); return { start, end }; } },
|
|
{ label: 'Últimos 90 dias', getRange: () => rangeForLastDays(90) },
|
|
{ label: 'Este Ano', getRange: () => { const end = endOfLocalDay(new Date()); const start = startOfLocalDay(new Date(end.getFullYear(), 0, 1)); return { start, end }; } },
|
|
{ label: 'Todo o Período', getRange: () => { const end = endOfLocalDay(new Date()); const start = startOfLocalDay(new Date(2000, 0, 1)); return { start, end }; } },
|
|
];
|
|
|
|
const REFRESH_OPTIONS = [
|
|
{ label: 'Desligado', value: 0 },
|
|
{ label: '5s', value: 5000 },
|
|
{ label: '10s', value: 10000 },
|
|
{ label: '30s', value: 30000 },
|
|
{ label: '1m', value: 60000 },
|
|
{ label: '5m', value: 300000 },
|
|
];
|
|
|
|
const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange, refreshInterval, setRefreshInterval, onManualRefresh }) => {
|
|
const [isPresetOpen, setIsPresetOpen] = useState(false);
|
|
const startRef = useRef<HTMLInputElement>(null);
|
|
const endRef = useRef<HTMLInputElement>(null);
|
|
|
|
const formatDisplayDate = (date: Date) => {
|
|
return date.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
};
|
|
|
|
const formatDateForInput = (date: Date) => {
|
|
return formatDateParam(date);
|
|
};
|
|
|
|
const handleStartChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newStart = parseLocalDateInput(e.target.value);
|
|
if (newStart && !isNaN(newStart.getTime())) {
|
|
const start = startOfLocalDay(newStart);
|
|
const end = dateRange.end < start ? endOfLocalDay(start) : dateRange.end;
|
|
onChange({ start, end });
|
|
}
|
|
};
|
|
|
|
const handleEndChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newEnd = parseLocalDateInput(e.target.value);
|
|
if (newEnd && !isNaN(newEnd.getTime())) {
|
|
const end = endOfLocalDay(newEnd);
|
|
const start = dateRange.start > end ? startOfLocalDay(end) : dateRange.start;
|
|
onChange({ start, end });
|
|
}
|
|
};
|
|
|
|
const openPicker = (ref: React.RefObject<HTMLInputElement | null>) => {
|
|
if (ref.current) {
|
|
try {
|
|
if ('showPicker' in HTMLInputElement.prototype) {
|
|
ref.current.showPicker();
|
|
} else {
|
|
ref.current.focus();
|
|
}
|
|
} catch {
|
|
ref.current.focus();
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
{/* Date Presets Dropdown */}
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setIsPresetOpen(!isPresetOpen)}
|
|
className="flex items-center gap-2 bg-dark-card border border-dark-border px-4 py-2.5 rounded-xl shadow-sm hover:border-brand-primary transition-colors text-sm font-medium text-dark-text cursor-pointer"
|
|
>
|
|
<Calendar size={16} className="text-dark-muted shrink-0" />
|
|
<span>{formatDisplayDate(dateRange.start)} - {formatDisplayDate(dateRange.end)}</span>
|
|
<ChevronDown size={14} className="text-dark-muted ml-1" />
|
|
</button>
|
|
|
|
{isPresetOpen && (
|
|
<>
|
|
<div className="fixed inset-0 z-10" onClick={() => setIsPresetOpen(false)}></div>
|
|
<div className="absolute right-0 mt-2 w-56 bg-dark-card border border-dark-border rounded-xl shadow-xl z-20 py-2 max-h-[32rem] overflow-y-auto">
|
|
<div className="px-4 pb-3 pt-1 border-b border-dark-border mb-2 flex flex-col gap-2">
|
|
<span className="text-xs font-bold text-dark-muted uppercase tracking-widest">Período Customizado</span>
|
|
<div className="flex flex-col gap-1.5">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="text-xs font-medium text-dark-text w-6">De:</span>
|
|
<div
|
|
className="relative bg-dark-input border border-dark-border text-dark-text text-xs rounded-lg px-2 py-1 focus-within:border-brand-primary w-full cursor-pointer overflow-hidden flex items-center h-6"
|
|
onClick={() => openPicker(startRef)}
|
|
>
|
|
<span className="w-full text-center">{formatDisplayDate(dateRange.start)}</span>
|
|
<input
|
|
ref={startRef}
|
|
type="date"
|
|
value={formatDateForInput(dateRange.start)}
|
|
onChange={handleStartChange}
|
|
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="text-xs font-medium text-dark-text w-6">Até:</span>
|
|
<div
|
|
className="relative bg-dark-input border border-dark-border text-dark-text text-xs rounded-lg px-2 py-1 focus-within:border-brand-primary w-full cursor-pointer overflow-hidden flex items-center h-6"
|
|
onClick={() => openPicker(endRef)}
|
|
>
|
|
<span className="w-full text-center">{formatDisplayDate(dateRange.end)}</span>
|
|
<input
|
|
ref={endRef}
|
|
type="date"
|
|
value={formatDateForInput(dateRange.end)}
|
|
onChange={handleEndChange}
|
|
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<span className="px-4 text-xs font-bold text-dark-muted uppercase tracking-widest mb-1 block">Atalhos</span>
|
|
{PRESETS.map((preset) => (
|
|
<button
|
|
key={preset.label}
|
|
onClick={() => {
|
|
onChange(preset.getRange());
|
|
setIsPresetOpen(false);
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-sm text-dark-muted hover:text-dark-text hover:bg-dark-input transition-colors cursor-pointer"
|
|
>
|
|
{preset.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Auto Refresh Dropdown */}
|
|
{setRefreshInterval && onManualRefresh && (
|
|
<div className="flex items-center bg-dark-card border border-dark-border rounded-xl shadow-sm overflow-hidden hover:border-brand-primary transition-colors">
|
|
<button
|
|
onClick={onManualRefresh}
|
|
className="flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium text-dark-text hover:text-brand-primary hover:bg-dark-input transition-colors border-r border-dark-border cursor-pointer"
|
|
title="Atualizar Agora"
|
|
>
|
|
<RefreshCw size={16} className="text-brand-primary" />
|
|
<span>Atualizar</span>
|
|
</button>
|
|
<select
|
|
value={refreshInterval}
|
|
onChange={(e) => setRefreshInterval(Number(e.target.value))}
|
|
className="appearance-none bg-transparent text-sm font-bold text-dark-muted pl-4 pr-10 py-2.5 focus:outline-none cursor-pointer relative"
|
|
style={{ backgroundImage: `url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a1a1aa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e")`, backgroundRepeat: 'no-repeat', backgroundPosition: 'right 0.75rem center', backgroundSize: '1em' }}
|
|
>
|
|
{REFRESH_OPTIONS.map(opt => (
|
|
<option key={opt.label} value={opt.value} className="bg-dark-card font-medium text-dark-text">
|
|
{opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DateRangePicker;
|