From 3ee6e90128d782578ecc302f54e88e2ab2f76b28 Mon Sep 17 00:00:00 2001 From: DeathKaioken Date: Thu, 16 Oct 2025 16:35:17 +0200 Subject: [PATCH] feat: add Matrix Management --- .../matrix-management/hooks/createMatrix.ts | 45 +++ .../matrix-management/hooks/getMatrixStats.ts | 31 ++ src/app/admin/matrix-management/page.tsx | 325 ++++++++++++------ 3 files changed, 305 insertions(+), 96 deletions(-) create mode 100644 src/app/admin/matrix-management/hooks/createMatrix.ts create mode 100644 src/app/admin/matrix-management/hooks/getMatrixStats.ts diff --git a/src/app/admin/matrix-management/hooks/createMatrix.ts b/src/app/admin/matrix-management/hooks/createMatrix.ts new file mode 100644 index 0000000..d54d00d --- /dev/null +++ b/src/app/admin/matrix-management/hooks/createMatrix.ts @@ -0,0 +1,45 @@ +export type CreateMatrixResult = { + ok: boolean + status: number + body?: any + message?: string +} + +export async function createMatrix(params: { + token: string + name: string + email: string + force?: boolean + baseUrl?: string +}): Promise { + const { token, name, email, force = false, baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || '' } = params + if (!token) return { ok: false, status: 401, message: 'Missing token' } + + const url = new URL(`${baseUrl}/api/matrix/create`) + url.searchParams.set('name', name) + url.searchParams.set('email', email) + if (force) url.searchParams.set('force', 'true') + + try { + const res = await fetch(url.toString(), { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + }) + let body: any = null + try { body = await res.json() } catch {} + if (!res.ok) { + return { + ok: false, + status: res.status, + body, + message: body?.message || `Create matrix failed (${res.status})` + } + } + return { ok: true, status: res.status, body } + } catch (err) { + return { ok: false, status: 0, message: 'Network error' } + } +} diff --git a/src/app/admin/matrix-management/hooks/getMatrixStats.ts b/src/app/admin/matrix-management/hooks/getMatrixStats.ts new file mode 100644 index 0000000..ed705fe --- /dev/null +++ b/src/app/admin/matrix-management/hooks/getMatrixStats.ts @@ -0,0 +1,31 @@ +export type GetMatrixStatsResult = { + ok: boolean + status: number + body?: any + message?: string +} + +export async function getMatrixStats(params: { + token: string + baseUrl?: string +}): Promise { + const { token, baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || '' } = params + if (!token) return { ok: false, status: 401, message: 'Missing token' } + + const url = `${baseUrl}/api/matrix/stats` + try { + const res = await fetch(url, { + method: 'GET', + headers: { Authorization: `Bearer ${token}` }, + credentials: 'include', + }) + let body: any = null + try { body = await res.json() } catch {} + if (!res.ok) { + return { ok: false, status: res.status, body, message: body?.message || `Fetch stats failed (${res.status})` } + } + return { ok: true, status: res.status, body } + } catch { + return { ok: false, status: 0, message: 'Network error' } + } +} diff --git a/src/app/admin/matrix-management/page.tsx b/src/app/admin/matrix-management/page.tsx index 7c6c955..b8c9163 100644 --- a/src/app/admin/matrix-management/page.tsx +++ b/src/app/admin/matrix-management/page.tsx @@ -12,6 +12,8 @@ import { import PageLayout from '../../components/PageLayout' import { useRouter } from 'next/navigation' import useAuthStore from '../../store/authStore' +import { createMatrix } from './hooks/createMatrix' +import { getMatrixStats } from './hooks/getMatrixStats' type Matrix = { id: string @@ -23,9 +25,9 @@ type Matrix = { } export default function MatrixManagementPage() { - // Auth guard const router = useRouter() const user = useAuthStore(s => s.user) + const token = useAuthStore(s => s.accessToken) const isAdmin = !!user && ( @@ -43,58 +45,83 @@ export default function MatrixManagementPage() { } }, [user, isAdmin, router]) - const [matrices, setMatrices] = useState([ - { - id: 'm1', - name: 'Gold Matrix', - status: 'active', - usersCount: 128, - createdAt: new Date(Date.now() - 5 * 24 * 3600 * 1000).toISOString(), - topNodeEmail: 'alice@example.com', - }, - { - id: 'm2', - name: 'Silver Matrix', - status: 'inactive', - usersCount: 64, - createdAt: new Date(Date.now() - 15 * 24 * 3600 * 1000).toISOString(), - topNodeEmail: 'bob@example.com', - }, - { - id: 'm3', - name: 'Bronze Matrix', - status: 'active', - usersCount: 42, - createdAt: new Date(Date.now() - 40 * 24 * 3600 * 1000).toISOString(), - topNodeEmail: 'charlie@example.com', - }, - ]) + const [matrices, setMatrices] = useState([]) + const [stats, setStats] = useState({ total: 0, active: 0, totalUsers: 0 }) + const [statsLoading, setStatsLoading] = useState(false) + const [statsError, setStatsError] = useState('') const [createOpen, setCreateOpen] = useState(false) const [createName, setCreateName] = useState('') const [createEmail, setCreateEmail] = useState('') const [formError, setFormError] = useState('') - const stats = useMemo(() => { - const total = matrices.length - const active = matrices.filter(m => m.status === 'active').length - const totalUsers = matrices.reduce((acc, m) => acc + (m.usersCount || 0), 0) - return { total, active, totalUsers } - }, [matrices]) + const [createLoading, setCreateLoading] = useState(false) + const [forcePrompt, setForcePrompt] = useState<{ name: string; email: string } | null>(null) + const [createSuccess, setCreateSuccess] = useState<{ name: string; email: string } | null>(null) + + const loadStats = async () => { + if (!token) return + setStatsLoading(true) + setStatsError('') + try { + const res = await getMatrixStats({ token }) + console.log('📊 MatrixManagement: GET /matrix/stats ->', res.status, res.body) + if (res.ok) { + const payload = res.body?.data || res.body || {} + const apiMatrices: any[] = Array.isArray(payload.matrices) ? payload.matrices : [] + const mapped: Matrix[] = apiMatrices.map((m: any, idx: number) => { + const isActive = !!m?.isActive + const createdAt = m?.createdAt || m?.ego_activated_at || m?.activatedAt || new Date().toISOString() + const topNodeEmail = m?.topNodeEmail || m?.masterTopUserEmail || m?.email || '' + return { + id: String(m?.rootUserId ?? m?.id ?? `m-${idx}`), + name: String(m?.name ?? 'Unnamed Matrix'), + status: isActive ? 'active' : 'inactive', + usersCount: Number(m?.usersCount ?? 0), + createdAt: String(createdAt), + topNodeEmail: String(topNodeEmail), + } + }) + setMatrices(mapped) + const activeMatrices = Number(payload.activeMatrices ?? mapped.filter(m => m.status === 'active').length) + const totalMatrices = Number(payload.totalMatrices ?? mapped.length) + const totalUsersSubscribed = Number(payload.totalUsersSubscribed ?? 0) + setStats({ total: totalMatrices, active: activeMatrices, totalUsers: totalUsersSubscribed }) + console.log('✅ MatrixManagement: mapped stats:', { total: totalMatrices, active: activeMatrices, totalUsers: totalUsersSubscribed }) + console.log('✅ MatrixManagement: mapped matrices sample:', mapped.slice(0, 3)) + } else { + setStatsError(res.message || 'Failed to load matrix stats.') + } + } catch (e) { + console.error('❌ MatrixManagement: stats load error', e) + setStatsError('Network error while loading matrix stats.') + } finally { + setStatsLoading(false) + } + } + + useEffect(() => { + loadStats() + }, [token]) const resetForm = () => { setCreateName('') setCreateEmail('') setFormError('') + setForcePrompt(null) + setCreateSuccess(null) } const validateEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()) - const handleCreate = (e: React.FormEvent) => { + const handleCreate = async (e: React.FormEvent) => { e.preventDefault() const name = createName.trim() const email = createEmail.trim() + setFormError('') + setCreateSuccess(null) + setForcePrompt(null) if (!name) { setFormError('Please provide a matrix name.') @@ -104,18 +131,57 @@ export default function MatrixManagementPage() { setFormError('Please provide a valid top-node email.') return } - - const newMatrix: Matrix = { - id: `m-${Date.now()}`, - name, - status: 'active', - usersCount: 0, - createdAt: new Date().toISOString(), - topNodeEmail: email, + if (!token) { + setFormError('Not authenticated. Please log in again.') + return + } + + setCreateLoading(true) + try { + const res = await createMatrix({ token, name, email }) + console.log('🧱 MatrixManagement: create result ->', res.status, res.body) + if (res.ok && res.body?.success) { + const createdName = res.body?.data?.name || name + const createdEmail = res.body?.data?.masterTopUserEmail || email + setCreateSuccess({ name: createdName, email: createdEmail }) + await loadStats() + setCreateName('') + setCreateEmail('') + } else if (res.status === 409) { + setForcePrompt({ name, email }) + } else { + setFormError(res.message || 'Failed to create matrix.') + } + } catch (err) { + setFormError('Network error while creating the matrix.') + } finally { + setCreateLoading(false) + } + } + + const confirmForce = async () => { + if (!forcePrompt || !token) return + setFormError('') + setCreateLoading(true) + try { + const res = await createMatrix({ token, name: forcePrompt.name, email: forcePrompt.email, force: true }) + console.log('🧱 MatrixManagement: force-create result ->', res.status, res.body) + if (res.ok && res.body?.success) { + const createdName = res.body?.data?.name || forcePrompt.name + const createdEmail = res.body?.data?.masterTopUserEmail || forcePrompt.email + setCreateSuccess({ name: createdName, email: createdEmail }) + setForcePrompt(null) + setCreateName('') + setCreateEmail('') + await loadStats() + } else { + setFormError(res.message || 'Failed to create matrix (force).') + } + } catch { + setFormError('Network error while forcing the matrix creation.') + } finally { + setCreateLoading(false) } - setMatrices(prev => [newMatrix, ...prev]) - setCreateOpen(false) - resetForm() } const toggleStatus = (id: string) => { @@ -180,6 +246,13 @@ export default function MatrixManagementPage() { + {/* Error banner for stats */} + {statsError && ( +
+ {statsError} +
+ )} + {/* Stats */}
@@ -189,60 +262,76 @@ export default function MatrixManagementPage() { {/* Matrix cards */}
- {matrices.map(m => ( -
-
-
-

{m.name}

- -
- -
-
- - {m.usersCount} - users -
-
- - - {new Date(m.createdAt).toLocaleDateString()} - -
-
- - {m.topNodeEmail} -
-
- -
- - + {statsLoading ? ( + // Simple skeleton + Array.from({ length: 3 }).map((_, i) => ( +
+
+
+
+
+
-
- ))} + )) + ) : matrices.length === 0 ? ( +
No matrices found.
+ ) : ( + matrices.map(m => ( +
+
+
+

{m.name}

+ +
+ +
+
+ + {m.usersCount} + users +
+
+ + + {new Date(m.createdAt).toLocaleDateString()} + +
+
+ + {m.topNodeEmail} +
+
+ +
+ + +
+
+
+ )) + )}
{/* Create Matrix Modal */} {createOpen && (
-
setCreateOpen(false)} /> +
{ setCreateOpen(false); resetForm() }} />
@@ -255,13 +344,51 @@ export default function MatrixManagementPage() {
+ {/* Success banner */} + {createSuccess && ( +
+ Matrix created successfully. +
+ Name: {createSuccess.name}{' '} + Top node: {createSuccess.email} +
+
+ )} + + {/* 409 force prompt */} + {forcePrompt && ( +
+ A matrix configuration already exists for this selection. +
+ + +
+
+ )} + + {/* Form fields */}
setCreateName(e.target.value)} - className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + disabled={createLoading} + className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:bg-gray-100" placeholder="e.g., Platinum Matrix" />
@@ -271,28 +398,34 @@ export default function MatrixManagementPage() { type="email" value={createEmail} onChange={e => setCreateEmail(e.target.value)} - className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + disabled={createLoading} + className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:bg-gray-100" placeholder="owner@example.com" />
+ {formError && (
{formError}
)} +