Update WebCheck-in

This commit is contained in:
Tanuja Pansare 2026-07-08 10:25:38 +05:30
parent 980e0d9156
commit 6e7665a545
4 changed files with 156 additions and 17 deletions

View File

@ -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}

View File

@ -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)} />

View File

@ -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 && (

View File

@ -403,6 +403,7 @@ export interface Participant {
checkedIn?: boolean;
checkedInAt?: string;
notes?: string;
accommodation?: boolean;
tags?: string[];
tenantOrganization?: string;
eventMemberships?: unknown[];