Actualizar server.js

This commit is contained in:
2026-02-16 08:22:00 +00:00
parent 9d29245ff1
commit f23a3b679a

View File

@@ -332,7 +332,7 @@ app.get("/public/assignment/:token", async (req, res) => {
try { try {
const { token } = req.params; const { token } = req.params;
// MODO DEBUG: Traemos el registro exista o no, y le pedimos a la BD su hora exacta (db_now) // Comprobación MODO BLINDADO (Extrae todo, exista o no)
const q = await pool.query(` const q = await pool.query(`
SELECT ap.*, s.raw_data, u.full_name as worker_name, CURRENT_TIMESTAMP as db_now SELECT ap.*, s.raw_data, u.full_name as worker_name, CURRENT_TIMESTAMP as db_now
FROM assignment_pings ap FROM assignment_pings ap
@@ -341,37 +341,22 @@ app.get("/public/assignment/:token", async (req, res) => {
WHERE ap.token = $1 WHERE ap.token = $1
`, [token]); `, [token]);
if (q.rowCount === 0) { if (q.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace caducado o inexistente" });
return res.status(404).json({ ok: false, error: "El enlace no existe en la base de datos." });
}
const data = q.rows[0]; const data = q.rows[0];
// Hacemos la comprobación manualmente para decidir si mandamos error o éxito
const isExpired = data.status !== 'pending' || new Date(data.expires_at) <= new Date(data.db_now); const isExpired = data.status !== 'pending' || new Date(data.expires_at) <= new Date(data.db_now);
if (isExpired) { if (isExpired) {
return res.status(404).json({ return res.status(404).json({ ok: false, error: "Este enlace ha caducado o ha sido reasignado." });
ok: false,
error: "Este enlace ha caducado o el servicio ya ha sido asignado.",
debug: {
estado_en_bd: data.status,
hora_limite_bd: data.expires_at,
hora_actual_bd: data.db_now
}
});
} }
res.json({ res.json({
ok: true, ok: true,
service: data.raw_data, service: data.raw_data,
worker: data.worker_name, worker: data.worker_name,
debug: { debug: { hora_limite_bd: data.expires_at, hora_actual_bd: data.db_now }
hora_limite_bd: data.expires_at,
hora_actual_bd: data.db_now
}
}); });
} catch (e) { res.status(500).json({ ok: false, error: e.message }); } } catch (e) { res.status(500).json({ ok: false }); }
}); });
app.post("/public/assignment/respond", async (req, res) => { app.post("/public/assignment/respond", async (req, res) => {
@@ -380,24 +365,29 @@ app.post("/public/assignment/respond", async (req, res) => {
const { token, action } = req.body; const { token, action } = req.body;
await client.query('BEGIN'); await client.query('BEGIN');
// LEEMOS EXACTAMENTE IGUAL QUE EN LA RUTA GET, BLOQUEANDO ERRORES FANTASMA
const q = await client.query( const q = await client.query(
"SELECT * FROM assignment_pings WHERE token = $1 AND status = 'pending' AND expires_at > CURRENT_TIMESTAMP", "SELECT *, CURRENT_TIMESTAMP as db_now FROM assignment_pings WHERE token = $1 FOR UPDATE",
[token] [token]
); );
if (q.rowCount === 0) throw new Error("Acción caducada");
if (q.rowCount === 0) throw new Error("Enlace no válido o inexistente");
const ping = q.rows[0]; const ping = q.rows[0];
const isExpired = ping.status !== 'pending' || new Date(ping.expires_at) <= new Date(ping.db_now);
if (isExpired) throw new Error("El tiempo se agotó justo antes de aceptar.");
if (action === 'accept') { if (action === 'accept') {
await client.query("UPDATE assignment_pings SET status = 'accepted' WHERE id = $1", [ping.id]); await client.query("UPDATE assignment_pings SET status = 'accepted' WHERE id = $1", [ping.id]);
// AÑADIDO: Guardar ID de operario en columna física y JSON // AÑADIDO: Guardar ID de operario (Forzamos tipo INT para evitar errores de JSONB)
await client.query(` await client.query(`
UPDATE scraped_services UPDATE scraped_services
SET status = 'imported', SET status = 'imported',
automation_status = 'completed', automation_status = 'completed',
assigned_to = $1, assigned_to = $1,
raw_data = raw_data || jsonb_build_object('assigned_to', $1) raw_data = raw_data || jsonb_build_object('assigned_to', $1::int)
WHERE id = $2 WHERE id = $2
`, [ping.user_id, ping.scraped_id]); `, [ping.user_id, ping.scraped_id]);
@@ -409,7 +399,8 @@ app.post("/public/assignment/respond", async (req, res) => {
res.json({ ok: true }); res.json({ ok: true });
} catch (e) { } catch (e) {
await client.query('ROLLBACK'); await client.query('ROLLBACK');
res.status(400).json({ ok: false }); console.error("ERROR AL RESPONDER TURNO:", e.message); // Por si acaso hay otro error
res.status(400).json({ ok: false, error: e.message });
} finally { } finally {
client.release(); client.release();
} }
@@ -419,10 +410,10 @@ app.post("/public/assignment/respond", async (req, res) => {
// 🔐 RUTAS AUTH Y PRIVADAS ( CRM ORIGINAL ) // 🔐 RUTAS AUTH Y PRIVADAS ( CRM ORIGINAL )
// ========================================== // ==========================================
app.post("/auth/register", async (req, res) => { const client = await pool.connect(); try { const { fullName, phone, address, dni, email, password } = req.body; const p = normalizePhone(phone); if (!fullName || !p || !email || !password) return res.status(400).json({ ok: false }); const passwordHash = await bcrypt.hash(password, 10); await client.query('BEGIN'); const insert = await client.query("INSERT INTO users (full_name, phone, address, dni, email, password_hash, role, owner_id, plan_tier) VALUES ($1, $2, $3, $4, $5, $6, 'admin', NULL, 'free') RETURNING id", [fullName, p, address, dni, email, passwordHash]); const userId = insert.rows[0].id; const code = genCode6(); const codeHash = await bcrypt.hash(code, 10); await client.query("INSERT INTO login_codes (user_id, phone, code_hash, expires_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '10 minutes')", [userId, p, codeHash]); app.post("/auth/register", async (req, res) => { const client = await pool.connect(); try { const { fullName, phone, address, dni, email, password } = req.body; const p = normalizePhone(phone); if (!fullName || !p || !email || !password) return res.status(400).json({ ok: false }); const passwordHash = await bcrypt.hash(password, 10); await client.query('BEGIN'); const insert = await client.query("INSERT INTO users (full_name, phone, address, dni, email, password_hash, role, owner_id, plan_tier) VALUES ($1, $2, $3, $4, $5, $6, 'admin', NULL, 'free') RETURNING id", [fullName, p, address, dni, email, passwordHash]); const userId = insert.rows[0].id; const code = genCode6(); const codeHash = await bcrypt.hash(code, 10); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); await client.query("INSERT INTO login_codes (user_id, phone, code_hash, expires_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '10 minutes')", [userId, p, codeHash]);
await sendWhatsAppCode(p, code); await sendWhatsAppCode(p, code);
await client.query('COMMIT'); res.json({ ok: true, phone: p }); } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } }); await client.query('COMMIT'); res.json({ ok: true, phone: p }); } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } });
app.post("/auth/verify", async (req, res) => { try { const { phone, code } = req.body; const p = normalizePhone(phone); const q = await pool.query(`SELECT lc.*, u.id as uid, u.email, u.role, u.owner_id FROM login_codes lc JOIN users u ON lc.user_id = u.id WHERE lc.phone=$1 AND lc.consumed_at IS NULL AND lc.expires_at > CURRENT_TIMESTAMP ORDER BY lc.created_at DESC LIMIT 1`, [p]); if (q.rowCount === 0) return res.status(400).json({ ok: false }); const row = q.rows[0]; if (!(await bcrypt.compare(String(code), row.code_hash))) return res.status(400).json({ ok: false }); await pool.query("UPDATE login_codes SET consumed_at=NOW() WHERE id=$1", [row.id]); await pool.query("UPDATE users SET is_verified=TRUE WHERE id=$1", [row.uid]); res.json({ ok: true, token: signToken({ id: row.uid, email: row.email, phone: p, role: row.role, owner_id: row.owner_id }) }); } catch (e) { res.status(500).json({ ok: false }); } }); app.post("/auth/verify", async (req, res) => { try { const { phone, code } = req.body; const p = normalizePhone(phone); const q = await pool.query(`SELECT lc.*, u.id as uid, u.email, u.role, u.owner_id FROM login_codes lc JOIN users u ON lc.user_id = u.id WHERE lc.phone=$1 AND lc.consumed_at IS NULL AND lc.expires_at > CURRENT_TIMESTAMP ORDER BY lc.created_at DESC LIMIT 1`, [p]); if (q.rowCount === 0) return res.status(400).json({ ok: false }); const row = q.rows[0]; if (!(await bcrypt.compare(String(code), row.code_hash))) return res.status(400).json({ ok: false }); await pool.query("UPDATE login_codes SET consumed_at=CURRENT_TIMESTAMP WHERE id=$1", [row.id]); await pool.query("UPDATE users SET is_verified=TRUE WHERE id=$1", [row.uid]); res.json({ ok: true, token: signToken({ id: row.uid, email: row.email, phone: p, role: row.role, owner_id: row.owner_id }) }); } catch (e) { res.status(500).json({ ok: false }); } });
app.post("/auth/login", async (req, res) => { try { const { email, password } = req.body; const q = await pool.query("SELECT * FROM users WHERE email=$1", [email]); if (q.rowCount === 0) return res.status(401).json({ ok: false }); let user = null; for (const u of q.rows) { if (await bcrypt.compare(password, u.password_hash)) { user = u; break; } } if (!user) return res.status(401).json({ ok: false }); res.json({ ok: true, token: signToken(user) }); } catch(e) { res.status(500).json({ ok: false }); } }); app.post("/auth/login", async (req, res) => { try { const { email, password } = req.body; const q = await pool.query("SELECT * FROM users WHERE email=$1", [email]); if (q.rowCount === 0) return res.status(401).json({ ok: false }); let user = null; for (const u of q.rows) { if (await bcrypt.compare(password, u.password_hash)) { user = u; break; } } if (!user) return res.status(401).json({ ok: false }); res.json({ ok: true, token: signToken(user) }); } catch(e) { res.status(500).json({ ok: false }); } });
app.get("/whatsapp/status", authMiddleware, (req, res, next) => requirePlan(req, res, next, 'whatsapp_enabled'), async (req, res) => { app.get("/whatsapp/status", authMiddleware, (req, res, next) => requirePlan(req, res, next, 'whatsapp_enabled'), async (req, res) => {
@@ -471,7 +462,6 @@ app.get("/providers/scraped", authMiddleware, async (req, res) => {
ap.token as active_token, ap.token as active_token,
EXTRACT(EPOCH FROM (ap.expires_at - CURRENT_TIMESTAMP)) as seconds_left, EXTRACT(EPOCH FROM (ap.expires_at - CURRENT_TIMESTAMP)) as seconds_left,
u.full_name as current_worker_name, u.full_name as current_worker_name,
-- Obtenemos objeto con nombre y teléfono de los operarios que fallaron
(SELECT json_agg(json_build_object('name', u2.full_name, 'phone', u2.phone)) (SELECT json_agg(json_build_object('name', u2.full_name, 'phone', u2.phone))
FROM assignment_pings ap2 FROM assignment_pings ap2
JOIN users u2 ON ap2.user_id = u2.id JOIN users u2 ON ap2.user_id = u2.id
@@ -483,7 +473,6 @@ app.get("/providers/scraped", authMiddleware, async (req, res) => {
ORDER BY s.created_at DESC ORDER BY s.created_at DESC
`, [req.user.accountId]); `, [req.user.accountId]);
// Transformamos esos segundos en una fecha universal perfecta para tu web (automatizacion.html)
const services = q.rows.map(row => { const services = q.rows.map(row => {
if (row.seconds_left && row.seconds_left > 0) { if (row.seconds_left && row.seconds_left > 0) {
row.token_expires_at = new Date(Date.now() + (row.seconds_left * 1000)); row.token_expires_at = new Date(Date.now() + (row.seconds_left * 1000));
@@ -496,7 +485,6 @@ app.get("/providers/scraped", authMiddleware, async (req, res) => {
res.json({ ok: true, services }); res.json({ ok: true, services });
} catch (e) { } catch (e) {
console.error("Error en GET scraped:", e.message);
res.status(500).json({ ok: false }); res.status(500).json({ ok: false });
} }
}); });
@@ -508,7 +496,6 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => {
if (!guild_id || !cp) return res.status(400).json({ ok: false, error: "Faltan datos (Gremio o CP)" }); if (!guild_id || !cp) return res.status(400).json({ ok: false, error: "Faltan datos (Gremio o CP)" });
// 1. Obtener datos del expediente para el mensaje
const serviceQ = await pool.query("SELECT raw_data, provider FROM scraped_services WHERE id = $1", [id]); const serviceQ = await pool.query("SELECT raw_data, provider FROM scraped_services WHERE id = $1", [id]);
if (serviceQ.rowCount === 0) 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" });
@@ -517,11 +504,9 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => {
const poblacion = raw["Población"] || raw["POBLACION-PROVINCIA"] || "---"; const poblacion = raw["Población"] || raw["POBLACION-PROVINCIA"] || "---";
const gremioNombre = raw["Gremio"] || "Servicio General"; const gremioNombre = raw["Gremio"] || "Servicio General";
// Limpiar dirección: Quitar números y pisos (regex para detectar números y lo que sigue)
const direccionCompleta = raw["Dirección"] || raw["DOMICILIO"] || ""; const direccionCompleta = raw["Dirección"] || raw["DOMICILIO"] || "";
const direccionLimpia = direccionCompleta.split(/[0-9]/)[0].trim(); const direccionLimpia = direccionCompleta.split(/[0-9]/)[0].trim();
// 2. Buscar operarios disponibles
const workersQ = await pool.query(` const 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
@@ -537,18 +522,19 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => {
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');
// LA SOLUCIÓN DEFINITIVA: await pool.query(`
// 1. Postgres inserta la fecha calculando 5 minutos con su reloj
// 2. Le pedimos a Postgres que nos devuelva el texto de la hora ya formateada para Madrid
const pingRes = await pool.query(`
INSERT INTO assignment_pings (scraped_id, user_id, token, expires_at) INSERT INTO assignment_pings (scraped_id, user_id, token, expires_at)
VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '5 minutes') VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '5 minutes')
RETURNING to_char((CURRENT_TIMESTAMP + INTERVAL '5 minutes') AT TIME ZONE 'Europe/Madrid', 'HH24:MI') as hora_limite
`, [id, worker.id, token]); `, [id, worker.id, token]);
const horaCaducidad = pingRes.rows[0].hora_limite; // CÁLCULO DE HORA 100% FIABLE: Se lo pedimos a Node forzando a España
// Así siempre saldrá "0:40" en lugar de "23:40" en el texto de WhatsApp
const horaCaducidad = new Date(Date.now() + 5 * 60 * 1000).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'Europe/Madrid'
});
// 3. Construir mensaje de WhatsApp con toda la información solicitada
const link = `https://web.integrarepara.es/aceptar.html?t=${token}`; const link = `https://web.integrarepara.es/aceptar.html?t=${token}`;
const mensaje = `🛠️ *NUEVO SERVICIO ASIGNADO A TI* const mensaje = `🛠️ *NUEVO SERVICIO ASIGNADO A TI*
@@ -618,7 +604,6 @@ app.put('/providers/scraped/:id', authMiddleware, async (req, res) => {
const { automation_status, name, phone, address, status } = req.body; const { automation_status, name, phone, address, status } = req.body;
try { try {
// ACCIÓN PARA LA PAPELERA: Si enviamos solo automation_status, reseteamos el estado
if (automation_status) { if (automation_status) {
await pool.query( await pool.query(
`UPDATE scraped_services SET automation_status = $1 WHERE id = $2 AND owner_id = $3`, `UPDATE scraped_services SET automation_status = $1 WHERE id = $2 AND owner_id = $3`,
@@ -627,7 +612,6 @@ app.put('/providers/scraped/:id', authMiddleware, async (req, res) => {
return res.json({ ok: true }); return res.json({ ok: true });
} }
// ACCIÓN PARA ARCHIVAR: Si el frontend manda status 'archived'
if (status === 'archived') { if (status === 'archived') {
await pool.query( await pool.query(
`UPDATE scraped_services SET status = 'archived', automation_status = 'manual' WHERE id = $2 AND owner_id = $3`, `UPDATE scraped_services SET status = 'archived', automation_status = 'manual' WHERE id = $2 AND owner_id = $3`,
@@ -636,7 +620,6 @@ app.put('/providers/scraped/:id', authMiddleware, async (req, res) => {
return res.json({ ok: true }); return res.json({ ok: true });
} }
// EDICIÓN NORMAL: Mantenemos tu lógica de actualizar datos del cliente
const current = await pool.query('SELECT raw_data FROM scraped_services WHERE id = $1 AND owner_id = $2', [id, req.user.accountId]); const current = await pool.query('SELECT raw_data 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' });
@@ -817,7 +800,8 @@ setInterval(async () => {
SELECT ap.id, ap.scraped_id, ap.user_id, s.owner_id, s.raw_data SELECT ap.id, ap.scraped_id, ap.user_id, s.owner_id, s.raw_data
FROM assignment_pings ap FROM assignment_pings ap
JOIN scraped_services s ON ap.scraped_id = s.id JOIN scraped_services s ON ap.scraped_id = s.id
WHERE ap.status = 'pending' AND ap.expires_at < CURRENT_TIMESTAMP WHERE ap.status = 'pending'
AND EXTRACT(EPOCH FROM (ap.expires_at - CURRENT_TIMESTAMP)) <= 0
AND s.automation_status = 'in_progress' AND s.automation_status = 'in_progress'
`); `);