From 8c10ae40505690d77e1c7e6dfdc74484def76044 Mon Sep 17 00:00:00 2001 From: marsalva Date: Sun, 1 Mar 2026 15:05:45 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 166 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 105 insertions(+), 61 deletions(-) diff --git a/server.js b/server.js index 1cd1143..80b6a5d 100644 --- a/server.js +++ b/server.js @@ -2019,82 +2019,126 @@ app.post("/public/assignment/:token/reject", async (req, res) => { }); // ========================================== -// 💰 MOTOR FINANCIERO Y CONTABILIDAD (PREPARADO PARA ROBOT PDF) +// 📄 MOTOR DE PRESUPUESTOS Y CATÁLOGO DE ARTÍCULOS // ========================================== -// Creamos la tabla financiera preparada para facturas y robots pool.query(` - CREATE TABLE IF NOT EXISTS service_financials ( + CREATE TABLE IF NOT EXISTS articles ( id SERIAL PRIMARY KEY, - scraped_id INT REFERENCES scraped_services(id) ON DELETE CASCADE UNIQUE, - amount DECIMAL(10,2) DEFAULT 0.00, - payment_method TEXT, - is_paid BOOLEAN DEFAULT false, - invoice_ref TEXT, - pdf_raw_data JSONB, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() + owner_id INT REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + price DECIMAL(10,2) DEFAULT 0.00, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS budgets ( + id SERIAL PRIMARY KEY, + owner_id INT REFERENCES users(id) ON DELETE CASCADE, + client_phone TEXT, + client_name TEXT, + client_address TEXT, + items JSONB DEFAULT '[]', + subtotal DECIMAL(10,2) DEFAULT 0.00, + tax DECIMAL(10,2) DEFAULT 0.00, + total DECIMAL(10,2) DEFAULT 0.00, + status TEXT DEFAULT 'pending', -- pending, accepted, rejected, converted + created_at TIMESTAMP DEFAULT NOW() ); `).catch(console.error); -// Obtener toda la contabilidad -app.get("/financials", authMiddleware, async (req, res) => { +// --- CATÁLOGO DE ARTÍCULOS --- +app.get("/articles", authMiddleware, async (req, res) => { try { - // 1. Truco de magia: Si hay servicios que no tienen ficha financiera, se la creamos automáticamente. - // Si tiene compañía asignada, le ponemos "Cobro Banco" por defecto. Si no, "Pendiente". - await pool.query(` - INSERT INTO service_financials (scraped_id, payment_method) - SELECT id, - CASE WHEN raw_data->>'Compañía' IS NOT NULL AND raw_data->>'Compañía' != '' AND raw_data->>'Compañía' != 'Particular' - THEN 'Cobro Banco' - ELSE 'Pendiente' END - FROM scraped_services - WHERE owner_id = $1 AND id NOT IN (SELECT scraped_id FROM service_financials) - `, [req.user.accountId]); - - // 2. Devolvemos la lista cruzando las finanzas con los datos del servicio - const q = await pool.query(` - SELECT f.*, s.service_ref, s.raw_data, s.status - FROM service_financials f - JOIN scraped_services s ON f.scraped_id = s.id - WHERE s.owner_id = $1 - ORDER BY f.updated_at DESC - `, [req.user.accountId]); - - res.json({ ok: true, financials: q.rows }); - } catch(e) { - console.error("Error financiero:", e); - res.status(500).json({ ok: false }); - } + const q = await pool.query("SELECT * FROM articles WHERE owner_id=$1 ORDER BY name ASC", [req.user.accountId]); + res.json({ ok: true, articles: q.rows }); + } catch(e) { res.status(500).json({ok: false}); } }); -// Guardar un cobro/pago -app.put("/financials/:id", authMiddleware, async (req, res) => { +app.post("/articles", authMiddleware, async (req, res) => { try { - const { amount, payment_method } = req.body; - const parsedAmount = parseFloat(amount) || 0; - - // NUEVA REGLA: Si el método de pago es "Pendiente", NO está pagado, - // independientemente del importe que tenga apuntado (Ej: Presupuestos). - const isPaid = payment_method !== 'Pendiente'; + const { name, price } = req.body; + await pool.query("INSERT INTO articles (owner_id, name, price) VALUES ($1, $2, $3)", [req.user.accountId, name, price]); + res.json({ ok: true }); + } catch(e) { res.status(500).json({ok: false}); } +}); - await pool.query(` - UPDATE service_financials - SET amount = $1, payment_method = $2, is_paid = $3, updated_at = NOW() - WHERE scraped_id = $4 - `, [parsedAmount, payment_method, isPaid, req.params.id]); +app.put("/articles/:id", authMiddleware, async (req, res) => { + try { + const { name, price } = req.body; + await pool.query("UPDATE articles SET name=$1, price=$2 WHERE id=$3 AND owner_id=$4", [name, price, req.params.id, req.user.accountId]); + res.json({ ok: true }); + } catch(e) { res.status(500).json({ok: false}); } +}); - // LOG AUTOMÁTICO DE TRAZABILIDAD - const userQ = await pool.query("SELECT full_name FROM users WHERE id=$1", [req.user.sub]); - const userName = userQ.rows[0]?.full_name || "Sistema"; +// --- PRESUPUESTOS --- +app.get("/budgets", authMiddleware, async (req, res) => { + try { + const q = await pool.query("SELECT * FROM budgets WHERE owner_id=$1 ORDER BY created_at DESC", [req.user.accountId]); + res.json({ ok: true, budgets: q.rows }); + } catch(e) { res.status(500).json({ok: false}); } +}); + +app.post("/budgets", authMiddleware, async (req, res) => { + try { + const { client_phone, client_name, client_address, items, subtotal, tax, total } = req.body; await pool.query( - "INSERT INTO scraped_service_logs (scraped_id, user_name, action, details) VALUES ($1, $2, $3, $4)", - [req.params.id, userName, "Cobro Actualizado", `Importe: ${parsedAmount}€ | Método: ${payment_method}`] + "INSERT INTO budgets (owner_id, client_phone, client_name, client_address, items, subtotal, tax, total) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + [req.user.accountId, client_phone, client_name, client_address, JSON.stringify(items), subtotal, tax, total] + ); + res.json({ ok: true }); + } catch(e) { res.status(500).json({ok: false}); } +}); + +app.patch("/budgets/:id/status", authMiddleware, async (req, res) => { + try { + const { status } = req.body; + await pool.query("UPDATE budgets SET status=$1 WHERE id=$2 AND owner_id=$3", [status, req.params.id, req.user.accountId]); + res.json({ ok: true }); + } catch(e) { res.status(500).json({ok: false}); } +}); + +// Convertir Presupuesto en Servicio Activo +app.post("/budgets/:id/convert", authMiddleware, async (req, res) => { + try { + const { date, time } = req.body; + const bq = await pool.query("SELECT * FROM budgets WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]); + if (bq.rowCount === 0) return res.status(404).json({ok: false}); + const budget = bq.rows[0]; + + // 1. Montamos el Raw Data para el servicio + const rawData = { + "Nombre Cliente": budget.client_name, + "Teléfono": budget.client_phone, + "Dirección": budget.client_address, + "Compañía": "Particular", + "Descripción": "PRESUPUESTO ACEPTADO.\n" + budget.items.map(i => `${i.qty}x ${i.concept}`).join("\n"), + "scheduled_date": date, + "scheduled_time": time + }; + + // 2. Insertamos en el Panel Operativo (Buzón) + const insertSvc = await pool.query( + "INSERT INTO scraped_services (owner_id, provider, service_ref, status, automation_status, raw_data) VALUES ($1, 'particular', $2, 'pending', 'manual', $3) RETURNING id", + [req.user.accountId, `PRE-${budget.id}`, JSON.stringify(rawData)] ); - res.json({ ok: true, is_paid: isPaid }); - } catch(e) { - console.error(e); - res.status(500).json({ ok: false }); + // 3. Marcamos presupuesto como convertido y le enlazamos la ficha financiera por el total + await pool.query("UPDATE budgets SET status='converted' WHERE id=$1", [budget.id]); + await pool.query( + "INSERT INTO service_financials (scraped_id, amount, payment_method) VALUES ($1, $2, 'Pendiente')", + [insertSvc.rows[0].id, budget.total] + ); + + // 4. Mandamos WhatsApp de confirmación + if (budget.client_phone) { + const [y, m, d] = date.split('-'); + const msg = `✅ *PRESUPUESTO ACEPTADO*\n\nHola ${budget.client_name}, confirmamos la aceptación del presupuesto por un total de *${budget.total}€*.\n\nEl servicio ha sido agendado para el *${d}/${m}/${y} a las ${time}*. ¡Gracias por confiar en nosotros!`; + sendWhatsAppAuto(budget.client_phone, msg, `cliente_${req.user.accountId}`, false).catch(console.error); + } + + res.json({ ok: true }); + } catch(e) { + console.error("Error convirtiendo presupuesto:", e); + res.status(500).json({ok: false}); } });