mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 10:08:46 +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 { cn } from "@/lib/utils";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from "@/components/ui/hover-card";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
/** Stat-tile contract (dataviz skill): label · value · optional delta ·
|
/** 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). */
|
/** Makes the whole tile a link (adds hover border + focus ring). */
|
||||||
to?: string;
|
to?: string;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
|
/** Extra detail revealed on hover (HoverCard). */
|
||||||
|
hoverContent?: React.ReactNode;
|
||||||
/** Set false for non-monetary values that shouldn't blur (counts, dates). */
|
/** Set false for non-monetary values that shouldn't blur (counts, dates). */
|
||||||
sensitive?: boolean;
|
sensitive?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -35,6 +42,7 @@ export function StatTile({
|
|||||||
children,
|
children,
|
||||||
to,
|
to,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
|
hoverContent,
|
||||||
sensitive = true,
|
sensitive = true,
|
||||||
className,
|
className,
|
||||||
}: StatTileProps) {
|
}: StatTileProps) {
|
||||||
@@ -83,8 +91,7 @@ export function StatTile({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!to) return card;
|
const wrapped = to ? (
|
||||||
return (
|
|
||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
aria-label={ariaLabel ?? label}
|
aria-label={ariaLabel ?? label}
|
||||||
@@ -92,5 +99,17 @@ export function StatTile({
|
|||||||
>
|
>
|
||||||
{card}
|
{card}
|
||||||
</Link>
|
</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 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() {
|
function GastoDelCicloCard() {
|
||||||
const q = useQuery({
|
const q = useQuery({
|
||||||
queryKey: ["budget", "month", CYCLE.year, CYCLE.month],
|
queryKey: ["budget", "month", CYCLE.year, CYCLE.month],
|
||||||
@@ -44,6 +74,25 @@ function GastoDelCicloCard() {
|
|||||||
loading={q.isPending}
|
loading={q.isPending}
|
||||||
to="/budget"
|
to="/budget"
|
||||||
ariaLabel="Ver presupuesto"
|
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 && (
|
{balance !== null && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@@ -105,6 +154,17 @@ function ProximasCuotasCard() {
|
|||||||
loading={q.isPending}
|
loading={q.isPending}
|
||||||
to="/financiamientos"
|
to="/financiamientos"
|
||||||
ariaLabel="Ver 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 && (
|
{topThree.length > 0 && (
|
||||||
<ul className="space-y-0.5 text-xs text-muted-foreground">
|
<ul className="space-y-0.5 text-xs text-muted-foreground">
|
||||||
@@ -146,6 +206,17 @@ function PensionCard() {
|
|||||||
loading={q.isPending}
|
loading={q.isPending}
|
||||||
to="/pensions"
|
to="/pensions"
|
||||||
ariaLabel="Ver pensiones"
|
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 && (
|
{q.data && q.data.length > 0 && (
|
||||||
<ul className="space-y-0.5 text-xs text-muted-foreground">
|
<ul className="space-y-0.5 text-xs text-muted-foreground">
|
||||||
|
|||||||
Reference in New Issue
Block a user