mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 14:53:18 +00:00
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>
277 lines
7.5 KiB
Vue
277 lines
7.5 KiB
Vue
<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>
|