feat: custom date range filter on calendar page

- Backend: /api/eco/calendar accepts date_from + date_to query params
  when period=custom; get_calendar() uses them directly without override
  Limit raised to 5000 for wide date ranges
- Frontend: "Custom" tab at end of period list; shows two date inputs
  (from/to) with Apply button; displays day count; fetches on Apply click
  (not on every keystroke to avoid hammering the API)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-26 17:17:01 +02:00
parent c36b2b2198
commit d9762deca7
3 changed files with 69 additions and 17 deletions

View File

@@ -41,16 +41,17 @@ interface ImportStatus {
// ── Constants ─────────────────────────────────────────────────────────────────
const PERIODS = [
{ key: 'recent', label: 'Recent' },
{ key: 'today', label: 'Today' },
{ key: 'tomorrow', label: 'Tomorrow' },
{ key: 'this_week', label: 'This Week' },
{ key: 'next_week', label: 'Next Week' },
{ key: 'this_month', label: 'This Month' },
{ key: 'next_month', label: 'Next Month' },
{ key: 'yesterday', label: 'Yesterday' },
{ key: 'previous_week', label: 'Prev Week' },
{ key: 'previous_month','label': 'Prev Month' },
{ key: 'recent', label: 'Recent' },
{ key: 'today', label: 'Today' },
{ key: 'tomorrow', label: 'Tomorrow' },
{ key: 'this_week', label: 'This Week' },
{ key: 'next_week', label: 'Next Week' },
{ key: 'this_month', label: 'This Month' },
{ key: 'next_month', label: 'Next Month' },
{ key: 'yesterday', label: 'Yesterday' },
{ key: 'previous_week', label: 'Prev Week' },
{ key: 'previous_month', label: 'Prev Month' },
{ key: 'custom', label: '📅 Custom' },
] as const
const ALL_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD', 'NZD', 'CHF', 'CNY']
@@ -306,6 +307,8 @@ function ImpactFilter({ selected, onChange }: { selected: string[]; onChange: (v
export default function CalendarPage() {
const [period, setPeriod] = useState<string>('recent')
const [customFrom, setCustomFrom] = useState<string>('')
const [customTo, setCustomTo] = useState<string>('')
const [currencies, setCurrencies] = useState<string[]>(['USD'])
const [impacts, setImpacts] = useState<string[]>(['high', 'medium'])
const [data, setData] = useState<CalendarResponse | null>(null)
@@ -323,10 +326,16 @@ export default function CalendarPage() {
}, [])
const fetchEvents = useCallback(async () => {
// For custom period, require both dates
if (period === 'custom' && (!customFrom || !customTo)) return
setLoading(true)
setError(null)
try {
const params = new URLSearchParams({ period, limit: '500' })
const params = new URLSearchParams({ period, limit: '2000' })
if (period === 'custom') {
params.set('date_from', customFrom)
params.set('date_to', customTo)
}
if (currencies.length > 0) params.set('currencies', currencies.join(','))
if (impacts.length > 0) params.set('impacts', impacts.join(','))
const r: CalendarResponse = await fetch(`${API}/api/eco/calendar?${params}`).then(x => x.json())
@@ -336,7 +345,7 @@ export default function CalendarPage() {
} finally {
setLoading(false)
}
}, [period, currencies, impacts])
}, [period, customFrom, customTo, currencies, impacts])
useEffect(() => { fetchStats() }, [fetchStats])
useEffect(() => { fetchEvents() }, [fetchEvents])
@@ -409,6 +418,38 @@ export default function CalendarPage() {
))}
</div>
{/* Custom date range picker */}
{period === 'custom' && (
<div className="flex flex-wrap items-center gap-3 mb-3 p-3 bg-slate-800 border border-slate-700 rounded-lg">
<span className="text-slate-400 text-sm">From</span>
<input
type="date"
value={customFrom}
onChange={e => setCustomFrom(e.target.value)}
className="bg-slate-700 border border-slate-600 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-blue-500"
/>
<span className="text-slate-400 text-sm">To</span>
<input
type="date"
value={customTo}
onChange={e => setCustomTo(e.target.value)}
className="bg-slate-700 border border-slate-600 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-blue-500"
/>
<button
onClick={fetchEvents}
disabled={!customFrom || !customTo}
className="px-3 py-1.5 rounded bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-sm text-white"
>
Apply
</button>
{customFrom && customTo && (
<span className="text-slate-500 text-xs">
{Math.round((new Date(customTo).getTime() - new Date(customFrom).getTime()) / 86400000) + 1} days
</span>
)}
</div>
)}
{/* Filters toggle */}
<div className="mb-3">
<button