Files
help_desk/routes/agent.js
T

275 lines
12 KiB
JavaScript

const express = require('express');
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 {
let tickets;
if (['superadmin', 'admin'].includes(req.session.user.rol)) {
tickets = await Ticket.findAll(req.session.user.company_id);
} else {
tickets = await Ticket.findByAgent(req.session.user.company_id, req.session.user.id);
}
const stats = await Ticket.countByStatus(req.session.user.company_id);
const departments = await Department.findAll(req.session.user.company_id);
res.render('agent/dashboard', { tickets, stats, departments });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cargar dashboard');
res.redirect('/');
}
});
router.get('/ticket/:id', isAgent, async (req, res) => {
try {
const ticket = await Ticket.findById(req.params.id);
if (!ticket) {
req.flash('error_msg', 'Ticket no encontrado');
return res.redirect('/agent/dashboard');
}
if (ticket.company_id !== req.session.user.company_id && req.session.user.rol !== 'superadmin') {
req.flash('error_msg', 'Acceso denegado');
return res.redirect('/agent/dashboard');
}
const respuestas = await Ticket.getResponses(req.params.id);
let agents = [];
if (['superadmin', 'admin'].includes(req.session.user.rol)) {
const User = require('../models/User');
const allUsers = await User.findAll(req.session.user.company_id);
agents = allUsers.filter(u => u.rol === 'agent');
}
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');
res.redirect('/agent/dashboard');
}
});
router.post('/ticket/:id/respond', isAgent, async (req, res) => {
const { mensaje } = req.body;
if (!mensaje) {
req.flash('error_msg', 'El mensaje es obligatorio');
return res.redirect(`/agent/ticket/${req.params.id}`);
}
try {
await Ticket.addResponse(req.params.id, req.session.user.id, mensaje);
req.flash('success_msg', 'Respuesta enviada');
res.redirect(`/agent/ticket/${req.params.id}`);
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al enviar respuesta');
res.redirect(`/agent/ticket/${req.params.id}`);
}
});
router.post('/ticket/:id/status', isAgent, async (req, res) => {
const { status, assigned_to } = req.body;
try {
const data = {};
if (status) data.status = status;
if (assigned_to && ['superadmin', 'admin'].includes(req.session.user.rol)) {
data.assigned_to = assigned_to;
}
await Ticket.update(req.params.id, data);
req.flash('success_msg', 'Ticket actualizado');
res.redirect(`/agent/ticket/${req.params.id}`);
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al actualizar ticket');
res.redirect(`/agent/ticket/${req.params.id}`);
}
});
router.post('/ticket/:id/assign', isAgent, async (req, res) => {
if (!['superadmin', 'admin'].includes(req.session.user.rol)) {
req.flash('error_msg', 'Acceso denegado');
return res.redirect('/agent/dashboard');
}
const { assigned_to } = req.body;
try {
await Ticket.update(req.params.id, { assigned_to: assigned_to || null });
if (assigned_to) {
await Ticket.update(req.params.id, { status: 'en_progreso' });
}
req.flash('success_msg', 'Ticket asignado');
res.redirect(`/agent/ticket/${req.params.id}`);
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al asignar ticket');
res.redirect(`/agent/ticket/${req.params.id}`);
}
});
// 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;