feat: replace year/quarter filters with free date range pickers
Deploy HiHala Dashboard / deploy (push) Successful in 8s

Dashboard: PeriodPicker replaces year + quarter dropdowns. Defaults to
current month. YoY stat card now compares same range vs previous year.

Comparison: two independent PeriodPicker blocks (Period A and Period B).
Changing Period A auto-updates Period B to same period previous year,
but Period B remains freely editable.

Both pages use filterDataByDateRange; Filters type drops year/quarter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-04-19 15:02:06 +03:00
parent 9064df82be
commit 0f6881309c
7 changed files with 270 additions and 253 deletions
+147
View File
@@ -0,0 +1,147 @@
import React, { useState, useEffect } from 'react';
import { useLanguage } from '../../contexts/LanguageContext';
import type { Season } from '../../types';
interface Props {
startDate: string;
endDate: string;
onChange: (start: string, end: string) => void;
availableYears: number[];
seasons?: Season[];
}
const PRESETS: Record<string, (year: number) => { start: string; end: string }> = {
jan: y => ({ start: `${y}-01-01`, end: `${y}-01-31` }),
feb: y => ({ start: `${y}-02-01`, end: `${y}-02-28` }),
mar: y => ({ start: `${y}-03-01`, end: `${y}-03-31` }),
apr: y => ({ start: `${y}-04-01`, end: `${y}-04-30` }),
may: y => ({ start: `${y}-05-01`, end: `${y}-05-31` }),
jun: y => ({ start: `${y}-06-01`, end: `${y}-06-30` }),
jul: y => ({ start: `${y}-07-01`, end: `${y}-07-31` }),
aug: y => ({ start: `${y}-08-01`, end: `${y}-08-31` }),
sep: y => ({ start: `${y}-09-01`, end: `${y}-09-30` }),
oct: y => ({ start: `${y}-10-01`, end: `${y}-10-31` }),
nov: y => ({ start: `${y}-11-01`, end: `${y}-11-30` }),
dec: y => ({ start: `${y}-12-01`, end: `${y}-12-31` }),
q1: y => ({ start: `${y}-01-01`, end: `${y}-03-31` }),
q2: y => ({ start: `${y}-04-01`, end: `${y}-06-30` }),
q3: y => ({ start: `${y}-07-01`, end: `${y}-09-30` }),
q4: y => ({ start: `${y}-10-01`, end: `${y}-12-31` }),
h1: y => ({ start: `${y}-01-01`, end: `${y}-06-30` }),
h2: y => ({ start: `${y}-07-01`, end: `${y}-12-31` }),
full: y => ({ start: `${y}-01-01`, end: `${y}-12-31` }),
};
function guessPreset(start: string, end: string): { preset: string; year: number } {
const year = parseInt(start.slice(0, 4));
for (const [key, fn] of Object.entries(PRESETS)) {
const p = fn(year);
if (p.start === start && p.end === end) return { preset: key, year };
}
return { preset: 'custom', year };
}
export default function PeriodPicker({ startDate, endDate, onChange, availableYears, seasons = [] }: Props) {
const { t } = useLanguage();
const [year, setYear] = useState<number>(() => guessPreset(startDate, endDate).year || new Date().getFullYear());
const [preset, setPreset] = useState<string>(() => guessPreset(startDate, endDate).preset);
// When parent updates dates externally (e.g. auto-fill from Period A), sync internal state
useEffect(() => {
const { preset: p, year: y } = guessPreset(startDate, endDate);
setPreset(p);
if (p !== 'custom') setYear(y);
}, [startDate, endDate]);
const handlePreset = (value: string) => {
setPreset(value);
if (value === 'custom') return;
if (value.startsWith('season-')) {
const season = seasons.find(s => String(s.Id) === value.replace('season-', ''));
if (season) onChange(season.StartDate, season.EndDate);
return;
}
const range = PRESETS[value]?.(year);
if (range) onChange(range.start, range.end);
};
const handleYear = (newYear: number) => {
setYear(newYear);
if (preset !== 'custom' && !preset.startsWith('season-')) {
const range = PRESETS[preset]?.(newYear);
if (range) onChange(range.start, range.end);
}
};
const handleStart = (value: string) => {
setPreset('custom');
onChange(value, endDate);
};
const handleEnd = (value: string) => {
setPreset('custom');
onChange(startDate, value);
};
return (
<div style={{ display: 'contents' }}>
<div className="control-group">
<label>{t('comparison.period')}</label>
<select value={preset} onChange={e => handlePreset(e.target.value)}>
<option value="custom">{t('comparison.custom')}</option>
<option value="jan">{t('months.january')}</option>
<option value="feb">{t('months.february')}</option>
<option value="mar">{t('months.march')}</option>
<option value="apr">{t('months.april')}</option>
<option value="may">{t('months.may')}</option>
<option value="jun">{t('months.june')}</option>
<option value="jul">{t('months.july')}</option>
<option value="aug">{t('months.august')}</option>
<option value="sep">{t('months.september')}</option>
<option value="oct">{t('months.october')}</option>
<option value="nov">{t('months.november')}</option>
<option value="dec">{t('months.december')}</option>
<option value="q1">{t('time.q1')}</option>
<option value="q2">{t('time.q2')}</option>
<option value="q3">{t('time.q3')}</option>
<option value="q4">{t('time.q4')}</option>
<option value="h1">{t('time.h1')}</option>
<option value="h2">{t('time.h2')}</option>
<option value="full">{t('time.fullYear')}</option>
{seasons.length > 0 && (
<optgroup label={t('comparison.seasons') || 'Seasons'}>
{seasons.map(s => (
<option key={s.Id} value={`season-${s.Id}`}>
{s.Name} {s.HijriYear}
</option>
))}
</optgroup>
)}
</select>
</div>
{preset !== 'custom' && !preset.startsWith('season-') && availableYears.length > 0 && (
<div className="control-group">
<label>{t('filters.year')}</label>
<select value={year} onChange={e => handleYear(parseInt(e.target.value))}>
{availableYears.map(y => <option key={y} value={y}>{y}</option>)}
</select>
</div>
)}
{(preset === 'custom' || preset.startsWith('season-')) && (
<>
<div className="control-group">
<label>{t('comparison.from')}</label>
<input type="date" value={startDate} onChange={e => handleStart(e.target.value)} />
</div>
<div className="control-group">
<label>{t('comparison.to')}</label>
<input type="date" value={endDate} onChange={e => handleEnd(e.target.value)} />
</div>
</>
)}
</div>
);
}
+1
View File
@@ -5,3 +5,4 @@ export { default as FilterControls } from './FilterControls';
export { default as MultiSelect } from './MultiSelect';
export { default as StatCard } from './StatCard';
export { default as ToggleSwitch } from './ToggleSwitch';
export { default as PeriodPicker } from './PeriodPicker';