Actualizar server.js
This commit is contained in:
118
server.js
118
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) => {
|
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[effectiveDate].times.push(slot);
|
||||||
agendaMap[row.date].times.push(slot);
|
}
|
||||||
}
|
});
|
||||||
});
|
};
|
||||||
|
blockSlots(morningBase);
|
||||||
afternoonBase.forEach(slot => {
|
blockSlots(afternoonBase);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🧠 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 });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user