Actualizar server.js

This commit is contained in:
2026-03-20 19:26:23 +00:00
parent e01cf938c5
commit 578130c9f3

110
server.js
View File

@@ -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) => { app.get("/public/portal/:token/slots", async (req, res) => {
try { try {
const { token } = req.params; 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 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" }; 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) { function genSlots(start, end) {
if(!start || !end) return []; if(!start || !end) return [];
let s = []; let s = [];
@@ -1300,16 +1299,13 @@ app.get("/public/portal/:token/slots", async (req, res) => {
let cur = sh * 60 + sm; let cur = sh * 60 + sm;
let limit = eh * 60 + em; 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) { while(cur + 60 <= limit) {
s.push(`${String(Math.floor(cur/60)).padStart(2,'0')}:${String(cur%60).padStart(2,'0')}`); 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; return s;
} }
// Creamos la plantilla de horas libres del día
const morningBase = genSlots(pSet.m_start, pSet.m_end); const morningBase = genSlots(pSet.m_start, pSet.m_end);
const afternoonBase = genSlots(pSet.a_start, pSet.a_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" }); if (!assignedTo) return res.status(400).json({ ok: false, error: "No hay operario asignado" });
const raw = service.raw_data || {}; const raw = service.raw_data || {};
const targetZone = (raw["Población"] || raw["POBLACION-PROVINCIA"] || raw["Código Postal"] || "").toLowerCase().trim();
const targetGuildId = raw["guild_id"]; 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(` const agendaQ = await pool.query(`
SELECT COALESCE(NULLIF(raw_data->>'scheduled_date', ''), raw_data->>'requested_date') as date, SELECT raw_data->>'scheduled_date' as date,
COALESCE(NULLIF(raw_data->>'scheduled_time', ''), raw_data->>'requested_time') as time, 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->>'duration_minutes' as duration,
raw_data->>'Población' as poblacion, raw_data->>'Población' as poblacion,
raw_data->>'Código Postal' as cp, raw_data->>'Código Postal' as cp,
@@ -1335,74 +1350,86 @@ app.get("/public/portal/:token/slots", async (req, res) => {
FROM scraped_services FROM scraped_services
WHERE assigned_to = $1 WHERE assigned_to = $1
AND status != 'archived' AND status != 'archived'
AND ( AND id != $2
(raw_data->>'scheduled_date' IS NOT NULL AND raw_data->>'scheduled_date' >= CURRENT_DATE::text) `, [assignedTo, serviceId]);
OR
(raw_data->>'appointment_status' = 'pending' AND raw_data->>'requested_date' >= CURRENT_DATE::text)
)
`, [assignedTo]);
const agendaMap = {}; const agendaMap = {};
agendaQ.rows.forEach(row => { 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)) { if (row.provider === 'SYSTEM_BLOCK' && row.blocked_guild_id && String(row.blocked_guild_id) !== String(targetGuildId)) {
return; 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); const dur = parseInt(row.duration || 60);
if (row.time) { if (effectiveTime) {
let [th, tm] = row.time.split(':').map(Number); let [th, tm] = effectiveTime.split(':').map(Number);
let startMin = th * 60 + tm; let startMin = th * 60 + tm;
let endMin = startMin + dur; let endMin = startMin + dur;
// Bloquea cualquier franja de 1 HORA que se solape con este aviso const blockSlots = (base) => {
// Como ahora generamos horas en punto (o y media, según config), chequeamos si el tramo de 60 mins pisa al servicio base.forEach(slot => {
morningBase.forEach(slot => {
let [sh, sm] = slot.split(':').map(Number); let [sh, sm] = slot.split(':').map(Number);
let slotStart = sh * 60 + sm; let slotStart = sh * 60 + sm;
let slotEnd = slotStart + 60; let slotEnd = slotStart + 60;
if(slotStart < endMin && slotEnd > startMin) { if(slotStart < endMin && slotEnd > startMin) {
agendaMap[row.date].times.push(slot); agendaMap[effectiveDate].times.push(slot);
} }
}); });
};
afternoonBase.forEach(slot => { blockSlots(morningBase);
let [sh, sm] = slot.split(':').map(Number); blockSlots(afternoonBase);
let slotStart = sh * 60 + sm;
let slotEnd = slotStart + 60;
if(slotStart < endMin && slotEnd > startMin) {
agendaMap[row.date].times.push(slot);
}
});
} }
}); });
// 🧠 3. GENERAR LOS DÍAS DISPONIBLES PARA ESTE CLIENTE
const availableDays = []; const availableDays = [];
let d = new Date(); let d = new Date();
d.setDate(d.getDate() + 1); d.setDate(d.getDate() + 1);
let daysAdded = 0; let daysAdded = 0;
// 🛑 LÍMITE DE 5 DÍAS HÁBILES PARA EVITAR ERRORES CON HOMESERVE
while(daysAdded < 5) { while(daysAdded < 5) {
// 🛑 NUEVO: Omitir domingos (0) y sábados (6) if (d.getDay() !== 0 && d.getDay() !== 6) { // Ignora Sábado y Domingo
if (d.getDay() !== 0 && d.getDay() !== 6) {
const dateStr = d.toISOString().split('T')[0]; const dateStr = d.toISOString().split('T')[0];
const dayData = agendaMap[dateStr]; const dayData = agendaMap[dateStr];
let isDayAllowed = true; let isDayAllowed = true;
if (dayData && dayData.zone && targetZone) { // 📍 REGLA CRÍTICA DE RUTAS: Si el día está bautizado con otra ciudad que NO es la del cliente, DENEGADO
if (!dayData.zone.includes(targetZone) && !targetZone.includes(dayData.zone)) { if (dayData && dayData.city && targetCity) {
if (dayData.city !== targetCity) {
isDayAllowed = false; isDayAllowed = false;
} }
} }
// Si la ciudad coincide (o si el día está virgen):
if (isDayAllowed) { if (isDayAllowed) {
const takenTimes = dayData ? dayData.times : []; const takenTimes = dayData ? dayData.times : [];
// Filtramos nuestra plantilla contra los huecos ocupados
const availMorning = morningBase.filter(t => !takenTimes.includes(t)); const availMorning = morningBase.filter(t => !takenTimes.includes(t));
const availAfternoon = afternoonBase.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) { if (availMorning.length > 0 || availAfternoon.length > 0) {
availableDays.push({ availableDays.push({
date: dateStr, date: dateStr,
@@ -1417,7 +1444,10 @@ app.get("/public/portal/:token/slots", async (req, res) => {
d.setDate(d.getDate() + 1); d.setDate(d.getDate() + 1);
} }
res.json({ ok: true, days: availableDays }); 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 });
}
}); });