Actualizar server.js
This commit is contained in:
188
server.js
188
server.js
@@ -1449,6 +1449,9 @@ async function triggerWhatsAppEvent(ownerId, serviceId, eventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 🔐 CREDENCIALES DE PROVEEDORES
|
||||||
|
// ==========================================
|
||||||
app.post("/providers/credentials", authMiddleware, async (req, res) => {
|
app.post("/providers/credentials", authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { provider, username, password } = req.body;
|
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) => {
|
app.get("/providers/scraped", authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Pedimos a Postgres que calcule los SEGUNDOS que faltan y enviamos la cuenta exacta
|
|
||||||
const q = await pool.query(`
|
const q = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
s.*,
|
s.*,
|
||||||
@@ -1491,11 +1493,8 @@ app.get("/providers/scraped", authMiddleware, async (req, res) => {
|
|||||||
delete row.seconds_left;
|
delete row.seconds_left;
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ ok: true, services });
|
res.json({ ok: true, services });
|
||||||
} catch (e) {
|
} catch (e) { res.status(500).json({ ok: false }); }
|
||||||
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) => {
|
app.post("/providers/automate/:id", authMiddleware, async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
console.log(`\n🤖 [AUTOMATE] Iniciando proceso para ID: ${id}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { guild_id, cp, useDelay } = req.body;
|
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]);
|
const serviceQ = await pool.query("SELECT raw_data, provider, owner_id FROM scraped_services WHERE id = $1", [id]);
|
||||||
if (serviceQ.rowCount === 0) {
|
if (serviceQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Expediente no encontrado" });
|
||||||
console.error(`❌ [AUTOMATE] Expediente ${id} no encontrado en la DB`);
|
|
||||||
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(`
|
let workersQ = await pool.query(`
|
||||||
SELECT u.id, u.full_name, u.phone
|
SELECT u.id, u.full_name, u.phone FROM users u
|
||||||
FROM users u
|
|
||||||
JOIN user_guilds ug ON u.id = ug.user_id
|
JOIN user_guilds ug ON u.id = ug.user_id
|
||||||
WHERE u.owner_id = $1
|
WHERE u.owner_id = $1 AND u.role = 'operario' AND u.status = 'active'
|
||||||
AND u.role = 'operario'
|
AND ug.guild_id = $2 AND u.zones::jsonb @> $3::jsonb
|
||||||
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() }])]);
|
`, [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) {
|
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(`
|
workersQ = await pool.query(`
|
||||||
SELECT u.id, u.full_name, u.phone
|
SELECT u.id, u.full_name, u.phone FROM users u
|
||||||
FROM users u
|
|
||||||
JOIN user_guilds ug ON u.id = ug.user_id
|
JOIN user_guilds ug ON u.id = ug.user_id
|
||||||
WHERE u.owner_id = $1
|
WHERE u.owner_id = $1 AND u.role = 'operario' AND u.status = 'active' AND ug.guild_id = $2
|
||||||
AND u.role = 'operario'
|
|
||||||
AND u.status = 'active'
|
|
||||||
AND ug.guild_id = $2
|
|
||||||
`, [req.user.accountId, guild_id]);
|
`, [req.user.accountId, guild_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workersQ.rowCount === 0) {
|
if (workersQ.rowCount === 0) return res.status(404).json({ ok: false, error: "No hay operarios disponibles" });
|
||||||
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" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Marcar como "En progreso"
|
|
||||||
await pool.query("UPDATE scraped_services SET automation_status = 'in_progress' WHERE id = $1", [id]);
|
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 worker = workersQ.rows[Math.floor(Math.random() * workersQ.rows.length)];
|
||||||
const token = crypto.randomBytes(16).toString('hex');
|
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 msg = `🛠️ *NUEVO SERVICIO DISPONIBLE*\n📍 Zona: ${(cp && cp !== "00000") ? cp : "Asignada"}\n🔗 https://web.integrarepara.es/aceptar.html?t=${token}`;
|
||||||
const horaCaducidad = new Date(Date.now() + 5 * 60 * 1000).toLocaleTimeString('es-ES', {
|
sendWhatsAppAuto(worker.phone, msg, `cliente_${req.user.accountId}`, useDelay).catch(console.error);
|
||||||
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}`;
|
|
||||||
|
|
||||||
// 6. Envío WA
|
await registrarMovimiento(id, req.user.sub, "Bolsa Automática", `Notificación enviada a: ${worker.full_name}`);
|
||||||
const instanceName = `cliente_${req.user.accountId}`;
|
res.json({ ok: true });
|
||||||
sendWhatsAppAuto(worker.phone, mensaje, instanceName, useDelay).catch(e => console.error("Error WA Automate:", e.message));
|
} catch (e) { res.status(500).json({ ok: false }); }
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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]);
|
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' });
|
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;
|
const oldWorkerId = current.rows[0].assigned_to || rawActual.assigned_to;
|
||||||
let finalAssignedTo = assigned_to !== undefined ? (assigned_to === "" ? null : assigned_to) : oldWorkerId;
|
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 oldDate = rawActual.scheduled_date || "";
|
||||||
const newDate = extra.scheduled_date !== undefined ? extra.scheduled_date : oldDate;
|
const newDate = extra.scheduled_date !== undefined ? extra.scheduled_date : oldDate;
|
||||||
const dateChanged = newDate !== "" && newDate !== oldDate;
|
const dateChanged = newDate !== "" && newDate !== oldDate;
|
||||||
const statusChanged = newStatus !== oldStatus;
|
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 = "";
|
let stName = "";
|
||||||
if (newStatus) {
|
if (newStatus) {
|
||||||
const statusQ = await pool.query("SELECT name FROM service_statuses WHERE id=$1", [newStatus]);
|
const statusQ = await pool.query("SELECT name FROM service_statuses WHERE id=$1", [newStatus]);
|
||||||
stName = (statusQ.rows[0]?.name || "").toLowerCase();
|
stName = (statusQ.rows[0]?.name || "").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusChanged) {
|
// WhatsApp Eventos
|
||||||
if ((stName.includes('pendiente') && !stName.includes('cita')) || stName.includes('desasignado') || stName.includes('asignado') || stName.includes('anulado') || stName.includes('esperando')) {
|
if (stName.includes('asignado') && finalAssignedTo && statusChanged) {
|
||||||
if (!extra.scheduled_date) {
|
await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_assigned');
|
||||||
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)
|
|
||||||
if ((statusChanged && stName.includes('citado') && newDate !== "") || (dateChanged && stName.includes('citado'))) {
|
if ((statusChanged && stName.includes('citado') && newDate !== "") || (dateChanged && stName.includes('citado'))) {
|
||||||
if (oldDate === "") await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_date');
|
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 = {
|
const updatedRawData = {
|
||||||
...rawActual, ...extra,
|
...rawActual, ...extra,
|
||||||
"Nombre Cliente": name || rawActual["Nombre Cliente"],
|
"Nombre Cliente": name || rawActual["Nombre Cliente"],
|
||||||
"Teléfono": phone || rawActual["Teléfono"],
|
"Teléfono": phone || rawActual["Teléfono"],
|
||||||
"Dirección": address || rawActual["Dirección"],
|
"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
|
"status_operativo": newStatus
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentDbStatus = current.rows[0].status;
|
|
||||||
const finalIsUrgent = is_urgent !== undefined ? is_urgent : current.rows[0].is_urgent;
|
|
||||||
|
|
||||||
await pool.query(
|
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`,
|
`UPDATE scraped_services SET raw_data = $1, assigned_to = $2 WHERE id = $3 AND owner_id = $4`,
|
||||||
[JSON.stringify(updatedRawData), currentDbStatus, finalIsUrgent, finalAssignedTo, id, req.user.accountId]
|
[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) {
|
if (statusChanged && stName.includes('asignado') && finalAssignedTo) {
|
||||||
triggerHomeServeRobot(req.user.accountId, id, 'assign').catch(console.error);
|
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);
|
triggerHomeServeRobot(req.user.accountId, id, 'date').catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error actualización manual:", error);
|
console.error(error);
|
||||||
res.status(500).json({ 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) => {
|
app.get("/services/check-ref", authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { ref } = req.query;
|
const { ref } = req.query;
|
||||||
const q = await pool.query(
|
const q = await pool.query("SELECT id FROM scraped_services WHERE service_ref = $1 AND owner_id = $2", [ref, req.user.accountId]);
|
||||||
"SELECT id FROM scraped_services WHERE service_ref = $1 AND owner_id = $2",
|
|
||||||
[ref, req.user.accountId]
|
|
||||||
);
|
|
||||||
res.json({ exists: q.rowCount > 0 });
|
res.json({ exists: q.rowCount > 0 });
|
||||||
} catch (e) { res.status(500).json({ ok: false }); }
|
} catch (e) { res.status(500).json({ ok: false }); }
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user