From 52ae68c18122c475ba02746faa5356632761dd61 Mon Sep 17 00:00:00 2001 From: MarioVicente Date: Tue, 7 Oct 2025 11:38:56 -0600 Subject: [PATCH] replacing App.jsx with a Router component to manage routes and render different components based on the URL path. --- src/App.jsx | 167 ++++++++++++++++++---------------------------------- 1 file changed, 56 insertions(+), 111 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index b325ec6..b50d788 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,21 +1,27 @@ import React, { useState, useMemo, useEffect } from 'react'; -import { getUser, login, logout } from './auth'; import { PlusCircle, Trash2, CreditCard, TrendingUp, TrendingDown, DollarSign, Download, Upload } from 'lucide-react'; import { PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; -import { useTransactions } from './hooks/useTransactions'; // API hacia Azure Functions/Cosmos +import { useTransactions } from './hooks/useTransactions'; +import { getUser, login, logout } from './auth'; -const FinanceTracker = () => { - // === Filtro: por mes o histórico === - const [rangeMode, setRangeMode] = useState('month'); // 'month' | 'all' - const [month, setMonth] = useState(new Date().toISOString().slice(0, 7)); // YYYY-MM (default: mes actual) +const COLORS = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316']; - // Para pedir datos: si histórico => pasamos "" como month para traer todo +function fmt(ymDate) { return ymDate.toISOString().slice(0, 7); } +function addMonths(ym, delta) { + const [y, m] = ym.split('-').map(Number); + const d = new Date(y, (m - 1) + delta, 1); + return fmt(d); +} + +const FinanceTracker = () => { + // ====== Filtros (mes / histórico) ====== + const [rangeMode, setRangeMode] = useState('month'); // 'month' | 'all' + const [month, setMonth] = useState(new Date().toISOString().slice(0, 7)); const queryMonth = rangeMode === 'all' ? '' : month; - // --- Sesión Entra ID (SWA) --- + // ====== Sesión Entra ID (SWA) ====== const [user, setUser] = useState(null); const [loadingUser, setLoadingUser] = useState(true); - useEffect(() => { let mounted = true; (async () => { @@ -29,65 +35,15 @@ const FinanceTracker = () => { return () => { mounted = false; }; }, []); - if (loadingUser) { - return
Cargando sesión…
; - } - - if (!user) { - // Con el config ya te redirige a /login en 401, pero este fallback ayuda - window.location.href = '/login'; - return null; - } - - // Si no hay usuario aún, puedes mostrar un loader o un botón de login. - // Con tu staticwebapp.config.json ya configurado, SWA redirige 401→/login, - // pero este fallback es útil por si accedes directo a /login o en local. - if (!user) { - return ( -
-

Finance Tracker

-

Necesitas iniciar sesión.

- -
- ); - } - - - // === Datos desde Cosmos vía hook === - const userId = user.userId; // o user.userDetails si prefieres particionar por email + // ====== Datos (hook SIEMPRE se llama para mantener orden de hooks) ====== + const userId = user?.userId ?? null; // el backend ya toma userId del token const { items, loading, error, create, remove } = useTransactions(userId, queryMonth); - // Helpers para navegar meses - function fmt(ymDate) { return ymDate.toISOString().slice(0, 7); } - function addMonths(ym, delta) { - const [y, m] = ym.split('-').map(Number); - const d = new Date(y, (m - 1) + delta, 1); - return fmt(d); - } - const gotoPrev = () => setMonth(addMonths(month, -1)); - const gotoNext = () => setMonth(addMonths(month, 1)); - - // Adaptar items → shape de UI - const transactions = useMemo(() => { - return (items || []).map(t => ({ - id: t.id, - date: t.date, - type: t.amount < 0 ? 'egreso' : 'ingreso', - category: t.category, - amount: Math.abs(t.amount), - description: t.note ?? '' - })); - }, [items]); - - // Estado local de tarjetas (UI) + // ====== Estado local (resto de hooks antes de cualquier return) ====== const [creditCards, setCreditCards] = useState([ { id: 1, name: 'Visa Oro', limit: 30000, balance: 15000, cutoffDay: 15, paymentDay: 25 }, { id: 2, name: 'Mastercard', limit: 20000, balance: 5000, cutoffDay: 10, paymentDay: 20 } ]); - - // Form transacción const [newTransaction, setNewTransaction] = useState({ date: new Date().toISOString().split('T')[0], type: 'egreso', @@ -95,16 +51,9 @@ const FinanceTracker = () => { amount: '', description: '' }); - - // Form tarjeta const [newCard, setNewCard] = useState({ - name: '', - limit: '', - balance: '', - cutoffDay: '', - paymentDay: '' + name: '', limit: '', balance: '', cutoffDay: '', paymentDay: '' }); - const [activeTab, setActiveTab] = useState('dashboard'); const categories = { @@ -112,30 +61,35 @@ const FinanceTracker = () => { egreso: ['Alimentación', 'Transporte', 'Vivienda', 'Servicios', 'Entretenimiento', 'Salud', 'Educación', 'Otros'] }; - const COLORS = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316']; + const transactions = useMemo(() => { + return (items || []).map(t => ({ + id: t.id, + date: t.date, + type: t.amount < 0 ? 'egreso' : 'ingreso', + category: t.category, + amount: Math.abs(t.amount), + description: t.note ?? '' + })); + }, [items]); - // Crear en Cosmos + // ====== Acciones ====== const addTransaction = async () => { const { category, amount, description, date, type } = newTransaction; const n = Number(amount); + if (!userId) { alert('No hay usuario autenticado'); return; } if (!category || Number.isNaN(n)) return; - const signed = type === 'egreso' ? -Math.abs(n) : Math.abs(n); - await create({ date, amount: signed, category, note: description, account: 'Other' }); - + const res = await create({ date, amount: signed, category, note: description, account: 'Other' }); + // reset setNewTransaction({ date: new Date().toISOString().split('T')[0], - type: 'egreso', - category: '', - amount: '', - description: '' + type: 'egreso', category: '', amount: '', description: '' }); + return res; }; - // Borrar en Cosmos const deleteTransaction = async (id) => { await remove(id); }; - // Export/Import local (import crea transacciones en Cosmos) const exportData = () => { const data = { transactions, creditCards, exportDate: new Date().toISOString() }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); @@ -154,8 +108,7 @@ const FinanceTracker = () => { reader.onload = async (e) => { try { const data = JSON.parse(e.target.result); - let ok = 0, fail = 0, errors = []; - + let ok = 0, fail = 0; if (Array.isArray(data.transactions)) { const chunks = (arr, size) => arr.reduce((a,_,i)=> (i%size? a[a.length-1].push(arr[i]) : a.push([arr[i]]), a), []); for (const batch of chunks(data.transactions, 5)) { @@ -169,11 +122,10 @@ const FinanceTracker = () => { account: 'Other' }); })); - results.forEach(r => r.status === 'fulfilled' ? ok++ : (fail++, errors.push(String(r.reason)))); + results.forEach(r => r.status === 'fulfilled' ? ok++ : fail++); } } if (Array.isArray(data.creditCards)) setCreditCards(data.creditCards); - alert(`Importación completada. Éxitos: ${ok}, Fallos: ${fail}`); } catch (err) { console.error(err); @@ -183,10 +135,10 @@ const FinanceTracker = () => { reader.readAsText(file); }; - // Estadísticas (sobre el rango actualmente cargado) + // ====== Estadísticas ====== const statistics = useMemo(() => { - const totalIngresos = transactions.filter(t => t.type === 'ingreso').reduce((sum, t) => sum + t.amount, 0); - const totalEgresos = transactions.filter(t => t.type === 'egreso').reduce((sum, t) => sum + t.amount, 0); + const totalIngresos = transactions.filter(t => t.type === 'ingreso').reduce((s, t) => s + t.amount, 0); + const totalEgresos = transactions.filter(t => t.type === 'egreso').reduce((s, t) => s + t.amount, 0); const balance = totalIngresos - totalEgresos; const egresosPorCategoria = transactions @@ -203,13 +155,21 @@ const FinanceTracker = () => { }, {}); const lineData = Object.values(transactionsByMonth).sort((a, b) => a.month.localeCompare(b.month)); - const totalCreditLimit = creditCards.reduce((sum, c) => sum + c.limit, 0); - const totalCreditUsed = creditCards.reduce((sum, c) => sum + c.balance, 0); + const totalCreditLimit = creditCards.reduce((s, c) => s + c.limit, 0); + const totalCreditUsed = creditCards.reduce((s, c) => s + c.balance, 0); const creditAvailable = totalCreditLimit - totalCreditUsed; return { totalIngresos, totalEgresos, balance, pieData, lineData, totalCreditLimit, totalCreditUsed, creditAvailable }; }, [transactions, creditCards]); + // ====== Early returns (después de TODOS los hooks) ====== + if (loadingUser) return
Cargando sesión…
; + if (!user) { window.location.href = '/login'; return null; } + + const gotoPrev = () => setMonth(addMonths(month, -1)); + const gotoNext = () => setMonth(addMonths(month, 1)); + + // ====== UI ====== return (
@@ -218,7 +178,8 @@ const FinanceTracker = () => { Control de Finanzas Personales -
+
+ {user.userDetails}
-
- {user.userDetails} - -
- - {/* Tabs */}
{['dashboard', 'transacciones', 'tarjetas'].map(tab => ( @@ -256,11 +210,7 @@ const FinanceTracker = () => { {/* Selector de rango (Mes / Histórico) */}
- setRangeMode(e.target.value)} className="border rounded px-2 py-1"> @@ -282,7 +232,6 @@ const FinanceTracker = () => { )}
- {/* Estados de carga/errores (para dashboard/transacciones) */} {(activeTab !== 'tarjetas') && ( <> {loading &&

Cargando transacciones…

} @@ -550,18 +499,14 @@ const FinanceTracker = () => { /> setNewCard({ ...newCard, cutoffDay: e.target.value })} className="border border-slate-300 rounded-lg px-4 py-2" /> setNewCard({ ...newCard, paymentDay: e.target.value })} className="border border-slate-300 rounded-lg px-4 py-2"