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 }}