EventBuzz_Console/src/app/components/dashboard/EventHomeModules.tsx

2187 lines
78 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useEffect, useState } from "react";
import { BarChart2, CalendarDays, Check, FileText, Globe, Image, Images, Link2, Mail, MessageSquare, Palette, Plus, Share2, ShieldCheck, Sparkles, Store, Tag, Ticket, Trophy, Upload } from "lucide-react";
import { useBreakpoint } from "../../hooks/useBreakpoint";
import { useAppNav } from "../../contexts/AppNavContext";
import { useAuth } from "../../contexts/AuthContext";
import { usePermission } from "../../hooks/usePermission";
import { eventsApi } from "../../../services/api/events";
import { eventStaffApi } from "../../../services/api/eventStaff";
import { quizPollsApi } from "../../../services/api/quizPolls";
import { badgeBatchesApi } from "../../../services/api/badgeBatches";
import { eventTicketsApi } from "../../../services/api/eventTickets";
import { campaignsApi } from "../../../services/api/campaigns";
import { countConfiguredRewardRules, RewardRulesSettingsPanel } from "../rewards/rewardRulesSettings";
import { ManageStaticMicrositePanel } from "../microsite-v2/ManageStaticMicrositePanel";
import { VisitorThemeSettingsPanel } from "../panels/VisitorThemeSettingsPanel";
import { CreateFormPanel } from "../panels/CreateFormPanel";
import { CreateEmailCampaignPanel } from "../panels/CreateEmailCampaignPanel";
import { FormSubmissionsPanel } from "../panels/FormSubmissionsPanel";
import { ShareFormPanel } from "../forms/ShareFormPanel";
import { CreateBadgePanel } from "../panels/CreateBadgePanel";
import { EventBadgesPanel } from "./EventBadgesPanel";
import { EventDocumentsPanel } from "./EventDocumentsPanel";
import { PhotoWallManagePanel } from "./PhotoWallManagePanel";
import { SocialShareManagePanel } from "./SocialShareManagePanel";
import { AiAvatarManagePanel } from "./AiAvatarManagePanel";
import { EventPhotoSearchPanel } from "../panels/EventPhotoSearchPanel";
import { photoWallApi } from "../../../services/api/photoWall";
import { eventPhotoSearchApi } from "../../../services/api/eventPhotoSearch";
import { socialShareApi } from "../../../services/api/socialShare";
import { aiAvatarApi } from "../../../services/api/aiAvatar";
import { photoWallUploadUrl, photoWallPresentUrl } from "../../utils/photoWallUrls";
import { photoSearchVisitorUrl } from "../../utils/photoSearchUrls";
import { socialShareVisitorUrl } from "../../utils/socialShareUrls";
import { aiAvatarVisitorUrl } from "../../utils/aiAvatarUrls";
import { EventEmailCampaignsPanel } from "./EventEmailCampaignsPanel";
import { WhatsAppBlastHistoryPanel } from "./WhatsAppBlastHistoryPanel";
import { SendWhatsAppNotificationPanel } from "./SendWhatsAppNotificationPanel";
import { whatsappBlastsApi } from "../../../services/api/whatsappBlasts";
import { EventExhibitorsPanel } from "./EventExhibitorsPanel";
import { AddExhibitorPanel } from "../panels/AddExhibitorPanel";
import { AddQuizPollModal, buildQuizPollSettingsPayload } from "../quiz/AddQuizPollModal";
import type { QuizPollModalForm } from "../quiz/AddQuizPollModal";
import type { Question } from "../quiz/AddQuestionModal";
import { modalQuestionToApi } from "../quiz/quizQuestionMappers";
import {
sessionsApi,
tracksApi,
formsApi,
staticMicrositeApi,
staticMicrositePublicUrl,
visitorThemeApi,
exhibitorsApi,
type ApiExhibitor,
} from "../../../services/api";
import type { FormWithSubmissionCount } from "../../../services/api/types";
import { publicFormPageUrl } from "../../utils/publicFormUrl";
function ModuleCard({
icon,
title,
description,
meta,
headerAction,
actions,
}: {
icon: React.ReactNode;
title: string;
description?: string;
meta?: string;
headerAction?: React.ReactNode;
actions: React.ReactNode;
}) {
const bp = useBreakpoint();
const isMobile = bp === "mobile";
return (
<div
style={{
background: "var(--card)",
border: "1px solid var(--border)",
borderRadius: "var(--radius-card)",
padding: isMobile ? "14px" : "16px",
display: "flex",
flexDirection: "column",
gap: "12px",
minHeight: "auto",
}}
>
<div style={{ display: "flex", alignItems: "flex-start", gap: "10px", flex: 1 }}>
<div
style={{
width: "36px",
height: "36px",
borderRadius: "var(--radius-button)",
background: "color-mix(in srgb, var(--primary) 12%, transparent)",
color: "var(--primary)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
{icon}
</div>
<div style={{ minWidth: 0, flex: 1 }}>
<div style={{ display: "flex", alignItems: "flex-start", gap: "6px" }}>
<p
style={{
fontFamily: "var(--font-inter)",
fontSize: "var(--text-base)",
fontWeight: "var(--font-weight-medium)",
color: "var(--foreground)",
margin: 0,
lineHeight: 1.3,
flex: 1,
minWidth: 0,
}}
>
{title}
</p>
{headerAction}
</div>
{description ? (
<p
style={{
fontSize: "var(--text-xs)",
fontFamily: "var(--font-noto-sans)",
color: "var(--muted)",
margin: "4px 0 0",
lineHeight: 1.4,
}}
>
{description}
</p>
) : null}
{meta ? (
<p
style={{
fontSize: "var(--text-xs)",
fontFamily: "var(--font-noto-sans)",
color: "var(--muted)",
margin: "6px 0 0",
}}
>
{meta}
</p>
) : null}
</div>
</div>
<div style={{ display: "flex", gap: "6px", flexWrap: "nowrap" }}>{actions}</div>
</div>
);
}
function moduleButtonStyle(primary = false): React.CSSProperties {
return {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
gap: "6px",
flex: "1 1 0",
minWidth: 0,
padding: "6px 8px",
borderRadius: "var(--radius-button)",
border: primary ? "1px solid var(--primary)" : "1px solid var(--border)",
background: "var(--card)",
color: primary ? "var(--primary)" : "var(--card-foreground)",
fontSize: "var(--text-xs)",
fontFamily: "var(--font-noto-sans)",
fontWeight: "var(--font-weight-medium)",
cursor: "pointer",
};
}
function formDisplayType(apiType: string): string {
if (apiType === "feedback") return "Feedback";
if (apiType === "registration") return "Registration";
if (apiType === "web-check-in") return "Web check-in";
if (apiType === "other") return "Other";
if (apiType === "survey" || apiType === "custom") return "Other";
return apiType.charAt(0).toUpperCase() + apiType.slice(1);
}
const NEW_FORM_OPTIONS: { value: "registration" | "feedback" | "web-check-in" | "other"; label: string }[] = [
{ value: "registration", label: "Registration" },
{ value: "feedback", label: "Feedback" },
{ value: "web-check-in", label: "Web check-in" },
{ value: "other", label: "Other" },
];
function formPanelMode(apiType: string): "feedback" | "registration" | "other" | "web-check-in" {
if (apiType === "feedback") return "feedback";
if (apiType === "registration") return "registration";
if (apiType === "web-check-in") return "web-check-in";
return "other";
}
function formMetaLine(form: FormWithSubmissionCount): string {
const count = form.submissionCount ?? 0;
return `${formDisplayType(form.type)} · ${count.toLocaleString()} response${count === 1 ? "" : "s"}`;
}
function formLinkCopyButtonStyle(copied: boolean): React.CSSProperties {
return {
width: "26px",
height: "26px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "var(--radius-button)",
border: "1px solid var(--border)",
background: copied ? "#dcfce7" : "var(--card)",
color: copied ? "#15803d" : "var(--muted)",
cursor: "pointer",
flexShrink: 0,
padding: 0,
};
}
function formHeaderIconButtonStyle(): React.CSSProperties {
return {
...formLinkCopyButtonStyle(false),
color: "var(--muted)",
};
}
export function EventHomeModules() {
const { selectedEventId, setActiveNav, setPermissionsOpenAddOnce, setOpenAgendaSessionPanelOnce } = useAppNav();
const { user } = useAuth();
const { canModule } = usePermission();
const isSuperAdmin = user?.role === "super_admin";
const canReadForms = canModule("forms", "read");
const canWriteForms = canModule("forms", "write");
const canReadSessions = canModule("sessions", "read");
const canWriteSessions = canModule("sessions", "write");
const canReadUserMgmt = canModule("user_management", "read");
const canWriteUserMgmt = canModule("user_management", "write");
const canReadActivities =
canModule("live_polls", "read") || canModule("multiple_quiz_modes", "read");
const canWriteActivities =
canModule("live_polls", "write") || canModule("multiple_quiz_modes", "write");
const canReadBadges = canModule("custom_badge_and_print_file_creation", "read");
const canWriteBadges = canModule("custom_badge_and_print_file_creation", "write");
const canReadTicketing = canModule("ticketing_free_and_paid", "read");
const canReadMicrosite = canModule("event_website", "read");
const canReadRewards = canModule("gamification", "read");
const canReadWebCheckIn = canModule("web_check_in", "read");
const canWriteWebCheckIn = canModule("web_check_in", "write");
const canReadDocuments = canModule("content_management", "read");
const canReadPhotoWall = canModule("photo_wall", "read");
const canWritePhotoWall = canModule("photo_wall", "write");
const canReadSocialShare = canModule("social_share", "read");
const canWriteSocialShare = canModule("social_share", "write");
const canReadAiAvatar = canModule("ai_avatar", "read");
const canWriteAiAvatar = canModule("ai_avatar", "write");
const canReadEmailCampaigns = canModule("email_outreach_campaigns", "read");
const canWriteEmailCampaigns = canModule("email_outreach_campaigns", "write");
const canReadWaNotifications = canModule("custom_template_for_wa_messaging", "read");
const canWriteWaNotifications = canModule("custom_template_for_wa_messaging", "write");
const canReadExhibitors = canModule("exhibitors", "read");
const canWriteExhibitors = canModule("exhibitors", "write");
const bp = useBreakpoint();
const isMobile = bp === "mobile";
const isTablet = bp === "tablet";
const gridTemplateColumns = isMobile
? "1fr"
: isTablet
? "repeat(2, minmax(0, 1fr))"
: "repeat(auto-fill, minmax(240px, 1fr))";
const [eventForms, setEventForms] = useState<FormWithSubmissionCount[]>([]);
const [formEditTarget, setFormEditTarget] = useState<FormWithSubmissionCount | null>(null);
const [formResponsesTarget, setFormResponsesTarget] = useState<FormWithSubmissionCount | null>(null);
const [formShareTarget, setFormShareTarget] = useState<FormWithSubmissionCount | null>(null);
const [newFormMode, setNewFormMode] = useState<"registration" | "feedback" | "other" | "web-check-in" | null>(null);
const [newFormType, setNewFormType] = useState<"registration" | "feedback" | "web-check-in" | "other">("registration");
const [copiedFormLinkId, setCopiedFormLinkId] = useState<string | null>(null);
const [pollQuizCounts, setPollQuizCounts] = useState({ polls: 0, quizzes: 0 });
const [pollQuizLoading, setPollQuizLoading] = useState(false);
const [showAddPoll, setShowAddPoll] = useState(false);
const [showAddQuiz, setShowAddQuiz] = useState(false);
const [staffCounts, setStaffCounts] = useState({ members: 0, invites: 0 });
const [staffLoading, setStaffLoading] = useState(false);
const [badgeCount, setBadgeCount] = useState(0);
const [badgeLoading, setBadgeLoading] = useState(false);
const [eventName, setEventName] = useState<string | null>(null);
const [badgesOpen, setBadgesOpen] = useState(false);
const [showAddBadge, setShowAddBadge] = useState(false);
const [duplicateBadgeBatchId, setDuplicateBadgeBatchId] = useState<string | null>(null);
const [ticketCount, setTicketCount] = useState(0);
const [ticketLoading, setTicketLoading] = useState(false);
const [rewardRulesCount, setRewardRulesCount] = useState(0);
const [rewardLoading, setRewardLoading] = useState(false);
const [rewardRulesOpen, setRewardRulesOpen] = useState(false);
const [staticMicrositeOpen, setStaticMicrositeOpen] = useState(false);
const [staticMicrositeMeta, setStaticMicrositeMeta] = useState<string | null>(null);
const [staticMicrositeLoading, setStaticMicrositeLoading] = useState(false);
const [visitorThemeOpen, setVisitorThemeOpen] = useState(false);
const [visitorThemeMeta, setVisitorThemeMeta] = useState<string | null>(null);
const [visitorThemeLoading, setVisitorThemeLoading] = useState(false);
const [agendaCounts, setAgendaCounts] = useState({ tracks: 0, sessions: 0 });
const [agendaLoading, setAgendaLoading] = useState(false);
const [eventSettings, setEventSettings] = useState<Record<string, unknown>>({});
const [webCheckInEnabled, setWebCheckInEnabled] = useState(false);
const [webCheckInLoading, setWebCheckInLoading] = useState(false);
const [showDocumentsPanel, setShowDocumentsPanel] = useState(false);
const [showEventPhotoSearchPanel, setShowEventPhotoSearchPanel] = useState(false);
const [photoSearchEnabled, setPhotoSearchEnabled] = useState(false);
const [photoSearchLoading, setPhotoSearchLoading] = useState(false);
const [photoSearchCount, setPhotoSearchCount] = useState(0);
const [photoSearchToken, setPhotoSearchToken] = useState<string | null>(null);
const [copiedPhotoSearchLink, setCopiedPhotoSearchLink] = useState(false);
const [showPhotoWallPanel, setShowPhotoWallPanel] = useState(false);
const [photoWallEnabled, setPhotoWallEnabled] = useState(false);
const [photoWallLoading, setPhotoWallLoading] = useState(false);
const [photoWallCount, setPhotoWallCount] = useState(0);
const [photoWallUploadToken, setPhotoWallUploadToken] = useState<string | null>(null);
const [photoWallPresentToken, setPhotoWallPresentToken] = useState<string | null>(null);
const [copiedPhotoWallLink, setCopiedPhotoWallLink] = useState<"upload" | "present" | null>(null);
const [showSocialSharePanel, setShowSocialSharePanel] = useState(false);
const [socialShareEnabled, setSocialShareEnabled] = useState(false);
const [socialShareLoading, setSocialShareLoading] = useState(false);
const [socialShareFrameCount, setSocialShareFrameCount] = useState(0);
const [socialShareToken, setSocialShareToken] = useState<string | null>(null);
const [copiedSocialShareLink, setCopiedSocialShareLink] = useState(false);
const [showAiAvatarPanel, setShowAiAvatarPanel] = useState(false);
const [aiAvatarEnabled, setAiAvatarEnabled] = useState(false);
const [aiAvatarLoading, setAiAvatarLoading] = useState(false);
const [aiAvatarCount, setAiAvatarCount] = useState(0);
const [aiAvatarToken, setAiAvatarToken] = useState<string | null>(null);
const [copiedAiAvatarLink, setCopiedAiAvatarLink] = useState(false);
const [emailCampaignCount, setEmailCampaignCount] = useState(0);
const [emailCampaignLoading, setEmailCampaignLoading] = useState(false);
const [showCreateEmailCampaign, setShowCreateEmailCampaign] = useState(false);
const [showEmailCampaignsPanel, setShowEmailCampaignsPanel] = useState(false);
const [waBlastCount, setWaBlastCount] = useState(0);
const [waBlastLoading, setWaBlastLoading] = useState(false);
const [showWaBlastHistory, setShowWaBlastHistory] = useState(false);
const [showSendWaNotification, setShowSendWaNotification] = useState(false);
const [exhibitorCount, setExhibitorCount] = useState(0);
const [exhibitorLoading, setExhibitorLoading] = useState(false);
const [showExhibitorsPanel, setShowExhibitorsPanel] = useState(false);
const [showAddExhibitor, setShowAddExhibitor] = useState(false);
const [editingExhibitor, setEditingExhibitor] = useState<ApiExhibitor | null>(null);
const actionsDisabled = !selectedEventId;
const photoSearchHeaderButtonStyle: React.CSSProperties = {
width: "26px",
height: "26px",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "var(--radius-button)",
border: "1px solid var(--border)",
background: "var(--card)",
color: "var(--muted)",
cursor: actionsDisabled ? "not-allowed" : "pointer",
opacity: actionsDisabled ? 0.55 : 1,
flexShrink: 0,
padding: 0,
};
useEffect(() => {
if (!selectedEventId) {
setEventName(null);
return;
}
let cancelled = false;
void eventsApi.get(selectedEventId).then((res) => {
if (cancelled) return;
setEventName(res.data?.name ?? null);
setEventSettings((res.data?.settings as Record<string, unknown> | undefined) ?? {});
setWebCheckInEnabled(Boolean((res.data?.settings as Record<string, unknown> | undefined)?.webCheckInEnabled));
});
return () => {
cancelled = true;
};
}, [selectedEventId]);
const loadEventForms = useCallback(async () => {
if (!selectedEventId) {
setEventForms([]);
return;
}
try {
const forms = await formsApi.listForEvent(selectedEventId);
const sorted = [...forms].sort((a, b) =>
(a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" }),
);
setEventForms(sorted);
} catch {
setEventForms([]);
}
}, [selectedEventId]);
useEffect(() => {
void loadEventForms();
}, [loadEventForms]);
const loadPollQuizCounts = useCallback(async () => {
if (!selectedEventId) {
setPollQuizCounts({ polls: 0, quizzes: 0 });
setPollQuizLoading(false);
return;
}
setPollQuizLoading(true);
try {
const res = await quizPollsApi.listByEvent(selectedEventId);
const items = res.data?.quizPolls ?? [];
setPollQuizCounts({
polls: items.filter((q) => q.type === "poll").length,
quizzes: items.filter((q) => q.type === "quiz").length,
});
} catch {
setPollQuizCounts({ polls: 0, quizzes: 0 });
} finally {
setPollQuizLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadPollQuizCounts();
}, [loadPollQuizCounts]);
const loadStaffCounts = useCallback(async () => {
if (!selectedEventId) {
setStaffCounts({ members: 0, invites: 0 });
setStaffLoading(false);
return;
}
setStaffLoading(true);
try {
const res = await eventStaffApi.listStaff(selectedEventId);
if (res.data) {
setStaffCounts({
members: res.data.members?.length ?? 0,
invites: res.data.invites?.length ?? 0,
});
} else {
setStaffCounts({ members: 0, invites: 0 });
}
} catch {
setStaffCounts({ members: 0, invites: 0 });
} finally {
setStaffLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadStaffCounts();
}, [loadStaffCounts]);
const loadBadgeCount = useCallback(async () => {
if (!selectedEventId) {
setBadgeCount(0);
setBadgeLoading(false);
return;
}
setBadgeLoading(true);
try {
const res = await badgeBatchesApi.list({ event: selectedEventId, limit: 100 });
setBadgeCount(res.data?.batches?.length ?? 0);
} catch {
setBadgeCount(0);
} finally {
setBadgeLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadBadgeCount();
}, [loadBadgeCount]);
const loadTicketCount = useCallback(async () => {
if (!selectedEventId) {
setTicketCount(0);
setTicketLoading(false);
return;
}
setTicketLoading(true);
try {
const tickets = await eventTicketsApi.list(selectedEventId);
setTicketCount(tickets.length);
} catch {
setTicketCount(0);
} finally {
setTicketLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadTicketCount();
}, [loadTicketCount]);
const loadRewardRulesMeta = useCallback(async () => {
if (!selectedEventId) {
setRewardRulesCount(0);
setRewardLoading(false);
return;
}
setRewardLoading(true);
try {
const res = await eventsApi.get(selectedEventId);
setRewardRulesCount(countConfiguredRewardRules(res.data?.rewardRules));
} catch {
setRewardRulesCount(0);
} finally {
setRewardLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadRewardRulesMeta();
}, [loadRewardRulesMeta]);
const loadStaticMicrositeMeta = useCallback(async () => {
if (!selectedEventId) {
setStaticMicrositeMeta(null);
setStaticMicrositeLoading(false);
return;
}
setStaticMicrositeLoading(true);
try {
const res = await staticMicrositeApi.getForEvent(selectedEventId);
if (res.data?.subdomain) {
const status = res.data.status === "published" ? "Published" : "Draft";
setStaticMicrositeMeta(`${staticMicrositePublicUrl(res.data.subdomain)} · ${status}`);
} else {
setStaticMicrositeMeta("Not configured");
}
} catch {
setStaticMicrositeMeta("Not configured");
} finally {
setStaticMicrositeLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadStaticMicrositeMeta();
}, [loadStaticMicrositeMeta]);
const loadVisitorThemeMeta = useCallback(async () => {
if (!selectedEventId) {
setVisitorThemeMeta(null);
setVisitorThemeLoading(false);
return;
}
setVisitorThemeLoading(true);
try {
const res = await visitorThemeApi.get(selectedEventId);
const mode = res.data?.theme;
if (mode === "gradient") setVisitorThemeMeta("Gradient");
else if (mode === "background") setVisitorThemeMeta("Background");
else setVisitorThemeMeta("Simple");
} catch {
setVisitorThemeMeta("Simple");
} finally {
setVisitorThemeLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadVisitorThemeMeta();
}, [loadVisitorThemeMeta]);
const loadAgendaMeta = useCallback(async () => {
if (!selectedEventId) {
setAgendaCounts({ tracks: 0, sessions: 0 });
setAgendaLoading(false);
return;
}
setAgendaLoading(true);
try {
const [trRes, sesRes] = await Promise.all([
tracksApi.list(selectedEventId),
sessionsApi.list(selectedEventId),
]);
const tracks = (trRes.data as { tracks?: unknown[] } | undefined)?.tracks ?? [];
const sessions = (sesRes.data as { sessions?: unknown[] } | undefined)?.sessions ?? [];
setAgendaCounts({ tracks: tracks.length, sessions: sessions.length });
} catch {
setAgendaCounts({ tracks: 0, sessions: 0 });
} finally {
setAgendaLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadAgendaMeta();
}, [loadAgendaMeta]);
const loadEmailCampaignMeta = useCallback(async () => {
if (!selectedEventId) {
setEmailCampaignCount(0);
setEmailCampaignLoading(false);
return;
}
setEmailCampaignLoading(true);
try {
const res = await campaignsApi.listForEvent(selectedEventId);
const campaigns = res.data?.campaigns ?? [];
setEmailCampaignCount(campaigns.filter((c) => c.channel === "email").length);
} catch {
setEmailCampaignCount(0);
} finally {
setEmailCampaignLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadEmailCampaignMeta();
}, [loadEmailCampaignMeta]);
const loadWaBlastMeta = useCallback(async () => {
if (!selectedEventId || !canReadWaNotifications) {
setWaBlastCount(0);
setWaBlastLoading(false);
return;
}
setWaBlastLoading(true);
try {
const res = await whatsappBlastsApi.list(selectedEventId);
setWaBlastCount(res.data?.blasts?.length ?? 0);
} catch {
setWaBlastCount(0);
} finally {
setWaBlastLoading(false);
}
}, [selectedEventId, canReadWaNotifications]);
useEffect(() => {
void loadWaBlastMeta();
}, [loadWaBlastMeta]);
const loadExhibitorMeta = useCallback(async () => {
if (!selectedEventId) {
setExhibitorCount(0);
setExhibitorLoading(false);
return;
}
setExhibitorLoading(true);
try {
const res = await exhibitorsApi.list(selectedEventId);
setExhibitorCount(res.data?.total ?? res.data?.exhibitors?.length ?? 0);
} catch {
setExhibitorCount(0);
} finally {
setExhibitorLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadExhibitorMeta();
}, [loadExhibitorMeta]);
const loadPhotoWallMeta = useCallback(async () => {
if (!selectedEventId || !canReadPhotoWall) {
setPhotoWallEnabled(false);
setPhotoWallCount(0);
setPhotoWallUploadToken(null);
setPhotoWallPresentToken(null);
setPhotoWallLoading(false);
return;
}
setPhotoWallLoading(true);
try {
const res = await photoWallApi.get(selectedEventId);
const data = res.data;
setPhotoWallEnabled(Boolean(data?.enabled));
setPhotoWallCount(data?.photoCounts?.total ?? 0);
setPhotoWallUploadToken(data?.uploadToken ?? null);
setPhotoWallPresentToken(data?.presentToken ?? null);
} catch {
setPhotoWallEnabled(false);
setPhotoWallCount(0);
setPhotoWallUploadToken(null);
setPhotoWallPresentToken(null);
} finally {
setPhotoWallLoading(false);
}
}, [selectedEventId, canReadPhotoWall]);
useEffect(() => {
void loadPhotoWallMeta();
}, [loadPhotoWallMeta]);
const loadSocialShareMeta = useCallback(async () => {
if (!selectedEventId || !canReadSocialShare) {
setSocialShareEnabled(false);
setSocialShareFrameCount(0);
setSocialShareToken(null);
setSocialShareLoading(false);
return;
}
setSocialShareLoading(true);
try {
const res = await socialShareApi.get(selectedEventId);
const data = res.data;
setSocialShareEnabled(Boolean(data?.enabled));
setSocialShareFrameCount(data?.frameCount ?? data?.frames?.length ?? 0);
setSocialShareToken(data?.uploadToken ?? null);
} catch {
setSocialShareEnabled(false);
setSocialShareFrameCount(0);
setSocialShareToken(null);
} finally {
setSocialShareLoading(false);
}
}, [selectedEventId, canReadSocialShare]);
useEffect(() => {
void loadSocialShareMeta();
}, [loadSocialShareMeta]);
const loadAiAvatarMeta = useCallback(async () => {
if (!selectedEventId || !canReadAiAvatar) {
setAiAvatarEnabled(false);
setAiAvatarCount(0);
setAiAvatarToken(null);
setAiAvatarLoading(false);
return;
}
setAiAvatarLoading(true);
try {
const res = await aiAvatarApi.get(selectedEventId);
const data = res.data;
setAiAvatarEnabled(Boolean(data?.enabled));
setAiAvatarCount(data?.avatarCount ?? data?.avatars?.length ?? 0);
setAiAvatarToken(data?.uploadToken ?? null);
} catch {
setAiAvatarEnabled(false);
setAiAvatarCount(0);
setAiAvatarToken(null);
} finally {
setAiAvatarLoading(false);
}
}, [selectedEventId, canReadAiAvatar]);
useEffect(() => {
void loadAiAvatarMeta();
}, [loadAiAvatarMeta]);
const loadPhotoSearchMeta = useCallback(async () => {
if (!selectedEventId) {
setPhotoSearchEnabled(false);
setPhotoSearchCount(0);
setPhotoSearchToken(null);
setPhotoSearchLoading(false);
return;
}
setPhotoSearchLoading(true);
try {
const res = await eventPhotoSearchApi.get(selectedEventId);
const data = res.data;
setPhotoSearchEnabled(Boolean(data?.enabled));
setPhotoSearchCount(data?.photoCount ?? 0);
setPhotoSearchToken(data?.searchToken ?? null);
} catch {
setPhotoSearchEnabled(false);
setPhotoSearchCount(0);
setPhotoSearchToken(null);
} finally {
setPhotoSearchLoading(false);
}
}, [selectedEventId]);
useEffect(() => {
void loadPhotoSearchMeta();
}, [loadPhotoSearchMeta]);
const handleBadgeSuccess = () => {
setShowAddBadge(false);
setDuplicateBadgeBatchId(null);
void loadBadgeCount();
};
const openAddBadge = () => {
setDuplicateBadgeBatchId(null);
setShowAddBadge(true);
};
const openDuplicateBadge = (batchId: string) => {
setBadgesOpen(false);
setDuplicateBadgeBatchId(batchId);
setShowAddBadge(true);
};
const handleFormEditSuccess = () => {
setFormEditTarget(null);
void loadEventForms();
};
const handleNewFormSuccess = () => {
setNewFormMode(null);
void loadEventForms();
};
const copyFormPublicLink = useCallback(
(form: FormWithSubmissionCount) => {
const url = publicFormPageUrl(form, selectedEventId);
void navigator.clipboard.writeText(url).then(() => {
setCopiedFormLinkId(form._id);
window.setTimeout(() => {
setCopiedFormLinkId((prev) => (prev === form._id ? null : prev));
}, 4000);
});
},
[selectedEventId],
);
const pollQuizMeta = !selectedEventId
? "Select an event to create polls and quizzes"
: pollQuizLoading
? "Loading…"
: `${pollQuizCounts.polls} poll${pollQuizCounts.polls === 1 ? "" : "s"} · ${pollQuizCounts.quizzes} quiz${pollQuizCounts.quizzes === 1 ? "" : "zes"}`;
const permissionsMeta = !selectedEventId
? "Select an event to manage team access"
: staffLoading
? "Loading…"
: `${staffCounts.members} member${staffCounts.members === 1 ? "" : "s"} · ${staffCounts.invites} pending invite${staffCounts.invites === 1 ? "" : "s"}`;
const badgeMeta = !selectedEventId
? "Select an event to manage badges"
: badgeLoading
? "Loading…"
: `${badgeCount} badge${badgeCount === 1 ? "" : "s"}`;
const ticketMeta = !selectedEventId
? "Select an event to configure tickets"
: ticketLoading
? "Loading…"
: `${ticketCount} ticket type${ticketCount === 1 ? "" : "s"}`;
const rewardMeta = !selectedEventId
? "Select an event to configure reward rules"
: rewardLoading
? "Loading…"
: rewardRulesCount === 0
? "No rules configured"
: `${rewardRulesCount} rule${rewardRulesCount === 1 ? "" : "s"} configured`;
const visitorThemeCardMeta = !selectedEventId
? "Select an event to manage visitor theme"
: visitorThemeLoading
? "Loading…"
: visitorThemeMeta || "Simple";
const micrositeMeta = !selectedEventId
? "Select an event to manage microsite"
: staticMicrositeLoading
? "Loading…"
: staticMicrositeMeta || "Not configured";
const agendaMeta = !selectedEventId
? "Select an event to manage the agenda"
: agendaLoading
? "Loading…"
: `${agendaCounts.tracks} track${agendaCounts.tracks === 1 ? "" : "s"} · ${agendaCounts.sessions} session${agendaCounts.sessions === 1 ? "" : "s"}`;
const emailCampaignMeta = !selectedEventId
? "Select an event to manage email campaigns"
: emailCampaignLoading
? "Loading…"
: `${emailCampaignCount} email campaign${emailCampaignCount === 1 ? "" : "s"}`;
const waBlastMeta = !selectedEventId
? "Select an event to send WhatsApp notifications"
: waBlastLoading
? "Loading…"
: `${waBlastCount} notification${waBlastCount === 1 ? "" : "s"}`;
const exhibitorMeta = !selectedEventId
? "Select an event to manage exhibitors"
: exhibitorLoading
? "Loading…"
: `${exhibitorCount} exhibitor${exhibitorCount === 1 ? "" : "s"}`;
const webCheckInMeta = !selectedEventId
? "Select an event to configure web check-in"
: webCheckInLoading
? "Saving…"
: webCheckInEnabled
? "Enabled"
: "Disabled";
const photoWallMeta = !selectedEventId
? "Select an event to configure photo wall"
: photoWallLoading
? "Loading…"
: photoWallEnabled
? `Enabled · ${photoWallCount} photo${photoWallCount === 1 ? "" : "s"}`
: "Disabled";
const socialShareMeta = !selectedEventId
? "Select an event to configure social share"
: socialShareLoading
? "Loading…"
: socialShareEnabled
? `Enabled · ${socialShareFrameCount} frame${socialShareFrameCount === 1 ? "" : "s"}`
: "Disabled";
const aiAvatarMeta = !selectedEventId
? "Select an event to configure AI avatar"
: aiAvatarLoading
? "Loading…"
: aiAvatarEnabled
? `Enabled · ${aiAvatarCount} avatar${aiAvatarCount === 1 ? "" : "s"}`
: "Disabled";
const photoSearchMeta = !selectedEventId
? "Select an event to configure photo search"
: photoSearchLoading
? "Loading…"
: photoSearchEnabled
? `Enabled · ${photoSearchCount} photo${photoSearchCount === 1 ? "" : "s"}`
: "Disabled";
const handleWebCheckInToggle = async (enabled: boolean) => {
if (!selectedEventId) return;
setWebCheckInLoading(true);
try {
const nextSettings = { ...(eventSettings || {}), webCheckInEnabled: enabled };
const res = await eventsApi.update(selectedEventId, { settings: nextSettings });
if (!res.error) {
setEventSettings(nextSettings);
setWebCheckInEnabled(enabled);
}
} finally {
setWebCheckInLoading(false);
}
};
const handlePhotoWallToggle = async (enabled: boolean) => {
if (!selectedEventId || !canWritePhotoWall) return;
setPhotoWallLoading(true);
try {
const res = await photoWallApi.update(selectedEventId, { enabled });
if (!res.error && res.data) {
setPhotoWallEnabled(Boolean(res.data.enabled));
setPhotoWallCount(res.data.photoCounts?.total ?? 0);
setPhotoWallUploadToken(res.data.uploadToken ?? null);
setPhotoWallPresentToken(res.data.presentToken ?? null);
}
} finally {
setPhotoWallLoading(false);
}
};
const handleSocialShareToggle = async (enabled: boolean) => {
if (!selectedEventId || !canWriteSocialShare) return;
setSocialShareLoading(true);
try {
const res = await socialShareApi.update(selectedEventId, { enabled });
if (!res.error && res.data) {
setSocialShareEnabled(Boolean(res.data.enabled));
setSocialShareFrameCount(res.data.frameCount ?? res.data.frames?.length ?? 0);
setSocialShareToken(res.data.uploadToken ?? null);
}
} finally {
setSocialShareLoading(false);
}
};
const copySocialShareLink = async () => {
if (!socialShareToken) return;
try {
await navigator.clipboard.writeText(socialShareVisitorUrl(socialShareToken));
setCopiedSocialShareLink(true);
window.setTimeout(() => setCopiedSocialShareLink(false), 1500);
} catch {
/* ignore */
}
};
const handleAiAvatarToggle = async (enabled: boolean) => {
if (!selectedEventId || !canWriteAiAvatar) return;
setAiAvatarLoading(true);
try {
const res = await aiAvatarApi.update(selectedEventId, { enabled });
if (!res.error && res.data) {
setAiAvatarEnabled(Boolean(res.data.enabled));
setAiAvatarCount(res.data.avatarCount ?? res.data.avatars?.length ?? 0);
setAiAvatarToken(res.data.uploadToken ?? null);
}
} finally {
setAiAvatarLoading(false);
}
};
const copyAiAvatarLink = async () => {
if (!aiAvatarToken) return;
try {
await navigator.clipboard.writeText(aiAvatarVisitorUrl(aiAvatarToken));
setCopiedAiAvatarLink(true);
window.setTimeout(() => setCopiedAiAvatarLink(false), 1500);
} catch {
/* ignore */
}
};
const handlePhotoSearchToggle = async (enabled: boolean) => {
if (!selectedEventId) return;
setPhotoSearchLoading(true);
try {
const res = await eventPhotoSearchApi.update(selectedEventId, { enabled });
if (!res.error && res.data) {
setPhotoSearchEnabled(Boolean(res.data.enabled));
setPhotoSearchCount(res.data.photoCount ?? 0);
setPhotoSearchToken(res.data.searchToken ?? null);
}
} finally {
setPhotoSearchLoading(false);
}
};
const copyPhotoSearchLink = async () => {
if (!photoSearchToken) return;
const url = photoSearchVisitorUrl(photoSearchToken);
try {
await navigator.clipboard.writeText(url);
setCopiedPhotoSearchLink(true);
window.setTimeout(() => setCopiedPhotoSearchLink(false), 1500);
} catch {
/* ignore */
}
};
const copyPhotoWallLink = async (kind: "upload" | "present") => {
const url =
kind === "upload" && photoWallUploadToken
? photoWallUploadUrl(photoWallUploadToken)
: kind === "present" && photoWallPresentToken
? photoWallPresentUrl(photoWallPresentToken)
: "";
if (!url) return;
try {
await navigator.clipboard.writeText(url);
setCopiedPhotoWallLink(kind);
window.setTimeout(() => setCopiedPhotoWallLink(null), 1500);
} catch {
/* ignore */
}
};
const handleSavePollQuiz = async (
kind: "poll" | "quiz",
data: { form: QuizPollModalForm; questions: Question[] },
) => {
if (!selectedEventId) return;
const sesRes = await sessionsApi.list(selectedEventId);
const firstSession = sesRes.data?.sessions?.[0];
const payload = {
...(firstSession?._id ? { session: firstSession._id } : {}),
title: data.form.title,
type: kind,
description: data.form.description || undefined,
questions: data.questions.map((q) => modalQuestionToApi(q, kind)),
settings: buildQuizPollSettingsPayload(data.form, kind),
};
const res = await quizPollsApi.create(selectedEventId, payload);
if (!res.error) {
if (kind === "poll") setShowAddPoll(false);
else setShowAddQuiz(false);
void loadPollQuizCounts();
}
};
return (
<>
<section
aria-label="Event modules"
style={{
display: "grid",
gridTemplateColumns,
gap: "12px",
}}
>
{canReadSessions ? (
<ModuleCard
icon={<CalendarDays size={18} />}
title="Agenda"
meta={agendaMeta}
actions={
<>
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setActiveNav("agenda")}
onMouseEnter={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
}}
onMouseLeave={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
}}
>
View Agenda
</button>
<button
type="button"
disabled={actionsDisabled || !canWriteSessions}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || !canWriteSessions ? 0.55 : 1,
cursor: actionsDisabled || !canWriteSessions ? "not-allowed" : "pointer",
}}
onClick={() => {
setOpenAgendaSessionPanelOnce(true);
setActiveNav("agenda");
}}
>
New Session
</button>
</>
}
/>
) : null}
{canReadForms ? eventForms.map((form) => {
const linkCopied = copiedFormLinkId === form._id;
return (
<ModuleCard
key={form._id}
icon={<FileText size={18} />}
title={form.name?.trim() || "Untitled form"}
meta={formMetaLine(form)}
headerAction={
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "4px", flexShrink: 0 }}>
<button
type="button"
title={linkCopied ? "Copied to clipboard" : "Copy public form link"}
disabled={actionsDisabled}
style={{
...formLinkCopyButtonStyle(linkCopied),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => copyFormPublicLink(form)}
onMouseEnter={(e) => {
if (actionsDisabled || linkCopied) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
}}
onMouseLeave={(e) => {
if (linkCopied) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
(e.currentTarget as HTMLElement).style.color = "var(--muted)";
}}
>
{linkCopied ? <Check size={12} strokeWidth={3} /> : <Link2 size={12} />}
</button>
{isSuperAdmin ? (
<button
type="button"
title="Share form"
disabled={actionsDisabled}
style={{
...formHeaderIconButtonStyle(),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setFormShareTarget(form)}
onMouseEnter={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
(e.currentTarget as HTMLElement).style.color = "var(--muted)";
}}
>
<Share2 size={12} />
</button>
) : null}
</div>
}
actions={
<>
{canWriteForms ? (
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setFormEditTarget(form)}
onMouseEnter={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
}}
onMouseLeave={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
}}
>
Edit
</button>
) : null}
{canReadForms ? (
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setFormResponsesTarget(form)}
>
Responses
</button>
) : null}
</>
}
/>
);
}) : null}
{canWriteForms ? (
<ModuleCard
icon={<Plus size={18} />}
title="Create New Form"
description="Add a form assigned to this event"
actions={
<>
<select
disabled={actionsDisabled}
value={newFormType}
onChange={(e) => setNewFormType(e.target.value as typeof newFormType)}
style={{
...moduleButtonStyle(),
flex: "1 1 0",
appearance: "auto",
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
>
{NEW_FORM_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(true),
flex: "0 0 auto",
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setNewFormMode(newFormType)}
>
Create
</button>
</>
}
/>
) : null}
{canReadUserMgmt ? (
<ModuleCard
icon={<ShieldCheck size={18} />}
title="Users & Permissions"
meta={permissionsMeta}
actions={
<>
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setActiveNav("permissions")}
onMouseEnter={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
}}
onMouseLeave={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
}}
>
View
</button>
{canWriteUserMgmt ? (
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => {
setPermissionsOpenAddOnce(true);
setActiveNav("permissions");
}}
>
Add New
</button>
) : null}
</>
}
/>
) : null}
{canReadActivities ? (
<ModuleCard
icon={<BarChart2 size={18} />}
title="Activities"
meta={pollQuizMeta}
actions={
<>
<button
type="button"
disabled={actionsDisabled || !canWriteActivities}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled || !canWriteActivities ? 0.55 : 1,
cursor: actionsDisabled || !canWriteActivities ? "not-allowed" : "pointer",
}}
onClick={() => setShowAddPoll(true)}
onMouseEnter={(e) => {
if (actionsDisabled || !canWriteActivities) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
}}
onMouseLeave={(e) => {
if (actionsDisabled || !canWriteActivities) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
}}
>
Poll
</button>
<button
type="button"
disabled={actionsDisabled || !canWriteActivities}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || !canWriteActivities ? 0.55 : 1,
cursor: actionsDisabled || !canWriteActivities ? "not-allowed" : "pointer",
}}
onClick={() => setShowAddQuiz(true)}
>
Quiz
</button>
</>
}
/>
) : null}
{canReadBadges ? (
<ModuleCard
icon={<Tag size={18} />}
title="Tag & Badge Configuration"
meta={badgeMeta}
actions={
<>
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setBadgesOpen(true)}
onMouseEnter={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
}}
onMouseLeave={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
}}
>
View Badges
</button>
{canWriteBadges ? (
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={openAddBadge}
>
Add Badge
</button>
) : null}
</>
}
/>
) : null}
{canReadTicketing ? (
<ModuleCard
icon={<Ticket size={18} />}
title="Ticket configuration"
meta={ticketMeta}
actions={
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setActiveNav("ticketing")}
>
View
</button>
}
/>
) : null}
<ModuleCard
icon={<Palette size={18} />}
title="Visitor Theme"
description="Colors and background for visitor event, session, quiz, and photo pages"
meta={visitorThemeCardMeta}
actions={
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setVisitorThemeOpen(true)}
>
Manage
</button>
}
/>
{canReadMicrosite ? (
<ModuleCard
icon={<Globe size={18} />}
title="Microsite"
meta={micrositeMeta}
actions={
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setStaticMicrositeOpen(true)}
>
Manage
</button>
}
/>
) : null}
{canReadRewards ? (
<ModuleCard
icon={<Trophy size={18} />}
title="Reward Points Config"
meta={rewardMeta}
actions={
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setRewardRulesOpen(true)}
>
View
</button>
}
/>
) : null}
{canReadWebCheckIn ? (
<ModuleCard
icon={<Plus size={18} />}
title="Web check-in"
meta={webCheckInMeta}
description="Show or hide accommodation choice for participants"
actions={
webCheckInEnabled ? (
<button
type="button"
disabled={actionsDisabled || webCheckInLoading || !canWriteWebCheckIn}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled || webCheckInLoading || !canWriteWebCheckIn ? 0.55 : 1,
cursor:
actionsDisabled || webCheckInLoading || !canWriteWebCheckIn
? "not-allowed"
: "pointer",
}}
onClick={() => void handleWebCheckInToggle(false)}
>
Disable
</button>
) : (
<button
type="button"
disabled={actionsDisabled || webCheckInLoading || !canWriteWebCheckIn}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || webCheckInLoading || !canWriteWebCheckIn ? 0.55 : 1,
cursor:
actionsDisabled || webCheckInLoading || !canWriteWebCheckIn
? "not-allowed"
: "pointer",
}}
onClick={() => void handleWebCheckInToggle(true)}
>
Enable
</button>
)
}
/>
) : null}
{canReadDocuments ? (
<ModuleCard
icon={<Upload size={18} />}
title="Upload Document"
description="Manage documents uploaded for this event"
actions={
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setShowDocumentsPanel(true)}
>
Manage
</button>
}
/>
) : null}
<ModuleCard
icon={<Image size={18} />}
title="Event Photo Search"
meta={photoSearchMeta}
description="Upload event photos and share visitor Find Photos"
headerAction={
photoSearchEnabled && photoSearchToken ? (
<button
type="button"
title="Copy visitor search link"
aria-label="Copy visitor search link"
disabled={actionsDisabled}
style={photoSearchHeaderButtonStyle}
onClick={() => void copyPhotoSearchLink()}
>
{copiedPhotoSearchLink ? <Check size={12} /> : <Link2 size={12} />}
</button>
) : null
}
actions={
<>
{photoSearchEnabled ? (
<button
type="button"
disabled={actionsDisabled || photoSearchLoading}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled || photoSearchLoading ? 0.55 : 1,
cursor: actionsDisabled || photoSearchLoading ? "not-allowed" : "pointer",
}}
onClick={() => void handlePhotoSearchToggle(false)}
>
Disable
</button>
) : (
<button
type="button"
disabled={actionsDisabled || photoSearchLoading}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || photoSearchLoading ? 0.55 : 1,
cursor: actionsDisabled || photoSearchLoading ? "not-allowed" : "pointer",
}}
onClick={() => void handlePhotoSearchToggle(true)}
>
Enable
</button>
)}
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(photoSearchEnabled),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setShowEventPhotoSearchPanel(true)}
>
Manage
</button>
</>
}
/>
{canReadSocialShare ? (
<ModuleCard
icon={<Share2 size={18} />}
title="Social Share"
meta={socialShareMeta}
description="Multi-frame photo create, download, and share"
headerAction={
socialShareEnabled && socialShareToken ? (
<button
type="button"
title="Copy visitor link"
onClick={() => void copySocialShareLink()}
style={formHeaderIconButtonStyle()}
>
{copiedSocialShareLink ? <Check size={14} /> : <Link2 size={14} />}
</button>
) : null
}
actions={
<>
{socialShareEnabled ? (
<button
type="button"
disabled={actionsDisabled || socialShareLoading || !canWriteSocialShare}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled || socialShareLoading || !canWriteSocialShare ? 0.55 : 1,
cursor:
actionsDisabled || socialShareLoading || !canWriteSocialShare
? "not-allowed"
: "pointer",
}}
onClick={() => void handleSocialShareToggle(false)}
>
Disable
</button>
) : (
<button
type="button"
disabled={actionsDisabled || socialShareLoading || !canWriteSocialShare}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || socialShareLoading || !canWriteSocialShare ? 0.55 : 1,
cursor:
actionsDisabled || socialShareLoading || !canWriteSocialShare
? "not-allowed"
: "pointer",
}}
onClick={() => void handleSocialShareToggle(true)}
>
Enable
</button>
)}
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(socialShareEnabled),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setShowSocialSharePanel(true)}
>
Manage
</button>
</>
}
/>
) : null}
{canReadAiAvatar ? (
<ModuleCard
icon={<Sparkles size={18} />}
title="AI Avatar"
meta={aiAvatarMeta}
description="AI head-swap avatars from visitor selfies"
headerAction={
aiAvatarEnabled && aiAvatarToken ? (
<button
type="button"
title="Copy visitor link"
onClick={() => void copyAiAvatarLink()}
style={formHeaderIconButtonStyle()}
>
{copiedAiAvatarLink ? <Check size={14} /> : <Link2 size={14} />}
</button>
) : null
}
actions={
<>
{aiAvatarEnabled ? (
<button
type="button"
disabled={actionsDisabled || aiAvatarLoading || !canWriteAiAvatar}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled || aiAvatarLoading || !canWriteAiAvatar ? 0.55 : 1,
cursor:
actionsDisabled || aiAvatarLoading || !canWriteAiAvatar
? "not-allowed"
: "pointer",
}}
onClick={() => void handleAiAvatarToggle(false)}
>
Disable
</button>
) : (
<button
type="button"
disabled={actionsDisabled || aiAvatarLoading || !canWriteAiAvatar}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || aiAvatarLoading || !canWriteAiAvatar ? 0.55 : 1,
cursor:
actionsDisabled || aiAvatarLoading || !canWriteAiAvatar
? "not-allowed"
: "pointer",
}}
onClick={() => void handleAiAvatarToggle(true)}
>
Enable
</button>
)}
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(aiAvatarEnabled),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setShowAiAvatarPanel(true)}
>
Manage
</button>
</>
}
/>
) : null}
{canReadPhotoWall ? (
<ModuleCard
icon={<Images size={18} />}
title="Photo Wall"
meta={photoWallMeta}
description="Visitor selfie uploads and projector wall"
headerAction={
photoWallEnabled && photoWallUploadToken && photoWallPresentToken ? (
<div style={{ display: "flex", gap: "4px" }}>
<button
type="button"
title="Copy visitor upload link"
onClick={() => void copyPhotoWallLink("upload")}
style={formHeaderIconButtonStyle()}
>
{copiedPhotoWallLink === "upload" ? <Check size={14} /> : <Link2 size={14} />}
</button>
<button
type="button"
title="Copy presenter link"
onClick={() => void copyPhotoWallLink("present")}
style={formHeaderIconButtonStyle()}
>
{copiedPhotoWallLink === "present" ? <Check size={14} /> : <Share2 size={14} />}
</button>
</div>
) : null
}
actions={
<>
{photoWallEnabled ? (
<button
type="button"
disabled={actionsDisabled || photoWallLoading || !canWritePhotoWall}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled || photoWallLoading || !canWritePhotoWall ? 0.55 : 1,
cursor:
actionsDisabled || photoWallLoading || !canWritePhotoWall
? "not-allowed"
: "pointer",
}}
onClick={() => void handlePhotoWallToggle(false)}
>
Disable
</button>
) : (
<button
type="button"
disabled={actionsDisabled || photoWallLoading || !canWritePhotoWall}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || photoWallLoading || !canWritePhotoWall ? 0.55 : 1,
cursor:
actionsDisabled || photoWallLoading || !canWritePhotoWall
? "not-allowed"
: "pointer",
}}
onClick={() => void handlePhotoWallToggle(true)}
>
Enable
</button>
)}
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(photoWallEnabled),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setShowPhotoWallPanel(true)}
>
Manage
</button>
</>
}
/>
) : null}
{canReadWaNotifications ? (
<ModuleCard
icon={<MessageSquare size={18} />}
title="WhatsApp Notifications"
meta={waBlastMeta}
description="Blast a message to this event’s open WhatsApp chats (24-hour window)"
actions={
<>
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setShowWaBlastHistory(true)}
onMouseEnter={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
}}
onMouseLeave={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
}}
>
History
</button>
<button
type="button"
disabled={actionsDisabled || !canWriteWaNotifications}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || !canWriteWaNotifications ? 0.55 : 1,
cursor: actionsDisabled || !canWriteWaNotifications ? "not-allowed" : "pointer",
}}
onClick={() => setShowSendWaNotification(true)}
>
Send
</button>
</>
}
/>
) : null}
{canReadEmailCampaigns ? (
<ModuleCard
icon={<Mail size={18} />}
title="Email Campaign"
meta={emailCampaignMeta}
actions={
<>
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setShowEmailCampaignsPanel(true)}
onMouseEnter={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
}}
onMouseLeave={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
}}
>
Manage
</button>
<button
type="button"
disabled={actionsDisabled || !canWriteEmailCampaigns}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || !canWriteEmailCampaigns ? 0.55 : 1,
cursor: actionsDisabled || !canWriteEmailCampaigns ? "not-allowed" : "pointer",
}}
onClick={() => setShowCreateEmailCampaign(true)}
>
Create
</button>
</>
}
/>
) : null}
{canReadExhibitors ? (
<ModuleCard
icon={<Store size={18} />}
title="Exhibitors"
meta={exhibitorMeta}
description="Companies and booths for this event"
actions={
<>
<button
type="button"
disabled={actionsDisabled}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled ? 0.55 : 1,
cursor: actionsDisabled ? "not-allowed" : "pointer",
}}
onClick={() => setShowExhibitorsPanel(true)}
onMouseEnter={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
}}
onMouseLeave={(e) => {
if (actionsDisabled) return;
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
}}
>
Manage
</button>
<button
type="button"
disabled={actionsDisabled || !canWriteExhibitors}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || !canWriteExhibitors ? 0.55 : 1,
cursor: actionsDisabled || !canWriteExhibitors ? "not-allowed" : "pointer",
}}
onClick={() => {
setEditingExhibitor(null);
setShowAddExhibitor(true);
}}
>
Add exhibitor
</button>
</>
}
/>
) : null}
</section>
{formShareTarget ? (
<ShareFormPanel formId={formShareTarget._id} onClose={() => setFormShareTarget(null)} />
) : null}
{formEditTarget && selectedEventId ? (
<CreateFormPanel
mode={formPanelMode(formEditTarget.type)}
eventId={selectedEventId}
formId={formEditTarget._id}
onClose={() => setFormEditTarget(null)}
onSuccess={handleFormEditSuccess}
/>
) : null}
{formResponsesTarget ? (
<FormSubmissionsPanel
formId={formResponsesTarget._id}
formName={formResponsesTarget.name?.trim() || "Form"}
onClose={() => setFormResponsesTarget(null)}
/>
) : null}
{newFormMode && selectedEventId ? (
<CreateFormPanel
mode={newFormMode}
eventId={selectedEventId}
onClose={() => setNewFormMode(null)}
onSuccess={handleNewFormSuccess}
/>
) : null}
{showAddPoll ? (
<AddQuizPollModal
kind="poll"
eventId={selectedEventId ?? undefined}
onClose={() => setShowAddPoll(false)}
onSave={(data) => void handleSavePollQuiz("poll", data)}
/>
) : null}
{showAddQuiz ? (
<AddQuizPollModal
kind="quiz"
eventId={selectedEventId ?? undefined}
onClose={() => setShowAddQuiz(false)}
onSave={(data) => void handleSavePollQuiz("quiz", data)}
/>
) : null}
{badgesOpen && selectedEventId ? (
<EventBadgesPanel
eventId={selectedEventId}
eventName={eventName ?? undefined}
onClose={() => setBadgesOpen(false)}
onAddBadge={() => {
setBadgesOpen(false);
openAddBadge();
}}
onDuplicateBadge={openDuplicateBadge}
/>
) : null}
{showAddBadge && selectedEventId ? (
<CreateBadgePanel
key={duplicateBadgeBatchId ?? "create"}
initialEventId={selectedEventId}
duplicateFromBatchId={duplicateBadgeBatchId}
onClose={() => {
setShowAddBadge(false);
setDuplicateBadgeBatchId(null);
}}
onSuccess={handleBadgeSuccess}
/>
) : null}
{rewardRulesOpen && selectedEventId ? (
<RewardRulesSettingsPanel
eventId={selectedEventId}
onClose={() => setRewardRulesOpen(false)}
onSaved={() => void loadRewardRulesMeta()}
/>
) : null}
{visitorThemeOpen && selectedEventId ? (
<VisitorThemeSettingsPanel
eventId={selectedEventId}
onClose={() => setVisitorThemeOpen(false)}
onSaved={() => void loadVisitorThemeMeta()}
/>
) : null}
{staticMicrositeOpen && selectedEventId ? (
<ManageStaticMicrositePanel
eventId={selectedEventId}
onClose={() => setStaticMicrositeOpen(false)}
onSaved={() => void loadStaticMicrositeMeta()}
/>
) : null}
{showDocumentsPanel && selectedEventId ? (
<EventDocumentsPanel
eventId={selectedEventId}
eventName={eventName ?? undefined}
onClose={() => setShowDocumentsPanel(false)}
/>
) : null}
{showEventPhotoSearchPanel && selectedEventId ? (
<EventPhotoSearchPanel
open
eventId={selectedEventId}
eventName={eventName ?? undefined}
onClose={() => setShowEventPhotoSearchPanel(false)}
onChanged={() => void loadPhotoSearchMeta()}
/>
) : null}
{showPhotoWallPanel && selectedEventId ? (
<PhotoWallManagePanel
eventId={selectedEventId}
eventName={eventName ?? undefined}
canWrite={canWritePhotoWall}
onClose={() => setShowPhotoWallPanel(false)}
onChanged={() => void loadPhotoWallMeta()}
/>
) : null}
{showSocialSharePanel && selectedEventId ? (
<SocialShareManagePanel
eventId={selectedEventId}
eventName={eventName ?? undefined}
canWrite={canWriteSocialShare}
onClose={() => setShowSocialSharePanel(false)}
onChanged={() => void loadSocialShareMeta()}
/>
) : null}
{showAiAvatarPanel && selectedEventId ? (
<AiAvatarManagePanel
eventId={selectedEventId}
eventName={eventName ?? undefined}
canWrite={canWriteAiAvatar}
onClose={() => setShowAiAvatarPanel(false)}
onChanged={() => void loadAiAvatarMeta()}
/>
) : null}
{showWaBlastHistory && selectedEventId ? (
<WhatsAppBlastHistoryPanel
eventId={selectedEventId}
eventName={eventName ?? undefined}
onClose={() => setShowWaBlastHistory(false)}
/>
) : null}
{showSendWaNotification && selectedEventId ? (
<SendWhatsAppNotificationPanel
eventId={selectedEventId}
eventName={eventName ?? undefined}
onClose={() => setShowSendWaNotification(false)}
onSuccess={() => void loadWaBlastMeta()}
/>
) : null}
{showEmailCampaignsPanel && selectedEventId ? (
<EventEmailCampaignsPanel
eventId={selectedEventId}
eventName={eventName ?? undefined}
onClose={() => setShowEmailCampaignsPanel(false)}
onChanged={() => void loadEmailCampaignMeta()}
/>
) : null}
{showCreateEmailCampaign && selectedEventId ? (
<CreateEmailCampaignPanel
eventId={selectedEventId}
onClose={() => setShowCreateEmailCampaign(false)}
onSuccess={() => void loadEmailCampaignMeta()}
/>
) : null}
{showExhibitorsPanel && selectedEventId ? (
<EventExhibitorsPanel
eventId={selectedEventId}
eventName={eventName ?? undefined}
canWrite={canWriteExhibitors}
onClose={() => setShowExhibitorsPanel(false)}
onAdd={() => {
setShowExhibitorsPanel(false);
setEditingExhibitor(null);
setShowAddExhibitor(true);
}}
onEdit={(ex) => {
setShowExhibitorsPanel(false);
setEditingExhibitor(ex);
setShowAddExhibitor(true);
}}
onChanged={() => void loadExhibitorMeta()}
/>
) : null}
{showAddExhibitor && selectedEventId ? (
<AddExhibitorPanel
key={editingExhibitor?._id ?? "create"}
eventId={selectedEventId}
eventName={eventName ?? undefined}
exhibitor={editingExhibitor}
onClose={() => {
setShowAddExhibitor(false);
setEditingExhibitor(null);
}}
onSuccess={() => {
void loadExhibitorMeta();
setShowExhibitorsPanel(true);
}}
/>
) : null}
</>
);
}