feat: asignaciones obligaciones/tareas + fixes backend
- Migracion 046: tablas obligacion_asignaciones y tarea_asignaciones - Servicio y controller de asignaciones (CRUD + listados) - Fix: enviar correo welcome al invitar usuario nuevo - Fix: quitar JOIN users de queries tenant (usar Prisma en BD central) - Fix: req.params.obligacionId correcto en asignaciones controller - Fix: orden rutas estaticas antes de dinamicas en cartera.routes - Fix: owner/cfo ven todas las asignaciones en getAsignacionesPorSupervisor - Fix: validar que entidad pertenezca a cartera padre en subcartera - Nuevo endpoint GET /carteras/asignaciones/sin-asignar - Nuevo endpoint GET /tareas/mis-tareas
This commit is contained in:
137
apps/api/src/controllers/asignaciones.controller.ts
Normal file
137
apps/api/src/controllers/asignaciones.controller.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import type { Request, Response, NextFunction } from 'express';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import * as asignacionesService from '../services/asignaciones.service.js';
|
||||||
|
import { getEntidadesVisibles } from '../utils/entidades-visibles.js';
|
||||||
|
import { AppError } from '../middlewares/error.middleware.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida que el auxiliar pertenezca al supervisor (o que el caller sea owner).
|
||||||
|
* Owner puede asignar a cualquier auxiliar del tenant.
|
||||||
|
*/
|
||||||
|
async function validarAuxiliarDelSupervisor(
|
||||||
|
pool: import('pg').Pool,
|
||||||
|
supervisorUserId: string,
|
||||||
|
auxiliarUserId: string,
|
||||||
|
callerRole: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (callerRole === 'owner') return;
|
||||||
|
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`SELECT 1 FROM auxiliar_supervisores
|
||||||
|
WHERE auxiliar_user_id = $1 AND supervisor_user_id = $2
|
||||||
|
LIMIT 1`,
|
||||||
|
[auxiliarUserId, supervisorUserId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new AppError(403, 'El auxiliar no pertenece a tu equipo');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Obligaciones ──
|
||||||
|
|
||||||
|
export async function asignarObligacion(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const obligacionId = String(req.params.obligacionId);
|
||||||
|
const schema = z.object({ auxiliarUserId: z.string().uuid() });
|
||||||
|
const { auxiliarUserId } = schema.parse(req.body);
|
||||||
|
|
||||||
|
await validarAuxiliarDelSupervisor(
|
||||||
|
req.tenantPool!,
|
||||||
|
req.user!.userId,
|
||||||
|
auxiliarUserId,
|
||||||
|
req.user!.role,
|
||||||
|
);
|
||||||
|
|
||||||
|
await asignacionesService.asignarObligacion(
|
||||||
|
req.tenantPool!,
|
||||||
|
obligacionId,
|
||||||
|
auxiliarUserId,
|
||||||
|
req.user!.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ message: 'Obligación asignada' });
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err instanceof z.ZodError) return next(new AppError(400, err.errors[0].message));
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function desasignarObligacion(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const obligacionId = String(req.params.obligacionId);
|
||||||
|
await asignacionesService.desasignarObligacion(req.tenantPool!, obligacionId);
|
||||||
|
res.json({ message: 'Asignación de obligación eliminada' });
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tareas ──
|
||||||
|
|
||||||
|
export async function asignarTarea(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const tareaId = String(req.params.id);
|
||||||
|
const schema = z.object({ auxiliarUserId: z.string().uuid() });
|
||||||
|
const { auxiliarUserId } = schema.parse(req.body);
|
||||||
|
|
||||||
|
await validarAuxiliarDelSupervisor(
|
||||||
|
req.tenantPool!,
|
||||||
|
req.user!.userId,
|
||||||
|
auxiliarUserId,
|
||||||
|
req.user!.role,
|
||||||
|
);
|
||||||
|
|
||||||
|
await asignacionesService.asignarTarea(
|
||||||
|
req.tenantPool!,
|
||||||
|
tareaId,
|
||||||
|
auxiliarUserId,
|
||||||
|
req.user!.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ message: 'Tarea asignada' });
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err instanceof z.ZodError) return next(new AppError(400, err.errors[0].message));
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function desasignarTarea(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const tareaId = String(req.params.id);
|
||||||
|
await asignacionesService.desasignarTarea(req.tenantPool!, tareaId);
|
||||||
|
res.json({ message: 'Asignación de tarea eliminada' });
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Listados ──
|
||||||
|
|
||||||
|
export async function listPorSupervisor(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const data = await asignacionesService.getAsignacionesPorSupervisor(
|
||||||
|
req.tenantPool!,
|
||||||
|
req.user!.userId,
|
||||||
|
req.user!.role,
|
||||||
|
);
|
||||||
|
res.json(data);
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listPorAuxiliar(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const data = await asignacionesService.getAsignacionesPorAuxiliar(
|
||||||
|
req.tenantPool!,
|
||||||
|
req.user!.userId,
|
||||||
|
);
|
||||||
|
res.json(data);
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listSinAsignar(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const entidadIds = await getEntidadesVisibles(req.tenantPool!, req.user!.userId, req.user!.role);
|
||||||
|
const [obligaciones, tareas] = await Promise.all([
|
||||||
|
asignacionesService.getObligacionesSinAsignar(req.tenantPool!, entidadIds),
|
||||||
|
asignacionesService.getTareasSinAsignar(req.tenantPool!, entidadIds),
|
||||||
|
]);
|
||||||
|
res.json({ obligaciones, tareas });
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import type { Request, Response, NextFunction } from 'express';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { AppError } from '../middlewares/error.middleware.js';
|
import { AppError } from '../middlewares/error.middleware.js';
|
||||||
import * as tareasService from '../services/tareas.service.js';
|
import * as tareasService from '../services/tareas.service.js';
|
||||||
|
import { getEntidadesVisibles } from '../utils/entidades-visibles.js';
|
||||||
import { emailService } from '../services/email/email.service.js';
|
import { emailService } from '../services/email/email.service.js';
|
||||||
import { getUserEmailById } from '../utils/memberships.js';
|
import { getUserEmailById } from '../utils/memberships.js';
|
||||||
import { env } from '../config/env.js';
|
import { env } from '../config/env.js';
|
||||||
@@ -164,6 +165,17 @@ export async function descompletarPeriodo(req: Request, res: Response, next: Nex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listMisTareas(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
rejectClienteRole(req);
|
||||||
|
const entidadIds = await getEntidadesVisibles(req.tenantPool!, req.user!.userId, req.user!.role);
|
||||||
|
const tareas = await tareasService.listTareasConPeriodoPorContribuyentes(req.tenantPool!, entidadIds);
|
||||||
|
res.json(tareas);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function seedDefaults(req: Request, res: Response, next: NextFunction) {
|
export async function seedDefaults(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
rejectClienteRole(req);
|
rejectClienteRole(req);
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS obligacion_asignaciones (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
obligacion_id uuid NOT NULL REFERENCES obligaciones_contribuyente(id) ON DELETE CASCADE,
|
||||||
|
auxiliar_user_id uuid NOT NULL,
|
||||||
|
asignado_por uuid NOT NULL,
|
||||||
|
asignado_at timestamptz DEFAULT now(),
|
||||||
|
UNIQUE (obligacion_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tarea_asignaciones (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
tarea_id uuid NOT NULL REFERENCES tareas_catalogo(id) ON DELETE CASCADE,
|
||||||
|
auxiliar_user_id uuid NOT NULL,
|
||||||
|
asignado_por uuid NOT NULL,
|
||||||
|
asignado_at timestamptz DEFAULT now(),
|
||||||
|
UNIQUE (tarea_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_obligacion_asignaciones_auxiliar ON obligacion_asignaciones(auxiliar_user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tarea_asignaciones_auxiliar ON tarea_asignaciones(auxiliar_user_id);
|
||||||
@@ -2,6 +2,7 @@ import { Router, type IRouter } from 'express';
|
|||||||
import { authenticate, authorize } from '../middlewares/auth.middleware.js';
|
import { authenticate, authorize } from '../middlewares/auth.middleware.js';
|
||||||
import { tenantMiddleware } from '../middlewares/tenant.middleware.js';
|
import { tenantMiddleware } from '../middlewares/tenant.middleware.js';
|
||||||
import * as ctrl from '../controllers/cartera.controller.js';
|
import * as ctrl from '../controllers/cartera.controller.js';
|
||||||
|
import * as asignacionesCtrl from '../controllers/asignaciones.controller.js';
|
||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
|
|
||||||
@@ -11,6 +12,11 @@ router.use(tenantMiddleware);
|
|||||||
// Static routes first
|
// Static routes first
|
||||||
router.get('/supervisores', authorize('owner'), ctrl.getSupervisores);
|
router.get('/supervisores', authorize('owner'), ctrl.getSupervisores);
|
||||||
|
|
||||||
|
// Asignaciones de obligaciones/tareas a auxiliares (antes de /:id para evitar match dinámico)
|
||||||
|
router.get('/asignaciones', authorize('owner', 'supervisor'), asignacionesCtrl.listPorSupervisor);
|
||||||
|
router.get('/asignaciones/mias', authorize('auxiliar'), asignacionesCtrl.listPorAuxiliar);
|
||||||
|
router.get('/asignaciones/sin-asignar', authorize('owner', 'supervisor'), asignacionesCtrl.listSinAsignar);
|
||||||
|
|
||||||
// Read: owner + supervisor + auxiliar
|
// Read: owner + supervisor + auxiliar
|
||||||
router.get('/', authorize('owner', 'supervisor', 'auxiliar'), ctrl.list);
|
router.get('/', authorize('owner', 'supervisor', 'auxiliar'), ctrl.list);
|
||||||
router.get('/:id', authorize('owner', 'supervisor', 'auxiliar'), ctrl.getById);
|
router.get('/:id', authorize('owner', 'supervisor', 'auxiliar'), ctrl.getById);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as ctrl from '../controllers/contribuyente.controller.js';
|
|||||||
import * as configCtrl from '../controllers/contribuyente-config.controller.js';
|
import * as configCtrl from '../controllers/contribuyente-config.controller.js';
|
||||||
import * as facturacionCtrl from '../controllers/facturacion.controller.js';
|
import * as facturacionCtrl from '../controllers/facturacion.controller.js';
|
||||||
import * as obligacionesCtrl from '../controllers/obligaciones.controller.js';
|
import * as obligacionesCtrl from '../controllers/obligaciones.controller.js';
|
||||||
|
import * as asignacionesCtrl from '../controllers/asignaciones.controller.js';
|
||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
|
|
||||||
@@ -50,4 +51,8 @@ router.post('/:id/obligaciones/:obligacionId/uncomplete', authorize('owner', 'cf
|
|||||||
router.post('/:id/obligaciones/:obligacionId/complete-periodo', authorize('owner', 'cfo', 'contador', 'auxiliar'), obligacionesCtrl.completePeriodo);
|
router.post('/:id/obligaciones/:obligacionId/complete-periodo', authorize('owner', 'cfo', 'contador', 'auxiliar'), obligacionesCtrl.completePeriodo);
|
||||||
router.post('/:id/obligaciones/:obligacionId/uncomplete-periodo', authorize('owner', 'cfo', 'contador', 'auxiliar'), obligacionesCtrl.uncompletePeriodo);
|
router.post('/:id/obligaciones/:obligacionId/uncomplete-periodo', authorize('owner', 'cfo', 'contador', 'auxiliar'), obligacionesCtrl.uncompletePeriodo);
|
||||||
|
|
||||||
|
// Asignación de obligaciones a auxiliares (supervisor/owner)
|
||||||
|
router.post('/:id/obligaciones/:obligacionId/asignar', authorize('owner', 'supervisor'), asignacionesCtrl.asignarObligacion);
|
||||||
|
router.delete('/:id/obligaciones/:obligacionId/asignar', authorize('owner', 'supervisor'), asignacionesCtrl.desasignarObligacion);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Router, type IRouter } from 'express';
|
import { Router, type IRouter } from 'express';
|
||||||
import { authenticate } from '../middlewares/auth.middleware.js';
|
import { authenticate, authorize } from '../middlewares/auth.middleware.js';
|
||||||
import { tenantMiddleware } from '../middlewares/tenant.middleware.js';
|
import { tenantMiddleware } from '../middlewares/tenant.middleware.js';
|
||||||
import * as ctrl from '../controllers/tareas.controller.js';
|
import * as ctrl from '../controllers/tareas.controller.js';
|
||||||
|
import * as asignacionesCtrl from '../controllers/asignaciones.controller.js';
|
||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
|
|
||||||
router.use(authenticate);
|
router.use(authenticate);
|
||||||
router.use(tenantMiddleware);
|
router.use(tenantMiddleware);
|
||||||
|
|
||||||
|
router.get('/mis-tareas', ctrl.listMisTareas);
|
||||||
router.get('/', ctrl.listTareas);
|
router.get('/', ctrl.listTareas);
|
||||||
router.post('/', ctrl.createTarea);
|
router.post('/', ctrl.createTarea);
|
||||||
router.post('/seed', ctrl.seedDefaults);
|
router.post('/seed', ctrl.seedDefaults);
|
||||||
@@ -17,4 +19,8 @@ router.delete('/:id', ctrl.deleteTarea);
|
|||||||
router.post('/periodo/:id/completar', ctrl.completarPeriodo);
|
router.post('/periodo/:id/completar', ctrl.completarPeriodo);
|
||||||
router.delete('/periodo/:id/completar', ctrl.descompletarPeriodo);
|
router.delete('/periodo/:id/completar', ctrl.descompletarPeriodo);
|
||||||
|
|
||||||
|
// Asignación de tareas a auxiliares (supervisor/owner)
|
||||||
|
router.post('/:id/asignar', authorize('owner', 'supervisor'), asignacionesCtrl.asignarTarea);
|
||||||
|
router.delete('/:id/asignar', authorize('owner', 'supervisor'), asignacionesCtrl.desasignarTarea);
|
||||||
|
|
||||||
export { router as tareasRoutes };
|
export { router as tareasRoutes };
|
||||||
|
|||||||
303
apps/api/src/services/asignaciones.service.ts
Normal file
303
apps/api/src/services/asignaciones.service.ts
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import type { Pool } from 'pg';
|
||||||
|
import { prisma } from '../config/database.js';
|
||||||
|
|
||||||
|
// ── Asignación de obligaciones ──
|
||||||
|
|
||||||
|
export async function asignarObligacion(
|
||||||
|
pool: Pool,
|
||||||
|
obligacionId: string,
|
||||||
|
auxiliarUserId: string,
|
||||||
|
asignadoPor: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO obligacion_asignaciones (obligacion_id, auxiliar_user_id, asignado_por)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (obligacion_id)
|
||||||
|
DO UPDATE SET auxiliar_user_id = $2, asignado_por = $3, asignado_at = now()`,
|
||||||
|
[obligacionId, auxiliarUserId, asignadoPor],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function desasignarObligacion(pool: Pool, obligacionId: string): Promise<void> {
|
||||||
|
await pool.query('DELETE FROM obligacion_asignaciones WHERE obligacion_id = $1', [obligacionId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Asignación de tareas ──
|
||||||
|
|
||||||
|
export async function asignarTarea(
|
||||||
|
pool: Pool,
|
||||||
|
tareaId: string,
|
||||||
|
auxiliarUserId: string,
|
||||||
|
asignadoPor: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO tarea_asignaciones (tarea_id, auxiliar_user_id, asignado_por)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (tarea_id)
|
||||||
|
DO UPDATE SET auxiliar_user_id = $2, asignado_por = $3, asignado_at = now()`,
|
||||||
|
[tareaId, auxiliarUserId, asignadoPor],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function desasignarTarea(pool: Pool, tareaId: string): Promise<void> {
|
||||||
|
await pool.query('DELETE FROM tarea_asignaciones WHERE tarea_id = $1', [tareaId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Listados ──
|
||||||
|
|
||||||
|
export interface AsignacionObligacion {
|
||||||
|
id: string;
|
||||||
|
obligacionId: string;
|
||||||
|
obligacionNombre: string;
|
||||||
|
contribuyenteId: string;
|
||||||
|
contribuyenteRfc: string;
|
||||||
|
contribuyenteRazonSocial: string;
|
||||||
|
auxiliarUserId: string;
|
||||||
|
auxiliarNombre: string | null;
|
||||||
|
asignadoPor: string;
|
||||||
|
asignadoAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsignacionTarea {
|
||||||
|
id: string;
|
||||||
|
tareaId: string;
|
||||||
|
tareaNombre: string;
|
||||||
|
contribuyenteId: string;
|
||||||
|
contribuyenteRfc: string;
|
||||||
|
contribuyenteRazonSocial: string;
|
||||||
|
auxiliarUserId: string;
|
||||||
|
auxiliarNombre: string | null;
|
||||||
|
asignadoPor: string;
|
||||||
|
asignadoAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveUserNames(userIds: string[]): Promise<Map<string, string>> {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
if (userIds.length === 0) return map;
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
where: { id: { in: userIds } },
|
||||||
|
select: { id: true, nombre: true },
|
||||||
|
});
|
||||||
|
for (const u of users) {
|
||||||
|
map.set(u.id, u.nombre);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve todas las asignaciones de obligaciones y tareas de los auxiliares
|
||||||
|
* que pertenecen al supervisor indicado (vía auxiliar_supervisores).
|
||||||
|
* Owner ve todas las asignaciones del tenant.
|
||||||
|
*/
|
||||||
|
export async function getAsignacionesPorSupervisor(
|
||||||
|
pool: Pool,
|
||||||
|
supervisorUserId: string,
|
||||||
|
role: string,
|
||||||
|
): Promise<{ obligaciones: AsignacionObligacion[]; tareas: AsignacionTarea[] }> {
|
||||||
|
const isOwner = role === 'owner' || role === 'cfo' || role === 'contador';
|
||||||
|
|
||||||
|
const whereObl = isOwner
|
||||||
|
? 'WHERE 1=1'
|
||||||
|
: 'WHERE EXISTS (SELECT 1 FROM auxiliar_supervisores asp WHERE asp.auxiliar_user_id = oa.auxiliar_user_id AND asp.supervisor_user_id = $1)';
|
||||||
|
const whereTarea = isOwner
|
||||||
|
? 'WHERE 1=1'
|
||||||
|
: 'WHERE EXISTS (SELECT 1 FROM auxiliar_supervisores asp WHERE asp.auxiliar_user_id = ta.auxiliar_user_id AND asp.supervisor_user_id = $1)';
|
||||||
|
const params = isOwner ? [] : [supervisorUserId];
|
||||||
|
|
||||||
|
const { rows: obligaciones } = await pool.query<AsignacionObligacion>(
|
||||||
|
`SELECT
|
||||||
|
oa.id,
|
||||||
|
oa.obligacion_id AS "obligacionId",
|
||||||
|
oc.nombre AS "obligacionNombre",
|
||||||
|
oc.contribuyente_id AS "contribuyenteId",
|
||||||
|
c.rfc AS "contribuyenteRfc",
|
||||||
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial",
|
||||||
|
oa.auxiliar_user_id AS "auxiliarUserId",
|
||||||
|
oa.asignado_por AS "asignadoPor",
|
||||||
|
oa.asignado_at AS "asignadoAt"
|
||||||
|
FROM obligacion_asignaciones oa
|
||||||
|
JOIN obligaciones_contribuyente oc ON oc.id = oa.obligacion_id
|
||||||
|
JOIN contribuyentes c ON c.entidad_id = oc.contribuyente_id
|
||||||
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
||||||
|
${whereObl}
|
||||||
|
ORDER BY oa.asignado_at DESC`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { rows: tareas } = await pool.query<AsignacionTarea>(
|
||||||
|
`SELECT
|
||||||
|
ta.id,
|
||||||
|
ta.tarea_id AS "tareaId",
|
||||||
|
tc.nombre AS "tareaNombre",
|
||||||
|
tc.contribuyente_id AS "contribuyenteId",
|
||||||
|
c.rfc AS "contribuyenteRfc",
|
||||||
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial",
|
||||||
|
ta.auxiliar_user_id AS "auxiliarUserId",
|
||||||
|
ta.asignado_por AS "asignadoPor",
|
||||||
|
ta.asignado_at AS "asignadoAt"
|
||||||
|
FROM tarea_asignaciones ta
|
||||||
|
JOIN tareas_catalogo tc ON tc.id = ta.tarea_id
|
||||||
|
JOIN contribuyentes c ON c.entidad_id = tc.contribuyente_id
|
||||||
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
||||||
|
${whereTarea}
|
||||||
|
ORDER BY ta.asignado_at DESC`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
const allAuxIds = [...new Set([
|
||||||
|
...obligaciones.map(o => o.auxiliarUserId),
|
||||||
|
...tareas.map(t => t.auxiliarUserId),
|
||||||
|
])];
|
||||||
|
const names = await resolveUserNames(allAuxIds);
|
||||||
|
|
||||||
|
return {
|
||||||
|
obligaciones: obligaciones.map(o => ({ ...o, auxiliarNombre: names.get(o.auxiliarUserId) ?? null })),
|
||||||
|
tareas: tareas.map(t => ({ ...t, auxiliarNombre: names.get(t.auxiliarUserId) ?? null })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve las asignaciones del auxiliar logueado.
|
||||||
|
*/
|
||||||
|
export async function getAsignacionesPorAuxiliar(
|
||||||
|
pool: Pool,
|
||||||
|
auxiliarUserId: string,
|
||||||
|
): Promise<{ obligaciones: AsignacionObligacion[]; tareas: AsignacionTarea[] }> {
|
||||||
|
const { rows: obligaciones } = await pool.query<AsignacionObligacion>(
|
||||||
|
`SELECT
|
||||||
|
oa.id,
|
||||||
|
oa.obligacion_id AS "obligacionId",
|
||||||
|
oc.nombre AS "obligacionNombre",
|
||||||
|
oc.contribuyente_id AS "contribuyenteId",
|
||||||
|
c.rfc AS "contribuyenteRfc",
|
||||||
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial",
|
||||||
|
oa.auxiliar_user_id AS "auxiliarUserId",
|
||||||
|
oa.asignado_por AS "asignadoPor",
|
||||||
|
oa.asignado_at AS "asignadoAt"
|
||||||
|
FROM obligacion_asignaciones oa
|
||||||
|
JOIN obligaciones_contribuyente oc ON oc.id = oa.obligacion_id
|
||||||
|
JOIN contribuyentes c ON c.entidad_id = oc.contribuyente_id
|
||||||
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
||||||
|
WHERE oa.auxiliar_user_id = $1
|
||||||
|
ORDER BY oa.asignado_at DESC`,
|
||||||
|
[auxiliarUserId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { rows: tareas } = await pool.query<AsignacionTarea>(
|
||||||
|
`SELECT
|
||||||
|
ta.id,
|
||||||
|
ta.tarea_id AS "tareaId",
|
||||||
|
tc.nombre AS "tareaNombre",
|
||||||
|
tc.contribuyente_id AS "contribuyenteId",
|
||||||
|
c.rfc AS "contribuyenteRfc",
|
||||||
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial",
|
||||||
|
ta.auxiliar_user_id AS "auxiliarUserId",
|
||||||
|
ta.asignado_por AS "asignadoPor",
|
||||||
|
ta.asignado_at AS "asignadoAt"
|
||||||
|
FROM tarea_asignaciones ta
|
||||||
|
JOIN tareas_catalogo tc ON tc.id = ta.tarea_id
|
||||||
|
JOIN contribuyentes c ON c.entidad_id = tc.contribuyente_id
|
||||||
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
||||||
|
WHERE ta.auxiliar_user_id = $1
|
||||||
|
ORDER BY ta.asignado_at DESC`,
|
||||||
|
[auxiliarUserId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const names = await resolveUserNames([auxiliarUserId]);
|
||||||
|
const auxName = names.get(auxiliarUserId) ?? null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
obligaciones: obligaciones.map(o => ({ ...o, auxiliarNombre: auxName })),
|
||||||
|
tareas: tareas.map(t => ({ ...t, auxiliarNombre: auxName })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve obligaciones activas sin asignar para los contribuyentes indicados.
|
||||||
|
*/
|
||||||
|
export async function getObligacionesSinAsignar(
|
||||||
|
pool: Pool,
|
||||||
|
entidadIds: string[],
|
||||||
|
): Promise<Omit<AsignacionObligacion, 'id' | 'auxiliarUserId' | 'auxiliarNombre' | 'asignadoPor' | 'asignadoAt'>[]> {
|
||||||
|
if (entidadIds.length === 0) return [];
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`SELECT
|
||||||
|
oc.id AS "obligacionId",
|
||||||
|
oc.nombre AS "obligacionNombre",
|
||||||
|
oc.contribuyente_id AS "contribuyenteId",
|
||||||
|
c.rfc AS "contribuyenteRfc",
|
||||||
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial"
|
||||||
|
FROM obligaciones_contribuyente oc
|
||||||
|
JOIN contribuyentes c ON c.entidad_id = oc.contribuyente_id
|
||||||
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
||||||
|
LEFT JOIN obligacion_asignaciones oa ON oa.obligacion_id = oc.id
|
||||||
|
WHERE oc.activa = true AND oa.id IS NULL AND oc.contribuyente_id = ANY($1)
|
||||||
|
ORDER BY c.rfc, oc.nombre`,
|
||||||
|
[entidadIds],
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve tareas activas sin asignar para los contribuyentes indicados.
|
||||||
|
*/
|
||||||
|
export async function getTareasSinAsignar(
|
||||||
|
pool: Pool,
|
||||||
|
entidadIds: string[],
|
||||||
|
): Promise<Omit<AsignacionTarea, 'id' | 'auxiliarUserId' | 'auxiliarNombre' | 'asignadoPor' | 'asignadoAt'>[]> {
|
||||||
|
if (entidadIds.length === 0) return [];
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`SELECT
|
||||||
|
tc.id AS "tareaId",
|
||||||
|
tc.nombre AS "tareaNombre",
|
||||||
|
tc.contribuyente_id AS "contribuyenteId",
|
||||||
|
c.rfc AS "contribuyenteRfc",
|
||||||
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial"
|
||||||
|
FROM tareas_catalogo tc
|
||||||
|
JOIN contribuyentes c ON c.entidad_id = tc.contribuyente_id
|
||||||
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
||||||
|
LEFT JOIN tarea_asignaciones ta ON ta.tarea_id = tc.id
|
||||||
|
WHERE tc.active = true AND ta.id IS NULL AND tc.contribuyente_id = ANY($1)
|
||||||
|
ORDER BY c.rfc, tc.nombre`,
|
||||||
|
[entidadIds],
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve el auxiliar asignado a una obligación (o null).
|
||||||
|
*/
|
||||||
|
export async function getAuxiliarAsignadoObligacion(
|
||||||
|
pool: Pool,
|
||||||
|
obligacionId: string,
|
||||||
|
): Promise<{ auxiliarUserId: string; auxiliarNombre: string | null } | null> {
|
||||||
|
const { rows } = await pool.query<{ auxiliar_user_id: string }>(
|
||||||
|
`SELECT oa.auxiliar_user_id
|
||||||
|
FROM obligacion_asignaciones oa
|
||||||
|
WHERE oa.obligacion_id = $1`,
|
||||||
|
[obligacionId],
|
||||||
|
);
|
||||||
|
if (rows.length === 0) return null;
|
||||||
|
const auxId = rows[0].auxiliar_user_id;
|
||||||
|
const names = await resolveUserNames([auxId]);
|
||||||
|
return { auxiliarUserId: auxId, auxiliarNombre: names.get(auxId) ?? null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve el auxiliar asignado a una tarea (o null).
|
||||||
|
*/
|
||||||
|
export async function getAuxiliarAsignadoTarea(
|
||||||
|
pool: Pool,
|
||||||
|
tareaId: string,
|
||||||
|
): Promise<{ auxiliarUserId: string; auxiliarNombre: string | null } | null> {
|
||||||
|
const { rows } = await pool.query<{ auxiliar_user_id: string }>(
|
||||||
|
`SELECT ta.auxiliar_user_id
|
||||||
|
FROM tarea_asignaciones ta
|
||||||
|
WHERE ta.tarea_id = $1`,
|
||||||
|
[tareaId],
|
||||||
|
);
|
||||||
|
if (rows.length === 0) return null;
|
||||||
|
const auxId = rows[0].auxiliar_user_id;
|
||||||
|
const names = await resolveUserNames([auxId]);
|
||||||
|
return { auxiliarUserId: auxId, auxiliarNombre: names.get(auxId) ?? null };
|
||||||
|
}
|
||||||
@@ -110,6 +110,17 @@ export async function deleteCartera(pool: Pool, id: string): Promise<boolean> {
|
|||||||
|
|
||||||
// Entidades in cartera
|
// Entidades in cartera
|
||||||
export async function addEntidadToCartera(pool: Pool, carteraId: string, entidadId: string): Promise<void> {
|
export async function addEntidadToCartera(pool: Pool, carteraId: string, entidadId: string): Promise<void> {
|
||||||
|
// Si es subcartera, validar que la entidad pertenezca a la cartera padre
|
||||||
|
const cartera = await getCarteraById(pool, carteraId);
|
||||||
|
if (cartera?.parentId) {
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
'SELECT 1 FROM cartera_entidades WHERE cartera_id = $1 AND entidad_id = $2',
|
||||||
|
[cartera.parentId, entidadId],
|
||||||
|
);
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new Error('La entidad no pertenece a la cartera padre de esta subcartera');
|
||||||
|
}
|
||||||
|
}
|
||||||
await pool.query('INSERT INTO cartera_entidades (cartera_id, entidad_id) VALUES ($1, $2) ON CONFLICT DO NOTHING', [carteraId, entidadId]);
|
await pool.query('INSERT INTO cartera_entidades (cartera_id, entidad_id) VALUES ($1, $2) ON CONFLICT DO NOTHING', [carteraId, entidadId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ export interface ObligacionContribuyente {
|
|||||||
completadaPor: string | null;
|
completadaPor: string | null;
|
||||||
periodoCompletado: string | null;
|
periodoCompletado: string | null;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
|
auxiliarAsignadoId?: string | null;
|
||||||
|
auxiliarAsignadoNombre?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCatalogo(): ObligacionFiscal[] {
|
export function getCatalogo(): ObligacionFiscal[] {
|
||||||
@@ -146,15 +148,18 @@ export function getCatalogo(): ObligacionFiscal[] {
|
|||||||
|
|
||||||
export async function getObligaciones(pool: Pool, contribuyenteId: string): Promise<ObligacionContribuyente[]> {
|
export async function getObligaciones(pool: Pool, contribuyenteId: string): Promise<ObligacionContribuyente[]> {
|
||||||
const { rows } = await pool.query(`
|
const { rows } = await pool.query(`
|
||||||
SELECT id, contribuyente_id AS "contribuyenteId", catalogo_id AS "catalogoId",
|
SELECT
|
||||||
nombre, fundamento, frecuencia, fecha_limite AS "fechaLimite", categoria,
|
oc.id, oc.contribuyente_id AS "contribuyenteId", oc.catalogo_id AS "catalogoId",
|
||||||
activa, es_recomendada AS "esRecomendada", es_custom AS "esCustom",
|
oc.nombre, oc.fundamento, oc.frecuencia, oc.fecha_limite AS "fechaLimite", oc.categoria,
|
||||||
completada, completada_at AS "completadaAt", completada_por AS "completadaPor",
|
oc.activa, oc.es_recomendada AS "esRecomendada", oc.es_custom AS "esCustom",
|
||||||
periodo_completado AS "periodoCompletado",
|
oc.completada, oc.completada_at AS "completadaAt", oc.completada_por AS "completadaPor",
|
||||||
created_at AS "createdAt"
|
oc.periodo_completado AS "periodoCompletado",
|
||||||
FROM obligaciones_contribuyente
|
oc.created_at AS "createdAt",
|
||||||
WHERE contribuyente_id = $1
|
oa.auxiliar_user_id AS "auxiliarAsignadoId"
|
||||||
ORDER BY categoria, nombre
|
FROM obligaciones_contribuyente oc
|
||||||
|
LEFT JOIN obligacion_asignaciones oa ON oa.obligacion_id = oc.id
|
||||||
|
WHERE oc.contribuyente_id = $1
|
||||||
|
ORDER BY oc.categoria, oc.nombre
|
||||||
`, [contribuyenteId]);
|
`, [contribuyenteId]);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface TareaCatalogo {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
orden: number;
|
orden: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
auxiliarAsignadoId?: string | null;
|
||||||
|
auxiliarAsignadoNombre?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TareaPeriodo {
|
export interface TareaPeriodo {
|
||||||
@@ -47,6 +49,8 @@ const ROW_TO_TAREA = (r: any): TareaCatalogo => ({
|
|||||||
active: r.active,
|
active: r.active,
|
||||||
orden: r.orden,
|
orden: r.orden,
|
||||||
createdAt: r.created_at,
|
createdAt: r.created_at,
|
||||||
|
auxiliarAsignadoId: r.auxiliarAsignadoId ?? null,
|
||||||
|
auxiliarAsignadoNombre: r.auxiliarAsignadoNombre ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ROW_TO_PERIODO = (r: any): TareaPeriodo => ({
|
const ROW_TO_PERIODO = (r: any): TareaPeriodo => ({
|
||||||
@@ -68,9 +72,13 @@ function sanitizeUuid(id: string): string {
|
|||||||
|
|
||||||
export async function listTareas(pool: Pool, contribuyenteId: string): Promise<TareaCatalogo[]> {
|
export async function listTareas(pool: Pool, contribuyenteId: string): Promise<TareaCatalogo[]> {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT * FROM tareas_catalogo
|
`SELECT
|
||||||
WHERE contribuyente_id = $1 AND active = true
|
tc.*,
|
||||||
ORDER BY orden, nombre`,
|
ta.auxiliar_user_id AS "auxiliarAsignadoId"
|
||||||
|
FROM tareas_catalogo tc
|
||||||
|
LEFT JOIN tarea_asignaciones ta ON ta.tarea_id = tc.id
|
||||||
|
WHERE tc.contribuyente_id = $1 AND tc.active = true
|
||||||
|
ORDER BY tc.orden, tc.nombre`,
|
||||||
[sanitizeUuid(contribuyenteId)],
|
[sanitizeUuid(contribuyenteId)],
|
||||||
);
|
);
|
||||||
return rows.map(ROW_TO_TAREA);
|
return rows.map(ROW_TO_TAREA);
|
||||||
@@ -272,6 +280,59 @@ export async function listTareasConPeriodoActual(
|
|||||||
return tareas.map(t => ({ ...t, periodoActual: periodos.get(t.id) ?? null }));
|
return tareas.map(t => ({ ...t, periodoActual: periodos.get(t.id) ?? null }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TareaConContribuyente extends TareaConPeriodo {
|
||||||
|
contribuyenteRfc: string;
|
||||||
|
contribuyenteRazonSocial: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lee tareas activas con periodo actual para una lista de contribuyentes.
|
||||||
|
* Útil para la vista "Mis Tareas".
|
||||||
|
*/
|
||||||
|
export async function listTareasConPeriodoPorContribuyentes(
|
||||||
|
pool: Pool,
|
||||||
|
contribuyenteIds: string[],
|
||||||
|
): Promise<TareaConContribuyente[]> {
|
||||||
|
if (contribuyenteIds.length === 0) return [];
|
||||||
|
|
||||||
|
// Materializar periodos para cada contribuyente en paralelo
|
||||||
|
await Promise.all(contribuyenteIds.map(id => materializarPeriodos(pool, id)));
|
||||||
|
|
||||||
|
const { rows: tareasRows } = await pool.query(
|
||||||
|
`SELECT tc.*, c.entidad_id AS "contribuyenteId",
|
||||||
|
c.rfc AS "contribuyenteRfc",
|
||||||
|
COALESCE(r.razon_social, c.rfc) AS "contribuyenteRazonSocial"
|
||||||
|
FROM tareas_catalogo tc
|
||||||
|
JOIN contribuyentes c ON c.entidad_id = tc.contribuyente_id
|
||||||
|
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
||||||
|
WHERE tc.contribuyente_id = ANY($1::uuid[]) AND tc.active = true
|
||||||
|
ORDER BY c.rfc, tc.orden, tc.nombre`,
|
||||||
|
[contribuyenteIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tareasRows.length === 0) return [];
|
||||||
|
|
||||||
|
const tareaIds = tareasRows.map((r: any) => r.id);
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const { rows: periodoRows } = await pool.query(
|
||||||
|
`SELECT DISTINCT ON (tarea_id) *
|
||||||
|
FROM tarea_periodos
|
||||||
|
WHERE tarea_id = ANY($1::uuid[])
|
||||||
|
AND (completada = false OR fecha_limite >= $2::date)
|
||||||
|
ORDER BY tarea_id, fecha_limite ASC`,
|
||||||
|
[tareaIds, today],
|
||||||
|
);
|
||||||
|
const periodos = new Map(periodoRows.map((r: any) => [r.tarea_id, ROW_TO_PERIODO(r)]));
|
||||||
|
|
||||||
|
return tareasRows.map((r: any) => ({
|
||||||
|
...ROW_TO_TAREA(r),
|
||||||
|
contribuyenteId: r.contribuyenteId,
|
||||||
|
contribuyenteRfc: r.contribuyenteRfc,
|
||||||
|
contribuyenteRazonSocial: r.contribuyenteRazonSocial,
|
||||||
|
periodoActual: periodos.get(r.id) ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Completar / descompletar periodo ───
|
// ─── Completar / descompletar periodo ───
|
||||||
|
|
||||||
const ROLES_SUPERVISOR = new Set(['owner', 'cfo', 'supervisor']);
|
const ROLES_SUPERVISOR = new Set(['owner', 'cfo', 'supervisor']);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { prisma } from '../config/database.js';
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { getDespachoPlanLimits } from './plan-catalogo.service.js';
|
import { getDespachoPlanLimits } from './plan-catalogo.service.js';
|
||||||
|
import { emailService } from './email/email.service.js';
|
||||||
import type { UserListItem, UserInvite, UserUpdate, Role } from '@horux/shared';
|
import type { UserListItem, UserInvite, UserUpdate, Role } from '@horux/shared';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,6 +100,13 @@ export async function inviteUsuario(tenantId: string, data: UserInvite): Promise
|
|||||||
lastTenantId: tenantId,
|
lastTenantId: tenantId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enviar correo de bienvenida con credenciales (non-blocking)
|
||||||
|
emailService.sendWelcome(data.email, {
|
||||||
|
nombre: data.nombre,
|
||||||
|
email: data.email,
|
||||||
|
tempPassword,
|
||||||
|
}).catch(err => console.error('[EMAIL] Welcome email failed:', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
const rolId = await getRolId(data.role);
|
const rolId = await getRolId(data.role);
|
||||||
@@ -224,8 +232,9 @@ export async function createUsuarioGlobal(
|
|||||||
// Si el email ya existe como user global, agregamos membership en este tenant
|
// Si el email ya existe como user global, agregamos membership en este tenant
|
||||||
let user = await prisma.user.findUnique({ where: { email: data.email } });
|
let user = await prisma.user.findUnique({ where: { email: data.email } });
|
||||||
|
|
||||||
|
let tempPassword: string | null = null;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const tempPassword = randomBytes(4).toString('hex');
|
tempPassword = randomBytes(4).toString('hex');
|
||||||
const passwordHash = await bcrypt.hash(tempPassword, 12);
|
const passwordHash = await bcrypt.hash(tempPassword, 12);
|
||||||
user = await prisma.user.create({
|
user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -235,6 +244,13 @@ export async function createUsuarioGlobal(
|
|||||||
lastTenantId: tenantId,
|
lastTenantId: tenantId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enviar correo de bienvenida con credenciales (non-blocking)
|
||||||
|
emailService.sendWelcome(data.email, {
|
||||||
|
nombre: data.nombre,
|
||||||
|
email: data.email,
|
||||||
|
tempPassword,
|
||||||
|
}).catch(err => console.error('[EMAIL] Welcome email failed:', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
const rolId = await getRolId(data.role);
|
const rolId = await getRolId(data.role);
|
||||||
|
|||||||
Reference in New Issue
Block a user