diff --git a/server.js b/server.js index 96d35da..e560815 100644 --- a/server.js +++ b/server.js @@ -488,96 +488,6 @@ async function ensureInstance(instanceName) { // 🚀 RUTAS PÚBLICAS (MÓVIL OPERARIO) // ========================================== -app.get("/public/assignment/:token", async (req, res) => { - try { - const { token } = req.params; - - // 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 - JOIN scraped_services s ON ap.scraped_id = s.id - JOIN users u ON ap.user_id = u.id - WHERE ap.token = $1 - `, [token]); - - if (q.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace caducado o inexistente" }); - - const data = q.rows[0]; - 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 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 } - }); - } catch (e) { res.status(500).json({ ok: false }); } -}); - -app.post("/public/assignment/respond", async (req, res) => { - const client = await pool.connect(); - try { - const { token, action } = req.body; - await client.query('BEGIN'); - - const q = await client.query( - "SELECT *, CURRENT_TIMESTAMP as db_now FROM assignment_pings WHERE token = $1 FOR UPDATE", - [token] - ); - - if (q.rowCount === 0) throw new Error("Enlace no válido"); - const ping = q.rows[0]; - - if (action === 'accept') { - await client.query("UPDATE assignment_pings SET status = 'accepted' WHERE id = $1", [ping.id]); - - // 1. Obtenemos el owner para poder enviar el WA - const ownerQ = await client.query("SELECT owner_id FROM scraped_services WHERE id = $1", [ping.scraped_id]); - const ownerId = ownerQ.rows[0].owner_id; - - // 2. DISPARAMOS EL WHATSAPP Y ESPERAMOS EL RESULTADO - const waEnviadoExito = await triggerWhatsAppEvent(ownerId, ping.scraped_id, 'wa_evt_assigned'); - - // 3. REGLA ESTRICTA: Decidimos el estado según el éxito del mensaje - const estadoFinalNombre = waEnviadoExito ? 'Esperando al Cliente' : 'Asignado'; - - const statusQ = await client.query( - "SELECT id FROM service_statuses WHERE owner_id = $1 AND name = $2 LIMIT 1", - [ownerId, estadoFinalNombre] - ); - const idFinal = statusQ.rows[0]?.id; - - // 4. Actualizamos el servicio con el operario y el estado correcto - 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::int, 'status_operativo', $3::text) - WHERE id = $2 - `, [ping.user_id, ping.scraped_id, idFinal]); - - } else { - await client.query("UPDATE assignment_pings SET status = 'rejected', expires_at = CURRENT_TIMESTAMP WHERE id = $1", [ping.id]); - } - - await client.query('COMMIT'); - res.json({ ok: true }); - } catch (e) { - await client.query('ROLLBACK'); - res.status(400).json({ ok: false, error: e.message }); - } finally { client.release(); } -}); - -// ========================================== -// 🌐 RUTAS PÚBLICAS: PORTAL DEL CLIENTE (SIN FRICCIÓN) -// ========================================== - // 1. Cargar datos del cliente, logo y empresa app.get("/public/portal/:token", async (req, res) => { try { @@ -598,10 +508,11 @@ app.get("/public/portal/:token", async (req, res) => { SELECT id, service_ref as title, raw_data->>'Descripción' as description, raw_data->>'scheduled_date' as scheduled_date, raw_data->>'scheduled_time' as scheduled_time, + raw_data->>'appointment_status' as appointment_status, created_at, - raw_data->>'status_operativo' as estado_operativo, is_urgent, - (SELECT full_name FROM users WHERE id = scraped_services.assigned_to) as assigned_worker + (SELECT full_name FROM users WHERE id = scraped_services.assigned_to) as assigned_worker, + (SELECT name FROM service_statuses WHERE id::text = raw_data->>'status_operativo') as real_status_name FROM scraped_services WHERE owner_id = $1 AND (raw_data->>'Teléfono' ILIKE $2 OR raw_data->>'TELEFONO' ILIKE $2 OR raw_data->>'TELEFONOS' ILIKE $2) @@ -609,13 +520,16 @@ app.get("/public/portal/:token", async (req, res) => { `, [clientData.owner_id, `%${phoneRaw}%`]); const services = scrapedQ.rows.map(s => { - let statusName = "Pendiente de Asignar"; let color = "gray"; - if (s.estado_operativo === 'asignado_operario') { statusName = "Asignado a Técnico"; color = "blue"; } - if (s.estado_operativo === 'citado') { statusName = "Visita Agendada"; color = "emerald"; } - if (s.estado_operativo === 'de_camino') { statusName = "Técnico de Camino"; color = "indigo"; } - if (s.estado_operativo === 'trabajando') { statusName = "En Reparación"; color = "amber"; } - if (s.estado_operativo === 'incidencia') { statusName = "Pausado / Incidencia"; color = "red"; } - if (s.estado_operativo === 'terminado') { statusName = "Terminado"; color = "purple"; } + // Evaluamos el nombre real de la base de datos + let stNameDb = (s.real_status_name || 'Pendiente de Asignar').toLowerCase(); + let finalStatusName = s.real_status_name || "Pendiente de Asignar"; + + if (stNameDb.includes('asignado') || stNameDb.includes('esperando')) { finalStatusName = "Asignado a Técnico"; } + if (stNameDb.includes('citado')) { finalStatusName = "Visita Agendada"; } + if (stNameDb.includes('camino')) { finalStatusName = "Técnico de Camino"; } + if (stNameDb.includes('trabajando')) { finalStatusName = "En Reparación"; } + if (stNameDb.includes('incidencia')) { finalStatusName = "Pausado / Incidencia"; } + if (stNameDb.includes('terminado') || stNameDb.includes('finalizado') || stNameDb.includes('anulado') || stNameDb.includes('desasignado')) { finalStatusName = "Terminado"; } return { id: s.id, @@ -623,9 +537,9 @@ app.get("/public/portal/:token", async (req, res) => { description: s.description || "Avería reportada.", scheduled_date: s.scheduled_date, scheduled_time: s.scheduled_time, + appointment_status: s.appointment_status, created_at: s.created_at, - status_name: statusName, - status_color: color, + status_name: finalStatusName, assigned_worker: s.assigned_worker || "Pendiente" }; }); @@ -639,7 +553,6 @@ app.get("/public/portal/:token", async (req, res) => { } catch (e) { res.status(500).json({ ok: false, error: "Error de servidor" }); } }); -// 2. Obtener huecos disponibles inteligentes (CON HORARIOS DINÁMICOS Y TRAMOS DE 1 HORA) // 2. Obtener huecos disponibles inteligentes (CON HORARIOS DINÁMICOS Y TRAMOS DE 1 HORA) app.get("/public/portal/:token/slots", async (req, res) => { try {