diff --git a/web-panel/pages/usuarios.vue b/web-panel/pages/usuarios.vue
index 5bf983c..b9a4bcc 100644
--- a/web-panel/pages/usuarios.vue
+++ b/web-panel/pages/usuarios.vue
@@ -113,11 +113,8 @@
-
-
-
-
+
@@ -184,10 +181,11 @@ const userForm = reactive({
const roleForm = reactive({
template: "operativo",
- code: "",
label: "",
});
+const RESERVED_ROLE_CODES = new Set(["tenant_admin", "platform_admin", "user", "operativo"]);
+
const templateOptions = [
{ label: "Administrador operativo", value: "admin_operativo" },
{ label: "Supervisor de obra", value: "supervisor_obra" },
@@ -384,20 +382,54 @@ async function resetAccess() {
}
}
+function slugRoleCode(text: string): string {
+ let s = text
+ .normalize("NFD")
+ .replace(/\p{M}/gu, "")
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "_")
+ .replace(/^_+|_+$/g, "")
+ .replace(/_+/g, "_");
+ if (!s || !/^[a-z]/.test(s)) s = `rol_${s || "nuevo"}`;
+ return s.slice(0, 64);
+}
+
+function uniqueRoleCode(base: string): string {
+ const taken = new Set(roles.value.map((r) => r.code));
+ let code = base;
+ let n = 2;
+ while (taken.has(code) || RESERVED_ROLE_CODES.has(code)) {
+ const suffix = `_${n}`;
+ code = `${base.slice(0, Math.max(1, 64 - suffix.length))}${suffix}`;
+ n++;
+ }
+ return code;
+}
+
+function roleCodeFromForm(): string {
+ const label = roleForm.label.trim();
+ const base = slugRoleCode(label || roleForm.template);
+ return uniqueRoleCode(base);
+}
+
function openNewRole() {
roleForm.template = "operativo";
- roleForm.code = "";
- roleForm.label = "";
+ roleForm.label = templateOptions.find((o) => o.value === "operativo")?.label ?? "";
showRoleDialog.value = true;
}
async function saveRole() {
saving.value = true;
try {
- const code = (roleForm.code || roleForm.template).trim().toLowerCase();
+ const label = roleForm.label.trim();
+ if (!label) {
+ toast.add({ severity: "warn", summary: "Indique la etiqueta del rol", life: 3000 });
+ return;
+ }
+ const code = roleCodeFromForm();
await api("/v1/iam/roles", {
method: "POST",
- json: { code, label: roleForm.label || code, template: roleForm.template },
+ json: { code, label, template: roleForm.template },
});
toast.add({ severity: "success", summary: "Rol creado", life: 2500 });
showRoleDialog.value = false;
@@ -510,6 +542,14 @@ function syncChrome() {
watch([selectedUser, selectedRole, treeKey], syncChrome);
+watch(
+ () => roleForm.template,
+ (tpl) => {
+ const opt = templateOptions.find((o) => o.value === tpl);
+ if (opt) roleForm.label = opt.label;
+ },
+);
+
onMounted(async () => {
if (!treeKey.value) treeKey.value = "users-active";
await load();