diff --git a/server.js b/server.js index 2a6c557..e982876 100644 --- a/server.js +++ b/server.js @@ -857,51 +857,14 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { let agendaOcupadaTexto = "El técnico tiene la agenda libre en horario laboral."; if (datosExpediente.worker_id) { - // 🛑 NUEVO: Consulta avanzada que lee Duraciones, Bloqueos y Citas Pendientes - const agendaQ = await pool.query(` - SELECT - COALESCE(NULLIF(raw_data->>'scheduled_date', ''), raw_data->>'requested_date') as date, - COALESCE(NULLIF(raw_data->>'scheduled_time', ''), raw_data->>'requested_time') as time, - raw_data->>'duration_minutes' as duration, - raw_data->>'Población' as pob, - provider - FROM scraped_services - WHERE assigned_to = $1 - AND status != 'archived' - AND id != $2 - AND ( - (raw_data->>'scheduled_date' IS NOT NULL AND raw_data->>'scheduled_date' >= CURRENT_DATE::text) - OR - (raw_data->>'appointment_status' = 'pending' AND raw_data->>'requested_date' >= CURRENT_DATE::text) - ) - ORDER BY date ASC, time ASC - `, [datosExpediente.worker_id, datosExpediente.dbId]); - + const agendaQ = await pool.query("SELECT raw_data->>'scheduled_date' as date, raw_data->>'scheduled_time' as time, raw_data->>'Población' as pob FROM scraped_services WHERE assigned_to = $1 AND raw_data->>'scheduled_date' >= CURRENT_DATE::text AND status != 'archived' AND id != $2 ORDER BY date ASC, time ASC", [datosExpediente.worker_id, datosExpediente.dbId]); 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] = []; - - // 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 endMin = (h * 60 + m) + dur; - let endH = String(Math.floor(endMin / 60) % 24).padStart(2, '0'); - let endM = String(endMin % 60).padStart(2, '0'); - - let tipo = r.provider === 'SYSTEM_BLOCK' ? 'BLOQUEO/AUSENCIA' : 'CITA'; - let lugar = r.pob || 'Otra zona'; - - ocupaciones[r.date].push(`De ${r.time} a ${endH}:${endM} (${tipo} en ${lugar})`); - } - }); - - const lineas = Object.keys(ocupaciones).map(d => `- Día ${d}:\n * ${ocupaciones[d].join("\n * ")}`); + agendaQ.rows.forEach(r => { if(r.date && r.time) { if(!ocupaciones[r.date]) ocupaciones[r.date] = []; ocupaciones[r.date].push(`${r.time} (en ${r.pob || 'otra zona'})`); } }); + const lineas = Object.keys(ocupaciones).map(d => `- Día ${d}: Ocupado a las ${ocupaciones[d].join(", ")}`); if(lineas.length > 0) { - agendaOcupadaTexto = "Ocupaciones actuales del técnico (Citas confirmadas, citas pendientes de aprobar 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 entre el final de una cita y el inicio de la siguiente. NUNCA ofrezcas una hora que esté pegada a otra cita si hay desplazamiento."; + agendaOcupadaTexto = "Citas actuales:\n " + lineas.join("\n ") + + "\n 👉 IMPORTANTE: Todas las demás horas (09:00, 11:00, 12:00, etc.) ESTÁN TOTALMENTE LIBRES. Ofrécelas sin miedo."; } } } @@ -961,7 +924,7 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { } else if (esUrgencia) { directivaEstricta = `🛑 ESTADO ACTUAL: SERVICIO DE URGENCIA.\nTU ÚNICO OBJETIVO: Tranquilizar al cliente. Dile que al ser urgencia el técnico está avisado.\nPROHIBICIÓN ABSOLUTA: No des cita ni propongas horas.`; } else if (hayCitaPendiente) { - directivaEstricta = `🛑 ESTADO ACTUAL: CITA PENDIENTE DE APROBACIÓN POR TÉCNICO.\n📅 Propuesta actual: El día ${datosExpediente.cita_pendiente_fecha} ${tramoPendiente}.\nTU ÚNICO OBJETIVO: Informar al cliente que estamos esperando confirmación del técnico.\n\n⚠️ EXCEPCIÓN CRÍTICA (REAGENDAR/CANCELAR): \nSi el cliente te dice que NO PUEDE ir, o pide CAMBIAR, CANCELAR o ANULAR esta propuesta, DEBES PERMITIRLO:\n1. Dile que anulamos la petición anterior sin problema.\n2. Mira la "AGENDA DEL TÉCNICO" y ofrécele un hueco libre nuevo.\n3. Si acepta el nuevo hueco, MANDA OBLIGATORIAMENTE el código: [PROPUESTA:YYYY-MM-DD HH:mm] (esto borrará la propuesta anterior y pondrá la nueva).`; + directivaEstricta = `🛑 ESTADO ACTUAL: CITA PENDIENTE DE APROBACIÓN POR TÉCNICO.\n📅 Propuesta actual: El día ${datosExpediente.cita_pendiente_fecha} ${tramoPendiente}.\nTU ÚNICO OBJETIVO: Informar al cliente que estamos esperando confirmación del técnico.\n⚠️ REGLA CRÍTICA: Ignora el historial si no coincide con esta propuesta.\nPROHIBICIÓN ABSOLUTA: No agendes de nuevo.`; } else if (tieneCitaConfirmada) { directivaEstricta = `🛑 ESTADO ACTUAL: CITA 100% CONFIRMADA.\n📅 Día: ${datosExpediente.cita}.\n⏰ Tramo horario: ${tramoConfirmado}.\nTU OBJETIVO PRINCIPAL: Recordar la cita actual.\n\n⚠️ EXCEPCIÓN CRÍTICA (REAGENDAR): \nSi el cliente te dice que NO PUEDE ir, o pide CAMBIAR, MODIFICAR o ANULAR la cita, DEBES PERMITIRLO:\n1. Dile que no hay problema en cambiarla.\n2. Mira la "AGENDA DEL TÉCNICO" y ofrécele un hueco libre nuevo.\n3. Si acepta el nuevo hueco, ⚠️ INDÍSCALE EXPRESAMENTE que le pasas la nota al técnico para que él se lo confirme (NUNCA digas que ya está 100% confirmada). MANDA OBLIGATORIAMENTE el código: [PROPUESTA:YYYY-MM-DD HH:mm]`; } else { @@ -974,8 +937,6 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { --- CONTEXTO BÁSICO --- - Hoy es: ${fechaHoyTexto}. (Año 2026). - Horario de la empresa: L-V de ${horarios.m_start} a ${horarios.m_end} y de ${horarios.a_start} a ${horarios.a_end}. Fines de semana solo URGENCIAS. - - Problema o Avería reportada por el cliente: ${datosExpediente.averia || 'Avería general (no especificada)'}. - - Localidad del cliente actual: ${datosExpediente.poblacion || 'Localidad no especificada'}. --- AGENDA DEL TÉCNICO ASIGNADO --- ${agendaOcupadaTexto} @@ -3828,7 +3789,7 @@ app.post("/services/:id/chat/read", authMiddleware, async (req, res) => { }); // ========================================== -// 💬 CHAT Y COMUNICACIÓN INTERNA +// 💬 CHAT Y COMUNICACIÓN INTERNA (TIPO iTRAMIT) // ========================================== app.get("/services/:id/chat", authMiddleware, async (req, res) => { @@ -3947,8 +3908,7 @@ app.post("/webhook/evolution", async (req, res) => { s.raw_data->>'appointment_status' as appointment_status, s.raw_data->>'requested_date' as cita_pendiente_fecha, s.raw_data->>'requested_time' as cita_pendiente_hora, - s.raw_data->>'Compañía' as compania, - COALESCE(s.raw_data->>'Descripción', s.raw_data->>'DESCRIPCION') as averia + s.raw_data->>'Compañía' as compania FROM scraped_services s LEFT JOIN users u ON s.assigned_to = u.id LEFT JOIN service_statuses st ON (s.raw_data->>'status_operativo')::text = st.id::text @@ -3989,7 +3949,7 @@ app.post("/webhook/evolution", async (req, res) => { const lastMsg = checkHumanQ.rows[0]; const diffMinutos = (new Date() - new Date(lastMsg.created_at)) / (1000 * 60); // Como tu mensaje se guardó como 'operario', aquí saltará esta regla y detendrá a la IA durante 120 min - if (['admin', 'superadmin', 'operario'].includes(lastMsg.sender_role) && diffMinutos < 0) return; + if (['admin', 'superadmin', 'operario'].includes(lastMsg.sender_role) && diffMinutos < 120) return; } // 🧠 LLAMADA A LA IA (Con la hora inyectada) @@ -4006,8 +3966,7 @@ app.post("/webhook/evolution", async (req, res) => { appointment_status: service.appointment_status, cita_pendiente_fecha: service.cita_pendiente_fecha, cita_pendiente_hora: service.cita_pendiente_hora, - compania: service.compania, - averia: service.averia // 👈 NUEVO: LE PASAMOS LA AVERÍA AL CEREBRO + compania: service.compania // 👈 NUEVO: PASAMOS LA COMPAÑÍA }); if (respuestaIA) {