@burdenoff/microfe-atheletebridge 2026.1031.3 → 2026.1102.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atheletebridge/AthleteBridgeRoot.d.ts.map +1 -1
- package/dist/atheletebridge/AthleteBridgeRoot.js +63 -47
- package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
- package/dist/atheletebridge/AthleteBridgeRoutes.d.ts.map +1 -1
- package/dist/atheletebridge/AthleteBridgeRoutes.js +62 -6
- package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
- package/dist/atheletebridge/components/StatusBadge.d.ts.map +1 -1
- package/dist/atheletebridge/components/StatusBadge.js +13 -1
- package/dist/atheletebridge/components/StatusBadge.js.map +1 -1
- package/dist/atheletebridge/components/registration/PaymentSummaryPanel.d.ts +6 -0
- package/dist/atheletebridge/components/registration/PaymentSummaryPanel.d.ts.map +1 -0
- package/dist/atheletebridge/components/registration/PaymentSummaryPanel.js +104 -0
- package/dist/atheletebridge/components/registration/PaymentSummaryPanel.js.map +1 -0
- package/dist/atheletebridge/components/registration/RegistrationEntryFields.d.ts +15 -0
- package/dist/atheletebridge/components/registration/RegistrationEntryFields.d.ts.map +1 -0
- package/dist/atheletebridge/components/registration/RegistrationEntryFields.js +236 -0
- package/dist/atheletebridge/components/registration/RegistrationEntryFields.js.map +1 -0
- package/dist/atheletebridge/components/registration/entryRules.d.ts +23 -0
- package/dist/atheletebridge/components/registration/entryRules.d.ts.map +1 -0
- package/dist/atheletebridge/components/registration/entryRules.js +29 -0
- package/dist/atheletebridge/components/registration/entryRules.js.map +1 -0
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts +1 -0
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
- package/dist/atheletebridge/hooks/useRegistrationApi.d.ts +54 -0
- package/dist/atheletebridge/hooks/useRegistrationApi.d.ts.map +1 -0
- package/dist/atheletebridge/hooks/useRegistrationApi.js +265 -0
- package/dist/atheletebridge/hooks/useRegistrationApi.js.map +1 -0
- package/dist/atheletebridge/pages/registrations/MyRegistrationsPage.d.ts +10 -0
- package/dist/atheletebridge/pages/registrations/MyRegistrationsPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/registrations/MyRegistrationsPage.js +89 -0
- package/dist/atheletebridge/pages/registrations/MyRegistrationsPage.js.map +1 -0
- package/dist/atheletebridge/pages/registrations/RecordRegistrationPage.d.ts +10 -0
- package/dist/atheletebridge/pages/registrations/RecordRegistrationPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/registrations/RecordRegistrationPage.js +94 -0
- package/dist/atheletebridge/pages/registrations/RecordRegistrationPage.js.map +1 -0
- package/dist/atheletebridge/pages/registrations/RegisterPage.d.ts +11 -0
- package/dist/atheletebridge/pages/registrations/RegisterPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/registrations/RegisterPage.js +160 -0
- package/dist/atheletebridge/pages/registrations/RegisterPage.js.map +1 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormBuilderPage.d.ts +10 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormBuilderPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormBuilderPage.js +602 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormBuilderPage.js.map +1 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormDetailPage.d.ts +9 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormDetailPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormDetailPage.js +317 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormDetailPage.js.map +1 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormsListPage.d.ts +9 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormsListPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormsListPage.js +127 -0
- package/dist/atheletebridge/pages/registrations/RegistrationFormsListPage.js.map +1 -0
- package/dist/atheletebridge/pages/registrations/RegistrationSubmissionPage.d.ts +11 -0
- package/dist/atheletebridge/pages/registrations/RegistrationSubmissionPage.d.ts.map +1 -0
- package/dist/atheletebridge/pages/registrations/RegistrationSubmissionPage.js +411 -0
- package/dist/atheletebridge/pages/registrations/RegistrationSubmissionPage.js.map +1 -0
- package/dist/atheletebridge/types/registration.d.ts +220 -0
- package/dist/atheletebridge/types/registration.d.ts.map +1 -0
- package/dist/atheletebridge/utils/money.d.ts +22 -0
- package/dist/atheletebridge/utils/money.d.ts.map +1 -0
- package/dist/atheletebridge/utils/money.js +38 -0
- package/dist/atheletebridge/utils/money.js.map +1 -0
- package/dist/atheletebridge/utils/registration.d.ts +17 -0
- package/dist/atheletebridge/utils/registration.d.ts.map +1 -0
- package/dist/atheletebridge/utils/registration.js +25 -0
- package/dist/atheletebridge/utils/registration.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegistrationFormBuilderPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/registrations/RegistrationFormBuilderPage.tsx"],"sourcesContent":["/**\n * Registration form builder (create / edit)\n * @module atheletebridge/pages/registrations\n *\n * Club staff define the form's details, its questions and its fees\n * (BOFF-4135). Questions, fees and currency lock once the first registration\n * arrives — the server enforces the same rule.\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowDown, ArrowLeft, ArrowUp, Loader2, Plus, Save, Trash2 } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useTeams } from '../../hooks/useAthleteBridgeApi';\nimport {\n useCreateRegistrationForm,\n useRegistrationForm,\n useUpdateRegistrationForm,\n} from '../../hooks/useRegistrationApi';\nimport { minorToInput, parseMoney } from '../../utils/money';\nimport { keyFromLabel } from '../../utils/registration';\nimport type {\n AthRegistrationFeeInput,\n AthRegistrationFeeType,\n AthRegistrationFieldInput,\n AthRegistrationFieldType,\n AthRegistrationForm,\n} from '../../types/registration';\n\nconst FIELD_TYPE_LABELS: Record<AthRegistrationFieldType, string> = {\n SHORT_TEXT: 'Short text',\n LONG_TEXT: 'Paragraph',\n EMAIL: 'Email',\n PHONE: 'Phone',\n NUMBER: 'Number',\n DATE: 'Date',\n SELECT: 'Dropdown',\n CHECKBOX: 'Checkbox (yes / consent)',\n};\n\nconst CURRENCIES = ['USD', 'INR', 'GBP', 'EUR', 'AUD', 'CAD', 'SGD', 'AED'] as const;\n\ninterface QuestionDraft {\n uid: string;\n key: string;\n label: string;\n fieldType: AthRegistrationFieldType;\n required: boolean;\n optionsText: string;\n helpText: string;\n}\n\ninterface FeeDraft {\n uid: string;\n name: string;\n amountText: string;\n feeType: AthRegistrationFeeType;\n description: string;\n}\n\ninterface FormDraft {\n title: string;\n description: string;\n season: string;\n teamId: string;\n slug: string;\n currency: string;\n opensAt: string;\n closesAt: string;\n maxSubmissions: string;\n waiverText: string;\n questions: QuestionDraft[];\n fees: FeeDraft[];\n}\n\n// Row identities only need to be unique within the page; a random UUID stays\n// unique across HMR reloads and Strict Mode double renders, unlike a counter.\nconst nextUid = () => `draft-${crypto.randomUUID()}`;\n\nconst EMPTY_DRAFT: FormDraft = {\n title: '',\n description: '',\n season: '',\n teamId: '',\n slug: '',\n currency: 'USD',\n opensAt: '',\n closesAt: '',\n maxSubmissions: '',\n waiverText: '',\n questions: [],\n fees: [],\n};\n\n/** `datetime-local` value for an ISO instant (in the viewer's timezone). */\nfunction toLocalInput(iso: string | null): string {\n if (!iso) return '';\n const date = new Date(iso);\n const offset = date.getTimezoneOffset() * 60_000;\n return new Date(date.getTime() - offset).toISOString().slice(0, 16);\n}\n\nfunction fromLocalInput(value: string): string | undefined {\n return value ? new Date(value).toISOString() : undefined;\n}\n\nfunction draftFromForm(form: AthRegistrationForm): FormDraft {\n return {\n title: form.title,\n description: form.description ?? '',\n season: form.season ?? '',\n teamId: form.teamId ?? '',\n slug: form.slug,\n currency: form.currency,\n opensAt: toLocalInput(form.opensAt),\n closesAt: toLocalInput(form.closesAt),\n maxSubmissions: form.maxSubmissions != null ? String(form.maxSubmissions) : '',\n waiverText: form.waiverText ?? '',\n questions: form.fields.map((field) => ({\n uid: nextUid(),\n key: field.key,\n label: field.label,\n fieldType: field.fieldType,\n required: field.required,\n optionsText: field.options.join(', '),\n helpText: field.helpText ?? '',\n })),\n fees: form.fees.map((fee) => ({\n uid: nextUid(),\n name: fee.name,\n amountText: minorToInput(fee.amount, form.currency),\n feeType: fee.feeType,\n description: fee.description ?? '',\n })),\n };\n}\n\nfunction move<T>(list: T[], index: number, delta: number): T[] {\n const target = index + delta;\n if (target < 0 || target >= list.length) return list;\n const next = [...list];\n const [item] = next.splice(index, 1);\n if (item !== undefined) next.splice(target, 0, item);\n return next;\n}\n\nconst iconButton =\n 'inline-flex h-8 w-8 items-center justify-center rounded-md border border-border-seam text-text-secondary transition-colors hover:bg-bg-sunken disabled:opacity-40';\n\nexport function RegistrationFormBuilderPage() {\n const { id } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigateTo } = useAthleteBridge();\n\n const existing = useRegistrationForm(id);\n const teams = useTeams({ pagination: { limit: 100 } });\n const createForm = useCreateRegistrationForm();\n const updateForm = useUpdateRegistrationForm();\n\n const [draft, setDraft] = useState<FormDraft>(EMPTY_DRAFT);\n const [error, setError] = useState<string | null>(null);\n\n // Seed the draft once per form. A background refetch (window focus, or a new\n // registration changing the submission count) must not overwrite edits the\n // user has not saved yet; the structural lock below still reads live data.\n const seededFormId = useRef<string | null>(null);\n useEffect(() => {\n if (isEdit && existing.data && seededFormId.current !== existing.data.id) {\n seededFormId.current = existing.data.id;\n setDraft(draftFromForm(existing.data));\n }\n }, [isEdit, existing.data]);\n\n const locked = isEdit && (existing.data?.submissionCount ?? 0) > 0;\n const set = <K extends keyof FormDraft>(key: K, value: FormDraft[K]) =>\n setDraft((prev) => ({ ...prev, [key]: value }));\n const setQuestion = (uid: string, patch: Partial<QuestionDraft>) =>\n set(\n 'questions',\n draft.questions.map((q) => (q.uid === uid ? { ...q, ...patch } : q))\n );\n const setFee = (uid: string, patch: Partial<FeeDraft>) =>\n set(\n 'fees',\n draft.fees.map((f) => (f.uid === uid ? { ...f, ...patch } : f))\n );\n\n const buildStructure = ():\n { fields: AthRegistrationFieldInput[]; fees: AthRegistrationFeeInput[] } | string => {\n // Reserve every saved question's key first, so a new question placed\n // above an existing one cannot generate a key that collides with it.\n const taken = new Set<string>(draft.questions.map((q) => q.key).filter(Boolean));\n const fields: AthRegistrationFieldInput[] = [];\n for (const question of draft.questions) {\n const label = question.label.trim();\n if (!label) return 'Every question needs a label.';\n const key = question.key || keyFromLabel(label, taken);\n taken.add(key);\n const options = question.optionsText\n .split(',')\n .map((o) => o.trim())\n .filter(Boolean);\n if (question.fieldType === 'SELECT' && options.length === 0) {\n return `Add at least one option to the dropdown \"${label}\".`;\n }\n fields.push({\n key,\n label,\n fieldType: question.fieldType,\n required: question.required,\n options: question.fieldType === 'SELECT' ? options : undefined,\n helpText: question.helpText.trim() || undefined,\n });\n }\n const fees: AthRegistrationFeeInput[] = [];\n for (const fee of draft.fees) {\n const name = fee.name.trim();\n if (!name) return 'Every fee needs a name.';\n const amount = parseMoney(fee.amountText, draft.currency);\n if (amount === null) return `Enter a valid amount for \"${name}\".`;\n fees.push({\n name,\n amount,\n feeType: fee.feeType,\n description: fee.description.trim() || undefined,\n });\n }\n return { fields, fees };\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!draft.title.trim()) {\n setError('Give the form a title.');\n return;\n }\n const maxSubmissions = draft.maxSubmissions.trim()\n ? Number.parseInt(draft.maxSubmissions, 10)\n : undefined;\n if (maxSubmissions !== undefined && (Number.isNaN(maxSubmissions) || maxSubmissions < 1)) {\n setError('Capacity must be a positive number.');\n return;\n }\n const structure = locked ? null : buildStructure();\n if (typeof structure === 'string') {\n setError(structure);\n return;\n }\n\n try {\n if (isEdit && id) {\n await updateForm.mutateAsync({\n id,\n input: {\n title: draft.title.trim(),\n description: draft.description.trim() || null,\n season: draft.season.trim() || null,\n teamId: draft.teamId || null,\n opensAt: fromLocalInput(draft.opensAt) ?? null,\n closesAt: fromLocalInput(draft.closesAt) ?? null,\n maxSubmissions: maxSubmissions ?? null,\n waiverText: draft.waiverText.trim() || null,\n ...(structure\n ? { currency: draft.currency, fields: structure.fields, fees: structure.fees }\n : {}),\n },\n });\n navigateTo(`/registrations/${id}`);\n } else if (structure) {\n const created = await createForm.mutateAsync({\n title: draft.title.trim(),\n description: draft.description.trim() || undefined,\n season: draft.season.trim() || undefined,\n teamId: draft.teamId || undefined,\n slug: draft.slug.trim() || undefined,\n currency: draft.currency,\n opensAt: fromLocalInput(draft.opensAt),\n closesAt: fromLocalInput(draft.closesAt),\n maxSubmissions,\n waiverText: draft.waiverText.trim() || undefined,\n fields: structure.fields,\n fees: structure.fees,\n });\n navigateTo(`/registrations/${created.id}`);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to save the registration form.');\n }\n };\n\n const saving = createForm.isPending || updateForm.isPending;\n const title = isEdit ? 'Edit registration form' : 'New registration form';\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title={title}>\n <LoadingState label=\"Loading registration form…\" />\n </PageLayout>\n );\n }\n if (isEdit && (existing.isError || !existing.data)) {\n return (\n <PageLayout title={title}>\n <ErrorState\n title=\"Registration form not found\"\n message=\"This form does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/registrations')}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={title}\n description=\"Set the details, the questions families answer and the fees they pay.\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && id ? `/registrations/${id}` : '/registrations')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"max-w-3xl space-y-8\">\n <section className=\"space-y-5\">\n <FormField label=\"Form title\" htmlFor=\"title\" required>\n <input\n id=\"title\"\n className={fieldInputClass}\n value={draft.title}\n onChange={(e) => set('title', e.target.value)}\n placeholder=\"e.g. U12 Boys — 2026/27 season registration\"\n />\n </FormField>\n <FormField label=\"Description\" htmlFor=\"description\">\n <textarea\n id=\"description\"\n rows={3}\n className={fieldInputClass}\n value={draft.description}\n onChange={(e) => set('description', e.target.value)}\n placeholder=\"Who this is for, training days, what is included.\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-3\">\n <FormField label=\"Season\" htmlFor=\"season\">\n <input\n id=\"season\"\n className={fieldInputClass}\n value={draft.season}\n onChange={(e) => set('season', e.target.value)}\n placeholder=\"2026/27\"\n />\n </FormField>\n <FormField label=\"Team\" htmlFor=\"teamId\" hint=\"Optional squad this registers for.\">\n <select\n id=\"teamId\"\n className={fieldInputClass}\n value={draft.teamId}\n onChange={(e) => set('teamId', e.target.value)}\n >\n <option value=\"\">No specific team</option>\n {(teams.data?.items ?? []).map((team) => (\n <option key={team.id} value={team.id}>\n {team.name}\n </option>\n ))}\n </select>\n </FormField>\n <FormField\n label=\"Currency\"\n htmlFor=\"currency\"\n hint={locked ? 'Locked after the first registration.' : undefined}\n >\n <select\n id=\"currency\"\n className={fieldInputClass}\n value={draft.currency}\n disabled={locked}\n onChange={(e) => set('currency', e.target.value)}\n >\n {[...new Set([draft.currency, ...CURRENCIES])].map((code) => (\n <option key={code} value={code}>\n {code}\n </option>\n ))}\n </select>\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-3\">\n <FormField label=\"Opens\" htmlFor=\"opensAt\" hint=\"Leave empty to open on publish.\">\n <input\n id=\"opensAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={draft.opensAt}\n onChange={(e) => set('opensAt', e.target.value)}\n />\n </FormField>\n <FormField label=\"Closes\" htmlFor=\"closesAt\" hint=\"Leave empty to close manually.\">\n <input\n id=\"closesAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={draft.closesAt}\n onChange={(e) => set('closesAt', e.target.value)}\n />\n </FormField>\n <FormField label=\"Capacity\" htmlFor=\"maxSubmissions\" hint=\"Maximum registrations.\">\n <input\n id=\"maxSubmissions\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={draft.maxSubmissions}\n onChange={(e) => set('maxSubmissions', e.target.value)}\n placeholder=\"Unlimited\"\n />\n </FormField>\n </div>\n {!isEdit && (\n <FormField\n label=\"Share-link name\"\n htmlFor=\"slug\"\n hint=\"Optional. Lowercase letters, digits and dashes. Generated from the title when empty.\"\n >\n <input\n id=\"slug\"\n className={fieldInputClass}\n value={draft.slug}\n onChange={(e) => set('slug', e.target.value)}\n placeholder=\"u12-boys-2026\"\n />\n </FormField>\n )}\n </section>\n\n <section className=\"space-y-3\" aria-labelledby=\"builder-questions\">\n <div className=\"flex items-center justify-between\">\n <h2 id=\"builder-questions\" className=\"text-base font-semibold text-text-primary\">\n Questions\n </h2>\n <button\n type=\"button\"\n disabled={locked}\n onClick={() =>\n set('questions', [\n ...draft.questions,\n {\n uid: nextUid(),\n key: '',\n label: '',\n fieldType: 'SHORT_TEXT',\n required: false,\n optionsText: '',\n helpText: '',\n },\n ])\n }\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Plus className=\"h-4 w-4\" /> Add question\n </button>\n </div>\n <p className=\"text-xs text-text-secondary\">\n Participant name and contact details are always collected.\n {locked && ' Questions are locked because this form already has registrations.'}\n </p>\n {draft.questions.map((question, index) => (\n <div\n key={question.uid}\n className=\"space-y-3 rounded-lg border border-border-seam bg-bg-surface p-4\"\n data-testid=\"builder-question\"\n >\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-[1fr_12rem]\">\n <FormField label={`Question ${index + 1}`} htmlFor={`q-label-${question.uid}`}>\n <input\n id={`q-label-${question.uid}`}\n className={fieldInputClass}\n value={question.label}\n disabled={locked}\n onChange={(e) => setQuestion(question.uid, { label: e.target.value })}\n placeholder=\"e.g. Date of birth\"\n />\n </FormField>\n <FormField label=\"Answer type\" htmlFor={`q-type-${question.uid}`}>\n <select\n id={`q-type-${question.uid}`}\n className={fieldInputClass}\n value={question.fieldType}\n disabled={locked}\n onChange={(e) =>\n setQuestion(question.uid, {\n fieldType: e.target.value as AthRegistrationFieldType,\n })\n }\n >\n {(Object.keys(FIELD_TYPE_LABELS) as AthRegistrationFieldType[]).map((type) => (\n <option key={type} value={type}>\n {FIELD_TYPE_LABELS[type]}\n </option>\n ))}\n </select>\n </FormField>\n </div>\n {question.fieldType === 'SELECT' && (\n <FormField\n label=\"Options\"\n htmlFor={`q-options-${question.uid}`}\n hint=\"Comma-separated.\"\n >\n <input\n id={`q-options-${question.uid}`}\n className={fieldInputClass}\n value={question.optionsText}\n disabled={locked}\n onChange={(e) => setQuestion(question.uid, { optionsText: e.target.value })}\n placeholder=\"XS, S, M, L\"\n />\n </FormField>\n )}\n <FormField label=\"Help text\" htmlFor={`q-help-${question.uid}`}>\n <input\n id={`q-help-${question.uid}`}\n className={fieldInputClass}\n value={question.helpText}\n disabled={locked}\n onChange={(e) => setQuestion(question.uid, { helpText: e.target.value })}\n />\n </FormField>\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={question.required}\n disabled={locked}\n onChange={(e) => setQuestion(question.uid, { required: e.target.checked })}\n className=\"h-4 w-4 rounded border-border-seam text-action-primary-bg focus:ring-[var(--color-focus-ring)]\"\n />\n Required\n </label>\n <span className=\"flex gap-2\">\n <button\n type=\"button\"\n aria-label=\"Move question up\"\n className={iconButton}\n disabled={locked || index === 0}\n onClick={() => set('questions', move(draft.questions, index, -1))}\n >\n <ArrowUp className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n aria-label=\"Move question down\"\n className={iconButton}\n disabled={locked || index === draft.questions.length - 1}\n onClick={() => set('questions', move(draft.questions, index, 1))}\n >\n <ArrowDown className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n aria-label=\"Remove question\"\n className={iconButton}\n disabled={locked}\n onClick={() =>\n set(\n 'questions',\n draft.questions.filter((q) => q.uid !== question.uid)\n )\n }\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </span>\n </div>\n </div>\n ))}\n </section>\n\n <section className=\"space-y-3\" aria-labelledby=\"builder-fees\">\n <div className=\"flex items-center justify-between\">\n <h2 id=\"builder-fees\" className=\"text-base font-semibold text-text-primary\">\n Fees\n </h2>\n <button\n type=\"button\"\n disabled={locked}\n onClick={() =>\n set('fees', [\n ...draft.fees,\n {\n uid: nextUid(),\n name: '',\n amountText: '',\n feeType: 'REQUIRED',\n description: '',\n },\n ])\n }\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Plus className=\"h-4 w-4\" /> Add fee\n </button>\n </div>\n <p className=\"text-xs text-text-secondary\">\n Required fees are charged on every registration; optional ones (kit, tournament entry)\n are opt-in. Payments are recorded by the club office as they arrive.\n </p>\n {draft.fees.map((fee) => (\n <div\n key={fee.uid}\n className=\"grid grid-cols-1 gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 sm:grid-cols-[1fr_9rem_9rem_auto]\"\n data-testid=\"builder-fee\"\n >\n <FormField label=\"Fee name\" htmlFor={`f-name-${fee.uid}`}>\n <input\n id={`f-name-${fee.uid}`}\n className={fieldInputClass}\n value={fee.name}\n disabled={locked}\n onChange={(e) => setFee(fee.uid, { name: e.target.value })}\n placeholder=\"Membership\"\n />\n </FormField>\n <FormField label={`Amount (${draft.currency})`} htmlFor={`f-amount-${fee.uid}`}>\n <input\n id={`f-amount-${fee.uid}`}\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={fee.amountText}\n disabled={locked}\n onChange={(e) => setFee(fee.uid, { amountText: e.target.value })}\n placeholder=\"150.00\"\n />\n </FormField>\n <FormField label=\"Type\" htmlFor={`f-type-${fee.uid}`}>\n <select\n id={`f-type-${fee.uid}`}\n className={fieldInputClass}\n value={fee.feeType}\n disabled={locked}\n onChange={(e) =>\n setFee(fee.uid, { feeType: e.target.value as AthRegistrationFeeType })\n }\n >\n <option value=\"REQUIRED\">Required</option>\n <option value=\"OPTIONAL\">Optional</option>\n </select>\n </FormField>\n <div className=\"flex items-end\">\n <button\n type=\"button\"\n aria-label=\"Remove fee\"\n className={iconButton}\n disabled={locked}\n onClick={() =>\n set(\n 'fees',\n draft.fees.filter((f) => f.uid !== fee.uid)\n )\n }\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ))}\n </section>\n\n <section>\n <FormField\n label=\"Waiver / consent\"\n htmlFor=\"waiverText\"\n hint=\"Optional. When set, registrants must accept it to submit.\"\n >\n <textarea\n id=\"waiverText\"\n rows={4}\n className={fieldInputClass}\n value={draft.waiverText}\n onChange={(e) => set('waiverText', e.target.value)}\n />\n </FormField>\n </section>\n\n {error && (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {error}\n </p>\n )}\n\n <div className=\"pt-2\">\n <button\n type=\"submit\"\n disabled={saving}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {saving ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Save className=\"h-4 w-4\" />}\n {isEdit ? 'Save changes' : 'Create form'}\n </button>\n </div>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAgCA,IAAM,IAA8D;CAClE,YAAY;CACZ,WAAW;CACX,OAAO;CACP,OAAO;CACP,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,UAAU;AACZ,GAEM,IAAa;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;CAAO;CAAO;AAAK,GAqCpE,UAAgB,SAAS,OAAO,WAAW,KAE3C,IAAyB;CAC7B,OAAO;CACP,aAAa;CACb,QAAQ;CACR,QAAQ;CACR,MAAM;CACN,UAAU;CACV,SAAS;CACT,UAAU;CACV,gBAAgB;CAChB,YAAY;CACZ,WAAW,CAAC;CACZ,MAAM,CAAC;AACT;AAGA,SAAS,EAAa,GAA4B;CAChD,IAAI,CAAC,GAAK,OAAO;CACjB,IAAM,IAAO,IAAI,KAAK,CAAG,GACnB,IAAS,EAAK,kBAAkB,IAAI;CAC1C,OAAO,IAAI,KAAK,EAAK,QAAQ,IAAI,CAAM,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACpE;AAEA,SAAS,EAAe,GAAmC;CACzD,OAAO,IAAQ,IAAI,KAAK,CAAK,EAAE,YAAY,IAAI,KAAA;AACjD;AAEA,SAAS,EAAc,GAAsC;CAC3D,OAAO;EACL,OAAO,EAAK;EACZ,aAAa,EAAK,eAAe;EACjC,QAAQ,EAAK,UAAU;EACvB,QAAQ,EAAK,UAAU;EACvB,MAAM,EAAK;EACX,UAAU,EAAK;EACf,SAAS,EAAa,EAAK,OAAO;EAClC,UAAU,EAAa,EAAK,QAAQ;EACpC,gBAAgB,EAAK,kBAAkB,OAAqC,KAA9B,OAAO,EAAK,cAAc;EACxE,YAAY,EAAK,cAAc;EAC/B,WAAW,EAAK,OAAO,KAAK,OAAW;GACrC,KAAK,EAAQ;GACb,KAAK,EAAM;GACX,OAAO,EAAM;GACb,WAAW,EAAM;GACjB,UAAU,EAAM;GAChB,aAAa,EAAM,QAAQ,KAAK,IAAI;GACpC,UAAU,EAAM,YAAY;EAC9B,EAAE;EACF,MAAM,EAAK,KAAK,KAAK,OAAS;GAC5B,KAAK,EAAQ;GACb,MAAM,EAAI;GACV,YAAY,EAAa,EAAI,QAAQ,EAAK,QAAQ;GAClD,SAAS,EAAI;GACb,aAAa,EAAI,eAAe;EAClC,EAAE;CACJ;AACF;AAEA,SAAS,EAAQ,GAAW,GAAe,GAAoB;CAC7D,IAAM,IAAS,IAAQ;CACvB,IAAI,IAAS,KAAK,KAAU,EAAK,QAAQ,OAAO;CAChD,IAAM,IAAO,CAAC,GAAG,CAAI,GACf,CAAC,KAAQ,EAAK,OAAO,GAAO,CAAC;CAEnC,OADI,MAAS,KAAA,KAAW,EAAK,OAAO,GAAQ,GAAG,CAAI,GAC5C;AACT;AAEA,IAAM,IACJ;AAEF,SAAgB,IAA8B;CAC5C,IAAM,EAAE,UAAO,EAA0B,GACnC,IAAS,EAAQ,GACjB,EAAE,kBAAe,EAAiB,GAElC,IAAW,EAAoB,CAAE,GACjC,IAAQ,EAAS,EAAE,YAAY,EAAE,OAAO,IAAI,EAAE,CAAC,GAC/C,IAAa,EAA0B,GACvC,IAAa,EAA0B,GAEvC,CAAC,GAAO,KAAY,EAAoB,CAAW,GACnD,CAAC,GAAO,KAAY,EAAwB,IAAI,GAKhD,IAAe,EAAsB,IAAI;CAC/C,QAAgB;EACd,AAAI,KAAU,EAAS,QAAQ,EAAa,YAAY,EAAS,KAAK,OACpE,EAAa,UAAU,EAAS,KAAK,IACrC,EAAS,EAAc,EAAS,IAAI,CAAC;CAEzC,GAAG,CAAC,GAAQ,EAAS,IAAI,CAAC;CAE1B,IAAM,IAAS,MAAW,EAAS,MAAM,mBAAmB,KAAK,GAC3D,KAAkC,GAAQ,MAC9C,GAAU,OAAU;EAAE,GAAG;GAAO,IAAM;CAAM,EAAE,GAC1C,KAAe,GAAa,MAChC,EACE,aACA,EAAM,UAAU,KAAK,MAAO,EAAE,QAAQ,IAAM;EAAE,GAAG;EAAG,GAAG;CAAM,IAAI,CAAE,CACrE,GACI,KAAU,GAAa,MAC3B,EACE,QACA,EAAM,KAAK,KAAK,MAAO,EAAE,QAAQ,IAAM;EAAE,GAAG;EAAG,GAAG;CAAM,IAAI,CAAE,CAChE,GAEI,UACiF;EAGrF,IAAM,IAAQ,IAAI,IAAY,EAAM,UAAU,KAAK,MAAM,EAAE,GAAG,EAAE,OAAO,OAAO,CAAC,GACzE,IAAsC,CAAC;EAC7C,KAAK,IAAM,KAAY,EAAM,WAAW;GACtC,IAAM,IAAQ,EAAS,MAAM,KAAK;GAClC,IAAI,CAAC,GAAO,OAAO;GACnB,IAAM,IAAM,EAAS,OAAO,EAAa,GAAO,CAAK;GACrD,EAAM,IAAI,CAAG;GACb,IAAM,IAAU,EAAS,YACtB,MAAM,GAAG,EACT,KAAK,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;GACjB,IAAI,EAAS,cAAc,YAAY,EAAQ,WAAW,GACxD,OAAO,4CAA4C,EAAM;GAE3D,EAAO,KAAK;IACV;IACA;IACA,WAAW,EAAS;IACpB,UAAU,EAAS;IACnB,SAAS,EAAS,cAAc,WAAW,IAAU,KAAA;IACrD,UAAU,EAAS,SAAS,KAAK,KAAK,KAAA;GACxC,CAAC;EACH;EACA,IAAM,IAAkC,CAAC;EACzC,KAAK,IAAM,KAAO,EAAM,MAAM;GAC5B,IAAM,IAAO,EAAI,KAAK,KAAK;GAC3B,IAAI,CAAC,GAAM,OAAO;GAClB,IAAM,IAAS,EAAW,EAAI,YAAY,EAAM,QAAQ;GACxD,IAAI,MAAW,MAAM,OAAO,6BAA6B,EAAK;GAC9D,EAAK,KAAK;IACR;IACA;IACA,SAAS,EAAI;IACb,aAAa,EAAI,YAAY,KAAK,KAAK,KAAA;GACzC,CAAC;EACH;EACA,OAAO;GAAE;GAAQ;EAAK;CACxB,GAEM,IAAe,OAAO,MAAuB;EAGjD,IAFA,EAAE,eAAe,GACjB,EAAS,IAAI,GACT,CAAC,EAAM,MAAM,KAAK,GAAG;GACvB,EAAS,wBAAwB;GACjC;EACF;EACA,IAAM,IAAiB,EAAM,eAAe,KAAK,IAC7C,OAAO,SAAS,EAAM,gBAAgB,EAAE,IACxC,KAAA;EACJ,IAAI,MAAmB,KAAA,MAAc,OAAO,MAAM,CAAc,KAAK,IAAiB,IAAI;GACxF,EAAS,qCAAqC;GAC9C;EACF;EACA,IAAM,IAAY,IAAS,OAAO,EAAe;EACjD,IAAI,OAAO,KAAc,UAAU;GACjC,EAAS,CAAS;GAClB;EACF;EAEA,IAAI;GACF,AAAI,KAAU,KACZ,MAAM,EAAW,YAAY;IAC3B;IACA,OAAO;KACL,OAAO,EAAM,MAAM,KAAK;KACxB,aAAa,EAAM,YAAY,KAAK,KAAK;KACzC,QAAQ,EAAM,OAAO,KAAK,KAAK;KAC/B,QAAQ,EAAM,UAAU;KACxB,SAAS,EAAe,EAAM,OAAO,KAAK;KAC1C,UAAU,EAAe,EAAM,QAAQ,KAAK;KAC5C,gBAAgB,KAAkB;KAClC,YAAY,EAAM,WAAW,KAAK,KAAK;KACvC,GAAI,IACA;MAAE,UAAU,EAAM;MAAU,QAAQ,EAAU;MAAQ,MAAM,EAAU;KAAK,IAC3E,CAAC;IACP;GACF,CAAC,GACD,EAAW,kBAAkB,GAAI,KACxB,KAeT,EAAW,mBAAkB,MAdP,EAAW,YAAY;IAC3C,OAAO,EAAM,MAAM,KAAK;IACxB,aAAa,EAAM,YAAY,KAAK,KAAK,KAAA;IACzC,QAAQ,EAAM,OAAO,KAAK,KAAK,KAAA;IAC/B,QAAQ,EAAM,UAAU,KAAA;IACxB,MAAM,EAAM,KAAK,KAAK,KAAK,KAAA;IAC3B,UAAU,EAAM;IAChB,SAAS,EAAe,EAAM,OAAO;IACrC,UAAU,EAAe,EAAM,QAAQ;IACvC;IACA,YAAY,EAAM,WAAW,KAAK,KAAK,KAAA;IACvC,QAAQ,EAAU;IAClB,MAAM,EAAU;GAClB,CAAC,GACoC,IAAI;EAE7C,SAAS,GAAK;GACZ,EAAS,aAAe,QAAQ,EAAI,UAAU,uCAAuC;EACvF;CACF,GAEM,IAAS,EAAW,aAAa,EAAW,WAC5C,IAAQ,IAAS,2BAA2B;CAqBlD,OAnBI,KAAU,EAAS,YAEnB,kBAAC,GAAD;EAAmB;YACjB,kBAAC,GAAD,EAAc,OAAM,6BAA8B,CAAA;CACxC,CAAA,IAGZ,MAAW,EAAS,WAAW,CAAC,EAAS,QAEzC,kBAAC,GAAD;EAAmB;YACjB,kBAAC,GAAD;GACE,OAAM;GACN,SAAQ;GACR,eAAe,EAAW,gBAAgB;EAC3C,CAAA;CACS,CAAA,IAKd,kBAAC,GAAD;EACS;EACP,aAAY;EACZ,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,KAAU,IAAK,kBAAkB,MAAO,gBAAgB;GAClF,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,GAAC,SAC3B;;YAGV,kBAAC,QAAD;GAAM,UAAU;GAAc,WAAU;aAAxC;IACE,kBAAC,WAAD;KAAS,WAAU;eAAnB;MACE,kBAAC,GAAD;OAAW,OAAM;OAAa,SAAQ;OAAQ,UAAA;iBAC5C,kBAAC,SAAD;QACE,IAAG;QACH,WAAW;QACX,OAAO,EAAM;QACb,WAAW,MAAM,EAAI,SAAS,EAAE,OAAO,KAAK;QAC5C,aAAY;OACb,CAAA;MACQ,CAAA;MACX,kBAAC,GAAD;OAAW,OAAM;OAAc,SAAQ;iBACrC,kBAAC,YAAD;QACE,IAAG;QACH,MAAM;QACN,WAAW;QACX,OAAO,EAAM;QACb,WAAW,MAAM,EAAI,eAAe,EAAE,OAAO,KAAK;QAClD,aAAY;OACb,CAAA;MACQ,CAAA;MACX,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SAAW,OAAM;SAAS,SAAQ;mBAChC,kBAAC,SAAD;UACE,IAAG;UACH,WAAW;UACX,OAAO,EAAM;UACb,WAAW,MAAM,EAAI,UAAU,EAAE,OAAO,KAAK;UAC7C,aAAY;SACb,CAAA;QACQ,CAAA;QACX,kBAAC,GAAD;SAAW,OAAM;SAAO,SAAQ;SAAS,MAAK;mBAC5C,kBAAC,UAAD;UACE,IAAG;UACH,WAAW;UACX,OAAO,EAAM;UACb,WAAW,MAAM,EAAI,UAAU,EAAE,OAAO,KAAK;oBAJ/C,CAME,kBAAC,UAAD;WAAQ,OAAM;qBAAG;UAAwB,CAAA,IACvC,EAAM,MAAM,SAAS,CAAC,GAAG,KAAK,MAC9B,kBAAC,UAAD;WAAsB,OAAO,EAAK;qBAC/B,EAAK;UACA,GAFK,EAAK,EAEV,CACT,CACK;;QACC,CAAA;QACX,kBAAC,GAAD;SACE,OAAM;SACN,SAAQ;SACR,MAAM,IAAS,yCAAyC,KAAA;mBAExD,kBAAC,UAAD;UACE,IAAG;UACH,WAAW;UACX,OAAO,EAAM;UACb,UAAU;UACV,WAAW,MAAM,EAAI,YAAY,EAAE,OAAO,KAAK;oBAE9C,CAAC,GAAG,IAAI,IAAI,CAAC,EAAM,UAAU,GAAG,CAAU,CAAC,CAAC,EAAE,KAAK,MAClD,kBAAC,UAAD;WAAmB,OAAO;qBACvB;UACK,GAFK,CAEL,CACT;SACK,CAAA;QACC,CAAA;OACR;;MACL,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SAAW,OAAM;SAAQ,SAAQ;SAAU,MAAK;mBAC9C,kBAAC,SAAD;UACE,IAAG;UACH,MAAK;UACL,WAAW;UACX,OAAO,EAAM;UACb,WAAW,MAAM,EAAI,WAAW,EAAE,OAAO,KAAK;SAC/C,CAAA;QACQ,CAAA;QACX,kBAAC,GAAD;SAAW,OAAM;SAAS,SAAQ;SAAW,MAAK;mBAChD,kBAAC,SAAD;UACE,IAAG;UACH,MAAK;UACL,WAAW;UACX,OAAO,EAAM;UACb,WAAW,MAAM,EAAI,YAAY,EAAE,OAAO,KAAK;SAChD,CAAA;QACQ,CAAA;QACX,kBAAC,GAAD;SAAW,OAAM;SAAW,SAAQ;SAAiB,MAAK;mBACxD,kBAAC,SAAD;UACE,IAAG;UACH,WAAU;UACV,WAAW;UACX,OAAO,EAAM;UACb,WAAW,MAAM,EAAI,kBAAkB,EAAE,OAAO,KAAK;UACrD,aAAY;SACb,CAAA;QACQ,CAAA;OACR;;MACJ,CAAC,KACA,kBAAC,GAAD;OACE,OAAM;OACN,SAAQ;OACR,MAAK;iBAEL,kBAAC,SAAD;QACE,IAAG;QACH,WAAW;QACX,OAAO,EAAM;QACb,WAAW,MAAM,EAAI,QAAQ,EAAE,OAAO,KAAK;QAC3C,aAAY;OACb,CAAA;MACQ,CAAA;KAEN;;IAET,kBAAC,WAAD;KAAS,WAAU;KAAY,mBAAgB;eAA/C;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,MAAD;QAAI,IAAG;QAAoB,WAAU;kBAA4C;OAE7E,CAAA,GACJ,kBAAC,UAAD;QACE,MAAK;QACL,UAAU;QACV,eACE,EAAI,aAAa,CACf,GAAG,EAAM,WACT;SACE,KAAK,EAAQ;SACb,KAAK;SACL,OAAO;SACP,WAAW;SACX,UAAU;SACV,aAAa;SACb,UAAU;QACZ,CACF,CAAC;QAEH,WAAU;kBAjBZ,CAmBE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GAAC,eACtB;SACL;;MACL,kBAAC,KAAD;OAAG,WAAU;iBAAb,CAA2C,8DAExC,KAAU,oEACV;;MACF,EAAM,UAAU,KAAK,GAAU,MAC9B,kBAAC,OAAD;OAEE,WAAU;OACV,eAAY;iBAHd;QAKE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAW,OAAO,YAAY,IAAQ;UAAK,SAAS,WAAW,EAAS;oBACtE,kBAAC,SAAD;WACE,IAAI,WAAW,EAAS;WACxB,WAAW;WACX,OAAO,EAAS;WAChB,UAAU;WACV,WAAW,MAAM,EAAY,EAAS,KAAK,EAAE,OAAO,EAAE,OAAO,MAAM,CAAC;WACpE,aAAY;UACb,CAAA;SACQ,CAAA,GACX,kBAAC,GAAD;UAAW,OAAM;UAAc,SAAS,UAAU,EAAS;oBACzD,kBAAC,UAAD;WACE,IAAI,UAAU,EAAS;WACvB,WAAW;WACX,OAAO,EAAS;WAChB,UAAU;WACV,WAAW,MACT,EAAY,EAAS,KAAK,EACxB,WAAW,EAAE,OAAO,MACtB,CAAC;qBAGD,OAAO,KAAK,CAAiB,EAAiC,KAAK,MACnE,kBAAC,UAAD;YAAmB,OAAO;sBACvB,EAAkB;WACb,GAFK,CAEL,CACT;UACK,CAAA;SACC,CAAA,CACR;;QACJ,EAAS,cAAc,YACtB,kBAAC,GAAD;SACE,OAAM;SACN,SAAS,aAAa,EAAS;SAC/B,MAAK;mBAEL,kBAAC,SAAD;UACE,IAAI,aAAa,EAAS;UAC1B,WAAW;UACX,OAAO,EAAS;UAChB,UAAU;UACV,WAAW,MAAM,EAAY,EAAS,KAAK,EAAE,aAAa,EAAE,OAAO,MAAM,CAAC;UAC1E,aAAY;SACb,CAAA;QACQ,CAAA;QAEb,kBAAC,GAAD;SAAW,OAAM;SAAY,SAAS,UAAU,EAAS;mBACvD,kBAAC,SAAD;UACE,IAAI,UAAU,EAAS;UACvB,WAAW;UACX,OAAO,EAAS;UAChB,UAAU;UACV,WAAW,MAAM,EAAY,EAAS,KAAK,EAAE,UAAU,EAAE,OAAO,MAAM,CAAC;SACxE,CAAA;QACQ,CAAA;QACX,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,SAAD;WACE,MAAK;WACL,SAAS,EAAS;WAClB,UAAU;WACV,WAAW,MAAM,EAAY,EAAS,KAAK,EAAE,UAAU,EAAE,OAAO,QAAQ,CAAC;WACzE,WAAU;UACX,CAAA,GAAC,UAEG;aACP,kBAAC,QAAD;UAAM,WAAU;oBAAhB;WACE,kBAAC,UAAD;YACE,MAAK;YACL,cAAW;YACX,WAAW;YACX,UAAU,KAAU,MAAU;YAC9B,eAAe,EAAI,aAAa,EAAK,EAAM,WAAW,GAAO,EAAE,CAAC;sBAEhE,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;WACxB,CAAA;WACR,kBAAC,UAAD;YACE,MAAK;YACL,cAAW;YACX,WAAW;YACX,UAAU,KAAU,MAAU,EAAM,UAAU,SAAS;YACvD,eAAe,EAAI,aAAa,EAAK,EAAM,WAAW,GAAO,CAAC,CAAC;sBAE/D,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA;WAC1B,CAAA;WACR,kBAAC,UAAD;YACE,MAAK;YACL,cAAW;YACX,WAAW;YACX,UAAU;YACV,eACE,EACE,aACA,EAAM,UAAU,QAAQ,MAAM,EAAE,QAAQ,EAAS,GAAG,CACtD;sBAGF,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;WACvB,CAAA;UACJ;WACH;;OACF;SA1GE,EAAS,GA0GX,CACN;KACM;;IAET,kBAAC,WAAD;KAAS,WAAU;KAAY,mBAAgB;eAA/C;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,MAAD;QAAI,IAAG;QAAe,WAAU;kBAA4C;OAExE,CAAA,GACJ,kBAAC,UAAD;QACE,MAAK;QACL,UAAU;QACV,eACE,EAAI,QAAQ,CACV,GAAG,EAAM,MACT;SACE,KAAK,EAAQ;SACb,MAAM;SACN,YAAY;SACZ,SAAS;SACT,aAAa;QACf,CACF,CAAC;QAEH,WAAU;kBAfZ,CAiBE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GAAC,UACtB;SACL;;MACL,kBAAC,KAAD;OAAG,WAAU;iBAA8B;MAGxC,CAAA;MACF,EAAM,KAAK,KAAK,MACf,kBAAC,OAAD;OAEE,WAAU;OACV,eAAY;iBAHd;QAKE,kBAAC,GAAD;SAAW,OAAM;SAAW,SAAS,UAAU,EAAI;mBACjD,kBAAC,SAAD;UACE,IAAI,UAAU,EAAI;UAClB,WAAW;UACX,OAAO,EAAI;UACX,UAAU;UACV,WAAW,MAAM,EAAO,EAAI,KAAK,EAAE,MAAM,EAAE,OAAO,MAAM,CAAC;UACzD,aAAY;SACb,CAAA;QACQ,CAAA;QACX,kBAAC,GAAD;SAAW,OAAO,WAAW,EAAM,SAAS;SAAI,SAAS,YAAY,EAAI;mBACvE,kBAAC,SAAD;UACE,IAAI,YAAY,EAAI;UACpB,WAAU;UACV,WAAW;UACX,OAAO,EAAI;UACX,UAAU;UACV,WAAW,MAAM,EAAO,EAAI,KAAK,EAAE,YAAY,EAAE,OAAO,MAAM,CAAC;UAC/D,aAAY;SACb,CAAA;QACQ,CAAA;QACX,kBAAC,GAAD;SAAW,OAAM;SAAO,SAAS,UAAU,EAAI;mBAC7C,kBAAC,UAAD;UACE,IAAI,UAAU,EAAI;UAClB,WAAW;UACX,OAAO,EAAI;UACX,UAAU;UACV,WAAW,MACT,EAAO,EAAI,KAAK,EAAE,SAAS,EAAE,OAAO,MAAgC,CAAC;oBANzE,CASE,kBAAC,UAAD;WAAQ,OAAM;qBAAW;UAAgB,CAAA,GACzC,kBAAC,UAAD;WAAQ,OAAM;qBAAW;UAAgB,CAAA,CACnC;;QACC,CAAA;QACX,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,UAAD;UACE,MAAK;UACL,cAAW;UACX,WAAW;UACX,UAAU;UACV,eACE,EACE,QACA,EAAM,KAAK,QAAQ,MAAM,EAAE,QAAQ,EAAI,GAAG,CAC5C;oBAGF,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;SACvB,CAAA;QACL,CAAA;OACF;SAvDE,EAAI,GAuDN,CACN;KACM;;IAET,kBAAC,WAAD,EAAA,UACE,kBAAC,GAAD;KACE,OAAM;KACN,SAAQ;KACR,MAAK;eAEL,kBAAC,YAAD;MACE,IAAG;MACH,MAAM;MACN,WAAW;MACX,OAAO,EAAM;MACb,WAAW,MAAM,EAAI,cAAc,EAAE,OAAO,KAAK;KAClD,CAAA;IACQ,CAAA,EACJ,CAAA;IAER,KACC,kBAAC,KAAD;KAAG,MAAK;KAAQ,WAAU;eACvB;IACA,CAAA;IAGL,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,UAAD;MACE,MAAK;MACL,UAAU;MACV,WAAU;gBAHZ,CAKG,IAAS,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,IAAI,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GACnF,IAAS,iBAAiB,aACrB;;IACL,CAAA;GACD;;CACI,CAAA;AAEhB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration form detail (club staff)
|
|
3
|
+
* @module atheletebridge/pages/registrations
|
|
4
|
+
*
|
|
5
|
+
* The form's lifecycle (publish / close / archive), its share link, the fee
|
|
6
|
+
* reconciliation for this form and every registration received (BOFF-4135).
|
|
7
|
+
*/
|
|
8
|
+
export declare function RegistrationFormDetailPage(): import("react").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=RegistrationFormDetailPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegistrationFormDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/registrations/RegistrationFormDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAiDH,wBAAgB,0BAA0B,gCAuRzC"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { useAthleteBridge as e } from "../../context/AthleteBridgeContext.js";
|
|
2
|
+
import { PageLayout as t } from "../../components/PageLayout.js";
|
|
3
|
+
import { StatusBadge as n } from "../../components/StatusBadge.js";
|
|
4
|
+
import { formatDate as r } from "../../utils/formatters.js";
|
|
5
|
+
import { EmptyState as i } from "../../components/EmptyState.js";
|
|
6
|
+
import { ErrorState as a, LoadingState as o } from "../../components/LoadingState.js";
|
|
7
|
+
import { CursorPager as s } from "../../components/CursorPager.js";
|
|
8
|
+
import { FilterSelect as c } from "../../components/FilterSelect.js";
|
|
9
|
+
import { useRegistrationForm as l, useRegistrationFormLifecycle as u, useRegistrationSubmissions as d } from "../../hooks/useRegistrationApi.js";
|
|
10
|
+
import { formatMoney as f } from "../../utils/money.js";
|
|
11
|
+
import { registrationShareUrl as p } from "../../utils/registration.js";
|
|
12
|
+
import { PaymentSummaryPanel as m } from "../../components/registration/PaymentSummaryPanel.js";
|
|
13
|
+
import { useState as h } from "react";
|
|
14
|
+
import { Archive as g, ArrowLeft as _, Check as v, Copy as y, Lock as b, Pencil as x, Send as S, UserPlus as C } from "lucide-react";
|
|
15
|
+
import { useParams as ee } from "react-router";
|
|
16
|
+
import { Fragment as w, jsx as T, jsxs as E } from "react/jsx-runtime";
|
|
17
|
+
//#region src/atheletebridge/pages/registrations/RegistrationFormDetailPage.tsx
|
|
18
|
+
var D = 25, O = [
|
|
19
|
+
{
|
|
20
|
+
value: "SUBMITTED",
|
|
21
|
+
label: "Awaiting review"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
value: "ACCEPTED",
|
|
25
|
+
label: "Accepted"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
value: "DECLINED",
|
|
29
|
+
label: "Declined"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
value: "WITHDRAWN",
|
|
33
|
+
label: "Withdrawn"
|
|
34
|
+
}
|
|
35
|
+
], k = [
|
|
36
|
+
{
|
|
37
|
+
value: "UNPAID",
|
|
38
|
+
label: "Unpaid"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
value: "PARTIALLY_PAID",
|
|
42
|
+
label: "Partially paid"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: "PAID",
|
|
46
|
+
label: "Paid"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
value: "NOT_REQUIRED",
|
|
50
|
+
label: "No fee"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: "REFUNDED",
|
|
54
|
+
label: "Refunded"
|
|
55
|
+
}
|
|
56
|
+
], A = "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50", j = "inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50";
|
|
57
|
+
function M() {
|
|
58
|
+
let { id: M } = ee(), { navigateTo: N, basePath: P } = e(), F = l(M), I = u(), [L, R] = h(""), [z, B] = h(""), [V, H] = h(0), [U, W] = h(!1), [G, K] = h(null), q = d({
|
|
59
|
+
formId: M,
|
|
60
|
+
status: L || void 0,
|
|
61
|
+
paymentStatus: z || void 0,
|
|
62
|
+
pagination: {
|
|
63
|
+
limit: D,
|
|
64
|
+
offset: V * D
|
|
65
|
+
}
|
|
66
|
+
}, !!M);
|
|
67
|
+
if (F.isLoading) return /* @__PURE__ */ T(t, {
|
|
68
|
+
title: "Registration form",
|
|
69
|
+
children: /* @__PURE__ */ T(o, { label: "Loading registration form…" })
|
|
70
|
+
});
|
|
71
|
+
if (F.isError || !F.data || !M) return /* @__PURE__ */ T(t, {
|
|
72
|
+
title: "Registration form",
|
|
73
|
+
children: /* @__PURE__ */ T(a, {
|
|
74
|
+
title: "Registration form not found",
|
|
75
|
+
message: "This form does not exist, was archived, or you do not have access to it.",
|
|
76
|
+
onRetry: () => N("/registrations")
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
let J = F.data, Y = p(window.location.origin, P ?? "/", J.slug), X = J.status === "PUBLISHED" || J.status === "CLOSED", Z = async (e) => {
|
|
80
|
+
if (K(null), !(e === "archive" && !window.confirm("Archive this form? Its registrations and payments are kept."))) try {
|
|
81
|
+
await I.mutateAsync({
|
|
82
|
+
id: M,
|
|
83
|
+
action: e
|
|
84
|
+
}), e === "archive" && N("/registrations");
|
|
85
|
+
} catch (e) {
|
|
86
|
+
K(e instanceof Error ? e.message : "That action failed.");
|
|
87
|
+
}
|
|
88
|
+
}, Q = async () => {
|
|
89
|
+
try {
|
|
90
|
+
await navigator.clipboard.writeText(Y), W(!0), window.setTimeout(() => W(!1), 2e3);
|
|
91
|
+
} catch {
|
|
92
|
+
K("Could not copy — select the link and copy it manually.");
|
|
93
|
+
}
|
|
94
|
+
}, $ = q.data?.items ?? [];
|
|
95
|
+
return /* @__PURE__ */ E(t, {
|
|
96
|
+
title: /* @__PURE__ */ E("span", {
|
|
97
|
+
className: "flex flex-wrap items-center gap-3",
|
|
98
|
+
children: [
|
|
99
|
+
J.title,
|
|
100
|
+
" ",
|
|
101
|
+
/* @__PURE__ */ T(n, { status: J.status }),
|
|
102
|
+
J.status === "PUBLISHED" && !J.isOpen && /* @__PURE__ */ T(n, {
|
|
103
|
+
status: "Not open",
|
|
104
|
+
variant: "warning"
|
|
105
|
+
})
|
|
106
|
+
]
|
|
107
|
+
}),
|
|
108
|
+
description: [
|
|
109
|
+
J.season,
|
|
110
|
+
`${J.submissionCount} registered${J.maxSubmissions ? ` of ${J.maxSubmissions}` : ""}`,
|
|
111
|
+
J.closesAt ? `closes ${r(J.closesAt)}` : null
|
|
112
|
+
].filter(Boolean).join(" · "),
|
|
113
|
+
actions: /* @__PURE__ */ E("div", {
|
|
114
|
+
className: "flex flex-wrap gap-2",
|
|
115
|
+
children: [
|
|
116
|
+
/* @__PURE__ */ E("button", {
|
|
117
|
+
type: "button",
|
|
118
|
+
className: A,
|
|
119
|
+
onClick: () => N("/registrations"),
|
|
120
|
+
children: [/* @__PURE__ */ T(_, { className: "h-4 w-4" }), " All forms"]
|
|
121
|
+
}),
|
|
122
|
+
/* @__PURE__ */ E("button", {
|
|
123
|
+
type: "button",
|
|
124
|
+
className: A,
|
|
125
|
+
onClick: () => N(`/registrations/${M}/edit`),
|
|
126
|
+
children: [/* @__PURE__ */ T(x, { className: "h-4 w-4" }), " Edit"]
|
|
127
|
+
}),
|
|
128
|
+
/* @__PURE__ */ E("button", {
|
|
129
|
+
type: "button",
|
|
130
|
+
className: A,
|
|
131
|
+
onClick: () => N(`/registrations/${M}/record`),
|
|
132
|
+
children: [/* @__PURE__ */ T(C, { className: "h-4 w-4" }), " Record registration"]
|
|
133
|
+
}),
|
|
134
|
+
(J.status === "DRAFT" || J.status === "CLOSED") && /* @__PURE__ */ E("button", {
|
|
135
|
+
type: "button",
|
|
136
|
+
className: j,
|
|
137
|
+
disabled: I.isPending,
|
|
138
|
+
onClick: () => void Z("publish"),
|
|
139
|
+
children: [
|
|
140
|
+
/* @__PURE__ */ T(S, { className: "h-4 w-4" }),
|
|
141
|
+
" ",
|
|
142
|
+
J.status === "CLOSED" ? "Re-open" : "Publish"
|
|
143
|
+
]
|
|
144
|
+
}),
|
|
145
|
+
J.status === "PUBLISHED" && /* @__PURE__ */ E("button", {
|
|
146
|
+
type: "button",
|
|
147
|
+
className: A,
|
|
148
|
+
disabled: I.isPending,
|
|
149
|
+
onClick: () => void Z("close"),
|
|
150
|
+
children: [/* @__PURE__ */ T(b, { className: "h-4 w-4" }), " Close"]
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ E("button", {
|
|
153
|
+
type: "button",
|
|
154
|
+
className: A,
|
|
155
|
+
disabled: I.isPending,
|
|
156
|
+
onClick: () => void Z("archive"),
|
|
157
|
+
children: [/* @__PURE__ */ T(g, { className: "h-4 w-4" }), " Archive"]
|
|
158
|
+
})
|
|
159
|
+
]
|
|
160
|
+
}),
|
|
161
|
+
children: [
|
|
162
|
+
G && /* @__PURE__ */ T("p", {
|
|
163
|
+
role: "alert",
|
|
164
|
+
className: "mb-4 text-sm text-status-error-text",
|
|
165
|
+
children: G
|
|
166
|
+
}),
|
|
167
|
+
X ? /* @__PURE__ */ E("section", {
|
|
168
|
+
className: "mb-6 rounded-lg border border-border-seam bg-bg-surface p-4",
|
|
169
|
+
"aria-label": "Share link",
|
|
170
|
+
children: [
|
|
171
|
+
/* @__PURE__ */ T("p", {
|
|
172
|
+
className: "mb-2 text-sm font-medium text-text-primary",
|
|
173
|
+
children: "Share link for families"
|
|
174
|
+
}),
|
|
175
|
+
/* @__PURE__ */ E("div", {
|
|
176
|
+
className: "flex flex-wrap items-center gap-2",
|
|
177
|
+
children: [/* @__PURE__ */ T("code", {
|
|
178
|
+
className: "min-w-0 flex-1 truncate rounded-md bg-bg-sunken px-3 py-2 text-xs text-text-secondary",
|
|
179
|
+
"data-testid": "registration-share-url",
|
|
180
|
+
children: Y
|
|
181
|
+
}), /* @__PURE__ */ E("button", {
|
|
182
|
+
type: "button",
|
|
183
|
+
className: A,
|
|
184
|
+
onClick: () => void Q(),
|
|
185
|
+
children: [
|
|
186
|
+
T(U ? v : y, { className: "h-4 w-4" }),
|
|
187
|
+
" ",
|
|
188
|
+
U ? "Copied" : "Copy"
|
|
189
|
+
]
|
|
190
|
+
})]
|
|
191
|
+
}),
|
|
192
|
+
/* @__PURE__ */ T("p", {
|
|
193
|
+
className: "mt-2 text-xs text-text-secondary",
|
|
194
|
+
children: "Families sign in to AthleteBridge with any account, fill in the form and track their registration under “My registrations”."
|
|
195
|
+
})
|
|
196
|
+
]
|
|
197
|
+
}) : /* @__PURE__ */ T("p", {
|
|
198
|
+
className: "mb-6 rounded-lg border border-border-seam bg-bg-sunken p-4 text-sm text-text-secondary",
|
|
199
|
+
children: "This form is a draft. Publish it to get a share link families can use."
|
|
200
|
+
}),
|
|
201
|
+
/* @__PURE__ */ T(m, { formId: M }),
|
|
202
|
+
/* @__PURE__ */ E("div", {
|
|
203
|
+
className: "mb-4 flex flex-wrap items-center gap-3",
|
|
204
|
+
children: [
|
|
205
|
+
/* @__PURE__ */ T("h2", {
|
|
206
|
+
className: "mr-auto text-base font-semibold text-text-primary",
|
|
207
|
+
children: "Registrations"
|
|
208
|
+
}),
|
|
209
|
+
/* @__PURE__ */ T(c, {
|
|
210
|
+
ariaLabel: "Filter by review status",
|
|
211
|
+
allLabel: "All statuses",
|
|
212
|
+
value: L,
|
|
213
|
+
onChange: (e) => {
|
|
214
|
+
R(e), H(0);
|
|
215
|
+
},
|
|
216
|
+
options: O
|
|
217
|
+
}),
|
|
218
|
+
/* @__PURE__ */ T(c, {
|
|
219
|
+
ariaLabel: "Filter by payment status",
|
|
220
|
+
allLabel: "All payments",
|
|
221
|
+
value: z,
|
|
222
|
+
onChange: (e) => {
|
|
223
|
+
B(e), H(0);
|
|
224
|
+
},
|
|
225
|
+
options: k
|
|
226
|
+
})
|
|
227
|
+
]
|
|
228
|
+
}),
|
|
229
|
+
q.isLoading ? /* @__PURE__ */ T(o, { label: "Loading registrations…" }) : q.isError ? /* @__PURE__ */ T(a, { onRetry: () => void q.refetch() }) : $.length === 0 ? /* @__PURE__ */ T(i, {
|
|
230
|
+
illustration: "empty-data",
|
|
231
|
+
icon: /* @__PURE__ */ T(C, { className: "h-10 w-10" }),
|
|
232
|
+
title: "No registrations yet",
|
|
233
|
+
description: L || z ? "No registrations match these filters." : "Share the link with families, or record a paper registration yourself."
|
|
234
|
+
}) : /* @__PURE__ */ E(w, { children: [/* @__PURE__ */ T("div", {
|
|
235
|
+
className: "overflow-x-auto rounded-lg border border-border-seam",
|
|
236
|
+
children: /* @__PURE__ */ E("table", {
|
|
237
|
+
className: "w-full text-sm",
|
|
238
|
+
"data-testid": "registration-submissions",
|
|
239
|
+
children: [/* @__PURE__ */ T("thead", {
|
|
240
|
+
className: "bg-bg-sunken text-left text-xs uppercase tracking-wide text-text-secondary",
|
|
241
|
+
children: /* @__PURE__ */ E("tr", { children: [
|
|
242
|
+
/* @__PURE__ */ T("th", {
|
|
243
|
+
className: "px-4 py-2 font-medium",
|
|
244
|
+
children: "Participant"
|
|
245
|
+
}),
|
|
246
|
+
/* @__PURE__ */ T("th", {
|
|
247
|
+
className: "px-4 py-2 font-medium",
|
|
248
|
+
children: "Submitted"
|
|
249
|
+
}),
|
|
250
|
+
/* @__PURE__ */ T("th", {
|
|
251
|
+
className: "px-4 py-2 font-medium",
|
|
252
|
+
children: "Status"
|
|
253
|
+
}),
|
|
254
|
+
/* @__PURE__ */ T("th", {
|
|
255
|
+
className: "px-4 py-2 font-medium",
|
|
256
|
+
children: "Payment"
|
|
257
|
+
}),
|
|
258
|
+
/* @__PURE__ */ T("th", {
|
|
259
|
+
className: "px-4 py-2 text-right font-medium",
|
|
260
|
+
children: "Outstanding"
|
|
261
|
+
})
|
|
262
|
+
] })
|
|
263
|
+
}), /* @__PURE__ */ T("tbody", {
|
|
264
|
+
className: "divide-y divide-border-seam bg-bg-surface",
|
|
265
|
+
children: $.map((e) => /* @__PURE__ */ E("tr", {
|
|
266
|
+
tabIndex: 0,
|
|
267
|
+
role: "link",
|
|
268
|
+
onClick: () => N(`/registrations/${M}/submissions/${e.id}`),
|
|
269
|
+
onKeyDown: (t) => {
|
|
270
|
+
t.key === "Enter" && N(`/registrations/${M}/submissions/${e.id}`);
|
|
271
|
+
},
|
|
272
|
+
className: "cursor-pointer text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
273
|
+
children: [
|
|
274
|
+
/* @__PURE__ */ E("td", {
|
|
275
|
+
className: "px-4 py-2",
|
|
276
|
+
children: [/* @__PURE__ */ T("span", {
|
|
277
|
+
className: "block font-medium",
|
|
278
|
+
children: e.participantName
|
|
279
|
+
}), /* @__PURE__ */ T("span", {
|
|
280
|
+
className: "block text-xs text-text-secondary",
|
|
281
|
+
children: e.source === "STAFF_ENTRY" ? "Entered by staff" : e.contactEmail ?? "Via share link"
|
|
282
|
+
})]
|
|
283
|
+
}),
|
|
284
|
+
/* @__PURE__ */ T("td", {
|
|
285
|
+
className: "px-4 py-2 text-text-secondary",
|
|
286
|
+
children: r(e.submittedAt)
|
|
287
|
+
}),
|
|
288
|
+
/* @__PURE__ */ T("td", {
|
|
289
|
+
className: "px-4 py-2",
|
|
290
|
+
children: /* @__PURE__ */ T(n, { status: e.status })
|
|
291
|
+
}),
|
|
292
|
+
/* @__PURE__ */ T("td", {
|
|
293
|
+
className: "px-4 py-2",
|
|
294
|
+
children: /* @__PURE__ */ T(n, { status: e.paymentStatus })
|
|
295
|
+
}),
|
|
296
|
+
/* @__PURE__ */ T("td", {
|
|
297
|
+
className: "px-4 py-2 text-right tabular-nums",
|
|
298
|
+
children: f(e.amountOutstanding, e.currency)
|
|
299
|
+
})
|
|
300
|
+
]
|
|
301
|
+
}, e.id))
|
|
302
|
+
})]
|
|
303
|
+
})
|
|
304
|
+
}), /* @__PURE__ */ T(s, {
|
|
305
|
+
className: "mt-6",
|
|
306
|
+
page: V,
|
|
307
|
+
hasMore: q.data?.pageInfo.hasMore ?? !1,
|
|
308
|
+
onPrev: () => H((e) => Math.max(0, e - 1)),
|
|
309
|
+
onNext: () => H((e) => e + 1)
|
|
310
|
+
})] })
|
|
311
|
+
]
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
export { M as RegistrationFormDetailPage };
|
|
316
|
+
|
|
317
|
+
//# sourceMappingURL=RegistrationFormDetailPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegistrationFormDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/registrations/RegistrationFormDetailPage.tsx"],"sourcesContent":["/**\n * Registration form detail (club staff)\n * @module atheletebridge/pages/registrations\n *\n * The form's lifecycle (publish / close / archive), its share link, the fee\n * reconciliation for this form and every registration received (BOFF-4135).\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router';\nimport { Archive, ArrowLeft, Check, Copy, Lock, Pencil, Send, UserPlus } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FilterSelect } from '../../components/FilterSelect';\nimport { CursorPager } from '../../components/CursorPager';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { PaymentSummaryPanel } from '../../components/registration/PaymentSummaryPanel';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n type RegistrationFormLifecycleAction,\n useRegistrationForm,\n useRegistrationFormLifecycle,\n useRegistrationSubmissions,\n} from '../../hooks/useRegistrationApi';\nimport { formatDate } from '../../utils/formatters';\nimport { formatMoney } from '../../utils/money';\nimport { registrationShareUrl } from '../../utils/registration';\nimport type {\n AthRegistrationPaymentStatus,\n AthRegistrationSubmissionStatus,\n} from '../../types/registration';\n\nconst PAGE_SIZE = 25;\n\nconst STATUS_OPTS: ReadonlyArray<{ value: AthRegistrationSubmissionStatus; label: string }> = [\n { value: 'SUBMITTED', label: 'Awaiting review' },\n { value: 'ACCEPTED', label: 'Accepted' },\n { value: 'DECLINED', label: 'Declined' },\n { value: 'WITHDRAWN', label: 'Withdrawn' },\n];\n\nconst PAYMENT_OPTS: ReadonlyArray<{ value: AthRegistrationPaymentStatus; label: string }> = [\n { value: 'UNPAID', label: 'Unpaid' },\n { value: 'PARTIALLY_PAID', label: 'Partially paid' },\n { value: 'PAID', label: 'Paid' },\n { value: 'NOT_REQUIRED', label: 'No fee' },\n { value: 'REFUNDED', label: 'Refunded' },\n];\n\nconst secondaryButton =\n 'inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50';\nconst primaryButton =\n 'inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50';\n\nexport function RegistrationFormDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo, basePath } = useAthleteBridge();\n const form = useRegistrationForm(id);\n const lifecycle = useRegistrationFormLifecycle();\n\n const [status, setStatus] = useState<AthRegistrationSubmissionStatus | ''>('');\n const [paymentStatus, setPaymentStatus] = useState<AthRegistrationPaymentStatus | ''>('');\n const [page, setPage] = useState(0);\n const [copied, setCopied] = useState(false);\n const [actionError, setActionError] = useState<string | null>(null);\n\n const submissions = useRegistrationSubmissions(\n {\n formId: id,\n status: status || undefined,\n paymentStatus: paymentStatus || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n },\n Boolean(id)\n );\n\n if (form.isLoading) {\n return (\n <PageLayout title=\"Registration form\">\n <LoadingState label=\"Loading registration form…\" />\n </PageLayout>\n );\n }\n if (form.isError || !form.data || !id) {\n return (\n <PageLayout title=\"Registration form\">\n <ErrorState\n title=\"Registration form not found\"\n message=\"This form does not exist, was archived, or you do not have access to it.\"\n onRetry={() => navigateTo('/registrations')}\n />\n </PageLayout>\n );\n }\n\n const data = form.data;\n const shareUrl = registrationShareUrl(window.location.origin, basePath ?? '/', data.slug);\n const shareable = data.status === 'PUBLISHED' || data.status === 'CLOSED';\n\n const run = async (action: RegistrationFormLifecycleAction) => {\n setActionError(null);\n if (\n action === 'archive' &&\n !window.confirm('Archive this form? Its registrations and payments are kept.')\n )\n return;\n try {\n await lifecycle.mutateAsync({ id, action });\n if (action === 'archive') navigateTo('/registrations');\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'That action failed.');\n }\n };\n\n const copyLink = async () => {\n try {\n await navigator.clipboard.writeText(shareUrl);\n setCopied(true);\n window.setTimeout(() => setCopied(false), 2000);\n } catch {\n setActionError('Could not copy — select the link and copy it manually.');\n }\n };\n\n const items = submissions.data?.items ?? [];\n\n return (\n <PageLayout\n title={\n <span className=\"flex flex-wrap items-center gap-3\">\n {data.title} <StatusBadge status={data.status} />\n {data.status === 'PUBLISHED' && !data.isOpen && (\n <StatusBadge status=\"Not open\" variant=\"warning\" />\n )}\n </span>\n }\n description={[\n data.season,\n `${data.submissionCount} registered${data.maxSubmissions ? ` of ${data.maxSubmissions}` : ''}`,\n data.closesAt ? `closes ${formatDate(data.closesAt)}` : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n className={secondaryButton}\n onClick={() => navigateTo('/registrations')}\n >\n <ArrowLeft className=\"h-4 w-4\" /> All forms\n </button>\n <button\n type=\"button\"\n className={secondaryButton}\n onClick={() => navigateTo(`/registrations/${id}/edit`)}\n >\n <Pencil className=\"h-4 w-4\" /> Edit\n </button>\n <button\n type=\"button\"\n className={secondaryButton}\n onClick={() => navigateTo(`/registrations/${id}/record`)}\n >\n <UserPlus className=\"h-4 w-4\" /> Record registration\n </button>\n {(data.status === 'DRAFT' || data.status === 'CLOSED') && (\n <button\n type=\"button\"\n className={primaryButton}\n disabled={lifecycle.isPending}\n onClick={() => void run('publish')}\n >\n <Send className=\"h-4 w-4\" /> {data.status === 'CLOSED' ? 'Re-open' : 'Publish'}\n </button>\n )}\n {data.status === 'PUBLISHED' && (\n <button\n type=\"button\"\n className={secondaryButton}\n disabled={lifecycle.isPending}\n onClick={() => void run('close')}\n >\n <Lock className=\"h-4 w-4\" /> Close\n </button>\n )}\n <button\n type=\"button\"\n className={secondaryButton}\n disabled={lifecycle.isPending}\n onClick={() => void run('archive')}\n >\n <Archive className=\"h-4 w-4\" /> Archive\n </button>\n </div>\n }\n >\n {actionError && (\n <p role=\"alert\" className=\"mb-4 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {shareable ? (\n <section\n className=\"mb-6 rounded-lg border border-border-seam bg-bg-surface p-4\"\n aria-label=\"Share link\"\n >\n <p className=\"mb-2 text-sm font-medium text-text-primary\">Share link for families</p>\n <div className=\"flex flex-wrap items-center gap-2\">\n <code\n className=\"min-w-0 flex-1 truncate rounded-md bg-bg-sunken px-3 py-2 text-xs text-text-secondary\"\n data-testid=\"registration-share-url\"\n >\n {shareUrl}\n </code>\n <button type=\"button\" className={secondaryButton} onClick={() => void copyLink()}>\n {copied ? <Check className=\"h-4 w-4\" /> : <Copy className=\"h-4 w-4\" />}{' '}\n {copied ? 'Copied' : 'Copy'}\n </button>\n </div>\n <p className=\"mt-2 text-xs text-text-secondary\">\n Families sign in to AthleteBridge with any account, fill in the form and track their\n registration under “My registrations”.\n </p>\n </section>\n ) : (\n <p className=\"mb-6 rounded-lg border border-border-seam bg-bg-sunken p-4 text-sm text-text-secondary\">\n This form is a draft. Publish it to get a share link families can use.\n </p>\n )}\n\n <PaymentSummaryPanel formId={id} />\n\n <div className=\"mb-4 flex flex-wrap items-center gap-3\">\n <h2 className=\"mr-auto text-base font-semibold text-text-primary\">Registrations</h2>\n <FilterSelect\n ariaLabel=\"Filter by review status\"\n allLabel=\"All statuses\"\n value={status}\n onChange={(v) => {\n setStatus(v);\n setPage(0);\n }}\n options={STATUS_OPTS}\n />\n <FilterSelect\n ariaLabel=\"Filter by payment status\"\n allLabel=\"All payments\"\n value={paymentStatus}\n onChange={(v) => {\n setPaymentStatus(v);\n setPage(0);\n }}\n options={PAYMENT_OPTS}\n />\n </div>\n\n {submissions.isLoading ? (\n <LoadingState label=\"Loading registrations…\" />\n ) : submissions.isError ? (\n <ErrorState onRetry={() => void submissions.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<UserPlus className=\"h-10 w-10\" />}\n title=\"No registrations yet\"\n description={\n status || paymentStatus\n ? 'No registrations match these filters.'\n : 'Share the link with families, or record a paper registration yourself.'\n }\n />\n ) : (\n <>\n <div className=\"overflow-x-auto rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\" data-testid=\"registration-submissions\">\n <thead className=\"bg-bg-sunken text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2 font-medium\">Participant</th>\n <th className=\"px-4 py-2 font-medium\">Submitted</th>\n <th className=\"px-4 py-2 font-medium\">Status</th>\n <th className=\"px-4 py-2 font-medium\">Payment</th>\n <th className=\"px-4 py-2 text-right font-medium\">Outstanding</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam bg-bg-surface\">\n {items.map((row) => (\n <tr\n key={row.id}\n tabIndex={0}\n role=\"link\"\n onClick={() => navigateTo(`/registrations/${id}/submissions/${row.id}`)}\n onKeyDown={(e) => {\n if (e.key === 'Enter')\n navigateTo(`/registrations/${id}/submissions/${row.id}`);\n }}\n className=\"cursor-pointer text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <td className=\"px-4 py-2\">\n <span className=\"block font-medium\">{row.participantName}</span>\n <span className=\"block text-xs text-text-secondary\">\n {row.source === 'STAFF_ENTRY'\n ? 'Entered by staff'\n : (row.contactEmail ?? 'Via share link')}\n </span>\n </td>\n <td className=\"px-4 py-2 text-text-secondary\">{formatDate(row.submittedAt)}</td>\n <td className=\"px-4 py-2\">\n <StatusBadge status={row.status} />\n </td>\n <td className=\"px-4 py-2\">\n <StatusBadge status={row.paymentStatus} />\n </td>\n <td className=\"px-4 py-2 text-right tabular-nums\">\n {formatMoney(row.amountOutstanding, row.currency)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={submissions.data?.pageInfo.hasMore ?? false}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration forms list (club staff)
|
|
3
|
+
* @module atheletebridge/pages/registrations
|
|
4
|
+
*
|
|
5
|
+
* Every registration form in the workspace with its lifecycle state, intake
|
|
6
|
+
* and the workspace-wide fee reconciliation summary (BOFF-4135).
|
|
7
|
+
*/
|
|
8
|
+
export declare function RegistrationFormsListPage(): import("react").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=RegistrationFormsListPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegistrationFormsListPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/registrations/RegistrationFormsListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA0BH,wBAAgB,yBAAyB,gCA0HxC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useAbTranslate as e } from "../../i18n.js";
|
|
2
|
+
import { useAthleteBridge as t } from "../../context/AthleteBridgeContext.js";
|
|
3
|
+
import { PageLayout as n } from "../../components/PageLayout.js";
|
|
4
|
+
import { StatusBadge as r } from "../../components/StatusBadge.js";
|
|
5
|
+
import { formatDate as i } from "../../utils/formatters.js";
|
|
6
|
+
import { EmptyState as a } from "../../components/EmptyState.js";
|
|
7
|
+
import { ErrorState as o, LoadingState as s } from "../../components/LoadingState.js";
|
|
8
|
+
import { CursorPager as c } from "../../components/CursorPager.js";
|
|
9
|
+
import { FilterSelect as l } from "../../components/FilterSelect.js";
|
|
10
|
+
import { useRegistrationForms as u } from "../../hooks/useRegistrationApi.js";
|
|
11
|
+
import { formatMoney as d } from "../../utils/money.js";
|
|
12
|
+
import { PaymentSummaryPanel as f } from "../../components/registration/PaymentSummaryPanel.js";
|
|
13
|
+
import { useState as p } from "react";
|
|
14
|
+
import { ClipboardList as m, Plus as h } from "lucide-react";
|
|
15
|
+
import { Fragment as g, jsx as _, jsxs as v } from "react/jsx-runtime";
|
|
16
|
+
//#region src/atheletebridge/pages/registrations/RegistrationFormsListPage.tsx
|
|
17
|
+
var y = 20, b = [
|
|
18
|
+
{
|
|
19
|
+
value: "DRAFT",
|
|
20
|
+
label: "Draft"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
value: "PUBLISHED",
|
|
24
|
+
label: "Published"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
value: "CLOSED",
|
|
28
|
+
label: "Closed"
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
function x() {
|
|
32
|
+
let x = e(), { navigateTo: S } = t(), [C, w] = p(""), [T, E] = p(0), D = u({
|
|
33
|
+
status: C || void 0,
|
|
34
|
+
pagination: {
|
|
35
|
+
limit: y,
|
|
36
|
+
offset: T * y
|
|
37
|
+
}
|
|
38
|
+
}), O = D.data?.items ?? [], k = /* @__PURE__ */ v("button", {
|
|
39
|
+
type: "button",
|
|
40
|
+
onClick: () => S("/registrations/new"),
|
|
41
|
+
className: "inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90",
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ _(h, { className: "h-4 w-4" }),
|
|
44
|
+
" ",
|
|
45
|
+
x("atheletebridge.pages.registrations.new", "New registration form")
|
|
46
|
+
]
|
|
47
|
+
});
|
|
48
|
+
return /* @__PURE__ */ v(n, {
|
|
49
|
+
title: x("atheletebridge.pages.registrations.title", "Registrations"),
|
|
50
|
+
description: x("atheletebridge.pages.registrations.description", "Season, team and program registration forms, the families who signed up, and the fees you have collected."),
|
|
51
|
+
hero: !0,
|
|
52
|
+
gradientTitle: !0,
|
|
53
|
+
actions: k,
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ _(f, {}),
|
|
56
|
+
/* @__PURE__ */ _("div", {
|
|
57
|
+
className: "mb-4 flex flex-wrap items-center gap-3",
|
|
58
|
+
children: /* @__PURE__ */ _(l, {
|
|
59
|
+
ariaLabel: x("atheletebridge.pages.registrations.filterStatus", "Filter by status"),
|
|
60
|
+
allLabel: x("atheletebridge.pages.registrations.allStatuses", "All statuses"),
|
|
61
|
+
value: C,
|
|
62
|
+
onChange: (e) => {
|
|
63
|
+
w(e), E(0);
|
|
64
|
+
},
|
|
65
|
+
options: b
|
|
66
|
+
})
|
|
67
|
+
}),
|
|
68
|
+
D.isLoading ? /* @__PURE__ */ _(s, { label: x("atheletebridge.pages.registrations.loading", "Loading registration forms…") }) : D.isError ? /* @__PURE__ */ _(o, { onRetry: () => void D.refetch() }) : O.length === 0 ? /* @__PURE__ */ _(a, {
|
|
69
|
+
illustration: "empty-data",
|
|
70
|
+
icon: /* @__PURE__ */ _(m, { className: "h-10 w-10" }),
|
|
71
|
+
title: x("atheletebridge.pages.registrations.empty", "No registration forms yet"),
|
|
72
|
+
description: x("atheletebridge.pages.registrations.emptyHint", "Create a form with the questions and fees for your season, publish it, and share the link with families."),
|
|
73
|
+
action: k
|
|
74
|
+
}) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _("ul", {
|
|
75
|
+
className: "divide-y divide-border-seam overflow-hidden rounded-lg border border-border-seam bg-bg-surface",
|
|
76
|
+
children: O.map((e) => {
|
|
77
|
+
let t = e.fees.filter((e) => e.feeType === "REQUIRED").reduce((e, t) => e + t.amount, 0);
|
|
78
|
+
return /* @__PURE__ */ _("li", { children: /* @__PURE__ */ v("button", {
|
|
79
|
+
type: "button",
|
|
80
|
+
onClick: () => S(`/registrations/${e.id}`),
|
|
81
|
+
className: "flex w-full flex-wrap items-center justify-between gap-3 px-4 py-3 text-left transition-colors hover:bg-bg-sunken",
|
|
82
|
+
children: [/* @__PURE__ */ v("span", {
|
|
83
|
+
className: "min-w-0",
|
|
84
|
+
children: [/* @__PURE__ */ _("span", {
|
|
85
|
+
className: "block truncate font-medium text-text-primary",
|
|
86
|
+
children: e.title
|
|
87
|
+
}), /* @__PURE__ */ v("span", {
|
|
88
|
+
className: "block text-xs text-text-secondary",
|
|
89
|
+
children: [[
|
|
90
|
+
e.season,
|
|
91
|
+
`${e.submissionCount} registered`,
|
|
92
|
+
e.maxSubmissions ? `cap ${e.maxSubmissions}` : null
|
|
93
|
+
].filter(Boolean).join(" · "), e.closesAt ? ` · closes ${i(e.closesAt, {
|
|
94
|
+
hour: void 0,
|
|
95
|
+
minute: void 0
|
|
96
|
+
})}` : ""]
|
|
97
|
+
})]
|
|
98
|
+
}), /* @__PURE__ */ v("span", {
|
|
99
|
+
className: "flex items-center gap-3",
|
|
100
|
+
children: [
|
|
101
|
+
/* @__PURE__ */ _("span", {
|
|
102
|
+
className: "text-sm tabular-nums text-text-secondary",
|
|
103
|
+
children: t > 0 ? d(t, e.currency) : "Free"
|
|
104
|
+
}),
|
|
105
|
+
e.status === "PUBLISHED" && !e.isOpen ? /* @__PURE__ */ _(r, {
|
|
106
|
+
status: "Not open",
|
|
107
|
+
variant: "warning"
|
|
108
|
+
}) : null,
|
|
109
|
+
/* @__PURE__ */ _(r, { status: e.status })
|
|
110
|
+
]
|
|
111
|
+
})]
|
|
112
|
+
}) }, e.id);
|
|
113
|
+
})
|
|
114
|
+
}), /* @__PURE__ */ _(c, {
|
|
115
|
+
className: "mt-6",
|
|
116
|
+
page: T,
|
|
117
|
+
hasMore: D.data?.pageInfo.hasMore ?? !1,
|
|
118
|
+
onPrev: () => E((e) => Math.max(0, e - 1)),
|
|
119
|
+
onNext: () => E((e) => e + 1)
|
|
120
|
+
})] })
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
export { x as RegistrationFormsListPage };
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=RegistrationFormsListPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegistrationFormsListPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/registrations/RegistrationFormsListPage.tsx"],"sourcesContent":["/**\n * Registration forms list (club staff)\n * @module atheletebridge/pages/registrations\n *\n * Every registration form in the workspace with its lifecycle state, intake\n * and the workspace-wide fee reconciliation summary (BOFF-4135).\n */\n\nimport { useState } from 'react';\nimport { ClipboardList, Plus } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FilterSelect } from '../../components/FilterSelect';\nimport { CursorPager } from '../../components/CursorPager';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { PaymentSummaryPanel } from '../../components/registration/PaymentSummaryPanel';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useRegistrationForms } from '../../hooks/useRegistrationApi';\nimport { useAbTranslate } from '../../i18n';\nimport { formatDate } from '../../utils/formatters';\nimport { formatMoney } from '../../utils/money';\nimport type { AthRegistrationFormStatus } from '../../types/registration';\n\nconst PAGE_SIZE = 20;\n\nconst STATUS_OPTS: ReadonlyArray<{ value: AthRegistrationFormStatus; label: string }> = [\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'CLOSED', label: 'Closed' },\n];\n\nexport function RegistrationFormsListPage() {\n const tr = useAbTranslate();\n const { navigateTo } = useAthleteBridge();\n const [status, setStatus] = useState<AthRegistrationFormStatus | ''>('');\n const [page, setPage] = useState(0);\n\n const forms = useRegistrationForms({\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const items = forms.data?.items ?? [];\n\n const newButton = (\n <button\n type=\"button\"\n onClick={() => navigateTo('/registrations/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.registrations.new', 'New registration form')}\n </button>\n );\n\n return (\n <PageLayout\n title={tr('atheletebridge.pages.registrations.title', 'Registrations')}\n description={tr(\n 'atheletebridge.pages.registrations.description',\n 'Season, team and program registration forms, the families who signed up, and the fees you have collected.'\n )}\n hero\n gradientTitle\n actions={newButton}\n >\n <PaymentSummaryPanel />\n\n <div className=\"mb-4 flex flex-wrap items-center gap-3\">\n <FilterSelect\n ariaLabel={tr('atheletebridge.pages.registrations.filterStatus', 'Filter by status')}\n allLabel={tr('atheletebridge.pages.registrations.allStatuses', 'All statuses')}\n value={status}\n onChange={(v) => {\n setStatus(v);\n setPage(0);\n }}\n options={STATUS_OPTS}\n />\n </div>\n\n {forms.isLoading ? (\n <LoadingState\n label={tr('atheletebridge.pages.registrations.loading', 'Loading registration forms…')}\n />\n ) : forms.isError ? (\n <ErrorState onRetry={() => void forms.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title={tr('atheletebridge.pages.registrations.empty', 'No registration forms yet')}\n description={tr(\n 'atheletebridge.pages.registrations.emptyHint',\n 'Create a form with the questions and fees for your season, publish it, and share the link with families.'\n )}\n action={newButton}\n />\n ) : (\n <>\n <ul className=\"divide-y divide-border-seam overflow-hidden rounded-lg border border-border-seam bg-bg-surface\">\n {items.map((form) => {\n const total = form.fees\n .filter((fee) => fee.feeType === 'REQUIRED')\n .reduce((sum, fee) => sum + fee.amount, 0);\n return (\n <li key={form.id}>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/registrations/${form.id}`)}\n className=\"flex w-full flex-wrap items-center justify-between gap-3 px-4 py-3 text-left transition-colors hover:bg-bg-sunken\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium text-text-primary\">\n {form.title}\n </span>\n <span className=\"block text-xs text-text-secondary\">\n {[\n form.season,\n `${form.submissionCount} registered`,\n form.maxSubmissions ? `cap ${form.maxSubmissions}` : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n {form.closesAt\n ? ` · closes ${formatDate(form.closesAt, { hour: undefined, minute: undefined })}`\n : ''}\n </span>\n </span>\n <span className=\"flex items-center gap-3\">\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {total > 0 ? formatMoney(total, form.currency) : 'Free'}\n </span>\n {form.status === 'PUBLISHED' && !form.isOpen ? (\n <StatusBadge status=\"Not open\" variant=\"warning\" />\n ) : null}\n <StatusBadge status={form.status} />\n </span>\n </button>\n </li>\n );\n })}\n </ul>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={forms.data?.pageInfo.hasMore ?? false}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|