import React, { useCallback, useEffect, useState } from "react"; import { X, Download, ChevronLeft, ChevronRight, MailPlus } from "lucide-react"; import { formsApi } from "../../../services/api"; import { useAuth } from "../../contexts/AuthContext"; import type { Form, FormField, FormSubmissionRecord } from "../../../services/api"; import { buildFormSubmissionsExportFilename } from "../../utils/formExportFilename"; function dataToObj(data: unknown): Record { if (!data || typeof data !== "object") return {}; return data as Record; } function formatWhen(iso?: string): string { if (!iso) return "—"; try { return new Date(iso).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short", }); } catch { return "—"; } } function participantEmail( p: FormSubmissionRecord["participant"] ): string { if (!p || typeof p === "string") return "—"; return p.email?.trim() || "—"; } function participantName(p: FormSubmissionRecord["participant"]): string { if (!p || typeof p === "string") return "there"; return [p.firstName, p.lastName].filter(Boolean).join(" ").trim() || "there";} function eventNameFromForm(form: Form | null): string { const ev = form?.event; if (ev && typeof ev === "object" && ev.name?.trim()) return ev.name.trim(); return "Event"; } type Props = { formId: string; formName: string; onClose: () => void; }; const PAGE_SIZE = 20; /** Response field columns in the table; CSV export always includes every field (see backend). */ const SUBMISSIONS_TABLE_PREVIEW_FIELDS = 4; export function FormSubmissionsPanel({ formId, formName, onClose }: Props) { const [formDef, setFormDef] = useState
(null); const [q, setQ] = useState(""); const [appliedQ, setAppliedQ] = useState(""); const [dateFrom, setDateFrom] = useState(""); const [dateTo, setDateTo] = useState(""); const [appliedFrom, setAppliedFrom] = useState(""); const [appliedTo, setAppliedTo] = useState(""); const [page, setPage] = useState(1); const [rows, setRows] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [exporting, setExporting] = useState(false); const [replyTarget, setReplyTarget] = useState(null); const [replySubject, setReplySubject] = useState(""); const [replyBody, setReplyBody] = useState(""); const [replying, setReplying] = useState(false); const [replyMessage, setReplyMessage] = useState(null); const { user } = useAuth(); const fieldLabels = (formDef?.fields || []) .map((f: FormField) => f.label?.trim()) .filter(Boolean) as string[]; const tableFieldLabels = fieldLabels.slice(0, SUBMISSIONS_TABLE_PREVIEW_FIELDS); const tableHiddenFieldCount = Math.max(0, fieldLabels.length - tableFieldLabels.length); const loadForm = useCallback(async () => { try { const f = await formsApi.get(formId); setFormDef(f); } catch { setFormDef(null); } }, [formId]); const fetchPage = useCallback(async () => { setLoading(true); setErr(null); try { const res = await formsApi.listSubmissions(formId, { q: appliedQ || undefined, dateFrom: appliedFrom || undefined, dateTo: appliedTo || undefined, page, limit: PAGE_SIZE, }); setRows(res.submissions); setTotal(res.total); } catch (e) { setErr(e instanceof Error ? e.message : "Failed to load submissions."); setRows([]); setTotal(0); } finally { setLoading(false); } }, [formId, appliedQ, appliedFrom, appliedTo, page]); useEffect(() => { void loadForm(); }, [loadForm]); useEffect(() => { void fetchPage(); }, [fetchPage]); const applyFilters = () => { setAppliedQ(q.trim()); setAppliedFrom(dateFrom); setAppliedTo(dateTo); setPage(1); }; const onExport = async () => { setExporting(true); try { const { blob } = await formsApi.exportSubmissionsCsv(formId, { q: appliedQ || undefined, dateFrom: appliedFrom || undefined, dateTo: appliedTo || undefined, }); const filename = buildFormSubmissionsExportFilename( eventNameFromForm(formDef), formName || formDef?.name || "Form", ); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } catch (e) { setErr(e instanceof Error ? e.message : "Export failed."); } finally { setExporting(false); } }; const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const openReply = (submission: FormSubmissionRecord) => { const email = participantEmail(submission.participant); if (email === "—") { setReplyMessage("This submission does not include an email address."); return; } setReplyTarget(submission); setReplySubject(`Re: Your response to ${formName}`); setReplyBody(`Hi ${participantName(submission.participant)},\n\nThank you for your response.\n\nBest regards,\n${user?.firstName || user?.email || "EventBuzz"}`); setReplyMessage(null); }; const sendReply = async () => { if (!replyTarget) return; const to = participantEmail(replyTarget.participant); if (!to || to === "—") { setReplyMessage("Please choose a submission with a valid email address."); return; } setReplying(true); setReplyMessage(null); try { await formsApi.replyToSubmission(formId, { to, subject: replySubject, body: replyBody }); setReplyMessage("Email sent successfully."); setReplyTarget(null); setReplySubject(""); setReplyBody(""); } catch (e) { setReplyMessage(e instanceof Error ? e.message : "Failed to send reply."); } finally { setReplying(false); } }; return ( <>

Submissions

{formName}

setQ(e.target.value)} onKeyDown={(e) => e.key === "Enter" && applyFilters()} placeholder="e.g. john*@*" style={{ width: "100%", padding: "8px 10px", fontSize: "var(--text-sm)", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--background)", color: "var(--foreground)", boxSizing: "border-box", }} />
setDateFrom(e.target.value)} style={{ padding: "8px 10px", fontSize: "var(--text-sm)", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--background)", color: "var(--foreground)", }} />
setDateTo(e.target.value)} style={{ padding: "8px 10px", fontSize: "var(--text-sm)", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--background)", color: "var(--foreground)", }} />

Showing {total.toLocaleString()} match{total === 1 ? "" : "es"}. List view is paged; export includes all matches and{" "} all form fields as columns. {tableHiddenFieldCount > 0 ? ` The table only shows the first ${SUBMISSIONS_TABLE_PREVIEW_FIELDS} response fields (${tableHiddenFieldCount} more in the file only).` : null}

{err ? (

{err}

) : null} {loading ? (

Loading…

) : rows.length === 0 ? (

No submissions for this filter.

) : (
{tableFieldLabels.map((lab) => ( ))} {tableHiddenFieldCount > 0 ? ( ) : null} {rows.map((s) => { const o = dataToObj(s.data); return ( {tableFieldLabels.map((lab) => ( ))} {tableHiddenFieldCount > 0 ? ( ) : null} ); })}
Reply Submitted Email {lab} +{tableHiddenFieldCount} more…
{formatWhen(s.submittedAt)} {participantEmail(s.participant)} {formatCell(o[lab])} Export for full data
)}
{replyTarget ? (

Reply to {participantEmail(replyTarget.participant)}

This sends a real email from the connected mailbox configuration.

setReplySubject(e.target.value)} placeholder="Subject" style={{ padding: "8px 10px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--card)", color: "var(--foreground)" }} />