234 lines
8.4 KiB
Vue
234 lines
8.4 KiB
Vue
<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 & 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>
|