- Obligaciones: las obligaciones activas sin registro en obligacion_periodos para el periodo actual ahora se cuentan como pendientes (antes daban 0) - Tareas: se materializan los periodos antes de contar para que las tareas sin registro previo aparezcan como pendientes - Usa CTEs separadas para obligaciones y tareas evitando producto cartesiano
521 lines
21 KiB
TypeScript
521 lines
21 KiB
TypeScript
import type { Pool } from 'pg';
|
|
import { prisma } from '../config/database.js';
|
|
import { materializarPeriodos } from './tareas.service.js';
|
|
|
|
export interface ContribuyentesStats {
|
|
totalContribuyentes: number;
|
|
ultimaExtraccion: Date | null;
|
|
/** % global del despacho de obligaciones+tareas del periodo seleccionado completadas vs total. */
|
|
progresoDelMes: number;
|
|
/** Declaraciones cuyo `created_at` cae en el periodo seleccionado. */
|
|
declaracionesPresentadas: number;
|
|
/** Subset del anterior con `pdf_pago` no nulo. */
|
|
declaracionesPagadas: number;
|
|
/** Obligaciones de declaración pendientes de periodos anteriores al seleccionado. */
|
|
declaracionesAtrasadas: number;
|
|
/** Tareas pendientes con fecha_limite anterior al inicio del periodo seleccionado. */
|
|
tareasAtrasadas: number;
|
|
}
|
|
|
|
/**
|
|
* Métricas para la pestaña "Contribuyentes" del módulo Despacho (owner-only).
|
|
*
|
|
* Periodo: si se pasa `año`/`mes`, las métricas mensuales se calculan para
|
|
* ese periodo. Default = mes en curso.
|
|
*
|
|
* - totalContribuyentes / ultimaExtraccion: independientes del periodo.
|
|
* - progresoDelMes: % global obligaciones+tareas del periodo completadas.
|
|
* - declaracionesPresentadas/pagadas: declaraciones con `created_at` en el
|
|
* periodo seleccionado (presentación, no devengo).
|
|
* - declaracionesAtrasadas: obligaciones-periodo NO completadas con periodo
|
|
* anterior al seleccionado, donde la obligación sea de tipo declaración
|
|
* (categoría contiene 'mensual'/'anual'/'declaración' — heurística laxa
|
|
* ya que el catálogo no tiene un flag explícito).
|
|
* - tareasAtrasadas: tareas-periodo NO completadas con fecha_limite anterior
|
|
* al primer día del periodo seleccionado.
|
|
*/
|
|
export async function getContribuyentesStats(
|
|
pool: Pool,
|
|
tenantId: string,
|
|
año?: number,
|
|
mes?: number,
|
|
): Promise<ContribuyentesStats> {
|
|
const { rows: [{ count }] } = await pool.query<{ count: number }>(
|
|
`SELECT COUNT(*)::int AS count
|
|
FROM contribuyentes c
|
|
JOIN entidades_gestionadas e ON e.id = c.entidad_id
|
|
WHERE e.active = true`,
|
|
);
|
|
|
|
const last = await prisma.satSyncJob.findFirst({
|
|
where: { tenantId, status: 'completed' },
|
|
orderBy: { completedAt: 'desc' },
|
|
select: { completedAt: true },
|
|
});
|
|
|
|
// Periodo: usa el filtrado o cae al mes en curso.
|
|
const now = new Date();
|
|
const _año = año ?? now.getFullYear();
|
|
const _mes = mes ?? now.getMonth() + 1;
|
|
const periodoMes = `${_año}-${String(_mes).padStart(2, '0')}`;
|
|
const inicioMes = `${_año}-${String(_mes).padStart(2, '0')}-01`;
|
|
const finMes = new Date(_año, _mes, 0).toISOString().split('T')[0];
|
|
|
|
const { rows: [progresoRow] } = await pool.query<{ total: number; completadas: number }>(
|
|
`SELECT
|
|
(SELECT COUNT(*)::int FROM obligacion_periodos op
|
|
JOIN obligaciones_contribuyente oc ON oc.id = op.obligacion_id
|
|
WHERE oc.activa = true AND op.periodo = $1)
|
|
+
|
|
(SELECT COUNT(*)::int FROM tarea_periodos tp
|
|
JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
|
|
WHERE tc.active = true AND tp.fecha_limite BETWEEN $2::date AND $3::date)
|
|
AS total,
|
|
(SELECT COUNT(*)::int FROM obligacion_periodos op
|
|
JOIN obligaciones_contribuyente oc ON oc.id = op.obligacion_id
|
|
WHERE oc.activa = true AND op.periodo = $1 AND op.completada = true)
|
|
+
|
|
(SELECT COUNT(*)::int FROM tarea_periodos tp
|
|
JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
|
|
WHERE tc.active = true AND tp.fecha_limite BETWEEN $2::date AND $3::date AND tp.completada = true)
|
|
AS completadas`,
|
|
[periodoMes, inicioMes, finMes],
|
|
);
|
|
const progresoDelMes = progresoRow.total > 0
|
|
? Math.round((progresoRow.completadas / progresoRow.total) * 100)
|
|
: 0;
|
|
|
|
const { rows: [decRow] } = await pool.query<{ presentadas: number; pagadas: number }>(
|
|
`SELECT
|
|
COUNT(*)::int AS presentadas,
|
|
COUNT(*) FILTER (WHERE pdf_pago IS NOT NULL)::int AS pagadas
|
|
FROM declaraciones_provisionales
|
|
WHERE created_at >= $1::date AND created_at < ($2::date + interval '1 day')`,
|
|
[inicioMes, finMes],
|
|
);
|
|
|
|
// Atrasadas de periodos anteriores al seleccionado.
|
|
// Para declaraciones (obligaciones) usamos `op.periodo < periodoMes`.
|
|
// Heurística "es declaración": categoría contiene 'mensual', 'anual',
|
|
// 'declaración' o el nombre incluye 'declaración' (case insensitive).
|
|
const { rows: [atrRow] } = await pool.query<{ decl_atr: number; tar_atr: number }>(
|
|
`SELECT
|
|
(SELECT COUNT(*)::int FROM obligacion_periodos op
|
|
JOIN obligaciones_contribuyente oc ON oc.id = op.obligacion_id
|
|
WHERE oc.activa = true
|
|
AND op.completada = false
|
|
AND op.periodo < $1
|
|
AND (
|
|
LOWER(COALESCE(oc.categoria, '')) ~ 'mensual|anual|declarac'
|
|
OR LOWER(oc.nombre) LIKE '%declarac%'
|
|
)
|
|
) AS decl_atr,
|
|
(SELECT COUNT(*)::int FROM tarea_periodos tp
|
|
JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
|
|
WHERE tc.active = true
|
|
AND tp.completada = false
|
|
AND tp.fecha_limite < $2::date
|
|
) AS tar_atr`,
|
|
[periodoMes, inicioMes],
|
|
);
|
|
|
|
return {
|
|
totalContribuyentes: count,
|
|
ultimaExtraccion: last?.completedAt ?? null,
|
|
progresoDelMes,
|
|
declaracionesPresentadas: decRow.presentadas,
|
|
declaracionesPagadas: decRow.pagadas,
|
|
declaracionesAtrasadas: atrRow.decl_atr,
|
|
tareasAtrasadas: atrRow.tar_atr,
|
|
};
|
|
}
|
|
|
|
export interface ContribuyenteAsignado {
|
|
contribuyenteId: string;
|
|
rfc: string;
|
|
nombre: string;
|
|
carteraNombre: string | null;
|
|
obligacionesPendientes: number;
|
|
obligacionesAtrasadas: number;
|
|
obligacionesCompletadas: number;
|
|
tareasPendientes: number;
|
|
tareasAtrasadas: number;
|
|
tareasCompletadas: number;
|
|
}
|
|
|
|
/**
|
|
* Resuelve los contribuyentes asignados al usuario actual según su rol y la
|
|
* estructura de carteras:
|
|
*
|
|
* - **owner / cfo**: TODOS los contribuyentes del despacho.
|
|
* - **supervisor**: contribuyentes que están en una cartera donde
|
|
* `c.supervisor_user_id = userId` o en una subcartera de tales carteras.
|
|
* - **auxiliar**: contribuyentes en carteras donde `c.auxiliar_user_id = userId`.
|
|
* - **otros (contador, cliente, etc.)**: vacío.
|
|
*
|
|
* Las métricas se calculan usando el periodo `año`/`mes` como pivote: lo
|
|
* "atrasado" es lo NO completado de periodos anteriores al filtrado.
|
|
*/
|
|
export async function getMisAsignados(
|
|
pool: Pool,
|
|
userId: string,
|
|
userRole: string,
|
|
año?: number,
|
|
mes?: number,
|
|
): Promise<ContribuyenteAsignado[]> {
|
|
let baseFilter: string;
|
|
const params: unknown[] = [];
|
|
if (userRole === 'owner' || userRole === 'cfo') {
|
|
baseFilter = `e.active = true`;
|
|
} else if (userRole === 'supervisor') {
|
|
params.push(userId);
|
|
baseFilter = `e.active = true AND ce.cartera_id IN (
|
|
SELECT id FROM carteras WHERE supervisor_user_id = $1
|
|
UNION
|
|
SELECT id FROM carteras WHERE parent_id IN (
|
|
SELECT id FROM carteras WHERE supervisor_user_id = $1
|
|
)
|
|
)`;
|
|
} else if (userRole === 'auxiliar') {
|
|
params.push(userId);
|
|
baseFilter = `e.active = true AND ce.cartera_id IN (
|
|
SELECT id FROM carteras WHERE auxiliar_user_id = $1
|
|
)`;
|
|
} else {
|
|
return [];
|
|
}
|
|
|
|
const { rows } = await pool.query(
|
|
`SELECT DISTINCT c.entidad_id AS contribuyente_id, c.rfc, e.nombre,
|
|
(SELECT cart.nombre FROM carteras cart
|
|
JOIN cartera_entidades cee ON cee.cartera_id = cart.id
|
|
WHERE cee.entidad_id = c.entidad_id LIMIT 1) AS cartera_nombre
|
|
FROM contribuyentes c
|
|
JOIN entidades_gestionadas e ON e.id = c.entidad_id
|
|
LEFT JOIN cartera_entidades ce ON ce.entidad_id = c.entidad_id
|
|
WHERE ${baseFilter}
|
|
ORDER BY e.nombre`,
|
|
params,
|
|
);
|
|
|
|
// Para cada contribuyente, contar pendientes/atrasados/completados de obligaciones+tareas.
|
|
// Hacemos una sola query agregada por contribuyente con CTEs.
|
|
const ids = rows.map(r => r.contribuyente_id);
|
|
if (ids.length === 0) return [];
|
|
|
|
// Periodo pivote: usa el filtrado o cae al mes en curso.
|
|
const now = new Date();
|
|
const _año = año ?? now.getFullYear();
|
|
const _mes = mes ?? now.getMonth() + 1;
|
|
const periodoMes = `${_año}-${String(_mes).padStart(2, '0')}`;
|
|
const inicioMes = `${_año}-${String(_mes).padStart(2, '0')}-01`;
|
|
const finMes = new Date(_año, _mes, 0).toISOString().split('T')[0];
|
|
|
|
// Materializar periodos de tareas antes de contar (evita que tareas sin
|
|
// registro en tarea_periodos aparezcan como 0).
|
|
await Promise.all(ids.map(id => materializarPeriodos(pool, id).catch(() => {})));
|
|
|
|
const { rows: stats } = await pool.query(
|
|
`WITH obligaciones_activas AS (
|
|
SELECT id, contribuyente_id FROM obligaciones_contribuyente
|
|
WHERE contribuyente_id = ANY($4::uuid[]) AND activa = true
|
|
),
|
|
op_actual AS (
|
|
SELECT obligacion_id, completada FROM obligacion_periodos
|
|
WHERE obligacion_id IN (SELECT id FROM obligaciones_activas) AND periodo = $1
|
|
),
|
|
op_atrasadas AS (
|
|
SELECT obligacion_id, COUNT(*) as atrasadas FROM obligacion_periodos
|
|
WHERE obligacion_id IN (SELECT id FROM obligaciones_activas) AND periodo < $1 AND completada = false
|
|
GROUP BY obligacion_id
|
|
),
|
|
obl AS (
|
|
SELECT oa.contribuyente_id,
|
|
COUNT(*) FILTER (WHERE op_a.completada IS NULL OR op_a.completada = false)::int AS pendientes,
|
|
COALESCE(SUM(op_atr.atrasadas), 0)::int AS atrasadas,
|
|
COUNT(*) FILTER (WHERE op_a.completada = true)::int AS completadas
|
|
FROM obligaciones_activas oa
|
|
LEFT JOIN op_actual op_a ON op_a.obligacion_id = oa.id
|
|
LEFT JOIN op_atrasadas op_atr ON op_atr.obligacion_id = oa.id
|
|
GROUP BY oa.contribuyente_id
|
|
),
|
|
tareas_activas AS (
|
|
SELECT id, contribuyente_id FROM tareas_catalogo
|
|
WHERE contribuyente_id = ANY($4::uuid[]) AND active = true
|
|
),
|
|
tar_actual AS (
|
|
SELECT tarea_id, completada FROM tarea_periodos
|
|
WHERE tarea_id IN (SELECT id FROM tareas_activas)
|
|
AND fecha_limite BETWEEN $2::date AND $3::date
|
|
),
|
|
tar_atrasadas AS (
|
|
SELECT tarea_id, COUNT(*) as atrasadas FROM tarea_periodos
|
|
WHERE tarea_id IN (SELECT id FROM tareas_activas)
|
|
AND fecha_limite < $2::date AND completada = false
|
|
GROUP BY tarea_id
|
|
),
|
|
tar AS (
|
|
SELECT ta.contribuyente_id,
|
|
COUNT(*) FILTER (WHERE tar_a.completada IS NULL OR tar_a.completada = false)::int AS pendientes,
|
|
COALESCE(SUM(tar_atr.atrasadas), 0)::int AS atrasadas,
|
|
COUNT(*) FILTER (WHERE tar_a.completada = true)::int AS completadas
|
|
FROM tareas_activas ta
|
|
LEFT JOIN tar_actual tar_a ON tar_a.tarea_id = ta.id
|
|
LEFT JOIN tar_atrasadas tar_atr ON tar_atr.tarea_id = ta.id
|
|
GROUP BY ta.contribuyente_id
|
|
)
|
|
SELECT
|
|
obl.contribuyente_id AS obl_id, obl.pendientes AS obl_pen, obl.atrasadas AS obl_atr, obl.completadas AS obl_com,
|
|
tar.contribuyente_id AS tar_id, tar.pendientes AS tar_pen, tar.atrasadas AS tar_atr, tar.completadas AS tar_com
|
|
FROM obl
|
|
FULL OUTER JOIN tar ON tar.contribuyente_id = obl.contribuyente_id`,
|
|
[periodoMes, inicioMes, finMes, ids],
|
|
);
|
|
|
|
const statsMap = new Map<string, {
|
|
obl: { pen: number; atr: number; com: number };
|
|
tar: { pen: number; atr: number; com: number };
|
|
}>();
|
|
for (const s of stats) {
|
|
const id = s.obl_id || s.tar_id;
|
|
if (!id) continue;
|
|
statsMap.set(id, {
|
|
obl: { pen: s.obl_pen ?? 0, atr: s.obl_atr ?? 0, com: s.obl_com ?? 0 },
|
|
tar: { pen: s.tar_pen ?? 0, atr: s.tar_atr ?? 0, com: s.tar_com ?? 0 },
|
|
});
|
|
}
|
|
|
|
const result = rows.map(r => {
|
|
const s = statsMap.get(r.contribuyente_id);
|
|
return {
|
|
contribuyenteId: r.contribuyente_id,
|
|
rfc: r.rfc,
|
|
nombre: r.nombre,
|
|
carteraNombre: r.cartera_nombre,
|
|
obligacionesPendientes: s?.obl.pen ?? 0,
|
|
obligacionesAtrasadas: s?.obl.atr ?? 0,
|
|
obligacionesCompletadas: s?.obl.com ?? 0,
|
|
tareasPendientes: s?.tar.pen ?? 0,
|
|
tareasAtrasadas: s?.tar.atr ?? 0,
|
|
tareasCompletadas: s?.tar.com ?? 0,
|
|
};
|
|
});
|
|
// Ordena por atrasos descendente — los más rezagados arriba.
|
|
result.sort((a, b) => {
|
|
const atrasoA = a.obligacionesAtrasadas + a.tareasAtrasadas;
|
|
const atrasoB = b.obligacionesAtrasadas + b.tareasAtrasadas;
|
|
if (atrasoA !== atrasoB) return atrasoB - atrasoA;
|
|
return a.nombre.localeCompare(b.nombre);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
export interface MiembroEquipo {
|
|
userId: string;
|
|
nombre: string;
|
|
email: string;
|
|
rol: 'supervisor' | 'auxiliar';
|
|
contribuyentes: number;
|
|
obligacionesAtrasadas: number;
|
|
tareasAtrasadas: number;
|
|
totalPendientes: number;
|
|
/** completadas + pendientes del periodo filtrado (sin atrasos). */
|
|
totalPeriodo: number;
|
|
completadasPeriodo: number;
|
|
avancePct: number | null;
|
|
}
|
|
|
|
export interface SupervisorConAuxiliares extends MiembroEquipo {
|
|
auxiliares: MiembroEquipo[];
|
|
}
|
|
|
|
export interface EquipoStatsResponse {
|
|
supervisores: SupervisorConAuxiliares[];
|
|
/** Auxiliares activos sin entrada en `auxiliar_supervisores`. Solo owner los ve. */
|
|
huerfanos: MiembroEquipo[];
|
|
}
|
|
|
|
/**
|
|
* Resumen de avance por miembro del equipo, en estructura jerárquica
|
|
* supervisor → auxiliares + lista de auxiliares "huérfanos" (sin supervisor).
|
|
*
|
|
* - **owner / cfo**: ve TODOS los supervisores + auxiliares sin supervisor.
|
|
* - **supervisor**: ve solo a sí mismo con sus auxiliares (sin huérfanos).
|
|
*
|
|
* Métricas calculadas por periodo (`año`/`mes` o mes en curso).
|
|
*/
|
|
export async function getEquipoStats(
|
|
pool: Pool,
|
|
userId: string,
|
|
userRole: string,
|
|
tenantId: string,
|
|
año?: number,
|
|
mes?: number,
|
|
): Promise<EquipoStatsResponse> {
|
|
// 1. Construir mapa supervisor → auxiliares.
|
|
//
|
|
// La relación se infiere desde `carteras`:
|
|
// - Si una cartera tiene `auxiliar_user_id` Y `supervisor_user_id` no nulos,
|
|
// ese par directo cuenta.
|
|
// - Si una subcartera (parent_id no nulo) tiene `auxiliar_user_id`, su
|
|
// supervisor es el `supervisor_user_id` del parent.
|
|
//
|
|
// Fallback: tabla legacy `auxiliar_supervisores`. La unión con DISTINCT
|
|
// evita duplicados si un auxiliar aparece en ambas fuentes.
|
|
const { rows: paresRows } = await pool.query<{ supervisor_user_id: string; auxiliar_user_id: string }>(
|
|
`SELECT DISTINCT supervisor_user_id, auxiliar_user_id FROM (
|
|
SELECT c.supervisor_user_id, c.auxiliar_user_id
|
|
FROM carteras c
|
|
WHERE c.auxiliar_user_id IS NOT NULL
|
|
AND c.supervisor_user_id IS NOT NULL
|
|
UNION
|
|
SELECT p.supervisor_user_id, sub.auxiliar_user_id
|
|
FROM carteras sub
|
|
JOIN carteras p ON p.id = sub.parent_id
|
|
WHERE sub.auxiliar_user_id IS NOT NULL
|
|
AND p.supervisor_user_id IS NOT NULL
|
|
UNION
|
|
SELECT supervisor_user_id, auxiliar_user_id FROM auxiliar_supervisores
|
|
) t WHERE supervisor_user_id IS NOT NULL AND auxiliar_user_id IS NOT NULL`,
|
|
);
|
|
|
|
let pares = paresRows.map(r => ({ supervisorId: r.supervisor_user_id, auxiliarId: r.auxiliar_user_id }));
|
|
if (userRole === 'supervisor') {
|
|
pares = pares.filter(p => p.supervisorId === userId);
|
|
} else if (userRole !== 'owner' && userRole !== 'cfo') {
|
|
return { supervisores: [], huerfanos: [] };
|
|
}
|
|
|
|
// 2. Agrupar auxiliares por supervisor.
|
|
const supervisorIds = [...new Set(pares.map(p => p.supervisorId))];
|
|
const auxiliaresPorSup = new Map<string, string[]>();
|
|
for (const p of pares) {
|
|
if (!auxiliaresPorSup.has(p.supervisorId)) auxiliaresPorSup.set(p.supervisorId, []);
|
|
auxiliaresPorSup.get(p.supervisorId)!.push(p.auxiliarId);
|
|
}
|
|
|
|
// 3. Para cada user (supervisor o auxiliar), calcular su miembro.
|
|
const result: SupervisorConAuxiliares[] = [];
|
|
for (const supId of supervisorIds) {
|
|
const supMiembro = await calcularMiembro(pool, supId, 'supervisor', año, mes);
|
|
if (!supMiembro) continue;
|
|
const auxiliares: MiembroEquipo[] = [];
|
|
for (const auxId of auxiliaresPorSup.get(supId) ?? []) {
|
|
const auxMiembro = await calcularMiembro(pool, auxId, 'auxiliar', año, mes);
|
|
if (auxMiembro) auxiliares.push(auxMiembro);
|
|
}
|
|
auxiliares.sort((a, b) => b.totalPendientes - a.totalPendientes);
|
|
result.push({ ...supMiembro, auxiliares });
|
|
}
|
|
|
|
result.sort((a, b) => b.totalPendientes - a.totalPendientes);
|
|
|
|
// 4. Auxiliares "huérfanos" (sin entrada en auxiliar_supervisores). Solo
|
|
// el owner los ve para que pueda asignarles supervisor.
|
|
let huerfanos: MiembroEquipo[] = [];
|
|
if (userRole === 'owner' || userRole === 'cfo') {
|
|
const auxiliaresMapeados = new Set(pares.map(p => p.auxiliarId));
|
|
const auxiliaresActivos = await prisma.tenantMembership.findMany({
|
|
where: { tenantId, active: true, rol: { nombre: 'auxiliar' } },
|
|
include: { user: { select: { id: true, active: true } } },
|
|
});
|
|
const huerfanosIds = auxiliaresActivos
|
|
.filter(m => m.user.active && !auxiliaresMapeados.has(m.userId))
|
|
.map(m => m.userId);
|
|
for (const auxId of huerfanosIds) {
|
|
const aux = await calcularMiembro(pool, auxId, 'auxiliar', año, mes);
|
|
if (aux) huerfanos.push(aux);
|
|
}
|
|
huerfanos.sort((a, b) => b.totalPendientes - a.totalPendientes);
|
|
}
|
|
|
|
return { supervisores: result, huerfanos };
|
|
}
|
|
|
|
async function calcularMiembro(
|
|
pool: Pool,
|
|
uId: string,
|
|
rol: 'supervisor' | 'auxiliar',
|
|
año?: number,
|
|
mes?: number,
|
|
): Promise<MiembroEquipo | null> {
|
|
const userInfo = await prisma.user.findUnique({
|
|
where: { id: uId },
|
|
select: { nombre: true, email: true, active: true },
|
|
});
|
|
if (!userInfo || !userInfo.active) return null;
|
|
|
|
const filter = rol === 'supervisor'
|
|
? `ce.cartera_id IN (SELECT id FROM carteras WHERE supervisor_user_id = $1
|
|
UNION SELECT id FROM carteras WHERE parent_id IN (SELECT id FROM carteras WHERE supervisor_user_id = $1))`
|
|
: `ce.cartera_id IN (SELECT id FROM carteras WHERE auxiliar_user_id = $1)`;
|
|
|
|
const { rows: contribRows } = await pool.query<{ entidad_id: string }>(
|
|
`SELECT DISTINCT ce.entidad_id FROM cartera_entidades ce WHERE ${filter}`,
|
|
[uId],
|
|
);
|
|
const contribIds = contribRows.map(r => r.entidad_id);
|
|
|
|
// Periodo pivote
|
|
const now = new Date();
|
|
const _año = año ?? now.getFullYear();
|
|
const _mes = mes ?? now.getMonth() + 1;
|
|
const periodoMes = `${_año}-${String(_mes).padStart(2, '0')}`;
|
|
const inicioMes = `${_año}-${String(_mes).padStart(2, '0')}-01`;
|
|
const finMes = new Date(_año, _mes, 0).toISOString().split('T')[0];
|
|
|
|
let obl = 0, tar = 0, total = 0, completadas = 0;
|
|
if (contribIds.length > 0) {
|
|
const { rows: [agg] } = await pool.query<{
|
|
obl_atr: number; tar_atr: number;
|
|
obl_pen: number; obl_com: number;
|
|
tar_pen: number; tar_com: number;
|
|
}>(
|
|
`SELECT
|
|
(SELECT COUNT(*)::int FROM obligacion_periodos op
|
|
JOIN obligaciones_contribuyente oc ON oc.id = op.obligacion_id
|
|
WHERE oc.contribuyente_id = ANY($1::uuid[])
|
|
AND oc.activa = true AND op.completada = false AND op.periodo < $2) AS obl_atr,
|
|
(SELECT COUNT(*)::int FROM tarea_periodos tp
|
|
JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
|
|
WHERE tc.contribuyente_id = ANY($1::uuid[])
|
|
AND tc.active = true AND tp.completada = false AND tp.fecha_limite < $3::date) AS tar_atr,
|
|
(SELECT COUNT(*)::int FROM obligacion_periodos op
|
|
JOIN obligaciones_contribuyente oc ON oc.id = op.obligacion_id
|
|
WHERE oc.contribuyente_id = ANY($1::uuid[])
|
|
AND oc.activa = true AND op.periodo = $2 AND op.completada = false) AS obl_pen,
|
|
(SELECT COUNT(*)::int FROM obligacion_periodos op
|
|
JOIN obligaciones_contribuyente oc ON oc.id = op.obligacion_id
|
|
WHERE oc.contribuyente_id = ANY($1::uuid[])
|
|
AND oc.activa = true AND op.periodo = $2 AND op.completada = true) AS obl_com,
|
|
(SELECT COUNT(*)::int FROM tarea_periodos tp
|
|
JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
|
|
WHERE tc.contribuyente_id = ANY($1::uuid[])
|
|
AND tc.active = true AND tp.fecha_limite BETWEEN $3::date AND $4::date AND tp.completada = false) AS tar_pen,
|
|
(SELECT COUNT(*)::int FROM tarea_periodos tp
|
|
JOIN tareas_catalogo tc ON tc.id = tp.tarea_id
|
|
WHERE tc.contribuyente_id = ANY($1::uuid[])
|
|
AND tc.active = true AND tp.fecha_limite BETWEEN $3::date AND $4::date AND tp.completada = true) AS tar_com`,
|
|
[contribIds, periodoMes, inicioMes, finMes],
|
|
);
|
|
obl = agg.obl_atr;
|
|
tar = agg.tar_atr;
|
|
completadas = agg.obl_com + agg.tar_com;
|
|
total = completadas + agg.obl_pen + agg.tar_pen;
|
|
}
|
|
|
|
return {
|
|
userId: uId,
|
|
nombre: userInfo.nombre,
|
|
email: userInfo.email,
|
|
rol,
|
|
contribuyentes: contribIds.length,
|
|
obligacionesAtrasadas: obl,
|
|
tareasAtrasadas: tar,
|
|
totalPendientes: obl + tar,
|
|
totalPeriodo: total,
|
|
completadasPeriodo: completadas,
|
|
avancePct: total > 0 ? Math.round((completadas / total) * 100) : null,
|
|
};
|
|
}
|