From 578130c9f3f2e2e3556c499b50577bc5d57a8c0e Mon Sep 17 00:00:00 2001 From: marsalva Date: Fri, 20 Mar 2026 19:26:23 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 118 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 44 deletions(-) diff --git a/server.js b/server.js index 43fac17..2162032 100644 --- a/server.js +++ b/server.js @@ -1277,7 +1277,7 @@ app.get("/public/portal/:token", async (req, res) => { } }); -// 2. Obtener huecos disponibles inteligentes (CON HORARIOS DINÁMICOS Y TRAMOS DE 1 HORA) +// 2. Obtener huecos disponibles inteligentes (CON HORARIOS DINÁMICOS, TRAMOS Y ENRUTAMIENTO PRO) app.get("/public/portal/:token/slots", async (req, res) => { try { const { token } = req.params; @@ -1291,7 +1291,6 @@ app.get("/public/portal/:token/slots", async (req, res) => { const userQ = await pool.query("SELECT portal_settings FROM users WHERE id = $1", [ownerId]); const pSet = userQ.rows[0]?.portal_settings || { m_start:"09:00", m_end:"14:00", a_start:"16:00", a_end:"19:00" }; - // Función para generar huecos cada 60 minutos (Ventanas de 1 hora) function genSlots(start, end) { if(!start || !end) return []; let s = []; @@ -1300,16 +1299,13 @@ app.get("/public/portal/:token/slots", async (req, res) => { let cur = sh * 60 + sm; let limit = eh * 60 + em; - // Queremos ventanas de 1 hora completas. - // Si el límite es 15:00, el último hueco que puede empezar es a las 14:00. while(cur + 60 <= limit) { s.push(`${String(Math.floor(cur/60)).padStart(2,'0')}:${String(cur%60).padStart(2,'0')}`); - cur += 60; // Avanzamos de hora en hora + cur += 60; } return s; } - // Creamos la plantilla de horas libres del día const morningBase = genSlots(pSet.m_start, pSet.m_end); const afternoonBase = genSlots(pSet.a_start, pSet.a_end); @@ -1321,12 +1317,31 @@ app.get("/public/portal/:token/slots", async (req, res) => { if (!assignedTo) return res.status(400).json({ ok: false, error: "No hay operario asignado" }); const raw = service.raw_data || {}; - const targetZone = (raw["Población"] || raw["POBLACION-PROVINCIA"] || raw["Código Postal"] || "").toLowerCase().trim(); const targetGuildId = raw["guild_id"]; + // 🧠 1. EXTRAER LAS ZONAS DEL OPERARIO PARA EL ALGORITMO DE ENRUTAMIENTO + const workerQ = await pool.query("SELECT zones FROM users WHERE id = $1", [assignedTo]); + const workerZones = workerQ.rows[0]?.zones || []; + + function getCityForCP(cp, fallbackPop) { + let cleanCP = String(cp || "").trim(); + // Buscamos si el CP coincide con la tabla configurada del trabajador + const zone = workerZones.find(z => z.cps === cleanCP); + if (zone && zone.city) return zone.city.toUpperCase().trim(); + // Si el CP no existe en la tabla o no hay, usamos la población escrita como respaldo + return String(fallbackPop || "").toUpperCase().trim(); + } + + // Bautizamos al cliente actual que intenta coger cita con su Ciudad Principal + const targetCity = getCityForCP(raw["Código Postal"] || raw["C.P."], raw["Población"] || raw["POBLACION-PROVINCIA"]); + + // 🧠 2. EXTRAER LA AGENDA TOTAL (FIRMADOS + PENDIENTES DE APROBAR) const agendaQ = await pool.query(` - SELECT COALESCE(NULLIF(raw_data->>'scheduled_date', ''), raw_data->>'requested_date') as date, - COALESCE(NULLIF(raw_data->>'scheduled_time', ''), raw_data->>'requested_time') as time, + SELECT raw_data->>'scheduled_date' as date, + raw_data->>'scheduled_time' as time, + raw_data->>'requested_date' as req_date, + raw_data->>'requested_time' as req_time, + raw_data->>'appointment_status' as appt_status, raw_data->>'duration_minutes' as duration, raw_data->>'Población' as poblacion, raw_data->>'Código Postal' as cp, @@ -1335,74 +1350,86 @@ app.get("/public/portal/:token/slots", async (req, res) => { FROM scraped_services WHERE assigned_to = $1 AND status != 'archived' - AND ( - (raw_data->>'scheduled_date' IS NOT NULL AND raw_data->>'scheduled_date' >= CURRENT_DATE::text) - OR - (raw_data->>'appointment_status' = 'pending' AND raw_data->>'requested_date' >= CURRENT_DATE::text) - ) - `, [assignedTo]); + AND id != $2 + `, [assignedTo, serviceId]); const agendaMap = {}; + agendaQ.rows.forEach(row => { + // Determinar la fecha y hora REALES que ocupan sitio (Firmes o Pendientes) + let effectiveDate = row.date; + let effectiveTime = row.time; + + // 🛡️ REGLA: Si está pendiente de aprobar por la oficina, ¡BLOQUEA EL HUECO PARA EL RESTO! + if (row.appt_status === 'pending' && row.req_date && row.req_time) { + effectiveDate = row.req_date; + effectiveTime = row.req_time; + } + + // Ignorar citas que ya pasaron + if (!effectiveDate || new Date(effectiveDate) < new Date(new Date().toISOString().split('T')[0])) return; + + // Ignorar bloqueos de sistema que pertenezcan a otros gremios if (row.provider === 'SYSTEM_BLOCK' && row.blocked_guild_id && String(row.blocked_guild_id) !== String(targetGuildId)) { return; } - if (!agendaMap[row.date]) agendaMap[row.date] = { times: [], zone: (row.poblacion || row.cp || "").toLowerCase().trim() }; + if (!agendaMap[effectiveDate]) agendaMap[effectiveDate] = { times: [], city: null }; - // Bloqueamos la agenda evaluando la duración estimada real del aviso/bloqueo + // 🏙️ Bautizamos el día con la ciudad del PRIMER servicio agendado + if (row.provider !== 'SYSTEM_BLOCK' && !agendaMap[effectiveDate].city) { + agendaMap[effectiveDate].city = getCityForCP(row.cp, row.poblacion); + } + + // Bloqueamos la franja de tiempo específica calculando duraciones const dur = parseInt(row.duration || 60); - if (row.time) { - let [th, tm] = row.time.split(':').map(Number); + if (effectiveTime) { + let [th, tm] = effectiveTime.split(':').map(Number); let startMin = th * 60 + tm; let endMin = startMin + dur; - // Bloquea cualquier franja de 1 HORA que se solape con este aviso - // Como ahora generamos horas en punto (o y media, según config), chequeamos si el tramo de 60 mins pisa al servicio - morningBase.forEach(slot => { - let [sh, sm] = slot.split(':').map(Number); - let slotStart = sh * 60 + sm; - let slotEnd = slotStart + 60; - if(slotStart < endMin && slotEnd > startMin) { - agendaMap[row.date].times.push(slot); - } - }); - - afternoonBase.forEach(slot => { - let [sh, sm] = slot.split(':').map(Number); - let slotStart = sh * 60 + sm; - let slotEnd = slotStart + 60; - if(slotStart < endMin && slotEnd > startMin) { - agendaMap[row.date].times.push(slot); - } - }); + const blockSlots = (base) => { + base.forEach(slot => { + let [sh, sm] = slot.split(':').map(Number); + let slotStart = sh * 60 + sm; + let slotEnd = slotStart + 60; + if(slotStart < endMin && slotEnd > startMin) { + agendaMap[effectiveDate].times.push(slot); + } + }); + }; + blockSlots(morningBase); + blockSlots(afternoonBase); } }); + // 🧠 3. GENERAR LOS DÍAS DISPONIBLES PARA ESTE CLIENTE const availableDays = []; let d = new Date(); d.setDate(d.getDate() + 1); let daysAdded = 0; + // 🛑 LÍMITE DE 5 DÍAS HÁBILES PARA EVITAR ERRORES CON HOMESERVE while(daysAdded < 5) { - // 🛑 NUEVO: Omitir domingos (0) y sábados (6) - if (d.getDay() !== 0 && d.getDay() !== 6) { + if (d.getDay() !== 0 && d.getDay() !== 6) { // Ignora Sábado y Domingo const dateStr = d.toISOString().split('T')[0]; const dayData = agendaMap[dateStr]; let isDayAllowed = true; - if (dayData && dayData.zone && targetZone) { - if (!dayData.zone.includes(targetZone) && !targetZone.includes(dayData.zone)) { + // 📍 REGLA CRÍTICA DE RUTAS: Si el día está bautizado con otra ciudad que NO es la del cliente, DENEGADO + if (dayData && dayData.city && targetCity) { + if (dayData.city !== targetCity) { isDayAllowed = false; } } + // Si la ciudad coincide (o si el día está virgen): if (isDayAllowed) { const takenTimes = dayData ? dayData.times : []; - // Filtramos nuestra plantilla contra los huecos ocupados const availMorning = morningBase.filter(t => !takenTimes.includes(t)); const availAfternoon = afternoonBase.filter(t => !takenTimes.includes(t)); + // Solo añadimos el día si tiene AL MENOS una hora libre en su ciudad if (availMorning.length > 0 || availAfternoon.length > 0) { availableDays.push({ date: dateStr, @@ -1417,7 +1444,10 @@ app.get("/public/portal/:token/slots", async (req, res) => { d.setDate(d.getDate() + 1); } res.json({ ok: true, days: availableDays }); - } catch (e) { console.error("Error Slots:", e); res.status(500).json({ ok: false }); } + } catch (e) { + console.error("Error Slots:", e); + res.status(500).json({ ok: false }); + } });