From fe62e50a857d55d5da3a7c11f20aaceef4c555d3 Mon Sep 17 00:00:00 2001 From: Carlos Escalante Date: Fri, 12 Jun 2026 15:27:06 -0600 Subject: [PATCH] Cosmetics: shared PeriodNavigator, breadcrumb, meter labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/components/MeterLabelsDialog.tsx | 87 +++++++++++++++++++ frontend/src/components/PeriodNavigator.tsx | 52 +++++++++++ frontend/src/pages/Budget.tsx | 67 +++++++------- frontend/src/pages/Proyecciones.tsx | 22 +++-- frontend/src/pages/ServiciosMunicipales.tsx | 33 +++++-- 5 files changed, 211 insertions(+), 50 deletions(-) create mode 100644 frontend/src/components/MeterLabelsDialog.tsx create mode 100644 frontend/src/components/PeriodNavigator.tsx diff --git a/frontend/src/components/MeterLabelsDialog.tsx b/frontend/src/components/MeterLabelsDialog.tsx new file mode 100644 index 0000000..e902147 --- /dev/null +++ b/frontend/src/components/MeterLabelsDialog.tsx @@ -0,0 +1,87 @@ +import { useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; + +import api, { type UserSettingsData } from '@/lib/api'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; + +interface Props { + meterIds: string[]; + current: Record; + settingsData: UserSettingsData; + onClose: () => void; +} + +/** Human names for water meters, persisted in UserSettings (review UX-24). */ +export default function MeterLabelsDialog({ + meterIds, + current, + settingsData, + onClose, +}: Props) { + const queryClient = useQueryClient(); + const [labels, setLabels] = useState>(() => ({ + ...current, + })); + const [saving, setSaving] = useState(false); + + const handleSave = async () => { + setSaving(true); + try { + const cleaned = Object.fromEntries( + Object.entries(labels).filter(([, v]) => v.trim() !== ''), + ); + await api.patch('/settings/', { + data: { ...settingsData, meter_labels: cleaned }, + }); + toast.success('Etiquetas guardadas'); + queryClient.invalidateQueries({ queryKey: ['settings'] }); + onClose(); + } catch { + toast.error('No se pudieron guardar las etiquetas'); + } finally { + setSaving(false); + } + }; + + return ( + { if (!open) onClose(); }}> + + + Etiquetas de medidores + +
+ {meterIds.map((id) => ( +
+ + + setLabels((prev) => ({ ...prev, [id]: e.target.value })) + } + /> +
+ ))} +
+ + + + +
+
+ ); +} diff --git a/frontend/src/components/PeriodNavigator.tsx b/frontend/src/components/PeriodNavigator.tsx new file mode 100644 index 0000000..561580b --- /dev/null +++ b/frontend/src/components/PeriodNavigator.tsx @@ -0,0 +1,52 @@ +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; + +interface Props { + label: string; + onPrev: () => void; + onNext: () => void; + prevDisabled?: boolean; + nextDisabled?: boolean; + /** 'outline' for page-level (year), 'ghost' for inline (month) */ + variant?: 'outline' | 'ghost'; + labelClassName?: string; +} + +/** The one way to step through periods (UX-03) — same chevron pattern for + * years and months across Budget and Proyecciones. */ +export default function PeriodNavigator({ + label, + onPrev, + onNext, + prevDisabled, + nextDisabled, + variant = 'outline', + labelClassName = 'w-16 text-center font-semibold tabular-nums', +}: Props) { + return ( +
+ + {label} + +
+ ); +} diff --git a/frontend/src/pages/Budget.tsx b/frontend/src/pages/Budget.tsx index 28b32bf..bd6fadb 100644 --- a/frontend/src/pages/Budget.tsx +++ b/frontend/src/pages/Budget.tsx @@ -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 */}
+ {cameFromProyecciones && ( + + )}

Presupuesto

-
- - {year} - -
+ setYear(year - 1)} + onNext={() => setYear(year + 1)} + prevDisabled={year <= MIN_YEAR} + nextDisabled={year >= MAX_YEAR} + />
@@ -133,29 +148,15 @@ export default function Budget() { Detalle Transacciones -
- - - {MONTH_NAMES[selectedMonth]} {year} - - -
+ setSelectedMonth(selectedMonth - 1)} + onNext={() => setSelectedMonth(selectedMonth + 1)} + prevDisabled={selectedMonth <= 1} + nextDisabled={selectedMonth >= 12} + /> diff --git a/frontend/src/pages/Proyecciones.tsx b/frontend/src/pages/Proyecciones.tsx index 014721d..b133e83 100644 --- a/frontend/src/pages/Proyecciones.tsx +++ b/frontend/src/pages/Proyecciones.tsx @@ -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() {

Proyecciones

-
- - {year} - -
+ setYear(year - 1)} + onNext={() => setYear(year + 1)} + prevDisabled={year <= MIN_YEAR} + nextDisabled={year >= MAX_YEAR} + /> {/* 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); diff --git a/frontend/src/pages/ServiciosMunicipales.tsx b/frontend/src/pages/ServiciosMunicipales.tsx index 6ec4cd3..2a21720 100644 --- a/frontend/src/pages/ServiciosMunicipales.tsx +++ b/frontend/src/pages/ServiciosMunicipales.tsx @@ -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([]); const fileInputRef = useRef(null); + const settingsQ = useQuery({ + queryKey: ['settings'], + queryFn: () => getUserSettings().then((r) => r.data), + }); + const meterLabels = ((settingsQ.data?.data?.meter_labels ?? {}) as Record); + 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 && (
-

- - Consumo de Agua (m³) -

+
+

+ + Consumo de Agua (m³) +

+ +
@@ -435,7 +450,7 @@ export default function ServiciosMunicipales() { opacity: hiddenMeters.has(value) ? 0.4 : 1, cursor: 'pointer', }}> - Medidor {value} + {meterLabel(value)} )} /> @@ -806,6 +821,14 @@ export default function ServiciosMunicipales() {
+ {showLabelEditor && ( + setShowLabelEditor(false)} + /> + )} ); }