panels-origin/web-panel/pages/usuarios.vue
Alberto Martinez 41273dfb04 Gastos, almacén y control presupuestal
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Validación de endpoints y SPs

Se probaron todos los endpoints nuevos (gastos, almacén, control presupuestal, IAM) y sus RPCs asociados.

### Bugs corregidos en migraciones/SQL

1. **IAM grants en schema core** — `iam-004e` intentaba `GRANT` sobre `core` con rol `iam_owner` (sin permiso). Los grants cruzados se movieron a `core-027-iam-rpc-cross-grants.sql`.
2. **`_cost_settings` ambiguo** — columnas `budget_warn_pct` etc. colisionaban con `RETURNS TABLE` en PL/pgSQL, rompiendo `fn_expense_create` y control presupuestal. Corregido en `core-028-fix-cost-settings-ambiguous.sql`.

### Tests añadidos

- `api/cost_modules_test.ts` — CRUD RPC gastos, flujo almacén completo, control presupuestal, IAM permisos
- `scripts/crud-smoke-test.sh` — smoke HTTP de todos los endpoints nuevos

### Resultados

- `deno test cost_modules_test.ts` — 5/5 OK
- `./scripts/crud-smoke-test.sh` — todos los checks OK (gastos CRUD, IVA, cost-settings, almacén, transferencias, cost-control, IAM)
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-eeec3b5c-f789-43e8-a353-0755e4706377?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-eeec3b5c-f789-43e8-a353-0755e4706377&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>
2026-09-04 04:29:02 +00:00

78 lines
2.8 KiB
Vue

<template>
<div class="usuarios-page">
<DeskTableBar v-model="tableQ" placeholder="Buscar usuario">
<Chip :label="`${users.length} usuarios`" icon="pi pi-users" />
</DeskTableBar>
<DataTable :value="listed" data-key="id" paginator :rows="25" scrollable scroll-height="flex">
<Column field="username" header="Usuario" style="width: 10rem" />
<Column field="display_name" header="Nombre" />
<Column field="role_code" header="Rol" style="width: 9rem" />
<Column field="email" header="Correo" style="width: 14rem" />
</DataTable>
<Panel header="Permisos del rol operativo (user)" class="mt-4">
<div class="perm-grid">
<div v-for="perm in permissions" :key="perm.code" class="perm-item">
<Checkbox v-model="rolePerms" :input-id="perm.code" :value="perm.code" />
<label :for="perm.code">{{ perm.label }}</label>
</div>
</div>
<Button label="Guardar permisos" icon="pi pi-save" class="mt-3" @click="saveRolePerms" />
</Panel>
</div>
</template>
<script setup lang="ts">
import { displayErrorMessage } from "~/composables/api-response";
type UserRow = { id: number; username: string; display_name: string; role_code: string; email: string };
type PermRow = { code: string; label: string };
const desk = useDesktop();
const { api } = useApi();
const toast = useToast();
const tableQ = ref("");
const users = ref<UserRow[]>([]);
const permissions = ref<PermRow[]>([]);
const rolePerms = ref<string[]>([]);
const listed = computed(() => {
const q = tableQ.value.toLowerCase();
return users.value.filter((u) => !q || [u.username, u.display_name, u.email].join(" ").toLowerCase().includes(q));
});
async function load() {
const [u, p, rp] = await Promise.all([
api<{ data?: { users?: UserRow[] } }>("/v1/iam/users"),
api<{ data?: { permissions?: PermRow[] } }>("/v1/iam/permissions"),
api<{ data?: { permissions?: string[] } }>("/v1/iam/roles/user/permissions"),
]);
users.value = u.data?.users ?? [];
permissions.value = p.data?.permissions ?? [];
rolePerms.value = rp.data?.permissions ?? [];
}
async function saveRolePerms() {
try {
const r = await api<{ message?: string }>("/v1/iam/roles/user/permissions", {
method: "PUT",
json: { permissions: rolePerms.value },
});
toast.add({ severity: "success", summary: r.message || "Permisos guardados", life: 2500 });
} catch (e) {
toast.add({ severity: "error", summary: displayErrorMessage((e as { data?: unknown }).data ?? e), life: 4000 });
}
}
onMounted(async () => {
desk.configure({ tab: "Usuarios IAM", ribbon: [] });
await load();
});
</script>
<style scoped>
.perm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 8px 16px; }
.perm-item { display: flex; align-items: center; gap: 8px; font-size: 13px; }
</style>