295 lines
13 KiB
TypeScript
295 lines
13 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import Header from '../../components/nav/Header'
|
|
import Footer from '../../components/Footer'
|
|
import { UsersIcon } from '@heroicons/react/24/outline'
|
|
import { useAdminPools } from './hooks/getlist'
|
|
import useAuthStore from '../../store/authStore'
|
|
import { addPool } from './hooks/addPool'
|
|
import { useRouter } from 'next/navigation'
|
|
import { setPoolInactive, setPoolActive } from './hooks/poolStatus'
|
|
import PageTransitionEffect from '../../components/animation/pageTransitionEffect'
|
|
import CreateNewPoolModal from './components/createNewPoolModal'
|
|
|
|
type Pool = {
|
|
id: string
|
|
pool_name: string
|
|
description?: string
|
|
price?: number
|
|
pool_type?: 'coffee' | 'other'
|
|
is_active?: boolean
|
|
membersCount: number
|
|
createdAt: string
|
|
}
|
|
|
|
export default function PoolManagementPage() {
|
|
const router = useRouter()
|
|
|
|
// Modal state
|
|
const [creating, setCreating] = React.useState(false)
|
|
const [createError, setCreateError] = React.useState<string>('')
|
|
const [createSuccess, setCreateSuccess] = React.useState<string>('')
|
|
const [createModalOpen, setCreateModalOpen] = React.useState(false)
|
|
const [archiveError, setArchiveError] = React.useState<string>('')
|
|
|
|
// Token and API URL
|
|
const token = useAuthStore.getState().accessToken
|
|
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'
|
|
|
|
// Replace local fetch with hook
|
|
const { pools: initialPools, loading, error, refresh } = useAdminPools()
|
|
const [pools, setPools] = React.useState<Pool[]>([])
|
|
const [showInactive, setShowInactive] = React.useState(false)
|
|
|
|
React.useEffect(() => {
|
|
if (!loading && !error) {
|
|
setPools(initialPools)
|
|
}
|
|
}, [initialPools, loading, error])
|
|
|
|
const filteredPools = pools.filter(p => showInactive ? !p.is_active : p.is_active)
|
|
|
|
// REPLACED: handleCreatePool to accept data from modal with new schema fields
|
|
async function handleCreatePool(data: { pool_name: string; description: string; price: number; pool_type: 'coffee' | 'other' }) {
|
|
setCreateError('')
|
|
setCreateSuccess('')
|
|
const pool_name = data.pool_name.trim()
|
|
const description = data.description.trim()
|
|
if (!pool_name) {
|
|
setCreateError('Please provide a pool name.')
|
|
return
|
|
}
|
|
setCreating(true)
|
|
try {
|
|
const res = await addPool({ pool_name, description: description || undefined, price: data.price, pool_type: data.pool_type, is_active: true })
|
|
if (res.ok && res.body?.data) {
|
|
setCreateSuccess('Pool created successfully.')
|
|
await refresh?.()
|
|
setTimeout(() => {
|
|
setCreateModalOpen(false)
|
|
setCreateSuccess('')
|
|
}, 1500)
|
|
} else {
|
|
setCreateError(res.message || 'Failed to create pool.')
|
|
}
|
|
} catch {
|
|
setCreateError('Network error while creating pool.')
|
|
} finally {
|
|
setCreating(false)
|
|
}
|
|
}
|
|
|
|
async function handleArchive(poolId: string) {
|
|
setArchiveError('')
|
|
const res = await setPoolInactive(poolId)
|
|
if (res.ok) {
|
|
await refresh?.()
|
|
} else {
|
|
setArchiveError(res.message || 'Failed to deactivate pool.')
|
|
}
|
|
}
|
|
|
|
async function handleSetActive(poolId: string) {
|
|
setArchiveError('')
|
|
const res = await setPoolActive(poolId)
|
|
if (res.ok) {
|
|
await refresh?.()
|
|
} else {
|
|
setArchiveError(res.message || 'Failed to activate pool.')
|
|
}
|
|
}
|
|
|
|
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'))
|
|
)
|
|
|
|
// NEW: block rendering until we decide access
|
|
const [authChecked, setAuthChecked] = React.useState(false)
|
|
React.useEffect(() => {
|
|
// When user is null -> unauthenticated; undefined means not loaded yet (store default may be null in this app).
|
|
if (user === null) {
|
|
router.replace('/login')
|
|
return
|
|
}
|
|
if (user && !isAdmin) {
|
|
router.replace('/')
|
|
return
|
|
}
|
|
// user exists and is admin
|
|
setAuthChecked(true)
|
|
}, [user, isAdmin, router])
|
|
|
|
// Early return: render nothing until authorized, prevents any flash
|
|
if (!authChecked) return null
|
|
|
|
// Remove Access Denied overlay; render normal content
|
|
return (
|
|
<PageTransitionEffect>
|
|
<div className="min-h-screen flex flex-col bg-gradient-to-tr from-blue-50 via-white to-blue-100">
|
|
<Header />
|
|
<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 className="bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8 relative z-0">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Pool Management</h1>
|
|
<p className="text-lg text-blue-700 mt-2">Create and manage user pools.</p>
|
|
</div>
|
|
<button
|
|
onClick={() => { setCreateModalOpen(true); createError && setCreateError(''); }}
|
|
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"
|
|
>
|
|
Create New Pool
|
|
</button>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-gray-600">Show:</span>
|
|
<button
|
|
onClick={() => setShowInactive(false)}
|
|
className={`px-4 py-2 text-sm font-medium rounded-lg transition ${!showInactive ? 'bg-blue-900 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
|
|
>
|
|
Active Pools
|
|
</button>
|
|
<button
|
|
onClick={() => setShowInactive(true)}
|
|
className={`px-4 py-2 text-sm font-medium rounded-lg transition ${showInactive ? 'bg-blue-900 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
|
|
>
|
|
Inactive Pools
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Pools List card */}
|
|
<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">Existing Pools</h2>
|
|
<span className="text-sm text-gray-600">{pools.length} total</span>
|
|
</div>
|
|
|
|
{/* Show archive errors */}
|
|
{archiveError && (
|
|
<div className="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
|
{archiveError}
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
|
{error}
|
|
</div>
|
|
)}
|
|
{loading ? (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<div key={i} className="rounded-2xl bg-white border border-gray-100 shadow p-5">
|
|
<div className="animate-pulse space-y-3">
|
|
<div className="h-5 w-1/2 bg-gray-200 rounded" />
|
|
<div className="h-4 w-3/4 bg-gray-200 rounded" />
|
|
<div className="h-4 w-2/3 bg-gray-100 rounded" />
|
|
<div className="h-8 w-full bg-gray-100 rounded" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{filteredPools.map(pool => (
|
|
<article key={pool.id} className="rounded-2xl bg-white border border-gray-100 shadow p-5 flex flex-col relative z-0">
|
|
<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>
|
|
<h3 className="text-lg font-semibold text-blue-900">{pool.pool_name}</h3>
|
|
</div>
|
|
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${!pool.is_active ? '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 ${!pool.is_active ? 'bg-gray-400' : 'bg-green-500'}`} />
|
|
{!pool.is_active ? 'Inactive' : 'Active'}
|
|
</span>
|
|
</div>
|
|
<p className="mt-2 text-sm text-gray-700">{pool.description || '-'}</p>
|
|
<div className="mt-4 grid grid-cols-2 gap-3 text-sm text-gray-600">
|
|
<div>
|
|
<span className="text-gray-500">Members</span>
|
|
<div className="font-medium text-gray-900">{pool.membersCount}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">Created</span>
|
|
<div className="font-medium text-gray-900">
|
|
{new Date(pool.createdAt).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: '2-digit' })}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="mt-5 flex items-center justify-between">
|
|
<button
|
|
className="px-4 py-2 text-xs font-medium rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300 transition"
|
|
onClick={() => {
|
|
const params = new URLSearchParams({
|
|
id: String(pool.id),
|
|
pool_name: pool.pool_name ?? '',
|
|
description: pool.description ?? '',
|
|
price: String(pool.price ?? 0),
|
|
pool_type: pool.pool_type ?? 'other',
|
|
is_active: pool.is_active ? 'true' : 'false',
|
|
createdAt: pool.createdAt ?? '',
|
|
})
|
|
router.push(`/admin/pool-management/manage?${params.toString()}`)
|
|
}}
|
|
>
|
|
Manage
|
|
</button>
|
|
{!pool.is_active ? (
|
|
<button
|
|
className="px-4 py-2 text-xs font-medium rounded-lg bg-green-100 text-green-800 hover:bg-green-200 transition"
|
|
onClick={() => handleSetActive(pool.id)}
|
|
title="Activate this pool"
|
|
>
|
|
Set Active
|
|
</button>
|
|
) : (
|
|
<button
|
|
className="px-4 py-2 text-xs font-medium rounded-lg bg-amber-100 text-amber-800 hover:bg-amber-200 transition"
|
|
onClick={() => handleArchive(pool.id)}
|
|
title="Archive this pool"
|
|
>
|
|
Archive
|
|
</button>
|
|
)}
|
|
</div>
|
|
</article>
|
|
))}
|
|
{filteredPools.length === 0 && !loading && !error && (
|
|
<div className="col-span-full text-center text-gray-500 italic py-6">
|
|
{showInactive ? 'No inactive pools found.' : 'No active pools found.'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
{/* Modal for creating a new pool */}
|
|
<CreateNewPoolModal
|
|
isOpen={createModalOpen}
|
|
onClose={() => { setCreateModalOpen(false); setCreateError(''); setCreateSuccess(''); }}
|
|
onCreate={handleCreatePool}
|
|
creating={creating}
|
|
error={createError}
|
|
success={createSuccess}
|
|
clearMessages={() => { setCreateError(''); setCreateSuccess(''); }}
|
|
/>
|
|
|
|
<Footer />
|
|
</div>
|
|
</PageTransitionEffect>
|
|
)
|
|
}
|