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:
Horux Dev
2026-05-23 23:40:12 +00:00
parent 0c7580aa44
commit f43cb165c6
11 changed files with 596 additions and 14 deletions

View File

@@ -17,6 +17,8 @@ export interface TareaCatalogo {
active: boolean;
orden: number;
createdAt: Date;
auxiliarAsignadoId?: string | null;
auxiliarAsignadoNombre?: string | null;
}
export interface TareaPeriodo {
@@ -47,6 +49,8 @@ const ROW_TO_TAREA = (r: any): TareaCatalogo => ({
active: r.active,
orden: r.orden,
createdAt: r.created_at,
auxiliarAsignadoId: r.auxiliarAsignadoId ?? null,
auxiliarAsignadoNombre: r.auxiliarAsignadoNombre ?? null,
});
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[]> {
const { rows } = await pool.query(
`SELECT * FROM tareas_catalogo
WHERE contribuyente_id = $1 AND active = true
ORDER BY orden, nombre`,
`SELECT
tc.*,
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)],
);
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 }));
}
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 ───
const ROLES_SUPERVISOR = new Set(['owner', 'cfo', 'supervisor']);