SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
4 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
3.6 KB · 29 lines tsx
Raw Blame History
1"use client";2import Link from "next/link";3import { useRouter } from "next/navigation";4import { Empty, Notice } from "@/components/ui";5type V = { id: string; status: string; proposedSlots: string[]; confirmedSlot: string | null; note: string | null; ownerId: string; requesterId: string; inquiryId: string | null; title: string | null; slug: string; city: string | null; createdAt: string };6const fmt = (iso: string) => new Date(iso).toLocaleString("fr-CA", { timeZone: "America/Toronto", weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });7const LABEL: Record<string, string> = { REQUESTED: "Demandée", CONFIRMED: "Confirmée", CANCELLED: "Annulée", DONE: "Effectuée" };8export function ViewingsView({ me, items }: { me: string; items: V[] }) {9  const router = useRouter();10  const act = async (id: string, action: string, slot?: string) => { const r = await fetch(`/api/viewings/${id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ action, slot }) }); if (!r.ok) alert((await r.json()).error?.message ?? "Erreur"); router.refresh(); };11  return (12    <div>13      <h1 className="text-2xl md:text-3xl font-extrabold tracking-tight mb-1">Visites</h1><p className="text-sm text-ink-2 mb-4">Heure de l'Est (America/Toronto). Le propriétaire confirme un des créneaux proposés ; les conflits à moins de 30 minutes sont refusés.</p>14      {items.length === 0 ? <Empty title="Aucune demande de visite" hint="Demandez une visite depuis une fiche publiée sur Toveli, ou attendez les demandes sur vos annonces." /> : (15        <ul className="space-y-3">{items.map((v) => { const owner = v.ownerId === me; return (16          <li key={v.id} className="tv-card p-4">17            <div className="flex flex-wrap items-center gap-2"><span className={`tv-badge ${v.status === "CONFIRMED" ? "bg-good-soft text-good" : v.status === "REQUESTED" ? "bg-warn-soft text-warn" : "bg-surface-2 text-ink-3"}`}>{LABEL[v.status]}</span><span className="text-xs text-ink-3">{owner ? "Pour mon annonce" : "Ma demande"} · {new Date(v.createdAt).toLocaleDateString("fr-CA")}</span></div>18            <p className="font-semibold mt-1"><Link href={`/annonce/${encodeURIComponent("toveli:" + v.slug)}`} className="hover:underline">{v.title ?? "Annonce"}</Link> <span className="text-ink-3 font-normal text-sm">· {v.city}</span></p>19            {v.note && <p className="text-sm text-ink-2 mt-1">« {v.note} »</p>}20            {v.status === "CONFIRMED" && v.confirmedSlot && <p className="text-sm mt-2">📅 <strong>{fmt(v.confirmedSlot)}</strong></p>}21            {v.status === "REQUESTED" && <div className="mt-2 flex flex-wrap gap-2">{v.proposedSlots.map((s) => owner ? <button key={s} type="button" onClick={() => act(v.id, "confirm", s)} className="tv-chip">Confirmer {fmt(s)}</button> : <span key={s} className="tv-chip">{fmt(s)}</span>)}</div>}22            <div className="mt-2 flex gap-3 text-xs">{!["CANCELLED", "DONE"].includes(v.status) && <button type="button" onClick={() => confirm("Annuler cette visite ?") && act(v.id, "cancel")} className="underline text-ink-3">Annuler</button>}{owner && v.status === "CONFIRMED" && <button type="button" onClick={() => act(v.id, "done")} className="underline text-ink-3">Marquer effectuée</button>}{v.inquiryId && <Link href={`/membre/messages/${v.inquiryId}`} className="underline text-brand-dark">Conversation</Link>}</div>23          </li>); })}</ul>24      )}25      <div className="mt-4"><Notice>Par sécurité, l'adresse exacte d'une annonce « rue seulement » ou « secteur » n'est communiquée qu'après confirmation d'une visite, par le propriétaire, dans la conversation.</Notice></div>26    </div>27  );28}29