profit-planet-frontend/src/app/personal-matrix/hooks/getStats.ts
2025-12-06 12:34:04 +01:00

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