agregar modulo usuarios con AG Grid + tema shadcn + integracion KAPPA employees
This commit is contained in:
@@ -12,6 +12,8 @@
|
|||||||
"@tauri-apps/api": "^2.11.0",
|
"@tauri-apps/api": "^2.11.0",
|
||||||
"@tauri-apps/cli": "^2.11.2",
|
"@tauri-apps/cli": "^2.11.2",
|
||||||
"@vueuse/core": "^14.3.0",
|
"@vueuse/core": "^14.3.0",
|
||||||
|
"ag-grid-community": "^35.3.0",
|
||||||
|
"ag-grid-vue3": "^35.3.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dexie": "^4.0.4",
|
"dexie": "^4.0.4",
|
||||||
@@ -459,6 +461,12 @@
|
|||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
|
"ag-charts-types": ["ag-charts-types@13.3.0", "", {}, "sha512-UMoAn908LC4ZIJSNfUckSBEFa79Mi1vFRA8qIRx+NusEuuFgXDioCZx4MxM7O3rDXlxTWH9DvQmcDjh7vyd89w=="],
|
||||||
|
|
||||||
|
"ag-grid-community": ["ag-grid-community@35.3.0", "", { "dependencies": { "ag-charts-types": "13.3.0" } }, "sha512-c9WQWB88J965IjBC/GPUX30aAZix10o6oYT86DWipcxgLZTIQlLSilJJEr1bno/245rPEAIMjhoU1gp9VIfURg=="],
|
||||||
|
|
||||||
|
"ag-grid-vue3": ["ag-grid-vue3@35.3.0", "", { "dependencies": { "ag-grid-community": "35.3.0" }, "peerDependencies": { "vue": "^3.5.32" } }, "sha512-7UnAt/xMosY3WX6Lg7t8YF6QyLkWQL/fvsYzY5hyPvBGtX84VrikfiO4zBesiJECkhj+AzfNlD2E89sBSVMTgQ=="],
|
||||||
|
|
||||||
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||||
|
|
||||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
"@tauri-apps/api": "^2.11.0",
|
"@tauri-apps/api": "^2.11.0",
|
||||||
"@tauri-apps/cli": "^2.11.2",
|
"@tauri-apps/cli": "^2.11.2",
|
||||||
"@vueuse/core": "^14.3.0",
|
"@vueuse/core": "^14.3.0",
|
||||||
|
"ag-grid-community": "^35.3.0",
|
||||||
|
"ag-grid-vue3": "^35.3.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dexie": "^4.0.4",
|
"dexie": "^4.0.4",
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* AG Grid theme — shadcn-vue New York style.
|
||||||
|
* Hereda variables CSS de shadcn para light/dark automático.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.ag-theme-alpha {
|
||||||
|
--ag-active-color: var(--primary);
|
||||||
|
--ag-background-color: var(--background);
|
||||||
|
--ag-border-color: var(--border);
|
||||||
|
--ag-header-background-color: var(--muted);
|
||||||
|
--ag-header-foreground-color: var(--muted-foreground);
|
||||||
|
--ag-header-column-separator-color: var(--border);
|
||||||
|
--ag-odd-row-background-color: transparent;
|
||||||
|
--ag-row-hover-color: var(--accent);
|
||||||
|
--ag-selected-row-background-color: color-mix(in oklch, var(--primary) 10%, transparent);
|
||||||
|
--ag-font-family: var(--font-sans);
|
||||||
|
--ag-font-size: 13px;
|
||||||
|
--ag-row-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-critical: solid 1px;
|
||||||
|
--ag-borders-secondary: solid 1px;
|
||||||
|
--ag-borders-row: none;
|
||||||
|
--ag-borders-input: solid 1px;
|
||||||
|
--ag-cell-horizontal-padding: 12px;
|
||||||
|
--ag-card-radius: var(--radius);
|
||||||
|
--ag-card-shadow: var(--shadow-sm);
|
||||||
|
--ag-wrapper-border-radius: var(--radius);
|
||||||
|
--ag-pagination-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-theme-alpha .ag-root-wrapper {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-theme-alpha .ag-header-cell {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-theme-alpha .ag-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import type {
|
|||||||
KappaPlanningEntry,
|
KappaPlanningEntry,
|
||||||
KappaBusinessRule,
|
KappaBusinessRule,
|
||||||
KappaRequirement,
|
KappaRequirement,
|
||||||
|
KappaEmployee,
|
||||||
|
PaginatedResponse,
|
||||||
} from '@/types/kappa'
|
} from '@/types/kappa'
|
||||||
|
|
||||||
const BASE = '/api'
|
const BASE = '/api'
|
||||||
@@ -92,6 +94,10 @@ class KappaAPI {
|
|||||||
return this.request<unknown[]>('GET', '/users/all/')
|
return this.request<unknown[]>('GET', '/users/all/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEmployees(page = 1): Promise<PaginatedResponse<KappaEmployee>> {
|
||||||
|
return this.request<PaginatedResponse<KappaEmployee>>('GET', `/employees/?page=${page}`)
|
||||||
|
}
|
||||||
|
|
||||||
async getUserStories(initiativeId?: number): Promise<KappaUserStory[]> {
|
async getUserStories(initiativeId?: number): Promise<KappaUserStory[]> {
|
||||||
const path = initiativeId ? `/userstorys/?initiative=${initiativeId}` : '/userstorys/'
|
const path = initiativeId ? `/userstorys/?initiative=${initiativeId}` : '/userstorys/'
|
||||||
return this.request<KappaUserStory[]>('GET', path)
|
return this.request<KappaUserStory[]>('GET', path)
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { kappa } from '@/services/kappa-api'
|
||||||
|
import type { KappaEmployee } from '@/types/kappa'
|
||||||
|
|
||||||
|
export interface AlphaUser {
|
||||||
|
id: number
|
||||||
|
email: string
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
full_name: string
|
||||||
|
is_active: boolean
|
||||||
|
cell?: string
|
||||||
|
role?: string
|
||||||
|
seniority?: string
|
||||||
|
projects: string[]
|
||||||
|
projects_count: number
|
||||||
|
employee_ref?: KappaEmployee
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUsersStore = defineStore('users', () => {
|
||||||
|
const users = ref<AlphaUser[]>([])
|
||||||
|
const employees = ref<KappaEmployee[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
const totalPages = ref(1)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
|
||||||
|
const activeUsers = computed(() => users.value.filter(u => u.is_active))
|
||||||
|
const devsByProject = computed(() => {
|
||||||
|
const map = new Map<string, AlphaUser[]>()
|
||||||
|
for (const u of users.value) {
|
||||||
|
for (const p of u.projects) {
|
||||||
|
if (!map.has(p)) map.set(p, [])
|
||||||
|
map.get(p)!.push(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchAll() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const [rawUsers, page1] = await Promise.all([
|
||||||
|
kappa.getUsers(),
|
||||||
|
kappa.getEmployees(1),
|
||||||
|
])
|
||||||
|
|
||||||
|
employees.value = page1.results
|
||||||
|
|
||||||
|
totalPages.value = Math.ceil(page1.count / 25)
|
||||||
|
if (totalPages.value > 1) {
|
||||||
|
const restPages = []
|
||||||
|
for (let p = 2; p <= totalPages.value; p++) {
|
||||||
|
restPages.push(kappa.getEmployees(p))
|
||||||
|
}
|
||||||
|
const rest = await Promise.all(restPages)
|
||||||
|
for (const r of rest) {
|
||||||
|
employees.value.push(...r.results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
users.value = buildUsers(rawUsers, employees.value)
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUsers(raw: unknown[], emps: KappaEmployee[]): AlphaUser[] {
|
||||||
|
return raw.map((u: any) => {
|
||||||
|
const userEmps = emps.filter(e => e.user === u.id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: u.id,
|
||||||
|
email: u.email || '',
|
||||||
|
first_name: u.first_name || '',
|
||||||
|
last_name: u.last_name || '',
|
||||||
|
full_name: u.full_name || `${u.first_name || ''} ${u.last_name || ''}`.trim() || u.email,
|
||||||
|
is_active: u.is_active !== undefined ? u.is_active : true,
|
||||||
|
projects: userEmps
|
||||||
|
.filter(e => e.initiative_name)
|
||||||
|
.map(e => e.initiative_name!),
|
||||||
|
projects_count: userEmps.filter(e => e.initiative).length,
|
||||||
|
employee_ref: userEmps[0] || undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLocal(id: number, data: Partial<AlphaUser>) {
|
||||||
|
const idx = users.value.findIndex(u => u.id === id)
|
||||||
|
if (idx !== -1) {
|
||||||
|
users.value[idx] = { ...users.value[idx], ...data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
employees,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
totalPages,
|
||||||
|
currentPage,
|
||||||
|
activeUsers,
|
||||||
|
devsByProject,
|
||||||
|
fetchAll,
|
||||||
|
updateLocal,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -5,6 +5,10 @@
|
|||||||
|
|
||||||
@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 {
|
||||||
|
|||||||
@@ -101,6 +101,21 @@ export interface KappaRequirement {
|
|||||||
name_requirement?: string
|
name_requirement?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KappaEmployee {
|
||||||
|
id: number
|
||||||
|
first_name?: string
|
||||||
|
last_name?: string
|
||||||
|
full_name?: string
|
||||||
|
email?: string
|
||||||
|
user?: number
|
||||||
|
initiative?: number | null
|
||||||
|
initiative_name?: string
|
||||||
|
initiative_key?: string
|
||||||
|
position?: string
|
||||||
|
is_active?: boolean
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaginatedResponse<T> {
|
export interface PaginatedResponse<T> {
|
||||||
count: number
|
count: number
|
||||||
next: string | null
|
next: string | null
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import SiteHeader from "@/components/dashboard/SiteHeader.vue"
|
|||||||
import SectionCards from "@/components/dashboard/SectionCards.vue"
|
import SectionCards from "@/components/dashboard/SectionCards.vue"
|
||||||
import DashboardView from "@/views/DashboardView.vue"
|
import DashboardView from "@/views/DashboardView.vue"
|
||||||
import ProjectListView from "@/views/ProjectListView.vue"
|
import ProjectListView from "@/views/ProjectListView.vue"
|
||||||
|
import UsersView from "@/views/UsersView.vue"
|
||||||
|
|
||||||
const sidebarStyle = {
|
const sidebarStyle = {
|
||||||
'--sidebar-width': '16rem',
|
'--sidebar-width': '16rem',
|
||||||
@@ -149,6 +150,9 @@ const tabContent: Record<string, { title: string; description: string; cards: {
|
|||||||
@select-project="openProject()"
|
@select-project="openProject()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="activeTab === 'team'">
|
||||||
|
<UsersView />
|
||||||
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<SectionCards :cards="tabContent[activeTab]?.cards ?? []" />
|
<SectionCards :cards="tabContent[activeTab]?.cards ?? []" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
<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>
|
||||||
Reference in New Issue
Block a user