Files
personal-admin-2026/apps/frontend/src/features/analysis/components/MonthlyVariationTable.tsx

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>
</>
);
}