Actualizar server.js

This commit is contained in:
2026-02-21 15:02:09 +00:00
parent 35758a36fe
commit b2077cf2c1

127
server.js
View File

@@ -1008,18 +1008,21 @@ app.get("/services/active", authMiddleware, async (req, res) => {
} catch (e) { res.status(500).json({ ok: false }); } } 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) => { app.put("/services/set-appointment/:id", authMiddleware, async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
let { date, time, status_operativo, ...extra } = req.body; 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' }); if (current.rowCount === 0) return res.status(404).json({ ok: false, error: 'No encontrado' });
const oldDate = current.rows[0].raw_data.scheduled_date || ""; const oldDate = current.rows[0].raw_data.scheduled_date || "";
const oldTime = current.rows[0].raw_data.scheduled_time || ""; const oldTime = current.rows[0].raw_data.scheduled_time || "";
const newDate = date || ""; const newDate = date || "";
const newTime = time || ""; const newTime = time || "";
let finalAssignedTo = current.rows[0].assigned_to; // Lo guardamos por defecto
// BLINDAJE DE ESTADO VACÍO // BLINDAJE DE ESTADO VACÍO
if (status_operativo === "") status_operativo = null; 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(); stName = (statusQ.rows[0]?.name || "").toLowerCase();
} }
// --- MOTOR DE EVENTOS --- // --- MOTOR DE EVENTOS Y DESASIGNACIÓN ---
if (stName.includes('asignado')) { if (stName.includes('asignado')) {
const waEnviadoExito = await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_assigned'); const waEnviadoExito = await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_assigned');
if (waEnviadoExito) { if (waEnviadoExito) {
@@ -1039,7 +1042,24 @@ app.put("/services/set-appointment/:id", authMiddleware, async (req, res) => {
status_operativo = estadoEsperando.rows[0].id; 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'); 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 (oldDate !== newDate || oldTime !== newTime) await triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_update');
} else if (stName.includes('camino')) { } 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 }; 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 }); res.json({ ok: true });
} catch (e) { } 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 // 📝 RUTAS DE PLANTILLAS DE MENSAJES
// ========================================== // ==========================================