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:
+91
-12
@@ -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 =>
|
||||
u.full_name?.toLowerCase().includes(q) ||
|
||||
u.email?.toLowerCase().includes(q)
|
||||
)
|
||||
if (q) {
|
||||
list = list.filter(u =>
|
||||
u.full_name?.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,10 +297,26 @@ 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="relative w-48">
|
||||
<Input v-model="searchQuery" placeholder="Buscar..." class="pl-8 h-8 text-sm" @input="currentPage = 1" />
|
||||
<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">
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user