184 lines
7.6 KiB
TypeScript
184 lines
7.6 KiB
TypeScript
import Link from "next/link";
|
|
import { BookingStatus, PaymentStatus } from "@/generated/prisma/enums";
|
|
import { listBookingsAdmin } from "@/lib/admin/bookings";
|
|
import { StatusBadge } from "@/components/admin/StatusBadge";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type PageProps = {
|
|
searchParams: Promise<{
|
|
q?: string;
|
|
status?: string;
|
|
paymentStatus?: string;
|
|
from?: string;
|
|
to?: string;
|
|
}>;
|
|
};
|
|
|
|
const STATUS_VALUES = new Set<string>([
|
|
BookingStatus.PENDING,
|
|
BookingStatus.CONFIRMED,
|
|
BookingStatus.CANCELLED,
|
|
BookingStatus.COMPLETED,
|
|
]);
|
|
const PAYMENT_VALUES = new Set<string>([
|
|
PaymentStatus.PENDING,
|
|
PaymentStatus.AUTHORIZED,
|
|
PaymentStatus.SUCCEEDED,
|
|
PaymentStatus.FAILED,
|
|
PaymentStatus.REFUNDED,
|
|
]);
|
|
|
|
function parseDate(v?: string): Date | undefined {
|
|
if (!v) return undefined;
|
|
const d = new Date(v);
|
|
return isNaN(d.getTime()) ? undefined : d;
|
|
}
|
|
|
|
export default async function BookingsAdminPage({ searchParams }: PageProps) {
|
|
const sp = await searchParams;
|
|
const filters = {
|
|
q: sp.q?.trim() || undefined,
|
|
status: STATUS_VALUES.has(sp.status ?? "") ? (sp.status as BookingStatus) : undefined,
|
|
paymentStatus: PAYMENT_VALUES.has(sp.paymentStatus ?? "")
|
|
? (sp.paymentStatus as PaymentStatus)
|
|
: undefined,
|
|
from: parseDate(sp.from),
|
|
to: parseDate(sp.to),
|
|
};
|
|
const bookings = await listBookingsAdmin(filters);
|
|
const dateFmt = new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "short", year: "2-digit" });
|
|
|
|
return (
|
|
<div className="mx-auto max-w-7xl">
|
|
<header className="mb-5 mt-2 flex items-end justify-between gap-3">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold text-zinc-900">Réservations</h1>
|
|
<p className="mt-1 text-sm text-zinc-500">
|
|
{bookings.length} résultat{bookings.length > 1 ? "s" : ""}
|
|
{bookings.length === 200 ? " (limite atteinte — affinez les filtres)" : ""}
|
|
</p>
|
|
</div>
|
|
</header>
|
|
|
|
<form className="mb-4 flex flex-wrap items-center gap-2 rounded-lg border border-zinc-200 bg-white p-3" method="get">
|
|
<input
|
|
type="text"
|
|
name="q"
|
|
defaultValue={filters.q ?? ""}
|
|
placeholder="Recherche ID, locataire, carbet…"
|
|
className="flex-1 min-w-[200px] rounded-md border border-zinc-300 px-3 py-1.5 text-sm focus:border-zinc-900 focus:outline-none"
|
|
/>
|
|
<select
|
|
name="status"
|
|
defaultValue={filters.status ?? ""}
|
|
className="rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm focus:border-zinc-900 focus:outline-none"
|
|
>
|
|
<option value="">Tous statuts</option>
|
|
<option value={BookingStatus.PENDING}>En attente</option>
|
|
<option value={BookingStatus.CONFIRMED}>Confirmé</option>
|
|
<option value={BookingStatus.CANCELLED}>Annulé</option>
|
|
<option value={BookingStatus.COMPLETED}>Terminé</option>
|
|
</select>
|
|
<select
|
|
name="paymentStatus"
|
|
defaultValue={filters.paymentStatus ?? ""}
|
|
className="rounded-md border border-zinc-300 bg-white px-2 py-1.5 text-sm focus:border-zinc-900 focus:outline-none"
|
|
>
|
|
<option value="">Tous paiements</option>
|
|
<option value={PaymentStatus.PENDING}>En attente</option>
|
|
<option value={PaymentStatus.AUTHORIZED}>Autorisé</option>
|
|
<option value={PaymentStatus.SUCCEEDED}>Payé</option>
|
|
<option value={PaymentStatus.FAILED}>Échec</option>
|
|
<option value={PaymentStatus.REFUNDED}>Remboursé</option>
|
|
</select>
|
|
<label className="flex items-center gap-1 text-xs text-zinc-500">
|
|
Du
|
|
<input
|
|
type="date"
|
|
name="from"
|
|
defaultValue={sp.from ?? ""}
|
|
className="rounded-md border border-zinc-300 px-2 py-1 text-sm"
|
|
/>
|
|
</label>
|
|
<label className="flex items-center gap-1 text-xs text-zinc-500">
|
|
au
|
|
<input
|
|
type="date"
|
|
name="to"
|
|
defaultValue={sp.to ?? ""}
|
|
className="rounded-md border border-zinc-300 px-2 py-1 text-sm"
|
|
/>
|
|
</label>
|
|
<button type="submit" className="rounded-md bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-zinc-800">
|
|
Filtrer
|
|
</button>
|
|
{(filters.q || filters.status || filters.paymentStatus || filters.from || filters.to) ? (
|
|
<Link href="/admin/bookings" className="text-sm text-zinc-500 hover:text-zinc-900">
|
|
Réinit.
|
|
</Link>
|
|
) : null}
|
|
</form>
|
|
|
|
<div className="overflow-hidden rounded-lg border border-zinc-200 bg-white">
|
|
<table className="w-full text-sm">
|
|
<thead className="border-b border-zinc-200 bg-zinc-50 text-xs uppercase tracking-wider text-zinc-500">
|
|
<tr>
|
|
<th className="px-4 py-2 text-left font-semibold">ID</th>
|
|
<th className="px-4 py-2 text-left font-semibold">Carbet</th>
|
|
<th className="px-4 py-2 text-left font-semibold">Locataire</th>
|
|
<th className="px-4 py-2 text-left font-semibold">Séjour</th>
|
|
<th className="px-4 py-2 text-right font-semibold">Pers.</th>
|
|
<th className="px-4 py-2 text-right font-semibold">Montant</th>
|
|
<th className="px-4 py-2 text-left font-semibold">Statut</th>
|
|
<th className="px-4 py-2 text-left font-semibold">Paiement</th>
|
|
<th className="px-4 py-2 text-right font-semibold">Créé</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-zinc-100">
|
|
{bookings.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={9} className="px-4 py-8 text-center text-sm text-zinc-500">
|
|
Aucune réservation ne correspond aux filtres.
|
|
</td>
|
|
</tr>
|
|
) : null}
|
|
{bookings.map((b) => (
|
|
<tr key={b.id} className="hover:bg-zinc-50">
|
|
<td className="px-4 py-2">
|
|
<Link href={`/admin/bookings/${b.id}`} className="font-mono text-[11px] text-zinc-900 hover:underline">
|
|
{b.id.slice(0, 10)}…
|
|
</Link>
|
|
</td>
|
|
<td className="px-4 py-2">
|
|
<Link href={`/admin/carbets/${b.carbet.id}`} className="font-medium text-zinc-900 hover:underline">
|
|
{b.carbet.title}
|
|
</Link>
|
|
<div className="text-[11px] text-zinc-500">
|
|
<code>/{b.carbet.slug}</code>
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-2 text-zinc-700">
|
|
{b.tenant.firstName} {b.tenant.lastName}
|
|
<div className="text-[11px] text-zinc-500">{b.tenant.email}</div>
|
|
</td>
|
|
<td className="px-4 py-2 text-zinc-700">
|
|
{dateFmt.format(b.startDate)} → {dateFmt.format(b.endDate)}
|
|
</td>
|
|
<td className="px-4 py-2 text-right font-mono text-zinc-700">{b.guestCount}</td>
|
|
<td className="px-4 py-2 text-right font-mono text-zinc-900">
|
|
{Number(b.amount).toFixed(2)} {b.currency}
|
|
</td>
|
|
<td className="px-4 py-2"><StatusBadge status={b.status} /></td>
|
|
<td className="px-4 py-2"><StatusBadge status={b.paymentStatus} /></td>
|
|
<td className="px-4 py-2 text-right text-[11px] text-zinc-500">
|
|
{dateFmt.format(b.createdAt)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|