From b2077cf2c1a4d3403717dce767f5391d4d593111 Mon Sep 17 00:00:00 2001 From: marsalva Date: Sat, 21 Feb 2026 15:02:09 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 127 +++++++++++------------------------------------------- 1 file changed, 25 insertions(+), 102 deletions(-) diff --git a/server.js b/server.js index df81159..a3f8b5d 100644 --- a/server.js +++ b/server.js @@ -1008,18 +1008,21 @@ app.get("/services/active", authMiddleware, async (req, res) => { } catch (e) { res.status(500).json({ ok: false }); } }); +// AÑADIDO: Ruta para fijar la cita o el estado operativo (REGLA ESTRICTA) app.put("/services/set-appointment/:id", authMiddleware, async (req, res) => { try { const { id } = req.params; let { date, time, status_operativo, ...extra } = req.body; - const current = await pool.query('SELECT raw_data FROM scraped_services WHERE id = $1 AND owner_id = $2', [id, req.user.accountId]); + // REPARADO: Añadido assigned_to al SELECT para poder avisar al operario + const current = await pool.query('SELECT raw_data, assigned_to FROM scraped_services WHERE id = $1 AND owner_id = $2', [id, req.user.accountId]); if (current.rowCount === 0) return res.status(404).json({ ok: false, error: 'No encontrado' }); const oldDate = current.rows[0].raw_data.scheduled_date || ""; const oldTime = current.rows[0].raw_data.scheduled_time || ""; const newDate = date || ""; const newTime = time || ""; + let finalAssignedTo = current.rows[0].assigned_to; // Lo guardamos por defecto // BLINDAJE DE ESTADO VACÍO if (status_operativo === "") status_operativo = null; @@ -1030,7 +1033,7 @@ app.put("/services/set-appointment/:id", authMiddleware, async (req, res) => { stName = (statusQ.rows[0]?.name || "").toLowerCase(); } - // --- MOTOR DE EVENTOS --- + // --- MOTOR DE EVENTOS Y DESASIGNACIÓN --- if (stName.includes('asignado')) { const waEnviadoExito = await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_assigned'); if (waEnviadoExito) { @@ -1039,7 +1042,24 @@ app.put("/services/set-appointment/:id", authMiddleware, async (req, res) => { status_operativo = estadoEsperando.rows[0].id; } } - } else if (stName.includes('citado') && newDate !== "") { + } + else if (stName.includes('pendiente de asignar') || stName.includes('desasignado')) { + // DESASIGNAR: Notificar al operario y borrar rastro + const oldWorkerId = finalAssignedTo || current.rows[0].raw_data.assigned_to; + if (oldWorkerId) { + const workerQ = await pool.query("SELECT full_name, phone FROM users WHERE id=$1", [oldWorkerId]); + if (workerQ.rowCount > 0) { + const w = workerQ.rows[0]; + const ref = current.rows[0].raw_data.service_ref || current.rows[0].raw_data["Referencia"] || id; + const msg = `⚠️ *AVISO DE DESASIGNACIÓN*\n\nHola ${w.full_name}, se te ha retirado el expediente *#${ref}*.\n\nYa no tienes que atender este servicio.`; + sendWhatsAppAuto(w.phone, msg, `cliente_${req.user.accountId}`, false).catch(console.error); + } + } + extra.assigned_to = null; + extra.assigned_to_name = null; + finalAssignedTo = null; // Borramos el físico también + } + else if (stName.includes('citado') && newDate !== "") { if (oldDate === "") await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_date'); else if (oldDate !== newDate || oldTime !== newTime) await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_update'); } else if (stName.includes('camino')) { @@ -1050,7 +1070,8 @@ app.put("/services/set-appointment/:id", authMiddleware, async (req, res) => { const updatedRawData = { ...current.rows[0].raw_data, ...extra, "scheduled_date": newDate, "scheduled_time": newTime, "status_operativo": status_operativo }; - await pool.query('UPDATE scraped_services SET raw_data = $1 WHERE id = $2 AND owner_id = $3', [JSON.stringify(updatedRawData), id, req.user.accountId]); + // ACTUALIZAMOS RAW DATA Y EL OPERARIO FÍSICO + await pool.query('UPDATE scraped_services SET raw_data = $1, assigned_to = $2 WHERE id = $3 AND owner_id = $4', [JSON.stringify(updatedRawData), finalAssignedTo, id, req.user.accountId]); res.json({ ok: true }); } catch (e) { @@ -1059,104 +1080,6 @@ app.put("/services/set-appointment/:id", authMiddleware, async (req, res) => { } }); -// AÑADIDO: Ruta para alta de expedientes manuales (Cola o Directo) -app.post("/services/manual-high", authMiddleware, async (req, res) => { - try { - const { phone, name, address, description, guild_id, assigned_to, mode } = req.body; - const serviceRef = "MAN-" + Date.now().toString().slice(-6); - const rawData = { "Nombre Cliente": name, "Teléfono": phone, "Dirección": address, "Descripción": description, "guild_id": guild_id }; - const insert = await pool.query(` - INSERT INTO scraped_services (owner_id, provider, service_ref, raw_data, status, automation_status, assigned_to) - VALUES ($1, 'MANUAL', $2, $3, 'pending', $4, $5) RETURNING id - `, [req.user.accountId, serviceRef, JSON.stringify(rawData), mode === 'auto' ? 'manual' : 'completed', mode === 'manual' ? assigned_to : null]); - - // Disparar Bienvenida / Alta - triggerWhatsAppEvent(req.user.accountId, insert.rows[0].id, 'wa_evt_welcome'); - - res.json({ ok: true }); - } catch (e) { res.status(500).json({ ok: false }); } -}); - - -app.get("/discovery/mappings", authMiddleware, async (req, res) => { - try { - const q = await pool.query("SELECT provider, original_key, target_key FROM variable_mappings WHERE owner_id = $1", [req.user.accountId]); - res.json(q.rows); - } catch (e) { res.status(500).json({ ok: false }); } -}); - -app.post("/discovery/save", authMiddleware, async (req, res) => { - const client = await pool.connect(); - try { - const { provider, mappings } = req.body; - await client.query('BEGIN'); - for (const m of mappings) { - await client.query(`INSERT INTO variable_mappings (owner_id, provider, original_key, target_key, is_ignored) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (owner_id, provider, original_key) DO UPDATE SET target_key = EXCLUDED.target_key, is_ignored = EXCLUDED.is_ignored`, [req.user.accountId, provider, m.original, m.target, m.ignored]); - } - await client.query('COMMIT'); - res.json({ ok: true }); - } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } -}); - -// AÑADIDO: Asegura que el cliente exista. Si no existe, lo crea y le asigna un token. -app.post("/clients/ensure", authMiddleware, async (req, res) => { - try { - const { phone, name, address } = req.body; - const p = normalizePhone(phone); - if(!p) return res.status(400).json({ok: false, error: "Sin teléfono"}); - - // 1. Buscamos si ya existe - const q = await pool.query("SELECT * FROM clients WHERE phone=$1 AND owner_id=$2 LIMIT 1", [p, req.user.accountId]); - if (q.rowCount > 0) return res.json({ ok: true, client: q.rows[0] }); - - // 2. Si no existe, lo creamos al vuelo en la agenda para que se genere su portal_token - const insert = await pool.query( - "INSERT INTO clients (owner_id, full_name, phone, addresses) VALUES ($1, $2, $3, $4) RETURNING *", - [req.user.accountId, name || 'Asegurado', p, JSON.stringify(address ? [address] : [])] - ); - res.json({ ok: true, client: insert.rows[0] }); - } catch (e) { res.status(500).json({ ok: false }); } -}); - -app.get("/clients", authMiddleware, async (req, res) => { - try { - const { search } = req.query; - let query = `SELECT c.*, c.portal_token, (SELECT COUNT(*) FROM services s WHERE s.client_id = c.id) as service_count FROM clients c WHERE c.owner_id = $1`; - const params = [req.user.accountId]; - if (search) { query += ` AND (c.full_name ILIKE $2 OR c.phone ILIKE $2)`; params.push(`%${search}%`); } - query += ` ORDER BY c.created_at DESC LIMIT 50`; - const q = await pool.query(query, params); - res.json({ ok: true, clients: q.rows }); - } catch (e) { res.status(500).json({ ok: false }); } -}); - -app.get("/clients/:id/details", authMiddleware, async (req, res) => { - try { - const clientId = req.params.id; - const clientQ = await pool.query("SELECT id, full_name, phone, addresses, email, notes, portal_token, created_at FROM clients WHERE id=$1 AND owner_id=$2", [clientId, req.user.accountId]); - if (clientQ.rowCount === 0) return res.status(404).json({ ok: false }); - const servicesQ = await pool.query(`SELECT s.*, st.name as status_name, st.color as status_color, u.full_name as assigned_name FROM services s LEFT JOIN service_statuses st ON s.status_id = st.id LEFT JOIN users u ON s.assigned_to = u.id WHERE s.client_id = $1 ORDER BY s.created_at DESC`, [clientId]); - res.json({ ok: true, client: clientQ.rows[0], services: servicesQ.rows }); - } catch (e) { res.status(500).json({ ok: false }); } -}); - -app.post("/clients", authMiddleware, async (req, res) => { - try { - const { full_name, phone, email, address, notes } = req.body; - const p = normalizePhone(phone); - const q = await pool.query("INSERT INTO clients (owner_id, full_name, phone, email, addresses, notes) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id", [req.user.accountId, full_name, p, email, JSON.stringify([address]), notes]); - res.json({ ok: true, id: q.rows[0].id }); - } catch (e) { res.status(500).json({ ok: false }); } -}); - -app.put("/clients/:id", authMiddleware, async (req, res) => { - try { - const { full_name, email, notes, addresses } = req.body; - await pool.query("UPDATE clients SET full_name=$1, email=$2, notes=$3, addresses=$4 WHERE id=$5 AND owner_id=$6", [full_name, email, notes, JSON.stringify(addresses), req.params.id, req.user.accountId]); - res.json({ ok: true }); - } catch (e) { res.status(500).json({ ok: false }); } -}); - // ========================================== // 📝 RUTAS DE PLANTILLAS DE MENSAJES // ==========================================