Files
Alpha/src/views/UsersView.vue
T

206 lines
6.6 KiB
Vue

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
import { useUsersStore } from '@/stores/users'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { IconExclamationCircle, IconUsers } from '@tabler/icons-vue'
ModuleRegistry.registerModules([AllCommunityModule])
const store = useUsersStore()
const gridApi = ref<GridApi | null>(null)
const columnDefs: ColDef[] = [
{
field: 'full_name',
headerName: 'Nombre',
flex: 2,
minWidth: 180,
sort: 'asc',
filter: 'agTextColumnFilter',
cellRenderer: (params: any) => {
return `<div class="flex items-center gap-2">
<div class="flex items-center justify-center size-7 rounded-full bg-primary/10 text-primary text-xs font-semibold">
${params.data.first_name?.[0] || ''}${params.data.last_name?.[0] || ''}
</div>
<span class="font-medium">${params.value}</span>
</div>`
},
},
{
field: 'email',
headerName: 'Email',
flex: 2,
minWidth: 200,
filter: 'agTextColumnFilter',
},
{
field: 'role',
headerName: 'Rol',
flex: 1,
minWidth: 100,
filter: 'agTextColumnFilter',
cellRenderer: (params: any) => {
if (!params.value) return `<span class="text-muted-foreground text-xs">—</span>`
const colors: Record<string, string> = {
BA: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
DEV: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',
PM: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
QA: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
PO: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400',
TL: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
}
const cls = colors[params.value] || 'bg-muted text-muted-foreground'
return `<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${cls}">${params.value}</span>`
},
},
{
field: 'cell',
headerName: 'Célula',
flex: 1,
minWidth: 100,
filter: 'agTextColumnFilter',
cellRenderer: (params: any) => {
if (!params.value) return `<span class="text-muted-foreground text-xs">—</span>`
return `<span class="inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium">${params.value}</span>`
},
},
{
field: 'seniority',
headerName: 'Seniority',
flex: 1,
minWidth: 100,
filter: 'agTextColumnFilter',
cellRenderer: (params: any) => {
if (!params.value) return `<span class="text-muted-foreground text-xs">—</span>`
const map: Record<string, string> = {
jr: 'Jr',
mid: 'Mid',
sr: 'Sr',
lead: 'Lead',
}
return `<span class="text-sm">${map[params.value] || params.value}</span>`
},
},
{
field: 'projects_count',
headerName: 'Proyectos',
flex: 1,
minWidth: 110,
filter: 'agNumberColumnFilter',
cellRenderer: (params: any) => {
const count = params.value || 0
if (count === 0) return `<span class="text-muted-foreground text-xs">—</span>`
const cls = count >= 5 ? 'text-destructive' : count >= 3 ? 'text-amber-500' : 'text-emerald-500'
return `<span class="font-semibold text-sm ${cls}">${count}</span>`
},
},
{
field: 'projects',
headerName: 'Proyectos asignados',
flex: 3,
minWidth: 220,
filter: 'agTextColumnFilter',
cellRenderer: (params: any) => {
const projects: string[] = params.value || []
if (projects.length === 0) return `<span class="text-muted-foreground text-xs">Sin asignación</span>`
const badges = projects.map((p: string) =>
`<span class="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs mr-1 mb-1">${p}</span>`
).join('')
return `<div class="flex flex-wrap gap-1 py-0.5">${badges}</div>`
},
autoHeight: true,
},
]
const defaultColDef: ColDef = {
sortable: true,
resizable: true,
wrapText: true,
}
function onGridReady(params: GridReadyEvent) {
gridApi.value = params.api
params.api.sizeColumnsToFit()
}
onMounted(() => {
store.fetchAll()
})
</script>
<template>
<div class="flex flex-col gap-4 px-4 lg:px-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Equipo</h2>
<p class="text-muted-foreground">
{{ store.users.length }} miembros · {{ store.employees.length }} asignaciones
</p>
</div>
<Badge variant="outline" class="text-sm">
{{ store.activeUsers.length }} activos
</Badge>
</div>
<!-- Loading -->
<div v-if="store.loading" class="space-y-2">
<Skeleton v-for="i in 5" :key="i" class="h-10 w-full" />
</div>
<!-- Error -->
<div v-else-if="store.error" class="flex flex-col items-center gap-3 py-12 text-center">
<IconExclamationCircle class="size-10 text-destructive" />
<p class="text-lg font-medium">Error al cargar usuarios</p>
<p class="text-sm text-muted-foreground">{{ store.error }}</p>
<button
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
@click="store.fetchAll()"
>
Reintentar
</button>
</div>
<!-- Empty -->
<div v-else-if="store.users.length === 0" class="flex flex-col items-center gap-3 py-12 text-center">
<IconUsers class="size-10 text-muted-foreground" />
<p class="text-lg font-medium">Sin usuarios</p>
<p class="text-sm text-muted-foreground">
No se encontraron usuarios en KAPPA.
</p>
</div>
<!-- Grid -->
<div v-else class="ag-theme-alpha w-full" style="height: calc(100vh - 14rem)">
<AgGridVue
:row-data="store.users"
:column-defs="columnDefs"
:default-col-def="defaultColDef"
:pagination="true"
:pagination-page-size="20"
:pagination-page-size-selector="[10, 20, 50, 100]"
:animate-rows="false"
row-selection="single"
dom-layout="normal"
@grid-ready="onGridReady"
/>
</div>
</div>
</template>
<style scoped>
:deep(.ag-cell) {
display: flex;
align-items: center;
}
:deep(.ag-header-cell) {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted-foreground);
}
</style>