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

@@ -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,

View File

@@ -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)

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