feat: inicializacion de Help Desk segun estandar Telcotronics con bitacora de errores e integracion MCP

This commit is contained in:
2026-10-16 23:29:32 -05:00
commit 0f2708462e
54 changed files with 7590 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const { isCompanyAdmin } = require('../middleware/auth');
const User = require('../models/User');
const Department = require('../models/Department');
const Ticket = require('../models/Ticket');
router.get('/dashboard', isCompanyAdmin, async (req, res) => {
try {
const users = await User.findAll(req.session.user.company_id);
const tickets = await Ticket.findAll(req.session.user.company_id);
const departments = await Department.findAll(req.session.user.company_id);
const stats = await Ticket.countByStatus(req.session.user.company_id);
res.render('admin/dashboard', { users, tickets, departments, stats });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cargar dashboard');
res.redirect('/');
}
});
router.get('/users', isCompanyAdmin, async (req, res) => {
try {
const users = await User.findAll(req.session.user.company_id);
res.render('admin/users', { users });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cargar usuarios');
res.redirect('/admin/dashboard');
}
});
router.get('/users/create', isCompanyAdmin, (req, res) => {
res.render('admin/create-user');
});
router.post('/users/create', isCompanyAdmin, async (req, res) => {
const { nombre, email, password, rol } = req.body;
if (!nombre || !email || !password) {
req.flash('error_msg', 'Todos los campos son obligatorios');
return res.redirect('/admin/users/create');
}
try {
const existing = await User.findByEmail(email);
if (existing) {
req.flash('error_msg', 'El email ya existe');
return res.redirect('/admin/users/create');
}
const hashedPassword = await bcrypt.hash(password, 10);
const userId = await User.create(req.session.user.company_id, nombre, email, hashedPassword, rol || 'client');
req.flash('success_msg', 'Usuario creado exitosamente');
res.redirect('/admin/users');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al crear usuario');
res.redirect('/admin/users/create');
}
});
router.post('/users/invite/:id', isCompanyAdmin, async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user || user.company_id !== req.session.user.company_id) {
req.flash('error_msg', 'Usuario no encontrado');
return res.redirect('/admin/users');
}
const token = await User.generateInviteToken(user.id);
const inviteLink = `${req.protocol}://${req.get('host')}/invite/${token}`;
req.flash('success_msg', `Link de invitación: ${inviteLink}`);
res.redirect('/admin/users');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al generar invitación');
res.redirect('/admin/users');
}
});
router.post('/users/delete/:id', isCompanyAdmin, async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user || user.company_id !== req.session.user.company_id) {
req.flash('error_msg', 'Usuario no encontrado');
return res.redirect('/admin/users');
}
if (user.rol === 'superadmin') {
req.flash('error_msg', 'No puedes eliminar al superadmin');
return res.redirect('/admin/users');
}
await User.delete(req.params.id);
req.flash('success_msg', 'Usuario eliminado');
res.redirect('/admin/users');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al eliminar usuario');
res.redirect('/admin/users');
}
});
router.get('/departments', isCompanyAdmin, async (req, res) => {
try {
const departments = await Department.findAll(req.session.user.company_id);
res.render('admin/departments', { departments });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cargar departamentos');
res.redirect('/admin/dashboard');
}
});
router.post('/departments/create', isCompanyAdmin, async (req, res) => {
const { nombre, descripcion } = req.body;
if (!nombre) {
req.flash('error_msg', 'El nombre es obligatorio');
return res.redirect('/admin/departments');
}
try {
await Department.create(req.session.user.company_id, nombre, descripcion || '');
req.flash('success_msg', 'Departamento creado');
res.redirect('/admin/departments');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al crear departamento');
res.redirect('/admin/departments');
}
});
router.post('/departments/update/:id', isCompanyAdmin, async (req, res) => {
const { nombre, descripcion } = req.body;
try {
await Department.update(req.params.id, nombre, descripcion || '');
req.flash('success_msg', 'Departamento actualizado');
res.redirect('/admin/departments');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al actualizar departamento');
res.redirect('/admin/departments');
}
});
router.post('/departments/delete/:id', isCompanyAdmin, async (req, res) => {
try {
await Department.delete(req.params.id);
req.flash('success_msg', 'Departamento eliminado');
res.redirect('/admin/departments');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al eliminar departamento');
res.redirect('/admin/departments');
}
});
module.exports = router;
+106
View File
@@ -0,0 +1,106 @@
const express = require('express');
const router = express.Router();
const { isAgent } = require('../middleware/auth');
const Ticket = require('../models/Ticket');
const Department = require('../models/Department');
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');
}
res.render('agent/view-ticket', { ticket, respuestas, agents });
} 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}`);
}
});
module.exports = router;
+157
View File
@@ -0,0 +1,157 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const User = require('../models/User');
const Company = require('../models/Company');
const Department = require('../models/Department');
router.get('/login', (req, res) => {
if (req.session.user) return res.redirect('/');
res.render('auth/login');
});
router.post('/login', async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.findByEmail(email);
if (!user) {
req.flash('error_msg', 'Email o contraseña incorrectos');
return res.redirect('/login');
}
if (!user.is_active) {
req.flash('error_msg', 'Cuenta desactivada');
return res.redirect('/login');
}
const company = await Company.findById(user.company_id);
if (company && !company.is_active) {
req.flash('error_msg', 'Tu empresa está desactivada');
return res.redirect('/login');
}
const match = await bcrypt.compare(password, user.password);
if (!match) {
req.flash('error_msg', 'Email o contraseña incorrectos');
return res.redirect('/login');
}
req.session.user = {
id: user.id,
company_id: user.company_id,
nombre: user.nombre,
email: user.email,
rol: user.rol
};
req.flash('success_msg', 'Inicio de sesión exitoso');
res.redirect('/');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al iniciar sesión');
res.redirect('/login');
}
});
router.get('/register', (req, res) => {
if (req.session.user) return res.redirect('/');
res.render('auth/register');
});
router.post('/register', async (req, res) => {
const { nombre, email, password, confirm_password, company_name } = req.body;
if (!nombre || !email || !password || !confirm_password) {
req.flash('error_msg', 'Todos los campos son obligatorios');
return res.redirect('/register');
}
if (password !== confirm_password) {
req.flash('error_msg', 'Las contraseñas no coinciden');
return res.redirect('/register');
}
if (password.length < 6) {
req.flash('error_msg', 'La contraseña debe tener al menos 6 caracteres');
return res.redirect('/register');
}
try {
const existing = await User.findByEmail(email);
if (existing) {
req.flash('error_msg', 'El email ya está registrado');
return res.redirect('/register');
}
// Create company or use default
let companyId = 1;
if (company_name) {
const slug = company_name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').slice(0, 100);
const slugExists = await Company.findBySlug(slug);
if (slugExists) {
req.flash('error_msg', 'El nombre de empresa ya está registrado');
return res.redirect('/register');
}
companyId = await Company.create(company_name, slug, 'free');
// Seed default departments for new company
await Department.create(companyId, 'Soporte Técnico', 'Problemas técnicos y de software');
await Department.create(companyId, 'Facturación', 'Problemas de facturación y pagos');
await Department.create(companyId, 'General', 'Consultas generales');
}
const hashedPassword = await bcrypt.hash(password, 10);
await User.create(companyId, nombre, email, hashedPassword, company_name ? 'admin' : 'client');
req.flash('success_msg', 'Registro exitoso, ahora puedes iniciar sesión');
res.redirect('/login');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al registrarse');
res.redirect('/register');
}
});
router.get('/invite/:token', async (req, res) => {
try {
const invite = await User.findByInviteToken(req.params.token);
if (!invite) {
req.flash('error_msg', 'Invitación inválida o expirada');
return res.redirect('/login');
}
res.render('auth/accept-invite', { token: req.params.token, email: invite.email });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al procesar invitación');
res.redirect('/login');
}
});
router.post('/invite/:token', async (req, res) => {
const { nombre, password, confirm_password } = req.body;
if (!nombre || !password || !confirm_password) {
req.flash('error_msg', 'Todos los campos son obligatorios');
return res.redirect(`/invite/${req.params.token}`);
}
if (password !== confirm_password) {
req.flash('error_msg', 'Las contraseñas no coinciden');
return res.redirect(`/invite/${req.params.token}`);
}
try {
const invite = await User.findByInviteToken(req.params.token);
if (!invite) {
req.flash('error_msg', 'Invitación inválida o expirada');
return res.redirect('/login');
}
const hashedPassword = await bcrypt.hash(password, 10);
await User.update(invite.id, {
nombre,
password: hashedPassword,
is_active: 1,
invite_token: null,
invite_token_expires: null
});
req.flash('success_msg', 'Cuenta activada, ahora puedes iniciar sesión');
res.redirect('/login');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al aceptar invitación');
res.redirect(`/invite/${req.params.token}`);
}
});
router.get('/logout', (req, res) => {
req.session.destroy();
res.redirect('/login');
});
module.exports = router;
+104
View File
@@ -0,0 +1,104 @@
const express = require('express');
const router = express.Router();
const { isAuthenticated } = require('../middleware/auth');
const Ticket = require('../models/Ticket');
const Department = require('../models/Department');
router.get('/dashboard', isAuthenticated, async (req, res) => {
try {
const tickets = await Ticket.findByUser(req.session.user.company_id, req.session.user.id);
res.render('client/dashboard', { tickets });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cargar tickets');
res.redirect('/');
}
});
router.get('/new-ticket', isAuthenticated, async (req, res) => {
try {
const departments = await Department.findAll(req.session.user.company_id);
res.render('client/new-ticket', { departments });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cargar formulario');
res.redirect('/client/dashboard');
}
});
router.post('/new-ticket', isAuthenticated, async (req, res) => {
const { department_id, asunto, mensaje, prioridad } = req.body;
if (!asunto || !mensaje) {
req.flash('error_msg', 'Asunto y mensaje son obligatorios');
return res.redirect('/client/new-ticket');
}
try {
await Ticket.create(req.session.user.company_id, req.session.user.id, department_id || null, asunto, mensaje, prioridad || 'media');
req.flash('success_msg', 'Ticket creado exitosamente');
res.redirect('/client/dashboard');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al crear ticket');
res.redirect('/client/new-ticket');
}
});
router.get('/ticket/:id', isAuthenticated, async (req, res) => {
try {
const ticket = await Ticket.findById(req.params.id);
if (!ticket) {
req.flash('error_msg', 'Ticket no encontrado');
return res.redirect('/client/dashboard');
}
if (ticket.company_id !== req.session.user.company_id) {
req.flash('error_msg', 'Acceso denegado');
return res.redirect('/client/dashboard');
}
if (ticket.user_id !== req.session.user.id && req.session.user.rol === 'client') {
req.flash('error_msg', 'Acceso denegado');
return res.redirect('/client/dashboard');
}
const respuestas = await Ticket.getResponses(req.params.id);
res.render('client/view-ticket', { ticket, respuestas });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cargar ticket');
res.redirect('/client/dashboard');
}
});
router.post('/ticket/:id/respond', isAuthenticated, async (req, res) => {
const { mensaje } = req.body;
if (!mensaje) {
req.flash('error_msg', 'El mensaje es obligatorio');
return res.redirect(`/client/ticket/${req.params.id}`);
}
try {
await Ticket.addResponse(req.params.id, req.session.user.id, mensaje);
req.flash('success_msg', 'Respuesta enviada');
res.redirect(`/client/ticket/${req.params.id}`);
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al enviar respuesta');
res.redirect(`/client/ticket/${req.params.id}`);
}
});
router.post('/ticket/:id/close', isAuthenticated, async (req, res) => {
try {
const ticket = await Ticket.findById(req.params.id);
if (ticket && ticket.company_id !== req.session.user.company_id) {
req.flash('error_msg', 'Acceso denegado');
return res.redirect('/client/dashboard');
}
await Ticket.update(req.params.id, { status: 'cerrado' });
req.flash('success_msg', 'Ticket cerrado');
res.redirect(`/client/ticket/${req.params.id}`);
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cerrar ticket');
res.redirect(`/client/ticket/${req.params.id}`);
}
});
module.exports = router;
+35
View File
@@ -0,0 +1,35 @@
const express = require('express');
const router = express.Router();
const { isAuthenticated } = require('../middleware/auth');
const Company = require('../models/Company');
const User = require('../models/User');
router.get('/', isAuthenticated, async (req, res) => {
try {
const company = await Company.findById(req.session.user.company_id);
const userCount = await Company.countUsers(company.id);
const ticketCount = await Company.countTickets(company.id);
res.render('company/settings', { company, userCount, ticketCount });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cargar configuración');
res.redirect('/');
}
});
router.post('/update', isAuthenticated, async (req, res) => {
const { name } = req.body;
try {
if (name) {
await Company.update(req.session.user.company_id, { name });
}
req.flash('success_msg', 'Empresa actualizada');
res.redirect('/company');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al actualizar');
res.redirect('/company');
}
});
module.exports = router;
+72
View File
@@ -0,0 +1,72 @@
const express = require('express');
const router = express.Router();
const { isSuperAdmin } = require('../middleware/auth');
const Company = require('../models/Company');
const User = require('../models/User');
const Ticket = require('../models/Ticket');
router.get('/companies', isSuperAdmin, async (req, res) => {
try {
const companies = await Company.findAll();
for (const c of companies) {
c.userCount = await Company.countUsers(c.id);
c.ticketCount = await Company.countTickets(c.id);
}
res.render('saas/companies', { companies });
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al cargar empresas');
res.redirect('/');
}
});
router.post('/companies/create', isSuperAdmin, async (req, res) => {
const { name, slug, plan } = req.body;
if (!name || !slug) {
req.flash('error_msg', 'Nombre y slug son obligatorios');
return res.redirect('/saas/companies');
}
try {
const existing = await Company.findBySlug(slug);
if (existing) {
req.flash('error_msg', 'El slug ya existe');
return res.redirect('/saas/companies');
}
const companyId = await Company.create(name, slug, plan || 'free');
const Department = require('../models/Department');
await Department.create(companyId, 'Soporte Técnico', 'Problemas técnicos');
await Department.create(companyId, 'General', 'Consultas generales');
const bcrypt = require('bcryptjs');
const hash = await bcrypt.hash('123456', 10);
await User.create(companyId, 'Admin', `admin@${slug}.com`, hash, 'admin');
req.flash('success_msg', `Empresa ${name} creada`);
res.redirect('/saas/companies');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al crear empresa');
res.redirect('/saas/companies');
}
});
router.post('/companies/update/:id', isSuperAdmin, async (req, res) => {
const { name, plan, max_users, max_tickets, is_active } = req.body;
try {
const data = {};
if (name) data.name = name;
if (plan) data.plan = plan;
if (max_users) data.max_users = parseInt(max_users);
if (max_tickets) data.max_tickets = parseInt(max_tickets);
data.is_active = is_active ? 1 : 0;
await Company.update(req.params.id, data);
req.flash('success_msg', 'Empresa actualizada');
res.redirect('/saas/companies');
} catch (error) {
console.error(error);
req.flash('error_msg', 'Error al actualizar empresa');
res.redirect('/saas/companies');
}
});
module.exports = router;