diff --git a/server.js b/server.js index b36fde4..a965352 100644 --- a/server.js +++ b/server.js @@ -1750,6 +1750,84 @@ app.post("/config/company", authMiddleware, async (req, res) => { } }); +// RUTA: Alta de expediente manual con validación de cliente +app.post("/services/manual-high", authMiddleware, async (req, res) => { + try { + const { phone, name, address, description, guild_id, assigned_to, duration_minutes, mode, is_company, company_name, company_ref } = req.body; + const ownerId = req.user.accountId; + + // 1. Manejo del Cliente (Buscamos si existe por teléfono) + const cleanPhone = phone.replace(/\D/g, ""); + let clientQ = await pool.query("SELECT id, addresses FROM clients WHERE phone LIKE $1 AND owner_id = $2", [`%${cleanPhone}%`, ownerId]); + + let clientId; + if (clientQ.rowCount > 0) { + clientId = clientQ.rows[0].id; + let currentAddrs = clientQ.rows[0].addresses || []; + // Si la dirección es nueva, la añadimos a su ficha + if (!currentAddrs.includes(address)) { + currentAddrs.push(address); + await pool.query("UPDATE clients SET addresses = $1 WHERE id = $2", [JSON.stringify(currentAddrs), clientId]); + } + } else { + // Si no existe, creamos cliente nuevo + const token = crypto.randomBytes(6).toString('hex'); + const newClient = await pool.query( + "INSERT INTO clients (owner_id, full_name, phone, addresses, portal_token) VALUES ($1, $2, $3, $4, $5) RETURNING id", + [ownerId, name, phone, JSON.stringify([address]), token] + ); + clientId = newClient.rows[0].id; + } + + // 2. Crear el Expediente + const rawData = { + "Nombre Cliente": name, + "Teléfono": phone, + "Dirección": address, + "Descripción": description, + "guild_id": guild_id, + "scheduled_date": "", + "scheduled_time": "", + "duration_minutes": duration_minutes || 60, + "Compañía": is_company ? company_name : "Particular" + }; + + const serviceReference = is_company ? company_ref : `M-${Date.now().toString().slice(-6)}`; + + const insertSvc = await pool.query( + `INSERT INTO scraped_services (owner_id, provider, service_ref, status, automation_status, assigned_to, raw_data) + VALUES ($1, 'MANUAL', $2, 'pending', $3, $4, $5) RETURNING id`, + [ + ownerId, + serviceReference, + mode === 'auto' ? 'in_progress' : 'manual', + assigned_to || null, + JSON.stringify(rawData) + ] + ); + + const newId = insertSvc.rows[0].id; + + // 3. Si se eligió "Mandar a la bolsa", llamamos internamente al robot + if (mode === 'auto' && guild_id) { + const port = process.env.PORT || 3000; + fetch(`http://127.0.0.1:${port}/providers/automate/${newId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': req.headers.authorization }, + body: JSON.stringify({ guild_id, cp: "00000" }) + }).catch(e => console.error("Error lanzando bolsa:", e)); + } + + // --- TRAZABILIDAD --- + await registrarMovimiento(newId, req.user.sub, "Alta Manual", `Servicio creado manualmente (${rawData["Compañía"]}).`); + + res.json({ ok: true, id: newId }); + } catch (e) { + console.error("Error Alta Manual:", e); + res.status(500).json({ ok: false }); + } +}); + // ========================================== // 🛠️ RUTAS DE GREMIOS E INTELIGENCIA ARTIFICIAL // ========================================== @@ -2279,94 +2357,6 @@ app.post("/budgets/:id/convert", authMiddleware, async (req, res) => { } }); -// Convertir Presupuesto en Servicio Activo (CON SOPORTE RED INTERNA) -app.post("/budgets/:id/convert", authMiddleware, async (req, res) => { - try { - const { date, time, guild_id, assigned_to, use_automation } = 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"), - "guild_id": guild_id || null, - "assigned_to": assigned_to || null, - "scheduled_date": date || "", - "scheduled_time": time || "" - }; - - // 2. Insertamos en el Panel Operativo (Buzón) empezando en manual - const insertSvc = await pool.query( - "INSERT INTO scraped_services (owner_id, provider, service_ref, status, automation_status, assigned_to, raw_data) VALUES ($1, 'particular', $2, 'pending', 'manual', $3, $4) RETURNING id", - [ - req.user.accountId, - `PRE-${budget.id}`, - assigned_to || null, - JSON.stringify(rawData) - ] - ); - - const newServiceId = insertSvc.rows[0].id; - - // 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')", - [newServiceId, budget.total] - ); - - // 4. Si pide automatización, la disparamos internamente llamando a nuestra propia IP (127.0.0.1) - if (use_automation && guild_id) { - const cpMatch = budget.client_address ? budget.client_address.match(/\b\d{5}\b/) : null; - const cp = cpMatch ? cpMatch[0] : "00000"; - - const port = process.env.PORT || 3000; - // IMPORTANTE: 127.0.0.1 en lugar de localhost para evitar errores en Node - const autoUrl = `http://127.0.0.1:${port}/providers/automate/${newServiceId}`; - - fetch(autoUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': req.headers.authorization - }, - body: JSON.stringify({ guild_id, cp, useDelay: false }) - }) - .then(r => r.json()) - .then(d => console.log("Llamada interna a automatización finalizada.", d)) - .catch(e => console.error("Error lanzando automatización interna:", e)); - - if (budget.client_phone) { - const msg = `✅ *PRESUPUESTO ACEPTADO*\n\nHola ${budget.client_name}, confirmamos la aceptación del presupuesto por un total de *${budget.total}€*.\n\nEn breve un técnico se pondrá en contacto contigo para agendar la cita. ¡Gracias por confiar en nosotros!`; - sendWhatsAppAuto(budget.client_phone, msg, `cliente_${req.user.accountId}`, false).catch(console.error); - } - } - else if (budget.client_phone && date && time) { - // Asignación directa a un técnico con fecha y hora - 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); - - if (assigned_to) { - const statusQ = await pool.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND name ILIKE '%asignado%' LIMIT 1", [req.user.accountId]); - if (statusQ.rowCount > 0) { - rawData.status_operativo = statusQ.rows[0].id; - await pool.query("UPDATE scraped_services SET raw_data = $1 WHERE id = $2", [JSON.stringify(rawData), newServiceId]); - } - } - } - - res.json({ ok: true }); - } catch(e) { - console.error("Error convirtiendo presupuesto:", e); - res.status(500).json({ok: false}); - } -}); // ========================================== @@ -2490,7 +2480,7 @@ app.post("/services/:id/log", authMiddleware, async (req, res) => { // Ruta para LEER el historial de un servicio app.get("/services/:id/logs", authMiddleware, async (req, res) => { try { - // JOIN para asegurar que el log pertenece a un servicio del dueño actual + // Cruce con la tabla principal para verificar el dueño (owner_id) const q = await pool.query(` SELECT l.* FROM scraped_service_logs l JOIN scraped_services s ON l.scraped_id = s.id