import { Link } from "@tanstack/react-router"; import { BarChart3, RefreshCw } from "lucide-react"; import { useMemo, useState } from "react"; import { CartesianGrid, Line, LineChart, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { Gauge, GaugeIndicator, GaugeRange, GaugeTrack, GaugeValueText, } from "@/components/ui/gauge"; import { useDailyMinMax, useDailyQuotes, useFetchQuotes, useQuoteHistory, useQuotes, } from "@/lib/queries"; import { RelativeTime } from "@/lib/time"; import { cn } from "@/lib/utils"; 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}`; } function formatTime(iso: string): string { const d = new Date(iso); return d.toLocaleTimeString("es-AR", { hour: "2-digit", minute: "2-digit" }); } function formatShortDate(iso: string): string { const d = new Date(iso); return d.toLocaleDateString("es-AR", { day: "2-digit", month: "2-digit", }); } function getGaugeColor(percentage: number | null): string { if (percentage === null) return "text-muted-foreground"; if (percentage >= 0.8) return "text-emerald-500"; if (percentage >= 0.5) return "text-yellow-500"; return "text-red-500"; } function StatCard({ label, buy, sell, buyDate, sellDate, }: { label: string; buy: string | number; sell: string | number; buyDate?: string; sellDate?: string; }) { return (

{label}

Compra:{" "} ${priceFormatter.format(Number(buy))} {buyDate && ( {formatShortDate(buyDate)} )}

Venta:{" "} ${priceFormatter.format(Number(sell))} {sellDate && ( {formatShortDate(sellDate)} )}

); } type Period = "day" | "month"; export function BeloPage() { const [period, setPeriod] = useState("day"); const now = new Date(); const todayStr = formatDate(now); const firstOfMonth = formatDate( new Date(now.getFullYear(), now.getMonth(), 1), ); const { data: quotes, isLoading: quotesLoading } = useQuotes(); const { mutate: doFetchQuotes, isPending: fetchPending } = useFetchQuotes(); const { data: dailyMinMax } = useDailyMinMax("BELO", todayStr, todayStr); const { data: monthlyMinMax } = useDailyMinMax( "BELO", firstOfMonth, todayStr, ); const { data: history, isLoading: historyLoading } = useQuoteHistory( "BELO", todayStr, todayStr, ); const { data: daily, isLoading: dailyLoading } = useDailyQuotes( "BELO", firstOfMonth, todayStr, ); const belo = quotes?.find((q) => q.type === "BELO"); const maxData = dailyMinMax?.[0]; const currentBuy = belo ? Number(belo.buy) : 0; const currentSell = belo ? Number(belo.sell) : 0; const minBuy = maxData ? Number(maxData.minBuy) : 0; const maxBuy = maxData ? Number(maxData.maxBuy) : 0; const minSell = maxData ? Number(maxData.minSell) : 0; const maxSell = maxData ? Number(maxData.maxSell) : 0; const gaugeValue = currentBuy > 0 && maxBuy > minBuy ? Math.min(currentBuy, maxBuy) : null; const gaugePercentage = useMemo(() => { if (gaugeValue !== null && maxBuy > minBuy) { return (gaugeValue - minBuy) / (maxBuy - minBuy); } return null; }, [gaugeValue, minBuy, maxBuy]); const chartData = useMemo(() => { if (period === "day") { if (!history) return []; return history.map((h) => ({ time: formatTime(h.timeStamp), buy: Number(h.buy), sell: Number(h.sell), })); } if (!daily) return []; return daily.map((d) => ({ time: formatShortDate(d.date), buy: d.buy, sell: d.sell, })); }, [history, daily, period]); const yDomain = useMemo(() => { if (chartData.length === 0) return [0, 0]; let min = Infinity; let max = -Infinity; for (const d of chartData) { if (d.buy < min) min = d.buy; if (d.sell < min) min = d.sell; if (d.buy > max) max = d.buy; if (d.sell > max) max = d.sell; } const range = max - min; return [min - range * 0.1, max + range * 0.1]; }, [chartData]); const maxBuyValue = useMemo(() => { if (chartData.length === 0) return 0; let max = -Infinity; for (const d of chartData) { if (d.buy > max) max = d.buy; } return max; }, [chartData]); const maxSellValue = useMemo(() => { if (chartData.length === 0) return 0; let max = -Infinity; for (const d of chartData) { if (d.sell > max) max = d.sell; } return max; }, [chartData]); const monthlyMax = useMemo(() => { if (!monthlyMinMax || monthlyMinMax.length === 0) return null; let maxBuyEntry = monthlyMinMax[0]; let maxSellEntry = monthlyMinMax[0]; for (const d of monthlyMinMax) { if (d.maxBuy > maxBuyEntry.maxBuy) maxBuyEntry = d; if (d.maxSell > maxSellEntry.maxSell) maxSellEntry = d; } return { maxBuy: maxBuyEntry.maxBuy, maxBuyDate: maxBuyEntry.date, maxSell: maxSellEntry.maxSell, maxSellDate: maxSellEntry.date, }; }, [monthlyMinMax]); const isFetching = quotesLoading || fetchPending; return (

BELO

{belo && ( Últ. actualización: )} Análisis

Avance

{ const pct = mx === m ? 100 : Math.round(((v - m) / (mx - m)) * 100); return `${pct}%`; }} >

Evolución del precio

{(period === "day" ? historyLoading : dailyLoading) ? (
Cargando...
) : chartData.length === 0 ? (
Sin datos para este período
) : (
`$${priceFormatter.format(v)}`} width={80} domain={yDomain} /> [ `$${priceFormatter.format(Number(value))}`, ]} // biome-ignore lint/suspicious/noExplicitAny: recharts types labelFormatter={(label: any) => period === "day" ? `Hora: ${label}` : `Fecha: ${label}` } contentStyle={{ backgroundColor: "var(--card)", border: "1px solid var(--border)", borderRadius: "var(--radius)", fontSize: 13, }} />
)}
Compra
Venta
); }