2187 lines
78 KiB
TypeScript
2187 lines
78 KiB
TypeScript
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}
|
||
</>
|
||
);
|
||
}
|