Update WebCheck-in
This commit is contained in:
parent
980e0d9156
commit
6e7665a545
|
|
@ -132,14 +132,16 @@ function moduleButtonStyle(primary = false): React.CSSProperties {
|
|||
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);
|
||||
}
|
||||
|
||||
function formPanelMode(apiType: string): "feedback" | "registration" | "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";
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +163,7 @@ export function EventHomeModules() {
|
|||
const [eventForms, setEventForms] = useState<FormWithSubmissionCount[]>([]);
|
||||
const [formEditTarget, setFormEditTarget] = useState<FormWithSubmissionCount | null>(null);
|
||||
const [formResponsesTarget, setFormResponsesTarget] = useState<FormWithSubmissionCount | null>(null);
|
||||
const [newFormMode, setNewFormMode] = useState<"registration" | "feedback" | "other" | null>(null);
|
||||
const [newFormMode, setNewFormMode] = useState<"registration" | "feedback" | "other" | "web-check-in" | null>(null);
|
||||
const [pollQuizCounts, setPollQuizCounts] = useState({ polls: 0, quizzes: 0 });
|
||||
const [pollQuizLoading, setPollQuizLoading] = useState(false);
|
||||
const [showAddPoll, setShowAddPoll] = useState(false);
|
||||
|
|
@ -181,6 +183,9 @@ export function EventHomeModules() {
|
|||
const [rewardRulesOpen, setRewardRulesOpen] = 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);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedEventId) {
|
||||
|
|
@ -191,6 +196,8 @@ export function EventHomeModules() {
|
|||
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;
|
||||
|
|
@ -419,6 +426,28 @@ export function EventHomeModules() {
|
|||
: agendaLoading
|
||||
? "Loading…"
|
||||
: `${agendaCounts.tracks} track${agendaCounts.tracks === 1 ? "" : "s"} · ${agendaCounts.sessions} session${agendaCounts.sessions === 1 ? "" : "s"}`;
|
||||
const webCheckInMeta = !selectedEventId
|
||||
? "Select an event to configure web check-in"
|
||||
: webCheckInLoading
|
||||
? "Saving…"
|
||||
: webCheckInEnabled
|
||||
? "Enabled"
|
||||
: "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 handleSavePollQuiz = async (
|
||||
kind: "poll" | "quiz",
|
||||
|
|
@ -715,6 +744,40 @@ export function EventHomeModules() {
|
|||
</button>
|
||||
}
|
||||
/>
|
||||
<ModuleCard
|
||||
icon={<Plus size={18} />}
|
||||
title="Web check-in"
|
||||
meta={webCheckInMeta}
|
||||
description="Show or hide accommodation choice for participants"
|
||||
actions={
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
disabled={actionsDisabled || webCheckInLoading}
|
||||
style={{
|
||||
...moduleButtonStyle(),
|
||||
opacity: actionsDisabled || webCheckInLoading ? 0.55 : 1,
|
||||
cursor: actionsDisabled || webCheckInLoading ? "not-allowed" : "pointer",
|
||||
}}
|
||||
onClick={() => void handleWebCheckInToggle(true)}
|
||||
>
|
||||
Enable
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={actionsDisabled || webCheckInLoading}
|
||||
style={{
|
||||
...moduleButtonStyle(true),
|
||||
opacity: actionsDisabled || webCheckInLoading ? 0.55 : 1,
|
||||
cursor: actionsDisabled || webCheckInLoading ? "not-allowed" : "pointer",
|
||||
}}
|
||||
onClick={() => void handleWebCheckInToggle(false)}
|
||||
>
|
||||
Disable
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ModuleCard
|
||||
icon={<Plus size={18} />}
|
||||
title="Create New Form"
|
||||
|
|
@ -765,6 +828,18 @@ export function EventHomeModules() {
|
|||
>
|
||||
Feedback
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={actionsDisabled}
|
||||
style={{
|
||||
...moduleButtonStyle(),
|
||||
opacity: actionsDisabled ? 0.55 : 1,
|
||||
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
||||
}}
|
||||
onClick={() => setNewFormMode("web-check-in")}
|
||||
>
|
||||
Web check-in
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={actionsDisabled}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { X, ChevronDown, ChevronUp, Check } from "lucide-react";
|
||||
import { participantsApi } from "../../../services/api";
|
||||
import { eventsApi, participantsApi } from "../../../services/api";
|
||||
import type { Participant } from "../../../services/api/types";
|
||||
|
||||
const IS: React.CSSProperties = {
|
||||
|
|
@ -131,6 +131,7 @@ interface ParticipantForm {
|
|||
phone: string;
|
||||
organization: string;
|
||||
designation: string;
|
||||
accommodation: "Yes" | "No";
|
||||
notes: string;
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +142,7 @@ const INITIAL: ParticipantForm = {
|
|||
phone: "",
|
||||
organization: "",
|
||||
designation: "",
|
||||
accommodation: "No",
|
||||
notes: "",
|
||||
};
|
||||
|
||||
|
|
@ -182,6 +184,18 @@ export function AddParticipantPanel({
|
|||
const [submitting, setSubmitting] = useState(false);
|
||||
const [fieldErrors, setFieldErrors] = useState<Partial<Record<FieldKey, string>>>({});
|
||||
const [saveErr, setSaveErr] = useState<string | null>(null);
|
||||
const [requiresAccommodation, setRequiresAccommodation] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void eventsApi.get(eventId).then((res) => {
|
||||
if (cancelled) return;
|
||||
setRequiresAccommodation(Boolean((res.data?.settings as Record<string, unknown> | undefined)?.webCheckInEnabled));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [eventId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!participantId) return;
|
||||
|
|
@ -195,6 +209,7 @@ export function AddParticipantPanel({
|
|||
phone: d.phone ?? "",
|
||||
organization: d.organization ?? "",
|
||||
designation: d.designation ?? "",
|
||||
accommodation: d.accommodation ? "Yes" : "No",
|
||||
notes: d.notes ?? "",
|
||||
});
|
||||
setPassedSections(new Set([1, 2, 3]));
|
||||
|
|
@ -260,6 +275,7 @@ export function AddParticipantPanel({
|
|||
phone: form.phone.trim() || undefined,
|
||||
organization: form.organization.trim() || undefined,
|
||||
designation: form.designation.trim() || undefined,
|
||||
accommodation: form.accommodation === "Yes",
|
||||
notes: form.notes.trim() || undefined,
|
||||
};
|
||||
const res = isEdit && participantId ? await participantsApi.update(participantId, payload) : await participantsApi.create(eventId, payload);
|
||||
|
|
@ -412,6 +428,37 @@ export function AddParticipantPanel({
|
|||
nextLabel="Save participant"
|
||||
nextDisabled={submitting}
|
||||
>
|
||||
{requiresAccommodation ? (
|
||||
<Field>
|
||||
<Label text="Accommodation" />
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
{(["Yes", "No"] as const).map((option) => {
|
||||
const active = form.accommodation === option;
|
||||
return (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
onClick={() => set("accommodation", option)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "8px 12px",
|
||||
borderRadius: "var(--radius-button)",
|
||||
border: active ? "1px solid var(--primary)" : "1px solid var(--border)",
|
||||
background: active ? "color-mix(in srgb, var(--primary) 12%, transparent)" : "var(--card)",
|
||||
color: active ? "var(--primary)" : "var(--card-foreground)",
|
||||
cursor: "pointer",
|
||||
fontSize: "var(--text-sm)",
|
||||
fontFamily: "var(--font-noto-sans)",
|
||||
fontWeight: "var(--font-weight-medium)",
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Field>
|
||||
) : null}
|
||||
<Field>
|
||||
<Label text="Notes" />
|
||||
<textarea style={{ ...IS, resize: "vertical", minHeight: "80px" }} placeholder="Internal notes (optional)" value={form.notes} onChange={(e) => set("notes", e.target.value)} />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* CreateFormPanel — shared by Feedback, Registration, and Other forms.
|
||||
* Pass mode="feedback" | "registration" | "other", eventId, optional formId, onSuccess.
|
||||
* CreateFormPanel — shared by Feedback, Registration, Other, and Web check-in forms.
|
||||
* Pass mode="feedback" | "registration" | "other" | "web-check-in", eventId, optional formId, onSuccess.
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Plus, Trash2, GripVertical } from "lucide-react";
|
||||
|
|
@ -9,7 +9,7 @@ import { useAuth } from "../../contexts/AuthContext";
|
|||
import { eventsApi, formsApi, participantListsApi, portfolioListOrganizationId } from "../../../services/api";
|
||||
import type { Form, FormField, ParticipantListRecord } from "../../../services/api";
|
||||
|
||||
type FormMode = "feedback" | "registration" | "other";
|
||||
type FormMode = "feedback" | "registration" | "other" | "web-check-in";
|
||||
|
||||
function eventIdFromFormRef(ev: Form["event"] | undefined): string {
|
||||
if (!ev) return "";
|
||||
|
|
@ -113,6 +113,15 @@ function initialRegQuestions(): Question[] {
|
|||
];
|
||||
}
|
||||
|
||||
function initialWebCheckInQuestions(): Question[] {
|
||||
return [
|
||||
{ id: "wc-core-fn", label: "First name", type: "Text", required: "Yes", mapsTo: "firstName", fixed: true },
|
||||
{ id: "wc-core-ln", label: "Last name", type: "Text", required: "Yes", mapsTo: "lastName", fixed: true },
|
||||
{ id: "wc-core-em", label: "Email", type: "Email", required: "Yes", mapsTo: "email", fixed: true },
|
||||
{ id: "wc-doc", label: "Upload document", type: "File Upload", required: "Yes", fixed: true },
|
||||
];
|
||||
}
|
||||
|
||||
function QuestionRow({ q, types, onPatch, onRemove, idx, canRemove }: {
|
||||
q: Question;
|
||||
types: string[];
|
||||
|
|
@ -430,14 +439,16 @@ export function CreateFormPanel({
|
|||
const isFeedback = mode === "feedback";
|
||||
const isReg = mode === "registration";
|
||||
const isOther = mode === "other";
|
||||
const isWebCheckIn = mode === "web-check-in";
|
||||
const TOTAL = isReg ? 5 : 3;
|
||||
const qTypes = isReg ? REG_FIELD_TYPES : isOther ? OTHER_Q_TYPES : FEEDBACK_Q_TYPES;
|
||||
const isFeedbackOrOther = isFeedback || isOther;
|
||||
const qTypes = isReg || isWebCheckIn ? OTHER_Q_TYPES : isOther ? OTHER_Q_TYPES : FEEDBACK_Q_TYPES;
|
||||
const isFeedbackOrOther = isFeedback || isOther || isWebCheckIn;
|
||||
const acc = useAccordion(TOTAL);
|
||||
|
||||
const [form, setForm] = useState<FForm>(() => {
|
||||
const base = initialForm(qTypes, isFeedbackOrOther);
|
||||
if (isReg) return { ...base, questions: initialRegQuestions() };
|
||||
if (isWebCheckIn) return { ...base, questions: initialWebCheckInQuestions() };
|
||||
return base;
|
||||
});
|
||||
const [fixedLists, setFixedLists] = useState<ParticipantListRecord[]>([]);
|
||||
|
|
@ -535,7 +546,7 @@ export function CreateFormPanel({
|
|||
const validateSection = useCallback((f: FForm, n: number): boolean => {
|
||||
if (n === 1) return f.name.trim().length > 0 && selectedEventId.trim().length > 0;
|
||||
if (n === 2) {
|
||||
if (isReg) {
|
||||
if (isReg || isWebCheckIn) {
|
||||
const coreOk = f.questions.filter((q) => q.fixed).every((q) => q.label.trim().length > 0);
|
||||
const extras = f.questions.filter((q) => !q.fixed);
|
||||
const extrasOk = extras.every((q) => {
|
||||
|
|
@ -554,7 +565,7 @@ export function CreateFormPanel({
|
|||
if (n === 4 && isReg) return true;
|
||||
if (n === (isReg ? 5 : 3)) return true;
|
||||
return true;
|
||||
}, [isReg, selectedEventId]);
|
||||
}, [isReg, isWebCheckIn, selectedEventId]);
|
||||
|
||||
const tryAdvance = useCallback((n: number) => {
|
||||
const valid = validateSection(form, n);
|
||||
|
|
@ -563,7 +574,7 @@ export function CreateFormPanel({
|
|||
}, [form, validateSection, acc]);
|
||||
|
||||
const buildPayload = useCallback(() => {
|
||||
const apiType = isReg ? "registration" : isOther ? "other" : "feedback";
|
||||
const apiType = isReg ? "registration" : isOther ? "other" : isWebCheckIn ? "web-check-in" : "feedback";
|
||||
const fields = isReg
|
||||
? form.questions.filter(q => q.label.trim()).map(q => toApiFieldReg(q))
|
||||
: form.questions.filter(q => q.label.trim()).map(q => toApiField(q, isFeedbackOrOther));
|
||||
|
|
@ -591,7 +602,7 @@ export function CreateFormPanel({
|
|||
: {}),
|
||||
},
|
||||
};
|
||||
}, [form, isReg, isOther, isFeedbackOrOther]);
|
||||
}, [form, isReg, isOther, isWebCheckIn, isFeedbackOrOther]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!form.name.trim() || !selectedEventId.trim()) {
|
||||
|
|
@ -626,14 +637,16 @@ export function CreateFormPanel({
|
|||
handleSave();
|
||||
}, [form, isReg, validateSection, handleSave]);
|
||||
|
||||
const breadcrumb = isFeedback ? "Forms & Workflow · Feedback" : isReg ? "Forms & Workflow · Registration" : "Forms & Workflow · Other Forms";
|
||||
const breadcrumb = isFeedback ? "Forms & Workflow · Feedback" : isReg ? "Forms & Workflow · Registration" : isWebCheckIn ? "Forms & Workflow · Web check-in" : "Forms & Workflow · Other Forms";
|
||||
const title = formId
|
||||
? (isFeedback ? "Edit Feedback Form" : isReg ? "Edit Registration Form" : "Edit Form")
|
||||
: (isFeedback ? "Create Feedback Form" : isReg ? "Create Registration Form" : "Create Other Form");
|
||||
? (isFeedback ? "Edit Feedback Form" : isReg ? "Edit Registration Form" : isWebCheckIn ? "Edit Web check-in Form" : "Edit Form")
|
||||
: (isFeedback ? "Create Feedback Form" : isReg ? "Create Registration Form" : isWebCheckIn ? "Create Web check-in Form" : "Create Other Form");
|
||||
const panelDescription = isFeedback
|
||||
? "Design a feedback form to collect responses from your attendees."
|
||||
: isReg
|
||||
? "Build a registration form; each submission creates or updates a participant."
|
||||
: isWebCheckIn
|
||||
? "Create a web check-in form with the required personal details and document upload fields."
|
||||
: "Build custom forms (other / general-purpose) for your event.";
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -701,7 +714,7 @@ export function CreateFormPanel({
|
|||
<Label text="Description" />
|
||||
<textarea style={{ ...IS, resize: "vertical", minHeight: "72px" }} placeholder="Briefly describe this form…" value={form.description} onChange={e => set("description", e.target.value)} onFocus={focusIn} onBlur={focusOut} />
|
||||
</Field>
|
||||
{isFeedbackOrOther && (
|
||||
{isFeedbackOrOther && !isWebCheckIn && (
|
||||
<Grid cols={2}>
|
||||
<Field>
|
||||
<Label text="Anonymous Responses" required />
|
||||
|
|
@ -727,6 +740,9 @@ export function CreateFormPanel({
|
|||
nextLabel={isReg ? "Participant actions" : "Settings"}
|
||||
sectionError={sectionErrors[2]}
|
||||
>
|
||||
{isWebCheckIn ? (
|
||||
<HelperText text="This form starts with First name, Last name, Email, and Upload document fields, and you can add more questions below." />
|
||||
) : null}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
{isReg ? (
|
||||
<>
|
||||
|
|
@ -754,7 +770,7 @@ export function CreateFormPanel({
|
|||
<Plus size={13} /> Add {isFeedbackOrOther ? "Question" : "Field"}
|
||||
</button>
|
||||
)}
|
||||
<HelperText text={isReg ? "Submissions must include the email field. Map extra fields to participant columns or customFields." : "Drag the grip icon to reorder questions."} />
|
||||
<HelperText text={isReg ? "Submissions must include the email field. Map extra fields to participant columns or customFields." : isWebCheckIn ? "The required core fields are locked in place; add more questions only if needed." : "Drag the grip icon to reorder questions."} />
|
||||
</AccordionSection>
|
||||
|
||||
{isReg && (
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@ export interface Participant {
|
|||
checkedIn?: boolean;
|
||||
checkedInAt?: string;
|
||||
notes?: string;
|
||||
accommodation?: boolean;
|
||||
tags?: string[];
|
||||
tenantOrganization?: string;
|
||||
eventMemberships?: unknown[];
|
||||
|
|
|
|||
Loading…
Reference in New Issue