Files
policy-ui/app/pages/settings/permissions.vue
Jordan Weingarten 67482f6629 WIP jordan
2026-04-16 11:11:44 -05:00

234 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { refDebounced } from '~/utils/refDebounced'
import { ROLES_SEGUROS_SEED, SEGUROS_PERMISSION_COLUMNS } from '~/data/roles-seguros'
import type { RoleRow } from '~/types/roles'
usePageTitle('Permissions · Settings')
const pageSize = ref(10)
const page = ref(1)
const search = ref('')
const debouncedSearch = refDebounced(search, 250)
const pageSizeItems = [
{ label: '10', value: 10 },
{ label: '25', value: 25 },
{ label: '50', value: 50 }
]
const rows = ref<RoleRow[]>([...ROLES_SEGUROS_SEED])
const filtered = computed(() => {
const q = debouncedSearch.value.trim().toLowerCase()
if (!q) return rows.value
return rows.value.filter(
(r) => String(r.id).includes(q) || r.description.toLowerCase().includes(q)
)
})
watch([debouncedSearch, pageSize], () => {
page.value = 1
})
const total = computed(() => filtered.value.length)
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
watch(pageCount, (c) => {
if (page.value > c) page.value = c
})
const pageRows = computed(() => {
const start = (page.value - 1) * pageSize.value
return filtered.value.slice(start, start + pageSize.value)
})
const rangeLabel = computed(() => {
if (total.value === 0) return 'Sin registros'
const start = (page.value - 1) * pageSize.value + 1
const end = Math.min(page.value * pageSize.value, total.value)
return `Mostrando ${start} a ${end} de ${total.value} registros`
})
function goPrev() {
page.value = Math.max(1, page.value - 1)
}
function goNext() {
page.value = Math.min(pageCount.value, page.value + 1)
}
function exportCsv() {
const headers = [
'ID',
'Description',
'Status',
...SEGUROS_PERMISSION_COLUMNS.map((c) => `SEGUROS_${c.key}`)
]
const lines = [headers.join(',')]
for (const r of filtered.value) {
const cells = [
r.id,
`"${r.description.replace(/"/g, '""')}"`,
r.active ? 'Active' : 'Inactive',
...SEGUROS_PERMISSION_COLUMNS.map((c) => (r.seguros[c.key] ? '1' : '0'))
]
lines.push(cells.join(','))
}
const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `roles-seguros-${new Date().toISOString().slice(0, 10)}.csv`
a.click()
URL.revokeObjectURL(url)
}
</script>
<template>
<div class="min-h-screen bg-gray-50 p-8">
<div class="mx-auto max-w-[90rem] space-y-6">
<div class="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm">
<AppBackToHome />
<span class="text-[var(--text-muted)] opacity-50" aria-hidden="true">|</span>
<NuxtLink to="/settings" class="font-medium text-[var(--text-muted)] transition hover:text-[var(--text-primary)]">
All settings
</NuxtLink>
</div>
<div>
<h1 class="text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Roles &amp; permissions</h1>
<p class="mt-2 max-w-3xl leading-relaxed text-gray-600">
Reference layout for the <strong class="font-medium text-[var(--text-primary)]">SEGUROS</strong> group: each role can
grant seven feature columns. Data is static until your API is connected.
</p>
</div>
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex flex-wrap items-center gap-3">
<span class="text-sm text-[var(--text-muted)]">Mostrar</span>
<USelect v-model="pageSize" :items="pageSizeItems" class="w-24" size="sm" />
<span class="text-sm text-[var(--text-muted)]">registros</span>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-[var(--text-muted)]">Buscar:</span>
<UInput
v-model="search"
icon="i-heroicons-magnifying-glass"
placeholder="ID o descripción"
class="w-64 max-w-full"
size="sm"
/>
</div>
</div>
<UCard :ui="{ body: { padding: 'p-0 sm:p-0' } }">
<div class="overflow-x-auto">
<table class="min-w-full border-collapse text-sm">
<thead>
<tr
class="border-b border-[var(--card-border)] bg-[var(--surface)]/90 text-left text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]"
>
<th class="whitespace-nowrap px-4 py-3">ID</th>
<th class="whitespace-nowrap px-4 py-3">Descripción</th>
<th class="whitespace-nowrap px-4 py-3">Estado</th>
<th
class="border-l border-[var(--card-border)] bg-[var(--badge-muted-bg)]/80 px-2 py-2 text-center"
:colspan="SEGUROS_PERMISSION_COLUMNS.length"
>
SEGUROS
</th>
</tr>
<tr class="border-b border-[var(--card-border)] bg-[var(--surface)] text-[var(--text-muted)]">
<th class="px-4 py-0" colspan="3" />
<th
v-for="col in SEGUROS_PERMISSION_COLUMNS"
:key="col.key"
class="border-l border-[var(--divider)] px-1 py-2 first:border-l-slate-200"
>
<div class="flex justify-center" :title="col.label">
<UIcon :name="col.icon" class="h-5 w-5" />
</div>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in pageRows"
:key="row.id"
class="border-b border-[var(--divider)] transition-colors hover:bg-[var(--surface)]/80"
>
<td class="whitespace-nowrap px-4 py-3 font-mono text-[var(--text-primary)]">{{ row.id }}</td>
<td class="max-w-xs px-4 py-3 font-medium text-[var(--text-primary)]">{{ row.description }}</td>
<td class="whitespace-nowrap px-4 py-3">
<UBadge v-if="row.active" color="success" variant="subtle" class="inline-flex items-center gap-1">
<UIcon name="i-heroicons-check" class="h-3.5 w-3.5" />
Activo
</UBadge>
<UBadge v-else color="neutral" variant="subtle">Inactivo</UBadge>
</td>
<td
v-for="col in SEGUROS_PERMISSION_COLUMNS"
:key="`${row.id}-${col.key}`"
class="border-l border-[var(--divider)] px-2 py-3 text-center first:border-l-slate-200"
>
<span
v-if="row.seguros[col.key]"
class="inline-flex h-7 w-7 items-center justify-center rounded-md bg-[var(--badge-muted-bg)] text-base font-semibold text-[var(--text-primary)]"
:title="`${col.label} — granted`"
aria-label="Permission granted"
>
×
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div
class="flex flex-wrap items-center justify-between gap-3 border-t border-[var(--card-border)] px-4 py-3"
>
<p class="text-sm text-[var(--text-muted)]">{{ rangeLabel }}</p>
<div class="flex items-center gap-1">
<UButton
icon="i-heroicons-chevron-left"
color="neutral"
variant="ghost"
size="sm"
:disabled="page <= 1"
aria-label="Previous page"
@click="goPrev"
/>
<span
class="inline-flex min-w-[2.25rem] items-center justify-center rounded-md bg-primary-500 px-2 py-1 text-sm font-medium text-white"
>
{{ page }}
</span>
<UButton
icon="i-heroicons-chevron-right"
color="neutral"
variant="ghost"
size="sm"
:disabled="page >= pageCount"
aria-label="Next page"
@click="goNext"
/>
</div>
</div>
</UCard>
<div class="flex flex-wrap items-center gap-2">
<UButton
icon="i-heroicons-arrow-up-tray"
color="neutral"
variant="outline"
size="sm"
@click="exportCsv"
>
Export
</UButton>
<span class="text-xs text-[var(--text-muted)]">Downloads CSV for filtered rows (browser only).</span>
</div>
</div>
</div>
</template>