From 1a508233682bdc9c36e1552183165e79d8fed977 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 19:11:14 +0000 Subject: [PATCH] IAM: grouped CRUD permission matrix with hints and bulk actions Replace the flat checkbox list with modules grouped in a Ver/Crear/Editar/ Eliminar grid, module descriptions, search, view-only shortcuts, and quick template apply. Co-authored-by: alberto.martinez --- web-panel/components/IamDetail.vue | 78 +++-- web-panel/components/IamPermissionMatrix.vue | 277 +++++++++++++++++ web-panel/composables/iamPermissionMatrix.ts | 294 +++++++++++++++++++ 3 files changed, 621 insertions(+), 28 deletions(-) create mode 100644 web-panel/components/IamPermissionMatrix.vue create mode 100644 web-panel/composables/iamPermissionMatrix.ts diff --git a/web-panel/components/IamDetail.vue b/web-panel/components/IamDetail.vue index 4a38178..8ea5106 100644 --- a/web-panel/components/IamDetail.vue +++ b/web-panel/components/IamDetail.vue @@ -17,34 +17,37 @@
Matriz del rol
-

{{ detail.iamRole.label }} ({{ detail.iamRole.code }})

+

{{ detail.iamRole.label }} ({{ detail.iamRole.code }})

Rol de sistema: no editable -
-
- - -
-
-
+ + diff --git a/web-panel/composables/iamPermissionMatrix.ts b/web-panel/composables/iamPermissionMatrix.ts new file mode 100644 index 0000000..642ad08 --- /dev/null +++ b/web-panel/composables/iamPermissionMatrix.ts @@ -0,0 +1,294 @@ +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>; +}; + +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 = { + view: "Ver", + create: "Crear", + update: "Editar", + delete: "Eliminar", +}; + +export const IAM_VERB_HINTS: Record = { + view: "Consultar y abrir pantallas", + create: "Registrar datos nuevos", + update: "Modificar lo existente", + delete: "Eliminar o dar de baja", +}; + +const MODULE_META: Record = { + 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.", + }, + 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 = { + 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", + "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(); + 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(); + 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> = {}; + 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> = {}; + 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, 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 = { + 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", + ], + }, +};