Actualizar server.js

This commit is contained in:
2026-02-21 15:33:37 +00:00
parent b2077cf2c1
commit b1314e6308

133
server.js
View File

@@ -610,6 +610,139 @@ app.get("/whatsapp/status", authMiddleware, (req, res, next) => requirePlan(req,
} catch (e) { res.status(500).json({ ok: false, error: e.message }); }
});
// ==========================================
// 🧠 MOTOR INTELIGENTE DE AGENDAMIENTO (RUTAS NUEVAS)
// ==========================================
// 1. Obtener huecos disponibles inteligentes
app.get("/public/portal/:token/slots", async (req, res) => {
try {
const { token } = req.params;
const { serviceId } = req.query;
// 1. Validar cliente y servicio
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 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" });
// 2. Extraer "Zona" del servicio actual (Población o CP) para agrupar
const raw = service.raw_data || {};
const targetZone = (raw["Población"] || raw["POBLACION-PROVINCIA"] || raw["Código Postal"] || "").toLowerCase().trim();
// 3. Buscar la agenda del operario para los próximos 14 días
const agendaQ = await pool.query(`
SELECT raw_data->>'scheduled_date' as date,
raw_data->>'scheduled_time' as time,
raw_data->>'Población' as poblacion,
raw_data->>'Código Postal' as cp
FROM scraped_services
WHERE assigned_to = $1
AND raw_data->>'scheduled_date' IS NOT NULL
AND raw_data->>'scheduled_date' >= CURRENT_DATE::text
`, [assignedTo]);
// Mapa de días ocupados y sus zonas "Ancla"
const agendaMap = {};
agendaQ.rows.forEach(row => {
if (!agendaMap[row.date]) agendaMap[row.date] = { times: [], zone: (row.poblacion || row.cp || "").toLowerCase().trim() };
agendaMap[row.date].times.push(row.time);
});
// 4. Generar próximos 10 días laborables
const availableDays = [];
let d = new Date();
d.setDate(d.getDate() + 1); // Empezamos desde mañana
let daysAdded = 0;
while(daysAdded < 10) {
// Saltamos domingos
if (d.getDay() !== 0) {
const dateStr = d.toISOString().split('T')[0]; // YYYY-MM-DD
const dayData = agendaMap[dateStr];
let isDayAllowed = true;
// REGLA DE AGRUPACIÓN (EL CEREBRO):
// Si el día ya tiene servicios, comprobamos si la zona coincide.
if (dayData && dayData.zone && targetZone) {
// Si el día está anclado a una zona diferente, bloqueamos el día para este cliente
if (!dayData.zone.includes(targetZone) && !targetZone.includes(dayData.zone)) {
isDayAllowed = false;
}
}
if (isDayAllowed) {
// Generar Huecos. Mañana: 9 a 13 | Tarde: 16 a 19
const morningSlots = ["09:00", "10:00", "11:00", "12:00", "13:00"];
const afternoonSlots = ["16:00", "17:00", "18:00", "19:00"];
const takenTimes = dayData ? dayData.times : [];
const availMorning = morningSlots.filter(t => !takenTimes.includes(t));
const availAfternoon = afternoonSlots.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 }); }
});
// 2. Guardar la cita elegida por el cliente
app.post("/public/portal/:token/book", async (req, res) => {
const client = await pool.connect();
try {
const { token } = req.params;
const { serviceId, date, time } = req.body;
await client.query('BEGIN');
const clientQ = await client.query("SELECT owner_id FROM clients WHERE portal_token = $1", [token]);
if (clientQ.rowCount === 0) throw new Error("Token inválido");
const ownerId = clientQ.rows[0].owner_id;
const serviceQ = await client.query("SELECT raw_data FROM scraped_services WHERE id=$1 AND owner_id=$2", [serviceId, ownerId]);
if (serviceQ.rowCount === 0) throw new Error("Servicio no encontrado");
const raw = serviceQ.rows[0].raw_data;
// Buscamos el estado "Citado"
const statusQ = await client.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND name ILIKE '%citado%' LIMIT 1", [ownerId]);
const idCitado = statusQ.rows[0]?.id || raw.status_operativo;
const updatedRaw = { ...raw, scheduled_date: date, scheduled_time: time, status_operativo: idCitado };
await client.query("UPDATE scraped_services SET raw_data = $1 WHERE id = $2", [JSON.stringify(updatedRaw), serviceId]);
// Disparamos el WhatsApp informando de la cita
await triggerWhatsAppEvent(ownerId, serviceId, 'wa_evt_date');
await client.query('COMMIT');
res.json({ ok: true });
} catch (e) {
await client.query('ROLLBACK');
console.error("Error Booking:", e);
res.status(500).json({ ok: false, error: e.message });
} finally { client.release(); }
});
// ==========================================
// ⚙️ MOTOR AUTOMÁTICO DE WHATSAPP
// ==========================================