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([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const abortRef = useRef(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(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const abortRef = useRef(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 } }