From 04d9d6cabc5fc47e3ef1a021ddccba21d4438f87 Mon Sep 17 00:00:00 2001 From: Ricardo Gonzalez Date: Tue, 26 May 2026 08:02:50 -0500 Subject: [PATCH] agregar modulo usuarios con AG Grid + tema shadcn + integracion KAPPA employees --- bun.lock | 8 ++ package.json | 2 + src/assets/ag-grid-alpha.css | 58 ++++++++++ src/services/kappa-api.ts | 6 + src/stores/users.ts | 111 ++++++++++++++++++ src/style.css | 4 + src/types/kappa.ts | 15 +++ src/views/NewDashboardView.vue | 4 + src/views/UsersView.vue | 205 +++++++++++++++++++++++++++++++++ 9 files changed, 413 insertions(+) create mode 100644 src/assets/ag-grid-alpha.css create mode 100644 src/stores/users.ts create mode 100644 src/views/UsersView.vue diff --git a/bun.lock b/bun.lock index 6388794..561777b 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,8 @@ "@tauri-apps/api": "^2.11.0", "@tauri-apps/cli": "^2.11.2", "@vueuse/core": "^14.3.0", + "ag-grid-community": "^35.3.0", + "ag-grid-vue3": "^35.3.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "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=="], + "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-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], diff --git a/package.json b/package.json index 71610ef..d36448c 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "@tauri-apps/api": "^2.11.0", "@tauri-apps/cli": "^2.11.2", "@vueuse/core": "^14.3.0", + "ag-grid-community": "^35.3.0", + "ag-grid-vue3": "^35.3.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dexie": "^4.0.4", diff --git a/src/assets/ag-grid-alpha.css b/src/assets/ag-grid-alpha.css new file mode 100644 index 0000000..bc028b9 --- /dev/null +++ b/src/assets/ag-grid-alpha.css @@ -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; +} diff --git a/src/services/kappa-api.ts b/src/services/kappa-api.ts index ab6dc85..fc4da2e 100644 --- a/src/services/kappa-api.ts +++ b/src/services/kappa-api.ts @@ -9,6 +9,8 @@ import type { KappaPlanningEntry, KappaBusinessRule, KappaRequirement, + KappaEmployee, + PaginatedResponse, } from '@/types/kappa' const BASE = '/api' @@ -92,6 +94,10 @@ class KappaAPI { return this.request('GET', '/users/all/') } + async getEmployees(page = 1): Promise> { + return this.request>('GET', `/employees/?page=${page}`) + } + async getUserStories(initiativeId?: number): Promise { const path = initiativeId ? `/userstorys/?initiative=${initiativeId}` : '/userstorys/' return this.request('GET', path) diff --git a/src/stores/users.ts b/src/stores/users.ts new file mode 100644 index 0000000..e10f4d8 --- /dev/null +++ b/src/stores/users.ts @@ -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([]) + const employees = ref([]) + const loading = ref(false) + const error = ref(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() + 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) { + 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, + } +}) diff --git a/src/style.css b/src/style.css index b5830c0..55d2291 100644 --- a/src/style.css +++ b/src/style.css @@ -5,6 +5,10 @@ @import "tw-animate-css"; +@import "ag-grid-community/styles/ag-grid.css"; + +@import "./assets/ag-grid-alpha.css"; + @custom-variant dark (&:is(.dark *)); :root { diff --git a/src/types/kappa.ts b/src/types/kappa.ts index e9a5c1a..93d16d7 100644 --- a/src/types/kappa.ts +++ b/src/types/kappa.ts @@ -101,6 +101,21 @@ export interface KappaRequirement { 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 { count: number next: string | null diff --git a/src/views/NewDashboardView.vue b/src/views/NewDashboardView.vue index 3d3db73..18a88e6 100644 --- a/src/views/NewDashboardView.vue +++ b/src/views/NewDashboardView.vue @@ -6,6 +6,7 @@ import SiteHeader from "@/components/dashboard/SiteHeader.vue" import SectionCards from "@/components/dashboard/SectionCards.vue" import DashboardView from "@/views/DashboardView.vue" import ProjectListView from "@/views/ProjectListView.vue" +import UsersView from "@/views/UsersView.vue" const sidebarStyle = { '--sidebar-width': '16rem', @@ -149,6 +150,9 @@ const tabContent: Record + diff --git a/src/views/UsersView.vue b/src/views/UsersView.vue new file mode 100644 index 0000000..de59c03 --- /dev/null +++ b/src/views/UsersView.vue @@ -0,0 +1,205 @@ + + + + +