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.", }, 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 = { 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(); 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", ], }, };