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 { 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 { 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 { 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 { 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> { const map = new Map(); 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'; // Relación supervisor → auxiliar se infiere desde carteras (directas y // subcarteras) con fallback a la tabla legacy auxiliar_supervisores. const supervisorFilter = isOwner ? '' : `AND EXISTS ( SELECT 1 FROM ( SELECT c.auxiliar_user_id FROM carteras c WHERE c.supervisor_user_id = $1 AND c.auxiliar_user_id IS NOT NULL UNION SELECT sub.auxiliar_user_id FROM carteras sub JOIN carteras p ON p.id = sub.parent_id WHERE p.supervisor_user_id = $1 AND sub.auxiliar_user_id IS NOT NULL UNION SELECT auxiliar_user_id FROM auxiliar_supervisores WHERE supervisor_user_id = $1 ) sup_aux WHERE sup_aux.auxiliar_user_id = __AUX_COL__ )`; const whereObl = isOwner ? 'WHERE 1=1' : `WHERE 1=1 ${supervisorFilter.replace(/__AUX_COL__/g, 'oa.auxiliar_user_id')}`; const whereTarea = isOwner ? 'WHERE 1=1' : `WHERE 1=1 ${supervisorFilter.replace(/__AUX_COL__/g, 'ta.auxiliar_user_id')}`; const params = isOwner ? [] : [supervisorUserId]; const { rows: obligaciones } = await pool.query( `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( `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( `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( `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[]> { 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[]> { 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 }; } /** * Devuelve los userIds de auxiliares que tienen al contribuyente en alguna * de sus subcarteras (carteras con auxiliar_user_id no nulo que contienen * al contribuyente en cartera_entidades). */ export async function getAuxiliaresElegibles( pool: Pool, contribuyenteId: string, ): Promise { const { rows } = await pool.query<{ auxiliar_user_id: string }>( `SELECT DISTINCT c.auxiliar_user_id FROM carteras c JOIN cartera_entidades ce ON ce.cartera_id = c.id WHERE ce.entidad_id = $1 AND c.auxiliar_user_id IS NOT NULL`, [contribuyenteId], ); return rows.map(r => r.auxiliar_user_id); }