Actualizar server.js
This commit is contained in:
133
server.js
133
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 }); }
|
} 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
|
// ⚙️ MOTOR AUTOMÁTICO DE WHATSAPP
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
Reference in New Issue
Block a user