diff --git a/src/app/components/dashboard/EventHomeModules.tsx b/src/app/components/dashboard/EventHomeModules.tsx index 21293219..fe6d82a7 100644 --- a/src/app/components/dashboard/EventHomeModules.tsx +++ b/src/app/components/dashboard/EventHomeModules.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from "react"; -import { BarChart2, CalendarDays, FileText, Plus, ShieldCheck, Tag, Ticket, Trophy } from "lucide-react"; +import { BarChart2, CalendarDays, FileText, Plus, ShieldCheck, Tag, Ticket, Trophy, Upload } from "lucide-react"; import { useBreakpoint } from "../../hooks/useBreakpoint"; import { useAppNav } from "../../contexts/AppNavContext"; import { eventsApi } from "../../../services/api/events"; @@ -12,6 +12,7 @@ import { CreateFormPanel } from "../panels/CreateFormPanel"; import { FormSubmissionsPanel } from "../panels/FormSubmissionsPanel"; import { CreateBadgePanel } from "../panels/CreateBadgePanel"; import { EventBadgesPanel } from "./EventBadgesPanel"; +import { EventDocumentsPanel } from "./EventDocumentsPanel"; import { AddQuizPollModal, buildQuizPollSettingsPayload } from "../quiz/AddQuizPollModal"; import type { QuizPollModalForm } from "../quiz/AddQuizPollModal"; import type { Question } from "../quiz/AddQuestionModal"; @@ -186,6 +187,7 @@ export function EventHomeModules() { const [eventSettings, setEventSettings] = useState>({}); const [webCheckInEnabled, setWebCheckInEnabled] = useState(false); const [webCheckInLoading, setWebCheckInLoading] = useState(false); + const [showDocumentsPanel, setShowDocumentsPanel] = useState(false); useEffect(() => { if (!selectedEventId) { @@ -750,32 +752,39 @@ export function EventHomeModules() { meta={webCheckInMeta} description="Show or hide accommodation choice for participants" actions={ - <> + webCheckInEnabled ? ( + ) : ( - + ) } /> } /> + } + title="Upload Document" + description="Manage documents uploaded for this event" + actions={ + + } + /> {formEditTarget && selectedEventId ? ( @@ -935,6 +963,14 @@ export function EventHomeModules() { onSaved={() => void loadRewardRulesMeta()} /> ) : null} + + {showDocumentsPanel && selectedEventId ? ( + setShowDocumentsPanel(false)} + /> + ) : null} ); } diff --git a/src/app/components/global/GlobalStubPage.tsx b/src/app/components/global/GlobalStubPage.tsx index 53466a1d..cb3bd4e8 100644 --- a/src/app/components/global/GlobalStubPage.tsx +++ b/src/app/components/global/GlobalStubPage.tsx @@ -1429,8 +1429,10 @@ export function AllParticipantsPage() { bulkDialog={bulk.bulkDialog} bulkBusy={bulk.bulkBusy} selectedCount={selectedIds.length} - tagBulkInput={bulk.tagBulkInput} - onTagBulkInputChange={bulk.setTagBulkInput} + selectedTagValues={bulk.selectedTagValues} + onToggleTag={bulk.toggleSelectedTag} + availableTags={tagOptions} + removeTagOptions={Array.from(new Set(list.filter((participant) => selectedIds.includes(participant.id)).flatMap((participant) => participant.tags)))} newListName={bulk.newListName} onNewListNameChange={bulk.setNewListName} newListDesc={bulk.newListDesc} @@ -1442,7 +1444,7 @@ export function AllParticipantsPage() { addToListId={bulk.addToListId} onAddToListIdChange={bulk.setAddToListId} onClose={() => bulk.setBulkDialog(null)} - onApplyTags={(action) => void bulk.runBulkTags(action)} + onApplyTags={(action, overrideTags) => void bulk.runBulkTags(action, overrideTags)} onAssignEvents={() => void bulk.runBulkAssignEvents()} onCreateList={() => void bulk.runBulkNewList()} onAddToList={() => void bulk.runBulkAddToList()} diff --git a/src/app/components/panels/FormSubmissionsPanel.tsx b/src/app/components/panels/FormSubmissionsPanel.tsx index e378ef45..0058d109 100644 --- a/src/app/components/panels/FormSubmissionsPanel.tsx +++ b/src/app/components/panels/FormSubmissionsPanel.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useState } from "react"; -import { X, Download, ChevronLeft, ChevronRight } from "lucide-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"; function dataToObj(data: unknown): Record { @@ -27,6 +28,11 @@ function participantEmail( 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"; +} + type Props = { formId: string; formName: string; @@ -51,6 +57,12 @@ export function FormSubmissionsPanel({ formId, formName, onClose }: Props) { 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()) @@ -127,6 +139,41 @@ export function FormSubmissionsPanel({ formId, formName, onClose }: Props) { 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 ( <>
+ Reply Submitted Email {tableFieldLabels.map((lab) => ( @@ -402,6 +450,29 @@ export function FormSubmissionsPanel({ formId, formName, onClose }: Props) { const o = dataToObj(s.data); return ( + + + {formatWhen(s.submittedAt)} @@ -428,6 +499,53 @@ export function FormSubmissionsPanel({ formId, formName, onClose }: Props) { )}
+ {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)" }} + /> +