342 lines
10 KiB
TypeScript
342 lines
10 KiB
TypeScript
import { ArrowDown, ArrowRight, ArrowUp, TrendingUp } from "lucide-react";
|
|
import { useMemo } from "react";
|
|
import { useInflation, useMonthlyLast } from "@/lib/queries";
|
|
|
|
type MonthKey = string;
|
|
|
|
type CurrencyValues = {
|
|
variation: number | null;
|
|
prev: number | null;
|
|
current: number | null;
|
|
};
|
|
|
|
type MonthRow = {
|
|
month: MonthKey;
|
|
label: string;
|
|
BELO: CurrencyValues;
|
|
BLUE: CurrencyValues;
|
|
BNA: CurrencyValues;
|
|
inflation: number | null;
|
|
};
|
|
|
|
const MONTH_NAMES = [
|
|
"Enero",
|
|
"Febrero",
|
|
"Marzo",
|
|
"Abril",
|
|
"Mayo",
|
|
"Junio",
|
|
"Julio",
|
|
"Agosto",
|
|
"Septiembre",
|
|
"Octubre",
|
|
"Noviembre",
|
|
"Diciembre",
|
|
];
|
|
|
|
function formatMonth(monthKey: string): string {
|
|
const [y, m] = monthKey.split("-");
|
|
return `${MONTH_NAMES[Number(m) - 1]} ${y}`;
|
|
}
|
|
|
|
function formatPct(value: number): string {
|
|
const sign = value >= 0 ? "+" : "";
|
|
return `${sign}${value.toFixed(1)}%`;
|
|
}
|
|
|
|
const priceFormatter = new Intl.NumberFormat("es-AR", {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
});
|
|
|
|
function formatPrice(value: number): string {
|
|
return `$${priceFormatter.format(value)}`;
|
|
}
|
|
|
|
function VariationIndicator({
|
|
variation,
|
|
inflation,
|
|
prev,
|
|
current,
|
|
}: {
|
|
variation: number | null;
|
|
inflation: number | null;
|
|
prev: number | null;
|
|
current: number | null;
|
|
}) {
|
|
if (variation === null) {
|
|
return (
|
|
<span className="text-xs text-muted-foreground inline-flex items-center gap-1">
|
|
<ArrowRight className="size-3" />
|
|
--
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const indicator = (
|
|
<span
|
|
className={`text-xs font-medium inline-flex items-center gap-1 tabular-nums ${
|
|
inflation !== null
|
|
? variation > inflation
|
|
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400 px-1.5 py-0.5 rounded"
|
|
: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 px-1.5 py-0.5 rounded"
|
|
: variation >= 0
|
|
? "text-emerald-500"
|
|
: "text-red-500"
|
|
}`}
|
|
>
|
|
{variation >= 0 ? (
|
|
<ArrowUp className="size-3" />
|
|
) : (
|
|
<ArrowDown className="size-3" />
|
|
)}
|
|
{formatPct(variation)}
|
|
</span>
|
|
);
|
|
|
|
if (prev !== null && current !== null) {
|
|
return (
|
|
<span className="inline-flex items-center gap-1.5">
|
|
{indicator}
|
|
<span className="text-[10px] text-muted-foreground tabular-nums">
|
|
{formatPrice(prev)} → {formatPrice(current)}
|
|
</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return indicator;
|
|
}
|
|
|
|
export function MonthlyVariationTable() {
|
|
const { data: monthlyData, isLoading: monthlyLoading } = useMonthlyLast();
|
|
const { data: inflationData, isLoading: inflationLoading } = useInflation();
|
|
|
|
const rows = useMemo<MonthRow[]>(() => {
|
|
if (!monthlyData || monthlyData.length === 0) return [];
|
|
|
|
const monthsMap = new Map<
|
|
MonthKey,
|
|
{ BELO: number | null; BLUE: number | null; BNA: number | null }
|
|
>();
|
|
|
|
for (const entry of monthlyData) {
|
|
const existing = monthsMap.get(entry.month) ?? {
|
|
BELO: null,
|
|
BLUE: null,
|
|
BNA: null,
|
|
};
|
|
if (entry.type === "BELO") existing.BELO = entry.buy;
|
|
if (entry.type === "BLUE") existing.BLUE = entry.buy;
|
|
if (entry.type === "BNA") existing.BNA = entry.buy;
|
|
monthsMap.set(entry.month, existing);
|
|
}
|
|
|
|
const inflationMap = new Map<MonthKey, number>();
|
|
if (inflationData) {
|
|
for (const inf of inflationData) {
|
|
const monthKey = inf.fecha.slice(0, 7);
|
|
inflationMap.set(monthKey, inf.valor);
|
|
}
|
|
}
|
|
|
|
const sortedMonths = [...monthsMap.keys()].sort();
|
|
|
|
const calcVariation = (
|
|
currentVal: number | null,
|
|
prevVal: number | null,
|
|
): number | null => {
|
|
if (currentVal === null || prevVal === null || prevVal === 0) return null;
|
|
return ((currentVal - prevVal) / prevVal) * 100;
|
|
};
|
|
|
|
return sortedMonths
|
|
.map((month, i) => {
|
|
const prev = i > 0 ? sortedMonths[i - 1] : null;
|
|
const current = monthsMap.get(month);
|
|
const prevData = prev ? monthsMap.get(prev) : null;
|
|
|
|
const beloVar = calcVariation(
|
|
current?.BELO ?? null,
|
|
prevData?.BELO ?? null,
|
|
);
|
|
const blueVar = calcVariation(
|
|
current?.BLUE ?? null,
|
|
prevData?.BLUE ?? null,
|
|
);
|
|
const bnaVar = calcVariation(
|
|
current?.BNA ?? null,
|
|
prevData?.BNA ?? null,
|
|
);
|
|
|
|
let inflation: number | null = null;
|
|
inflation = inflationMap.get(month) ?? null;
|
|
|
|
return {
|
|
month,
|
|
label: formatMonth(month),
|
|
BELO: {
|
|
variation: beloVar,
|
|
prev: prevData?.BELO ?? null,
|
|
current: current?.BELO ?? null,
|
|
},
|
|
BLUE: {
|
|
variation: blueVar,
|
|
prev: prevData?.BLUE ?? null,
|
|
current: current?.BLUE ?? null,
|
|
},
|
|
BNA: {
|
|
variation: bnaVar,
|
|
prev: prevData?.BNA ?? null,
|
|
current: current?.BNA ?? null,
|
|
},
|
|
inflation,
|
|
};
|
|
})
|
|
.reverse();
|
|
}, [monthlyData, inflationData]);
|
|
|
|
const isLoading = monthlyLoading || inflationLoading;
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-32 text-muted-foreground text-sm">
|
|
Cargando datos mensuales...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (rows.length === 0) {
|
|
return (
|
|
<div className="flex items-center justify-center h-32 text-muted-foreground text-sm">
|
|
Sin datos mensuales disponibles
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="space-y-2 sm:hidden">
|
|
{rows.map((row) => (
|
|
<div
|
|
key={row.month}
|
|
className="rounded-lg border bg-card p-3 text-sm"
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="font-medium">{row.label}</span>
|
|
{row.inflation !== null ? (
|
|
<span className="text-xs text-muted-foreground tabular-nums inline-flex items-center gap-1">
|
|
<TrendingUp className="size-3" />
|
|
{formatPct(row.inflation)} inflación
|
|
</span>
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">
|
|
Sin inflación
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<div>
|
|
<p className="text-[10px] text-muted-foreground mb-0.5">BELO</p>
|
|
<VariationIndicator
|
|
variation={row.BELO.variation}
|
|
inflation={row.inflation}
|
|
prev={row.BELO.prev}
|
|
current={row.BELO.current}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] text-muted-foreground mb-0.5">Blue</p>
|
|
<VariationIndicator
|
|
variation={row.BLUE.variation}
|
|
inflation={row.inflation}
|
|
prev={row.BLUE.prev}
|
|
current={row.BLUE.current}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] text-muted-foreground mb-0.5">
|
|
Oficial
|
|
</p>
|
|
<VariationIndicator
|
|
variation={row.BNA.variation}
|
|
inflation={row.inflation}
|
|
prev={row.BNA.prev}
|
|
current={row.BNA.current}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="hidden overflow-x-auto rounded-lg border sm:block">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b bg-muted/50">
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground">
|
|
Mes
|
|
</th>
|
|
<th className="px-3 py-2 text-right text-xs font-medium text-muted-foreground">
|
|
BELO
|
|
</th>
|
|
<th className="px-3 py-2 text-right text-xs font-medium text-muted-foreground">
|
|
Blue
|
|
</th>
|
|
<th className="px-3 py-2 text-right text-xs font-medium text-muted-foreground">
|
|
Oficial
|
|
</th>
|
|
<th className="px-3 py-2 text-right text-xs font-medium text-muted-foreground">
|
|
Inflación
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rows.map((row) => (
|
|
<tr
|
|
key={row.month}
|
|
className="border-b last:border-0 hover:bg-muted/30"
|
|
>
|
|
<td className="px-3 py-2.5 font-medium">{row.label}</td>
|
|
<td className="px-3 py-2.5 text-right">
|
|
<VariationIndicator
|
|
variation={row.BELO.variation}
|
|
inflation={row.inflation}
|
|
prev={row.BELO.prev}
|
|
current={row.BELO.current}
|
|
/>
|
|
</td>
|
|
<td className="px-3 py-2.5 text-right">
|
|
<VariationIndicator
|
|
variation={row.BLUE.variation}
|
|
inflation={row.inflation}
|
|
prev={row.BLUE.prev}
|
|
current={row.BLUE.current}
|
|
/>
|
|
</td>
|
|
<td className="px-3 py-2.5 text-right">
|
|
<VariationIndicator
|
|
variation={row.BNA.variation}
|
|
inflation={row.inflation}
|
|
prev={row.BNA.prev}
|
|
current={row.BNA.current}
|
|
/>
|
|
</td>
|
|
<td className="px-3 py-2.5 text-right">
|
|
{row.inflation !== null ? (
|
|
<span className="text-xs text-muted-foreground tabular-nums">
|
|
{formatPct(row.inflation)}
|
|
</span>
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">--</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|