feat(fsm): modulo de asignacion de tareas y visitas tecnicas integrado con tareas.telcotronics.com

This commit is contained in:
2026-10-17 03:06:16 -05:00
parent 0f2708462e
commit 3afcff699a
9 changed files with 802 additions and 8 deletions
+169 -1
View File
@@ -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;