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:
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { useSearchParams, Link } from 'react-router-dom';
|
||||
import { Line, Bar, Pie } from 'react-chartjs-2';
|
||||
import { Carousel, EmptyState, FilterControls, MultiSelect, StatCard } from './shared';
|
||||
import { Carousel, EmptyState, FilterControls, MultiSelect, PeriodPicker, StatCard } from './shared';
|
||||
import { ExportableChart } from './ChartExport';
|
||||
import { chartColors, chartPalette, createBaseOptions } from '../config/chartConfig';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import {
|
||||
filterData,
|
||||
filterDataByDateRange,
|
||||
calculateMetrics,
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
@@ -24,16 +24,22 @@ import {
|
||||
} from '../services/dataService';
|
||||
import type { DashboardProps, Filters, MuseumRecord, Season } from '../types';
|
||||
|
||||
function currentMonthRange(): { startDate: string; endDate: string } {
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth() + 1;
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const lastDay = new Date(y, m, 0).getDate();
|
||||
return { startDate: `${y}-${pad(m)}-01`, endDate: `${y}-${pad(m)}-${pad(lastDay)}` };
|
||||
}
|
||||
|
||||
const defaultFilters: Filters = {
|
||||
year: 'all',
|
||||
...currentMonthRange(),
|
||||
district: 'all',
|
||||
channel: [],
|
||||
museum: [],
|
||||
quarter: 'all'
|
||||
};
|
||||
|
||||
const filterKeys: (keyof Filters)[] = ['year', 'district', 'quarter'];
|
||||
|
||||
function Dashboard({ data, seasons, userRole, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT, allowedMuseums, allowedChannels }: DashboardProps) {
|
||||
const { t } = useLanguage();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -46,30 +52,24 @@ function Dashboard({ data, seasons, userRole, showDataLabels, setShowDataLabels,
|
||||
|
||||
// Initialize filters from URL or defaults
|
||||
const [filters, setFiltersState] = useState<Filters>(() => {
|
||||
const initial: Filters = { ...defaultFilters };
|
||||
filterKeys.forEach(key => {
|
||||
const value = searchParams.get(key);
|
||||
if (value) (initial as Record<string, unknown>)[key] = value;
|
||||
});
|
||||
const museumParam = searchParams.get('museum');
|
||||
if (museumParam) initial.museum = museumParam.split(',').filter(Boolean);
|
||||
const channelParam = searchParams.get('channel');
|
||||
if (channelParam) initial.channel = channelParam.split(',').filter(Boolean);
|
||||
return initial;
|
||||
const def = defaultFilters;
|
||||
return {
|
||||
startDate: searchParams.get('start') || def.startDate,
|
||||
endDate: searchParams.get('end') || def.endDate,
|
||||
district: searchParams.get('district') || 'all',
|
||||
museum: searchParams.get('museum')?.split(',').filter(Boolean) || [],
|
||||
channel: searchParams.get('channel')?.split(',').filter(Boolean) || [],
|
||||
};
|
||||
});
|
||||
|
||||
// Update both state and URL
|
||||
const setFilters = (newFilters: Filters | ((prev: Filters) => Filters)) => {
|
||||
const updated = typeof newFilters === 'function' ? newFilters(filters) : newFilters;
|
||||
setFiltersState(updated);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
filterKeys.forEach(key => {
|
||||
const val = (updated as Record<string, unknown>)[key] as string;
|
||||
if (val && val !== 'all') {
|
||||
params.set(key, val);
|
||||
}
|
||||
});
|
||||
params.set('start', updated.startDate);
|
||||
params.set('end', updated.endDate);
|
||||
if (updated.district !== 'all') params.set('district', updated.district);
|
||||
if (updated.museum.length > 0) params.set('museum', updated.museum.join(','));
|
||||
if (updated.channel.length > 0) params.set('channel', updated.channel.join(','));
|
||||
setSearchParams(params, { replace: true });
|
||||
@@ -97,7 +97,10 @@ function Dashboard({ data, seasons, userRole, showDataLabels, setShowDataLabels,
|
||||
return d;
|
||||
}, [data, allowedMuseums, allowedChannels]);
|
||||
|
||||
const filteredData = useMemo(() => filterData(permissionFilteredData, filters), [permissionFilteredData, filters]);
|
||||
const filteredData = useMemo(
|
||||
() => filterDataByDateRange(permissionFilteredData, filters.startDate, filters.endDate, filters),
|
||||
[permissionFilteredData, filters]
|
||||
);
|
||||
|
||||
const seasonFilteredData = useMemo(() => {
|
||||
if (!selectedSeason) return filteredData;
|
||||
@@ -134,13 +137,13 @@ function Dashboard({ data, seasons, userRole, showDataLabels, setShowDataLabels,
|
||||
const availableMuseums = useMemo(() => getMuseumsForDistrict(permissionFilteredData, filters.district), [permissionFilteredData, filters.district]);
|
||||
|
||||
const yoyChange = useMemo(() => {
|
||||
if (filters.year === 'all') return null;
|
||||
const prevYear = String(parseInt(filters.year) - 1);
|
||||
const prevData = permissionFilteredData.filter((row: MuseumRecord) => row.year === prevYear);
|
||||
const prevStart = filters.startDate.replace(/^(\d{4})/, (_, y) => String(parseInt(y) - 1));
|
||||
const prevEnd = filters.endDate.replace(/^(\d{4})/, (_, y) => String(parseInt(y) - 1));
|
||||
const prevData = filterDataByDateRange(permissionFilteredData, prevStart, prevEnd, filters);
|
||||
if (prevData.length === 0) return null;
|
||||
const prevMetrics = calculateMetrics(prevData, includeVAT);
|
||||
return prevMetrics.revenue > 0 ? ((metrics.revenue - prevMetrics.revenue) / prevMetrics.revenue * 100) : null;
|
||||
}, [permissionFilteredData, filters.year, metrics.revenue, includeVAT]);
|
||||
}, [permissionFilteredData, filters, metrics.revenue, includeVAT]);
|
||||
|
||||
// Revenue trend data (weekly or daily)
|
||||
const trendData = useMemo(() => {
|
||||
@@ -514,12 +517,13 @@ function Dashboard({ data, seasons, userRole, showDataLabels, setShowDataLabels,
|
||||
|
||||
<FilterControls title={t('filters.title')} onReset={resetFilters}>
|
||||
<FilterControls.Row>
|
||||
<FilterControls.Group label={t('filters.year')}>
|
||||
<select value={filters.year} onChange={e => setFilters({...filters, year: e.target.value})}>
|
||||
<option value="all">{t('filters.allYears')}</option>
|
||||
{years.map(y => <option key={y} value={y}>{y}</option>)}
|
||||
</select>
|
||||
</FilterControls.Group>
|
||||
<PeriodPicker
|
||||
startDate={filters.startDate}
|
||||
endDate={filters.endDate}
|
||||
onChange={(start, end) => setFilters({ ...filters, startDate: start, endDate: end })}
|
||||
availableYears={years.map(Number)}
|
||||
seasons={seasons}
|
||||
/>
|
||||
<FilterControls.Group label={t('filters.district')}>
|
||||
<select value={filters.district} onChange={e => setFilters({...filters, district: e.target.value, museum: []})}>
|
||||
<option value="all">{t('filters.allDistricts')}</option>
|
||||
@@ -542,15 +546,6 @@ function Dashboard({ data, seasons, userRole, showDataLabels, setShowDataLabels,
|
||||
allLabel={t('filters.allMuseums')}
|
||||
/>
|
||||
</FilterControls.Group>
|
||||
<FilterControls.Group label={t('filters.quarter')}>
|
||||
<select value={filters.quarter} onChange={e => setFilters({...filters, quarter: e.target.value})}>
|
||||
<option value="all">{t('filters.allQuarters')}</option>
|
||||
<option value="1">{t('time.q1')}</option>
|
||||
<option value="2">{t('time.q2')}</option>
|
||||
<option value="3">{t('time.q3')}</option>
|
||||
<option value="4">{t('time.q4')}</option>
|
||||
</select>
|
||||
</FilterControls.Group>
|
||||
<FilterControls.Group label={t('filters.season')}>
|
||||
<select value={selectedSeason} onChange={e => setSelectedSeason(e.target.value)}>
|
||||
<option value="">{t('filters.allSeasons')}</option>
|
||||
|
||||
Reference in New Issue
Block a user