'use client' import { useTranslation } from '../../../i18n/useTranslation'; import React, { useEffect, useMemo, useState, Suspense } from 'react' // CHANGED: add Suspense import { useSearchParams, useRouter } from 'next/navigation' import PageLayout from '../../../components/PageLayout' import { ArrowLeftIcon, MagnifyingGlassIcon, PlusIcon, UserIcon, BuildingOffice2Icon } from '@heroicons/react/24/outline' import { useMatrixUsers, MatrixUser } from './hooks/getStats' import useAuthStore from '../../../store/authStore' import { getMatrixStats } from '../hooks/getMatrixStats' import SearchModal from './components/searchModal' const DEFAULT_FETCH_DEPTH = 50 // provisional large depth to approximate unlimited const LEVEL_CAP = (level: number) => Math.pow(5, level) // L1=5, L2=25, ... function MatrixDetailPageInner() { const { t } = useTranslation() const sp = useSearchParams() const router = useRouter() const matrixId = sp.get('id') || 'm-1' const matrixName = sp.get('name') || 'Unnamed Matrix' const topNodeEmail = sp.get('top') || 'top@example.com' const rootUserIdParam = sp.get('rootUserId') const rootUserId = rootUserIdParam ? Number(rootUserIdParam) : undefined console.info('[MatrixDetailPage] Params', { matrixId, matrixName, topNodeEmail, rootUserId }) // Resolve rootUserId when missing by looking it up via stats const accessToken = useAuthStore(s => s.accessToken) const [resolvedRootUserId, setResolvedRootUserId] = useState(rootUserId) // NEW: track policy (DB) max depth (null => unlimited) const [policyMaxDepth, setPolicyMaxDepth] = useState(null) useEffect(() => { let cancelled = false async function resolveRoot() { if (rootUserId && rootUserId > 0) { console.info('[MatrixDetailPage] Using rootUserId from URL', { rootUserId }) setResolvedRootUserId(rootUserId) return } if (!accessToken) { console.warn('[MatrixDetailPage] No accessToken; cannot resolve rootUserId from stats') setResolvedRootUserId(undefined) return } if (!matrixId) { console.warn('[MatrixDetailPage] No matrixId; cannot resolve rootUserId from stats') setResolvedRootUserId(undefined) return } console.info('[MatrixDetailPage] Resolving rootUserId via stats for matrixId', { matrixId }) const res = await getMatrixStats({ token: accessToken, baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL }) console.debug('[MatrixDetailPage] getMatrixStats result', res) if (!res.ok) { console.error('[MatrixDetailPage] getMatrixStats failed', { status: res.status, message: res.message }) setResolvedRootUserId(undefined) return } const body = res.body || {} const matrices = (body?.data?.matrices ?? body?.matrices ?? []) as any[] console.debug('[MatrixDetailPage] Stats matrices overview', { count: matrices.length, ids: matrices.map((m: any) => m?.id ?? m?.matrixId), matrixIds: matrices.map((m: any) => m?.matrixId ?? m?.id), rootUserIds: matrices.map((m: any) => m?.rootUserId ?? m?.root_user_id), emails: matrices.map((m: any) => m?.topNodeEmail ?? m?.email) }) const found = matrices.find((m: any) => String(m?.id) === String(matrixId) || String(m?.matrixId) === String(matrixId) ) // NEW: extract policy maxDepth (may be null) const pmRaw = found?.maxDepth ?? found?.max_depth ?? null if (!cancelled) { setPolicyMaxDepth(pmRaw == null ? null : Number(pmRaw)) } const ru = Number(found?.rootUserId ?? found?.root_user_id) if (ru > 0 && !cancelled) { console.info('[MatrixDetailPage] Resolved rootUserId from stats', { matrixId, rootUserId: ru }) setResolvedRootUserId(ru) } else { console.warn('[MatrixDetailPage] Could not resolve rootUserId from stats', { matrixId, found }) setResolvedRootUserId(undefined) } } resolveRoot() return () => { cancelled = true } }, [matrixId, rootUserId, accessToken]) // Backend users (changed depth from 5 to DEFAULT_FETCH_DEPTH) const { users: fetchedUsers, loading: usersLoading, error: usersError, meta, refetch, serverMaxDepth } = useMatrixUsers(resolvedRootUserId, { depth: DEFAULT_FETCH_DEPTH, includeRoot: true, limit: 2000, offset: 0, matrixId, topNodeEmail }) // Prepare for backend fetches const [users, setUsers] = useState([]) useEffect(() => { console.info('[MatrixDetailPage] useMatrixUsers state', { loading: usersLoading, error: !!usersError, fetchedCount: fetchedUsers.length, meta }) setUsers(fetchedUsers) }, [fetchedUsers, usersLoading, usersError, meta]) // Modal state const [open, setOpen] = useState(false) // ADD: global search state (was removed) const [globalSearch, setGlobalSearch] = useState('') // Refresh overlay state const [refreshing, setRefreshing] = useState(false) // Collapsed state for each level const [collapsedLevels, setCollapsedLevels] = useState<{ [level: number]: boolean }>({ 0: true, 1: true, 2: true, 3: true, 4: true, 5: true }) // Per-level search const [levelSearch, setLevelSearch] = useState<{ [level: number]: string }>({ 0: '', 1: '', 2: '', 3: '', 4: '', 5: '' }) // Counts per level and next available level logic const byLevel = useMemo(() => { const map = new Map() users.forEach(u => { if (!u.name) { console.warn('[MatrixDetailPage] User missing name, fallback email used', { id: u.id, email: u.email }) } const lvl = (typeof u.level === 'number' && u.level >= 0) ? u.level : 0 const arr = map.get(lvl) || [] arr.push({ ...u, level: lvl }) map.set(lvl, arr) }) console.debug('[MatrixDetailPage] byLevel computed', { levels: Array.from(map.keys()), total: users.length }) return map }, [users]) const nextAvailableLevel = () => { let lvl = 1 while (true) { const current = byLevel.get(lvl)?.length || 0 if (current < LEVEL_CAP(lvl)) return lvl lvl += 1 if (lvl > 8) return lvl // safety ceiling in demo } } const addToMatrix = (u: Omit) => { const level = nextAvailableLevel() console.info('[MatrixDetailPage] addToMatrix', { userId: u.id, nextLevel: level }) setUsers(prev => [...prev, { ...u, level }]) } // Simple chip for user const UserChip = ({ u }: { u: MatrixUser }) => (
{u.type === 'company' ? : } {u.name}
) // Global search (already present) + node collapse state const [collapsedNodes, setCollapsedNodes] = useState>({}) const toggleNode = (id: number) => setCollapsedNodes(p => ({ ...p, [id]: !p[id] })) // Build children adjacency map const childrenMap = useMemo(() => { const m = new Map() users.forEach(u => { if (u.parentUserId != null) { const arr = m.get(u.parentUserId) || [] arr.push(u) m.set(u.parentUserId, arr) } }) // sort children by optional position then id m.forEach(arr => arr.sort((a,b) => { const pa = (a as any).position ?? 0 const pb = (b as any).position ?? 0 return pa - pb || a.id - b.id })) return m }, [users]) // Root node const rootNode = useMemo( () => users.find(u => u.level === 0 || u.id === resolvedRootUserId), [users, resolvedRootUserId] ) const rootChildren = useMemo(() => rootNode ? (childrenMap.get(rootNode.id) || []) : [], [rootNode, childrenMap]) const rootChildrenCount = useMemo(() => rootChildren.length, [rootChildren]) const displayedRootSlots = useMemo(() => rootChildren.filter(c => { const pos = Number((c as any).position ?? -1) return pos >= 1 && pos <= 5 }).length , [rootChildren]) // Rogue count if flags exist const rogueCount = useMemo( () => users.filter(u => (u as any).rogueUser || (u as any).rogue_user || (u as any).rogue).length, [users] ) // Filter match helper const searchLower = globalSearch.trim().toLowerCase() const matchesSearch = (u: MatrixUser) => !searchLower || u.name.toLowerCase().includes(searchLower) || u.email.toLowerCase().includes(searchLower) // Determine which nodes should be visible when searching: // Show all ancestors of matching nodes so the path is visible. const visibleIds = useMemo(() => { if (!searchLower) return new Set(users.map(u => u.id)) const matchIds = new Set() const parentMap = new Map() users.forEach(u => parentMap.set(u.id, u.parentUserId == null ? undefined : u.parentUserId)) users.forEach(u => { if (matchesSearch(u)) { let cur: number | undefined = u.id while (cur != null) { if (!matchIds.has(cur)) matchIds.add(cur) cur = parentMap.get(cur) } } }) return matchIds }, [users, searchLower]) // Auto-expand ancestors for search results useEffect(() => { if (!searchLower) return // Expand all visible nodes containing matches setCollapsedNodes(prev => { const next = { ...prev } visibleIds.forEach(id => { next[id] = false }) return next }) }, [searchLower, visibleIds]) // Tree renderer (inside component to access scope) const renderNode = (node: MatrixUser, depth: number) => { const children = childrenMap.get(node.id) || [] const hasChildren = children.length > 0 const collapsed = collapsedNodes[node.id] const highlight = matchesSearch(node) && searchLower.length > 0 if (!visibleIds.has(node.id)) return null const isRoot = node.level === 0 const pos = node.position ?? null return (
  • {hasChildren && ( )} {!hasChildren && } {node.type === 'company' ? : } {node.name} L{node.level} {(node as any).rogueUser || (node as any).rogue_user || (node as any).rogue ? ( Rogue ) : null} {pos != null && ( pos {pos} )} {isRoot && ( Unlimited; positions numbered sequentially )} {!isRoot && hasChildren && ( {children.length}/5 (slots 1–5) )}
    {hasChildren && !collapsed && (
      {children.map(c => renderNode(c, depth + 1))}
    )}
  • ) } // CSV export (now all users fetched) const exportCsv = () => { const rows = [['id','name','email','type','level','parentUserId','rogue']] users.forEach(u => rows.push([ u.id, u.name, u.email, u.type, u.level, u.parentUserId ?? '', ((u as any).rogueUser || (u as any).rogue_user || (u as any).rogue) ? 'true' : 'false' ] as any)) const csv = rows.map(r => r.map(v => `"${String(v).replace(/"/g,'""')}"`).join(',')).join('\n') const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }) const a = document.createElement('a') a.href = URL.createObjectURL(blob) a.download = `matrix-${matrixId}-unlimited.csv` a.click() URL.revokeObjectURL(a.href) } // When modal closes, refetch backend to sync page data const handleModalClose = () => { setOpen(false) setRefreshing(true) refetch() // triggers hook reload } // Stop spinner when hook finishes loading useEffect(() => { if (!usersLoading && refreshing) { setRefreshing(false) } }, [usersLoading, refreshing]) // REMOVE old isUnlimited derivation using serverMaxDepth; REPLACE with policy-based // const isUnlimited = !serverMaxDepth || serverMaxDepth <= 0; const isUnlimited = policyMaxDepth == null || policyMaxDepth <= 0 // NEW const policyDepth = (policyMaxDepth && policyMaxDepth > 0) ? policyMaxDepth : null const perLevelCounts = useMemo(() => { const m = new Map() users.forEach(u => { if (u.level != null && u.level >= 0) { m.set(u.level, (m.get(u.level) || 0) + 1) } }) return m }, [users]) const totalNonRoot = useMemo(() => users.filter(u => (u.level ?? 0) > 0).length, [users]) const fillMetrics = useMemo(() => { if (!policyDepth) return { label: 'N/A (unlimited policy)', highestFull: 'N/A' } let capacitySum = 0 let highestFullLevel: number | null = null for (let k = 1; k <= policyDepth; k++) { const cap = Math.pow(5, k) capacitySum += cap const lvlCount = perLevelCounts.get(k) || 0 if (lvlCount >= cap) highestFullLevel = k } if (capacitySum === 0) return { label: 'N/A', highestFull: 'N/A' } const pct = Math.round((totalNonRoot / capacitySum) * 100 * 100) / 100 return { label: `${pct}%`, highestFull: highestFullLevel == null ? 'None' : `L${highestFullLevel}` } }, [policyDepth, perLevelCounts, totalNonRoot]) return ( {/* Smooth refresh overlay */} {refreshing && (
    {t('autofix.k14a4b43e')}
    )}
    {/* Header card */}

    {matrixName}

    {t('autofix.k31d46514')}{topNodeEmail}

    Root: unlimited immediate children (sequential positions) Non-root: 5 children (positions 1–5) Policy depth (DB): {isUnlimited ? 'Unlimited' : policyMaxDepth} Fetch depth (client slice): {DEFAULT_FETCH_DEPTH} {serverMaxDepth != null && ( Server-reported max depth: {serverMaxDepth} )} Root children: {rootChildrenCount} (unlimited) Displayed slots under root (positions 1–5): {displayedRootSlots}/5
    {/* Banner for unlimited */} {isUnlimited && (
    Unlimited matrix: depth grows without a configured cap. Display limited by fetch slice ({DEFAULT_FETCH_DEPTH} levels requested).
    )} {/* Sticky controls (CHANGED depth display) */}
    setGlobalSearch(e.target.value)} placeholder={t('autofix.kd304af2e')} className="pl-8 pr-2 py-2 rounded-lg border border-gray-200 text-xs focus:ring-1 focus:ring-blue-900 focus:border-transparent w-full" />
    Policy depth: {isUnlimited ? 'Unlimited' : policyMaxDepth}
    {/* Small stats (CHANGED wording) */}
    {t('autofix.k65e33378')}
    {users.length}
    {t('autofix.kb343460d')}
    {rogueCount}
    Structure
    {t('autofix.kf3557acd')}
    {t('autofix.k776b751c')}
    {isUnlimited ? 'Unlimited' : policyMaxDepth}
    {t('autofix.k9683262f')}
    {fillMetrics.label}
    {t('autofix.k7f9568ec')}
    {fillMetrics.highestFull}
    {/* Unlimited hierarchical tree (replaces dynamic levels + grouped level list) */}

    Matrix Tree (Unlimited Depth)

    {t('autofix.kab4f5159')}

    {!rootNode && (
    {t('autofix.k4e61bc77')}
    )} {rootNode && (
      {renderNode(rootNode, 0)}
    )}
    {/* Vacancies placeholder */}

    Vacancies

    {t('autofix.k9b3266b5')}

    {/* Add Users Modal */} { addToMatrix(u) }} />
    ) } // CHANGED: default export wraps inner component in Suspense export default function MatrixDetailPage() { return (

    Loading...

    } > ) }