feat(p1): calendrier dispo + emails Resend + amount calculé + best-effort welcome/confirmation/refund
This commit is contained in:
parent
4e14854245
commit
b59b8a0af2
7 changed files with 585 additions and 6 deletions
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
|
@ -43,6 +43,26 @@ export function BookingForm({
|
|||
const [guestCount, setGuestCount] = useState(Math.min(2, capacity));
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [blockedDates, setBlockedDates] = useState<Set<string>>(new Set());
|
||||
|
||||
// Fetch availability sur les 90 prochains jours pour griser/avertir.
|
||||
useEffect(() => {
|
||||
const ctrl = new AbortController();
|
||||
const from = todayPlus(0);
|
||||
const to = todayPlus(90);
|
||||
fetch(`/api/carbets/${carbetId}/availability?from=${from}&to=${to}`, { signal: ctrl.signal })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((j) => {
|
||||
if (!j?.calendar) return;
|
||||
const blocked = new Set<string>();
|
||||
for (const d of j.calendar as { date: string; isAvailable: boolean }[]) {
|
||||
if (!d.isAvailable) blocked.add(d.date);
|
||||
}
|
||||
setBlockedDates(blocked);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => ctrl.abort();
|
||||
}, [carbetId]);
|
||||
|
||||
const nights = useMemo(() => Math.max(0, diffDays(startDate, endDate)), [startDate, endDate]);
|
||||
const total = nights * nightlyPrice;
|
||||
|
|
@ -50,7 +70,28 @@ export function BookingForm({
|
|||
const maxN = maxStayNights ?? 365;
|
||||
const nightsOk = nights >= minN && nights <= maxN;
|
||||
const guestOk = guestCount >= 1 && guestCount <= capacity;
|
||||
const canSubmit = nightsOk && guestOk && !busy;
|
||||
|
||||
// Vérifie qu'aucun jour de la plage sélectionnée n'est bloqué.
|
||||
const conflictDates = useMemo(() => {
|
||||
if (blockedDates.size === 0 || nights === 0) return [];
|
||||
const out: string[] = [];
|
||||
const startMs = new Date(startDate + "T00:00:00Z").getTime();
|
||||
for (let i = 0; i < nights; i++) {
|
||||
const d = new Date(startMs + i * 86400000).toISOString().slice(0, 10);
|
||||
if (blockedDates.has(d)) out.push(d);
|
||||
}
|
||||
return out;
|
||||
}, [blockedDates, startDate, nights]);
|
||||
const hasConflict = conflictDates.length > 0;
|
||||
|
||||
const canSubmit = nightsOk && guestOk && !busy && !hasConflict;
|
||||
|
||||
// Prochaines dates bloquées (max 6) pour affichage informatif.
|
||||
const upcomingBlocked = useMemo(() => {
|
||||
return Array.from(blockedDates)
|
||||
.sort()
|
||||
.slice(0, 6);
|
||||
}, [blockedDates]);
|
||||
|
||||
async function submit() {
|
||||
if (!isAuthenticated) {
|
||||
|
|
@ -142,6 +183,31 @@ export function BookingForm({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{hasConflict ? (
|
||||
<div className="rounded border border-rose-200 bg-rose-50 px-3 py-1.5 text-xs text-rose-700">
|
||||
Cette plage chevauche {conflictDates.length} jour{conflictDates.length > 1 ? "s" : ""} déjà
|
||||
pris ou bloqué{conflictDates.length > 1 ? "s" : ""} (
|
||||
{conflictDates.slice(0, 3).join(", ")}
|
||||
{conflictDates.length > 3 ? "…" : ""}). Changez les dates.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{upcomingBlocked.length > 0 && !hasConflict ? (
|
||||
<details className="rounded border border-zinc-100 bg-zinc-50 px-3 py-1.5 text-xs text-zinc-600">
|
||||
<summary className="cursor-pointer">Voir les prochaines dates indisponibles</summary>
|
||||
<div className="mt-1.5 flex flex-wrap gap-1">
|
||||
{upcomingBlocked.map((d) => (
|
||||
<code key={d} className="rounded bg-white px-1.5 py-0.5 text-[10px] text-zinc-700">
|
||||
{d}
|
||||
</code>
|
||||
))}
|
||||
{blockedDates.size > upcomingBlocked.length ? (
|
||||
<span className="text-[10px] text-zinc-500">+ {blockedDates.size - upcomingBlocked.length} autres</span>
|
||||
) : null}
|
||||
</div>
|
||||
</details>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="rounded border border-rose-200 bg-rose-50 px-3 py-1.5 text-xs text-rose-700">{error}</div>
|
||||
) : null}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue