Initial commit: spreewaldzeit + Dockerfile for Coolify (Next.js + Prisma/SQLite)

This commit is contained in:
2026-06-03 14:08:48 +02:00
committed by Ihor_Zhekov
commit bf5d79a919
94 changed files with 12480 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
const nav = [
{ href: "/admin/anfragen", label: "Anfragen" },
{ href: "/admin/kalender", label: "Kalender" },
{ href: "/admin/wohnungen", label: "Wohnungen" },
];
export function AdminNav({ email }: { email: string }) {
const pathname = usePathname();
const router = useRouter();
async function logout() {
await fetch("/api/admin/logout", { method: "POST" });
router.push("/admin/login");
router.refresh();
}
return (
<header className="bg-ink text-parchment">
<div className="container flex flex-col md:flex-row md:items-center justify-between gap-4 py-5">
<div className="flex items-center gap-8">
<Link href="/admin/anfragen" className="font-display text-xl tracking-tight">
Spreewaldzeit{" "}
<span className="text-parchment/50 text-sm ml-1">· Admin</span>
</Link>
<nav className="flex gap-1 md:gap-2">
{nav.map((item) => {
const active = pathname?.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={cn(
"px-3 py-1.5 rounded-full text-sm transition",
active
? "bg-parchment text-ink"
: "text-parchment/70 hover:text-parchment hover:bg-parchment/10"
)}
>
{item.label}
</Link>
);
})}
</nav>
</div>
<div className="flex items-center gap-4 text-sm">
<Link href="/" target="_blank" className="text-parchment/60 hover:text-parchment">
Website ansehen
</Link>
<span className="hidden md:inline text-parchment/40">·</span>
<span className="hidden md:inline text-parchment/70">{email}</span>
<button
onClick={logout}
className="px-3 py-1.5 rounded-full bg-parchment/10 hover:bg-parchment/20 text-sm transition"
>
Abmelden
</button>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,321 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { Input, Label, Textarea, FieldError } from "@/components/ui/Input";
import { Button } from "@/components/ui/Button";
import { formatPrice } from "@/lib/utils";
export interface EditorApartment {
id: string;
slug: string;
name: string;
tagline: string;
shortDescription: string;
description: string;
priceFrom: number; // Cent
maxGuests: number;
bedrooms: number;
sizeSqm: number;
features: string[];
images: string[];
airbnbUrl: string | null;
bookingUrl: string | null;
published: boolean;
}
export function ApartmentEditor({ apartment }: { apartment: EditorApartment }) {
const router = useRouter();
const [form, setForm] = useState(apartment);
const [priceEuro, setPriceEuro] = useState(String(Math.round(apartment.priceFrom / 100)));
const [airbnbUrl, setAirbnbUrl] = useState(apartment.airbnbUrl ?? "");
const [bookingUrl, setBookingUrl] = useState(apartment.bookingUrl ?? "");
const [featureInput, setFeatureInput] = useState("");
const [imageInput, setImageInput] = useState("");
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
function update<K extends keyof EditorApartment>(key: K, value: EditorApartment[K]) {
setForm((f) => ({ ...f, [key]: value }));
}
function addFeature() {
const v = featureInput.trim();
if (!v) return;
update("features", [...form.features, v]);
setFeatureInput("");
}
function removeFeature(i: number) {
update("features", form.features.filter((_, idx) => idx !== i));
}
function addImage() {
const v = imageInput.trim();
if (!v) return;
try {
new URL(v);
} catch {
setMessage({ kind: "err", text: "Bild-URL ist ungültig." });
return;
}
update("images", [...form.images, v]);
setImageInput("");
setMessage(null);
}
function removeImage(i: number) {
update("images", form.images.filter((_, idx) => idx !== i));
}
async function save() {
setSaving(true);
setMessage(null);
try {
const res = await fetch(`/api/admin/apartments/${form.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: form.name,
tagline: form.tagline,
shortDescription: form.shortDescription,
description: form.description,
priceFrom: Math.max(0, Math.round(Number(priceEuro) * 100)),
maxGuests: Number(form.maxGuests),
bedrooms: Number(form.bedrooms),
sizeSqm: Number(form.sizeSqm),
features: form.features,
images: form.images,
airbnbUrl: airbnbUrl.trim() || "",
bookingUrl: bookingUrl.trim() || "",
published: form.published,
}),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error ?? "Speichern fehlgeschlagen.");
}
setMessage({ kind: "ok", text: "Gespeichert." });
router.refresh();
} catch (err) {
setMessage({
kind: "err",
text: err instanceof Error ? err.message : "Unbekannter Fehler.",
});
} finally {
setSaving(false);
}
}
return (
<div className="bg-cream border border-ink/10 rounded-sm p-6 md:p-8">
<div className="flex items-start justify-between gap-4 mb-8">
<div>
<div className="eyebrow mb-1">/{form.slug}</div>
<h2 className="font-display text-3xl leading-tight">{form.name}</h2>
<div className="mt-2 text-sm text-ink/60">
ab {formatPrice(form.priceFrom)} · bis {form.maxGuests} Gäste · {form.sizeSqm} m²
</div>
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer shrink-0">
<input
type="checkbox"
checked={form.published}
onChange={(e) => update("published", e.target.checked)}
className="accent-moss-600 h-4 w-4"
/>
Veröffentlicht
</label>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<Label>Name</Label>
<Input value={form.name} onChange={(e) => update("name", e.target.value)} />
</div>
<div>
<Label>Kurztitel / Tagline</Label>
<Input value={form.tagline} onChange={(e) => update("tagline", e.target.value)} />
</div>
<div className="md:col-span-2">
<Label>Kurzbeschreibung</Label>
<Textarea
rows={2}
value={form.shortDescription}
onChange={(e) => update("shortDescription", e.target.value)}
/>
</div>
<div className="md:col-span-2">
<Label>Beschreibung</Label>
<Textarea
rows={6}
value={form.description}
onChange={(e) => update("description", e.target.value)}
/>
</div>
<div>
<Label>Preis ab (/Nacht)</Label>
<Input
type="number"
min={0}
value={priceEuro}
onChange={(e) => setPriceEuro(e.target.value)}
/>
</div>
<div>
<Label>Max. Gäste</Label>
<Input
type="number"
min={1}
value={form.maxGuests}
onChange={(e) => update("maxGuests", Number(e.target.value))}
/>
</div>
<div>
<Label>Schlafzimmer</Label>
<Input
type="number"
min={0}
value={form.bedrooms}
onChange={(e) => update("bedrooms", Number(e.target.value))}
/>
</div>
<div>
<Label>Größe (m²)</Label>
<Input
type="number"
min={1}
value={form.sizeSqm}
onChange={(e) => update("sizeSqm", Number(e.target.value))}
/>
</div>
</div>
{/* Features */}
<div className="mt-10">
<Label>Ausstattung</Label>
<div className="flex flex-wrap gap-2 mt-3 mb-3">
{form.features.map((f, i) => (
<span
key={`${f}-${i}`}
className="inline-flex items-center gap-2 bg-parchment border border-ink/15 px-3 py-1 rounded-full text-sm"
>
{f}
<button
onClick={() => removeFeature(i)}
aria-label="Entfernen"
className="text-ink/40 hover:text-red-700 text-lg leading-none"
>
×
</button>
</span>
))}
</div>
<div className="flex gap-2">
<Input
placeholder="z. B. Kachelofen"
value={featureInput}
onChange={(e) => setFeatureInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
addFeature();
}
}}
/>
<Button type="button" variant="outline" onClick={addFeature}>
Hinzufügen
</Button>
</div>
</div>
{/* Bilder */}
<div className="mt-10">
<Label>Bilder (URLs)</Label>
{form.images.length > 0 && (
<ul className="grid grid-cols-3 md:grid-cols-5 gap-3 mt-3 mb-4">
{form.images.map((src, i) => (
<li key={`${src}-${i}`} className="relative aspect-square rounded-sm overflow-hidden bg-ink/5 group">
<Image
src={src}
alt=""
fill
sizes="200px"
className="object-cover"
/>
<button
onClick={() => removeImage(i)}
className="absolute top-1 right-1 bg-ink/80 text-parchment text-xs w-6 h-6 rounded-full opacity-0 group-hover:opacity-100 transition"
aria-label="Bild entfernen"
>
×
</button>
</li>
))}
</ul>
)}
<div className="flex gap-2">
<Input
placeholder="https://…"
value={imageInput}
onChange={(e) => setImageInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
addImage();
}
}}
/>
<Button type="button" variant="outline" onClick={addImage}>
Hinzufügen
</Button>
</div>
<p className="mt-2 text-xs text-ink/50">
Für den MVP werden Bilder als URLs gepflegt. Später per Upload über S3/Cloudinary erweiterbar.
</p>
</div>
{/* Buchungsplattformen */}
<div className="mt-10">
<Label>Buchungsplattformen</Label>
<p className="mt-1 mb-4 text-xs text-ink/50">
Wenn Airbnb- oder Booking.com-Links gesetzt sind, werden sie auf der Wohnungsseite als externe Buchungsoption angezeigt.
</p>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label>Airbnb-Link</Label>
<Input
placeholder="https://www.airbnb.de/rooms/…"
value={airbnbUrl}
onChange={(e) => setAirbnbUrl(e.target.value)}
/>
</div>
<div>
<Label>Booking.com-Link</Label>
<Input
placeholder="https://www.booking.com/hotel/…"
value={bookingUrl}
onChange={(e) => setBookingUrl(e.target.value)}
/>
</div>
</div>
</div>
{/* Aktionen */}
<div className="mt-10 pt-6 border-t border-ink/10 flex items-center justify-between gap-4 flex-wrap">
<div className="text-sm">
{message && (
<span className={message.kind === "ok" ? "text-moss-700" : "text-red-700"}>
{message.text}
</span>
)}
</div>
<Button onClick={save} disabled={saving} size="lg">
{saving ? "Speichert…" : "Änderungen speichern"}
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,288 @@
"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>
);
}

View File

@@ -0,0 +1,159 @@
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { formatDate, nightsBetween } from "@/lib/utils";
import type { InquiryStatus } from "@/types";
import { INQUIRY_STATUS_LABELS } from "@/types";
export interface InquiryRowData {
id: string;
apartmentName: string;
arrival: string;
departure: string;
guests: number;
name: string;
email: string;
phone: string | null;
message: string;
status: InquiryStatus;
createdAt: string;
}
const statusClasses: Record<InquiryStatus, string> = {
new: "bg-moss-500 text-parchment",
read: "bg-ink/10 text-ink",
confirmed: "bg-moss-700 text-parchment",
declined: "bg-red-100 text-red-800",
archived: "bg-ink/5 text-ink/60",
};
export function InquiryRow({ inquiry }: { inquiry: InquiryRowData }) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [status, setStatus] = useState<InquiryStatus>(inquiry.status);
const [pending, startTransition] = useTransition();
const [saving, setSaving] = useState(false);
async function updateStatus(next: InquiryStatus) {
setSaving(true);
try {
const res = await fetch(`/api/admin/inquiries/${inquiry.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: next }),
});
if (!res.ok) throw new Error();
setStatus(next);
startTransition(() => router.refresh());
} catch {
alert("Konnte Status nicht speichern.");
} finally {
setSaving(false);
}
}
async function remove() {
if (!confirm("Anfrage wirklich löschen? Das kann nicht rückgängig gemacht werden.")) return;
const res = await fetch(`/api/admin/inquiries/${inquiry.id}`, { method: "DELETE" });
if (res.ok) router.refresh();
}
const nights = nightsBetween(inquiry.arrival, inquiry.departure);
return (
<li className="border border-ink/10 rounded-sm bg-cream overflow-hidden">
<button
onClick={() => setOpen((v) => !v)}
className="w-full grid md:grid-cols-[auto_1fr_auto_auto_auto] items-center gap-4 px-5 py-4 text-left hover:bg-ink/[0.02] transition"
>
<span
className={`text-[10px] uppercase tracking-wider px-2 py-1 rounded-full font-medium ${statusClasses[status]}`}
>
{INQUIRY_STATUS_LABELS[status]}
</span>
<div className="min-w-0">
<div className="font-medium truncate">{inquiry.name}</div>
<div className="text-xs text-ink/60 truncate">
{inquiry.apartmentName} · {inquiry.guests} Gäste · {nights} Nächte
</div>
</div>
<div className="hidden md:block text-sm text-ink/70">
{formatDate(inquiry.arrival)} {formatDate(inquiry.departure)}
</div>
<div className="hidden md:block text-xs text-ink/50 tabular-nums">
{formatDate(inquiry.createdAt)}
</div>
<span className="text-ink/40" aria-hidden>{open ? "▴" : "▾"}</span>
</button>
{open && (
<div className="border-t border-ink/10 px-5 py-5 bg-parchment/50 grid md:grid-cols-2 gap-6">
<dl className="space-y-3 text-sm">
<div>
<dt className="eyebrow mb-0.5">E-Mail</dt>
<dd>
<a href={`mailto:${inquiry.email}`} className="link-underline">
{inquiry.email}
</a>
</dd>
</div>
{inquiry.phone && (
<div>
<dt className="eyebrow mb-0.5">Telefon</dt>
<dd>{inquiry.phone}</dd>
</div>
)}
<div>
<dt className="eyebrow mb-0.5">Zeitraum</dt>
<dd>
{formatDate(inquiry.arrival)} {formatDate(inquiry.departure)} ({nights} N)
</dd>
</div>
<div>
<dt className="eyebrow mb-0.5">Eingegangen</dt>
<dd>{formatDate(inquiry.createdAt)}</dd>
</div>
</dl>
<div>
<div className="eyebrow mb-2">Nachricht</div>
<p className="whitespace-pre-line text-sm text-ink/85 leading-relaxed">
{inquiry.message || <span className="text-ink/40">(keine Nachricht)</span>}
</p>
</div>
<div className="md:col-span-2 flex flex-wrap items-center justify-between gap-4 pt-4 border-t border-ink/10">
<div className="flex items-center gap-3">
<label className="eyebrow">Status</label>
<select
value={status}
disabled={saving || pending}
onChange={(e) => updateStatus(e.target.value as InquiryStatus)}
className="bg-transparent border-b border-ink/25 focus:border-ink py-1.5 text-sm focus:outline-none"
>
{(Object.keys(INQUIRY_STATUS_LABELS) as InquiryStatus[]).map((s) => (
<option key={s} value={s}>{INQUIRY_STATUS_LABELS[s]}</option>
))}
</select>
</div>
<div className="flex items-center gap-3">
<a
href={`mailto:${inquiry.email}?subject=${encodeURIComponent("Ihre Anfrage — " + inquiry.apartmentName)}`}
className="px-4 py-2 text-sm rounded-full border border-ink/20 hover:bg-ink/5"
>
Antworten
</a>
<button
onClick={remove}
className="px-4 py-2 text-sm rounded-full text-red-700 hover:bg-red-50"
>
Löschen
</button>
</div>
</div>
</div>
)}
</li>
);
}