feat: replace year/quarter filters with free date range pickers
Deploy HiHala Dashboard / deploy (push) Successful in 8s
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user