feat(fsm): implementar gestion y asignacion de sets de herramientas a tecnicos

This commit is contained in:
2026-10-31 17:07:30 -05:00
parent 06ef042538
commit c07162c375
13 changed files with 1793 additions and 9 deletions
+214
View File
@@ -5,6 +5,9 @@ const { isCompanyAdmin } = require('../middleware/auth');
const User = require('../models/User');
const Department = require('../models/Department');
const Ticket = require('../models/Ticket');
const Herramienta = require('../models/Herramienta');
const HerramientaSet = require('../models/HerramientaSet');
const HerramientaAsignacion = require('../models/HerramientaAsignacion');
router.get('/dashboard', isCompanyAdmin, async (req, res) => {
try {
@@ -150,4 +153,215 @@ router.post('/departments/delete/:id', isCompanyAdmin, async (req, res) => {
}
});
// ==========================================
// GESTIÓN DE HERRAMIENTAS Y SETS / KITS FSM
// ==========================================
router.get('/herramientas', isCompanyAdmin, async (req, res) => {
try {
const companyId = req.session.user.company_id;
const [herramientas, sets, asignaciones, stats, allUsers] = await Promise.all([
Herramienta.findAll(companyId),
HerramientaSet.findAll(companyId),
HerramientaAsignacion.findAll(companyId),
Herramienta.countByEstado(companyId),
User.findAll(companyId)
]);
const tecnicos = allUsers.filter(u => u.rol === 'agent' || u.rol === 'admin');
res.render('admin/herramientas', {
title: 'Herramientas & Sets',
herramientas,
sets,
asignaciones,
stats,
tecnicos
});
} catch (error) {
console.error('Error al cargar herramientas:', error);
req.flash('error_msg', 'Error al cargar gestión de herramientas');
res.redirect('/admin/dashboard');
}
});
// Crear Herramienta Individual
router.post('/herramientas/create', isCompanyAdmin, async (req, res) => {
const { nombre, codigo, numero_serie, categoria, marca, modelo, estado, ubicacion_bodega, notas } = req.body;
if (!nombre) {
req.flash('error_msg', 'El nombre de la herramienta es obligatorio');
return res.redirect('/admin/herramientas');
}
try {
await Herramienta.create({
company_id: req.session.user.company_id,
nombre,
codigo: codigo || null,
numero_serie: numero_serie || null,
categoria: categoria || 'general',
marca: marca || null,
modelo: modelo || null,
estado: estado || 'operativo',
ubicacion_bodega: ubicacion_bodega || null,
notas: notas || null
});
req.flash('success_msg', `Herramienta "${nombre}" registrada correctamente`);
res.redirect('/admin/herramientas#tab-herramientas');
} catch (error) {
console.error('Error al crear herramienta:', error);
req.flash('error_msg', 'Error al registrar la herramienta: ' + error.message);
res.redirect('/admin/herramientas');
}
});
// Editar Herramienta Individual
router.post('/herramientas/edit/:id', isCompanyAdmin, async (req, res) => {
const { nombre, codigo, numero_serie, categoria, marca, modelo, estado, ubicacion_bodega, notas } = req.body;
try {
await Herramienta.update(req.params.id, req.session.user.company_id, {
nombre,
codigo,
numero_serie,
categoria,
marca,
modelo,
estado,
ubicacion_bodega,
notas
});
req.flash('success_msg', 'Herramienta actualizada exitosamente');
res.redirect('/admin/herramientas#tab-herramientas');
} catch (error) {
console.error('Error al actualizar herramienta:', error);
req.flash('error_msg', 'Error al actualizar herramienta: ' + error.message);
res.redirect('/admin/herramientas');
}
});
// Eliminar Herramienta Individual
router.post('/herramientas/delete/:id', isCompanyAdmin, async (req, res) => {
try {
await Herramienta.delete(req.params.id, req.session.user.company_id);
req.flash('success_msg', 'Herramienta eliminada');
res.redirect('/admin/herramientas#tab-herramientas');
} catch (error) {
console.error('Error al eliminar herramienta:', error);
req.flash('error_msg', 'Error al eliminar herramienta');
res.redirect('/admin/herramientas');
}
});
// Crear Set / Kit de Herramientas
router.post('/herramientas/sets/create', isCompanyAdmin, async (req, res) => {
const { nombre, codigo, descripcion, categoria, herramientas_ids } = req.body;
if (!nombre) {
req.flash('error_msg', 'El nombre del Set / Kit es obligatorio');
return res.redirect('/admin/herramientas');
}
try {
let items = [];
if (Array.isArray(herramientas_ids)) {
items = herramientas_ids;
} else if (herramientas_ids) {
items = [herramientas_ids];
}
await HerramientaSet.create({
company_id: req.session.user.company_id,
nombre,
codigo: codigo || null,
descripcion: descripcion || null,
categoria: categoria || 'general'
}, items);
req.flash('success_msg', `Kit de herramientas "${nombre}" creado con ${items.length} instrumentos.`);
res.redirect('/admin/herramientas#tab-sets');
} catch (error) {
console.error('Error al crear set de herramientas:', error);
req.flash('error_msg', 'Error al crear set: ' + error.message);
res.redirect('/admin/herramientas');
}
});
// Editar Set / Kit de Herramientas
router.post('/herramientas/sets/edit/:id', isCompanyAdmin, async (req, res) => {
const { nombre, codigo, descripcion, categoria, is_active, herramientas_ids } = req.body;
try {
let items = null;
if (herramientas_ids !== undefined) {
items = Array.isArray(herramientas_ids) ? herramientas_ids : (herramientas_ids ? [herramientas_ids] : []);
}
await HerramientaSet.update(req.params.id, req.session.user.company_id, {
nombre,
codigo,
descripcion,
categoria,
is_active: is_active === '1' || is_active === 1 ? 1 : 0
}, items);
req.flash('success_msg', 'Set / Kit actualizado exitosamente');
res.redirect('/admin/herramientas#tab-sets');
} catch (error) {
console.error('Error al actualizar set:', error);
req.flash('error_msg', 'Error al actualizar set: ' + error.message);
res.redirect('/admin/herramientas');
}
});
// Eliminar Set / Kit de Herramientas
router.post('/herramientas/sets/delete/:id', isCompanyAdmin, async (req, res) => {
try {
await HerramientaSet.delete(req.params.id, req.session.user.company_id);
req.flash('success_msg', 'Set / Kit eliminado');
res.redirect('/admin/herramientas#tab-sets');
} catch (error) {
console.error('Error al eliminar set:', error);
req.flash('error_msg', 'Error al eliminar set');
res.redirect('/admin/herramientas');
}
});
// Asignar Set de Herramientas a Técnico (Dotación Fija)
router.post('/herramientas/asignar', isCompanyAdmin, async (req, res) => {
const { tecnico_id, set_id, herramienta_id, tipo_asignacion, observaciones_entrega } = req.body;
if (!tecnico_id || (!set_id && !herramienta_id)) {
req.flash('error_msg', 'Debe seleccionar un técnico y al menos un set o herramienta');
return res.redirect('/admin/herramientas');
}
try {
await HerramientaAsignacion.create({
company_id: req.session.user.company_id,
tecnico_id: parseInt(tecnico_id),
set_id: set_id ? parseInt(set_id) : null,
herramienta_id: herramienta_id ? parseInt(herramienta_id) : null,
tipo_asignacion: tipo_asignacion || 'dotacion_fija',
observaciones_entrega: observaciones_entrega || 'Asignación administrativa de equipamiento',
asignado_por: req.session.user.id
});
req.flash('success_msg', 'Equipamiento asignado al técnico exitosamente');
res.redirect('/admin/herramientas#tab-asignaciones');
} catch (error) {
console.error('Error al asignar herramientas:', error);
req.flash('error_msg', 'Error al asignar equipamiento: ' + error.message);
res.redirect('/admin/herramientas');
}
});
// Registrar Devolución de Herramientas
router.post('/herramientas/devolver/:id', isCompanyAdmin, async (req, res) => {
const { observaciones_devolucion, estado } = req.body;
try {
await HerramientaAsignacion.devolver(req.params.id, req.session.user.company_id, {
observaciones_devolucion: observaciones_devolucion || 'Devuelto conforme a bodega',
estado: estado || 'devuelto'
});
req.flash('success_msg', 'Devolución de herramientas registrada correctamente');
res.redirect('/admin/herramientas#tab-asignaciones');
} catch (error) {
console.error('Error al registrar devolución:', error);
req.flash('error_msg', 'Error al registrar devolución');
res.redirect('/admin/herramientas');
}
});
module.exports = router;
+64 -5
View File
@@ -4,6 +4,9 @@ const { isAgent } = require('../middleware/auth');
const Ticket = require('../models/Ticket');
const Department = require('../models/Department');
const TicketVisita = require('../models/TicketVisita');
const HerramientaSet = require('../models/HerramientaSet');
const HerramientaAsignacion = require('../models/HerramientaAsignacion');
const User = require('../models/User');
const TareasHubService = require('../config/tareasHubService');
router.get('/dashboard', isAgent, async (req, res) => {
@@ -55,8 +58,9 @@ router.get('/ticket/:id', isAgent, async (req, res) => {
}
const tecnicosHub = await TareasHubService.obtenerTecnicos();
const setsHerramientas = await HerramientaSet.findAll(ticket.company_id);
res.render('agent/view-ticket', { ticket, respuestas, agents, visita, tecnicosHub });
res.render('agent/view-ticket', { ticket, respuestas, agents, visita, tecnicosHub, setsHerramientas });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cargar ticket');
@@ -139,7 +143,8 @@ router.post('/ticket/:id/visita', isAgent, async (req, res) => {
fechaVisita,
fechaFin,
prioridad,
checklist_items
checklist_items,
setHerramientasId
} = req.body;
if (!emailTecnico) {
@@ -147,6 +152,12 @@ router.post('/ticket/:id/visita', isAgent, async (req, res) => {
return res.redirect(`/agent/ticket/${req.params.id}`);
}
// Buscar información del set de herramientas si se seleccionó uno
let selectedSet = null;
if (setHerramientasId) {
selectedSet = await HerramientaSet.findById(setHerramientasId, ticket.company_id);
}
// Procesar checklist
let checklistArr = [];
if (typeof checklist_items === 'string') {
@@ -182,7 +193,7 @@ router.post('/ticket/:id/visita', isAgent, async (req, res) => {
});
// 2. Guardar en MariaDB ticket_visitas
await TicketVisita.create({
const visitaId = await TicketVisita.create({
company_id: ticket.company_id,
ticket_id: ticket.id,
external_id: hubRes.tareaId,
@@ -195,15 +206,39 @@ router.post('/ticket/:id/visita', isAgent, async (req, res) => {
fecha_fin: fechaFin ? new Date(fechaFin) : null,
estado: 'programada',
checklist_json: checklistArr,
historial_json: hubRes.tarea || null
historial_json: hubRes.tarea || null,
set_herramientas_id: selectedSet ? selectedSet.id : null,
set_herramientas_nombre: selectedSet ? selectedSet.nombre : null
});
// 2.1 Vincular asignación de set de herramientas a nivel FSM
if (selectedSet) {
try {
let tecUser = await User.findByEmail(emailTecnico);
if (!tecUser) {
tecUser = req.session.user;
}
await HerramientaAsignacion.create({
company_id: ticket.company_id,
tecnico_id: tecUser.id,
set_id: selectedSet.id,
ticket_visita_id: visitaId,
tipo_asignacion: 'visita_fsm',
observaciones_entrega: `Despachado con ${selectedSet.nombre} para visita FSM (Ticket #${ticket.id})`,
asignado_por: req.session.user.id
});
} catch (errAsign) {
console.warn('Advertencia registrando asignación FSM de herramientas:', errAsign.message);
}
}
// 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}\``;
const kitInfoStr = selectedSet ? `\n🧰 **Set de Herramientas asignado:** ${selectedSet.nombre} (${selectedSet.codigo || 'Kit FSM'})` : '';
const msgHilo = `📅 **Visita técnica programada**: Se ha asignado al técnico **${tecnicoNombre || emailTecnico}** para el **${fechaStr}**.${kitInfoStr}\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');
@@ -271,4 +306,28 @@ router.post('/ticket/:id/visita/sync', isAgent, async (req, res) => {
}
});
// Panel de "Mis Herramientas & Sets Asignados" para el Técnico / Agente
router.get('/herramientas', isAgent, async (req, res) => {
try {
const companyId = req.session.user.company_id;
const tecnicoId = req.session.user.id;
const asignaciones = await HerramientaAsignacion.findByTecnico(companyId, tecnicoId);
// Agrupar asignaciones activas e historial
const activas = asignaciones.filter(a => a.estado === 'asignado');
const historial = asignaciones.filter(a => a.estado !== 'asignado');
res.render('agent/herramientas', {
title: 'Mis Herramientas',
activas,
historial
});
} catch (error) {
console.error('Error al cargar herramientas del técnico:', error);
req.flash('error_msg', 'Error al consultar herramientas asignadas');
res.redirect('/agent/dashboard');
}
});
module.exports = router;
+39
View File
@@ -4,6 +4,7 @@ const Ticket = require('../models/Ticket');
const Company = require('../models/Company');
const Department = require('../models/Department');
const User = require('../models/User');
const HerramientaSet = require('../models/HerramientaSet');
/**
* Middleware para validar llamadas M2M mediante API Key o Token de Agente
@@ -351,4 +352,42 @@ async function handleAddNote(req, res) {
router.post('/external/tickets/:id/notas', handleAddNote);
router.post('/tickets/:id/respuestas', handleAddNote);
/**
* GET /api/external/herramientas/sets
* Consulta los sets y kits de herramientas configurados por empresa
*/
router.get('/external/herramientas/sets', async (req, res) => {
try {
const companyId = parseInt(req.query.empresa_id || req.query.company_id || 1);
const sets = await HerramientaSet.findAll(companyId);
res.json({
ok: true,
success: true,
company_id: companyId,
sets: sets.map(s => ({
id: s.id,
nombre: s.nombre,
codigo: s.codigo,
categoria: s.categoria,
descripcion: s.descripcion,
total_herramientas: s.total_herramientas,
herramientas: s.herramientas ? s.herramientas.map(h => ({
id: h.id,
nombre: h.nombre,
codigo: h.codigo,
numero_serie: h.numero_serie,
marca: h.marca,
modelo: h.modelo,
estado: h.estado
})) : []
}))
});
} catch (err) {
res.status(500).json({ ok: false, success: false, error: err.message });
}
});
router.get('/herramientas/sets', async (req, res) => {
return res.redirect(307, '/api/external/herramientas/sets');
});
module.exports = router;