Files
spreewaldzeit/components/admin/CalendarManager.tsx

289 lines
10 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 { useState } from "react";
import { useRouter } from "next/navigation";
import { formatDate } from "@/lib/utils";
import { Input, Label, Textarea, FieldError } from "@/components/ui/Input";
import { Button } from "@/components/ui/Button";
interface ApartmentLite {
id: string;
slug: string;
name: string;
}
export interface BlockRow {
id: string;
apartmentId: string;
apartmentName: string;
startDate: string;
endDate: string;
reason: string;
source: string;
note: string | null;
}
export function CalendarManager({
apartments,
blocks,
}: {
apartments: ApartmentLite[];
blocks: BlockRow[];
}) {
const router = useRouter();
const [apartmentId, setApartmentId] = useState(apartments[0]?.id ?? "");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [note, setNote] = useState("");
const [reason, setReason] = useState<"manual" | "maintenance" | "booking">("manual");
const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
// iCal sync state
const [icalApartmentId, setIcalApartmentId] = useState(apartments[0]?.id ?? "");
const [icalUrl, setIcalUrl] = useState("");
const [icalSource, setIcalSource] = useState<"airbnb" | "booking">("airbnb");
const [icalSyncing, setIcalSyncing] = useState(false);
const [icalMessage, setIcalMessage] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
setError(null);
try {
const res = await fetch("/api/admin/blocks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ apartmentId, startDate, endDate, note, reason }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error ?? "Konnte Zeitraum nicht sperren.");
}
setStartDate("");
setEndDate("");
setNote("");
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Unbekannter Fehler.");
} finally {
setSaving(false);
}
}
async function removeBlock(id: string) {
if (!confirm("Zeitraum freigeben?")) return;
const res = await fetch(`/api/admin/blocks/${id}`, { method: "DELETE" });
if (res.ok) router.refresh();
}
async function syncIcal(e: React.FormEvent) {
e.preventDefault();
if (!icalUrl.trim()) return;
setIcalSyncing(true);
setIcalMessage(null);
try {
const res = await fetch("/api/admin/sync-ical", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ apartmentId: icalApartmentId, icalUrl: icalUrl.trim(), source: icalSource }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error ?? "Synchronisation fehlgeschlagen.");
setIcalMessage({
kind: "ok",
text: `${data.imported} Buchungen importiert, ${data.deleted} alte entfernt.`,
});
setIcalUrl("");
router.refresh();
} catch (err) {
setIcalMessage({ kind: "err", text: err instanceof Error ? err.message : "Fehler." });
} finally {
setIcalSyncing(false);
}
}
return (
<div className="grid md:grid-cols-12 gap-10">
{/* Formular */}
<form onSubmit={onSubmit} className="md:col-span-5 bg-cream border border-ink/10 rounded-sm p-6 md:p-8 space-y-6 h-fit">
<div>
<h2 className="font-display text-2xl">Zeitraum sperren</h2>
<p className="text-sm text-ink/60 mt-1">
Für externe Buchungen, eigene Nutzung oder Wartung.
</p>
</div>
<div>
<Label htmlFor="apartment">Wohnung</Label>
<select
id="apartment"
value={apartmentId}
onChange={(e) => setApartmentId(e.target.value)}
className="w-full bg-transparent border-b border-ink/25 focus:border-ink py-2.5 focus:outline-none"
>
{apartments.map((a) => (
<option key={a.id} value={a.id}>{a.name}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="start">Von</Label>
<Input id="start" type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} required />
</div>
<div>
<Label htmlFor="end">Bis</Label>
<Input id="end" type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} required />
</div>
</div>
<div>
<Label htmlFor="reason">Grund</Label>
<select
id="reason"
value={reason}
onChange={(e) => setReason(e.target.value as typeof reason)}
className="w-full bg-transparent border-b border-ink/25 focus:border-ink py-2.5 focus:outline-none"
>
<option value="manual">Manuell gesperrt</option>
<option value="booking">Externe Buchung</option>
<option value="maintenance">Wartung / Pflege</option>
</select>
</div>
<div>
<Label htmlFor="note">Notiz (optional)</Label>
<Textarea id="note" rows={3} value={note} onChange={(e) => setNote(e.target.value)} />
</div>
<FieldError message={error ?? undefined} />
<Button type="submit" size="lg" disabled={saving} className="w-full">
{saving ? "Wird gespeichert…" : "Zeitraum sperren"}
</Button>
</form>
{/* Liste */}
<div className="md:col-span-7">
<div className="flex items-center justify-between mb-4">
<h2 className="font-display text-2xl">Gesperrte Zeiträume</h2>
<span className="text-xs text-ink/50">{blocks.length} gesamt</span>
</div>
{blocks.length === 0 ? (
<div className="bg-cream border border-ink/10 rounded-sm p-10 text-center text-ink/55">
Keine gesperrten Zeiträume.
</div>
) : (
<ul className="space-y-2">
{blocks.map((b) => (
<li
key={b.id}
className="grid grid-cols-[1fr_auto] items-center gap-4 border border-ink/10 bg-cream rounded-sm px-5 py-3"
>
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium">{b.apartmentName}</span>
<span className="text-xs uppercase tracking-wider text-ink/50 bg-ink/5 px-2 py-0.5 rounded">
{b.reason === "booking"
? "Buchung"
: b.reason === "maintenance"
? "Wartung"
: "Manuell"}
</span>
{b.source !== "manual" && (
<span className="text-xs uppercase tracking-wider text-moss-700 bg-moss-50 px-2 py-0.5 rounded">
{b.source}
</span>
)}
</div>
<div className="text-sm text-ink/70 mt-1">
{formatDate(b.startDate)} {formatDate(b.endDate)}
{b.note && <span className="text-ink/50"> · {b.note}</span>}
</div>
</div>
<button
onClick={() => removeBlock(b.id)}
className="text-sm px-3 py-1.5 rounded-full hover:bg-red-50 text-red-700 shrink-0"
>
Freigeben
</button>
</li>
))}
</ul>
)}
</div>
{/* iCal Sync */}
<div className="md:col-span-12 mt-2">
<div className="bg-moss-50 border border-moss-200 rounded-sm p-6 md:p-8">
<h2 className="font-display text-2xl mb-1">iCal-Synchronisation</h2>
<p className="text-sm text-ink/60 mb-6">
Fügen Sie den iCal-Link Ihres Airbnb- oder Booking.com-Inserats ein. Alle bisherigen
Buchungen dieser Quelle werden ersetzt. Den Link finden Sie in Airbnb unter{" "}
<strong>Kalender Verfügbarkeit exportieren</strong> und in Booking.com unter{" "}
<strong>Kalender iCal-Export</strong>.
</p>
<form onSubmit={syncIcal} className="grid md:grid-cols-12 gap-4 items-end">
<div className="md:col-span-3">
<Label htmlFor="ical-apartment">Wohnung</Label>
<select
id="ical-apartment"
value={icalApartmentId}
onChange={(e) => setIcalApartmentId(e.target.value)}
className="w-full bg-transparent border-b border-ink/25 focus:border-ink py-2.5 focus:outline-none"
>
{apartments.map((a) => (
<option key={a.id} value={a.id}>{a.name}</option>
))}
</select>
</div>
<div className="md:col-span-2">
<Label htmlFor="ical-source">Plattform</Label>
<select
id="ical-source"
value={icalSource}
onChange={(e) => setIcalSource(e.target.value as "airbnb" | "booking")}
className="w-full bg-transparent border-b border-ink/25 focus:border-ink py-2.5 focus:outline-none"
>
<option value="airbnb">Airbnb</option>
<option value="booking">Booking.com</option>
</select>
</div>
<div className="md:col-span-5">
<Label htmlFor="ical-url">iCal-URL</Label>
<Input
id="ical-url"
type="url"
placeholder="https://www.airbnb.de/calendar/ical/…"
value={icalUrl}
onChange={(e) => setIcalUrl(e.target.value)}
required
/>
</div>
<div className="md:col-span-2">
<Button type="submit" disabled={icalSyncing} className="w-full">
{icalSyncing ? "Synchronisiert…" : "Jetzt synchronisieren"}
</Button>
</div>
</form>
{icalMessage && (
<p className={`mt-3 text-sm ${icalMessage.kind === "ok" ? "text-moss-700" : "text-red-700"}`}>
{icalMessage.text}
</p>
)}
</div>
</div>
</div>
);
}