profit-planet-frontend/src/app/admin/pool-management/manage/page.tsx

353 lines
17 KiB
TypeScript

'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('pool_name') ?? 'Unnamed Pool'
const poolDescription = searchParams.get('description') ?? ''
const poolPrice = parseFloat(searchParams.get('price') ?? '0')
const poolType = searchParams.get('pool_type') as 'coffee' | 'other' || 'other'
const poolIsActive = searchParams.get('is_active') === 'true'
const poolCreatedAt = searchParams.get('createdAt') ?? new Date().toISOString()
// Members (no dummy data)
const [users, setUsers] = React.useState<PoolUser[]>([])
// Stats (no dummy data)
const [totalAmount, setTotalAmount] = React.useState<number>(0)
const [amountThisYear, setAmountThisYear] = React.useState<number>(0)
const [amountThisMonth, setAmountThisMonth] = React.useState<number>(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<string>('')
const [candidates, setCandidates] = React.useState<Array<{ id: string; name: string; email: string }>>([])
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 (
<PageTransitionEffect>
<div className="min-h-screen flex flex-col bg-gradient-to-tr from-blue-50 via-white to-blue-100">
<Header />
{/* main wrapper: avoid high z-index stacking */}
<main className="flex-1 py-8 px-4 sm:px-6 lg:px-8 relative z-0">
<div className="max-w-7xl mx-auto relative z-0">
{/* Header (remove sticky/z-10) */}
<header className="bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-3 mb-8 relative z-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-blue-50 border border-blue-200 flex items-center justify-center">
<UsersIcon className="h-5 w-5 text-blue-900" />
</div>
<div>
<h1 className="text-3xl font-extrabold text-blue-900 tracking-tight">{poolName}</h1>
<p className="text-sm text-blue-700">
{poolDescription ? poolDescription : 'Manage users and track pool funds'}
</p>
<div className="mt-1 flex items-center gap-2 text-xs text-gray-600">
<span className={`inline-flex items-center rounded-full px-2 py-0.5 font-medium ${!poolIsActive ? 'bg-gray-100 text-gray-700' : 'bg-green-100 text-green-800'}`}>
<span className={`mr-1.5 h-1.5 w-1.5 rounded-full ${!poolIsActive ? 'bg-gray-400' : 'bg-green-500'}`} />
{!poolIsActive ? 'Inactive' : 'Active'}
</span>
<span></span>
<span>Created {new Date(poolCreatedAt).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: '2-digit' })}</span>
<span></span>
<span className="text-gray-500">ID: {poolId}</span>
</div>
</div>
</div>
{/* Back to Pool Management */}
<button
onClick={() => router.push('/admin/pool-management')}
className="inline-flex items-center gap-2 rounded-lg bg-white text-blue-900 border border-blue-200 px-4 py-2 text-sm font-medium hover:bg-blue-50 transition"
title="Back to Pool Management"
>
Back
</button>
</div>
</header>
{/* Stats (now zero until backend wired) */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-8 relative z-0">
<div className="relative overflow-hidden rounded-2xl bg-white px-6 py-5 shadow-lg border border-gray-100">
<div className="flex items-center gap-3">
<div className="rounded-md bg-blue-900 p-2">
<BanknotesIcon className="h-5 w-5 text-white" />
</div>
<div>
<p className="text-sm text-gray-600">Total in Pool</p>
<p className="text-2xl font-semibold text-gray-900"> {totalAmount.toLocaleString()}</p>
</div>
</div>
</div>
<div className="relative overflow-hidden rounded-2xl bg-white px-6 py-5 shadow-lg border border-gray-100">
<div className="flex items-center gap-3">
<div className="rounded-md bg-amber-600 p-2">
<CalendarDaysIcon className="h-5 w-5 text-white" />
</div>
<div>
<p className="text-sm text-gray-600">This Year</p>
<p className="text-2xl font-semibold text-gray-900"> {amountThisYear.toLocaleString()}</p>
</div>
</div>
</div>
<div className="relative overflow-hidden rounded-2xl bg-white px-6 py-5 shadow-lg border border-gray-100">
<div className="flex items-center gap-3">
<div className="rounded-md bg-green-600 p-2">
<CalendarDaysIcon className="h-5 w-5 text-white" />
</div>
<div>
<p className="text-sm text-gray-600">Current Month</p>
<p className="text-2xl font-semibold text-gray-900"> {amountThisMonth.toLocaleString()}</p>
</div>
</div>
</div>
</div>
{/* Unified Members card: add button + list */}
<div className="rounded-2xl bg-white border border-gray-100 shadow-lg p-6 relative z-0">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-blue-900">Members</h2>
<button
onClick={() => { setSearchOpen(true); setQuery(''); setCandidates([]); setHasSearched(false); setError(''); }}
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-sm font-semibold shadow transition"
>
<PlusIcon className="h-5 w-5" />
Add User
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{users.map(u => (
<article key={u.id} className="rounded-2xl bg-white border border-gray-100 shadow p-5 flex flex-col">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="h-9 w-9 rounded-lg bg-blue-50 border border-blue-200 flex items-center justify-center">
<UsersIcon className="h-5 w-5 text-blue-900" />
</div>
<div>
<h3 className="text-sm font-semibold text-blue-900">{u.name}</h3>
<p className="text-xs text-gray-600">{u.email}</p>
</div>
</div>
<span className="inline-flex items-center rounded-full bg-blue-50 border border-blue-200 px-2 py-0.5 text-xs text-blue-900">
{u.contributed.toLocaleString()}
</span>
</div>
<div className="mt-3 text-xs text-gray-600">
Member since:{' '}
<span className="font-medium text-gray-900">
{new Date(u.joinedAt).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: '2-digit' })}
</span>
</div>
</article>
))}
{users.length === 0 && (
<div className="col-span-full text-center text-gray-500 italic py-6">
No users in this pool yet.
</div>
)}
</div>
</div>
</div>
</main>
<Footer />
{/* Search Modal (keep above with high z) */}
{searchOpen && (
<div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => setSearchOpen(false)} />
<div className="absolute inset-0 flex items-center justify-center p-4 sm:p-6">
<div className="w-full max-w-2xl rounded-2xl overflow-hidden bg-white shadow-2xl ring-1 ring-black/10 flex flex-col">
{/* Header */}
<div className="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
<h4 className="text-lg font-semibold text-blue-900">Add user to pool</h4>
<button
onClick={() => setSearchOpen(false)}
className="p-1.5 rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-700 transition"
aria-label="Close"
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
{/* Form */}
<form
onSubmit={e => { e.preventDefault(); void doSearch(); }}
className="px-6 py-4 grid grid-cols-1 md:grid-cols-5 gap-3 border-b border-gray-100"
>
<div className="md:col-span-3">
<div className="relative">
<MagnifyingGlassIcon className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search name or email…"
className="w-full rounded-md bg-gray-50 border border-gray-300 text-sm text-gray-900 placeholder-gray-400 pl-8 pr-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent transition"
/>
</div>
</div>
<div className="flex gap-2 md:col-span-2">
<button
type="submit"
disabled={loading || query.trim().length < 3}
className="flex-1 rounded-md bg-blue-900 hover:bg-blue-800 disabled:opacity-50 text-white px-3 py-2 text-sm font-medium shadow-sm transition"
>
{loading ? 'Searching…' : 'Search'}
</button>
<button
type="button"
onClick={() => { setQuery(''); setCandidates([]); setHasSearched(false); setError(''); }}
className="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition"
>
Clear
</button>
</div>
</form>
<div className="px-6 pt-1 pb-3 text-right text-xs text-gray-500">
Min. 3 characters
</div>
{/* Results */}
<div className="px-6 py-4">
{error && <div className="text-sm text-red-600 mb-3">{error}</div>}
{!error && query.trim().length < 3 && (
<div className="py-8 text-sm text-gray-500 text-center">
Enter at least 3 characters and click Search.
</div>
)}
{!error && hasSearched && loading && candidates.length === 0 && (
<ul className="space-y-0 divide-y divide-gray-200 border border-gray-200 rounded-md bg-gray-50">
{Array.from({ length: 5 }).map((_, i) => (
<li key={i} className="animate-pulse px-4 py-3">
<div className="h-3.5 w-36 bg-gray-200 rounded" />
<div className="mt-2 h-3 w-56 bg-gray-100 rounded" />
</li>
))}
</ul>
)}
{!error && hasSearched && !loading && candidates.length === 0 && (
<div className="py-8 text-sm text-gray-500 text-center">
No users match your search.
</div>
)}
{!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>
))}
</ul>
)}
{loading && candidates.length > 0 && (
<div className="pointer-events-none relative">
<div className="absolute inset-0 flex items-center justify-center bg-white/60">
<span className="h-5 w-5 rounded-full border-2 border-blue-900 border-b-transparent animate-spin" />
</div>
</div>
)}
</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>
</div>
</div>
</div>
)}
</div>
</PageTransitionEffect>
)
}