273 lines
9.6 KiB
TypeScript
273 lines
9.6 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
import { authFetch } from '../../utils/authFetch'
|
|
|
|
export type Level1Child = {
|
|
userId: number
|
|
email: string
|
|
name: string
|
|
position: number | null
|
|
}
|
|
|
|
export type Level2PlusItem = {
|
|
userId: number
|
|
depth: number
|
|
nameMasked: string
|
|
}
|
|
|
|
export type PersonalOverview = {
|
|
matrixInstanceId: string | number
|
|
totalUsersUnderMe: number
|
|
levelsFilled: number
|
|
immediateChildrenCount: number
|
|
rootSlotsRemaining: number | null
|
|
level1: Level1Child[]
|
|
level2Plus: Level2PlusItem[]
|
|
}
|
|
|
|
export type UserMatrix = {
|
|
id: string | number
|
|
name: string
|
|
membersCount?: number
|
|
createdAt?: string
|
|
description?: string
|
|
highestFullLevel?: number
|
|
percentFull?: number
|
|
filledSlots?: number
|
|
totalSlots?: number
|
|
totalUsers?: number
|
|
// new quick-summary fields from list endpoint
|
|
totalUsersUnderMe?: number
|
|
matrixFillPercent?: number
|
|
}
|
|
|
|
export function useMyMatrices() {
|
|
const [matrices, setMatrices] = useState<UserMatrix[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const abortRef = useRef<AbortController | null>(null)
|
|
|
|
useEffect(() => {
|
|
abortRef.current?.abort()
|
|
const controller = new AbortController()
|
|
abortRef.current = controller
|
|
|
|
const base = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '')
|
|
const url = `${base}/api/matrix/me/list`
|
|
console.log('[useMyMatrices] Fetching:', url)
|
|
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
authFetch(url, {
|
|
method: 'GET',
|
|
headers: { Accept: 'application/json' },
|
|
credentials: 'include',
|
|
signal: controller.signal as any
|
|
})
|
|
.then(async r => {
|
|
const ct = r.headers.get('content-type') || ''
|
|
console.log('[useMyMatrices] Status:', r.status, 'CT:', ct)
|
|
if (!r.ok || !ct.includes('application/json')) {
|
|
const txt = await r.text().catch(() => '')
|
|
console.warn('[useMyMatrices] Non-JSON or error body:', txt.slice(0, 200))
|
|
throw new Error(`Request failed: ${r.status} ${txt.slice(0, 120)}`)
|
|
}
|
|
const json = await r.json()
|
|
console.log('[useMyMatrices] Raw JSON:', json)
|
|
const arr = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : []
|
|
if (!Array.isArray(arr)) {
|
|
console.warn('[useMyMatrices] Expected array, got:', typeof arr)
|
|
}
|
|
const mapped: UserMatrix[] = arr.map((m: any, idx: number) => {
|
|
const id = m.id ?? m.matrixInstanceId ?? m._id
|
|
const name = String(m.name ?? m.title ?? `Matrix ${id ?? ''}`).trim()
|
|
|
|
// summary metrics directly from the list route
|
|
const totalUsersUnderMe =
|
|
m.totalUsersUnderMe != null ? Number(m.totalUsersUnderMe) : undefined
|
|
const highestFullLevel =
|
|
m.highestFullLevel != null ? Number(m.highestFullLevel) :
|
|
m.maxFilledLevel != null ? Number(m.maxFilledLevel) :
|
|
m.highestLevelFull != null ? Number(m.highestLevelFull) :
|
|
undefined
|
|
const matrixFillPercentRaw =
|
|
m.matrixFillPercent != null ? Number(m.matrixFillPercent) : undefined
|
|
const matrixFillPercent =
|
|
matrixFillPercentRaw != null ? Math.max(0, Math.min(100, matrixFillPercentRaw)) : undefined
|
|
|
|
// legacy/optional fields (kept for backward compatibility)
|
|
const membersCount =
|
|
m.membersCount != null ? Number(m.membersCount) :
|
|
m.memberCount != null ? Number(m.memberCount) :
|
|
m.totalUsers != null ? Number(m.totalUsers) :
|
|
undefined
|
|
const filledSlots =
|
|
m.filledSlots != null ? Number(m.filledSlots) :
|
|
m.occupiedSlots != null ? Number(m.occupiedSlots) :
|
|
undefined
|
|
const totalSlots =
|
|
m.totalSlots != null ? Number(m.totalSlots) :
|
|
m.capacitySlots != null ? Number(m.capacitySlots) :
|
|
undefined
|
|
const percentFromApi =
|
|
m.percentFull != null ? Number(m.percentFull) :
|
|
m.fillPercent != null ? Number(m.fillPercent) :
|
|
undefined
|
|
const percentComputed =
|
|
filledSlots != null && totalSlots != null && totalSlots > 0
|
|
? (filledSlots / totalSlots) * 100
|
|
: undefined
|
|
const percent = percentFromApi ?? percentComputed
|
|
const percentClamped = percent != null ? Math.max(0, Math.min(100, percent)) : undefined
|
|
const createdAt = m.createdAt ? String(m.createdAt) : (m.created_at ? String(m.created_at) : undefined)
|
|
const description = m.description != null ? String(m.description) : undefined
|
|
const totalUsers = m.totalUsers != null ? Number(m.totalUsers) : undefined
|
|
|
|
const out = {
|
|
id,
|
|
name,
|
|
membersCount,
|
|
createdAt,
|
|
description,
|
|
highestFullLevel, // from list
|
|
totalUsersUnderMe, // from list
|
|
matrixFillPercent, // from list
|
|
filledSlots,
|
|
totalSlots,
|
|
percentFull: percentClamped,
|
|
totalUsers
|
|
}
|
|
|
|
console.log('[useMyMatrices] Map item', idx, { source: m, mapped: out })
|
|
return out
|
|
})
|
|
console.log('[useMyMatrices] Final mapped list:', mapped)
|
|
setMatrices(mapped)
|
|
})
|
|
.catch((e: any) => {
|
|
if (controller.signal.aborted || e?.name === 'AbortError') {
|
|
console.log('[useMyMatrices] Aborted')
|
|
return
|
|
}
|
|
console.error('[useMyMatrices] Error:', e)
|
|
setError(e?.message || 'Failed to load matrices')
|
|
setMatrices([])
|
|
})
|
|
.finally(() => {
|
|
if (!controller.signal.aborted) {
|
|
setLoading(false)
|
|
console.log('[useMyMatrices] Done')
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
console.log('[useMyMatrices] Cleanup abort')
|
|
controller.abort()
|
|
}
|
|
}, [])
|
|
|
|
return { matrices, loading, error }
|
|
}
|
|
|
|
export function usePersonalMatrixOverview(matrixId?: string | number) {
|
|
const [data, setData] = useState<PersonalOverview | null>(null)
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const abortRef = useRef<AbortController | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (matrixId == null) {
|
|
console.log('[usePersonalMatrixOverview] No matrixId, skipping fetch')
|
|
setData(null)
|
|
setLoading(false)
|
|
return
|
|
}
|
|
|
|
abortRef.current?.abort()
|
|
const controller = new AbortController()
|
|
abortRef.current = controller
|
|
|
|
const base = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '')
|
|
const url = `${base}/api/matrix/${encodeURIComponent(String(matrixId))}/overview`
|
|
console.log('[usePersonalMatrixOverview] Fetching:', url)
|
|
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
authFetch(url, {
|
|
method: 'GET',
|
|
headers: { Accept: 'application/json' },
|
|
credentials: 'include',
|
|
signal: controller.signal as any
|
|
})
|
|
.then(async r => {
|
|
const ct = r.headers.get('content-type') || ''
|
|
console.log('[usePersonalMatrixOverview] Status:', r.status, 'CT:', ct)
|
|
const isJson = ct.includes('application/json')
|
|
if (!r.ok) {
|
|
const body = isJson ? await r.json().catch(() => ({})) : await r.text().catch(() => '')
|
|
const msg = isJson ? (body?.message || JSON.stringify(body) || 'Request failed') : (String(body || '').slice(0, 200) || 'Request failed')
|
|
throw new Error(`Request failed: ${r.status} ${msg}`)
|
|
}
|
|
if (!isJson) {
|
|
const txt = await r.text().catch(() => '')
|
|
throw new Error(`Request failed: ${r.status} ${txt.slice(0, 120)}`)
|
|
}
|
|
const json = await r.json()
|
|
console.log('[usePersonalMatrixOverview] Raw JSON:', json)
|
|
const d = json?.data || json
|
|
const mapped: PersonalOverview = {
|
|
matrixInstanceId: d.matrixInstanceId ?? matrixId,
|
|
totalUsersUnderMe: Number(d.totalUsersUnderMe ?? 0),
|
|
levelsFilled: Number(d.levelsFilled ?? 0),
|
|
immediateChildrenCount: Number(d.immediateChildrenCount ?? 0),
|
|
rootSlotsRemaining: d.rootSlotsRemaining == null ? null : Number(d.rootSlotsRemaining),
|
|
level1: Array.isArray(d.level1) ? d.level1.map((c: any) => ({
|
|
userId: Number(c.userId),
|
|
email: String(c.email || ''),
|
|
name: String(c.name || c.email || ''),
|
|
position: c.position == null ? null : Number(c.position)
|
|
})) : [],
|
|
level2Plus: Array.isArray(d.level2Plus) ? d.level2Plus.map((x: any) => ({
|
|
userId: Number(x.userId),
|
|
depth: Number(x.depth ?? 0),
|
|
nameMasked: String(x.name ?? '')
|
|
})) : []
|
|
}
|
|
console.log('[usePersonalMatrixOverview] Mapped:', mapped)
|
|
setData(mapped)
|
|
})
|
|
.catch((e: any) => {
|
|
if (controller.signal.aborted || e?.name === 'AbortError') {
|
|
console.log('[usePersonalMatrixOverview] Aborted')
|
|
return
|
|
}
|
|
console.error('[usePersonalMatrixOverview] Error:', e)
|
|
setError(e?.message || 'Failed to load overview')
|
|
setData(null)
|
|
})
|
|
.finally(() => {
|
|
if (!controller.signal.aborted) {
|
|
setLoading(false)
|
|
console.log('[usePersonalMatrixOverview] Done')
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
console.log('[usePersonalMatrixOverview] Cleanup abort')
|
|
controller.abort()
|
|
}
|
|
}, [matrixId])
|
|
|
|
const meta = useMemo(() => {
|
|
const m = {
|
|
countL1: data?.level1.length ?? 0,
|
|
countL2Plus: data?.level2Plus.length ?? 0
|
|
}
|
|
console.log('[usePersonalMatrixOverview] Meta computed:', m)
|
|
return m
|
|
}, [data])
|
|
|
|
return { data, loading, error, meta }
|
|
}
|