fix: restaurar filtros + paginacion en UsersView

- filtros por rol y proyecto restaurados (Select dropdowns)
- paginacion con navegacion numerica (‹ 1 2 ... N ›)
- columna Célula en tabla de usuarios
- contador de asignaciones en header
This commit is contained in:
2026-05-29 01:52:41 -05:00
parent 388fa09f3e
commit 37aedf9e58
+87 -8
View File
@@ -106,6 +106,10 @@ async function removeUserFromCell(cellId: string, userId: number) {
await loadCells()
}
function userCells(userId: number) {
return cells.value.filter(c => (cellMembers.value[c.id] || []).some((m: any) => m.userId === userId))
}
function userName(id: number): string {
return store.users.find(u => u.id === id)?.full_name || `Usuario ${id}`
}
@@ -113,15 +117,39 @@ function userName(id: number): string {
// ─── Table ───────────────────────────────────────────────
const searchQuery = ref('')
const currentPage = ref(1)
const filterRole = ref('__all')
const filterProject = ref('__all')
const PAGE_SIZE = 15
const uniqueRoles = computed(() => {
const roles = new Set(store.users.map(u => u.role).filter(Boolean))
return Array.from(roles).sort() as string[]
})
const uniqueProjects = computed(() => {
const projs = new Set<string>()
store.users.forEach(u => u.projects.forEach(p => projs.add(p)))
return Array.from(projs).sort()
})
const filteredUsers = computed(() => {
let list = store.users
const q = searchQuery.value.toLowerCase().trim()
if (!q) return store.users
return store.users.filter(u =>
if (q) {
list = list.filter(u =>
u.full_name?.toLowerCase().includes(q) ||
u.email?.toLowerCase().includes(q)
u.email?.toLowerCase().includes(q) ||
u.role?.toLowerCase().includes(q) ||
u.cell?.toLowerCase().includes(q)
)
}
if (filterRole.value && filterRole.value !== '__all') {
list = list.filter(u => u.role === filterRole.value)
}
if (filterProject.value && filterProject.value !== '__all') {
list = list.filter(u => u.projects.includes(filterProject.value))
}
return list
})
const totalPages = computed(() => Math.ceil(filteredUsers.value.length / PAGE_SIZE))
@@ -131,6 +159,19 @@ const paginatedUsers = computed(() => {
return filteredUsers.value.slice(start, start + PAGE_SIZE)
})
function goToPage(p: number) {
currentPage.value = Math.max(1, Math.min(p, totalPages.value))
}
const pageNumbers = computed(() => {
const total = totalPages.value
const curr = currentPage.value
if (total <= 5) return Array.from({ length: total }, (_, i) => i + 1)
if (curr <= 3) return [1, 2, 3, 4, "...", total]
if (curr >= total - 2) return [1, "...", total - 3, total - 2, total - 1, total]
return [1, "...", curr - 1, curr, curr + 1, "...", total]
})
function initials(u: { first_name?: string; last_name?: string }) {
return `${(u.first_name || '')[0] || ''}${(u.last_name || '')[0] || ''}`
}
@@ -147,7 +188,7 @@ onMounted(() => {
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">{{ t('users.teamTitle') }}</h2>
<p class="text-sm text-muted-foreground">{{ store.users.length }} usuarios · {{ cells.length }} células</p>
<p class="text-sm text-muted-foreground">{{ store.users.length }} usuarios · {{ store.employees.length }} asignaciones · {{ cells.length }} células</p>
</div>
<Button size="sm" @click="showNewCell = !showNewCell">
<IconPlus class="size-4 mr-1" />
@@ -256,11 +297,27 @@ onMounted(() => {
<!-- Users table -->
<Card>
<CardHeader class="pb-3 flex flex-row items-center justify-between">
<CardHeader class="pb-3 flex flex-col @md:flex-row items-start @md:items-center justify-between gap-3">
<CardTitle class="text-sm font-medium">{{ t('users.allUsers') }} ({{ store.users.length }})</CardTitle>
<div class="flex items-center gap-2 flex-wrap">
<Select v-model="filterRole">
<SelectTrigger class="h-8 w-[140px] text-xs"><SelectValue :placeholder="t('users.role')" /></SelectTrigger>
<SelectContent>
<SelectItem value="__all" class="text-xs">{{ t('users.allRoles') }}</SelectItem>
<SelectItem v-for="r in uniqueRoles" :key="r" :value="r" class="text-xs">{{ r }}</SelectItem>
</SelectContent>
</Select>
<Select v-model="filterProject">
<SelectTrigger class="h-8 w-[180px] text-xs"><SelectValue :placeholder="t('users.assignments')" /></SelectTrigger>
<SelectContent>
<SelectItem value="__all" class="text-xs">{{ t('users.allProjects') }}</SelectItem>
<SelectItem v-for="p in uniqueProjects" :key="p" :value="p" class="text-xs">{{ p }}</SelectItem>
</SelectContent>
</Select>
<div class="relative w-48">
<Input v-model="searchQuery" placeholder="Buscar..." class="pl-8 h-8 text-sm" @input="currentPage = 1" />
</div>
</div>
</CardHeader>
<CardContent class="p-0">
<Table>
@@ -269,7 +326,10 @@ onMounted(() => {
<TableHead class="w-[60px]">ID</TableHead>
<TableHead>{{ t('users.name') }}</TableHead>
<TableHead>{{ t('users.email') }}</TableHead>
<TableHead>Células</TableHead>
<TableHead class="w-[80px]">{{ t('users.role') }}</TableHead>
<TableHead class="w-[100px]">Célula</TableHead>
<TableHead class="w-[70px] text-right">{{ t('users.projects') }}</TableHead>
<TableHead>{{ t('users.assignments') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -283,8 +343,22 @@ onMounted(() => {
</TableCell>
<TableCell class="text-sm text-muted-foreground">{{ user.email }}</TableCell>
<TableCell>
<span v-for="c in cells.filter(c => cellMembers[c.id]?.some(m => m.userId === user.id))" :key="c.id" class="inline-flex rounded-full bg-muted px-2 py-0.5 text-xs mr-1">{{ c.name }}</span>
<span v-if="!cells.some(c => cellMembers[c.id]?.some(m => m.userId === user.id))" class="text-xs text-muted-foreground"></span>
<span v-if="user.role" class="inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium bg-muted">{{ user.role }}</span>
<span v-else class="text-muted-foreground text-sm"></span>
</TableCell>
<TableCell class="text-sm text-muted-foreground">
<template v-if="userCells(user.id).length > 0">
<span v-for="c in userCells(user.id)" :key="c.id" class="inline-flex rounded-full bg-muted px-2 py-0.5 text-[10px] mr-1">{{ c.name }}</span>
</template>
<span v-else></span>
</TableCell>
<TableCell class="text-sm text-right font-semibold">{{ user.projects_count || '—' }}</TableCell>
<TableCell>
<div class="flex flex-wrap gap-1">
<span v-for="p in user.projects.slice(0, 2)" :key="p" class="inline-flex rounded-full bg-muted px-2 py-0.5 text-[10px]">{{ p }}</span>
<span v-if="user.projects.length > 2" class="text-[10px] text-muted-foreground">+{{ user.projects.length - 2 }}</span>
<span v-if="!user.projects.length" class="text-muted-foreground text-sm"></span>
</div>
</TableCell>
</TableRow>
</TableBody>
@@ -292,6 +366,11 @@ onMounted(() => {
</CardContent>
<div v-if="totalPages > 1" class="flex items-center justify-between px-4 py-3 border-t text-sm">
<span class="text-muted-foreground text-xs">{{ filteredUsers.length }} usuarios · Página {{ currentPage }} de {{ totalPages }}</span>
<div class="flex items-center gap-1">
<a href="#" class="inline-flex items-center justify-center size-7 rounded border text-xs hover:bg-muted" :class="currentPage <= 1 ? 'opacity-30 pointer-events-none' : ''" @click.prevent="goToPage(currentPage - 1)"></a>
<a href="#" v-for="p in pageNumbers" :key="p" class="inline-flex items-center justify-center size-7 rounded border text-xs" :class="p === currentPage ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'" @click.prevent="typeof p === 'number' && goToPage(p)">{{ p }}</a>
<a href="#" class="inline-flex items-center justify-center size-7 rounded border text-xs hover:bg-muted" :class="currentPage >= totalPages ? 'opacity-30 pointer-events-none' : ''" @click.prevent="goToPage(currentPage + 1)"></a>
</div>
</div>
</Card>
</div>