187 lines
6.8 KiB
TypeScript
187 lines
6.8 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useState } from 'react'
|
||
import { useAdminI18n } from '@/components/I18nProvider'
|
||
import { ADMIN_API_BASE } from '@/lib/api'
|
||
|
||
interface Renter {
|
||
id: string
|
||
firstName: string
|
||
lastName: string
|
||
email: string
|
||
phone: string | null
|
||
isBlocked: boolean
|
||
createdAt: string
|
||
}
|
||
|
||
export default function AdminRentersPage() {
|
||
const { language, dict } = useAdminI18n()
|
||
const copy = {
|
||
en: {
|
||
title: 'Renters',
|
||
search: 'Search renters…',
|
||
name: 'Name',
|
||
email: 'Email',
|
||
phone: 'Phone',
|
||
status: 'Status',
|
||
joined: 'Joined',
|
||
loading: 'Loading…',
|
||
empty: 'No renters found',
|
||
blocked: 'Blocked',
|
||
active: 'Active',
|
||
unblock: 'Unblock',
|
||
block: 'Block',
|
||
},
|
||
fr: {
|
||
title: 'Locataires',
|
||
search: 'Rechercher des locataires…',
|
||
name: 'Nom',
|
||
email: 'E-mail',
|
||
phone: 'Téléphone',
|
||
status: 'Statut',
|
||
joined: 'Date d’inscription',
|
||
loading: 'Chargement…',
|
||
empty: 'Aucun locataire trouvé',
|
||
blocked: 'Bloqué',
|
||
active: 'Actif',
|
||
unblock: 'Débloquer',
|
||
block: 'Bloquer',
|
||
},
|
||
ar: {
|
||
title: 'المستأجرون',
|
||
search: 'ابحث عن مستأجرين…',
|
||
name: 'الاسم',
|
||
email: 'البريد الإلكتروني',
|
||
phone: 'الهاتف',
|
||
status: 'الحالة',
|
||
joined: 'تاريخ التسجيل',
|
||
loading: 'جارٍ التحميل…',
|
||
empty: 'لم يتم العثور على مستأجرين',
|
||
blocked: 'محظور',
|
||
active: 'نشط',
|
||
unblock: 'فك الحظر',
|
||
block: 'حظر',
|
||
},
|
||
}[language]
|
||
const [renters, setRenters] = useState<Renter[]>([])
|
||
const [filtered, setFiltered] = useState<Renter[]>([])
|
||
const [search, setSearch] = useState('')
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [actioning, setActioning] = useState<string | null>(null)
|
||
|
||
async function fetchRenters() {
|
||
try {
|
||
const res = await fetch(`${ADMIN_API_BASE}/admin/renters`, {
|
||
cache: 'no-store',
|
||
credentials: 'include',
|
||
})
|
||
const json = await res.json()
|
||
if (!res.ok) throw new Error(json?.message ?? 'Failed')
|
||
const list = Array.isArray(json.data) ? json.data : (json.data?.data ?? [])
|
||
setRenters(list)
|
||
setFiltered(list)
|
||
} catch (err: any) {
|
||
setError(err.message)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => { fetchRenters() }, [])
|
||
|
||
useEffect(() => {
|
||
const q = search.toLowerCase()
|
||
setFiltered(renters.filter((r) =>
|
||
`${r.firstName} ${r.lastName}`.toLowerCase().includes(q) || r.email.toLowerCase().includes(q)
|
||
))
|
||
}, [search, renters])
|
||
|
||
async function toggleBlock(id: string, isBlocked: boolean) {
|
||
setActioning(id)
|
||
try {
|
||
const endpoint = isBlocked ? 'unblock' : 'block'
|
||
const res = await fetch(`${ADMIN_API_BASE}/admin/renters/${id}/${endpoint}`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
})
|
||
if (!res.ok) throw new Error('Action failed')
|
||
await fetchRenters()
|
||
} catch (err: any) {
|
||
setError(err.message)
|
||
} finally {
|
||
setActioning(null)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="shell py-8 space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-xs uppercase tracking-[0.2em] text-emerald-400">{dict.platform}</p>
|
||
<h1 className="mt-1 text-3xl font-black">{copy.title}</h1>
|
||
</div>
|
||
<input
|
||
className="w-64 px-3 py-2 rounded-xl bg-zinc-800 border border-zinc-700 text-zinc-100 text-sm placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||
placeholder={copy.search}
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
{error && <div className="panel p-4 text-sm text-red-400">{error}</div>}
|
||
|
||
<div className="panel overflow-hidden">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-zinc-800">
|
||
<th className="text-left px-6 py-3 text-xs font-medium text-zinc-500 uppercase tracking-wider">{copy.name}</th>
|
||
<th className="text-left px-6 py-3 text-xs font-medium text-zinc-500 uppercase tracking-wider">{copy.email}</th>
|
||
<th className="text-left px-6 py-3 text-xs font-medium text-zinc-500 uppercase tracking-wider">{copy.phone}</th>
|
||
<th className="text-left px-6 py-3 text-xs font-medium text-zinc-500 uppercase tracking-wider">{copy.status}</th>
|
||
<th className="text-left px-6 py-3 text-xs font-medium text-zinc-500 uppercase tracking-wider">{copy.joined}</th>
|
||
<th className="px-6 py-3" />
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-zinc-800/60">
|
||
{loading ? (
|
||
<tr><td colSpan={6} className="px-6 py-12 text-center text-zinc-500">{copy.loading}</td></tr>
|
||
) : filtered.length === 0 ? (
|
||
<tr><td colSpan={6} className="px-6 py-12 text-center text-zinc-500">{copy.empty}</td></tr>
|
||
) : filtered.map((r) => (
|
||
<tr key={r.id} className="hover:bg-zinc-800/30 transition-colors">
|
||
<td className="px-6 py-4 font-medium text-zinc-100">{r.firstName} {r.lastName}</td>
|
||
<td className="px-6 py-4 text-zinc-400">{r.email}</td>
|
||
<td className="px-6 py-4 text-zinc-400">{r.phone ?? '—'}</td>
|
||
<td className="px-6 py-4">
|
||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||
r.isBlocked ? 'text-red-400 bg-red-950/40' : 'text-emerald-400 bg-emerald-950/40'
|
||
}`}>
|
||
{r.isBlocked ? copy.blocked : copy.active}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4 text-zinc-500 text-xs">{new Date(r.createdAt).toLocaleDateString()}</td>
|
||
<td className="px-6 py-4">
|
||
<button
|
||
onClick={() => toggleBlock(r.id, r.isBlocked)}
|
||
disabled={actioning === r.id}
|
||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors disabled:opacity-50 ${
|
||
r.isBlocked
|
||
? 'bg-emerald-950/50 hover:bg-emerald-900/50 text-emerald-400'
|
||
: 'bg-red-950/50 hover:bg-red-900/50 text-red-400'
|
||
}`}
|
||
>
|
||
{r.isBlocked ? copy.unblock : copy.block}
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|