redisenar vista de equipo: tarjetas de miembros + AG Grid con tema shadcn corregido
This commit is contained in:
@@ -1,58 +1,49 @@
|
|||||||
/**
|
/**
|
||||||
* AG Grid theme — shadcn-vue New York style.
|
* AG Grid theme — adaptado a shadcn-vue New York style.
|
||||||
* Hereda variables CSS de shadcn para light/dark automático.
|
* Usa las mismas CSS custom properties de shadcn para heredar light/dark.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.ag-theme-alpha {
|
.ag-theme-alpha-shadcn {
|
||||||
--ag-active-color: var(--primary);
|
--ag-active-color: var(--primary);
|
||||||
--ag-background-color: var(--background);
|
--ag-foreground-color: var(--foreground);
|
||||||
--ag-border-color: var(--border);
|
--ag-background-color: var(--card);
|
||||||
--ag-header-background-color: var(--muted);
|
--ag-secondary-foreground-color: var(--muted-foreground);
|
||||||
--ag-header-foreground-color: var(--muted-foreground);
|
--ag-header-foreground-color: var(--muted-foreground);
|
||||||
|
--ag-header-background-color: var(--muted);
|
||||||
|
--ag-disabled-foreground-color: var(--muted-foreground);
|
||||||
|
--ag-border-color: var(--border);
|
||||||
--ag-header-column-separator-color: var(--border);
|
--ag-header-column-separator-color: var(--border);
|
||||||
--ag-odd-row-background-color: transparent;
|
--ag-header-column-resize-handle-color: var(--border);
|
||||||
|
--ag-input-focus-border-color: var(--ring);
|
||||||
|
--ag-input-border-color: var(--border);
|
||||||
--ag-row-hover-color: var(--accent);
|
--ag-row-hover-color: var(--accent);
|
||||||
--ag-selected-row-background-color: color-mix(in oklch, var(--primary) 10%, transparent);
|
--ag-selected-row-background-color: color-mix(in oklch, var(--primary) 8%, var(--card));
|
||||||
|
--ag-range-selection-border-color: var(--primary);
|
||||||
--ag-font-family: var(--font-sans);
|
--ag-font-family: var(--font-sans);
|
||||||
--ag-font-size: 13px;
|
--ag-font-size: 13px;
|
||||||
--ag-row-height: 40px;
|
--ag-row-height: 44px;
|
||||||
--ag-header-height: 40px;
|
--ag-header-height: 40px;
|
||||||
--ag-range-selection-border-color: var(--primary);
|
|
||||||
--ag-input-focus-border-color: var(--ring);
|
|
||||||
--ag-foreground-color: var(--foreground);
|
|
||||||
--ag-secondary-foreground-color: var(--muted-foreground);
|
|
||||||
--ag-disabled-foreground-color: var(--muted-foreground);
|
|
||||||
--ag-header-column-resize-handle-color: var(--border);
|
|
||||||
--ag-checkbox-checked-color: var(--primary);
|
|
||||||
--ag-checkbox-unchecked-color: var(--muted-foreground);
|
|
||||||
--ag-checkbox-indeterminate-color: var(--primary);
|
|
||||||
--ag-input-border-color: var(--border);
|
|
||||||
--ag-icon-font-color: var(--muted-foreground);
|
|
||||||
--ag-borders: solid 1px;
|
--ag-borders: solid 1px;
|
||||||
--ag-borders-critical: solid 1px;
|
--ag-borders-critical: solid 1px;
|
||||||
--ag-borders-secondary: solid 1px;
|
--ag-borders-secondary: solid 1px;
|
||||||
--ag-borders-row: none;
|
--ag-borders-row: solid 1px;
|
||||||
--ag-borders-input: solid 1px;
|
--ag-cell-horizontal-padding: 16px;
|
||||||
--ag-cell-horizontal-padding: 12px;
|
--ag-card-radius: var(--radius-lg);
|
||||||
--ag-card-radius: var(--radius);
|
--ag-wrapper-border-radius: var(--radius-lg);
|
||||||
--ag-card-shadow: var(--shadow-sm);
|
|
||||||
--ag-wrapper-border-radius: var(--radius);
|
|
||||||
--ag-pagination-background-color: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-theme-alpha .ag-root-wrapper {
|
.ag-theme-alpha-shadcn .ag-root-wrapper {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius-lg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
font-family: var(--font-sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-theme-alpha .ag-header-cell {
|
.ag-theme-alpha-shadcn .ag-header-cell {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-transform: uppercase;
|
letter-spacing: 0.025em;
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-theme-alpha .ag-cell {
|
.ag-theme-alpha-shadcn .ag-row {
|
||||||
display: flex;
|
border-color: var(--border);
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { createApp } from 'vue'
|
|||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { i18n } from './i18n'
|
import { i18n } from './i18n'
|
||||||
|
import 'ag-grid-community/styles/ag-grid.css'
|
||||||
|
import './assets/ag-grid-alpha.css'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|||||||
@@ -5,10 +5,6 @@
|
|||||||
|
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@import "ag-grid-community/styles/ag-grid.css";
|
|
||||||
|
|
||||||
@import "./assets/ag-grid-alpha.css";
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
+189
-118
@@ -1,17 +1,67 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
import type { ColDef } from 'ag-grid-community'
|
||||||
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
|
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
|
||||||
import { useUsersStore } from '@/stores/users'
|
import { useUsersStore, type AlphaUser } from '@/stores/users'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { IconExclamationCircle, IconUsers } from '@tabler/icons-vue'
|
import {
|
||||||
|
IconExclamationCircle,
|
||||||
|
IconUsers,
|
||||||
|
IconCode,
|
||||||
|
IconClipboardText,
|
||||||
|
IconUserCog,
|
||||||
|
IconBuildingFactory,
|
||||||
|
} from '@tabler/icons-vue'
|
||||||
|
|
||||||
ModuleRegistry.registerModules([AllCommunityModule])
|
ModuleRegistry.registerModules([AllCommunityModule])
|
||||||
|
|
||||||
const store = useUsersStore()
|
const store = useUsersStore()
|
||||||
const gridApi = ref<GridApi | null>(null)
|
const gridReady = ref(false)
|
||||||
|
|
||||||
|
const roleIcons: Record<string, any> = {
|
||||||
|
DEV: IconCode,
|
||||||
|
BA: IconClipboardText,
|
||||||
|
PM: IconUserCog,
|
||||||
|
PO: IconBuildingFactory,
|
||||||
|
QA: IconCode,
|
||||||
|
TL: IconUserCog,
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleColors: Record<string, string> = {
|
||||||
|
DEV: 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-950',
|
||||||
|
BA: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950',
|
||||||
|
PM: 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-950',
|
||||||
|
PO: 'text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-950',
|
||||||
|
QA: 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950',
|
||||||
|
TL: 'text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-950',
|
||||||
|
}
|
||||||
|
|
||||||
|
const badgeColors: Record<string, string> = {
|
||||||
|
DEV: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',
|
||||||
|
BA: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||||
|
PM: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
||||||
|
PO: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400',
|
||||||
|
QA: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
|
||||||
|
TL: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamRoles = ['DEV', 'BA', 'PM', 'PO', 'QA', 'TL']
|
||||||
|
|
||||||
|
const teamMembers = computed(() =>
|
||||||
|
store.users.filter(u => teamRoles.includes(u.role || ''))
|
||||||
|
)
|
||||||
|
|
||||||
|
const stats = computed(() => ({
|
||||||
|
total: store.users.length,
|
||||||
|
active: store.activeUsers.length,
|
||||||
|
byRole: teamRoles.reduce((acc, role) => {
|
||||||
|
acc[role] = store.users.filter(u => u.role === role).length
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, number>),
|
||||||
|
}))
|
||||||
|
|
||||||
const columnDefs: ColDef[] = [
|
const columnDefs: ColDef[] = [
|
||||||
{
|
{
|
||||||
@@ -19,140 +69,86 @@ const columnDefs: ColDef[] = [
|
|||||||
headerName: 'Nombre',
|
headerName: 'Nombre',
|
||||||
flex: 2,
|
flex: 2,
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
sort: 'asc',
|
filter: true,
|
||||||
filter: 'agTextColumnFilter',
|
|
||||||
cellRenderer: (params: any) => {
|
cellRenderer: (params: any) => {
|
||||||
return `<div class="flex items-center gap-2">
|
const initials = `${params.data.first_name?.[0] || ''}${params.data.last_name?.[0] || ''}`
|
||||||
<div class="flex items-center justify-center size-7 rounded-full bg-primary/10 text-primary text-xs font-semibold">
|
return `<div class="flex items-center gap-2 py-1">
|
||||||
${params.data.first_name?.[0] || ''}${params.data.last_name?.[0] || ''}
|
<div class="flex items-center justify-center size-7 rounded-full bg-primary/10 text-primary text-xs font-semibold flex-shrink-0">${initials}</div>
|
||||||
</div>
|
|
||||||
<span class="font-medium">${params.value}</span>
|
<span class="font-medium">${params.value}</span>
|
||||||
</div>`
|
</div>`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{ field: 'email', headerName: 'Email', flex: 2, minWidth: 200, filter: true },
|
||||||
field: 'email',
|
|
||||||
headerName: 'Email',
|
|
||||||
flex: 2,
|
|
||||||
minWidth: 200,
|
|
||||||
filter: 'agTextColumnFilter',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'role',
|
field: 'role',
|
||||||
headerName: 'Rol',
|
headerName: 'Rol',
|
||||||
flex: 1,
|
width: 90,
|
||||||
minWidth: 100,
|
filter: true,
|
||||||
filter: 'agTextColumnFilter',
|
|
||||||
cellRenderer: (params: any) => {
|
cellRenderer: (params: any) => {
|
||||||
if (!params.value) return `<span class="text-muted-foreground text-xs">—</span>`
|
if (!params.value) return '—'
|
||||||
const colors: Record<string, string> = {
|
const cls = badgeColors[params.value] || 'bg-muted text-muted-foreground'
|
||||||
BA: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
return `<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${cls}">${params.value}</span>`
|
||||||
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: 'cell', headerName: 'Célula', width: 110, filter: true, cellRenderer: (p: any) => p.value || '—' },
|
||||||
|
{ field: 'seniority', headerName: 'Seniority', width: 100, filter: true, cellRenderer: (p: any) => p.value || '—' },
|
||||||
{
|
{
|
||||||
field: 'projects_count',
|
field: 'projects_count',
|
||||||
headerName: 'Proyectos',
|
headerName: 'Proyectos',
|
||||||
flex: 1,
|
width: 100,
|
||||||
minWidth: 110,
|
|
||||||
filter: 'agNumberColumnFilter',
|
filter: 'agNumberColumnFilter',
|
||||||
cellRenderer: (params: any) => {
|
cellRenderer: (params: any) => {
|
||||||
const count = params.value || 0
|
const c = params.value || 0
|
||||||
if (count === 0) return `<span class="text-muted-foreground text-xs">—</span>`
|
if (c === 0) return '—'
|
||||||
const cls = count >= 5 ? 'text-destructive' : count >= 3 ? 'text-amber-500' : 'text-emerald-500'
|
return `<span class="font-semibold">${c}</span>`
|
||||||
return `<span class="font-semibold text-sm ${cls}">${count}</span>`
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'projects',
|
field: 'projects',
|
||||||
headerName: 'Proyectos asignados',
|
headerName: 'Asignaciones',
|
||||||
flex: 3,
|
flex: 3,
|
||||||
minWidth: 220,
|
minWidth: 200,
|
||||||
filter: 'agTextColumnFilter',
|
|
||||||
cellRenderer: (params: any) => {
|
cellRenderer: (params: any) => {
|
||||||
const projects: string[] = params.value || []
|
const p: string[] = params.value || []
|
||||||
if (projects.length === 0) return `<span class="text-muted-foreground text-xs">Sin asignación</span>`
|
if (!p.length) return '—'
|
||||||
const badges = projects.map((p: string) =>
|
return p.map((n: string) => `<span class="inline-flex rounded-full bg-muted px-2 py-0.5 text-xs mr-1">${n}</span>`).join('')
|
||||||
`<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(() => {
|
onMounted(() => {
|
||||||
store.fetchAll()
|
store.fetchAll()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4 px-4 lg:px-6">
|
<div class="flex flex-col gap-6 px-4 lg:px-6">
|
||||||
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold tracking-tight">Equipo</h2>
|
<h2 class="text-2xl font-bold tracking-tight">Equipo</h2>
|
||||||
<p class="text-muted-foreground">
|
<p class="text-muted-foreground">
|
||||||
{{ store.users.length }} miembros · {{ store.employees.length }} asignaciones
|
{{ store.users.length }} miembros · {{ store.employees.length }} asignaciones en KAPPA
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" class="text-sm">
|
<Badge variant="outline" class="text-sm">
|
||||||
{{ store.activeUsers.length }} activos
|
{{ stats.active }} activos
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
<div v-if="store.loading" class="space-y-2">
|
<template v-if="store.loading">
|
||||||
<Skeleton v-for="i in 5" :key="i" class="h-10 w-full" />
|
<div class="grid grid-cols-1 gap-4 @xl:grid-cols-2 @3xl:grid-cols-4">
|
||||||
|
<Skeleton v-for="i in 4" :key="i" class="h-24 rounded-xl" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-4 @xl:grid-cols-2 @3xl:grid-cols-3">
|
||||||
|
<Skeleton v-for="i in 6" :key="i" class="h-40 rounded-xl" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Error -->
|
<!-- Error -->
|
||||||
<div v-else-if="store.error" class="flex flex-col items-center gap-3 py-12 text-center">
|
<template v-else-if="store.error">
|
||||||
|
<div class="flex flex-col items-center gap-3 py-12 text-center">
|
||||||
<IconExclamationCircle class="size-10 text-destructive" />
|
<IconExclamationCircle class="size-10 text-destructive" />
|
||||||
<p class="text-lg font-medium">Error al cargar usuarios</p>
|
<p class="text-lg font-medium">Error al cargar usuarios</p>
|
||||||
<p class="text-sm text-muted-foreground">{{ store.error }}</p>
|
<p class="text-sm text-muted-foreground">{{ store.error }}</p>
|
||||||
@@ -163,43 +159,118 @@ onMounted(() => {
|
|||||||
Reintentar
|
Reintentar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Empty -->
|
<!-- Empty -->
|
||||||
<div v-else-if="store.users.length === 0" class="flex flex-col items-center gap-3 py-12 text-center">
|
<template v-else-if="store.users.length === 0">
|
||||||
|
<div class="flex flex-col items-center gap-3 py-12 text-center">
|
||||||
<IconUsers class="size-10 text-muted-foreground" />
|
<IconUsers class="size-10 text-muted-foreground" />
|
||||||
<p class="text-lg font-medium">Sin usuarios</p>
|
<p class="text-lg font-medium">Sin usuarios</p>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">No se encontraron usuarios en KAPPA.</p>
|
||||||
No se encontraron usuarios en KAPPA.
|
</div>
|
||||||
</p>
|
</template>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="grid grid-cols-2 gap-3 @xl:grid-cols-3 @3xl:grid-cols-6">
|
||||||
|
<Card v-for="role in teamRoles" :key="role" class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card">
|
||||||
|
<CardHeader class="p-4 pb-2">
|
||||||
|
<CardTitle class="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<component
|
||||||
|
:is="roleIcons[role]"
|
||||||
|
class="size-4"
|
||||||
|
:class="roleColors[role]?.split(' ')[0]"
|
||||||
|
/>
|
||||||
|
{{ role }}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="p-4 pt-0">
|
||||||
|
<p class="text-2xl font-bold">{{ stats.byRole[role] || 0 }}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grid -->
|
<!-- Team Member Cards -->
|
||||||
<div v-else class="ag-theme-alpha w-full" style="height: calc(100vh - 14rem)">
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
||||||
|
Miembros del equipo · {{ teamMembers.length }}
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 gap-3 @xl:grid-cols-2 @3xl:grid-cols-3">
|
||||||
|
<Card
|
||||||
|
v-for="user in teamMembers"
|
||||||
|
:key="user.id"
|
||||||
|
class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card hover:border-primary/30 transition-colors"
|
||||||
|
>
|
||||||
|
<CardHeader class="p-4 pb-2">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center size-10 rounded-full text-sm font-bold"
|
||||||
|
:class="roleColors[user.role || ''] || 'bg-muted text-muted-foreground'"
|
||||||
|
>
|
||||||
|
{{ user.first_name?.[0] || '' }}{{ user.last_name?.[0] || '' }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-sm">{{ user.full_name }}</p>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ user.email }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
v-if="user.role"
|
||||||
|
:class="badgeColors[user.role] || ''"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ user.role }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="p-4 pt-0">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span v-if="user.cell" class="inline-flex items-center gap-1 rounded-md border px-2 py-0.5">
|
||||||
|
{{ user.cell }}
|
||||||
|
</span>
|
||||||
|
<span v-if="user.seniority" class="capitalize">{{ user.seniority }}</span>
|
||||||
|
<span class="font-medium text-foreground">{{ user.projects_count }} proyecto{{ user.projects_count !== 1 ? 's' : '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="user.projects.length" class="flex flex-wrap gap-1 mt-2">
|
||||||
|
<span
|
||||||
|
v-for="p in user.projects.slice(0, 3)"
|
||||||
|
:key="p"
|
||||||
|
class="inline-flex rounded-full bg-muted px-2 py-0.5 text-xs"
|
||||||
|
>
|
||||||
|
{{ p }}
|
||||||
|
</span>
|
||||||
|
<span v-if="user.projects.length > 3" class="text-xs text-muted-foreground">
|
||||||
|
+{{ user.projects.length - 3 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AG Grid table -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
||||||
|
Todos los usuarios
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
class="ag-theme-alpha-shadcn w-full rounded-lg border overflow-hidden"
|
||||||
|
:style="{ height: Math.max(300, Math.min(600, store.users.length * 45 + 45)) + 'px' }"
|
||||||
|
>
|
||||||
<AgGridVue
|
<AgGridVue
|
||||||
:row-data="store.users"
|
:row-data="store.users"
|
||||||
:column-defs="columnDefs"
|
:column-defs="columnDefs"
|
||||||
:default-col-def="defaultColDef"
|
:default-col-def="{ sortable: true, resizable: true }"
|
||||||
:pagination="true"
|
:pagination="true"
|
||||||
:pagination-page-size="20"
|
:pagination-page-size="15"
|
||||||
:pagination-page-size-selector="[10, 20, 50, 100]"
|
|
||||||
:animate-rows="false"
|
|
||||||
row-selection="single"
|
|
||||||
dom-layout="normal"
|
dom-layout="normal"
|
||||||
@grid-ready="onGridReady"
|
@grid-ready="gridReady = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</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>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user