Files
Calbook/components/admin/instant-meeting-panel.tsx

244 lines
11 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import { CheckCircle2, Copy, Megaphone, Plus, Send, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "sonner";
type InstantMeetingPerson = {
id: string;
name: string;
emailRecipients: string[];
calendarIds: string[];
};
type CacheEntry = { email: string; name: string; lastUsedAt: string };
type BootstrapResponse = {
people: InstantMeetingPerson[];
emailCache: CacheEntry[];
defaultSubject: string;
defaultTemplate: string;
};
type MeetingResult = {
sentCount: number;
scopeLabel: string;
meetingUrl: string;
};
export function InstantMeetingPanel() {
const [loading, setLoading] = useState(true);
const [sending, setSending] = useState(false);
const [people, setPeople] = useState<InstantMeetingPerson[]>([]);
const [emailCache, setEmailCache] = useState<CacheEntry[]>([]);
const [personScopeId, setPersonScopeId] = useState("all");
const [subjectOverride, setSubjectOverride] = useState("");
const [customMessage, setCustomMessage] = useState("");
const [cacheInput, setCacheInput] = useState("");
const [additionalRecipients, setAdditionalRecipients] = useState<Array<{ email: string; name: string }>>([]);
const [result, setResult] = useState<MeetingResult | null>(null);
const allRecipients = useMemo(() => additionalRecipients, [additionalRecipients]);
function addAdditionalEmail(raw: string, nameOverride?: string) {
const normalized = (raw ?? "").trim();
if (!normalized || !normalized.includes("@")) {
toast.error("Bitte eine gültige E-Mail-Adresse eingeben.");
return;
}
if (allRecipients.some((r) => r.email.toLowerCase() === normalized.toLowerCase())) {
toast.error("Adresse bereits ausgewählt.");
return;
}
const name = nameOverride?.trim() || normalized.split("@")[0] || "";
setAdditionalRecipients((prev) => [...prev, { email: normalized, name }]);
setCacheInput("");
}
function removeAdditionalEmail(email: string) {
setAdditionalRecipients((prev) => prev.filter((r) => r.email !== email));
}
const bootstrap = async () => {
setLoading(true);
try {
const res = await fetch("/api/admin/instant-meeting", { cache: "no-store" });
const data = (await res.json()) as BootstrapResponse & { message?: string };
if (!res.ok) { toast.error(data?.message ?? "Konnte Daten nicht laden."); return; }
setPeople(data.people ?? []);
setEmailCache(data.emailCache ?? []);
setSubjectOverride(data.defaultSubject ?? "");
} catch { toast.error("Konnte Daten nicht laden."); }
finally { setLoading(false); }
};
useEffect(() => { void bootstrap(); }, []);
const onSendMeeting = async () => {
setSending(true);
try {
const res = await fetch("/api/admin/instant-meeting", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
personScopeId,
subjectOverride: subjectOverride.trim() || undefined,
customMessage: customMessage.trim() || undefined,
additionalRecipients: additionalRecipients.map((r) => r.email)
})
});
const data = await res.json();
if (!res.ok) { toast.error(data?.message ?? "Versand fehlgeschlagen."); return; }
setResult({
sentCount: data.sentCount ?? 0,
scopeLabel: data.scopeLabel ?? "",
meetingUrl: data.meetingUrl ?? ""
});
setAdditionalRecipients([]);
toast.success("Instant Meeting gesendet.");
} catch { toast.error("Versand fehlgeschlagen."); }
finally { setSending(false); }
};
if (loading) return null;
const quickCache = emailCache.slice(0, 12);
return (
<div className="max-w-6xl mx-auto space-y-6">
<div>
<h1 className="text-3xl font-black tracking-tight text-slate-950">Instant Meeting</h1>
<p className="mt-1 text-sm font-medium text-slate-500">Spontanen Meeting-Link per E-Mail senden</p>
</div>
{/* Config section */}
<div className="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<div className="border-b border-slate-100 px-5 py-3 flex items-center gap-2">
<Megaphone className="h-4 w-4 text-slate-400" />
<h2 className="text-sm font-black text-slate-900">Konfiguration</h2>
</div>
<div className="p-5 space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="im-person">Person</Label>
<select
id="im-person"
value={personScopeId}
onChange={(e) => setPersonScopeId(e.target.value)}
className="h-11 w-full rounded-xl border border-slate-200 bg-slate-50 px-4 text-sm font-medium text-slate-900 transition-all focus:border-indigo-600 focus:outline-none focus:ring-1 focus:ring-indigo-600"
>
<option value="all">Alle / Beide Personen</option>
{people.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
<div className="space-y-1.5">
<Label htmlFor="im-subject">Mail-Betreff</Label>
<Input id="im-subject" value={subjectOverride} onChange={(e) => setSubjectOverride(e.target.value)} placeholder="Sofort-Meeting" />
<p className="text-xs text-slate-400">{"Platzhalter: {{companyName}}, {{recipientName}}, {{meetingUrl}}"}</p>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="im-message">Zusatznachricht (optional)</Label>
<Textarea id="im-message" rows={3} value={customMessage} onChange={(e) => setCustomMessage(e.target.value)} placeholder="Optionaler Zusatztext..." />
</div>
</div>
</div>
{/* Recipients section */}
<div className="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<div className="border-b border-slate-100 px-5 py-3 flex items-center gap-2">
<Megaphone className="h-4 w-4 text-slate-400" />
<h2 className="text-sm font-black text-slate-900">Empfänger</h2>
</div>
<div className="p-5 space-y-4">
<div className="space-y-1.5">
<Label htmlFor="im-email">Zusätzliche Empfänger</Label>
<div className="flex gap-2">
<Input id="im-email" list="im-cache" value={cacheInput} onChange={(e) => setCacheInput(e.target.value)} placeholder="name@example.com" />
<Button type="button" variant="secondary" onClick={() => addAdditionalEmail(cacheInput)}>
<Plus className="mr-1.5 h-4 w-4" /> Hinzufügen
</Button>
</div>
<datalist id="im-cache">
{emailCache.map((e) => <option key={e.email} value={e.email}>{e.name || e.email}</option>)}
</datalist>
</div>
{quickCache.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs font-bold uppercase tracking-wider text-slate-400">Letzte Kontakte</p>
<div className="flex flex-wrap gap-1.5">
{quickCache.map((entry) => (
<button
key={entry.email}
type="button"
onClick={() => addAdditionalEmail(entry.email, entry.name)}
className="rounded-full border border-slate-200 bg-white px-3 py-1 text-xs text-slate-600 hover:border-indigo-400 hover:text-indigo-600 transition"
>
{entry.name ? `${entry.name} · ${entry.email}` : entry.email}
</button>
))}
</div>
</div>
)}
{additionalRecipients.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs font-bold uppercase tracking-wider text-slate-400">Ausgewählt</p>
<div className="flex flex-wrap gap-1.5">
{additionalRecipients.map((entry) => (
<span key={entry.email} className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-slate-100 pl-3 pr-1.5 py-1 text-xs font-bold text-slate-600">
{entry.name ? `${entry.name} · ${entry.email}` : entry.email}
<button type="button" onClick={() => removeAdditionalEmail(entry.email)} className="rounded-full p-0.5 hover:bg-red-100 hover:text-red-600">
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
</div>
)}
<div className="rounded-xl border border-slate-200 bg-slate-50 p-3 text-sm flex items-center justify-between">
<p><span className="font-black text-slate-900">{allRecipients.length}</span> <span className="text-slate-500">Empfänger gesamt</span></p>
</div>
<Button type="button" onClick={() => { void onSendMeeting(); }} disabled={sending || allRecipients.length === 0} className="w-full md:w-auto">
<Send className="mr-2 h-4 w-4" />
{sending ? "Wird versendet..." : "Instant Meeting jetzt senden"}
</Button>
</div>
</div>
{/* Result */}
{result && (
<div className="rounded-2xl border border-emerald-200 bg-emerald-50 shadow-sm overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<div className="px-5 py-4 space-y-3">
<div className="flex items-center gap-2 text-emerald-800">
<CheckCircle2 className="h-5 w-5" />
<p className="font-bold">Versendet an {result.sentCount} Empfänger ({result.scopeLabel})</p>
</div>
<div className="rounded-xl border border-emerald-200 bg-white p-3">
<p className="text-xs text-slate-500 mb-1">Meeting-Link</p>
<p className="text-sm font-medium text-slate-900 break-all">{result.meetingUrl}</p>
</div>
<div className="flex gap-2">
<Button type="button" variant="outline" onClick={async () => { await navigator.clipboard.writeText(result.meetingUrl); toast.success("Meeting-Link kopiert."); }}>
<Copy className="mr-2 h-4 w-4" /> Link kopieren
</Button>
<Button type="button" onClick={() => window.open(result.meetingUrl, "_blank", "noopener,noreferrer")}>
Meeting öffnen
</Button>
</div>
</div>
</div>
)}
</div>
);
}