mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 08:08:48 +02:00
Inicio: hover-cards on stat tiles
Gasto del ciclo reveals the by-source breakdown, Próximas cuotas the full active-plan list, Pensión last-period rendimientos per fund. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,11 @@ import { Link } from "react-router-dom";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
/** Stat-tile contract (dataviz skill): label · value · optional delta ·
|
||||
@@ -21,6 +26,8 @@ export interface StatTileProps {
|
||||
/** Makes the whole tile a link (adds hover border + focus ring). */
|
||||
to?: string;
|
||||
ariaLabel?: string;
|
||||
/** Extra detail revealed on hover (HoverCard). */
|
||||
hoverContent?: React.ReactNode;
|
||||
/** Set false for non-monetary values that shouldn't blur (counts, dates). */
|
||||
sensitive?: boolean;
|
||||
className?: string;
|
||||
@@ -35,6 +42,7 @@ export function StatTile({
|
||||
children,
|
||||
to,
|
||||
ariaLabel,
|
||||
hoverContent,
|
||||
sensitive = true,
|
||||
className,
|
||||
}: StatTileProps) {
|
||||
@@ -83,8 +91,7 @@ export function StatTile({
|
||||
</Card>
|
||||
);
|
||||
|
||||
if (!to) return card;
|
||||
return (
|
||||
const wrapped = to ? (
|
||||
<Link
|
||||
to={to}
|
||||
aria-label={ariaLabel ?? label}
|
||||
@@ -92,5 +99,17 @@ export function StatTile({
|
||||
>
|
||||
{card}
|
||||
</Link>
|
||||
) : (
|
||||
card
|
||||
);
|
||||
|
||||
if (!hoverContent) return wrapped;
|
||||
return (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger render={<div className="h-full" />}>
|
||||
{wrapped}
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-80">{hoverContent}</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,36 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const CYCLE = currentBudgetCycle();
|
||||
|
||||
const SOURCE_LABELS: Record<string, string> = {
|
||||
CREDIT_CARD: "Tarjeta",
|
||||
CASH: "Efectivo",
|
||||
TRANSFER: "Transferencias",
|
||||
};
|
||||
|
||||
function HoverRows({
|
||||
title,
|
||||
rows,
|
||||
}: {
|
||||
title: string;
|
||||
rows: { label: string; value: string }[];
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-sm font-medium">{title}</p>
|
||||
<div className="space-y-1">
|
||||
{rows.map((r) => (
|
||||
<div key={r.label} className="flex justify-between gap-3 text-xs">
|
||||
<span className="text-muted-foreground truncate">{r.label}</span>
|
||||
<span data-sensitive className="font-mono shrink-0">
|
||||
{r.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GastoDelCicloCard() {
|
||||
const q = useQuery({
|
||||
queryKey: ["budget", "month", CYCLE.year, CYCLE.month],
|
||||
@@ -44,6 +74,25 @@ function GastoDelCicloCard() {
|
||||
loading={q.isPending}
|
||||
to="/budget"
|
||||
ariaLabel="Ver presupuesto"
|
||||
hoverContent={
|
||||
q.data ? (
|
||||
<HoverRows
|
||||
title="Desglose por fuente"
|
||||
rows={[
|
||||
...q.data.actuals_by_source
|
||||
.filter((s) => s.count > 0)
|
||||
.map((s) => ({
|
||||
label: `${SOURCE_LABELS[s.source] ?? s.source} (${s.count})`,
|
||||
value: formatAmount(s.net, "CRC"),
|
||||
})),
|
||||
{
|
||||
label: "Gran total egresos",
|
||||
value: formatAmount(q.data.gran_total_egresos, "CRC"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{balance !== null && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -105,6 +154,17 @@ function ProximasCuotasCard() {
|
||||
loading={q.isPending}
|
||||
to="/financiamientos"
|
||||
ariaLabel="Ver financiamientos"
|
||||
hoverContent={
|
||||
upcoming.length > 0 ? (
|
||||
<HoverRows
|
||||
title="Planes activos"
|
||||
rows={upcoming.map((p) => ({
|
||||
label: `${p.merchant} (${p.cuotas_billed}/${p.num_installments})`,
|
||||
value: `${formatShortDate(p.next_cuota_date!)} · ${formatAmount(nextCuotaOf(p), p.currency)}`,
|
||||
}))}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{topThree.length > 0 && (
|
||||
<ul className="space-y-0.5 text-xs text-muted-foreground">
|
||||
@@ -146,6 +206,17 @@ function PensionCard() {
|
||||
loading={q.isPending}
|
||||
to="/pensions"
|
||||
ariaLabel="Ver pensiones"
|
||||
hoverContent={
|
||||
q.data && q.data.length > 0 ? (
|
||||
<HoverRows
|
||||
title="Último período por fondo"
|
||||
rows={q.data.map((f) => ({
|
||||
label: `${f.fund} · rendimientos`,
|
||||
value: `+${formatAmount(f.rendimientos, "CRC")}`,
|
||||
}))}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{q.data && q.data.length > 0 && (
|
||||
<ul className="space-y-0.5 text-xs text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user