mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 10:28:47 +02:00
Standardize page headers and chart tokens across all pages
PageHeader on Budget, Proyecciones, Pensiones, Planificador, Municipalidad, Sincronización, Financiamientos, Asistente. Pensiones fund identity and Municipalidad meter/charge series now use the validated chart tokens (color-mix for translucent fills). Planificador stat cards on StatTile; deterministic chart rendering everywhere. Municipalidad page title now matches its nav/breadcrumb label. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@copilotkit/react-core/v2";
|
} from "@copilotkit/react-core/v2";
|
||||||
import { useCopilotAction } from "@copilotkit/react-core";
|
import { useCopilotAction } from "@copilotkit/react-core";
|
||||||
import { Sparkles } from "lucide-react";
|
import { Sparkles } from "lucide-react";
|
||||||
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { SpendingSummaryCard, type SpendingSummaryArgs } from "@/components/chat/ChatCards";
|
import { SpendingSummaryCard, type SpendingSummaryArgs } from "@/components/chat/ChatCards";
|
||||||
|
|
||||||
const STATIC_SUGGESTIONS = {
|
const STATIC_SUGGESTIONS = {
|
||||||
@@ -87,13 +88,11 @@ export default function Asistente() {
|
|||||||
// max-w wrapper are all min-h-0 flex columns) — no vh math.
|
// max-w wrapper are all min-h-0 flex columns) — no vh math.
|
||||||
<div className="flex flex-1 min-h-0 flex-col">
|
<div className="flex flex-1 min-h-0 flex-col">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h1 className="text-2xl font-bold tracking-tight flex items-center gap-2" style={{ fontFamily: "var(--font-heading)" }}>
|
<PageHeader
|
||||||
<Sparkles className="w-5 h-5 text-primary" />
|
icon={Sparkles}
|
||||||
Asistente
|
title="Asistente"
|
||||||
</h1>
|
subtitle="Pregúntale a WealthySmart sobre tus finanzas."
|
||||||
<p className="text-sm text-muted-foreground">
|
/>
|
||||||
Pregúntale a WealthySmart sobre tus finanzas.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-h-0 rounded-xl border border-border overflow-hidden bg-card">
|
<div className="flex-1 min-h-0 rounded-xl border border-border overflow-hidden bg-card">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { useBudget } from '@/hooks/useBudget';
|
import { useBudget } from '@/hooks/useBudget';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { PageHeader } from '@/components/page-header';
|
||||||
import ErrorState from '@/components/ErrorState';
|
import ErrorState from '@/components/ErrorState';
|
||||||
import PeriodNavigator from '@/components/PeriodNavigator';
|
import PeriodNavigator from '@/components/PeriodNavigator';
|
||||||
import MonthlyDetail from '@/components/budget/MonthlyDetail';
|
import MonthlyDetail from '@/components/budget/MonthlyDetail';
|
||||||
@@ -128,31 +129,32 @@ export default function Budget() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
{cameFromProyecciones && (
|
||||||
<div className="flex items-center gap-3">
|
<Button
|
||||||
{cameFromProyecciones && (
|
variant="ghost"
|
||||||
<Button
|
size="sm"
|
||||||
variant="ghost"
|
className="-mb-4 text-muted-foreground"
|
||||||
size="sm"
|
onClick={() => navigate('/proyecciones')}
|
||||||
className="mr-4 text-muted-foreground"
|
aria-label="Volver a Proyecciones"
|
||||||
onClick={() => navigate('/proyecciones')}
|
>
|
||||||
aria-label="Volver a Proyecciones"
|
<ArrowLeft className="w-4 h-4 mr-1" aria-hidden="true" />
|
||||||
>
|
Proyecciones
|
||||||
<ArrowLeft className="w-4 h-4 mr-1" aria-hidden="true" />
|
</Button>
|
||||||
Proyecciones
|
)}
|
||||||
</Button>
|
<PageHeader
|
||||||
)}
|
icon={Calculator}
|
||||||
<Calculator className="w-6 h-6 text-primary" />
|
title="Presupuesto"
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Presupuesto</h1>
|
subtitle="Proyección y transacciones del mes"
|
||||||
</div>
|
actions={
|
||||||
<PeriodNavigator
|
<PeriodNavigator
|
||||||
label={String(year)}
|
label={String(year)}
|
||||||
onPrev={() => setYear(year - 1)}
|
onPrev={() => setYear(year - 1)}
|
||||||
onNext={() => setYear(year + 1)}
|
onNext={() => setYear(year + 1)}
|
||||||
prevDisabled={year <= MIN_YEAR}
|
prevDisabled={year <= MIN_YEAR}
|
||||||
nextDisabled={year >= MAX_YEAR}
|
nextDisabled={year >= MAX_YEAR}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Tabs defaultValue="overview">
|
<Tabs defaultValue="overview">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import ErrorState from '@/components/ErrorState';
|
import ErrorState from '@/components/ErrorState';
|
||||||
|
import { PageHeader } from '@/components/page-header';
|
||||||
import ConvertToInstallmentsDialog from '@/components/transactions/ConvertToInstallmentsDialog';
|
import ConvertToInstallmentsDialog from '@/components/transactions/ConvertToInstallmentsDialog';
|
||||||
|
|
||||||
function formatDate(iso: string) {
|
function formatDate(iso: string) {
|
||||||
@@ -104,14 +105,11 @@ export default function Financiamientos() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-3">
|
<PageHeader
|
||||||
<Layers className="w-6 h-6 text-primary" />
|
icon={Layers}
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Financiamientos</h1>
|
title="Financiamientos"
|
||||||
</div>
|
subtitle="Compras Tasa Cero en cuotas sin intereses — las cuotas futuras ya cuentan en el presupuesto de sus meses."
|
||||||
<p className="text-sm text-muted-foreground -mt-4">
|
/>
|
||||||
Compras Tasa Cero pagadas en cuotas mensuales sin intereses. Las cuotas
|
|
||||||
futuras ya cuentan en el presupuesto de sus meses.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useMemo, useCallback, useRef } from 'react';
|
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import ErrorState from '@/components/ErrorState';
|
import ErrorState from '@/components/ErrorState';
|
||||||
|
import { PageHeader } from '@/components/page-header';
|
||||||
import PensionAssumptionsDialog, { type PensionAssumptions } from '@/components/PensionAssumptionsDialog';
|
import PensionAssumptionsDialog, { type PensionAssumptions } from '@/components/PensionAssumptionsDialog';
|
||||||
import {
|
import {
|
||||||
LineChart,
|
LineChart,
|
||||||
@@ -103,7 +104,7 @@ const FUNDS_DEFAULT: Record<FundKey, FundDef> = {
|
|||||||
key: 'FCL',
|
key: 'FCL',
|
||||||
name: 'FCL',
|
name: 'FCL',
|
||||||
fullName: 'Fondo de Capitalización Laboral',
|
fullName: 'Fondo de Capitalización Laboral',
|
||||||
color: '#3b82f6',
|
color: 'var(--chart-5)',
|
||||||
startBalance: 650_468,
|
startBalance: 650_468,
|
||||||
monthlyContribution: 150_000,
|
monthlyContribution: 150_000,
|
||||||
annualRate: 7.5,
|
annualRate: 7.5,
|
||||||
@@ -115,7 +116,7 @@ const FUNDS_DEFAULT: Record<FundKey, FundDef> = {
|
|||||||
key: 'ROP',
|
key: 'ROP',
|
||||||
name: 'ROP',
|
name: 'ROP',
|
||||||
fullName: 'Régimen Obligatorio de Pensiones',
|
fullName: 'Régimen Obligatorio de Pensiones',
|
||||||
color: '#10b981',
|
color: 'var(--chart-1)',
|
||||||
startBalance: 18_684_765,
|
startBalance: 18_684_765,
|
||||||
monthlyContribution: 120_000,
|
monthlyContribution: 120_000,
|
||||||
annualRate: 6.0,
|
annualRate: 6.0,
|
||||||
@@ -127,7 +128,7 @@ const FUNDS_DEFAULT: Record<FundKey, FundDef> = {
|
|||||||
key: 'VOL',
|
key: 'VOL',
|
||||||
name: 'VOL',
|
name: 'VOL',
|
||||||
fullName: 'Fondo Voluntario',
|
fullName: 'Fondo Voluntario',
|
||||||
color: '#f43f5e',
|
color: 'var(--chart-4)',
|
||||||
startBalance: 2_500_381,
|
startBalance: 2_500_381,
|
||||||
monthlyContribution: 400_000,
|
monthlyContribution: 400_000,
|
||||||
annualRate: 8.0,
|
annualRate: 8.0,
|
||||||
@@ -480,17 +481,11 @@ export default function Pensions() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* ── Page Header ─────────────────────────────────────────────────── */}
|
{/* ── Page Header ─────────────────────────────────────────────────── */}
|
||||||
<div className="flex items-center gap-3">
|
<PageHeader
|
||||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
icon={PiggyBank}
|
||||||
<PiggyBank className="w-5 h-5 text-primary" />
|
title="Pensiones"
|
||||||
</div>
|
subtitle="Seguimiento de aportes, rendimientos y proyecciones"
|
||||||
<div>
|
/>
|
||||||
<h1 className="text-2xl font-bold font-heading">Pensiones</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Seguimiento de aportes, rendimientos y proyecciones
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Section 1: Fund Overview Cards ──────────────────────────────── */}
|
{/* ── Section 1: Fund Overview Cards ──────────────────────────────── */}
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
@@ -606,7 +601,7 @@ export default function Pensions() {
|
|||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all border cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all border cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
style={{
|
style={{
|
||||||
borderColor: fund.color,
|
borderColor: fund.color,
|
||||||
background: active ? fund.color + '22' : 'transparent',
|
background: active ? `color-mix(in oklab, ${fund.color} 13%, transparent)` : 'transparent',
|
||||||
color: active ? fund.color : 'var(--muted-foreground)',
|
color: active ? fund.color : 'var(--muted-foreground)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -652,6 +647,7 @@ export default function Pensions() {
|
|||||||
stroke={FUNDS[key].color}
|
stroke={FUNDS[key].color}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dot={false}
|
dot={false}
|
||||||
|
isAnimationActive={false}
|
||||||
activeDot={{ r: 4 }}
|
activeDot={{ r: 4 }}
|
||||||
/>
|
/>
|
||||||
) : null,
|
) : null,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Line, LineChart, XAxis, YAxis } from 'recharts';
|
|||||||
import { Telescope } from 'lucide-react';
|
import { Telescope } from 'lucide-react';
|
||||||
|
|
||||||
import { getYearlyProjection } from '@/lib/api';
|
import { getYearlyProjection } from '@/lib/api';
|
||||||
|
import { PageHeader } from '@/components/page-header';
|
||||||
|
import { StatTile } from '@/components/stat-tile';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -78,15 +80,11 @@ export default function Planificador() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-3">
|
<PageHeader
|
||||||
<Telescope className="w-6 h-6 text-primary" aria-hidden="true" />
|
icon={Telescope}
|
||||||
<div>
|
title="Planificador"
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Planificador</h1>
|
subtitle="¿Qué pasa si ahorrás un poco más cada mes?"
|
||||||
<p className="text-sm text-muted-foreground">
|
/>
|
||||||
¿Qué pasa si ahorrás un poco más cada mes?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4 grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<CardContent className="p-4 grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
@@ -138,41 +136,18 @@ export default function Planificador() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
<Card>
|
<StatTile label="Ritmo actual" value={formatCRC(baselineEnd)} />
|
||||||
<CardContent className="p-4">
|
<StatTile label="Escenario" value={formatCRC(scenarioEnd)} />
|
||||||
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
<StatTile
|
||||||
Ritmo actual
|
label="Diferencia"
|
||||||
</p>
|
value={`+${formatCRC(scenarioEnd - baselineEnd)}`}
|
||||||
<p data-sensitive className="text-xl font-bold font-mono">
|
className="[&_span[data-sensitive]]:text-primary"
|
||||||
{formatCRC(baselineEnd)}
|
/>
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
|
||||||
Escenario
|
|
||||||
</p>
|
|
||||||
<p data-sensitive className="text-xl font-bold font-mono text-primary">
|
|
||||||
{formatCRC(scenarioEnd)}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
|
||||||
Diferencia
|
|
||||||
</p>
|
|
||||||
<p data-sensitive className="text-xl font-bold font-mono text-emerald-500">
|
|
||||||
+{formatCRC(scenarioEnd - baselineEnd)}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-sm uppercase tracking-wider text-muted-foreground">
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
Crecimiento comparado
|
Crecimiento comparado
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -191,8 +166,8 @@ export default function Planificador() {
|
|||||||
<ChartTooltipContent formatter={(value) => formatCRC(Number(value))} />
|
<ChartTooltipContent formatter={(value) => formatCRC(Number(value))} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Line type="monotone" dataKey="baseline" stroke="var(--chart-2)" dot={false} strokeWidth={2} />
|
<Line type="monotone" dataKey="baseline" stroke="var(--chart-2)" dot={false} strokeWidth={2} isAnimationActive={false} />
|
||||||
<Line type="monotone" dataKey="scenario" stroke="var(--chart-1)" dot={false} strokeWidth={2} />
|
<Line type="monotone" dataKey="scenario" stroke="var(--chart-1)" dot={false} strokeWidth={2} isAnimationActive={false} />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useBudget } from '@/hooks/useBudget';
|
|||||||
import { formatAmount } from '@/lib/format';
|
import { formatAmount } from '@/lib/format';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { PageHeader } from '@/components/page-header';
|
||||||
import ErrorState from '@/components/ErrorState';
|
import ErrorState from '@/components/ErrorState';
|
||||||
import PeriodNavigator from '@/components/PeriodNavigator';
|
import PeriodNavigator from '@/components/PeriodNavigator';
|
||||||
import YearlyOverview from '@/components/budget/YearlyOverview';
|
import YearlyOverview from '@/components/budget/YearlyOverview';
|
||||||
@@ -30,20 +31,20 @@ export default function Proyecciones() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
<PageHeader
|
||||||
<div className="flex items-center justify-between">
|
icon={TrendingUp}
|
||||||
<div className="flex items-center gap-3">
|
title="Proyecciones"
|
||||||
<TrendingUp className="w-6 h-6 text-primary" />
|
subtitle="Presupuesto anual mes a mes"
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Proyecciones</h1>
|
actions={
|
||||||
</div>
|
<PeriodNavigator
|
||||||
<PeriodNavigator
|
label={String(year)}
|
||||||
label={String(year)}
|
onPrev={() => setYear(year - 1)}
|
||||||
onPrev={() => setYear(year - 1)}
|
onNext={() => setYear(year + 1)}
|
||||||
onNext={() => setYear(year + 1)}
|
prevDisabled={year <= MIN_YEAR}
|
||||||
prevDisabled={year <= MIN_YEAR}
|
nextDisabled={year >= MAX_YEAR}
|
||||||
nextDisabled={year >= MAX_YEAR}
|
/>
|
||||||
/>
|
}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{/* Annual summary cards */}
|
{/* Annual summary cards */}
|
||||||
{projection && (
|
{projection && (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useCallback, useRef, useMemo } from 'react';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { MONTH_NAMES_ES_SHORT as MONTH_NAMES_ES } from '@/lib/dates';
|
import { MONTH_NAMES_ES_SHORT as MONTH_NAMES_ES } from '@/lib/dates';
|
||||||
import MeterLabelsDialog from '@/components/MeterLabelsDialog';
|
import MeterLabelsDialog from '@/components/MeterLabelsDialog';
|
||||||
|
import { PageHeader } from '@/components/page-header';
|
||||||
import ErrorState from '@/components/ErrorState';
|
import ErrorState from '@/components/ErrorState';
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
@@ -51,13 +52,13 @@ import {
|
|||||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const METER_COLORS: Record<string, string> = {
|
const METER_COLORS: Record<string, string> = {
|
||||||
'7335': '#3b82f6',
|
'7335': 'var(--chart-5)',
|
||||||
'7345': '#10b981',
|
'7345': 'var(--chart-1)',
|
||||||
'9345': '#f59e0b',
|
'9345': 'var(--chart-2)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const DEFAULT_METER_COLOR = '#8b5cf6';
|
const DEFAULT_METER_COLOR = 'var(--chart-3)';
|
||||||
|
|
||||||
// ─── Utilities ───────────────────────────────────────────────────────────────
|
// ─── Utilities ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -105,8 +106,11 @@ function getMeterIds(readings: WaterMeterReading[]): string[] {
|
|||||||
|
|
||||||
// ─── Charge Trend Data ───────────────────────────────────────────────────────
|
// ─── Charge Trend Data ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Fixed categorical slots (validated palette); series beyond the 5th read
|
||||||
|
// muted — never cycle hues.
|
||||||
const CHARGE_COLORS = [
|
const CHARGE_COLORS = [
|
||||||
'#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16',
|
'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)',
|
||||||
|
'var(--chart-5)', 'var(--muted-foreground)',
|
||||||
];
|
];
|
||||||
|
|
||||||
interface ChargeTrendPoint {
|
interface ChargeTrendPoint {
|
||||||
@@ -306,17 +310,11 @@ export default function ServiciosMunicipales() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* ── Page Header ─────────────────────────────────────────────────── */}
|
{/* ── Page Header ─────────────────────────────────────────────────── */}
|
||||||
<div className="flex items-center gap-3">
|
<PageHeader
|
||||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
icon={Droplets}
|
||||||
<Droplets className="w-5 h-5 text-primary" />
|
title="Municipalidad"
|
||||||
</div>
|
subtitle="Municipalidad de Belén — recibos y consumo de agua"
|
||||||
<div>
|
/>
|
||||||
<h1 className="text-2xl font-bold font-heading">Servicios Municipales</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Municipalidad de Belén — recibos y consumo de agua
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Summary Cards ───────────────────────────────────────────────── */}
|
{/* ── Summary Cards ───────────────────────────────────────────────── */}
|
||||||
<section className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<section className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
@@ -546,7 +544,7 @@ export default function ServiciosMunicipales() {
|
|||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey={name}
|
dataKey={name}
|
||||||
name={name}
|
name={name}
|
||||||
stroke={CHARGE_COLORS[i % CHARGE_COLORS.length]}
|
stroke={CHARGE_COLORS[Math.min(i, CHARGE_COLORS.length - 1)]}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dot={{ r: 3 }}
|
dot={{ r: 3 }}
|
||||||
activeDot={{ r: 5 }}
|
activeDot={{ r: 5 }}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { formatRelativeAge } from '@/lib/dates';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { PageHeader } from '@/components/page-header';
|
||||||
import ErrorState from '@/components/ErrorState';
|
import ErrorState from '@/components/ErrorState';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
@@ -38,29 +39,25 @@ export default function SyncStatus() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<PageHeader
|
||||||
<div className="flex items-center gap-3">
|
icon={RefreshCw}
|
||||||
<RefreshCw className="w-6 h-6 text-primary" aria-hidden="true" />
|
title="Sincronización"
|
||||||
<div>
|
subtitle="Estado de los flujos automáticos de datos (n8n y tipo de cambio)"
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Sincronización</h1>
|
actions={
|
||||||
<p className="text-sm text-muted-foreground">
|
<Button
|
||||||
Estado de los flujos automáticos de datos (n8n y tipo de cambio)
|
variant="outline"
|
||||||
</p>
|
size="sm"
|
||||||
</div>
|
onClick={() => query.refetch()}
|
||||||
</div>
|
disabled={query.isFetching}
|
||||||
<Button
|
>
|
||||||
variant="outline"
|
<RefreshCw
|
||||||
size="sm"
|
className={query.isFetching ? 'w-4 h-4 mr-2 animate-spin' : 'w-4 h-4 mr-2'}
|
||||||
onClick={() => query.refetch()}
|
aria-hidden="true"
|
||||||
disabled={query.isFetching}
|
/>
|
||||||
>
|
Actualizar
|
||||||
<RefreshCw
|
</Button>
|
||||||
className={query.isFetching ? 'w-4 h-4 mr-2 animate-spin' : 'w-4 h-4 mr-2'}
|
}
|
||||||
aria-hidden="true"
|
/>
|
||||||
/>
|
|
||||||
Actualizar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{query.isError ? (
|
{query.isError ? (
|
||||||
<ErrorState
|
<ErrorState
|
||||||
|
|||||||
Reference in New Issue
Block a user