From fff84c552c3ae9ecf0b6a862871f1fe2c581be95 Mon Sep 17 00:00:00 2001 From: Ricardo Gonzalez Date: Fri, 29 May 2026 00:14:30 -0500 Subject: [PATCH] users: tabla + sync detalle + lookups para roles/companies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - db.ts v6: tablas users + lookups con schema - users-db.ts: CRUD users + lookups - user-sync.ts: fetch /api/users/{id}/ con concurrencia 5 - kappa-api.ts: +getUserDetail(id) - users store: +syncDetails() con progreso + mapeo roleId→name - UsersView: boton 'Sincronizar roles' con progreso - UserRecord: username, isStaff, phone, position, lastActive, createdAt --- src/services/db.ts | 33 +++++++++++++- src/services/kappa-api.ts | 4 ++ src/services/user-sync.ts | 95 +++++++++++++++++++++++++++++++++++++++ src/services/users-db.ts | 38 ++++++++++++++++ src/stores/users.ts | 38 ++++++++++++++++ src/views/UsersView.vue | 18 ++++++-- 6 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 src/services/user-sync.ts create mode 100644 src/services/users-db.ts diff --git a/src/services/db.ts b/src/services/db.ts index 09e33d1..5dacf65 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -69,6 +69,33 @@ export interface QAPlanRecord { createdAt: string } +export interface UserRecord { + id: number + email: string + firstName: string + lastName: string + fullName: string + username: string | null + isActive: boolean + isStaff: boolean + roleId: number | null + cellId: number | null + seniority: string | null + companyId: number | null + phone: string | null + position: string | null + lastActive: string | null + createdAt: string | null + updatedAt: string +} + +export interface LookupRecord { + type: string // 'role' | 'company' | 'seniority' + id: number + name: string + description: string | null +} + const db = new Dexie('alpha-core') as Dexie & { settings: Dexie.Table project_docs: Dexie.Table @@ -77,9 +104,11 @@ const db = new Dexie('alpha-core') as Dexie & { project_state: Dexie.Table hu_drafts: Dexie.Table qa_plans: Dexie.Table + users: Dexie.Table + lookups: Dexie.Table } -db.version(5).stores({ +db.version(6).stores({ settings: '&key', project_docs: '&projectId, projectName, updatedAt', sessions: '++id, projectId, date', @@ -87,6 +116,8 @@ db.version(5).stores({ project_state: '&projectId', hu_drafts: '&id, projectId, syncStatus', qa_plans: '&id, projectId', + users: '&id', + lookups: '&[type+id], type', }) export default db diff --git a/src/services/kappa-api.ts b/src/services/kappa-api.ts index f127994..ce64be5 100644 --- a/src/services/kappa-api.ts +++ b/src/services/kappa-api.ts @@ -99,6 +99,10 @@ class KappaAPI { return this.request('GET', '/users/all/') } + async getUserDetail(id: number): Promise> { + return this.request>('GET', `/users/${id}/`) + } + async getEmployees(page = 1): Promise> { return this.request>('GET', `/employees/?page=${page}`) } diff --git a/src/services/user-sync.ts b/src/services/user-sync.ts new file mode 100644 index 0000000..c5a00e3 --- /dev/null +++ b/src/services/user-sync.ts @@ -0,0 +1,95 @@ +import { kappa } from '@/services/kappa-api' +import { saveUser, saveLookup } from '@/services/users-db' +import type { UserRecord } from '@/services/db' + +const CONCURRENCY = 5 + +interface RawUserDetail { + id: number + email?: string + first_name?: string + last_name?: string + username?: string + is_active?: boolean + is_staff?: boolean + phone?: string + position?: string + role?: number | null + role_detail?: { id: number; name: string } | null + employee?: { company?: number | null; company_detail?: { id: number; name: string } | null } | null + last_login?: string + date_joined?: string +} + +/** + * Sincroniza detalles de usuarios desde KAPPA. + * Itera sobre los IDs dados, con límite de concurrencia. + * Guarda en tablas `users` + `lookups` (roles, companies). + */ +export async function syncUserDetails(userIds: number[], onProgress?: (done: number, total: number) => void): Promise<{ ok: number; fail: number }> { + let ok = 0 + let fail = 0 + const total = userIds.length + + // Procesar en lotes concurrentes + for (let i = 0; i < userIds.length; i += CONCURRENCY) { + const batch = userIds.slice(i, i + CONCURRENCY) + const results = await Promise.allSettled(batch.map(id => fetchAndSaveUser(id))) + + for (const r of results) { + if (r.status === 'fulfilled' && r.value) ok++ + else fail++ + } + + onProgress?.(Math.min(i + CONCURRENCY, total), total) + } + + console.log(`[UserSync] ${ok} OK, ${fail} fail de ${total}`) + return { ok, fail } +} + +async function fetchAndSaveUser(id: number): Promise { + try { + const raw = await kappa.getUserDetail(id) as unknown as RawUserDetail + if (!raw || !raw.id) return false + + const now = new Date().toISOString() + + // Guardar usuario + const user: UserRecord = { + id: raw.id, + email: raw.email || '', + firstName: raw.first_name || '', + lastName: raw.last_name || '', + fullName: `${raw.first_name || ''} ${raw.last_name || ''}`.trim() || raw.email || '', + username: raw.username || null, + isActive: raw.is_active !== undefined ? raw.is_active : true, + isStaff: raw.is_staff || false, + roleId: raw.role ?? null, + cellId: null, + seniority: null, + companyId: raw.employee?.company ?? null, + phone: raw.phone || null, + position: raw.position || null, + lastActive: raw.last_login || null, + createdAt: raw.date_joined || null, + updatedAt: now, + } + await saveUser(user) + + // Guardar lookup de rol + if (raw.role_detail?.id && raw.role_detail?.name) { + await saveLookup('role', raw.role_detail.id, raw.role_detail.name) + } + + // Guardar lookup de compañía + if (raw.employee?.company_detail?.id && raw.employee?.company_detail?.name) { + await saveLookup('company', raw.employee.company_detail.id, raw.employee.company_detail.name) + } + + return true + } catch (e) { + console.error(`[UserSync] Error fetching user ${id}:`, e) + return false + } +} diff --git a/src/services/users-db.ts b/src/services/users-db.ts new file mode 100644 index 0000000..66f7d47 --- /dev/null +++ b/src/services/users-db.ts @@ -0,0 +1,38 @@ +import db, { type UserRecord, type LookupRecord } from '@/services/db' + +// ─── Users ─────────────────────────────────────────────── + +export async function saveUser(u: UserRecord) { + await db.users.put(u) +} + +export async function getUsers(): Promise { + return db.users.toArray() +} + +export async function getUser(id: number): Promise { + return db.users.get(id) +} + +export async function deleteUser(id: number) { + await db.users.delete(id) +} + +// ─── Lookups ───────────────────────────────────────────── + +export async function saveLookup(type: string, id: number, name: string, description?: string) { + await db.lookups.put({ type, id, name, description: description ?? null }) +} + +export async function getLookups(type: string): Promise { + return db.lookups.where('type').equals(type).toArray() +} + +export async function getLookup(type: string, id: number): Promise { + return db.lookups.get([type, id]) +} + +export async function lookupsAsOptions(type: string): Promise<{ value: number; label: string }[]> { + const items = await getLookups(type) + return items.map(i => ({ value: i.id, label: i.name })) +} diff --git a/src/stores/users.ts b/src/stores/users.ts index c958c55..6018560 100644 --- a/src/stores/users.ts +++ b/src/stores/users.ts @@ -2,6 +2,8 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { kappa } from '@/services/kappa-api' import { tauriDb, type AlphaUserRecord, type CellRecord } from '@/services/tauri-db' +import { syncUserDetails } from '@/services/user-sync' +import { getLookups, getUsers } from '@/services/users-db' import type { KappaEmployee } from '@/types/kappa' export interface AlphaUser { @@ -28,6 +30,10 @@ export const useUsersStore = defineStore('users', () => { const error = ref(null) const totalPages = ref(1) const currentPage = ref(1) + const syncing = ref(false) + const syncProgress = ref(0) + const syncTotal = ref(0) + const roles = ref<{ id: number; name: string }[]>([]) const activeUsers = computed(() => users.value.filter(u => u.is_active)) const devsByProject = computed(() => { @@ -123,6 +129,33 @@ export const useUsersStore = defineStore('users', () => { } } + async function syncDetails() { + const userIds = users.value.map(u => u.id) + if (userIds.length === 0) return + syncing.value = true + syncProgress.value = 0 + syncTotal.value = userIds.length + try { + await syncUserDetails(userIds, (done, total) => { syncProgress.value = done; syncTotal.value = total }) + const roleItems = await getLookups('role') + roles.value = roleItems.map(r => ({ id: r.id, name: r.name })) + const dbUsers = await getUsers() + // Mapear roleId → role name a AlphaUser + for (const u of users.value) { + const record = dbUsers.find(d => d.id === u.id) + if (record?.roleId) { + const role = roles.value.find(r => r.id === record.roleId) + if (role) u.role = role.name + } + } + console.log(`[Users] Sync complete: ${dbUsers.length} users in DB, ${roles.value.length} roles`) + } catch (e) { + console.error('[Users] Sync error:', e) + } finally { + syncing.value = false + } + } + async function updateUserFields(id: number, role: string | null, seniority: string | null, cellId: number | null) { await tauriDb.updateUserFields(id, role, seniority, cellId) const idx = users.value.findIndex(u => u.id === id) @@ -164,11 +197,16 @@ export const useUsersStore = defineStore('users', () => { cells, loading, error, + syncing, + syncProgress, + syncTotal, + roles, totalPages, currentPage, activeUsers, devsByProject, fetchAll, + syncDetails, updateLocal, updateUserFields, createCell, diff --git a/src/views/UsersView.vue b/src/views/UsersView.vue index 788cdd5..a338de3 100644 --- a/src/views/UsersView.vue +++ b/src/views/UsersView.vue @@ -25,6 +25,7 @@ import { IconChevronLeft, IconChevronRight, IconSearch, + IconRefresh, } from '@tabler/icons-vue' const { t } = useI18n() @@ -114,9 +115,18 @@ onMounted(() => store.fetchAll()) {{ t('users.teamSubtitle', { users: store.users.length, emps: store.employees.length }) }}

- - {{ t('users.activeCount', { count: stats.active }) }} - +
+ + + Sincronizando {{ store.syncProgress }}/{{ store.syncTotal }}... + + + {{ t('users.activeCount', { count: stats.active }) }} + +
@@ -247,6 +257,7 @@ onMounted(() => store.fetchAll()) + ID {{ t('users.name') }} {{ t('users.email') }} {{ t('users.role') }} @@ -258,6 +269,7 @@ onMounted(() => store.fetchAll()) + {{ user.id }}