feat: implement pool members management functionality in admin panel
This commit is contained in:
parent
430c72d4cd
commit
7d9399df4b
4
ToDo.txt
4
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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<PoolUser[]>([])
|
||||
const [membersLoading, setMembersLoading] = React.useState(false)
|
||||
const [membersError, setMembersError] = React.useState<string>('')
|
||||
|
||||
// Stats (no dummy data)
|
||||
const [totalAmount, setTotalAmount] = React.useState<number>(0)
|
||||
@ -67,11 +71,43 @@ function PoolManagePageInner() {
|
||||
const [error, setError] = React.useState<string>('')
|
||||
const [candidates, setCandidates] = React.useState<Array<{ id: string; name: string; email: string }>>([])
|
||||
const [hasSearched, setHasSearched] = React.useState(false)
|
||||
const [selectedCandidates, setSelectedCandidates] = React.useState<Set<string>>(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() {
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{users.length === 0 && (
|
||||
{membersLoading && (
|
||||
<div className="col-span-full text-center text-gray-500 italic py-6">
|
||||
Loading members...
|
||||
</div>
|
||||
)}
|
||||
{membersError && !membersLoading && (
|
||||
<div className="col-span-full text-center text-red-600 py-6">
|
||||
{membersError}
|
||||
</div>
|
||||
)}
|
||||
{users.length === 0 && !membersLoading && !membersError && (
|
||||
<div className="col-span-full text-center text-gray-500 italic py-6">
|
||||
No users in this pool yet.
|
||||
</div>
|
||||
@ -307,22 +429,30 @@ function PoolManagePageInner() {
|
||||
{!error && candidates.length > 0 && (
|
||||
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-lg bg-white">
|
||||
{candidates.map(u => (
|
||||
<li key={u.id} className="px-4 py-3 flex items-center justify-between gap-3 hover:bg-gray-50 transition">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<UsersIcon className="h-4 w-4 text-blue-900" />
|
||||
<span className="text-sm font-medium text-gray-900 truncate max-w-[200px]">{u.name}</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-[11px] text-gray-600 break-all">{u.email}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => addUserFromModal(u)}
|
||||
className="shrink-0 inline-flex items-center rounded-md bg-blue-900 hover:bg-blue-800 text-white px-3 py-1.5 text-xs font-medium shadow-sm transition"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
<li key={u.id} className="px-4 py-3 flex items-center justify-between gap-3 hover:bg-gray-50 transition">
|
||||
<label className="min-w-0 flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-900 focus:ring-blue-900"
|
||||
checked={selectedCandidates.has(u.id)}
|
||||
onChange={() => toggleCandidate(u.id)}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<UsersIcon className="h-4 w-4 text-blue-900" />
|
||||
<span className="text-sm font-medium text-gray-900 truncate max-w-[200px]">{u.name}</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-[11px] text-gray-600 break-all">{u.email}</div>
|
||||
</div>
|
||||
</label>
|
||||
<button
|
||||
onClick={() => addUserFromModal(u)}
|
||||
className="shrink-0 inline-flex items-center rounded-md bg-blue-900 hover:bg-blue-800 text-white px-3 py-1.5 text-xs font-medium shadow-sm transition"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{loading && candidates.length > 0 && (
|
||||
@ -335,13 +465,25 @@ function PoolManagePageInner() {
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-3 border-t border-gray-100 flex items-center justify-end bg-gray-50">
|
||||
<button
|
||||
onClick={() => setSearchOpen(false)}
|
||||
className="text-sm rounded-md px-4 py-2 font-medium bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 transition"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
<div className="px-6 py-3 border-t border-gray-100 flex items-center justify-between bg-gray-50">
|
||||
<div className="text-xs text-gray-600">
|
||||
{selectedCandidates.size > 0 ? `${selectedCandidates.size} selected` : 'No users selected'}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setSearchOpen(false)}
|
||||
className="text-sm rounded-md px-4 py-2 font-medium bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 transition"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
<button
|
||||
onClick={addSelectedUsers}
|
||||
disabled={selectedCandidates.size === 0 || savingMembers}
|
||||
className="text-sm rounded-md px-4 py-2 font-medium bg-blue-900 text-white hover:bg-blue-800 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{savingMembers ? 'Adding…' : 'Add Selected'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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<string | number>) {
|
||||
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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user