karbe/src/app/admin/bookings/page.tsx

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>
);
}