From ed801ca3ae1782ba39c86b5242e2cd3335c40a4c Mon Sep 17 00:00:00 2001 From: marsalva Date: Wed, 25 Feb 2026 22:34:16 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 106 ++++++++++++++++++++++++++---------------------------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/server.js b/server.js index ac96718..ae14b89 100644 --- a/server.js +++ b/server.js @@ -502,69 +502,65 @@ async function ensureInstance(instanceName) { // 🚀 RUTAS PÚBLICAS (MÓVIL OPERARIO) // ========================================== -// 1. Cargar datos del cliente, logo y empresa +// ========================================== +// 🔗 PORTAL PÚBLICO DEL CLIENTE (Mejorado con Historial) +// ========================================== app.get("/public/portal/:token", async (req, res) => { try { const { token } = req.params; - const clientQ = await pool.query(` - SELECT c.id, c.full_name, c.phone, c.addresses, c.owner_id, - u.company_slug, u.full_name as company_name, u.company_logo - FROM clients c - JOIN users u ON c.owner_id = u.id - WHERE c.portal_token = $1 - `, [token]); + + // 1. Validar Token y Obtener Cliente + const qClient = await pool.query("SELECT * FROM clients WHERE portal_token = $1", [token]); + if (qClient.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace inválido" }); + const client = qClient.rows[0]; - if (clientQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace no válido o caducado" }); - const clientData = clientQ.rows[0]; + // 2. Obtener la Configuración de la Empresa + const qConfig = await pool.query("SELECT * FROM config WHERE account_id = $1", [client.owner_id]); + let company = { name: "IntegraRepara", logo: "" }; + if (qConfig.rowCount > 0 && qConfig.rows[0].portal_settings) { + company = qConfig.rows[0].portal_settings; + } - const phoneRaw = clientData.phone.replace('+34', ''); - const scrapedQ = await pool.query(` - 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, - is_urgent, - (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) - ORDER BY created_at DESC - `, [clientData.owner_id, `%${phoneRaw}%`]); + // 3. Buscar TODOS los servicios asociados al teléfono del cliente + // Usamos el teléfono para buscar en el JSONB (cuidando las minúsculas y mayúsculas de las llaves posibles) + const qServices = await pool.query(` + SELECT + s.id, s.service_ref, s.status, s.raw_data, s.is_urgent, + st.name as status_name, st.is_final as status_is_final, + u.name as assigned_worker + FROM scraped_services s + LEFT JOIN service_statuses st ON st.id::text = s.raw_data->>'status_operativo' + LEFT JOIN users u ON u.id = s.assigned_to + WHERE s.owner_id = $1 + AND s.provider != 'SYSTEM_BLOCK' + AND ( + s.raw_data->>'Teléfono' LIKE '%' || $2 || '%' OR + s.raw_data->>'TELEFONOS' LIKE '%' || $2 || '%' OR + s.raw_data->>'TELEFONO' LIKE '%' || $2 || '%' + ) + ORDER BY + st.is_final ASC, -- Activos primero + s.created_at DESC -- Luego ordenados por fecha de creación + `, [client.owner_id, client.phone]); - const services = scrapedQ.rows.map(s => { - // 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"; + // Formatear servicios para enviarlos limpios al cliente + const formattedServices = qServices.rows.map(s => ({ + id: s.id, + title: s.is_urgent ? `🚨 URGENTE: Expediente #${s.service_ref}` : `Expediente #${s.service_ref}`, + description: s.raw_data["Descripción"] || s.raw_data["DESCRIPCION"] || "Revisión técnica solicitada.", + status_name: s.status_name || 'Pendiente', + is_final: s.status_is_final, + scheduled_date: s.raw_data.scheduled_date, + scheduled_time: s.raw_data.scheduled_time, + assigned_worker: s.assigned_worker || 'Pendiente' + })); - 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"; } + res.json({ ok: true, client, company, services: formattedServices }); - return { - id: s.id, - title: (s.is_urgent ? "🚨 URGENTE: " : "") + "Expediente #" + s.title, - 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: finalStatusName, - assigned_worker: s.assigned_worker || "Pendiente" - }; - }); - - res.json({ - ok: true, - client: { name: clientData.full_name, phone: clientData.phone, addresses: clientData.addresses }, - company: { name: clientData.company_name, slug: clientData.company_slug, logo: clientData.company_logo }, - services: services - }); - } catch (e) { res.status(500).json({ ok: false, error: "Error de servidor" }); } + } catch (e) { + console.error("Error en Portal Cliente:", e); + res.status(500).json({ ok: false, error: "Server error" }); + } }); // 2. Obtener huecos disponibles inteligentes (CON HORARIOS DINÁMICOS Y TRAMOS DE 1 HORA)