Spaces:
Running
Running
| import { useState, useRef, useEffect, useMemo } from 'react'; | |
| import { useTranslation } from 'react-i18next'; | |
| import { ScanLine, Calendar, ArrowRight } from 'lucide-react'; | |
| import { format } from 'date-fns'; | |
| import { fr } from 'date-fns/locale/fr'; | |
| import { enUS } from 'date-fns/locale/en-US'; | |
| import { Button } from '@/components/ui/button'; | |
| type Period = 'today' | '7days' | 'custom'; | |
| interface HeaderProps { | |
| title: string; | |
| onScanClick?: () => void; | |
| activePeriod?: Period; | |
| onPeriodChange?: (period: Period) => void; | |
| customStartDate?: string; | |
| customEndDate?: string; | |
| onCustomDateChange?: (startDate: string, endDate: string) => void; | |
| } | |
| function computeDateRange(period: Period, customStart?: string, customEnd?: string): { start: Date; end: Date } { | |
| const now = new Date(); | |
| switch (period) { | |
| case 'today': { | |
| const midnight = new Date(now); | |
| midnight.setHours(0, 0, 0, 0); | |
| return { start: midnight, end: now }; | |
| } | |
| case '7days': { | |
| const sevenDaysAgo = new Date(now); | |
| sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); | |
| sevenDaysAgo.setHours(0, 0, 0, 0); | |
| return { start: sevenDaysAgo, end: now }; | |
| } | |
| case 'custom': { | |
| if (customStart && customEnd) { | |
| // Parse as local date (YYYY-MM-DD) to avoid UTC timezone shift | |
| const [sy, sm, sd] = customStart.split('-').map(Number); | |
| const [ey, em, ed] = customEnd.split('-').map(Number); | |
| return { start: new Date(sy, sm - 1, sd), end: new Date(ey, em - 1, ed) }; | |
| } | |
| return { start: now, end: now }; | |
| } | |
| } | |
| } | |
| function formatDateLabel(date: Date, locale: string): string { | |
| const loc = locale === 'fr' ? fr : enUS; | |
| return format(date, 'd MMM', { locale: loc }); | |
| } | |
| export default function Header({ | |
| title, | |
| onScanClick, | |
| activePeriod: controlledPeriod, | |
| onPeriodChange, | |
| customStartDate, | |
| customEndDate, | |
| onCustomDateChange, | |
| }: HeaderProps) { | |
| const { t, i18n } = useTranslation(); | |
| const [showDatePicker, setShowDatePicker] = useState(false); | |
| const [internalPeriod, setInternalPeriod] = useState<Period>('today'); | |
| const [internalStart, setInternalStart] = useState(''); | |
| const [internalEnd, setInternalEnd] = useState(''); | |
| const activePeriod = controlledPeriod ?? internalPeriod; | |
| const startDate = customStartDate ?? internalStart; | |
| const endDate = customEndDate ?? internalEnd; | |
| const handlePeriodChange = (p: Period) => { | |
| setInternalPeriod(p); | |
| onPeriodChange?.(p); | |
| if (p === 'custom') { | |
| setShowDatePicker(true); | |
| } else { | |
| setShowDatePicker(false); | |
| } | |
| }; | |
| const handleCustomDateChange = (start: string, end: string) => { | |
| setInternalStart(start); | |
| setInternalEnd(end); | |
| onCustomDateChange?.(start, end); | |
| }; | |
| const datePickerRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| if (!showDatePicker) return; | |
| function handleClick(e: MouseEvent) { | |
| if (datePickerRef.current && !datePickerRef.current.contains(e.target as Node)) { | |
| setShowDatePicker(false); | |
| } | |
| } | |
| document.addEventListener('mousedown', handleClick); | |
| return () => document.removeEventListener('mousedown', handleClick); | |
| }, [showDatePicker]); | |
| const dateRange = useMemo( | |
| () => computeDateRange(activePeriod, startDate, endDate), | |
| [activePeriod, startDate, endDate] | |
| ); | |
| const today = format(new Date(), 'yyyy-MM-dd'); | |
| return ( | |
| <header className="flex h-20 items-center justify-between bg-background px-8 z-10 sticky top-0 border-b border-border/50"> | |
| <h2 className="text-2xl font-bold tracking-tight text-slate-900">{title}</h2> | |
| <div className="flex items-center gap-4"> | |
| {/* Period filter */} | |
| <div className="flex items-center gap-2 bg-card p-1 rounded-lg border border-border shadow-sm"> | |
| <button | |
| onClick={() => handlePeriodChange('today')} | |
| className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${ | |
| activePeriod === 'today' | |
| ? 'bg-slate-100 text-slate-900 hover:bg-slate-200' | |
| : 'text-slate-500 hover:bg-slate-50 hover:text-slate-900' | |
| }`} | |
| > | |
| {t('scan.today')} | |
| </button> | |
| <button | |
| onClick={() => handlePeriodChange('7days')} | |
| className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${ | |
| activePeriod === '7days' | |
| ? 'bg-slate-100 text-slate-900 hover:bg-slate-200' | |
| : 'text-slate-500 hover:bg-slate-50 hover:text-slate-900' | |
| }`} | |
| > | |
| {t('scan.last7days')} | |
| </button> | |
| <div className="relative" ref={datePickerRef}> | |
| <button | |
| onClick={() => { | |
| if (activePeriod === 'custom') { | |
| setShowDatePicker(!showDatePicker); | |
| } else { | |
| handlePeriodChange('custom'); | |
| } | |
| }} | |
| className={`px-3 py-1.5 text-xs font-medium rounded-md flex items-center gap-1.5 transition-colors ${ | |
| activePeriod === 'custom' | |
| ? 'bg-primary/10 text-primary border border-primary/20 hover:bg-primary/20' | |
| : 'text-slate-500 hover:bg-slate-50 hover:text-slate-900' | |
| }`} | |
| > | |
| <span>{t('scan.custom')}</span> | |
| <Calendar className="h-3.5 w-3.5" /> | |
| </button> | |
| {/* Custom date picker dropdown */} | |
| {showDatePicker && ( | |
| <div className="absolute right-0 top-full mt-2 w-72 rounded-xl bg-white p-4 shadow-lg border border-slate-200 z-50"> | |
| <div className="space-y-3"> | |
| <div> | |
| <label className="block text-xs font-medium text-slate-500 uppercase tracking-wide mb-1.5"> | |
| {t('scan.startDate')} | |
| </label> | |
| <input | |
| type="date" | |
| max={endDate || today} | |
| value={startDate} | |
| onChange={(e) => handleCustomDateChange(e.target.value, endDate)} | |
| className="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-900 focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none transition-colors" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-medium text-slate-500 uppercase tracking-wide mb-1.5"> | |
| {t('scan.endDate')} | |
| </label> | |
| <input | |
| type="date" | |
| min={startDate} | |
| max={today} | |
| value={endDate} | |
| onChange={(e) => handleCustomDateChange(startDate, e.target.value)} | |
| className="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-900 focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none transition-colors" | |
| /> | |
| </div> | |
| {startDate && endDate && ( | |
| <button | |
| onClick={() => setShowDatePicker(false)} | |
| className="w-full rounded-lg bg-primary px-3 py-2 text-sm font-medium text-white hover:bg-primary/90 transition-colors" | |
| > | |
| {t('common.save')} | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Date range display */} | |
| <div className="hidden sm:flex items-center gap-2 bg-card px-3 py-1.5 rounded-lg border border-border shadow-sm text-sm text-slate-600"> | |
| <span className="font-medium text-slate-900"> | |
| {formatDateLabel(dateRange.start, i18n.language)} | |
| </span> | |
| <ArrowRight className="h-3.5 w-3.5 text-slate-400" /> | |
| <span className="font-medium text-slate-900"> | |
| {formatDateLabel(dateRange.end, i18n.language)} | |
| </span> | |
| </div> | |
| <div className="h-8 w-px bg-slate-200 mx-2" /> | |
| {/* Scan Button */} | |
| <Button | |
| onClick={onScanClick} | |
| className="gap-2 shadow-md shadow-blue-500/20 hover:shadow-blue-500/30 active:scale-95 transition-all" | |
| > | |
| <ScanLine className="h-[18px] w-[18px]" /> | |
| {t('header.scanNow')} | |
| </Button> | |
| </div> | |
| </header> | |
| ); | |
| } | |