Files
carmanagement/apps/admin/src/app/dashboard/renters/page.tsx
T
2026-06-10 00:40:19 -04:00

187 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 dinscription',
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>
)
}