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
-
-
-
-
-
-
-
+
+
+ Marque columnas Ver / Crear / Editar / Eliminar por módulo. Pase el cursor sobre cada casilla para ver qué habilita.
+
+
+
+
+
+
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",
+ ],
+ },
+};