diff --git a/src/app/components/nav/Header.tsx b/src/app/components/nav/Header.tsx
index 1806736..54336fd 100644
--- a/src/app/components/nav/Header.tsx
+++ b/src/app/components/nav/Header.tsx
@@ -28,7 +28,6 @@ import {
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import useAuthStore from '../../store/authStore';
import { Avatar } from '../avatar';
-import LanguageSwitcher from '../LanguageSwitcher';
// Replace current shopItems definition with detailed version (adds icon & description)
const shopItems = [
@@ -297,22 +296,31 @@ export default function Header() {
Affiliate-Links
- {/* Memberships */}
- router.push('/memberships')}
- className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
- >
- Memberships
-
-
+ {/* Remove Memberships */}
{/* Referral Management - match others (no highlight) */}
{userPresent && hasReferralPerm && (
- { console.log('🧠Header: navigate to /referral-management'); router.push('/referral-management') }}
- className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
- >
- Referral Management
-
+ <>
+ { console.log('🧠Header: navigate to /referral-management'); router.push('/referral-management') }}
+ className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
+ >
+ Referral Management
+
+ {/* New: Personal Matrix */}
+ router.push('/personal-matrix')}
+ className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
+ >
+ Personal Matrix
+
+ {/* New: Coffee Abonnements */}
+ router.push('/coffee-abonnements')}
+ className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
+ >
+ Coffee Abonnements
+
+ >
)}
{/* About us */}
@@ -383,8 +391,20 @@ export default function Header() {
)}
- {/* Language & theme remain after auth slot */}
-
+ {/* Replace LanguageSwitcher with English-only dropdown + note */}
+
+
+ English
+
+
+
+ We are currently working on implementing other languages.
+
+
+
: }
{isDark ? 'Light Mode' : 'Dark Mode'}
-
-
-
+ {/* Replace LanguageSwitcher with English-only dropdown + note for mobile */}
+
+
+ Language: English
+
+
+
+ We are currently working on implementing other languages.
+
+
{/* Navigation / Shop after that */}
@@ -610,21 +637,29 @@ export default function Header() {
>
Affiliate-Links
- {/* Memberships */}
-
{ router.push('/memberships'); setMobileMenuOpen(false); }}
- className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
- >
- Memberships
-
- {/* Referral Management - match others (no highlight) */}
+ {/* Remove Memberships */}
+ {/* Referral Management + new items */}
{hasReferralPerm && (
-
{ console.log('🧠Header Mobile: navigate to /referral-management'); router.push('/referral-management'); setMobileMenuOpen(false); }}
- className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
- >
- Referral Management
-
+ <>
+
{ console.log('🧠Header Mobile: navigate to /referral-management'); router.push('/referral-management'); setMobileMenuOpen(false); }}
+ className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
+ >
+ Referral Management
+
+
{ router.push('/personal-matrix'); setMobileMenuOpen(false); }}
+ className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
+ >
+ Personal Matrix
+
+
{ router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
+ className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
+ >
+ Coffee Abonnements
+
+ >
)}
{/* About us */}
Affiliate-Links
-
{ router.push('/memberships'); setMobileMenuOpen(false); }}
- className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
- >
- Memberships
-
+ {/* Remove Memberships */}
{ router.push('/about-us'); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
About us
+ {/* Language (English-only) in logged-out mobile */}
+
+
+
+ Language: English
+
+
+
+ We are currently working on implementing other languages.
+
+
+
{ router.push('/login'); setMobileMenuOpen(false); }}
diff --git a/src/app/personal-matrix/hooks/getStats.ts b/src/app/personal-matrix/hooks/getStats.ts
index 2817721..d4dc4f4 100644
--- a/src/app/personal-matrix/hooks/getStats.ts
+++ b/src/app/personal-matrix/hooks/getStats.ts
@@ -24,23 +24,38 @@ export type PersonalOverview = {
level2Plus: Level2PlusItem[]
}
-export function usePersonalMatrixOverview() {
- const [data, setData] = useState(null)
+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(() => {
- // Abort any inflight when re-running
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/overview`
+ const url = `${base}/api/matrix/me/list`
+ console.log('[useMyMatrices] Fetching:', url)
setLoading(true)
- // Do not clear error if we already have valid data; only clear when starting fresh
setError(null)
authFetch(url, {
@@ -51,14 +66,153 @@ export function usePersonalMatrixOverview() {
})
.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)
+ if (!r.ok || !ct.includes('application/json')) {
+ const txt = await r.text().catch(() => '')
+ console.warn('[usePersonalMatrixOverview] 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('[usePersonalMatrixOverview] Raw JSON:', json)
const d = json?.data || json
const mapped: PersonalOverview = {
- matrixInstanceId: d.matrixInstanceId,
+ matrixInstanceId: d.matrixInstanceId ?? matrixId,
totalUsersUnderMe: Number(d.totalUsersUnderMe ?? 0),
levelsFilled: Number(d.levelsFilled ?? 0),
immediateChildrenCount: Number(d.immediateChildrenCount ?? 0),
@@ -75,37 +229,39 @@ export function usePersonalMatrixOverview() {
nameMasked: String(x.name ?? '')
})) : []
}
+ console.log('[usePersonalMatrixOverview] Mapped:', mapped)
setData(mapped)
})
.catch((e: any) => {
- // Ignore aborts (during navigation/unmount or re-run)
- if (controller.signal.aborted) {
- // Do not set error; just exit quietly
- return
- }
- // Some environments throw DOMException name 'AbortError'
- if (e?.name === 'AbortError') {
+ 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')
}
})
- // Cleanup: abort on unmount
return () => {
+ console.log('[usePersonalMatrixOverview] Cleanup abort')
controller.abort()
}
- }, [])
+ }, [matrixId])
- const meta = useMemo(() => ({
- countL1: data?.level1.length ?? 0,
- countL2Plus: data?.level2Plus.length ?? 0
- }), [data])
+ 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 }
}
diff --git a/src/app/personal-matrix/page.tsx b/src/app/personal-matrix/page.tsx
index 4de2a42..43e50d5 100644
--- a/src/app/personal-matrix/page.tsx
+++ b/src/app/personal-matrix/page.tsx
@@ -1,21 +1,44 @@
'use client'
-import React, { useEffect } from 'react'
+import React, { useEffect, useMemo, useState } from 'react'
import useAuthStore from '../store/authStore'
import { useRouter } from 'next/navigation'
import PageLayout from '../components/PageLayout'
import { UsersIcon, CalendarDaysIcon, ArrowLeftIcon } from '@heroicons/react/24/outline'
-import { usePersonalMatrixOverview } from './hooks/getStats'
+import { usePersonalMatrixOverview, useMyMatrices } from './hooks/getStats'
export default function PersonalMatrixPage() {
const router = useRouter()
const user = useAuthStore(s => s.user)
- const { data, loading, error, meta } = usePersonalMatrixOverview()
+
+ const { matrices, loading: loadingMatrices, error: matricesError } = useMyMatrices()
+ const [selectedId, setSelectedId] = useState(undefined)
+
+ // Auto-select if only one matrix available
+ useEffect(() => {
+ if (selectedId == null && matrices.length === 1) {
+ setSelectedId(matrices[0].id)
+ }
+ }, [matrices, selectedId])
+
+ const { data, loading, error, meta } = usePersonalMatrixOverview(selectedId)
useEffect(() => {
if (user === null) router.replace('/login')
}, [user, router])
if (user === null) return null
+ useEffect(() => {
+ console.log('[PersonalMatrixPage] matrices loading:', loadingMatrices, 'error:', matricesError)
+ }, [loadingMatrices, matricesError])
+
+ useEffect(() => {
+ console.log('[PersonalMatrixPage] matrices list:', matrices)
+ }, [matrices])
+
+ useEffect(() => {
+ console.log('[PersonalMatrixPage] selectedId:', selectedId)
+ }, [selectedId])
+
return (
@@ -24,97 +47,211 @@ export default function PersonalMatrixPage() {
router.push('/')}
+ onClick={() => (selectedId == null ? router.push('/') : setSelectedId(undefined))}
className="inline-flex items-center gap-2 text-sm text-blue-900 hover:text-blue-700"
>
- Back
+ {selectedId == null ? 'Back' : 'Back to matrices'}
-
My Matrix Overview
-
Personal subtree, privacy-preserving beyond level 1.
+
+ {selectedId == null ? 'My Matrices' : 'My Matrix Overview'}
+
+
+ {selectedId == null
+ ? 'Select which matrix you want to inspect.'
+ : 'Personal subtree, privacy-preserving beyond level 1.'}
+
- {error && (
+ {/* Errors */}
+ {matricesError && (
+
+ {matricesError}
+
+ )}
+ {error && selectedId != null && (
{error}
)}
-
-
-
Total users under me
-
{data?.totalUsersUnderMe ?? (loading ? '…' : 0)}
-
-
-
Immediate children
-
{data?.immediateChildrenCount ?? (loading ? '…' : 0)}
-
-
-
Levels filled
-
{data?.levelsFilled ?? (loading ? '…' : 0)}
-
-
-
-
-
-
Level 1 (Direct Children)
-
- Up to the first five direct children. {data?.rootSlotsRemaining != null ? `Root slots remaining: ${data.rootSlotsRemaining}` : ''}
-
-
-
- {loading &&
Loading…
}
- {!loading && (data?.level1?.length ?? 0) === 0 && (
-
No direct children.
+ {/* Matrix card list (overview) */}
+ {selectedId == null && (
+
+ {loadingMatrices && (
+
)}
-
- {data?.level1.map((c) => (
-
-
-
{c.name}
-
- {c.position != null ? `pos ${c.position}` : 'pos —'}
-
-
- {c.email}
-
- ))}
-
-
-
-
-
-
-
Level 2+
-
Masked names for deeper descendants.
-
-
- {loading &&
Loading…
}
- {!loading && (data?.level2Plus?.length ?? 0) === 0 && (
-
No deeper descendants.
+ {!loadingMatrices && matrices.length === 0 && (
+
+ You are not part of any matrix yet.
+
+ )}
+ {!loadingMatrices && matrices.length > 0 && (
+
+ {matrices.map((m, idx) => {
+ // Prefer new fields from list; fall back to legacy computed percent
+ const percent = m.matrixFillPercent ?? m.percentFull ?? 0
+ const totalUsersDisplay = m.totalUsersUnderMe ?? m.membersCount ?? m.totalUsers
+ console.log('[PersonalMatrixPage] Card', idx, {
+ id: m.id,
+ name: m.name,
+ totalUsersUnderMe: m.totalUsersUnderMe,
+ highestFullLevel: m.highestFullLevel,
+ matrixFillPercent: m.matrixFillPercent,
+ legacyPercentFull: m.percentFull,
+ filledSlots: m.filledSlots,
+ totalSlots: m.totalSlots,
+ totalUsers: m.totalUsers,
+ membersCount: m.membersCount
+ })
+ return (
+
+
+
{m.name}
+
+
+ Total members: {' '}
+ {totalUsersDisplay != null ? totalUsersDisplay : 'N/A'}
+
+
+ Highest full level: {' '}
+ {m.highestFullLevel != null ? m.highestFullLevel : 'N/A'}
+
+
+
Matrix fill:
+
+
+ {Math.max(0, Math.min(100, percent)).toFixed(2)}%
+
+
+ {m.createdAt && (
+
+ Created: {' '}
+ {new Date(m.createdAt).toLocaleDateString()}
+
+ )}
+ {m.description && (
+
+ {m.description}
+
+ )}
+
+
+ setSelectedId(m.id)}
+ className="inline-flex items-center rounded-md bg-blue-800 px-3 py-1.5 text-sm font-semibold text-white hover:bg-blue-900"
+ >
+ View overview
+
+
+
+
+ )
+ })}
+
)}
-
-
+ )}
-
-
Overview meta
-
- Matrix instance: {data?.matrixInstanceId ?? (loading ? '…' : '—')}{' '}
- • Level1: {meta.countL1} • Level2+: {meta.countL2Plus}
-
-
+ {/* Selected matrix overview */}
+ {selectedId != null && (
+ <>
+ {/* Stats cards */}
+
+
+
Total users under me
+
+ {data?.totalUsersUnderMe ?? (loading ? '…' : 0)}
+
+
+
+
Immediate children
+
+ {data?.immediateChildrenCount ?? (loading ? '…' : 0)}
+
+
+
+
Levels filled
+
+ {data?.levelsFilled ?? (loading ? '…' : 0)}
+
+
+
+
+ {/* Level 1 */}
+
+
+
Level 1 (Direct Children)
+
+ Up to the first five direct children. {data?.rootSlotsRemaining != null ? `Root slots remaining: ${data.rootSlotsRemaining}` : ''}
+
+
+
+ {loading &&
Loading…
}
+ {!loading && (data?.level1?.length ?? 0) === 0 && (
+
No direct children.
+ )}
+
+ {data?.level1.map((c) => (
+
+
+
{c.name}
+
+ {c.position != null ? `pos ${c.position}` : 'pos —'}
+
+
+ {c.email}
+
+ ))}
+
+
+
+
+ {/* Level 2+ */}
+
+
+
Level 2+
+
Masked names for deeper descendants.
+
+
+ {loading &&
Loading…
}
+ {!loading && (data?.level2Plus?.length ?? 0) === 0 && (
+
No deeper descendants.
+ )}
+
+
+
+
+ {/* Meta */}
+
+
Overview meta
+
+ Matrix instance: {data?.matrixInstanceId ?? (loading ? '…' : '—')}{' '}
+ • Level1: {meta.countL1} • Level2+: {meta.countL2Plus}
+
+
+ >
+ )}