diff --git a/server.js b/server.js index 7d4f400..4a96fe0 100644 --- a/server.js +++ b/server.js @@ -1449,6 +1449,9 @@ async function triggerWhatsAppEvent(ownerId, serviceId, eventType) { } } +// ========================================== +// 🔐 CREDENCIALES DE PROVEEDORES +// ========================================== app.post("/providers/credentials", authMiddleware, async (req, res) => { try { const { provider, username, password } = req.body; @@ -1464,7 +1467,6 @@ app.post("/providers/credentials", authMiddleware, async (req, res) => { app.get("/providers/scraped", authMiddleware, async (req, res) => { try { - // Pedimos a Postgres que calcule los SEGUNDOS que faltan y enviamos la cuenta exacta const q = await pool.query(` SELECT s.*, @@ -1491,11 +1493,8 @@ app.get("/providers/scraped", authMiddleware, async (req, res) => { delete row.seconds_left; return row; }); - res.json({ ok: true, services }); - } catch (e) { - res.status(500).json({ ok: false }); - } + } catch (e) { res.status(500).json({ ok: false }); } }); // ========================================== @@ -1503,95 +1502,57 @@ app.get("/providers/scraped", authMiddleware, async (req, res) => { // ========================================== app.post("/providers/automate/:id", authMiddleware, async (req, res) => { const { id } = req.params; - console.log(`\n🤖 [AUTOMATE] Iniciando proceso para ID: ${id}`); - try { const { guild_id, cp, useDelay } = req.body; + if (!guild_id) return res.status(400).json({ ok: false, error: "Falta Gremio" }); - if (!guild_id) { - console.error("❌ [AUTOMATE] Faltan datos: guild_id"); - return res.status(400).json({ ok: false, error: "Falta Gremio" }); - } - - // 1. Verificar si el expediente existe const serviceQ = await pool.query("SELECT raw_data, provider, owner_id FROM scraped_services WHERE id = $1", [id]); - if (serviceQ.rowCount === 0) { - console.error(`❌ [AUTOMATE] Expediente ${id} no encontrado en la DB`); - return res.status(404).json({ ok: false, error: "Expediente no encontrado" }); - } + if (serviceQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Expediente no encontrado" }); - // 2. Buscar operarios que cumplan Gremio + Zona (CP) + Activos let workersQ = await pool.query(` - SELECT u.id, u.full_name, u.phone - FROM users u + SELECT u.id, u.full_name, u.phone FROM users u JOIN user_guilds ug ON u.id = ug.user_id - WHERE u.owner_id = $1 - AND u.role = 'operario' - AND u.status = 'active' - AND ug.guild_id = $2 - AND u.zones::jsonb @> $3::jsonb + WHERE u.owner_id = $1 AND u.role = 'operario' AND u.status = 'active' + AND ug.guild_id = $2 AND u.zones::jsonb @> $3::jsonb `, [req.user.accountId, guild_id, JSON.stringify([{ cps: (cp || "00000").toString() }])]); - // MEJORA: Si no hay nadie para ese CP exacto (o si no se puso CP en el presupuesto), - // buscamos a CUALQUIER operario de ese gremio if (workersQ.rowCount === 0) { - console.log(`⚠️ [AUTOMATE] No hay operario para el CP exacto, buscando a cualquiera del gremio ${guild_id}...`); workersQ = await pool.query(` - SELECT u.id, u.full_name, u.phone - FROM users u + SELECT u.id, u.full_name, u.phone FROM users u JOIN user_guilds ug ON u.id = ug.user_id - WHERE u.owner_id = $1 - AND u.role = 'operario' - AND u.status = 'active' - AND ug.guild_id = $2 + WHERE u.owner_id = $1 AND u.role = 'operario' AND u.status = 'active' AND ug.guild_id = $2 `, [req.user.accountId, guild_id]); } - if (workersQ.rowCount === 0) { - console.warn(`❌ [AUTOMATE] No hay operarios activos para el Gremio:${guild_id}`); - return res.status(404).json({ ok: false, error: "No hay operarios disponibles para este gremio" }); - } + if (workersQ.rowCount === 0) return res.status(404).json({ ok: false, error: "No hay operarios disponibles" }); - // 3. Marcar como "En progreso" await pool.query("UPDATE scraped_services SET automation_status = 'in_progress' WHERE id = $1", [id]); - const worker = workersQ.rows[Math.floor(Math.random() * workersQ.rows.length)]; const token = crypto.randomBytes(16).toString('hex'); - // 4. Crear el Ping de asignación (5 minutos) - await pool.query(` - INSERT INTO assignment_pings (scraped_id, user_id, token, expires_at) - VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '5 minutes') - `, [id, worker.id, token]); + await pool.query(`INSERT INTO assignment_pings (scraped_id, user_id, token, expires_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '5 minutes')`, [id, worker.id, token]); - // 5. Preparar mensaje - const horaCaducidad = new Date(Date.now() + 5 * 60 * 1000).toLocaleTimeString('es-ES', { - hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Madrid' - }); - - const link = `https://web.integrarepara.es/aceptar.html?t=${token}`; - const cpMostrar = (cp && cp !== "00000") ? cp : "Zona asignada"; - const mensaje = `🛠️ *NUEVO SERVICIO DISPONIBLE*\n\n👤 *Operario:* ${worker.full_name}\n📍 *Zona:* ${cpMostrar}\n⏱️ *Expira:* ${horaCaducidad}\n\nRevisa y acepta aquí:\n🔗 ${link}`; + const msg = `🛠️ *NUEVO SERVICIO DISPONIBLE*\n📍 Zona: ${(cp && cp !== "00000") ? cp : "Asignada"}\n🔗 https://web.integrarepara.es/aceptar.html?t=${token}`; + sendWhatsAppAuto(worker.phone, msg, `cliente_${req.user.accountId}`, useDelay).catch(console.error); - // 6. Envío WA - const instanceName = `cliente_${req.user.accountId}`; - sendWhatsAppAuto(worker.phone, mensaje, instanceName, useDelay).catch(e => console.error("Error WA Automate:", e.message)); - - console.log(`✅ [AUTOMATE] Asignación enviada a ${worker.full_name}`); - - // --- INICIO TRAZABILIDAD --- - await registrarMovimiento(id, req.user.sub, "Bolsa Automática", `Aviso enviado a la bolsa. Notificación disparada a: ${worker.full_name}`); - // --- FIN TRAZABILIDAD --- - - res.json({ ok: true, message: "Automatismo iniciado con " + worker.full_name }); - - } catch (e) { - console.error("❌ [AUTOMATE] Error Crítico:", e.message); - res.status(500).json({ ok: false, error: e.message }); - } + await registrarMovimiento(id, req.user.sub, "Bolsa Automática", `Notificación enviada a: ${worker.full_name}`); + res.json({ ok: true }); + } catch (e) { res.status(500).json({ ok: false }); } }); -// 3. ACTUALIZACIÓN MANUAL NORMAL DE LA FICHA +// ========================================== +// 📝 ACTUALIZACIÓN MANUAL (RUTA PRINCIPAL) +// ========================================== +app.put('/providers/scraped/:id', authMiddleware, async (req, res) => { + const { id } = req.params; + let { automation_status, status, name, phone, address, cp, description, guild_id, assigned_to, assigned_to_name, internal_notes, client_notes, is_urgent, ...extra } = req.body; + + try { + if (automation_status) { + await pool.query(`UPDATE scraped_services SET automation_status = $1 WHERE id = $2 AND owner_id = $3`, [automation_status, id, req.user.accountId]); + return res.json({ ok: true }); + } + const current = await pool.query('SELECT raw_data, assigned_to, status, is_urgent FROM scraped_services WHERE id = $1 AND owner_id = $2', [id, req.user.accountId]); if (current.rows.length === 0) return res.status(404).json({ error: 'No encontrado' }); @@ -1603,121 +1564,60 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => { const oldWorkerId = current.rows[0].assigned_to || rawActual.assigned_to; let finalAssignedTo = assigned_to !== undefined ? (assigned_to === "" ? null : assigned_to) : oldWorkerId; - // 👇 MAGIA: Detectamos si la fecha ha cambiado independientemente del estado const oldDate = rawActual.scheduled_date || ""; const newDate = extra.scheduled_date !== undefined ? extra.scheduled_date : oldDate; const dateChanged = newDate !== "" && newDate !== oldDate; const statusChanged = newStatus !== oldStatus; - - // --- AVISO AL OPERARIO (ASIGNACIÓN NUEVA / DESASIGNACIÓN) --- - if (finalAssignedTo !== oldWorkerId) { - if (finalAssignedTo) { - const workerQ = await pool.query("SELECT full_name, phone FROM users WHERE id=$1", [finalAssignedTo]); - if (workerQ.rowCount > 0) { - const w = workerQ.rows[0]; - const ref = rawActual.service_ref || rawActual["Referencia"] || id; - const dir = address || rawActual["Dirección"] || "Ver ficha"; - const msg = `🛠️ *NUEVO SERVICIO ASIGNADO*\n\nHola ${w.full_name}, se te ha asignado el expediente *#${ref}*.\n📍 Población/Zona: ${cp || rawActual["Población"] || dir}\n\nRevisa tu panel de operario para agendar la cita.`; - sendWhatsAppAuto(w.phone, msg, `cliente_${req.user.accountId}`, false).catch(console.error); - } - } else if (oldWorkerId && !finalAssignedTo) { - 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 = rawActual.service_ref || rawActual["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); - } - } - } - // --- LÓGICA DE ESTADOS Y WHATSAPP AL CLIENTE --- let stName = ""; if (newStatus) { const statusQ = await pool.query("SELECT name FROM service_statuses WHERE id=$1", [newStatus]); stName = (statusQ.rows[0]?.name || "").toLowerCase(); } - if (statusChanged) { - if ((stName.includes('pendiente') && !stName.includes('cita')) || stName.includes('desasignado') || stName.includes('asignado') || stName.includes('anulado') || stName.includes('esperando')) { - if (!extra.scheduled_date) { - extra.scheduled_date = ""; - extra.scheduled_time = ""; - } - } - - if (stName.includes('asignado') && finalAssignedTo) { - const waEnviadoExito = await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_assigned'); - if (waEnviadoExito) { - const estadoEsperando = await pool.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND name='Esperando al Cliente' LIMIT 1", [req.user.accountId]); - if(estadoEsperando.rowCount > 0) { - newStatus = estadoEsperando.rows[0].id; - extra.status_operativo = newStatus; - await registrarMovimiento(id, req.user.sub, "Robot WA", "WhatsApp de asignación entregado. Estado cambiado a Esperando al Cliente."); - } - } else { - await registrarMovimiento(id, req.user.sub, "Robot WA", "WhatsApp de asignación falló o está desactivado."); - } - } - } - - // 🟢 NUEVO DISPARADOR WHATSAPP CITA (Se lanza si cambia el estado O la fecha) + // WhatsApp Eventos + if (stName.includes('asignado') && finalAssignedTo && statusChanged) { + await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_assigned'); + } + if ((statusChanged && stName.includes('citado') && newDate !== "") || (dateChanged && stName.includes('citado'))) { if (oldDate === "") await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_date'); - else if (oldDate !== newDate) await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_update'); + else await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_update'); } - // 4. UNIFICAR DATOS FINALES const updatedRawData = { ...rawActual, ...extra, "Nombre Cliente": name || rawActual["Nombre Cliente"], "Teléfono": phone || rawActual["Teléfono"], "Dirección": address || rawActual["Dirección"], - "Código Postal": cp || rawActual["Código Postal"], - "Descripción": description || rawActual["Descripción"], - "guild_id": guild_id, - "assigned_to": finalAssignedTo, - "assigned_to_name": assigned_to_name, - "internal_notes": internal_notes, - "client_notes": client_notes, - "Urgente": is_urgent ? "Sí" : "No", "status_operativo": newStatus }; - let currentDbStatus = current.rows[0].status; - const finalIsUrgent = is_urgent !== undefined ? is_urgent : current.rows[0].is_urgent; - await pool.query( - `UPDATE scraped_services SET raw_data = $1, status = $2, is_urgent = $3, assigned_to = $4 WHERE id = $5 AND owner_id = $6`, - [JSON.stringify(updatedRawData), currentDbStatus, finalIsUrgent, finalAssignedTo, id, req.user.accountId] + `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] ); - - await registrarMovimiento(id, req.user.sub, "Edición / Asignación", "Expediente actualizado o asignado."); - // 5. 🤖 DISPARAR ROBOT HOMESERVE (AHORA SÍ, LEE SI CAMBIA FECHA O ESTADO) + // Robot HomeServe if (statusChanged && stName.includes('asignado') && finalAssignedTo) { triggerHomeServeRobot(req.user.accountId, id, 'assign').catch(console.error); } - if ((statusChanged && stName.includes('citado') && updatedRawData.scheduled_date) || (dateChanged && stName.includes('citado'))) { + if ((statusChanged && stName.includes('citado')) || (dateChanged && stName.includes('citado'))) { triggerHomeServeRobot(req.user.accountId, id, 'date').catch(console.error); } res.json({ ok: true }); - } catch (error) { - console.error("Error actualización manual:", error); + console.error(error); res.status(500).json({ error: 'Error' }); } }); -// Validar si una referencia ya existe para este dueño +// Validar si una referencia ya existe app.get("/services/check-ref", authMiddleware, async (req, res) => { try { const { ref } = req.query; - const q = await pool.query( - "SELECT id FROM scraped_services WHERE service_ref = $1 AND owner_id = $2", - [ref, req.user.accountId] - ); + const q = await pool.query("SELECT id FROM scraped_services WHERE service_ref = $1 AND owner_id = $2", [ref, req.user.accountId]); res.json({ exists: q.rowCount > 0 }); } catch (e) { res.status(500).json({ ok: false }); } });