diff --git a/.agente/objetivos.md b/.agente/objetivos.md index 97f260d..f81c9cf 100644 --- a/.agente/objetivos.md +++ b/.agente/objetivos.md @@ -22,6 +22,7 @@ | Fecha | Objetivo | Evidencia | |---|---|---| +| 2026-10-17 | **OBJ-007:** Módulo de Asignación de Tareas y Visitas Técnicas (FSM) integrado con `tareas.telcotronics.com` | Endpoints externos en CT 132 activados (`POST /api/external/tareas` y `GET /api/external/tareas/:id/status`), tabla `ticket_visitas` en MariaDB con `historial_json`, servicio `config/tareasHubService.js`, modelo `models/TicketVisita.js`, rutas `/agent/ticket/:id/visita` y `/sync`, y Mini-Timeline en `views/agent/view-ticket.ejs` | | 2026-10-16 | **OBJ-006:** Integración de bitácora centralizada de errores y remoto Git | Módulo `config/errorReporter.js`, middleware global en `app.js`, script `scripts/consultar_errores.js` (`npm run check:errors`) y remoto `git.telcotronics.net/pablinux/help_desk.git` configurado | | 2026-10-16 | **OBJ-000:** Inicialización documental estandarizada según estándar Telcotronics | Estructura `.agente/` completa, espejo `infraestructura.md` sincronizado al 100%, `id_agente.md`, `agents.md` y `README.md` alineados | | 2026-05-16 | Implementación del backend base con Express 5 y MariaDB | Modelos `User`, `Department`, `Ticket`, rutas funcionales y conexión a `192.168.10.149` | diff --git a/.agente/proyecto_estructura.md b/.agente/proyecto_estructura.md index ae86f25..76f2b8a 100644 --- a/.agente/proyecto_estructura.md +++ b/.agente/proyecto_estructura.md @@ -24,15 +24,18 @@ help_desk/ │ ├── db.js # Pool de conexión a MariaDB con mysql2/promise │ ├── errorReporter.js # Cliente y reporter de errores hacia API-SIGMA-WEBCONTROL │ ├── init.sql # Esquema SQL relacional base -│ └── setup.js # Script de inicialización y migración multi-tenant +│ ├── setup.js # Script de inicialización y migración multi-tenant (incluye ticket_visitas) +│ └── tareasHubService.js # Conexión e integración con API externa tareas.telcotronics.com ├── scripts/ -│ └── consultar_errores.js # Script CLI para consultar la bitácora centralizada de errores +│ ├── consultar_errores.js # Script CLI para consultar la bitácora centralizada de errores +│ └── test_visita_flow.js # Script CLI de prueba e integración end-to-end para FSM ├── middleware/ │ └── auth.js # Middlewares: isAuthenticated, roles y loadCompany ├── models/ │ ├── Company.js # Consultas y operaciones para empresas/tenants │ ├── Department.js # Operaciones CRUD para departamentos │ ├── Ticket.js # Consultas, creación y respuestas de tickets +│ ├── TicketVisita.js # Modelo y persistencia para visitas técnicas y timeline FSM │ └── User.js # Autenticación, creación y gestión de usuarios ├── routes/ │ ├── auth.js # Rutas de autenticación (login, register, logout) @@ -91,10 +94,12 @@ help_desk/ | **GET** | `/client/tickets/:id` | `isAuthenticated`, `isClient` | Vista de detalle de un ticket y su conversación | | **POST** | `/client/tickets/:id/reply` | `isAuthenticated`, `isClient` | Publica una respuesta a un ticket propio | | **GET** | `/agent/dashboard` | `isAuthenticated`, `isAgent` | Bandeja de tickets de la empresa para agentes | -| **GET** | `/agent/tickets/:id` | `isAuthenticated`, `isAgent` | Detalle del ticket, cambio de estado y asignación | -| **POST** | `/agent/tickets/:id/reply` | `isAuthenticated`, `isAgent` | Envía respuesta oficial del agente | -| **POST** | `/agent/tickets/:id/status`| `isAuthenticated`, `isAgent` | Actualiza estado: `abierto`, `en_progreso`, `cerrado` | -| **POST** | `/agent/tickets/:id/assign`| `isAuthenticated`, `isAgent` | Asigna ticket a un agente específico | +| **GET** | `/agent/ticket/:id` | `isAuthenticated`, `isAgent` | Detalle del ticket, timeline de visitas y asignación | +| **POST** | `/agent/ticket/:id/respond` | `isAuthenticated`, `isAgent` | Envía respuesta oficial del agente | +| **POST** | `/agent/ticket/:id/status`| `isAuthenticated`, `isAgent` | Actualiza estado: `abierto`, `en_progreso`, `cerrado` | +| **POST** | `/agent/ticket/:id/assign`| `isAuthenticated`, `isAgent` | Asigna ticket a un agente específico | +| **POST** | `/agent/ticket/:id/visita`| `isAuthenticated`, `isAgent` | Programa visita técnica y crea tarea FSM en tareas.telcotronics.com | +| **POST** | `/agent/ticket/:id/visita/sync`| `isAuthenticated`, `isAgent` | Sincroniza estado, GPS, fotos y firma desde tareas.telcotronics.com | | **GET** | `/admin/dashboard` | `isAuthenticated`, `isAdmin` | Métricas y estadísticas de la empresa | | **GET** | `/admin/users` | `isAuthenticated`, `isAdmin` | Lista de usuarios del tenant | | **GET** | `/admin/users/create` | `isAuthenticated`, `isAdmin` | Formulario para crear agentes o administradores | diff --git a/.agente/proyecto_memoria.md b/.agente/proyecto_memoria.md index b28653b..1cae676 100644 --- a/.agente/proyecto_memoria.md +++ b/.agente/proyecto_memoria.md @@ -38,6 +38,17 @@ 6. **Estilos y Assets Livianos:** - Decisión: Bootstrap 5 y Font Awesome se cargan desde CDN público, complementados con `public/css/style.css` y `public/js/main.js` para personalizaciones locales. +7. **Módulo de Asignación de Tareas y Visitas Técnicas (FSM - Field Service Management):** + - Decisión: Integración directa bidireccional entre `help_desk` y el sistema central `tareas.telcotronics.com` (CT 132 `192.168.10.160:3000`). + - Cuando un ticket requiere atención presencial, el agente programa la visita desde el modal de Help Desk asignando a técnicos reales del Hub (obtenidos en vivo de `/api/external/users/list`). + - Se crea una tarea operativa en el Hub (`POST /api/external/tareas`) con destino (dirección, coordenadas GPS, referencia), prioridad y un checklist inicial de tareas. + - En MariaDB se almacena en la tabla `ticket_visitas`, incluyendo la columna `historial_json` (LONGTEXT) para cachear el timeline operativo (hora de llegada/check-in GPS, progreso del checklist, fotos de evidencia en terreno y firma digital del cliente). + - En la vista del ticket (`view-ticket.ejs`), se presenta un **Mini-Timeline de Seguimiento Operativo en Vivo** con botón de sincronización a demanda (`POST /agent/ticket/:id/visita/sync`). + - Automatización bidireccional: al programar se actualiza el ticket a `en_progreso` y se publica un mensaje en el hilo. Al detectar que la tarea externa fue completada en campo, se actualiza la visita a `completada` y se inserta una respuesta de cierre con las observaciones y firma del cliente en el hilo del ticket. + +8. **Bitácora Centralizada de Errores (API-SIGMA-WEBCONTROL):** + - Decisión: Integración con el servicio central de auditoría de errores en `http://192.168.10.149:3005` mediante `config/errorReporter.js`. Dispone de throttling en memoria (10 min) para no saturar la API central ante ráfagas de errores idénticos, y middleware global de Express en `app.js`. + --- ## Base de Datos @@ -51,6 +62,7 @@ - `departments`: Departamentos de soporte por empresa (id, company_id, nombre, descripcion, created_at). - `tickets`: Tickets de soporte (id, company_id, user_id, department_id, asunto, mensaje, status, prioridad, assigned_to, timestamps). - `ticket_respuestas`: Mensajes e hilo de conversación de cada ticket (id, ticket_id, user_id, mensaje, created_at). + - `ticket_visitas`: Visitas técnicas y tareas de campo vinculadas a tickets y al Hub de tareas (id, company_id, ticket_id, external_id, tecnico_email, tecnico_nombre, titulo, descripcion, ubicacion, fecha_visita, fecha_fin, estado, checklist_json, historial_json, created_at, updated_at). --- diff --git a/config/setup.js b/config/setup.js index 82ef81e..128e38f 100644 --- a/config/setup.js +++ b/config/setup.js @@ -90,6 +90,35 @@ async function setup() { await conn.query('ALTER TABLE tickets ADD FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE'); } catch (e) { if (e.errno !== 1826 && e.errno !== 1005) throw e; } + // Create ticket_visitas table + await conn.query(` + CREATE TABLE IF NOT EXISTS ticket_visitas ( + id INT AUTO_INCREMENT PRIMARY KEY, + company_id INT NOT NULL, + ticket_id INT NOT NULL, + external_id VARCHAR(100) NULL, + tecnico_email VARCHAR(150) NOT NULL, + tecnico_nombre VARCHAR(150) NULL, + titulo VARCHAR(255) NOT NULL, + descripcion TEXT NULL, + ubicacion VARCHAR(255) NULL, + latitud DECIMAL(10, 7) NULL, + longitud DECIMAL(10, 7) NULL, + fecha_visita DATETIME NULL, + fecha_fin DATETIME NULL, + estado ENUM('programada', 'en_camino', 'en_sitio', 'completada', 'cancelada') DEFAULT 'programada', + checklist_json TEXT NULL, + historial_json LONGTEXT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX (ticket_id), + INDEX (company_id), + INDEX (external_id), + FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE, + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + // Seed departments const [depts] = await conn.query('SELECT COUNT(*) as c FROM departments WHERE company_id = ?', [companyId]); if (depts[0].c === 0) { diff --git a/config/tareasHubService.js b/config/tareasHubService.js new file mode 100644 index 0000000..77dbdc0 --- /dev/null +++ b/config/tareasHubService.js @@ -0,0 +1,139 @@ +/** + * tareasHubService.js + * Servicio de integración con el sistema de Tareas y Gestión de Campo (FSM) de Telcotronics. + * Conecta con https://tareas.telcotronics.com/api/external + */ + +const HUB_API_URL = process.env.HUB_API_URL || 'https://tareas.telcotronics.com/api/external'; +const HUB_API_KEY = process.env.HUB_API_KEY || 'sigma_crm_test_key_123'; + +class TareasHubService { + /** + * Obtiene el listado de técnicos/usuarios disponibles en tareas.telcotronics.com + */ + static async obtenerTecnicos() { + try { + const url = `${HUB_API_URL}/users/list`; + const res = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'x-api-key': HUB_API_KEY + } + }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: Fallo al consultar técnicos en Hub`); + } + + const data = await res.json(); + return data.usuarios || []; + } catch (error) { + console.error('[TareasHubService.obtenerTecnicos]:', error.message); + return []; + } + } + + /** + * Programa una visita técnica / tarea operativa con trazabilidad de campo (FSM) + */ + static async crearVisita({ + ticketId, + titulo, + descripcion, + emailTecnico, + fechaVisita = null, + fechaFin = null, + ubicacion = '', + coordenadas = null, + referencia = '', + prioridad = 'Media', + checklist = [] + }) { + try { + const url = `${HUB_API_URL}/tareas`; + + let lat = null; + let lng = null; + if (coordenadas && typeof coordenadas === 'string' && coordenadas.includes(',')) { + const parts = coordenadas.split(','); + lat = parseFloat(parts[0].trim()) || null; + lng = parseFloat(parts[1].trim()) || null; + } + + const payload = { + titulo: titulo || `Visita Técnica - Ticket #${ticketId}`, + descripcion: descripcion || `Visita técnica generada desde Help Desk para el Ticket #${ticketId}`, + emailTecnico: emailTecnico, + prioridad: prioridad, + ticket_id: String(ticketId), + origen: 'HELP_DESK', + origen_externo_id: String(ticketId), + fechaInicio: fechaVisita ? new Date(fechaVisita).toISOString() : new Date().toISOString(), + fechaLimite: fechaFin ? new Date(fechaFin).toISOString() : (fechaVisita ? new Date(fechaVisita).toISOString() : null), + ubicacion: ubicacion, + destino: { + direccion: ubicacion, + latitud: lat, + longitud: lng, + referencia: referencia + }, + checklist_inicial: Array.isArray(checklist) && checklist.length > 0 + ? checklist + : [ + 'Revisión física de acometida y equipamiento', + 'Verificación de conectividad y potencia', + 'Pruebas operativas con el cliente y firma' + ] + }; + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'x-api-key': HUB_API_KEY + }, + body: JSON.stringify(payload) + }); + + const data = await res.json(); + if (!res.ok || !data.success) { + throw new Error(data.mensaje || data.error || `HTTP ${res.status} al crear tarea en Hub`); + } + + return data; + } catch (error) { + console.error('[TareasHubService.crearVisita]:', error.message); + throw error; + } + } + + /** + * Consulta el estado en tiempo real y el timeline operativo de la tarea/visita en el Hub + */ + static async consultarEstadoVisita(externalIdOrTicketId) { + try { + const url = `${HUB_API_URL}/tareas/${externalIdOrTicketId}/status`; + const res = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'x-api-key': HUB_API_KEY + } + }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: No se pudo obtener el estado de la tarea`); + } + + const data = await res.json(); + return data; + } catch (error) { + console.error('[TareasHubService.consultarEstadoVisita]:', error.message); + throw error; + } + } +} + +module.exports = TareasHubService; diff --git a/models/TicketVisita.js b/models/TicketVisita.js new file mode 100644 index 0000000..e8a5e8a --- /dev/null +++ b/models/TicketVisita.js @@ -0,0 +1,89 @@ +const db = require('../config/db'); + +class TicketVisita { + static async create(data) { + const { + company_id, + ticket_id, + external_id = null, + tecnico_email, + tecnico_nombre = null, + titulo, + descripcion = null, + ubicacion = null, + latitud = null, + longitud = null, + fecha_visita = null, + fecha_fin = null, + estado = 'programada', + checklist_json = null, + historial_json = null + } = data; + + const [result] = await db.query( + `INSERT INTO ticket_visitas + (company_id, ticket_id, external_id, tecnico_email, tecnico_nombre, titulo, descripcion, ubicacion, latitud, longitud, fecha_visita, fecha_fin, estado, checklist_json, historial_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + company_id, + ticket_id, + external_id, + tecnico_email, + tecnico_nombre, + titulo, + descripcion, + ubicacion, + latitud, + longitud, + fecha_visita, + fecha_fin, + estado, + typeof checklist_json === 'object' && checklist_json !== null ? JSON.stringify(checklist_json) : checklist_json, + typeof historial_json === 'object' && historial_json !== null ? JSON.stringify(historial_json) : historial_json + ] + ); + return result.insertId; + } + + static async findByTicket(companyId, ticketId) { + const [rows] = await db.query( + 'SELECT * FROM ticket_visitas WHERE company_id = ? AND ticket_id = ? ORDER BY id DESC LIMIT 1', + [companyId, ticketId] + ); + return rows[0] || null; + } + + static async findAllByTicket(companyId, ticketId) { + const [rows] = await db.query( + 'SELECT * FROM ticket_visitas WHERE company_id = ? AND ticket_id = ? ORDER BY id DESC', + [companyId, ticketId] + ); + return rows; + } + + static async findById(id) { + const [rows] = await db.query('SELECT * FROM ticket_visitas WHERE id = ?', [id]); + return rows[0] || null; + } + + static async update(id, data) { + const fields = []; + const values = []; + + for (const [key, val] of Object.entries(data)) { + fields.push(`${key} = ?`); + if ((key === 'checklist_json' || key === 'historial_json') && typeof val === 'object' && val !== null) { + values.push(JSON.stringify(val)); + } else { + values.push(val); + } + } + + if (fields.length === 0) return; + values.push(id); + + await db.query(`UPDATE ticket_visitas SET ${fields.join(', ')} WHERE id = ?`, values); + } +} + +module.exports = TicketVisita; diff --git a/routes/agent.js b/routes/agent.js index ac5a712..50484d7 100644 --- a/routes/agent.js +++ b/routes/agent.js @@ -3,6 +3,8 @@ const router = express.Router(); const { isAgent } = require('../middleware/auth'); const Ticket = require('../models/Ticket'); const Department = require('../models/Department'); +const TicketVisita = require('../models/TicketVisita'); +const TareasHubService = require('../config/tareasHubService'); router.get('/dashboard', isAgent, async (req, res) => { try { @@ -40,7 +42,21 @@ router.get('/ticket/:id', isAgent, async (req, res) => { const allUsers = await User.findAll(req.session.user.company_id); agents = allUsers.filter(u => u.rol === 'agent'); } - res.render('agent/view-ticket', { ticket, respuestas, agents }); + + const visita = await TicketVisita.findByTicket(ticket.company_id, req.params.id); + if (visita && visita.historial_json && typeof visita.historial_json === 'string') { + try { + visita.historial = JSON.parse(visita.historial_json); + } catch (e) { + visita.historial = null; + } + } else if (visita && visita.historial_json && typeof visita.historial_json === 'object') { + visita.historial = visita.historial_json; + } + + const tecnicosHub = await TareasHubService.obtenerTecnicos(); + + res.render('agent/view-ticket', { ticket, respuestas, agents, visita, tecnicosHub }); } catch (error) { console.error(error); req.flash('error_msg', 'Error al cargar ticket'); @@ -103,4 +119,156 @@ router.post('/ticket/:id/assign', isAgent, async (req, res) => { } }); +// Programar Visita Técnica hacia tareas.telcotronics.com (FSM / Hub) +router.post('/ticket/:id/visita', isAgent, async (req, res) => { + try { + const ticket = await Ticket.findById(req.params.id); + if (!ticket || (ticket.company_id !== req.session.user.company_id && req.session.user.rol !== 'superadmin')) { + req.flash('error_msg', 'Ticket no encontrado o sin permisos'); + return res.redirect('/agent/dashboard'); + } + + const { + emailTecnico, + tecnicoNombre, + titulo, + descripcion, + ubicacion, + coordenadas, + referencia, + fechaVisita, + fechaFin, + prioridad, + checklist_items + } = req.body; + + if (!emailTecnico) { + req.flash('error_msg', 'Debe seleccionar un técnico responsable para la visita'); + return res.redirect(`/agent/ticket/${req.params.id}`); + } + + // Procesar checklist + let checklistArr = []; + if (typeof checklist_items === 'string') { + checklistArr = checklist_items.split('\n').map(s => s.trim()).filter(Boolean); + } else if (Array.isArray(checklist_items)) { + checklistArr = checklist_items.filter(Boolean); + } + + if (checklistArr.length === 0) { + checklistArr = [ + 'Revisión física de acometida y equipamiento', + 'Verificación de conectividad y potencia', + 'Pruebas operativas con el cliente y firma' + ]; + } + + const tituloVisita = titulo || `Visita Técnica: ${ticket.asunto} (Ticket #${ticket.id})`; + const descVisita = descripcion || ticket.mensaje || 'Atención técnica presencial solicitada'; + + // 1. Crear en tareas.telcotronics.com + const hubRes = await TareasHubService.crearVisita({ + ticketId: ticket.id, + titulo: tituloVisita, + descripcion: descVisita, + emailTecnico, + fechaVisita: fechaVisita || null, + fechaFin: fechaFin || null, + ubicacion: ubicacion || '', + coordenadas: coordenadas || '', + referencia: referencia || '', + prioridad: prioridad || 'Media', + checklist: checklistArr + }); + + // 2. Guardar en MariaDB ticket_visitas + await TicketVisita.create({ + company_id: ticket.company_id, + ticket_id: ticket.id, + external_id: hubRes.tareaId, + tecnico_email: emailTecnico, + tecnico_nombre: tecnicoNombre || emailTecnico, + titulo: tituloVisita, + descripcion: descVisita, + ubicacion: ubicacion || '', + fecha_visita: fechaVisita ? new Date(fechaVisita) : new Date(), + fecha_fin: fechaFin ? new Date(fechaFin) : null, + estado: 'programada', + checklist_json: checklistArr, + historial_json: hubRes.tarea || null + }); + + // 3. Actualizar estado del ticket a 'en_progreso' + await Ticket.update(ticket.id, { status: 'en_progreso' }); + + // 4. Registrar respuesta en el hilo del ticket + const fechaStr = fechaVisita ? new Date(fechaVisita).toLocaleString('es-EC') : 'A coordinar'; + const msgHilo = `📅 **Visita técnica programada**: Se ha asignado al técnico **${tecnicoNombre || emailTecnico}** para el **${fechaStr}**.\n\n📍 **Ubicación:** ${ubicacion || 'En sitio del cliente'}${referencia ? ` (${referencia})` : ''}\n\n📋 **Checklist operativo inicial:**\n${checklistArr.map(c => `- ${c}`).join('\n')}\n\n*ID de tarea en Hub:* \`${hubRes.tareaId}\``; + await Ticket.addResponse(ticket.id, req.session.user.id, msgHilo); + + req.flash('success_msg', 'Visita técnica programada y sincronizada exitosamente con tareas.telcotronics.com'); + res.redirect(`/agent/ticket/${ticket.id}`); + + } catch (error) { + console.error('Error al programar visita técnica:', error); + req.flash('error_msg', 'Error al programar visita técnica: ' + error.message); + res.redirect(`/agent/ticket/${req.params.id}`); + } +}); + +// Sincronizar Estado y Timeline desde tareas.telcotronics.com +router.post('/ticket/:id/visita/sync', isAgent, async (req, res) => { + try { + const ticket = await Ticket.findById(req.params.id); + if (!ticket || (ticket.company_id !== req.session.user.company_id && req.session.user.rol !== 'superadmin')) { + req.flash('error_msg', 'Ticket no encontrado'); + return res.redirect('/agent/dashboard'); + } + + const visita = await TicketVisita.findByTicket(ticket.company_id, ticket.id); + if (!visita || !visita.external_id) { + req.flash('error_msg', 'No hay visita técnica activa vinculada con Hub'); + return res.redirect(`/agent/ticket/${ticket.id}`); + } + + const hubData = await TareasHubService.consultarEstadoVisita(visita.external_id); + const tarea = hubData.tarea; + + // Mapear estado + let nuevoEstado = visita.estado; + if (tarea.estado === 'Completado' || tarea.estado === 'Finalizada') { + nuevoEstado = 'completada'; + } else if (tarea.checkIn && tarea.checkIn.fecha) { + nuevoEstado = 'en_sitio'; + } else if (tarea.estado === 'En progreso') { + nuevoEstado = 'en_camino'; + } + + const estadoAnterior = visita.estado; + await TicketVisita.update(visita.id, { + estado: nuevoEstado, + historial_json: tarea + }); + + // Automatización bidireccional si se completó la visita + if (nuevoEstado === 'completada' && estadoAnterior !== 'completada') { + const observacion = tarea.observacion || 'Visita finalizada satisfactoriamente en terreno.'; + const progreso = tarea.progreso_checklist; + const firmante = (tarea.firma_cliente && tarea.firma_cliente.firmado) + ? `\n✍️ *Firmado por:* ${tarea.firma_cliente.nombreFirmante} (${tarea.firma_cliente.cedulaFirmante || 'Cliente'})` + : ''; + const msgCierre = `✅ **Visita técnica completada en terreno** por **${visita.tecnico_nombre}**.\n\n📝 **Observaciones de cierre:** ${observacion}\n📊 **Checklist completado:** ${progreso?.completados || 0}/${progreso?.total || 0} tareas.${firmante}`; + + await Ticket.addResponse(ticket.id, req.session.user.id, msgCierre); + } + + req.flash('success_msg', 'Estado de la visita técnica sincronizado con el Hub'); + res.redirect(`/agent/ticket/${ticket.id}`); + } catch (error) { + console.error('Error sincronizando visita:', error); + req.flash('error_msg', 'Error al sincronizar con Hub: ' + error.message); + res.redirect(`/agent/ticket/${req.params.id}`); + } +}); + module.exports = router; diff --git a/scripts/test_visita_flow.js b/scripts/test_visita_flow.js new file mode 100644 index 0000000..0d53855 --- /dev/null +++ b/scripts/test_visita_flow.js @@ -0,0 +1,110 @@ +const db = require('../config/db'); +const Ticket = require('../models/Ticket'); +const TicketVisita = require('../models/TicketVisita'); +const TareasHubService = require('../config/tareasHubService'); + +async function run() { + console.log('--- 1. Probando obtenerTecnicos() ---'); + const tecnicos = await TareasHubService.obtenerTecnicos(); + console.log(`Técnicos obtenidos (${tecnicos.length}):`, tecnicos.map(t => `${t.nombre || t.email} <${t.email}>`)); + + const emailTecnico = tecnicos.length > 0 ? tecnicos[0].email : 'pablinux@hotmail.es'; + const nombreTecnico = tecnicos.length > 0 ? (tecnicos[0].nombre || tecnicos[0].email) : 'Jefferson'; + + console.log('\n--- 2. Creando Visita Técnica en Hub (tareas.telcotronics.com) ---'); + const hubRes = await TareasHubService.crearVisita({ + ticketId: 1, + titulo: 'Visita de Prueba Automatizada - Help Desk', + descripcion: 'Verificación de conectividad de fibra óptica en cliente de prueba.', + emailTecnico: emailTecnico, + fechaVisita: new Date(), + ubicacion: 'Av. 10 de Agosto y Colón, Quito', + coordenadas: '-0.198321, -78.490123', + referencia: 'Edificio Plaza Piso 2', + prioridad: 'Alta', + checklist: [ + 'Revisión física de acometida y ONT', + 'Medición de potencia en splitter (-19dBm esperado)', + 'Prueba de velocidad LAN y firma de acta' + ] + }); + console.log('Respuesta de Hub:', hubRes); + + console.log('\n--- 3. Guardando en ticket_visitas (MariaDB) ---'); + const visitaId = await TicketVisita.create({ + company_id: 3, + ticket_id: 1, + external_id: hubRes.tareaId, + tecnico_email: emailTecnico, + tecnico_nombre: nombreTecnico, + titulo: 'Visita de Prueba Automatizada - Help Desk', + descripcion: 'Verificación de conectividad de fibra óptica en cliente de prueba.', + ubicacion: 'Av. 10 de Agosto y Colón, Quito', + fecha_visita: new Date(), + estado: 'programada', + checklist_json: [ + 'Revisión física de acometida y ONT', + 'Medición de potencia en splitter (-19dBm esperado)', + 'Prueba de velocidad LAN y firma de acta' + ], + historial_json: hubRes.tarea + }); + console.log('Visita creada en MariaDB con ID:', visitaId); + + console.log('\n--- 4. Consultando Visita creada en MariaDB ---'); + const visitaEnDb = await TicketVisita.findByTicket(3, 1); + console.log('Registro en DB:', { + id: visitaEnDb.id, + ticket_id: visitaEnDb.ticket_id, + external_id: visitaEnDb.external_id, + tecnico: visitaEnDb.tecnico_nombre, + estado: visitaEnDb.estado, + fecha_visita: visitaEnDb.fecha_visita + }); + + console.log('\n--- 5. Probando sincronización con Hub (consultarEstadoVisita) ---'); + const syncRes = await TareasHubService.consultarEstadoVisita(hubRes.tareaId); + console.log('Estado sincronizado desde Hub:', { + tareaId: syncRes.tarea._id, + estado: syncRes.tarea.estado, + checklistTotal: syncRes.tarea.checklist ? syncRes.tarea.checklist.length : 0, + progreso: syncRes.tarea.progreso_checklist + }); + + console.log('\n--- 6. Limpieza de prueba ---'); + // Limpiar en MariaDB + await db.query('DELETE FROM ticket_visitas WHERE id = ?', [visitaId]); + console.log('Registro eliminado de MariaDB'); + + // Limpiar tarea en CT 132 + try { + const http = require('http'); + await new Promise((resolve, reject) => { + const req = http.request({ + hostname: '192.168.10.160', + port: 3000, + path: `/api/external/tareas/${hubRes.tareaId}`, + method: 'DELETE', + headers: { 'x-api-key': 'sigma_crm_test_key_123' } + }, (res) => { + res.on('data', () => {}); + res.on('end', () => { + console.log(`Tarea ${hubRes.tareaId} eliminada de tareas.telcotronics.com (Status: ${res.statusCode})`); + resolve(); + }); + }); + req.on('error', reject); + req.end(); + }); + } catch (e) { + console.warn('Advertencia al eliminar tarea de prueba en Hub:', e.message); + } + + console.log('\n✅ ¡FLUJO COMPLETO DE VISITAS TÉCNICAS (FSM) VERIFICADO SATISFACTORIAMENTE!'); + process.exit(0); +} + +run().catch(err => { + console.error('❌ Error durante la prueba:', err); + process.exit(1); +}); diff --git a/views/agent/view-ticket.ejs b/views/agent/view-ticket.ejs index 8a8afe1..3f0a51c 100644 --- a/views/agent/view-ticket.ejs +++ b/views/agent/view-ticket.ejs @@ -58,6 +58,144 @@ <% } %> + + +
+
+
Visita Técnica (FSM)
+ <% if (visita) { %> + + <%= visita.estado %> + + <% } %> +
+
+ <% if (!visita) { %> +

Si este ticket requiere intervención presencial en sitio o avería física, puedes programar la visita técnica directamente en el sistema central de tareas.

+ + <% } else { %> + +
+ +
+
+ 1 + 📅 Programada +
+
+
Técnico: <%= visita.tecnico_nombre %>
+
<%= visita.tecnico_email %>
+
<%= visita.fecha_visita ? new Date(visita.fecha_visita).toLocaleString('es-EC') : 'Sin fecha' %>
+ <% if (visita.ubicacion) { %> +
<%= visita.ubicacion %>
+ <% } %> +
+
+ + +
+
+ 2 + 📍 Check-in en Sitio +
+
+ <% if (visita.historial && visita.historial.checkIn && visita.historial.checkIn.fecha) { %> +
Arribo confirmado
+
<%= new Date(visita.historial.checkIn.fecha).toLocaleTimeString('es-EC') %>
+ <% if (visita.historial.checkIn.latitud) { %> +
GPS: <%= visita.historial.checkIn.latitud %>, <%= visita.historial.checkIn.longitud %>
+ <% } %> + <% } else { %> + Pendiente de validación GPS + <% } %> +
+
+ + +
+
+ 3 + 📸 Avance y Evidencias +
+
+ <% if (visita.historial && visita.historial.progreso_checklist) { %> +
+ Checklist: + <%= visita.historial.progreso_checklist.completados %>/<%= visita.historial.progreso_checklist.total %> +
+
+
+
+ <% if (visita.historial.progreso_checklist.items && visita.historial.progreso_checklist.items.length > 0) { %> +
    + <% visita.historial.progreso_checklist.items.forEach(item => { %> +
  • + <%= item.texto %> +
  • + <% }) %> +
+ <% } %> + <% } else { %> +
Checklist en proceso
+ <% } %> + + + <% if (visita.historial && visita.historial.evidencias && visita.historial.evidencias.length > 0) { %> +
+
Fotos de campo:
+
+ <% visita.historial.evidencias.forEach(foto => { %> + + Evidencia + + <% }) %> +
+
+ <% } %> + + + <% if (visita.historial && visita.historial.firma_cliente && visita.historial.firma_cliente.firmado) { %> +
+ Firmado: <%= visita.historial.firma_cliente.nombreFirmante %> +
+ <% } else { %> + Pendiente firma cliente + <% } %> +
+
+ + +
+
+ 4 + 🏁 Finalización +
+
+ <% if (visita.estado === 'completada') { %> +
Visita Finalizada
+ <% } else { %> +
En gestión por personal de campo
+ <% } %> + +
+ +
+
+
+
+ + + <% } %> +
+
@@ -81,7 +219,9 @@ <%= r.usuario_rol %> <%= r.created_at %>
-

<%= r.mensaje %>

+
+

<%= r.mensaje %>

+
<% }) %> @@ -99,4 +239,105 @@ + + + + + <%- include('../partials/footer') %>