diff --git a/server.js b/server.js index 057bd96..4a1c534 100644 --- a/server.js +++ b/server.js @@ -4694,73 +4694,12 @@ app.get("/services/:id/logs", authMiddleware, async (req, res) => { }); // ========================================== -// 📄 MOTOR DE PRESUPUESTOS Y CATÁLOGO DE ARTÍCULOS +// 📄 MOTOR DE PRESUPUESTOS (continuación) — POST/PUT/PATCH de /budgets +// 🩹 FIX: este bloque duplicaba por completo la creación de tablas y las rutas +// GET/POST/PUT /articles, GET /budgets y DELETE /budgets/:id que ya se registran +// arriba (Express siempre usaba la primera copia; la segunda era código muerto). +// Se ha fusionado en un único bloque, dejando solo las rutas que eran nuevas aquí. // ========================================== -pool.query(` - CREATE TABLE IF NOT EXISTS articles ( - id SERIAL PRIMARY KEY, - 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', - created_at TIMESTAMP DEFAULT NOW() - ); -`).catch(console.error); - -// --- CATÁLOGO DE ARTÍCULOS --- -app.get("/articles", authMiddleware, async (req, res) => { - try { - 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}); } -}); - -app.post("/articles", authMiddleware, async (req, res) => { - try { - 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}); } -}); - -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}); } -}); - -// --- PRESUPUESTOS --- -app.get("/budgets", authMiddleware, async (req, res) => { - try { - const q = await pool.query(` - SELECT b.*, - s.status as linked_service_status, - st.name as linked_service_status_name - FROM budgets b - LEFT JOIN scraped_services s ON s.service_ref = 'PRE-' || b.id AND s.owner_id = b.owner_id - LEFT JOIN service_statuses st ON st.id::text = (s.raw_data->>'status_operativo')::text - WHERE b.owner_id=$1 - ORDER BY b.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; @@ -4851,36 +4790,7 @@ app.patch("/budgets/:id/status", authMiddleware, async (req, res) => { res.json({ ok: true }); } catch(e) { res.status(500).json({ok: false}); } }); - -app.delete("/budgets/:id", authMiddleware, async (req, res) => { - try { - const q = await pool.query("SELECT status FROM budgets WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]); - if (q.rowCount === 0) return res.status(404).json({ok: false, error: "No encontrado"}); - - const status = q.rows[0].status; - - const sq = await pool.query(` - SELECT st.name as status_name - FROM scraped_services s - LEFT JOIN service_statuses st ON st.id::text = (s.raw_data->>'status_operativo')::text - WHERE s.service_ref = $1 AND s.owner_id = $2 - `, [`PRE-${req.params.id}`, req.user.accountId]); - - let isAnulado = false; - if (sq.rowCount > 0 && sq.rows[0].status_name && sq.rows[0].status_name.toLowerCase().includes('anulado')) { - isAnulado = true; - } - - if ((status === 'accepted' || status === 'converted') && !isAnulado) { - return res.status(400).json({ok: false, error: "Para poder borrar un presupuesto, el servicio primero debe estar anulado."}); - } - - await pool.query("DELETE FROM budgets WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]); - res.json({ ok: true }); - } catch(e) { - res.status(500).json({ok: false, error: "Error interno"}); - } -}); +// (DELETE /budgets/:id ya está registrado más arriba, junto con GET/POST/PUT /articles y GET /budgets — no se repite aquí) // ========================================== // 🤖 API COLA DEL ROBOT (REEMPLAZO FIREBASE) @@ -4980,6 +4890,41 @@ app.get("/services/:id/chat", authMiddleware, async (req, res) => { } }); +// 🩹 FIX: faltaba el endpoint para ENVIAR mensajes desde el panel (solo existía el de leer historial) +app.post("/services/:id/chat", authMiddleware, async (req, res) => { + try { + const { id } = req.params; + const { message, is_internal } = req.body; + + if (!message || !message.toString().trim()) { + return res.status(400).json({ ok: false, error: "El mensaje está vacío" }); + } + + // Verificamos que el servicio pertenece a esta cuenta antes de escribir + const svcQ = await pool.query("SELECT id FROM scraped_services WHERE id=$1 AND owner_id=$2", [id, req.user.accountId]); + if (svcQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Servicio no encontrado" }); + + const isOperario = req.user.role === 'operario' || req.user.role === 'operario_cerrado'; + // Las notas internas son solo cosa de admin: un operario nunca puede marcar su mensaje como interno + const finalIsInternal = isOperario ? false : (is_internal === true || is_internal === 'true'); + + const userQ = await pool.query("SELECT full_name FROM users WHERE id=$1", [req.user.sub]); + const senderName = userQ.rows[0]?.full_name || (isOperario ? 'Operario' : 'Admin'); + + const insertQ = await pool.query( + `INSERT INTO service_communications (scraped_id, owner_id, sender_id, sender_name, sender_role, message, is_internal) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, sender_id, sender_name, sender_role, message, is_internal, created_at`, + [id, req.user.accountId, req.user.sub, senderName, req.user.role, message.toString().trim(), finalIsInternal] + ); + + res.json({ ok: true, message: insertQ.rows[0] }); + } catch (e) { + console.error("Error enviando mensaje de chat:", e); + res.status(500).json({ ok: false }); + } +}); + // 🤖 WEBHOOK CON ESCUDO HUMANO, MEMORIA Y GESTIÓN DE URGENCIAS // 🧠 Ejecuta realmente el turno de la IA para un expediente (llamado tras el debounce) async function procesarTurnoIA({ ownerId, service, mensajeTexto, telefonoCliente, instanceName }) {