Files
anouma/components/admin/AdminConversationsView.tsx
T

95 lines
6.2 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
type Conversation = { id: string; subject: string; name: string; email: string; status: string; lastMessageAt?: string };
type Message = { id: string; senderType: "visitor" | "admin"; senderName?: string; body: string; createdAt: string };
const statusLabel: Record<string, string> = { open: "Offen", waiting: "Wartet auf Antwort", closed: "Erledigt" };
export function AdminConversationsView() {
const [conversations, setConversations] = useState<Conversation[]>([]);
const [selected, setSelected] = useState<Conversation | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [reply, setReply] = useState("");
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
async function loadConversations() {
const response = await fetch("/api/conversations?limit=100&sort=-lastMessageAt&depth=0", { credentials: "include" });
if (!response.ok) throw new Error("Anfragen konnten nicht geladen werden.");
const data = await response.json();
setConversations(data.docs ?? []);
}
async function loadMessages(conversation: Conversation) {
setSelected(conversation);
const response = await fetch(`/api/conversation-messages?limit=100&sort=createdAt&where[conversation][equals]=${encodeURIComponent(conversation.id)}`, { credentials: "include" });
const data = await response.json();
setMessages(data.docs ?? []);
}
useEffect(() => {
let cancelled = false;
fetch("/api/conversations?limit=100&sort=-lastMessageAt&depth=0", { credentials: "include" })
.then(async (response) => {
if (!response.ok) throw new Error("Anfragen konnten nicht geladen werden.");
return response.json();
})
.then((data) => { if (!cancelled) setConversations(data.docs ?? []); })
.catch((err: Error) => { if (!cancelled) setError(err.message); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, []);
async function sendReply() {
if (!selected || !reply.trim()) return;
setBusy(true);
setError("");
try {
const response = await fetch("/api/conversation-messages", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ conversation: selected.id, senderType: "admin", senderName: "Anouma", body: reply.trim() }),
});
if (!response.ok) throw new Error("Antwort konnte nicht gesendet werden.");
setReply("");
await loadMessages(selected);
await loadConversations();
} catch (err) { setError(err instanceof Error ? err.message : "Antwort konnte nicht gesendet werden."); }
finally { setBusy(false); }
}
return (
<main style={{ maxWidth: 1200, padding: "32px 24px" }}>
<h1 style={{ margin: 0, fontSize: 28 }}>Anfragen & Nachrichten</h1>
<p style={{ margin: "8px 0 24px", color: "#66505f" }}>Kontakt- und Terminanfragen zentral bearbeiten. Antworten werden automatisch per E-Mail verschickt.</p>
{error && <p style={{ color: "#a33", marginBottom: 16 }}>{error}</p>}
<div style={{ display: "grid", gridTemplateColumns: "minmax(260px, .8fr) minmax(0, 1.4fr)", gap: 20, minHeight: 560 }}>
<section style={{ border: "1px solid #e6dcd7", borderRadius: 16, overflow: "auto" }}>
{loading && <p style={{ padding: 20 }}>Anfragen werden geladen …</p>}
{!loading && conversations.length === 0 && <p style={{ padding: 20 }}>Noch keine Anfragen.</p>}
{conversations.map((conversation) => (
<button key={conversation.id} type="button" onClick={() => loadMessages(conversation)} style={{ display: "block", width: "100%", padding: 16, textAlign: "left", border: 0, borderBottom: "1px solid #eee5e1", background: selected?.id === conversation.id ? "#fbf6f1" : "white", cursor: "pointer" }}>
<strong style={{ display: "block", color: "#66505f" }}>{conversation.subject}</strong>
<span style={{ display: "block", marginTop: 4, fontSize: 13 }}>{conversation.name} · {conversation.email}</span>
<span style={{ display: "block", marginTop: 6, fontSize: 12, color: "#8b616d" }}>{statusLabel[conversation.status] ?? conversation.status}{conversation.lastMessageAt ? ` · ${new Date(conversation.lastMessageAt).toLocaleDateString("de-DE")}` : ""}</span>
</button>
))}
</section>
<section style={{ display: "flex", flexDirection: "column", border: "1px solid #e6dcd7", borderRadius: 16, padding: 20 }}>
{!selected ? <p style={{ color: "#66505f" }}>Wähle links eine Anfrage aus.</p> : <>
<div style={{ borderBottom: "1px solid #eee5e1", paddingBottom: 14 }}><h2 style={{ margin: 0, fontSize: 20 }}>{selected.subject}</h2><p style={{ margin: "6px 0 0", fontSize: 13 }}>{selected.name} · {selected.email}</p></div>
<div style={{ flex: 1, overflow: "auto", padding: "18px 0" }}>
{messages.map((message) => <div key={message.id} style={{ maxWidth: "82%", margin: "0 0 14px auto", padding: 14, borderRadius: 14, background: message.senderType === "admin" ? "#f2e2d6" : "#fbf6f1", marginLeft: message.senderType === "admin" ? "18%" : 0 }}><div style={{ fontSize: 12, color: "#8b616d", marginBottom: 6 }}>{message.senderType === "admin" ? "Anouma" : message.senderName || selected.name} · {new Date(message.createdAt).toLocaleString("de-DE")}</div><div style={{ whiteSpace: "pre-wrap", lineHeight: 1.55 }}>{message.body}</div></div>)}
</div>
<div style={{ display: "flex", gap: 10, alignItems: "flex-end" }}><textarea value={reply} onChange={(event) => setReply(event.target.value)} placeholder="Antwort schreiben …" rows={4} style={{ flex: 1, resize: "vertical", border: "1px solid #d8c9c3", borderRadius: 12, padding: 12, font: "inherit" }} /><button type="button" disabled={busy || !reply.trim()} onClick={sendReply} style={{ border: 0, borderRadius: 999, padding: "12px 18px", background: "#8b616d", color: "white", cursor: "pointer" }}>{busy ? "Sende …" : "Antwort senden"}</button></div>
</>}
</section>
</div>
</main>
);
}