138 lines
5.1 KiB
TypeScript
138 lines
5.1 KiB
TypeScript
import { Minus, RefreshCw, TrendingDown, TrendingUp } from "lucide-react";
|
|
import { RelativeTime } from "@/lib/time";
|
|
import type { Quote } from "@/lib/api";
|
|
import { useQuotes, useFetchQuotes, useMonthlyPayedTotal } from "@/lib/queries";
|
|
import { useNavigate } from "@tanstack/react-router";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
|
|
const priceFormatter = new Intl.NumberFormat("es-AR", {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
});
|
|
|
|
function QuoteCard({ quote, title, to, variant }: { quote: Quote | undefined; title: string; to?: string; variant?: "default" | "minimal" }) {
|
|
const navigate = useNavigate();
|
|
const diff = quote ? Number(quote.difference) : 0;
|
|
const pct = quote ? Number(quote.percentage) : 0;
|
|
const isUp = diff > 0;
|
|
const isDown = diff < 0;
|
|
|
|
return (
|
|
<div
|
|
className="rounded-lg border p-4 cursor-pointer"
|
|
onClick={() => to && navigate({ to })}
|
|
onKeyDown={(e) => { if (e.key === "Enter" && to) navigate({ to }); }}
|
|
role={to ? "button" : undefined}
|
|
tabIndex={to ? 0 : undefined}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-1.5">
|
|
<p className="text-sm text-muted-foreground">{title}</p>
|
|
{quote && (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger className="cursor-pointer inline-flex">
|
|
{isUp ? (
|
|
<TrendingUp className="h-4 w-4 text-emerald-500" />
|
|
) : isDown ? (
|
|
<TrendingDown className="h-4 w-4 text-red-500" />
|
|
) : (
|
|
<Minus className="h-4 w-4 text-muted-foreground/50" />
|
|
)}
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>
|
|
{isUp
|
|
? `Subió $${priceFormatter.format(diff)} (${pct.toFixed(2)}%)`
|
|
: isDown
|
|
? `Bajó $${priceFormatter.format(Math.abs(diff))} (${pct.toFixed(2)}%)`
|
|
: "Sin cambios"}
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
)}
|
|
</div>
|
|
{quote && (
|
|
<span className="text-xs text-muted-foreground">
|
|
<RelativeTime date={quote.timeStamp} />
|
|
</span>
|
|
)}
|
|
</div>
|
|
{quote ? (
|
|
variant === "minimal" ? (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger className="cursor-default">
|
|
<p className="text-2xl font-bold tabular-nums text-left">${priceFormatter.format(Number(quote.buy))}</p>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
<p>Compra: ${priceFormatter.format(Number(quote.buy))}</p>
|
|
<p>Venta: ${priceFormatter.format(Number(quote.sell))}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
) : (
|
|
<div className="space-y-1">
|
|
<p className="text-sm">
|
|
Compra: <span className="text-lg font-bold">${priceFormatter.format(Number(quote.buy))}</span>
|
|
</p>
|
|
<p className="text-sm">
|
|
Venta: <span className="text-lg font-bold">${priceFormatter.format(Number(quote.sell))}</span>
|
|
</p>
|
|
</div>
|
|
)
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">Sin datos</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function DashboardPage() {
|
|
const { data: quotes, isLoading } = useQuotes();
|
|
const { mutate: doFetchQuotes, isPending } = useFetchQuotes();
|
|
|
|
const belo = quotes?.find((q) => q.type === "BELO");
|
|
const blue = quotes?.find((q) => q.type === "BLUE");
|
|
|
|
const now = new Date();
|
|
const { data: monthlyExpenses } = useMonthlyPayedTotal(now.getFullYear(), now.getMonth() + 1);
|
|
|
|
const isFetching = isLoading || isPending;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-semibold">Panel</h1>
|
|
<button
|
|
type="button"
|
|
disabled={isFetching}
|
|
onClick={() => doFetchQuotes()}
|
|
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`} />
|
|
Actualizar
|
|
</button>
|
|
</div>
|
|
<div className="grid gap-4 md:grid-cols-4">
|
|
<div className="rounded-lg border p-4">
|
|
<p className="text-sm text-muted-foreground">Total ingresos</p>
|
|
<p className="text-2xl font-bold">$0.00</p>
|
|
</div>
|
|
<div className="rounded-lg border p-4">
|
|
<p className="text-sm text-muted-foreground">Gastos del mes</p>
|
|
<p className="text-2xl font-bold">${priceFormatter.format(monthlyExpenses?.total ?? 0)}</p>
|
|
</div>
|
|
<QuoteCard quote={belo} title="BELO" to="/quotes/belo" variant="minimal" />
|
|
<QuoteCard quote={blue} title="BLUE" variant="minimal" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|