import React, { useCallback, useEffect, useState } from "react"; import { Plus, Trash2, Download, Settings, BarChart2, Eraser } from "lucide-react"; import { conciergeBotsApi, uploadMediaFile, whatsappTemplatesApi, type ConciergeBotRecord, type ConciergeBotReception, type ConciergeDripStats, type ConciergeReceptionDrip, type ConciergeReceptionInfo, } from "../../../services/api"; import type { WhatsAppTemplateRecord } from "../../../services/api/types"; import { useAppNav } from "../../contexts/AppNavContext"; import { WhatsAppCtaEditor, normalizeCtasForSave, type WhatsAppCtaItem } from "../whatsapp/WhatsAppCtaEditor"; import { FlatpickrDatetimeInput, toDatetimeLocalValue } from "../ui/FlatpickrDatetimeInput"; const IS: React.CSSProperties = { width: "100%", padding: "8px 12px", border: "1px solid var(--border)", borderRadius: "var(--radius-button)", background: "var(--input-background)", fontSize: "var(--text-sm)", color: "var(--card-foreground)", boxSizing: "border-box", }; const btnPrimary: React.CSSProperties = { display: "inline-flex", alignItems: "center", gap: 6, padding: "8px 14px", borderRadius: "var(--radius-button)", background: "var(--primary)", color: "var(--primary-foreground)", border: "none", cursor: "pointer", fontSize: "var(--text-sm)", }; const btnSecondary: React.CSSProperties = { ...btnPrimary, background: "var(--secondary)", color: "var(--card-foreground)", border: "1px solid var(--border)", }; const DEFAULT_WELCOME = "Hi {firstName}! Welcome to {eventName}."; const DEFAULT_BADGE = "We couldn't match your WhatsApp to a registration. Please send a clear photo of your badge QR code."; const DEFAULT_STAFF = "We still couldn't identify you. Please see the reception desk for help. You can also send another badge photo."; function emptyDrip(): ConciergeReceptionDrip { return { trigger: "after_first_inbound", delayMinutes: 15, sendAt: "", body: "", mediaType: "none", mediaUrl: "", contentKind: "custom", agendaCount: 3, templateId: "", ctas: [], }; } function receptionFromBot(bot: ConciergeBotRecord): ConciergeBotReception { const r = bot.reception || {}; return { enabled: Boolean(r.enabled), allowNonRegistered: Boolean(r.allowNonRegistered), prefillText: r.prefillText || "Hi", welcomeMessage: r.welcomeMessage || DEFAULT_WELCOME, welcomeCtas: Array.isArray(r.welcomeCtas) ? r.welcomeCtas : [], unmatchedBadgePrompt: r.unmatchedBadgePrompt || DEFAULT_BADGE, staffHandoffMessage: r.staffHandoffMessage || DEFAULT_STAFF, dripAudience: r.dripAudience === "all_inbound" ? "all_inbound" : "matched", drips: Array.isArray(r.drips) && r.drips.length ? r.drips.map((d) => ({ ...emptyDrip(), ...d })) : [], }; } export function WhatsAppReceptionTab({ bot, eventId, onSaved, }: { bot: ConciergeBotRecord; eventId: string; onSaved: () => Promise | void; }) { const { setActiveNav, setSettingsInitialSection } = useAppNav(); const [reception, setReception] = useState(() => receptionFromBot(bot)); const [info, setInfo] = useState(null); const [templates, setTemplates] = useState([]); const [msg, setMsg] = useState(null); const [saving, setSaving] = useState(false); const [qrWithLogo, setQrWithLogo] = useState(false); const [qrDownloading, setQrDownloading] = useState<"png" | "svg" | null>(null); const [statsDripId, setStatsDripId] = useState(null); const [statsLoading, setStatsLoading] = useState(false); const [statsError, setStatsError] = useState(null); const [stats, setStats] = useState(null); const [clearingSessions, setClearingSessions] = useState(false); useEffect(() => { setReception(receptionFromBot(bot)); }, [bot._id, bot.updatedAt]); const loadInfo = useCallback(async () => { const res = await conciergeBotsApi.getReception(bot._id, { logo: qrWithLogo }); if (!res.error && res.data) setInfo(res.data); }, [bot._id, qrWithLogo]); useEffect(() => { void loadInfo(); }, [loadInfo]); useEffect(() => { whatsappTemplatesApi.list({ status: "APPROVED" }).then((r) => { if (!r.error) setTemplates(r.data?.templates || []); }); }, []); const downloadQr = async (format: "png" | "svg") => { setQrDownloading(format); setMsg(null); try { await conciergeBotsApi.downloadReceptionQr(bot._id, format, { logo: qrWithLogo }); } catch (e) { setMsg(e instanceof Error ? e.message : "QR download failed"); } finally { setQrDownloading(null); } }; const patch = (partial: Partial) => setReception((prev) => ({ ...prev, ...partial })); const patchDrip = (index: number, partial: Partial) => { const drips = [...(reception.drips || [])]; drips[index] = { ...drips[index], ...partial }; patch({ drips }); }; const loadDripStats = async (dripId: string) => { if (!dripId) { setStatsError("Save reception first so this drip gets an id."); setStats(null); setStatsDripId(null); return; } if (statsDripId === dripId) { setStatsDripId(null); setStats(null); setStatsError(null); return; } setStatsDripId(dripId); setStatsLoading(true); setStatsError(null); setStats(null); try { const res = await conciergeBotsApi.getDripStats(bot._id, dripId); if (res.error) { setStatsError(res.error); setStats(null); } else { setStats(res.data?.stats ?? null); } } catch { setStatsError("Failed to load drip stats."); setStats(null); } finally { setStatsLoading(false); } }; const save = async () => { setSaving(true); setMsg(null); let dripsPayload: ConciergeReceptionDrip[]; let welcomeCtas: WhatsAppCtaItem[]; try { welcomeCtas = normalizeCtasForSave((reception.welcomeCtas || []) as WhatsAppCtaItem[]); dripsPayload = (reception.drips || []).map((d) => { const ctas = d.contentKind === "upcoming_agenda" ? [] : normalizeCtasForSave((d.ctas || []) as WhatsAppCtaItem[]); if (ctas.length && !String(d.body || "").trim()) { throw new Error("Drip message text is required when buttons are set."); } return { _id: d._id, trigger: d.trigger, delayMinutes: d.delayMinutes, body: d.body, mediaType: d.contentKind === "upcoming_agenda" ? "none" : d.mediaType, mediaUrl: d.contentKind === "upcoming_agenda" ? "" : d.mediaUrl, contentKind: d.contentKind || "custom", agendaCount: d.agendaCount, templateId: d.templateId || null, sendAt: d.sendAt || null, ctas, }; }); } catch (e) { setSaving(false); setMsg(e instanceof Error ? e.message : "Invalid buttons"); return; } const res = await conciergeBotsApi.update(bot._id, { reception: { ...reception, welcomeCtas, drips: dripsPayload, }, }); setSaving(false); if (res.error) { setMsg(res.error); return; } setMsg("Reception settings saved"); await onSaved(); await loadInfo(); }; const uploadDripMedia = async (index: number, file: File) => { const up = await uploadMediaFile(file, { eventId: eventId || "portfolio", module: "whatsapp/reception-drip" }); if (up.error || !up.url) { setMsg(up.error || "Upload failed"); return; } const mime = String(file.type || "").toLowerCase(); const mediaType = mime.startsWith("video/") ? "video" : "image"; patchDrip(index, { mediaUrl: up.url, mediaType }); }; const goSettings = () => { setSettingsInitialSection("whatsapp"); setActiveNav("settings"); }; const clearReceptionList = async () => { const count = info?.sessionCount ?? 0; const ok = window.confirm( count ? `Clear ${count} reception WhatsApp visitor${count === 1 ? "" : "s"} for this chatbot? The next message from those numbers will send the welcome and drip sequence again.` : "Clear the reception WhatsApp list for this chatbot? The next message from any previous number will send the welcome and drip sequence again.", ); if (!ok) return; setClearingSessions(true); setMsg(null); try { const res = await conciergeBotsApi.clearReceptionSessions(bot._id); if (res.error) { setMsg(res.error); return; } const n = res.data?.sessionsCleared ?? 0; setMsg( n ? `Cleared ${n} reception visitor${n === 1 ? "" : "s"}. Next inbound from those numbers is treated as new.` : "Reception list was already empty.", ); await loadInfo(); } finally { setClearingSessions(false); } }; const outside24h = (d: ConciergeReceptionDrip) => { if (d.trigger === "absolute") return true; return Number(d.delayMinutes || 0) > 24 * 60; }; return (

Desk / reception

Removes all WhatsApp visitors for this desk so the next message from an existing number is treated as new (welcome + drips).

{!info?.canSendMessages && (

WhatsApp Cloud is not ready to send. Configure WABA ID, token, and phone number ID in Organisation settings.

)} {info?.resolveError && info.canSendMessages && (

{info.resolveError}

)} {info?.displayPhone && (

Desk number (from organisation settings): {info.displayPhone}

)} {info?.qrPngBase64 && (
Reception WhatsApp QR

Logo uses high error correction so the code still scans. Prefer Meta’s brand guidelines for posters.

Open wa.me link
)}

Visitors scan the QR and send this text (default Hi). WhatsApp Cloud credentials stay in Organisation settings.