Actualizar server.js
This commit is contained in:
207
server.js
207
server.js
@@ -498,212 +498,7 @@ async function ensureInstance(instanceName) {
|
|||||||
return { baseUrl, headers };
|
return { baseUrl, headers };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
app.get("/public/portal/:token
|
||||||
// 🚀 RUTAS PÚBLICAS (MÓVIL OPERARIO)
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
// 1. Cargar datos del cliente, logo y empresa
|
|
||||||
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]);
|
|
||||||
|
|
||||||
if (clientQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace no válido o caducado" });
|
|
||||||
const clientData = clientQ.rows[0];
|
|
||||||
|
|
||||||
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}%`]);
|
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
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,
|
|
||||||
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" }); }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Obtener huecos disponibles inteligentes (CON HORARIOS DINÁMICOS Y TRAMOS DE 1 HORA)
|
|
||||||
app.get("/public/portal/:token/slots", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { token } = req.params;
|
|
||||||
const { serviceId } = req.query;
|
|
||||||
|
|
||||||
const clientQ = await pool.query("SELECT id, owner_id FROM clients WHERE portal_token = $1", [token]);
|
|
||||||
if (clientQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Token inválido" });
|
|
||||||
const ownerId = clientQ.rows[0].owner_id;
|
|
||||||
|
|
||||||
// EXTRAEMOS LA CONFIGURACIÓN DE HORARIOS DEL PORTAL
|
|
||||||
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 = [];
|
|
||||||
let [sh, sm] = start.split(':').map(Number);
|
|
||||||
let [eh, em] = end.split(':').map(Number);
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
|
|
||||||
const serviceQ = await pool.query("SELECT * FROM scraped_services WHERE id=$1", [serviceId]);
|
|
||||||
if (serviceQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Servicio no encontrado" });
|
|
||||||
|
|
||||||
const service = serviceQ.rows[0];
|
|
||||||
const assignedTo = service.assigned_to;
|
|
||||||
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"];
|
|
||||||
|
|
||||||
const agendaQ = await pool.query(`
|
|
||||||
SELECT raw_data->>'scheduled_date' as date,
|
|
||||||
raw_data->>'scheduled_time' as time,
|
|
||||||
raw_data->>'duration_minutes' as duration,
|
|
||||||
raw_data->>'Población' as poblacion,
|
|
||||||
raw_data->>'Código Postal' as cp,
|
|
||||||
provider,
|
|
||||||
raw_data->>'blocked_guild_id' as blocked_guild_id
|
|
||||||
FROM scraped_services
|
|
||||||
WHERE assigned_to = $1
|
|
||||||
AND raw_data->>'scheduled_date' IS NOT NULL
|
|
||||||
AND raw_data->>'scheduled_date' >= CURRENT_DATE::text
|
|
||||||
`, [assignedTo]);
|
|
||||||
|
|
||||||
const agendaMap = {};
|
|
||||||
agendaQ.rows.forEach(row => {
|
|
||||||
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() };
|
|
||||||
|
|
||||||
// Bloqueamos la agenda evaluando la duración estimada real del aviso/bloqueo
|
|
||||||
const dur = parseInt(row.duration || 60);
|
|
||||||
if (row.time) {
|
|
||||||
let [th, tm] = row.time.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 availableDays = [];
|
|
||||||
let d = new Date();
|
|
||||||
d.setDate(d.getDate() + 1);
|
|
||||||
|
|
||||||
let daysAdded = 0;
|
|
||||||
while(daysAdded < 10) {
|
|
||||||
if (d.getDay() !== 0) { // Omitir domingos
|
|
||||||
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)) {
|
|
||||||
isDayAllowed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
if (availMorning.length > 0 || availAfternoon.length > 0) {
|
|
||||||
availableDays.push({
|
|
||||||
date: dateStr,
|
|
||||||
displayDate: d.toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'long' }),
|
|
||||||
morning: availMorning,
|
|
||||||
afternoon: availAfternoon
|
|
||||||
});
|
|
||||||
daysAdded++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.setDate(d.getDate() + 1);
|
|
||||||
}
|
|
||||||
res.json({ ok: true, days: availableDays });
|
|
||||||
} catch (e) { console.error("Error Slots:", e); res.status(500).json({ ok: false }); }
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// --- RUTA PARA GUARDAR LA CITA SOLICITADA POR EL CLIENTE ---
|
// --- RUTA PARA GUARDAR LA CITA SOLICITADA POR EL CLIENTE ---
|
||||||
|
|||||||
Reference in New Issue
Block a user