From 51c54eb9059ad421ffd2967bdcafe54f9fb24486 Mon Sep 17 00:00:00 2001 From: DeathKaioken Date: Sat, 29 Nov 2025 13:13:36 +0100 Subject: [PATCH] feat: pool management fuckhead --- .../admin/pool-management/hooks/addPool.ts | 51 +++ .../pool-management/hooks/archivePool.ts | 45 +++ .../admin/pool-management/hooks/getlist.ts | 107 ++++++ src/app/admin/pool-management/manage/page.tsx | 351 ++++++++++++++++++ src/app/admin/pool-management/page.tsx | 342 +++++++++++++++++ src/app/components/nav/Header.tsx | 7 + src/app/utils/authFetch.ts | 22 +- 7 files changed, 924 insertions(+), 1 deletion(-) create mode 100644 src/app/admin/pool-management/hooks/addPool.ts create mode 100644 src/app/admin/pool-management/hooks/archivePool.ts create mode 100644 src/app/admin/pool-management/hooks/getlist.ts create mode 100644 src/app/admin/pool-management/manage/page.tsx create mode 100644 src/app/admin/pool-management/page.tsx diff --git a/src/app/admin/pool-management/hooks/addPool.ts b/src/app/admin/pool-management/hooks/addPool.ts new file mode 100644 index 0000000..8f69b8e --- /dev/null +++ b/src/app/admin/pool-management/hooks/addPool.ts @@ -0,0 +1,51 @@ +import { authFetch } from '../../../utils/authFetch'; + +export type AddPoolPayload = { + name: string; + description?: string; + state?: 'active' | 'inactive'; +}; + +export async function addPool(payload: AddPoolPayload) { + const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; + const url = `${BASE_URL}/api/admin/pools`; + const res = await authFetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(payload), + }); + + let body: any = null; + try { + body = await res.json(); + } catch { + body = null; + } + + const ok = res.status === 201 || res.ok; + const message = + body?.message || + (res.status === 409 + ? 'Pool name already exists.' + : res.status === 400 + ? 'Invalid request. Check name/state.' + : res.status === 401 + ? 'Unauthorized.' + : res.status === 403 + ? 'Forbidden.' + : res.status === 500 + ? 'Internal server error.' + : !ok + ? `Request failed (${res.status}).` + : ''); + + return { + ok, + status: res.status, + body, + message, + }; +} diff --git a/src/app/admin/pool-management/hooks/archivePool.ts b/src/app/admin/pool-management/hooks/archivePool.ts new file mode 100644 index 0000000..2c507f8 --- /dev/null +++ b/src/app/admin/pool-management/hooks/archivePool.ts @@ -0,0 +1,45 @@ +import { authFetch } from '../../../utils/authFetch'; + +export async function setPoolState( + id: string | number, + state: 'active' | 'inactive' | 'archived' +) { + const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; + const url = `${BASE_URL}/api/admin/pools/${id}/state`; + const res = await authFetch(url, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ state }), + }); + + let body: any = null; + try { + body = await res.json(); + } catch { + body = null; + } + + const ok = res.ok; + const message = + body?.message || + (res.status === 404 + ? 'Pool not found.' + : res.status === 400 + ? 'Invalid request.' + : res.status === 403 + ? 'Forbidden.' + : res.status === 500 + ? 'Server error.' + : !ok + ? `Request failed (${res.status}).` + : ''); + + return { ok, status: res.status, body, message }; +} + +export async function archivePoolById(id: string | number) { + return setPoolState(id, 'archived'); +} diff --git a/src/app/admin/pool-management/hooks/getlist.ts b/src/app/admin/pool-management/hooks/getlist.ts new file mode 100644 index 0000000..d5a1314 --- /dev/null +++ b/src/app/admin/pool-management/hooks/getlist.ts @@ -0,0 +1,107 @@ +import { useEffect, useState } from 'react'; +import { authFetch } from '../../../utils/authFetch'; +import { log } from '../../../utils/logger'; + +export type AdminPool = { + id: string; + name: string; + description?: string; + membersCount: number; + createdAt: string; + archived?: boolean; +}; + +export function useAdminPools() { + const [pools, setPools] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; + + useEffect(() => { + let cancelled = false; + async function load() { + setLoading(true); + setError(''); + const url = `${BASE_URL}/api/admin/pools`; // reverted to /api/admin/pools + log("🌐 Pools: GET", url); + try { + const headers = { Accept: 'application/json' }; + log("πŸ“€ Pools: Request headers:", headers); + + const res = await authFetch(url, { headers }); + log("πŸ“‘ Pools: Response status:", res.status); + + let body: any = null; + try { + body = await res.clone().json(); + const preview = JSON.stringify(body).slice(0, 600); + log("πŸ“¦ Pools: Response body preview:", preview); + } catch { + log("πŸ“¦ Pools: Response body is not JSON or failed to parse"); + } + + if (res.status === 401) { + if (!cancelled) setError('Unauthorized. Please log in.'); + return; + } + if (res.status === 403) { + if (!cancelled) setError('Forbidden. Admin access required.'); + return; + } + if (!res.ok) { + if (!cancelled) setError('Failed to load pools.'); + return; + } + + const apiItems: any[] = Array.isArray(body?.data) ? body.data : []; + log("πŸ”§ Pools: Mapping items count:", apiItems.length); + + const mapped: AdminPool[] = apiItems.map(item => ({ + id: String(item.id), + name: String(item.name ?? 'Unnamed Pool'), + description: String(item.description ?? ''), + membersCount: 0, + createdAt: String(item.created_at ?? new Date().toISOString()), + archived: String(item.state ?? '').toLowerCase() === 'archived', + })); + log("βœ… Pools: Mapped sample:", mapped.slice(0, 3)); + + if (!cancelled) setPools(mapped); + } catch (e: any) { + log("❌ Pools: Network or parsing error:", e?.message || e); + if (!cancelled) setError('Network error while loading pools.'); + } finally { + if (!cancelled) setLoading(false); + } + } + load(); + return () => { cancelled = true; }; + }, [BASE_URL]); + + return { + pools, + loading, + error, + refresh: async () => { + const url = `${BASE_URL}/api/admin/pools`; // reverted to /api/admin/pools + log("πŸ” Pools: Refresh GET", url); + const res = await authFetch(url, { headers: { Accept: 'application/json' } }); + if (!res.ok) { + log("❌ Pools: Refresh failed status:", res.status); + return false; + } + const body = await res.json(); + const apiItems: any[] = Array.isArray(body?.data) ? body.data : []; + setPools(apiItems.map(item => ({ + id: String(item.id), + name: String(item.name ?? 'Unnamed Pool'), + description: String(item.description ?? ''), + membersCount: 0, + createdAt: String(item.created_at ?? new Date().toISOString()), + archived: String(item.state ?? '').toLowerCase() === 'archived', + }))); + log("βœ… Pools: Refresh succeeded, items:", apiItems.length); + return true; + } + }; +} diff --git a/src/app/admin/pool-management/manage/page.tsx b/src/app/admin/pool-management/manage/page.tsx new file mode 100644 index 0000000..fcd85a0 --- /dev/null +++ b/src/app/admin/pool-management/manage/page.tsx @@ -0,0 +1,351 @@ +'use client' + +import React from 'react' +import Header from '../../../components/nav/Header' +import Footer from '../../../components/Footer' +import { UsersIcon, PlusIcon, BanknotesIcon, CalendarDaysIcon, MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline' +import { useRouter, useSearchParams } from 'next/navigation' +import useAuthStore from '../../../store/authStore' +import PageTransitionEffect from '../../../components/animation/pageTransitionEffect' + +type PoolUser = { + id: string + name: string + email: string + contributed: number + joinedAt: string // NEW: member since +} + +export default function PoolManagePage() { + const router = useRouter() + const searchParams = useSearchParams() + const user = useAuthStore(s => s.user) + const isAdmin = + !!user && + ( + (user as any)?.role === 'admin' || + (user as any)?.userType === 'admin' || + (user as any)?.isAdmin === true || + ((user as any)?.roles?.includes?.('admin')) + ) + + // Auth gate + const [authChecked, setAuthChecked] = React.useState(false) + React.useEffect(() => { + if (user === null) { + router.replace('/login') + return + } + if (user && !isAdmin) { + router.replace('/') + return + } + setAuthChecked(true) + }, [user, isAdmin, router]) + + // Read pool data from query params with fallbacks (hooks must be before any return) + const poolId = searchParams.get('id') ?? 'pool-unknown' + const poolName = searchParams.get('name') ?? 'Unnamed Pool' + const poolDescription = searchParams.get('description') ?? '' + const poolState = searchParams.get('state') ?? 'active' + const poolCreatedAt = searchParams.get('createdAt') ?? new Date().toISOString() + + // Members (no dummy data) + const [users, setUsers] = React.useState([]) + + // Stats (no dummy data) + const [totalAmount, setTotalAmount] = React.useState(0) + const [amountThisYear, setAmountThisYear] = React.useState(0) + const [amountThisMonth, setAmountThisMonth] = React.useState(0) + + // Search modal state + const [searchOpen, setSearchOpen] = React.useState(false) + const [query, setQuery] = React.useState('') + const [loading, setLoading] = React.useState(false) + const [error, setError] = React.useState('') + const [candidates, setCandidates] = React.useState>([]) + const [hasSearched, setHasSearched] = React.useState(false) + + // Early return AFTER all hooks are declared to keep consistent order + if (!authChecked) return null + + // Remove dummy candidate source; keep search scaffolding returning empty + async function doSearch() { + setError('') + const q = query.trim().toLowerCase() + if (q.length < 3) { + setHasSearched(false) + setCandidates([]) + return + } + setHasSearched(true) + setLoading(true) + setTimeout(() => { + setCandidates([]) // no local dummy results + setLoading(false) + }, 300) + } + + function addUserFromModal(u: { id: string; name: string; email: string }) { + // Append user to pool; contribution stays zero; joinedAt is now. + setUsers(prev => [{ id: u.id, name: u.name, email: u.email, contributed: 0, joinedAt: new Date().toISOString() }, ...prev]) + setSearchOpen(false) + setQuery('') + setCandidates([]) + setHasSearched(false) + setError('') + setLoading(false) + } + + return ( + +
+
+ {/* main wrapper: avoid high z-index stacking */} +
+
+ {/* Header (remove sticky/z-10) */} +
+
+
+
+ +
+
+

{poolName}

+

+ {poolDescription ? poolDescription : 'Manage users and track pool funds'} +

+
+ + + {poolState === 'archived' ? 'Archived' : 'Active'} + + β€’ + Created {new Date(poolCreatedAt).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: '2-digit' })} + β€’ + ID: {poolId} +
+
+
+ {/* Back to Pool Management */} + +
+
+ + {/* Stats (now zero until backend wired) */} +
+
+
+
+ +
+
+

Total in Pool

+

€ {totalAmount.toLocaleString()}

+
+
+
+
+
+
+ +
+
+

This Year

+

€ {amountThisYear.toLocaleString()}

+
+
+
+
+
+
+ +
+
+

Current Month

+

€ {amountThisMonth.toLocaleString()}

+
+
+
+
+ + {/* Unified Members card: add button + list */} +
+
+

Members

+ +
+
+ {users.map(u => ( +
+
+
+
+ +
+
+

{u.name}

+

{u.email}

+
+
+ + € {u.contributed.toLocaleString()} + +
+
+ Member since:{' '} + + {new Date(u.joinedAt).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: '2-digit' })} + +
+
+ ))} + {users.length === 0 && ( +
+ No users in this pool yet. +
+ )} +
+
+
+
+