panels-origin/web-panel/components/IamDetail.vue
Cursor Agent 1a50823368
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>
2026-09-08 19:11:14 +00:00

132 lines
4.3 KiB
Vue

<template>
<div v-if="detail.kind === 'iam-user' && detail.iamUser" class="iam-detail">
<div class="section-title">Usuario</div>
<p><strong>{{ detail.iamUser.display_name }}</strong></p>
<p class="muted">{{ detail.iamUser.username }}</p>
<p>Rol: {{ detail.iamUser.role_label || detail.iamUser.role_code }}</p>
<p>Estado: {{ detail.iamUser.status }}</p>
<p v-if="detail.iamUser.is_owner" class="owner-badge">Cuenta propietaria (solo SaaS)</p>
<template v-if="!detail.iamUser.is_owner">
<div class="section-title mt-3">Alcance</div>
<p v-if="detail.iamUser.scope_all_projects">Todas las obras</p>
<p v-else>Obras asignadas: {{ (detail.iamUser.project_ids ?? []).length }}</p>
<p>Central: {{ detail.iamUser.warehouse_central ? "Sí" : "No" }}</p>
<p>Obras: {{ detail.iamUser.warehouse_projects !== false ? "Sí" : "No" }}</p>
</template>
</div>
<div v-else-if="detail.kind === 'iam-role' && detail.iamRole" class="iam-detail">
<div class="section-title">Matriz del rol</div>
<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>
<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";
import {
filterAssignableCatalog,
IAM_ROLE_TEMPLATES,
type IamPermRow,
} from "~/composables/iamPermissionMatrix";
const props = defineProps<{
detail: {
kind: string;
iamUser?: Record<string, unknown>;
iamRole?: Record<string, unknown> & { id?: number; is_system?: boolean };
};
}>();
const { api } = useApi();
const toast = useToast();
const perms = usePermissions();
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 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?: 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) {
const r = await api<{ permissions?: string[] }>(`/v1/iam/roles/${roleId}/permissions`);
localPerms.value = r.permissions ?? [];
}
async function savePerms() {
const roleId = props.detail.iamRole?.id;
if (!roleId) return;
try {
await api(`/v1/iam/roles/${roleId}/permissions`, {
method: "PUT",
json: { permissions: localPerms.value },
});
toast.add({ severity: "success", summary: "Permisos guardados", life: 2500 });
} catch (e) {
toast.add({ severity: "error", summary: displayErrorMessage((e as { data?: unknown }).data ?? e), life: 4000 });
}
}
watch(
() => props.detail,
async (d) => {
if (!catalog.value.length) await loadCatalog();
if (d.kind === "iam-role" && d.iamRole?.id) await loadRolePerms(Number(d.iamRole.id));
},
{ immediate: true, deep: true },
);
</script>
<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; }
</style>