Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
109 changes: 109 additions & 0 deletions api/cards-post/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// api/cards-post/index.js
const { CosmosClient } = require("@azure/cosmos");
const { randomUUID } = require("crypto");

function getClientPrincipal(req) {
const v = req.headers["x-ms-client-principal"];
if (!v) return null;
try { return JSON.parse(Buffer.from(v, "base64").toString("utf8")); }
catch { return null; }
}

const endpoint = process.env.COSMOS_ENDPOINT;
const key = process.env.COSMOS_KEY;
const databaseId = process.env.COSMOS_DB || "finance";
const containerId = process.env.COSMOS_CARDS_CONTAINER || "cards";

module.exports = async function (context, req) {
const where = (step, extra) => context.log(`[cards-post] ${step}`, extra || "");
try {
where("start");

const principal = getClientPrincipal(req);
if (!principal) {
where("no principal");
context.res = { status: 401, body: { error: "Unauthorized" } };
return;
}

const userId = principal.userId;
const body = req.body || {};
// Normaliza/valida
const name = (body.name || "").toString().trim();
const limit = Number(body.limit);
const balance = Number(body.balance || 0);
const cutoffDay = Number(body.cutoffDay || 1);
const paymentDay = Number(body.paymentDay || 10);

if (!name) {
context.res = { status: 400, body: { error: "El nombre es requerido" } };
return;
}
if (!Number.isFinite(limit) || limit <= 0) {
context.res = { status: 400, body: { error: "El límite debe ser un número > 0" } };
return;
}
if (!Number.isFinite(balance) || balance < 0) {
context.res = { status: 400, body: { error: "El saldo debe ser un número >= 0" } };
return;
}
if (!Number.isInteger(cutoffDay) || cutoffDay < 1 || cutoffDay > 31) {
context.res = { status: 400, body: { error: "El día de corte debe estar entre 1 y 31" } };
return;
}
if (!Number.isInteger(paymentDay) || paymentDay < 1 || paymentDay > 31) {
context.res = { status: 400, body: { error: "El día de pago debe estar entre 1 y 31" } };
return;
}

where("cosmos init");
const client = new CosmosClient({ endpoint, key });
const container = client.database(databaseId).container(containerId);

// Dup por nombre (case-insensitive) + userId
where("dup-check", { name, userId });
const { resources: dup } = await container.items.query({
query: "SELECT TOP 1 c.id FROM c WHERE c.userId = @user AND LOWER(c.name) = LOWER(@name)",
parameters: [
{ name: "@user", value: userId },
{ name: "@name", value: name }
]
}).fetchAll();

if (dup && dup.length) {
where("dup-found", dup[0]);
context.res = { status: 409, body: { error: "Ya existe una tarjeta con ese nombre." } };
return;
}

const item = {
id: body.id || randomUUID(),
type: "card",
userId,
name,
limit,
balance,
cutoffDay,
paymentDay,
createdAt: new Date().toISOString()
};

where("create", item);
const { resource } = await container.items.create(item, { disableAutomaticIdGeneration: true });

where("success", resource?.id);
context.res = { status: 201, body: resource };
} catch (e) {
// Devuelve error detallado para verlo en el cliente
where("error", { message: e?.message, code: e?.code, body: e?.body });
let msg = e?.message || "Error al crear tarjeta";
// Si es error de Cosmos, intenta extraer más detalle
if (e?.body && typeof e.body === "string") {
try {
const jb = JSON.parse(e.body);
if (jb?.message) msg = jb.message;
} catch {}
}
context.res = { status: 500, body: { error: msg, code: e?.code || null } };
}
};
49 changes: 0 additions & 49 deletions api/cars-post/index.js

This file was deleted.

35 changes: 32 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ function useIsMobile() {
return isMobile;
}

function normalizeName(s) {
return (s || "").trim().toLocaleLowerCase();
}

const FinanceTracker = () => {
// ====== Filtros (mes / histórico) ======
const [rangeMode, setRangeMode] = useState('month'); // 'month' | 'all'
Expand Down Expand Up @@ -66,6 +70,8 @@ const FinanceTracker = () => {
const [newCard, setNewCard] = useState({
name: '', limit: '', balance: '', cutoffDay: '', paymentDay: ''
});
const [addingCard, setAddingCard] = useState(false);
const [cardMsg, setCardMsg] = useState(null); // {type:'ok'|'err', text:string} | null
const [activeTab, setActiveTab] = useState('dashboard');

const categories = {
Expand Down Expand Up @@ -576,7 +582,18 @@ const FinanceTracker = () => {
/>
<button
onClick={async () => {
if (newCard.name && newCard.limit) {
if (!newCard.name || !newCard.limit) {
setCardMsg({ type: 'err', text: 'Nombre y límite son requeridos.' });
return;
}
const exists = creditCards.some(c => normalizeName(c.name) === normalizeName(newCard.name));
if (exists) {
setCardMsg({ type: 'err', text: 'Ya tienes una tarjeta con ese nombre.' });
return;
}
try {
setAddingCard(true);
setCardMsg(null);
await createCard({
name: newCard.name,
limit: parseFloat(newCard.limit),
Expand All @@ -585,14 +602,26 @@ const FinanceTracker = () => {
paymentDay: parseInt(newCard.paymentDay, 10),
});
setNewCard({ name: '', limit: '', balance: '', cutoffDay: '', paymentDay: '' });
setCardMsg({ type: 'ok', text: 'Tarjeta agregada correctamente.' });
setTimeout(() => setCardMsg(null), 2000);
} catch (e) {
setCardMsg({ type: 'err', text: e?.message || 'Error al agregar la tarjeta.' });
} finally {
setAddingCard(false);
}
}}
className="bg-blue-600 text-white rounded-lg h-11 px-4 text-sm hover:bg-blue-700 flex items-center justify-center gap-2"
disabled={addingCard}
className={`bg-blue-600 text-white rounded-lg h-11 px-4 text-sm hover:bg-blue-700 flex items-center justify-center gap-2 ${addingCard ? 'opacity-70 cursor-not-allowed' : ''}`}
>
<PlusCircle size={18} />
Agregar
{addingCard ? 'Agregando…' : 'Agregar'}
</button>
</div>
{cardMsg && (
<p className={`mt-3 text-sm ${cardMsg.type === 'ok' ? 'text-green-600' : 'text-red-600'}`}>
{cardMsg.text}
</p>
)}
</div>

<div className="bg-white p-4 sm:p-6 rounded-xl shadow-md">
Expand Down
21 changes: 20 additions & 1 deletion src/api/cards.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// src/api/cards.js

export async function listCards() {
const r = await fetch('/api/cards', { credentials: 'include' });
if (!r.ok) throw new Error('Error al leer tarjetas');
Expand All @@ -11,7 +13,24 @@ export async function addCard(card) {
credentials: 'include',
body: JSON.stringify(card)
});
if (!r.ok) throw new Error('Error al crear tarjeta');

if (r.status === 409) throw new Error('Ya existe una tarjeta con ese nombre.');

if (!r.ok) {
// Muestra el error real del servidor
let msg = 'Error al crear tarjeta';
try {
const ct = r.headers.get('content-type') || '';
if (ct.includes('application/json')) {
const j = await r.json();
if (j?.error) msg = j.error;
} else {
const t = await r.text();
if (t) msg = t;
}
} catch {}
throw new Error(msg);
}
return r.json();
}

Expand Down
45 changes: 25 additions & 20 deletions src/hooks/useCards.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,46 @@
import { useEffect, useState, useCallback } from 'react';
import * as api from '../api/cards';
import { listCards, addCard, deleteCard } from '../api/cards'; // 👈 asegúrate de este path/nombres

export function useCards(userId) {
const [cards, setCards] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [cards, setCards] = useState([]);
const [loading, setLoad] = useState(false);
const [error, setError] = useState(null);

// carga inicial / cuando cambia el usuario
useEffect(() => {
let cancelled = false;
let alive = true;
async function load() {
if (!userId) { setCards([]); return; }
setLoading(true); setError(null);
setLoad(true);
setError(null);
try {
const data = await api.listCards();
if (!cancelled) setCards(data);
const data = await listCards(); // 👈 llamada a API
if (alive) setCards(Array.isArray(data) ? data : []);
} catch (e) {
if (!cancelled) setError(e);
if (alive) setError(e);
} finally {
if (!cancelled) setLoading(false);
if (alive) setLoad(false);
}
}
load();
return () => { cancelled = true; };
return () => { alive = false; };
}, [userId]);

// crear
const create = useCallback(async (card) => {
if (!userId) throw new Error('No usuario');
const c = await api.addCard(card);
setCards(prev => [c, ...prev]);
return c;
}, [userId]);
setError(null);
const created = await addCard(card); // 👈 llamada a API
// optimista / o puedes recargar listCards() si prefieres
setCards(prev => [created, ...prev]);
return created;
}, []);

// borrar
const remove = useCallback(async (id) => {
if (!userId) throw new Error('No usuario');
await api.deleteCard(id);
setCards(prev => prev.filter(x => x.id !== id));
}, [userId]);
setError(null);
await deleteCard(id); // 👈 llamada a API
setCards(prev => prev.filter(c => c.id !== id));
}, []);

return { cards, loading, error, create, remove };
}
Loading