- 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
172 lines
6.7 KiB
TypeScript
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,
|
|
}));
|
|
}
|