From d9762deca72bd832ef0efdcfad7d50898bbbcd37 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Fri, 26 Jun 2026 17:17:01 +0200 Subject: [PATCH] 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 --- backend/routers/eco.py | 8 +++- backend/services/ff_calendar.py | 13 ++++-- frontend/src/pages/CalendarPage.tsx | 65 +++++++++++++++++++++++------ 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/backend/routers/eco.py b/backend/routers/eco.py index ed57792..69bd9ba 100644 --- a/backend/routers/eco.py +++ b/backend/routers/eco.py @@ -421,10 +421,12 @@ def ff_scrape_status_ep() -> Dict[str, Any]: @router.get("/calendar") def ff_calendar( - period: str = Query("recent", description="recent|today|tomorrow|yesterday|this_week|next_week|previous_week|this_month|next_month|previous_month"), + period: str = Query("recent", description="recent|today|tomorrow|yesterday|this_week|next_week|previous_week|this_month|next_month|previous_month|custom"), + date_from: Optional[str] = Query(None, description="YYYY-MM-DD (used when period=custom)"), + date_to: Optional[str] = Query(None, description="YYYY-MM-DD (used when period=custom)"), currencies: Optional[str] = Query(None, description="Comma-separated: USD,EUR,GBP,..."), impacts: Optional[str] = Query(None, description="Comma-separated: high,medium,low"), - limit: int = Query(300, ge=1, le=2000), + limit: int = Query(500, ge=1, le=5000), offset: int = Query(0, ge=0), ) -> Dict[str, Any]: """Unified Forex Factory calendar — past releases + upcoming events.""" @@ -433,6 +435,8 @@ def ff_calendar( imp_list = [i.strip().lower() for i in impacts.split(",") if i.strip()] if impacts else None return get_calendar( period=period, + date_from=date_from, + date_to=date_to, currencies=cur_list, impacts=imp_list, limit=limit, diff --git a/backend/services/ff_calendar.py b/backend/services/ff_calendar.py index 7549df3..675de10 100644 --- a/backend/services/ff_calendar.py +++ b/backend/services/ff_calendar.py @@ -232,6 +232,8 @@ def sync_live() -> Dict[str, Any]: def get_calendar( period: str = "recent", + date_from: Optional[str] = None, + date_to: Optional[str] = None, currencies: Optional[List[str]] = None, impacts: Optional[List[str]] = None, limit: int = 300, @@ -243,6 +245,7 @@ def get_calendar( period options: recent | today | tomorrow | yesterday | this_week | next_week | previous_week | this_month | next_month | previous_month | custom + When period=custom, date_from and date_to are used directly. """ from services.database import get_conn @@ -250,10 +253,14 @@ def get_calendar( monday = today - timedelta(days=today.weekday()) sunday = monday + timedelta(days=6) - date_from: Optional[str] = None - date_to: Optional[str] = None + # For custom period, date_from/date_to come from the caller — don't override + if period != "custom": + date_from = None + date_to = None - if period == "today": + if period == "custom": + pass # date_from / date_to already set by caller + elif period == "today": date_from = date_to = str(today) elif period == "tomorrow": t = today + timedelta(days=1) diff --git a/frontend/src/pages/CalendarPage.tsx b/frontend/src/pages/CalendarPage.tsx index a467bc9..42f73ba 100644 --- a/frontend/src/pages/CalendarPage.tsx +++ b/frontend/src/pages/CalendarPage.tsx @@ -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('recent') + const [customFrom, setCustomFrom] = useState('') + const [customTo, setCustomTo] = useState('') const [currencies, setCurrencies] = useState(['USD']) const [impacts, setImpacts] = useState(['high', 'medium']) const [data, setData] = useState(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() { ))} + {/* Custom date range picker */} + {period === 'custom' && ( +
+ From + 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" + /> + To + 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" + /> + + {customFrom && customTo && ( + + {Math.round((new Date(customTo).getTime() - new Date(customFrom).getTime()) / 86400000) + 1} days + + )} +
+ )} + {/* Filters toggle */}