From b1314e6308e8fb8543a750cd94a3d084756a2a70 Mon Sep 17 00:00:00 2001 From: marsalva Date: Sat, 21 Feb 2026 15:33:37 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/server.js b/server.js index a3f8b5d..d1fe915 100644 --- a/server.js +++ b/server.js @@ -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 // ==========================================