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()
|
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 {
|
function userName(id: number): string {
|
||||||
return store.users.find(u => u.id === id)?.full_name || `Usuario ${id}`
|
return store.users.find(u => u.id === id)?.full_name || `Usuario ${id}`
|
||||||
}
|
}
|
||||||
@@ -113,15 +117,39 @@ function userName(id: number): string {
|
|||||||
// ─── Table ───────────────────────────────────────────────
|
// ─── Table ───────────────────────────────────────────────
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
|
const filterRole = ref('__all')
|
||||||
|
const filterProject = ref('__all')
|
||||||
const PAGE_SIZE = 15
|
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(() => {
|
const filteredUsers = computed(() => {
|
||||||
|
let list = store.users
|
||||||
const q = searchQuery.value.toLowerCase().trim()
|
const q = searchQuery.value.toLowerCase().trim()
|
||||||
if (!q) return store.users
|
if (q) {
|
||||||
return store.users.filter(u =>
|
list = list.filter(u =>
|
||||||
u.full_name?.toLowerCase().includes(q) ||
|
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))
|
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)
|
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 }) {
|
function initials(u: { first_name?: string; last_name?: string }) {
|
||||||
return `${(u.first_name || '')[0] || ''}${(u.last_name || '')[0] || ''}`
|
return `${(u.first_name || '')[0] || ''}${(u.last_name || '')[0] || ''}`
|
||||||
}
|
}
|
||||||
@@ -147,7 +188,7 @@ onMounted(() => {
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold tracking-tight">{{ t('users.teamTitle') }}</h2>
|
<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>
|
</div>
|
||||||
<Button size="sm" @click="showNewCell = !showNewCell">
|
<Button size="sm" @click="showNewCell = !showNewCell">
|
||||||
<IconPlus class="size-4 mr-1" />
|
<IconPlus class="size-4 mr-1" />
|
||||||
@@ -256,10 +297,26 @@ onMounted(() => {
|
|||||||
|
|
||||||
<!-- Users table -->
|
<!-- Users table -->
|
||||||
<Card>
|
<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>
|
<CardTitle class="text-sm font-medium">{{ t('users.allUsers') }} ({{ store.users.length }})</CardTitle>
|
||||||
<div class="relative w-48">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<Input v-model="searchQuery" placeholder="Buscar..." class="pl-8 h-8 text-sm" @input="currentPage = 1" />
|
<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>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="p-0">
|
<CardContent class="p-0">
|
||||||
@@ -269,7 +326,10 @@ onMounted(() => {
|
|||||||
<TableHead class="w-[60px]">ID</TableHead>
|
<TableHead class="w-[60px]">ID</TableHead>
|
||||||
<TableHead>{{ t('users.name') }}</TableHead>
|
<TableHead>{{ t('users.name') }}</TableHead>
|
||||||
<TableHead>{{ t('users.email') }}</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>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -283,8 +343,22 @@ onMounted(() => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-sm text-muted-foreground">{{ user.email }}</TableCell>
|
<TableCell class="text-sm text-muted-foreground">{{ user.email }}</TableCell>
|
||||||
<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="user.role" class="inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium bg-muted">{{ user.role }}</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-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>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -292,6 +366,11 @@ onMounted(() => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<div v-if="totalPages > 1" class="flex items-center justify-between px-4 py-3 border-t text-sm">
|
<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>
|
<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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user