From 7d9399df4b9ea020e2bd5aa3cf17621a18fc2c92 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Fri, 23 Jan 2026 21:54:11 +0100 Subject: [PATCH] feat: implement pool members management functionality in admin panel --- ToDo.txt | 4 +- .../admin/pool-management/hooks/getlist.ts | 4 +- src/app/admin/pool-management/manage/page.tsx | 214 +++++++++++++++--- src/app/utils/api.ts | 22 ++ 4 files changed, 204 insertions(+), 40 deletions(-) diff --git a/ToDo.txt b/ToDo.txt index a8f6230..235c710 100644 --- a/ToDo.txt +++ b/ToDo.txt @@ -23,7 +23,7 @@ Last updated: 2026-01-20 (Compromised User / Pool ) • [x] Compromised User Fix (SAT) -• [ ] Pools Complete Setup check and refactor -- Implementing Logging Layout from Alex -- Talk with him (SAT) +• [x] Pools Complete Setup check and refactor -- Implementing Logging Layout from Alex -- Talk with him (SAT) • [x] Adjust and add Functionality for Download Acc Data and Delete Acc (SAT) • [ ] News Management (own pages for news) + Adjust the Dashboard to Display Latest news • [ ] Unified Modal Design @@ -31,7 +31,7 @@ Last updated: 2026-01-20 • [ ] UserMgmt table refactor with actions and filter options (SAT?) • [ ] Remove irrelevant statuses in userverify filter • [ ] User Status 1 Feld das wir nicht benutzen -• [ ] +• [ ] Pool mulit user actions (select 5 -> add to pool) ================================================================================ QUICK SHARED / CROSSOVER ITEMS diff --git a/src/app/admin/pool-management/hooks/getlist.ts b/src/app/admin/pool-management/hooks/getlist.ts index 2429052..d64beca 100644 --- a/src/app/admin/pool-management/hooks/getlist.ts +++ b/src/app/admin/pool-management/hooks/getlist.ts @@ -65,7 +65,7 @@ export function useAdminPools() { price: Number(item.price ?? 0), pool_type: item.pool_type === 'coffee' ? 'coffee' : 'other', is_active: Boolean(item.is_active), - membersCount: 0, + membersCount: Number(item.members_count ?? item.membersCount ?? 0), createdAt: String(item.created_at ?? new Date().toISOString()), })); log("✅ Pools: Mapped sample:", mapped.slice(0, 3)); @@ -103,7 +103,7 @@ export function useAdminPools() { price: Number(item.price ?? 0), pool_type: item.pool_type === 'coffee' ? 'coffee' : 'other', is_active: Boolean(item.is_active), - membersCount: 0, + membersCount: Number(item.members_count ?? item.membersCount ?? 0), createdAt: String(item.created_at ?? new Date().toISOString()), }))); log("✅ Pools: Refresh succeeded, items:", apiItems.length); diff --git a/src/app/admin/pool-management/manage/page.tsx b/src/app/admin/pool-management/manage/page.tsx index ffd597d..825d1a3 100644 --- a/src/app/admin/pool-management/manage/page.tsx +++ b/src/app/admin/pool-management/manage/page.tsx @@ -7,6 +7,7 @@ import { UsersIcon, PlusIcon, BanknotesIcon, CalendarDaysIcon, MagnifyingGlassIc import { useRouter, useSearchParams } from 'next/navigation' import useAuthStore from '../../../store/authStore' import PageTransitionEffect from '../../../components/animation/pageTransitionEffect' +import { AdminAPI } from '../../../utils/api' type PoolUser = { id: string @@ -20,6 +21,7 @@ function PoolManagePageInner() { const router = useRouter() const searchParams = useSearchParams() const user = useAuthStore(s => s.user) + const token = useAuthStore(s => s.accessToken) const isAdmin = !!user && ( @@ -54,6 +56,8 @@ function PoolManagePageInner() { // Members (no dummy data) const [users, setUsers] = React.useState([]) + const [membersLoading, setMembersLoading] = React.useState(false) + const [membersError, setMembersError] = React.useState('') // Stats (no dummy data) const [totalAmount, setTotalAmount] = React.useState(0) @@ -67,11 +71,43 @@ function PoolManagePageInner() { const [error, setError] = React.useState('') const [candidates, setCandidates] = React.useState>([]) const [hasSearched, setHasSearched] = React.useState(false) + const [selectedCandidates, setSelectedCandidates] = React.useState>(new Set()) + const [savingMembers, setSavingMembers] = React.useState(false) + + async function fetchMembers() { + if (!token || !poolId || poolId === 'pool-unknown') return + setMembersError('') + setMembersLoading(true) + try { + const resp = await AdminAPI.getPoolMembers(token, poolId) + const rows = Array.isArray(resp?.members) ? resp.members : [] + const mapped: PoolUser[] = rows.map((row: any) => { + const name = row.company_name + ? String(row.company_name) + : [row.first_name, row.last_name].filter(Boolean).join(' ').trim() + return { + id: String(row.id), + name: name || String(row.email || '').trim() || 'Unnamed user', + email: String(row.email || '').trim(), + contributed: 0, + joinedAt: row.joined_at || new Date().toISOString() + } + }) + setUsers(mapped) + } catch (e: any) { + setMembersError(e?.message || 'Failed to load pool members.') + } finally { + setMembersLoading(false) + } + } + + React.useEffect(() => { + void fetchMembers() + }, [token, poolId]) // 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() @@ -80,23 +116,99 @@ function PoolManagePageInner() { setCandidates([]) return } + if (!token) { + setError('Authentication required.') + setHasSearched(true) + setCandidates([]) + return + } + setHasSearched(true) setLoading(true) - setTimeout(() => { - setCandidates([]) // no local dummy results + try { + const resp = await AdminAPI.getUserList(token) + const list = Array.isArray(resp?.users) ? resp.users : [] + + const existingIds = new Set(users.map(u => String(u.id))) + + const mapped = list + .filter((u: any) => u && u.role !== 'admin' && u.role !== 'super_admin') + .map((u: any) => { + const name = u.company_name + ? String(u.company_name) + : [u.first_name, u.last_name].filter(Boolean).join(' ').trim() + return { + id: String(u.id), + name: name || String(u.email || '').trim() || 'Unnamed user', + email: String(u.email || '').trim() + } + }) + .filter(u => !existingIds.has(u.id)) + .filter(u => { + const hay = `${u.name} ${u.email}`.toLowerCase() + return hay.includes(q) + }) + + setCandidates(mapped) + } catch (e: any) { + setError(e?.message || 'Failed to search users.') + setCandidates([]) + } finally { 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) + async function addUserFromModal(u: { id: string; name: string; email: string }) { + if (!token || !poolId || poolId === 'pool-unknown') return + setSavingMembers(true) setError('') - setLoading(false) + try { + await AdminAPI.addPoolMembers(token, poolId, [u.id]) + await fetchMembers() + setSearchOpen(false) + setQuery('') + setCandidates([]) + setHasSearched(false) + setSelectedCandidates(new Set()) + } catch (e: any) { + setError(e?.message || 'Failed to add user.') + } finally { + setLoading(false) + setSavingMembers(false) + } + } + + function toggleCandidate(id: string) { + setSelectedCandidates(prev => { + const next = new Set(prev) + if (next.has(id)) next.delete(id) + else next.add(id) + return next + }) + } + + async function addSelectedUsers() { + if (selectedCandidates.size === 0) return + const selectedList = candidates.filter(c => selectedCandidates.has(c.id)) + if (selectedList.length === 0) return + if (!token || !poolId || poolId === 'pool-unknown') return + setSavingMembers(true) + setError('') + try { + const userIds = selectedList.map(u => u.id) + await AdminAPI.addPoolMembers(token, poolId, userIds) + await fetchMembers() + setSearchOpen(false) + setQuery('') + setCandidates([]) + setHasSearched(false) + setSelectedCandidates(new Set()) + } catch (e: any) { + setError(e?.message || 'Failed to add users.') + } finally { + setLoading(false) + setSavingMembers(false) + } } return ( @@ -215,7 +327,17 @@ function PoolManagePageInner() { ))} - {users.length === 0 && ( + {membersLoading && ( +
+ Loading members... +
+ )} + {membersError && !membersLoading && ( +
+ {membersError} +
+ )} + {users.length === 0 && !membersLoading && !membersError && (
No users in this pool yet.
@@ -307,22 +429,30 @@ function PoolManagePageInner() { {!error && candidates.length > 0 && (
    {candidates.map(u => ( -
  • -
    -
    - - {u.name} -
    -
    {u.email}
    -
    - -
  • - ))} +
  • + + +
  • + ))}
)} {loading && candidates.length > 0 && ( @@ -335,13 +465,25 @@ function PoolManagePageInner() { {/* Footer */} -
- +
+
+ {selectedCandidates.size > 0 ? `${selectedCandidates.size} selected` : 'No users selected'} +
+
+ + +
diff --git a/src/app/utils/api.ts b/src/app/utils/api.ts index e56f432..f87250a 100644 --- a/src/app/utils/api.ts +++ b/src/app/utils/api.ts @@ -46,6 +46,8 @@ export const API_ENDPOINTS = { ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id', ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id', ADMIN_UPDATE_USER_STATUS: '/api/admin/update-user-status/:id', + // Pools (admin) + ADMIN_POOL_MEMBERS: '/api/admin/pools/:id/members', // Coffee products (admin) ADMIN_COFFEE_LIST: '/api/admin/coffee', ADMIN_COFFEE_CREATE: '/api/admin/coffee', @@ -343,6 +345,26 @@ export class AdminAPI { return response.json() } + static async getPoolMembers(token: string, poolId: string | number) { + const endpoint = API_ENDPOINTS.ADMIN_POOL_MEMBERS.replace(':id', String(poolId)) + const response = await ApiClient.get(endpoint, token) + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Failed to fetch pool members' })) + throw new Error(error.message || 'Failed to fetch pool members') + } + return response.json() + } + + static async addPoolMembers(token: string, poolId: string | number, userIds: Array) { + const endpoint = API_ENDPOINTS.ADMIN_POOL_MEMBERS.replace(':id', String(poolId)) + const response = await ApiClient.post(endpoint, { userIds }, token) + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Failed to add pool members' })) + throw new Error(error.message || 'Failed to add pool members') + } + return response.json() + } + static async updateUserStatus(token: string, userId: string, status: string) { const endpoint = API_ENDPOINTS.ADMIN_UPDATE_USER_STATUS.replace(':id', userId) const response = await ApiClient.patch(endpoint, { status }, token)