users: tabla + sync detalle + lookups para roles/companies
- 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
This commit is contained in:
+32
-1
@@ -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<SettingEntry, string>
|
||||
project_docs: Dexie.Table<ProjectDocRecord, number>
|
||||
@@ -77,9 +104,11 @@ const db = new Dexie('alpha-core') as Dexie & {
|
||||
project_state: Dexie.Table<ProjectStateRecord, number>
|
||||
hu_drafts: Dexie.Table<HuDraftRecord, string>
|
||||
qa_plans: Dexie.Table<QAPlanRecord, string>
|
||||
users: Dexie.Table<UserRecord, number>
|
||||
lookups: Dexie.Table<LookupRecord, string>
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -99,6 +99,10 @@ class KappaAPI {
|
||||
return this.request<unknown[]>('GET', '/users/all/')
|
||||
}
|
||||
|
||||
async getUserDetail(id: number): Promise<Record<string, unknown>> {
|
||||
return this.request<Record<string, unknown>>('GET', `/users/${id}/`)
|
||||
}
|
||||
|
||||
async getEmployees(page = 1): Promise<PaginatedResponse<KappaEmployee>> {
|
||||
return this.request<PaginatedResponse<KappaEmployee>>('GET', `/employees/?page=${page}`)
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<UserRecord[]> {
|
||||
return db.users.toArray()
|
||||
}
|
||||
|
||||
export async function getUser(id: number): Promise<UserRecord | undefined> {
|
||||
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<LookupRecord[]> {
|
||||
return db.lookups.where('type').equals(type).toArray()
|
||||
}
|
||||
|
||||
export async function getLookup(type: string, id: number): Promise<LookupRecord | undefined> {
|
||||
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 }))
|
||||
}
|
||||
Reference in New Issue
Block a user