Actualizar server.js
This commit is contained in:
72
server.js
72
server.js
@@ -332,7 +332,7 @@ app.get("/public/assignment/:token", async (req, res) => {
|
||||
try {
|
||||
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(`
|
||||
SELECT ap.*, s.raw_data, u.full_name as worker_name, CURRENT_TIMESTAMP as db_now
|
||||
FROM assignment_pings ap
|
||||
@@ -341,37 +341,22 @@ app.get("/public/assignment/:token", async (req, res) => {
|
||||
WHERE ap.token = $1
|
||||
`, [token]);
|
||||
|
||||
if (q.rowCount === 0) {
|
||||
return res.status(404).json({ ok: false, error: "El enlace no existe en la base de datos." });
|
||||
}
|
||||
if (q.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace caducado o inexistente" });
|
||||
|
||||
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);
|
||||
|
||||
if (isExpired) {
|
||||
return res.status(404).json({
|
||||
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
|
||||
}
|
||||
});
|
||||
return res.status(404).json({ ok: false, error: "Este enlace ha caducado o ha sido reasignado." });
|
||||
}
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
service: data.raw_data,
|
||||
worker: data.worker_name,
|
||||
debug: {
|
||||
hora_limite_bd: data.expires_at,
|
||||
hora_actual_bd: data.db_now
|
||||
}
|
||||
debug: { 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) => {
|
||||
@@ -380,24 +365,29 @@ app.post("/public/assignment/respond", async (req, res) => {
|
||||
const { token, action } = req.body;
|
||||
await client.query('BEGIN');
|
||||
|
||||
// LEEMOS EXACTAMENTE IGUAL QUE EN LA RUTA GET, BLOQUEANDO ERRORES FANTASMA
|
||||
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]
|
||||
);
|
||||
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 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') {
|
||||
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(`
|
||||
UPDATE scraped_services
|
||||
SET status = 'imported',
|
||||
automation_status = 'completed',
|
||||
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
|
||||
`, [ping.user_id, ping.scraped_id]);
|
||||
|
||||
@@ -409,7 +399,8 @@ app.post("/public/assignment/respond", async (req, res) => {
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
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 {
|
||||
client.release();
|
||||
}
|
||||
@@ -419,10 +410,10 @@ app.post("/public/assignment/respond", async (req, res) => {
|
||||
// 🔐 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 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.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,
|
||||
EXTRACT(EPOCH FROM (ap.expires_at - CURRENT_TIMESTAMP)) as seconds_left,
|
||||
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))
|
||||
FROM assignment_pings ap2
|
||||
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
|
||||
`, [req.user.accountId]);
|
||||
|
||||
// Transformamos esos segundos en una fecha universal perfecta para tu web (automatizacion.html)
|
||||
const services = q.rows.map(row => {
|
||||
if (row.seconds_left && row.seconds_left > 0) {
|
||||
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 });
|
||||
} catch (e) {
|
||||
console.error("Error en GET scraped:", e.message);
|
||||
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)" });
|
||||
|
||||
// 1. Obtener datos del expediente para el mensaje
|
||||
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" });
|
||||
|
||||
@@ -517,11 +504,9 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => {
|
||||
const poblacion = raw["Población"] || raw["POBLACION-PROVINCIA"] || "---";
|
||||
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 direccionLimpia = direccionCompleta.split(/[0-9]/)[0].trim();
|
||||
|
||||
// 2. Buscar operarios disponibles
|
||||
const workersQ = await pool.query(`
|
||||
SELECT u.id, u.full_name, u.phone
|
||||
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 token = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
// LA SOLUCIÓN DEFINITIVA:
|
||||
// 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(`
|
||||
await pool.query(`
|
||||
INSERT INTO assignment_pings (scraped_id, user_id, token, expires_at)
|
||||
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]);
|
||||
|
||||
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 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;
|
||||
|
||||
try {
|
||||
// ACCIÓN PARA LA PAPELERA: Si enviamos solo automation_status, reseteamos el estado
|
||||
if (automation_status) {
|
||||
await pool.query(
|
||||
`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 });
|
||||
}
|
||||
|
||||
// ACCIÓN PARA ARCHIVAR: Si el frontend manda status 'archived'
|
||||
if (status === 'archived') {
|
||||
await pool.query(
|
||||
`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 });
|
||||
}
|
||||
|
||||
// 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]);
|
||||
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
|
||||
FROM assignment_pings ap
|
||||
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'
|
||||
`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user