mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 10:43:18 +00:00
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 <alberto.martinez@mrdev.mx>
This commit is contained in:
parent
2f7f121121
commit
1a50823368
3 changed files with 621 additions and 28 deletions
|
|
@ -17,34 +17,37 @@
|
|||
|
||||
<div v-else-if="detail.kind === 'iam-role' && detail.iamRole" class="iam-detail">
|
||||
<div class="section-title">Matriz del rol</div>
|
||||
<p><strong>{{ detail.iamRole.label }}</strong> ({{ detail.iamRole.code }})</p>
|
||||
<p class="role-heading"><strong>{{ detail.iamRole.label }}</strong> ({{ detail.iamRole.code }})</p>
|
||||
<Message v-if="detail.iamRole.is_system" severity="info" :closable="false">Rol de sistema: no editable</Message>
|
||||
<div v-else class="perm-grid mt-3">
|
||||
<div v-for="perm in groupedPermissions" :key="perm.code" class="perm-item">
|
||||
<Checkbox
|
||||
v-model="localPerms"
|
||||
:input-id="perm.code"
|
||||
:value="perm.code"
|
||||
:disabled="!canEdit"
|
||||
/>
|
||||
<label :for="perm.code">{{ perm.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="canEdit"
|
||||
label="Guardar permisos"
|
||||
icon="pi pi-save"
|
||||
class="mt-3 w-full"
|
||||
size="small"
|
||||
@click="savePerms"
|
||||
/>
|
||||
<template v-else>
|
||||
<p class="matrix-intro">
|
||||
Marque columnas <strong>Ver / Crear / Editar / Eliminar</strong> por módulo. Pase el cursor sobre cada casilla para ver qué habilita.
|
||||
</p>
|
||||
<IamPermissionMatrix
|
||||
v-model="localPerms"
|
||||
:catalog="assignableCatalog"
|
||||
:templates="roleTemplates"
|
||||
:disabled="!canEdit"
|
||||
/>
|
||||
<Button
|
||||
v-if="canEdit"
|
||||
label="Guardar permisos"
|
||||
icon="pi pi-save"
|
||||
class="mt-3 w-full"
|
||||
size="small"
|
||||
@click="savePerms"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { displayErrorMessage } from "~/composables/api-response";
|
||||
|
||||
type PermRow = { code: string; label: string; module?: string; group?: string };
|
||||
import {
|
||||
filterAssignableCatalog,
|
||||
IAM_ROLE_TEMPLATES,
|
||||
type IamPermRow,
|
||||
} from "~/composables/iamPermissionMatrix";
|
||||
|
||||
const props = defineProps<{
|
||||
detail: {
|
||||
|
|
@ -57,18 +60,32 @@ const props = defineProps<{
|
|||
const { api } = useApi();
|
||||
const toast = useToast();
|
||||
const perms = usePermissions();
|
||||
const catalog = ref<PermRow[]>([]);
|
||||
const catalog = ref<IamPermRow[]>([]);
|
||||
const localPerms = ref<string[]>([]);
|
||||
|
||||
const canEdit = computed(() =>
|
||||
perms.can("users.update") && props.detail.kind === "iam-role" && !props.detail.iamRole?.is_system,
|
||||
);
|
||||
|
||||
const groupedPermissions = computed(() => catalog.value);
|
||||
const assignableCatalog = computed(() => filterAssignableCatalog(catalog.value, perms.isOwner.value));
|
||||
|
||||
const roleTemplates = computed(() => {
|
||||
if (!perms.isOwner.value) {
|
||||
const { admin_operativo: _a, ...rest } = IAM_ROLE_TEMPLATES;
|
||||
return rest;
|
||||
}
|
||||
return IAM_ROLE_TEMPLATES;
|
||||
});
|
||||
|
||||
async function loadCatalog() {
|
||||
const r = await api<{ permissions?: PermRow[] }>("/v1/iam/permissions");
|
||||
catalog.value = r.permissions ?? [];
|
||||
const r = await api<{ permissions?: IamPermRow[] }>("/v1/iam/permissions");
|
||||
catalog.value = (r.permissions ?? []).map((p) => ({
|
||||
code: p.code,
|
||||
label: p.label,
|
||||
module: p.module,
|
||||
verb: p.verb,
|
||||
group: (p as { group?: string }).group,
|
||||
}));
|
||||
}
|
||||
|
||||
async function loadRolePerms(roleId: number) {
|
||||
|
|
@ -103,8 +120,13 @@ watch(
|
|||
<style scoped>
|
||||
.iam-detail { font-size: 13px; }
|
||||
.section-title { font-weight: 600; margin-bottom: 4px; }
|
||||
.role-heading { margin: 0 0 8px; }
|
||||
.matrix-intro {
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-color-secondary);
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.muted { color: var(--text-color-secondary); }
|
||||
.owner-badge { color: var(--orange-500); font-weight: 600; }
|
||||
.perm-grid { display: grid; grid-template-columns: 1fr; gap: 6px; max-height: 50vh; overflow: auto; }
|
||||
.perm-item { display: flex; align-items: flex-start; gap: 8px; }
|
||||
</style>
|
||||
|
|
|
|||
277
web-panel/components/IamPermissionMatrix.vue
Normal file
277
web-panel/components/IamPermissionMatrix.vue
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
<template>
|
||||
<div class="iam-perm-matrix">
|
||||
<div class="matrix-toolbar">
|
||||
<IconField class="matrix-search">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="search" placeholder="Buscar módulo o permiso" size="small" class="w-full" />
|
||||
</IconField>
|
||||
<div class="matrix-quick">
|
||||
<Button label="Solo ver" size="small" text :disabled="disabled" @click="applyViewOnly" />
|
||||
<Button label="Limpiar" size="small" text severity="secondary" :disabled="disabled" @click="clearAll" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="templateOptions.length" class="matrix-templates">
|
||||
<span class="templates-label">Plantilla rápida</span>
|
||||
<Select
|
||||
v-model="pickedTemplate"
|
||||
:options="templateOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
placeholder="Aplicar conjunto…"
|
||||
size="small"
|
||||
class="w-full"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="matrix-scroll">
|
||||
<section
|
||||
v-for="mod in filteredModules"
|
||||
:key="mod.key"
|
||||
class="matrix-module"
|
||||
>
|
||||
<div class="module-head">
|
||||
<div class="module-title-row">
|
||||
<Checkbox
|
||||
:model-value="moduleChecked(mod) === 'all'"
|
||||
:indeterminate="moduleChecked(mod) === 'partial'"
|
||||
binary
|
||||
:disabled="disabled"
|
||||
@update:model-value="(v: boolean) => toggleModule(mod, v)"
|
||||
/>
|
||||
<span class="module-title">{{ mod.label }}</span>
|
||||
</div>
|
||||
<p class="module-desc">{{ mod.description }}</p>
|
||||
<div class="module-actions">
|
||||
<Button label="Todo" size="small" text :disabled="disabled" @click="toggleModule(mod, true)" />
|
||||
<Button label="Solo ver" size="small" text :disabled="disabled" @click="toggleModuleViewOnly(mod)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="matrix-table-wrap">
|
||||
<table class="matrix-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-area">Área</th>
|
||||
<th v-for="verb in mod.verbs" :key="verb" class="col-verb" :title="IAM_VERB_HINTS[verb]">
|
||||
{{ IAM_VERB_LABELS[verb] }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in mod.rows" :key="row.key">
|
||||
<td class="col-area">
|
||||
<span class="row-label">{{ row.label }}</span>
|
||||
<span v-if="row.hint && mod.rows.length > 1" class="row-hint">{{ row.hint }}</span>
|
||||
</td>
|
||||
<td v-for="verb in mod.verbs" :key="verb" class="col-verb">
|
||||
<Checkbox
|
||||
v-if="row.cells[verb]"
|
||||
v-model="model"
|
||||
:value="row.cells[verb]!.code"
|
||||
:disabled="disabled"
|
||||
v-tooltip.top="row.cells[verb]!.hint"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p v-if="!filteredModules.length" class="matrix-empty">Sin permisos que coincidan con la búsqueda.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
buildIamMatrixModules,
|
||||
IAM_VERB_HINTS,
|
||||
IAM_VERB_LABELS,
|
||||
moduleSelectionState,
|
||||
toggleCodes,
|
||||
viewOnlyCodes,
|
||||
type IamMatrixModule,
|
||||
type IamPermRow,
|
||||
} from "~/composables/iamPermissionMatrix";
|
||||
|
||||
const props = defineProps<{
|
||||
catalog: IamPermRow[];
|
||||
disabled?: boolean;
|
||||
templates?: Record<string, { label: string; permissions: string[] }>;
|
||||
}>();
|
||||
|
||||
const model = defineModel<string[]>({ required: true });
|
||||
|
||||
const search = ref("");
|
||||
const pickedTemplate = ref<string | null>(null);
|
||||
|
||||
const modules = computed(() => buildIamMatrixModules(props.catalog));
|
||||
|
||||
const templateOptions = computed(() =>
|
||||
Object.entries(props.templates ?? {}).map(([value, t]) => ({ value, label: t.label })),
|
||||
);
|
||||
|
||||
const filteredModules = computed(() => {
|
||||
const q = search.value.trim().toLowerCase();
|
||||
if (!q) return modules.value;
|
||||
return modules.value
|
||||
.map((mod) => {
|
||||
const haystack = [
|
||||
mod.label,
|
||||
mod.description,
|
||||
...mod.rows.flatMap((r) => [r.label, r.hint ?? "", ...Object.values(r.cells).map((c) => c?.label ?? "")]),
|
||||
].join(" ").toLowerCase();
|
||||
if (haystack.includes(q)) return mod;
|
||||
const rows = mod.rows.filter((row) => {
|
||||
const rowText = [row.label, row.hint ?? "", ...Object.values(row.cells).map((c) => c?.label ?? "")].join(" ").toLowerCase();
|
||||
return rowText.includes(q);
|
||||
});
|
||||
if (!rows.length) return null;
|
||||
return { ...mod, rows };
|
||||
})
|
||||
.filter(Boolean) as IamMatrixModule[];
|
||||
});
|
||||
|
||||
const selectedSet = computed(() => new Set(model.value));
|
||||
|
||||
function moduleChecked(mod: IamMatrixModule): "none" | "partial" | "all" {
|
||||
return moduleSelectionState(selectedSet.value, mod.codes);
|
||||
}
|
||||
|
||||
function toggleModule(mod: IamMatrixModule, on: boolean) {
|
||||
model.value = toggleCodes(model.value, mod.codes, on);
|
||||
}
|
||||
|
||||
function toggleModuleViewOnly(mod: IamMatrixModule) {
|
||||
model.value = toggleCodes(
|
||||
toggleCodes(model.value, mod.codes, false),
|
||||
viewOnlyCodes(mod.codes, props.catalog),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
function applyViewOnly() {
|
||||
const allCodes = modules.value.flatMap((m) => m.codes);
|
||||
model.value = viewOnlyCodes(allCodes, props.catalog);
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
model.value = [];
|
||||
}
|
||||
|
||||
function applyTemplate(key: string | null) {
|
||||
if (!key || !props.templates?.[key]) return;
|
||||
const allowed = new Set(props.catalog.map((p) => p.code));
|
||||
model.value = props.templates[key].permissions.filter((c) => allowed.has(c));
|
||||
pickedTemplate.value = null;
|
||||
}
|
||||
|
||||
watch(pickedTemplate, (key) => {
|
||||
if (key) applyTemplate(key);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iam-perm-matrix {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
}
|
||||
.matrix-toolbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.matrix-search { width: 100%; }
|
||||
.matrix-quick {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.matrix-templates {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.templates-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.matrix-scroll {
|
||||
max-height: 52vh;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
.matrix-module {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
background: var(--surface-card);
|
||||
}
|
||||
.module-head {
|
||||
padding: 8px 10px;
|
||||
background: var(--surface-50);
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
}
|
||||
.module-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.module-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
.module-desc {
|
||||
margin: 4px 0 6px 28px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.module-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 28px;
|
||||
}
|
||||
.matrix-table-wrap { overflow-x: auto; }
|
||||
.matrix-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
.matrix-table th,
|
||||
.matrix-table td {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.matrix-table th {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-color-secondary);
|
||||
background: var(--surface-ground);
|
||||
}
|
||||
.col-area { min-width: 7rem; text-align: left; }
|
||||
.col-verb { width: 2.5rem; text-align: center; }
|
||||
.row-label { display: block; font-weight: 500; }
|
||||
.row-hint {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: var(--text-color-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.matrix-empty {
|
||||
font-size: 12px;
|
||||
color: var(--text-color-secondary);
|
||||
text-align: center;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
</style>
|
||||
294
web-panel/composables/iamPermissionMatrix.ts
Normal file
294
web-panel/composables/iamPermissionMatrix.ts
Normal file
|
|
@ -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<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.",
|
||||
},
|
||||
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",
|
||||
"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",
|
||||
],
|
||||
},
|
||||
};
|
||||
Loading…
Reference in a new issue