import { Link, useNavigate } from "@tanstack/react-router"; import { subDays, subMonths } from "date-fns"; import { ArrowLeft } from "lucide-react"; import { useMemo, useState } from "react"; import { CartesianGrid, Line, LineChart, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { DatePicker } from "@/components/ui/date-picker"; import { useDailyQuotes, useHistoricalMinMax, useMonthlyLast, useQuotes, } from "@/lib/queries"; import { MonthlyVariationTable } from "./components/MonthlyVariationTable"; const CURRENCIES = ["BELO", "BLUE", "BNA"] as const; const CURRENCY_LABELS: Record = { BELO: "BELO", BLUE: "Dólar Blue", BNA: "Dólar Oficial", }; const priceFormatter = new Intl.NumberFormat("es-AR", { minimumFractionDigits: 2, maximumFractionDigits: 2, }); function formatDate(d: Date): string { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } const dateFormatter = new Intl.DateTimeFormat("es-AR", { day: "numeric", month: "long", year: "numeric", }); function formatShortDate(iso: string): string { const datePart = iso.split(" ")[0]; const [y, m, d] = datePart.split("-").map(Number); return dateFormatter.format(new Date(y, m - 1, d)); } export function AnalysisPage({ currency }: { currency: string }) { const navigate = useNavigate(); const today = new Date(); const defaultStart = subDays(today, 30); const [startDate, setStartDate] = useState(defaultStart); const [endDate, setEndDate] = useState(today); const startDateStr = formatDate(startDate); const endDateStr = formatDate(endDate); const { data: historical, isLoading: historicalLoading } = useHistoricalMinMax(currency); const { data: quotes } = useQuotes(); const { data: daily, isLoading: dailyLoading } = useDailyQuotes( currency, startDateStr, endDateStr, ); const { data: beloDaily, isLoading: beloLoading } = useDailyQuotes( "BELO", startDateStr, endDateStr, ); const { data: blueDaily, isLoading: blueLoading } = useDailyQuotes( "BLUE", startDateStr, endDateStr, ); const { data: bnaDaily, isLoading: bnaLoading } = useDailyQuotes( "BNA", startDateStr, endDateStr, ); const yesterday = subDays(today, 1); const yesterdayStr = formatDate(yesterday); const { data: yesterdayQuote } = useDailyQuotes( currency, yesterdayStr, yesterdayStr, ); const { data: monthlyLastData } = useMonthlyLast(); const chartData = useMemo(() => { if (!daily) return []; return daily.map((d) => ({ date: formatShortDate(d.date), buy: d.buy, })); }, [daily]); const comparisonData = useMemo(() => { const map = new Map< string, { date: string; BELO?: number; BLUE?: number; BNA?: number } >(); const add = ( rows: { date: string; buy: number }[] | undefined, key: "BELO" | "BLUE" | "BNA", ) => { rows?.forEach((d) => { const entry = map.get(d.date) ?? { date: d.date }; entry[key] = d.buy; map.set(d.date, entry); }); }; add(beloDaily, "BELO"); add(blueDaily, "BLUE"); add(bnaDaily, "BNA"); return [...map.values()] .sort((a, b) => a.date.localeCompare(b.date)) .map(({ date, ...rest }) => ({ date: formatShortDate(date), ...rest })); }, [beloDaily, blueDaily, bnaDaily]); const comparisonDomain = useMemo(() => { if (comparisonData.length === 0) return [0, 0] as [number, number]; let min = Infinity; let max = -Infinity; for (const d of comparisonData) { for (const key of ["BELO", "BLUE", "BNA"] as const) { const value = d[key]; if (value === undefined) continue; if (value < min) min = value; if (value > max) max = value; } } if (!Number.isFinite(min) || !Number.isFinite(max)) return [0, 0] as [number, number]; const range = max - min; return [min - range * 0.2, max + range * 0.2] as [number, number]; }, [comparisonData]); const { yDomain, minBuyValue, maxBuyValue, avgBuyValue } = useMemo(() => { if (chartData.length === 0) return { yDomain: [0, 0] as [number, number], minBuyValue: 0, maxBuyValue: 0, avgBuyValue: 0, }; let min = Infinity; let max = -Infinity; let sum = 0; for (const d of chartData) { if (d.buy < min) min = d.buy; if (d.buy > max) max = d.buy; sum += d.buy; } const range = max - min || 1; return { yDomain: [min - range * 0.1, max + range * 0.1] as [number, number], minBuyValue: min, maxBuyValue: max, avgBuyValue: sum / chartData.length, }; }, [chartData]); const currentQuote = quotes?.find((q) => q.type === currency); const currentBuy = currentQuote ? Number(currentQuote.buy) : 0; const yesterdayBuy = yesterdayQuote?.[0]?.buy ?? null; const prevDayDiff = yesterdayBuy !== null ? currentBuy - yesterdayBuy : null; const prevDayPct = prevDayDiff !== null && yesterdayBuy !== null && yesterdayBuy !== 0 ? (prevDayDiff / yesterdayBuy) * 100 : null; const previousMonthBuy = useMemo(() => { if (!monthlyLastData || monthlyLastData.length === 0) return null; const prevMonth = subMonths(today, 1); const prevMonthKey = `${prevMonth.getFullYear()}-${String(prevMonth.getMonth() + 1).padStart(2, "0")}`; const entry = monthlyLastData.find( (e) => e.month === prevMonthKey && e.type === currency, ); return entry?.buy ?? null; }, [monthlyLastData, currency, today]); const monthDiff = previousMonthBuy !== null ? currentBuy - previousMonthBuy : null; const monthPct = monthDiff !== null && previousMonthBuy !== null && previousMonthBuy !== 0 ? (monthDiff / previousMonthBuy) * 100 : null; const backTo = currency === "BELO" ? "/quotes/belo" : "/quotes"; const backLabel = currency === "BELO" ? "BELO" : "Cotizaciones"; function VariationValue({ value, pct, }: { value: number | null; pct: number | null; }) { if (value === null || pct === null) return

Sin datos

; const isPositive = value >= 0; const color = isPositive ? "text-emerald-500" : "text-red-500"; const sign = isPositive ? "+" : ""; return (

{sign}${priceFormatter.format(Math.abs(value))}{" "} ({sign} {pct.toFixed(2)}%)

); } return (
{backLabel}

Análisis

{CURRENCIES.map((c) => ( ))}

Cotización actual (compra)

${priceFormatter.format(currentBuy)}

Mínimo histórico (compra)

{historicalLoading ? (

Cargando...

) : ( <>

${priceFormatter.format(historical?.minBuy ?? 0)}

{historical?.minBuyDate && (

el {formatShortDate(historical.minBuyDate)}

)} )}

Máximo histórico (compra)

{historicalLoading ? (

Cargando...

) : ( <>

${priceFormatter.format(historical?.maxBuy ?? 0)}

{historical?.maxBuyDate && (

el {formatShortDate(historical.maxBuyDate)}

)} )}

Vs. día anterior

Vs. mes anterior

Evolución del precio de compra

Desde d && setStartDate(d)} placeholder="Fecha inicio" />
Hasta d && setEndDate(d)} placeholder="Fecha fin" />
{dailyLoading ? (
Cargando...
) : chartData.length === 0 ? (
Sin datos para este período
) : ( <>
`$${priceFormatter.format(v)}` } width={80} domain={yDomain} /> [ `$${priceFormatter.format(Number(value))}`, "Compra", ]} labelFormatter={(label: unknown) => `Fecha: ${label}`} contentStyle={{ backgroundColor: "var(--card)", border: "1px solid var(--border)", borderRadius: "var(--radius)", fontSize: 13, }} />
Compra
Máx
Mín
Prom
)}

Precio de compra por cotización

{beloLoading || blueLoading || bnaLoading ? (
Cargando...
) : comparisonData.length === 0 ? (
Sin datos para este período
) : ( <>
`$${priceFormatter.format(v)}` } width={80} domain={comparisonDomain} /> [ `$${priceFormatter.format(Number(value))}`, CURRENCY_LABELS[String(name)] ?? String(name), ]} labelFormatter={(label: unknown) => `Fecha: ${label}`} contentStyle={{ backgroundColor: "var(--card)", border: "1px solid var(--border)", borderRadius: "var(--radius)", fontSize: 13, }} />
BELO
Dólar Blue
Dólar Oficial
)}

Variación mensual vs. inflación

); }