Initial commit - Horux Despachos NL
This commit is contained in:
487
apps/api/src/services/despacho-stats.service.ts
Normal file
487
apps/api/src/services/despacho-stats.service.ts
Normal file
@@ -0,0 +1,487 @@
|
||||
import type { Pool } from 'pg';
|
||||
import { prisma } from '../config/database.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];
|
||||
|
||||
const { rows: stats } = await pool.query(
|
||||
`WITH obl AS (
|
||||
SELECT oc.contribuyente_id,
|
||||
COUNT(*) FILTER (WHERE op.completada = false AND op.periodo = $1)::int AS pendientes,
|
||||
COUNT(*) FILTER (WHERE op.completada = false AND op.periodo < $1)::int AS atrasadas,
|
||||
COUNT(*) FILTER (WHERE op.completada = true AND op.periodo = $1)::int AS completadas
|
||||
FROM obligaciones_contribuyente oc
|
||||
LEFT JOIN obligacion_periodos op ON op.obligacion_id = oc.id
|
||||
WHERE oc.contribuyente_id = ANY($4::uuid[]) AND oc.activa = true
|
||||
GROUP BY oc.contribuyente_id
|
||||
),
|
||||
tar AS (
|
||||
SELECT tc.contribuyente_id,
|
||||
COUNT(*) FILTER (WHERE tp.completada = false AND tp.fecha_limite BETWEEN $2::date AND $3::date)::int AS pendientes,
|
||||
COUNT(*) FILTER (WHERE tp.completada = false AND tp.fecha_limite < $2::date)::int AS atrasadas,
|
||||
COUNT(*) FILTER (WHERE tp.completada = true AND tp.fecha_limite BETWEEN $2::date AND $3::date)::int AS completadas
|
||||
FROM tareas_catalogo tc
|
||||
LEFT JOIN tarea_periodos tp ON tp.tarea_id = tc.id
|
||||
WHERE tc.contribuyente_id = ANY($4::uuid[]) AND tc.active = true
|
||||
GROUP BY tc.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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user