Skip to content

feat(events): application enrollment with form builder#383

Open
AdryanneKelly wants to merge 4 commits into
feat/eventsfrom
events/application-enrollment-with-form-builder
Open

feat(events): application enrollment with form builder#383
AdryanneKelly wants to merge 4 commits into
feat/eventsfrom
events/application-enrollment-with-form-builder

Conversation

@AdryanneKelly

Copy link
Copy Markdown

Summary

Implements the Application enrollment method: the participant fills a dynamic form defined by the organizer, the enrollment lands as pending, and the organizer approves or rejects it from the Admin panel. Reuses EnrollUserAction as the shared entrypoint (RSVP path untouched) and adds two new domain actions for the approval/rejection decision.

Flow: participant submits form → EnrollUserAction (pending, no capacity check) → Admin approves/rejects → ApproveApplicationAction (capacity check + confirm/waitlist) or RejectApplicationAction (reason + rejected)

Closes #237

What changed — app-modules/events

File Role
src/Enrollment/Actions/EnrollUserAction.php Application path: creates pending enrollment, validates application_data against the policy's schema (per-type: required, select/checkbox options), skips capacity check on submission.
src/Enrollment/Actions/ApproveApplicationAction.php Validates status is pending, capacity check with lockForUpdate, transitions to confirmed/waitlisted (or throws if full with no waitlist), dispatches EnrollmentConfirmed/EnrollmentWaitlisted, audit trail (triggered_by=admin, actor_id).
src/Enrollment/Actions/RejectApplicationAction.php Validates status is pending, stores rejection_reason, transitions to rejected, audit trail with reason.
src/Enrollment/DTOs/ApproveApplicationDTO.php, RejectApplicationDTO.php New DTOs for the two admin actions.
src/Enrollment/DTOs/EnrollUserDTO.php Adds applicationData payload.
src/Enrollment/Exceptions/EnrollmentException.php Adds enrollmentNotPending() and applicationDataInvalid() factories.
src/Enrollment/Models/Enrollment.php, EnrollmentPolicy.php Corrects @property typing for application_data / application_schema (list, not associative array) — removes a PHPStan type-mismatch cascade in the new validation code.
database/factories/EnrollmentPolicyFactory.php, database/seeders/EventsSeeder.php application() factory state + seed data covering all 4 field types (text/textarea/select/checkbox) across two Application-method events.
lang/{en,pt_BR}/{exceptions,pages}.php New user-facing strings.

What changed — app-modules/panel-admin

File Role
Filament/Resources/Events/Schemas/EventForm.php Repeater-based form builder for application_schema (type/label/required/options), visible only when enrollment_method = Application.
Filament/Resources/Events/RelationManagers/Actions/ApproveApplicationAction.php, RejectApplicationAction.php Table row actions — approve (confirmation modal) / reject (reason textarea) — call into the domain actions.
Filament/Resources/Events/RelationManagers/EnrollmentsRelationManager.php Wires the two actions plus a "View Application" modal on pending/decided rows.
resources/views/enrollments/application-data.blade.php Read-only rendering of submitted answers (boolean, array, and scalar aware).

What changed — app-modules/panel-app

File Role
Livewire/Events/EventDetail.php canApply computed prop, apply() action, pre-initializes checkbox-group state as arrays in mount().
resources/views/livewire/events/event-detail.blade.php Dynamic form rendered per field type; pending/rejected status views showing the submitted answers / rejection reason.

Design notes (the non-obvious bits)

checkbox = multi-select group, not a single boolean. The issue's schema ({type, label, required, options?}) doesn't spell this out, so it was ambiguous whether checkbox meant a single yes/no toggle or a group of checkboxes sharing one options list (like select, but multi-value). Went with the multi-select reading — checkbox now shows the same options builder as select in the Admin, and stores an array of selected values. Validation treats required as "at least one selected" and checks each selection against options.

Livewire needs the array pre-seeded. Multiple <input type="checkbox" wire:model="applicationFormData.{index}" value="..."> bound to the same array path only get grouped into an array by Livewire's front end if that path is already an array before the first interaction — otherwise every checkbox in the group ends up sharing one boolean and checking one checks them all. EventDetail::mount() now seeds applicationFormData[$index] = [] for every checkbox field up front.

Filament enum-cast gotcha. Select::make('enrollment_method')->options(EnrollmentMethod::class) auto-applies an EnumStateCast, so $get('enrollment_method') inside visible() closures returns the enum instance, not ->value. The Repeater's visibility check was comparing against the string value and silently never showed — fixed by comparing against the enum case directly.

Acceptance criteria

  • Organizer can define a dynamic form schema via Filament form builder in Admin
  • Participant sees and fills the dynamic form in App panel
  • Submission creates enrollment with status pending and application_data populated
  • Organizer can approve → enrollment transitions to confirmed (or waitlisted if full)
  • Organizer can reject with reason → enrollment transitions to rejected with reason stored
  • Capacity is checked on approval, not on submission
  • Application data is displayed in Admin enrollment detail
  • Transition audit trail records approver/rejector identity
  • Feature tests: submit application, approve, reject, approve when full (waitlist), approve when full (no waitlist)
  • Pint passes

Test plan

# Application enrollment tests
vendor/bin/pest app-modules/events/tests/Feature/EnrollUserActionTest.php \
  app-modules/events/tests/Feature/Enrollment/ApproveApplicationActionTest.php \
  app-modules/events/tests/Feature/Enrollment/RejectApplicationActionTest.php \
  app-modules/panel-app/tests/Feature/Events/ApplicationEnrollmentTest.php

# Full events + panel-app suite (must stay green)
vendor/bin/pest app-modules/events/tests app-modules/panel-app/tests

# Style
vendor/bin/pint --test app-modules/events app-modules/panel-admin app-modules/panel-app

Results:

  • Application enrollment tests: 34/34
  • events + panel-app suite: 181/181 (0 regressions)
  • Pint: clean

Scenarios covered: submit → pending + audit trail · missing applicationData → exception · missing required field → exception · no schema defined → pending · event past → rejected · approve → confirmed + audit trail · approve at capacity + waitlist → waitlisted · approve at capacity, no waitlist → event full exception · approve non-pending → exception · no capacity limit → always confirms · reject with reason → rejected · reject non-pending → exception · reject preserves application_data · apply button rendering · canApply/canConfirmPresence flags · pending status shows submitted answers · rejected shows reason · already applied → no apply button · missing required field → error notification.

Note on PHPStan: the module already carried ~172 pre-existing errors before this branch (untyped test-helper array params, Pest fluent-call false positives from Larastan) — out of scope here. This PR's own new type mismatch (EnrollUserAction::validateApplicationData against the application_schema/application_data docblocks) was fixed; remaining new findings are the same pre-existing categories, consistent with every other test file in the suite.

Out of scope (separate issues)

  • Admin bulk actions on pending applications (filter/view exist; bulk approve/reject not implemented).
  • Editing a submitted application before it's reviewed.

AdryanneKelly and others added 2 commits July 1, 2026 20:47
These were typed as associative arrays but are actually lists (schema
is indexed by field position, data by matching index), which caused
PHPStan to flag EnrollUserAction::validateApplicationData as dead code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@AdryanneKelly AdryanneKelly self-assigned this Jul 2, 2026
@AdryanneKelly AdryanneKelly added the enhancement New feature or request label Jul 2, 2026
@AdryanneKelly AdryanneKelly linked an issue Jul 2, 2026 that may be closed by this pull request
10 tasks
@AdryanneKelly AdryanneKelly changed the title Events/application enrollment with form builder feat(events): application enrollment with form builder Jul 2, 2026

@GabrielFVDev GabrielFVDev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Se der para ajustar isso, ao inves de trazer o id do usuario, ser o nome dele.
Fica show!

De resto, LGTM

…or choice fields

EnrollmentsRelationManager used recordTitleAttribute('id'), which reads
plain attributes only, so the delete confirmation showed the enrollment's
UUID instead of the participant's name — switched to recordTitle() with
a closure that reads the related user.

The application form builder let organizers save select/checkbox
questions with zero options, producing an empty dropdown or checkbox
group the participant couldn't fill.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@BrunaDomingues BrunaDomingues left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importante — strings hardcoded em inglês (i18n)

Onde:

image

Por quê: módulo events já tem lang/en e lang/pt_BR para pages/exceptions; Admin/App ficam inconsistentes.

Sugestão: mover para events::pages.* ou chaves Filament traduzíveis (__()), como no resto do módulo.

Approve/Reject action labels, modal text, and success notifications
(Admin), the answers viewer (Admin: rejection reason, yes/no, no answer),
and the "select an option" placeholder (App) were plain English literals,
inconsistent with the rest of the module's events::pages translations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@BrunaDomingues BrunaDomingues left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Só deixei o comentário das traduções mas de resto tá top!

@davicbtoliveira davicbtoliveira left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(events): application enrollment with form builder

4 participants