Cosmetics: shared PeriodNavigator, breadcrumb, meter labels

One chevron component now drives year/month stepping in Budget and
Proyecciones (UX-03). Jumping from Proyecciones to a Budget month shows
a back affordance (UX-22). Water meters can be named (Casa, Cochera…)
via a settings-persisted dialog; the chart legend uses the names
(UX-24).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-06-12 15:27:06 -06:00
parent 7dfe2da2a9
commit fe62e50a85
5 changed files with 211 additions and 50 deletions

View File

@@ -1,5 +1,6 @@
import { useState, useCallback } from 'react';
import { ChevronLeft, ChevronRight, Calculator, Download } from 'lucide-react';
import { ArrowLeft, Calculator, Download } from 'lucide-react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -9,6 +10,7 @@ import { useBudget } from '@/hooks/useBudget';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import ErrorState from '@/components/ErrorState';
import PeriodNavigator from '@/components/PeriodNavigator';
import MonthlyDetail from '@/components/budget/MonthlyDetail';
import RecurringItemsManager from '@/components/budget/RecurringItemsManager';
import TransactionList from '@/components/TransactionList';
@@ -34,6 +36,10 @@ export default function Budget() {
refresh,
} = useBudget(currentYear);
const queryClient = useQueryClient();
const location = useLocation();
const navigate = useNavigate();
const cameFromProyecciones =
(location.state as { from?: string } | null)?.from === 'proyecciones';
const [subTab, setSubTab] = useState<'detail' | 'transactions'>('detail');
const [txSearch, setTxSearch] = useState('');
@@ -103,18 +109,27 @@ export default function Budget() {
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{cameFromProyecciones && (
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/proyecciones')}
aria-label="Volver a Proyecciones"
>
<ArrowLeft className="w-4 h-4 mr-1" aria-hidden="true" />
Proyecciones
</Button>
)}
<Calculator className="w-6 h-6 text-primary" />
<h1 className="text-2xl font-bold tracking-tight">Presupuesto</h1>
</div>
<div className="flex items-center gap-1">
<Button variant="outline" size="icon" disabled={year <= MIN_YEAR} onClick={() => setYear(year - 1)}>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="w-16 text-center font-semibold tabular-nums">{year}</span>
<Button variant="outline" size="icon" disabled={year >= MAX_YEAR} onClick={() => setYear(year + 1)}>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
<PeriodNavigator
label={String(year)}
onPrev={() => setYear(year - 1)}
onNext={() => setYear(year + 1)}
prevDisabled={year <= MIN_YEAR}
nextDisabled={year >= MAX_YEAR}
/>
</div>
<Tabs defaultValue="overview">
@@ -133,29 +148,15 @@ export default function Budget() {
<TabsTrigger value="detail">Detalle</TabsTrigger>
<TabsTrigger value="transactions">Transacciones</TabsTrigger>
</TabsList>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
disabled={selectedMonth <= 1}
onClick={() => setSelectedMonth(selectedMonth - 1)}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="text-sm font-medium w-28 text-center">
{MONTH_NAMES[selectedMonth]} {year}
</span>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
disabled={selectedMonth >= 12}
onClick={() => setSelectedMonth(selectedMonth + 1)}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
<PeriodNavigator
variant="ghost"
label={`${MONTH_NAMES[selectedMonth]} ${year}`}
labelClassName="text-sm font-medium w-28 text-center"
onPrev={() => setSelectedMonth(selectedMonth - 1)}
onNext={() => setSelectedMonth(selectedMonth + 1)}
prevDisabled={selectedMonth <= 1}
nextDisabled={selectedMonth >= 12}
/>
</div>
<TabsContent value="detail" className="space-y-6 mt-4">

View File

@@ -1,12 +1,12 @@
import { ChevronLeft, ChevronRight, Loader2, TrendingUp } from 'lucide-react';
import { Loader2, TrendingUp } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useBudget } from '@/hooks/useBudget';
import { formatAmount } from '@/lib/format';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import ErrorState from '@/components/ErrorState';
import PeriodNavigator from '@/components/PeriodNavigator';
import YearlyOverview from '@/components/budget/YearlyOverview';
const MIN_YEAR = 2026;
@@ -36,15 +36,13 @@ export default function Proyecciones() {
<TrendingUp className="w-6 h-6 text-primary" />
<h1 className="text-2xl font-bold tracking-tight">Proyecciones</h1>
</div>
<div className="flex items-center gap-1">
<Button variant="outline" size="icon" disabled={year <= MIN_YEAR} onClick={() => setYear(year - 1)}>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="w-16 text-center font-semibold tabular-nums">{year}</span>
<Button variant="outline" size="icon" disabled={year >= MAX_YEAR} onClick={() => setYear(year + 1)}>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
<PeriodNavigator
label={String(year)}
onPrev={() => setYear(year - 1)}
onNext={() => setYear(year + 1)}
prevDisabled={year <= MIN_YEAR}
nextDisabled={year >= MAX_YEAR}
/>
</div>
{/* Annual summary cards */}
@@ -101,7 +99,7 @@ export default function Proyecciones() {
year={year}
onSelectMonth={(m) => {
setSelectedMonth(m);
navigate('/budget');
navigate('/budget', { state: { from: 'proyecciones' } });
}}
onSaveOverride={async (month, value) => {
await saveBalanceOverride(year, month, value);

View File

@@ -1,6 +1,7 @@
import { useState, useCallback, useRef, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { MONTH_NAMES_ES_SHORT as MONTH_NAMES_ES } from '@/lib/dates';
import MeterLabelsDialog from '@/components/MeterLabelsDialog';
import ErrorState from '@/components/ErrorState';
import {
BarChart,
@@ -44,6 +45,7 @@ import {
type MunicipalReceipt,
type MunicipalReceiptUploadResult,
type WaterMeterReading,
getUserSettings,
} from '@/lib/api';
// ─── Constants ───────────────────────────────────────────────────────────────
@@ -195,6 +197,14 @@ export default function ServiciosMunicipales() {
const [uploadResults, setUploadResults] = useState<MunicipalReceiptUploadResult[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null);
const settingsQ = useQuery({
queryKey: ['settings'],
queryFn: () => getUserSettings().then((r) => r.data),
});
const meterLabels = ((settingsQ.data?.data?.meter_labels ?? {}) as Record<string, string>);
const meterLabel = (id: string) => meterLabels[id] || `Medidor ${id}`;
const [showLabelEditor, setShowLabelEditor] = useState(false);
const municipalQ = useQuery({
queryKey: ['municipal'],
queryFn: async () => {
@@ -396,10 +406,15 @@ export default function ServiciosMunicipales() {
{/* ── Water Consumption Chart ─────────────────────────────────────── */}
{chartData.length > 0 && (
<section className="space-y-3">
<h2 className="text-base font-semibold font-heading flex items-center gap-2 text-muted-foreground uppercase tracking-wider">
<Droplets className="w-4 h-4" />
Consumo de Agua (m³)
</h2>
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold font-heading flex items-center gap-2 text-muted-foreground uppercase tracking-wider">
<Droplets className="w-4 h-4" />
Consumo de Agua (m³)
</h2>
<Button variant="outline" size="sm" onClick={() => setShowLabelEditor(true)}>
Etiquetas
</Button>
</div>
<Card>
<CardContent className="p-4">
<ResponsiveContainer width="100%" height={280}>
@@ -435,7 +450,7 @@ export default function ServiciosMunicipales() {
opacity: hiddenMeters.has(value) ? 0.4 : 1,
cursor: 'pointer',
}}>
Medidor {value}
{meterLabel(value)}
</span>
)}
/>
@@ -806,6 +821,14 @@ export default function ServiciosMunicipales() {
</CardContent>
</Card>
</section>
{showLabelEditor && (
<MeterLabelsDialog
meterIds={meterIds}
current={meterLabels}
settingsData={settingsQ.data?.data ?? {}}
onClose={() => setShowLabelEditor(false)}
/>
)}
</div>
);
}