import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import * as usuariosService from '../services/usuarios.service.js'; import { AppError } from '../middlewares/error.middleware.js'; import { isGlobalAdmin as checkGlobalAdmin } from '../utils/global-admin.js'; import { prisma } from '../config/database.js'; const inviteSchema = z.object({ email: z.string().email('email inválido'), nombre: z.string().min(2).max(100), // Legacy Horux360 roles + Despacho-specific roles role: z.enum(['contador', 'visor', 'auxiliar', 'supervisor', 'cliente']), supervisorUserId: z.string().uuid().optional(), // Required when role=auxiliar }); const updateSchema = z.object({ nombre: z.string().min(2).max(100).optional(), // Legacy Horux360 roles + Despacho-specific roles role: z.enum(['contador', 'visor', 'auxiliar', 'supervisor', 'cliente']).optional(), active: z.boolean().optional(), }); const updateGlobalSchema = z.object({ nombre: z.string().min(2).max(100).optional(), role: z.enum(['owner', 'cfo', 'contador', 'visor', 'auxiliar', 'supervisor', 'cliente']).optional(), active: z.boolean().optional(), tenantId: z.string().uuid().optional(), }); const createGlobalSchema = z.object({ email: z.string().email('email inválido'), nombre: z.string().min(2).max(100), role: z.enum(['contador', 'visor', 'auxiliar', 'supervisor', 'cliente']), tenantId: z.string().uuid('tenantId inválido'), supervisorUserId: z.string().uuid().optional(), }); async function isGlobalAdmin(req: Request): Promise { return checkGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId); } export async function getUsuarios(req: Request, res: Response, next: NextFunction) { try { const usuarios = await usuariosService.getUsuarios(req.user!.tenantId); res.json(usuarios); } catch (error) { next(error); } } /** * Obtiene todos los usuarios de todas las empresas (solo admin global) */ export async function getAllUsuarios(req: Request, res: Response, next: NextFunction) { try { if (!(await isGlobalAdmin(req))) { throw new AppError(403, 'Solo el administrador global puede ver todos los usuarios'); } const usuarios = await usuariosService.getAllUsuarios(); res.json(usuarios); } catch (error) { next(error); } } export async function inviteUsuario(req: Request, res: Response, next: NextFunction) { try { if (req.user!.role !== 'owner') { throw new AppError(403, 'Solo los dueños pueden invitar usuarios'); } const data = inviteSchema.parse(req.body); // Validate: auxiliar requires a supervisor if (data.role === 'auxiliar' && !data.supervisorUserId) { throw new AppError(400, 'Debes asignar un supervisor al auxiliar'); } const usuario = await usuariosService.inviteUsuario(req.user!.tenantId, data); // Store auxiliar→supervisor relationship in tenant DB if (data.role === 'auxiliar' && data.supervisorUserId && req.tenantPool) { await req.tenantPool.query( `INSERT INTO auxiliar_supervisores (auxiliar_user_id, supervisor_user_id) VALUES ($1, $2) ON CONFLICT (auxiliar_user_id) DO UPDATE SET supervisor_user_id = $2`, [usuario.id, data.supervisorUserId], ); } res.status(201).json(usuario); } catch (error) { if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message)); next(error); } } export async function updateUsuario(req: Request, res: Response, next: NextFunction) { try { if (req.user!.role !== 'owner') { throw new AppError(403, 'Solo los dueños pueden modificar usuarios'); } const userId = req.params.id as string; const data = updateSchema.parse(req.body); const usuario = await usuariosService.updateUsuario(req.user!.tenantId, userId, data); res.json(usuario); } catch (error) { if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message)); next(error); } } /** * Lee el supervisor actualmente asignado a un auxiliar. Resuelve desde 3 * fuentes (en orden de prioridad): * 1. `auxiliar_supervisores` (override explícito del owner desde /usuarios). * 2. Cartera donde el user es `auxiliar_user_id` y la misma tiene supervisor. * 3. Subcartera donde el user es `auxiliar_user_id`; el supervisor viene * del cartera padre. * * Devuelve `null` si no aparece en ninguna. */ export async function getSupervisor(req: Request, res: Response, next: NextFunction) { try { if (!req.tenantPool) throw new AppError(500, 'Tenant pool no disponible'); const userId = String(req.params.id); const { rows } = await req.tenantPool.query<{ supervisor_user_id: string }>( `SELECT supervisor_user_id FROM ( SELECT supervisor_user_id, 1 AS prio FROM auxiliar_supervisores WHERE auxiliar_user_id = $1 UNION ALL SELECT supervisor_user_id, 2 AS prio FROM carteras WHERE auxiliar_user_id = $1 AND supervisor_user_id IS NOT NULL UNION ALL SELECT p.supervisor_user_id, 3 AS prio FROM carteras sub JOIN carteras p ON p.id = sub.parent_id WHERE sub.auxiliar_user_id = $1 AND p.supervisor_user_id IS NOT NULL ) t WHERE supervisor_user_id IS NOT NULL ORDER BY prio LIMIT 1`, [userId], ); const supervisorUserId = rows[0]?.supervisor_user_id ?? null; let supervisorNombre: string | null = null; if (supervisorUserId) { const u = await prisma.user.findUnique({ where: { id: supervisorUserId }, select: { nombre: true }, }); supervisorNombre = u?.nombre ?? null; } res.json({ supervisorUserId, supervisorNombre }); } catch (error) { next(error); } } const supervisorSchema = z.object({ supervisorUserId: z.string().uuid().nullable(), }); /** * Asigna o elimina el supervisor de un auxiliar (BD tenant). * Solo owner/cfo. Pasar `null` borra la asignación. */ export async function updateSupervisor(req: Request, res: Response, next: NextFunction) { try { if (req.user!.role !== 'owner' && req.user!.role !== 'cfo') { throw new AppError(403, 'Solo el owner puede asignar supervisores'); } if (!req.tenantPool) throw new AppError(500, 'Tenant pool no disponible'); const userId = String(req.params.id); const { supervisorUserId } = supervisorSchema.parse(req.body); if (supervisorUserId === null) { await req.tenantPool.query( `DELETE FROM auxiliar_supervisores WHERE auxiliar_user_id = $1`, [userId], ); } else { await req.tenantPool.query( `INSERT INTO auxiliar_supervisores (auxiliar_user_id, supervisor_user_id) VALUES ($1, $2) ON CONFLICT (auxiliar_user_id) DO UPDATE SET supervisor_user_id = $2`, [userId, supervisorUserId], ); } res.json({ supervisorUserId }); } catch (error) { if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message)); next(error); } } export async function deleteUsuario(req: Request, res: Response, next: NextFunction) { try { if (req.user!.role !== 'owner') { throw new AppError(403, 'Solo los dueños pueden eliminar usuarios'); } const userId = req.params.id as string; if (userId === req.user!.userId) { throw new AppError(400, 'No puedes eliminar tu propia cuenta'); } await usuariosService.deleteUsuario(req.user!.tenantId, userId); res.status(204).send(); } catch (error) { next(error); } } /** * Crea un usuario globalmente (solo admin global) */ export async function createUsuarioGlobal(req: Request, res: Response, next: NextFunction) { try { if (!(await isGlobalAdmin(req))) { throw new AppError(403, 'Solo el administrador global puede crear usuarios'); } const data = createGlobalSchema.parse(req.body); if (data.role === 'auxiliar' && !data.supervisorUserId) { throw new AppError(400, 'Debes asignar un supervisor al auxiliar'); } const usuario = await usuariosService.createUsuarioGlobal(data.tenantId, { email: data.email, nombre: data.nombre, role: data.role, supervisorUserId: data.supervisorUserId, }); res.status(201).json(usuario); } catch (error) { if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message)); next(error); } } /** * Actualiza un usuario globalmente (puede cambiar de empresa) */ export async function updateUsuarioGlobal(req: Request, res: Response, next: NextFunction) { try { if (!(await isGlobalAdmin(req))) { throw new AppError(403, 'Solo el administrador global puede modificar usuarios globalmente'); } const userId = req.params.id as string; const data = updateGlobalSchema.parse(req.body); if (userId === req.user!.userId && data.tenantId) { throw new AppError(400, 'No puedes cambiar tu propia empresa'); } const usuario = await usuariosService.updateUsuarioGlobal(userId, data); res.json(usuario); } catch (error) { if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message)); next(error); } } /** * Elimina un usuario globalmente */ export async function deleteUsuarioGlobal(req: Request, res: Response, next: NextFunction) { try { if (!(await isGlobalAdmin(req))) { throw new AppError(403, 'Solo el administrador global puede eliminar usuarios globalmente'); } const userId = req.params.id as string; if (userId === req.user!.userId) { throw new AppError(400, 'No puedes eliminar tu propia cuenta'); } await usuariosService.deleteUsuarioGlobal(userId); res.status(204).send(); } catch (error) { next(error); } } /** * Get cliente accesos (which contribuyentes a client user can access) */ export async function getClienteAccesos(req: Request, res: Response, next: NextFunction) { try { if (req.user!.role !== 'owner') throw new AppError(403, 'No autorizado'); const userId = req.params.id as string; const { rows } = await req.tenantPool!.query( 'SELECT entidad_id AS "entidadId" FROM cliente_accesos WHERE user_id = $1', [userId], ); res.json({ data: rows.map(r => r.entidadId) }); } catch (error) { next(error); } } /** * Set cliente accesos (replace all accesos for a client user) */ export async function setClienteAccesos(req: Request, res: Response, next: NextFunction) { try { if (req.user!.role !== 'owner') throw new AppError(403, 'No autorizado'); const userId = req.params.id as string; const { entidadIds } = z.object({ entidadIds: z.array(z.string().uuid()), }).parse(req.body); // Replace all accesos await req.tenantPool!.query('DELETE FROM cliente_accesos WHERE user_id = $1', [userId]); for (const entidadId of entidadIds) { await req.tenantPool!.query( 'INSERT INTO cliente_accesos (user_id, entidad_id) VALUES ($1, $2) ON CONFLICT DO NOTHING', [userId, entidadId], ); } res.json({ data: entidadIds }); } catch (error) { if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message)); next(error); } }