Files
HoruxDespachosNuevo/apps/api/src/services/cartera.service.ts
Horux Dev f43cb165c6 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
2026-05-23 23:40:12 +00:00

172 lines
6.7 KiB
TypeScript

import type { Pool } from 'pg';
import { prisma } from '../config/database.js';
export interface CarteraRow {
id: string;
supervisorUserId: string | null;
auxiliarUserId: string | null;
parentId: string | null;
nombre: string;
descripcion: string | null;
createdAt: string;
entidadesCount?: number;
subcarterasCount?: number;
}
const BASE_SELECT = `
SELECT c.id, c.supervisor_user_id AS "supervisorUserId",
c.auxiliar_user_id AS "auxiliarUserId", c.parent_id AS "parentId",
c.nombre, c.descripcion, c.created_at AS "createdAt",
(SELECT count(*) FROM cartera_entidades ce WHERE ce.cartera_id = c.id)::int AS "entidadesCount",
(SELECT count(*) FROM carteras sc WHERE sc.parent_id = c.id)::int AS "subcarterasCount"
FROM carteras c
`;
/**
* List top-level carteras (parent_id IS NULL).
* If supervisorUserId is provided, filter by that supervisor.
*/
export async function listCarteras(pool: Pool, supervisorUserId?: string): Promise<CarteraRow[]> {
const conditions = ['c.parent_id IS NULL'];
const params: unknown[] = [];
if (supervisorUserId) {
params.push(supervisorUserId);
conditions.push(`c.supervisor_user_id = $${params.length}`);
}
const { rows } = await pool.query(
`${BASE_SELECT} WHERE ${conditions.join(' AND ')} ORDER BY c.created_at DESC`,
params,
);
return rows;
}
/**
* List subcarteras of a parent cartera.
*/
export async function listSubcarteras(pool: Pool, parentId: string): Promise<CarteraRow[]> {
const { rows } = await pool.query(
`${BASE_SELECT} WHERE c.parent_id = $1 ORDER BY c.nombre`,
[parentId],
);
return rows;
}
export async function getCarteraById(pool: Pool, id: string): Promise<CarteraRow | null> {
const { rows } = await pool.query(`${BASE_SELECT} WHERE c.id = $1`, [id]);
return rows[0] ?? null;
}
export async function createCartera(pool: Pool, data: {
supervisorUserId: string;
nombre: string;
descripcion?: string;
}): Promise<CarteraRow> {
const { rows: [row] } = await pool.query(`
INSERT INTO carteras (supervisor_user_id, nombre, descripcion)
VALUES ($1, $2, $3) RETURNING id
`, [data.supervisorUserId, data.nombre, data.descripcion ?? null]);
return (await getCarteraById(pool, row.id))!;
}
/**
* Create a subcartera within a parent cartera, assigned to an auxiliar.
*/
export async function createSubcartera(pool: Pool, data: {
parentId: string;
auxiliarUserId: string;
nombre: string;
descripcion?: string;
}): Promise<CarteraRow> {
const { rows: [row] } = await pool.query(`
INSERT INTO carteras (parent_id, auxiliar_user_id, nombre, descripcion)
VALUES ($1, $2, $3, $4) RETURNING id
`, [data.parentId, data.auxiliarUserId, data.nombre, data.descripcion ?? null]);
return (await getCarteraById(pool, row.id))!;
}
export async function updateCartera(pool: Pool, id: string, data: {
nombre?: string;
descripcion?: string;
supervisorUserId?: string;
}): Promise<CarteraRow | null> {
const existing = await getCarteraById(pool, id);
if (!existing) return null;
const sets: string[] = [];
const vals: unknown[] = [];
let idx = 1;
if (data.nombre !== undefined) { sets.push(`nombre = $${idx}`); vals.push(data.nombre); idx++; }
if (data.descripcion !== undefined) { sets.push(`descripcion = $${idx}`); vals.push(data.descripcion); idx++; }
if (data.supervisorUserId !== undefined) { sets.push(`supervisor_user_id = $${idx}`); vals.push(data.supervisorUserId); idx++; }
if (sets.length === 0) return existing;
vals.push(id);
await pool.query(`UPDATE carteras SET ${sets.join(', ')} WHERE id = $${idx}`, vals);
return (await getCarteraById(pool, id))!;
}
export async function deleteCartera(pool: Pool, id: string): Promise<boolean> {
const { rowCount } = await pool.query('DELETE FROM carteras WHERE id = $1', [id]);
return (rowCount ?? 0) > 0;
}
// Entidades in cartera
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]);
}
export async function removeEntidadFromCartera(pool: Pool, carteraId: string, entidadId: string): Promise<void> {
await pool.query('DELETE FROM cartera_entidades WHERE cartera_id = $1 AND entidad_id = $2', [carteraId, entidadId]);
}
export async function getCarteraEntidades(pool: Pool, carteraId: string): Promise<string[]> {
const { rows } = await pool.query('SELECT entidad_id AS "entidadId" FROM cartera_entidades WHERE cartera_id = $1', [carteraId]);
return rows.map(r => r.entidadId);
}
// Auxiliares assigned to a supervisor
export async function getAuxiliaresDelSupervisor(pool: Pool, supervisorUserId: string): Promise<Array<{ auxiliarUserId: string }>> {
const { rows } = await pool.query(
'SELECT auxiliar_user_id AS "auxiliarUserId" FROM auxiliar_supervisores WHERE supervisor_user_id = $1',
[supervisorUserId],
);
return rows;
}
// Legacy auxiliares in cartera (backward compat)
export async function addAuxiliarToCartera(pool: Pool, carteraId: string, auxiliarUserId: string): Promise<void> {
await pool.query('INSERT INTO cartera_auxiliares (cartera_id, auxiliar_user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING', [carteraId, auxiliarUserId]);
}
export async function removeAuxiliarFromCartera(pool: Pool, carteraId: string, auxiliarUserId: string): Promise<void> {
await pool.query('DELETE FROM cartera_auxiliares WHERE cartera_id = $1 AND auxiliar_user_id = $2', [carteraId, auxiliarUserId]);
}
export async function getCarteraAuxiliares(pool: Pool, carteraId: string): Promise<string[]> {
const { rows } = await pool.query('SELECT auxiliar_user_id AS "auxiliarUserId" FROM cartera_auxiliares WHERE cartera_id = $1', [carteraId]);
return rows.map(r => r.auxiliarUserId);
}
// Supervisors list (for the invite form dropdown)
export async function getSupervisores(pool: Pool, tenantId: string): Promise<Array<{ userId: string; nombre: string; email: string }>> {
// Query tenant_memberships joined with users for supervisor role (rolId=9)
const memberships = await prisma.tenantMembership.findMany({
where: { tenantId, rolId: 9, active: true },
include: { user: { select: { id: true, nombre: true, email: true } } },
});
return memberships.map(m => ({
userId: m.user.id,
nombre: m.user.nombre,
email: m.user.email,
}));
}