panels-origin/web-panel/composables/iamPermissionMatrix.ts
Alberto Martinez 90bcd48f95 Facturas, nómina, dispersión e IMSS (#28)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Resumen

Implementa las observaciones de facturas, nómina e IMSS sobre el catálogo de proveedores y la nómina semanal que ya existían.

## Qué queda disponible

- Proveedores: régimen fiscal, código postal, estatus activo/inactivo y filtro por nombre o RFC.
- Facturas recibidas y emitidas: Excel, UUID, revisión si el RFC no está en el catálogo, clasificación a obra/concepto y gasto `source = invoice` solo para la recibida clasificada.
- Nómina: bono, descuento con observación, horas y día festivo dentro del armado semanal. La semana pagada no acepta cambios.
- Préstamos: edición (libre sin pagos; con pagos solo nota y cuota) y estado de cuenta con saldo = entregado + comisión − pagos.
- Finiquitos y liquidaciones: conceptos, neto descontando descuentos vigentes y saldo de préstamo, y gasto al pagar.
- Dispersión: corte semanal, corrección antes de definitiva y comprobante.
- Historial de alta, baja y reingreso en la ficha, más movimientos IMSS.
- Reporte por periodo en Excel y PDF.
- Jornal IMSS distinto del jornal real y consulta de cuotas exportable a Excel.

## Verificación

`deno check` de la API pasó. En este entorno no hay Postgres, así que `api/pendientes_test.ts` queda listo para correr cuando la base tenga las migraciones 042–050 y el permiso IAM 007.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-5e23dc3b-921e-4091-9650-7091d3a52bbf?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-5e23dc3b-921e-4091-9650-7091d3a52bbf&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>
2026-09-25 03:26:30 +00:00

299 lines
8.6 KiB
TypeScript

export type IamPermRow = {
code: string;
label: string;
module?: string;
verb?: string;
group?: string;
};
export type IamVerb = "view" | "create" | "update" | "delete";
export type IamMatrixCell = {
code: string;
label: string;
hint: string;
};
export type IamMatrixRow = {
key: string;
label: string;
hint?: string;
cells: Partial<Record<IamVerb, IamMatrixCell>>;
};
export type IamMatrixModule = {
key: string;
label: string;
description: string;
rows: IamMatrixRow[];
codes: string[];
verbs: IamVerb[];
};
const VERB_ORDER: IamVerb[] = ["view", "create", "update", "delete"];
export const IAM_VERB_LABELS: Record<IamVerb, string> = {
view: "Ver",
create: "Crear",
update: "Editar",
delete: "Eliminar",
};
export const IAM_VERB_HINTS: Record<IamVerb, string> = {
view: "Consultar y abrir pantallas",
create: "Registrar datos nuevos",
update: "Modificar lo existente",
delete: "Eliminar o dar de baja",
};
const MODULE_META: Record<string, { label: string; description: string }> = {
workers: {
label: "Personal",
description: "Padrón de trabajadores, expediente y tablero kanban de altas.",
},
kanban: {
label: "Kanban",
description: "Tablero visual de seguimiento de personal.",
},
projects: {
label: "Obras",
description: "Alta, consulta y edición de proyectos/obras del tenant.",
},
companies: {
label: "Empresas",
description: "Empresas contratistas y subcontratos asociados a obras.",
},
budget: {
label: "Presupuesto",
description: "Partidas, importes y estructura económica de cada obra.",
},
work_program: {
label: "Programa de obra",
description: "Cronograma, avance físico e importación del programa.",
},
cost_control: {
label: "Control de costos",
description: "Comparativo programado vs real y tableros de costo.",
},
invoices: {
label: "Facturas",
description: "Comprobantes recibidos y emitidos, clasificación a la obra.",
},
expenses: {
label: "Gastos",
description: "Captura y seguimiento de egresos por obra.",
},
warehouse: {
label: "Almacén",
description: "Entradas, salidas, stock y transferencias de materiales.",
},
payroll: {
label: "Nómina",
description: "Periodos, pagos, préstamos y líneas de nómina.",
},
documents: {
label: "Documentos y gafetes",
description: "Expediente documental de trabajadores y credenciales.",
},
project_docs: {
label: "Documentos de obra",
description: "Expediente por categoría: planos, contratos, permisos, etc.",
},
users: {
label: "Usuarios IAM",
description: "Administración de cuentas, roles y accesos del tenant.",
},
settings: {
label: "Configuración",
description: "Parámetros generales del tenant (solo propietario).",
},
reports: {
label: "Reportes e inicio",
description: "Tablero inicial y reportes operativos.",
},
};
const PROJECT_DOC_GROUP_LABELS: Record<string, string> = {
tecnico: "Planos y técnicos",
contrato: "Contrato",
permisos: "Permisos",
ambiental: "Ambiental",
imss: "IMSS",
sst: "SST",
otro: "Otros",
};
const MODULE_ORDER = [
"reports",
"projects",
"work_program",
"budget",
"cost_control",
"invoices",
"expenses",
"workers",
"kanban",
"documents",
"project_docs",
"warehouse",
"payroll",
"companies",
"users",
"settings",
];
function verbHint(module: string, verb: IamVerb, label: string): string {
if (module === "project_docs" && verb === "create") return "Subir archivos en esta categoría";
if (module === "warehouse" && verb === "update") return "Salidas, transferencias y cierres";
if (module === "payroll" && verb === "update") return "Armar periodo y registrar pagos";
return label;
}
export function buildIamMatrixModules(catalog: IamPermRow[]): IamMatrixModule[] {
const byModule = new Map<string, IamPermRow[]>();
for (const p of catalog) {
if (!p.module || !p.verb) continue;
const list = byModule.get(p.module) ?? [];
list.push(p);
byModule.set(p.module, list);
}
const modules: IamMatrixModule[] = [];
for (const moduleKey of MODULE_ORDER) {
const perms = byModule.get(moduleKey);
if (!perms?.length) continue;
const meta = MODULE_META[moduleKey] ?? {
label: moduleKey.replace(/_/g, " "),
description: "Permisos del módulo.",
};
const verbs = VERB_ORDER.filter((v) => perms.some((p) => p.verb === v));
const rows: IamMatrixRow[] = [];
if (moduleKey === "project_docs") {
const groups = new Map<string, IamPermRow[]>();
for (const p of perms) {
const g = p.group ?? "otro";
const list = groups.get(g) ?? [];
list.push(p);
groups.set(g, list);
}
for (const [groupKey, groupPerms] of groups) {
const cells: Partial<Record<IamVerb, IamMatrixCell>> = {};
for (const p of groupPerms) {
const verb = p.verb as IamVerb;
cells[verb] = {
code: p.code,
label: p.label,
hint: verbHint(moduleKey, verb, p.label),
};
}
rows.push({
key: groupKey,
label: PROJECT_DOC_GROUP_LABELS[groupKey] ?? groupKey,
cells,
});
}
} else {
const cells: Partial<Record<IamVerb, IamMatrixCell>> = {};
for (const p of perms) {
const verb = p.verb as IamVerb;
cells[verb] = {
code: p.code,
label: p.label,
hint: verbHint(moduleKey, verb, p.label),
};
}
rows.push({ key: moduleKey, label: meta.label, hint: meta.description, cells });
}
modules.push({
key: moduleKey,
label: meta.label,
description: meta.description,
rows,
codes: perms.map((p) => p.code),
verbs,
});
}
return modules;
}
export function filterAssignableCatalog(catalog: IamPermRow[], isOwner: boolean): IamPermRow[] {
if (isOwner) return catalog;
return catalog.filter(
(p) => !p.code.startsWith("users.") && !p.code.startsWith("settings."),
);
}
export function toggleCodes(selected: string[], codes: string[], on: boolean): string[] {
const set = new Set(selected);
for (const code of codes) {
if (on) set.add(code);
else set.delete(code);
}
return [...set];
}
export function moduleSelectionState(selected: Set<string>, codes: string[]): "none" | "partial" | "all" {
const hit = codes.filter((c) => selected.has(c)).length;
if (hit === 0) return "none";
if (hit === codes.length) return "all";
return "partial";
}
export function viewOnlyCodes(codes: string[], catalog: IamPermRow[]): string[] {
const viewSet = new Set(catalog.filter((p) => p.verb === "view").map((p) => p.code));
return codes.filter((c) => viewSet.has(c));
}
/** Conjuntos predefinidos (mismas claves que api/iam_catalog.ts ROLE_TEMPLATES). */
export const IAM_ROLE_TEMPLATES: Record<string, { label: string; permissions: string[] }> = {
admin_operativo: {
label: "Administrador operativo",
permissions: [
"workers.view", "workers.create", "workers.update", "workers.delete", "kanban.view",
"projects.view", "projects.create", "projects.update",
"companies.view", "companies.create", "companies.update",
"budget.view", "budget.create", "budget.update", "budget.delete",
"work_program.view", "work_program.create", "work_program.update",
"cost_control.view",
"expenses.view", "expenses.create", "expenses.update", "expenses.delete",
"warehouse.view", "warehouse.create", "warehouse.update",
"payroll.view", "payroll.create", "payroll.update", "payroll.delete",
"documents.view", "documents.create",
"project_docs.tecnico.view", "project_docs.tecnico.create",
"project_docs.contrato.view", "project_docs.contrato.create",
"project_docs.permisos.view", "project_docs.permisos.create",
"project_docs.ambiental.view", "project_docs.ambiental.create",
"project_docs.imss.view", "project_docs.imss.create",
"project_docs.sst.view", "project_docs.sst.create",
"project_docs.otro.view", "project_docs.otro.create",
"reports.view",
],
},
supervisor_obra: {
label: "Supervisor de obra",
permissions: [
"projects.view", "work_program.view", "work_program.update",
"project_docs.tecnico.view", "reports.view",
],
},
contador: {
label: "Contador",
permissions: [
"expenses.view", "expenses.create", "expenses.update", "expenses.delete",
"cost_control.view", "reports.view", "payroll.view",
],
},
operativo: {
label: "Operativo",
permissions: [
"workers.view", "workers.create", "workers.update",
"documents.view", "documents.create", "reports.view", "kanban.view",
],
},
};