From baf0d0a98c054141ec0880132c4ac2bb4ff132b7 Mon Sep 17 00:00:00 2001 From: marsalva Date: Sat, 28 Mar 2026 12:46:37 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 66 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/server.js b/server.js index 46049b4..26a0516 100644 --- a/server.js +++ b/server.js @@ -895,12 +895,12 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { if (agendaQ.rowCount > 0) { const ocupaciones = {}; agendaQ.rows.forEach(r => { - if(r.date && r.time && r.time.includes(':')) { - if(!ocupaciones[r.date]) ocupaciones[r.date] = []; + if (r.date && r.time && r.time.includes(':')) { + if (!ocupaciones[r.date]) ocupaciones[r.date] = []; // Calculamos la hora de fin exacta sumando la duración (Ej: 60, 120, 180 min) let [h, m] = r.time.split(':').map(Number); - let dur = parseInt(r.duration || 60); + let dur = parseInt(r.duration || 60, 10); let endMin = (h * 60 + m) + dur; let endH = String(Math.floor(endMin / 60) % 24).padStart(2, '0'); let endM = String(endMin % 60).padStart(2, '0'); @@ -912,8 +912,17 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { } }); - const lineas = Object.keys(ocupaciones).map(d => `- Día ${d}:\n * ${ocupaciones[d].join("\n * ")}`); - if(lineas.length > 0) { + const lineas = Object.keys(ocupaciones).sort().map(d => { + const [y, m, day] = d.split('-').map(Number); + const fechaHumana = new Date(y, m - 1, day, 12, 0, 0).toLocaleDateString('es-ES', { + weekday: 'long', + day: 'numeric', + month: 'long' + }); + return `- Día ${fechaHumana} (${d}):\n * ${ocupaciones[d].join("\n * ")}`; + }); + + if (lineas.length > 0) { agendaOcupadaTexto = "Ocupaciones actuales del técnico (Citas confirmadas, pendientes y bloqueos):\n" + lineas.join("\n") + "\n\n👉 IMPORTANTE: Todas las horas que NO se solapen con esos tramos exactos ESTÁN LIBRES." + "\n🚨 REGLA LOGÍSTICA ESTRICTA: El técnico necesita tiempo para viajar. Si el cliente actual es de una localidad distinta a la cita anterior o posterior (ej: Algeciras vs La Línea), ES OBLIGATORIO dejar un margen de al menos 45-60 minutos de viaje entre el final de una cita y el inicio de la siguiente. NUNCA ofrezcas horas pegadas si hay desplazamiento."; @@ -4054,27 +4063,44 @@ app.post("/webhook/evolution", async (req, res) => { // 🛡️ REGEX BLINDADO: Pilla la etiqueta aunque la IA meta espacios raros const matchPropuesta = respuestaIA.match(/\[PROPUESTA:\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\]/i); + // 🧹 BORRAMOS EL TEXTO DEL CÓDIGO PARA QUE EL CLIENTE NO LO VEA NUNCA + let textoLimpio = respuestaIA.replace(/\[PROPUESTA:.*?\]/gi, "").replace(/código:/gi, "").trim(); + if (matchPropuesta) { const fechaSugerida = matchPropuesta[1]; const horaSugerida = matchPropuesta[2]; - - // 🚀 GUARDADO COMO PENDIENTE (Espera a que el Técnico la apruebe en la App o en la Oficina) - await pool.query(` - UPDATE scraped_services - SET raw_data = raw_data || jsonb_build_object( - 'requested_date', $1::text, - 'requested_time', $2::text, - 'appointment_status', 'pending' - ) WHERE id = $3 - `, [fechaSugerida, horaSugerida, service.id]); - } - // 🧹 BORRAMOS EL TEXTO DEL CÓDIGO PARA QUE EL CLIENTE NO LO VEA NUNCA - const textoLimpio = respuestaIA.replace(/\[PROPUESTA:.*?\]/gi, "").replace(/código:/gi, "").trim(); + // 🛡️ ESCUDO ANTI-SOLAPE: comprobamos antes de guardar la propuesta + const disponibilidad = await comprobarDisponibilidad( + ownerId, + service.assigned_to, + fechaSugerida, + horaSugerida, + 60, + service.id + ); + + if (disponibilidad.choca) { + console.log(`⛔ [DOBLE-BOOKING EVITADO] Exp ${service.service_ref} chocaba con ${disponibilidad.ref} a las ${disponibilidad.time}`); + textoLimpio = "Uy, perdona, se me acaban de cruzar los cables y justo me han bloqueado ese hueco por el sistema interno. 😅 ¿Me dices otra hora que te venga bien?"; + } else { + // 🚀 GUARDADO COMO PENDIENTE (Espera a que el Técnico la apruebe en la App o en la Oficina) + await pool.query(` + UPDATE scraped_services + SET raw_data = raw_data || jsonb_build_object( + 'requested_date', $1::text, + 'requested_time', $2::text, + 'appointment_status', 'pending' + ) WHERE id = $3 + `, [fechaSugerida, horaSugerida, service.id]); + } + } await sendWhatsAppAuto(telefonoCliente, textoLimpio, instanceName, true); - await pool.query(`INSERT INTO service_communications (scraped_id, owner_id, sender_name, sender_role, message) VALUES ($1, $2, $3, $4, $5)`, - [service.id, ownerId, "Asistente IA", "ia", textoLimpio]); + await pool.query( + `INSERT INTO service_communications (scraped_id, owner_id, sender_name, sender_role, message) VALUES ($1, $2, $3, $4, $5)`, + [service.id, ownerId, "Asistente IA", "ia", textoLimpio] + ); } } finally { candadosIA.delete(service.id);