95 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|