Skip to content

CE cutover: reroute intake/display, drop ce_* columns (PR 2)#1917

Merged
jmilljr24 merged 58 commits into
mainfrom
maebeale/continuing-education-cutover
Jul 11, 2026
Merged

CE cutover: reroute intake/display, drop ce_* columns (PR 2)#1917
jmilljr24 merged 58 commits into
mainfrom
maebeale/continuing-education-cutover

Conversation

@maebeale

@maebeale maebeale commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

🤖 PR, suggested 👤 review level: 🔬 Inspect — reroutes CE intake/display onto the new models, drops the old ce_*/credentials columns, and adds the admin CE management + professional-license surfaces

Foundation #1916 (the CE models) and #1833 (registrants-index CE column + filter) have both merged; this branch is rebased onto main and reconciled with them.

What is the goal of this PR and why is this important?

#1916 added ProfessionalLicense + ContinuingEducationRegistration but left intake, the callout, and read sites on the flat EventRegistration#ce_* columns. This PR completes the cutover, drops those columns, and builds out the admin + registrant surfaces for managing CE and professional licenses.

Cutover (intake, callout, read sites)

  • Intake — the CE-interest answer becomes a plain Yes/No opt-in (hours come from the event); opting in creates a CE registration against a found-or-created license.
  • Public callout — a registrant license form (type, number, US-states issuing-state dropdown, expiry), required vs (optional) markers, greyed placeholders.
  • Read sites — magic-ticket card, reminders, onboarding, CSV rerouted onto the new models; #ce_hours page gated on ce_eligible?.
  • EventRegistration aggregatorsce_requested?, ce_hours_total, ce_amount_owed_cents, ce_license_provided?, ce_paid_in_full?, ce_status_label over the CE registrations; ce_status filter scope.
  • Drop the ce_credit_requested / ce_hours_requested / ce_license_number columns (+ the legacy credentials column on people) and dead methods.

Admin CE management

  • Dedicated CE registration new/edit page (ContinuingEducationRegistrationsController + policy): license type/number/issuing-state/expiry, hours + cost (prefilled from the event), certificate issued toggle, and delete.
  • Cross-links both ways: admin "Registrant's CE page" / "Registrant's scholarship page" pills, with the registrant callouts showing an "Edit" pill and a "← Back to CE registration / scholarship" eyebrow when reached from admin.

Professional licenses on the person form + edit gating

  • Licenses (ProfessionalLicense: type, number, issuing-state, expiry) are managed on the person form; uniqueness scoped to (person, kind, number).
  • Gating staged for owner self-service (PersonPolicy flip is the only remaining step): a license is editable by an admin, or by its holder only while it has no CE registrations; once any CE reg exists it locks to admins — rendered read-only with admin-only styling. Enforced in three layers — ProfessionalLicensePolicy, the view, and a server-side backstop in PeopleController#update.
  • A license is never deletable once it has any CE registration (paid or not), since deletion would cascade away CE history.

Registration edit + card polish

  • Section-header icons tint only when their section has data (section_icon_class); "Requested" toggles are pure-CSS in their theme colours.
  • Stimulus: added ce_license_picker, date_placeholder; removed ce_credit_requested, scholarship_requested (folded into CSS).

Migrations

Four, timestamped to run after the foundation migrations now in main: add ce_requested, remove the ce_* columns, remove credentials from people, and scope license uniqueness to kind. All CE/credentials data is test data, so this is a clean cutover (no backfill).

Reconciliation with #1833 (merged to main mid-flight)

#1833 built the registrants-index scaffolding on the old columns. This PR keeps that scaffolding and repoints the data: ce_status is now a derived scope over the CE registrations (needs_license / requested / paid / issued / not_issued); the roster column gates on a CE record existing; ReminderRecipientFilter CE matching flows through the shared scope.

# removes any CE registrations; otherwise the registrant's (single) CE
# registration is found-or-built against the licence for the typed number, with
# the editable hours applied.
def reconcile_ce_registration

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 From Claude: The admin CE section posts under a separate ce namespace and is reconciled here rather than via accepts_nested_attributes_for. Editing a license number through the CE registration means touching a separate ProfessionalLicense (find-or-create), which nested attributes handle fragilely; this keeps it explicit and covers the common single-license case (multi-license is managed elsewhere).

# Read CE registrations from the in-memory collection rather than the DB when
# it's already loaded or this registration isn't persisted (e.g. the unsaved
# sample-ticket preview builds CE registrations without saving).
def ce_registrations_in_memory?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 From Claude: The aggregators read the in-memory collection when the registration is a new_record? so the unsaved sample-ticket preview (which builds a CE registration without saving) still renders a populated CE card — otherwise exists? would query the DB with a nil id and report no CE.

}
# Filter by CE state. "needs_license" is derived (a CE registration sits on a
# placeholder license); the rest match a ContinuingEducationRegistration#status.
scope :ce_status, ->(value) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 From Claude: needs_license is derived (a CE registration on a placeholder license, number IS NULL); the other options match a ContinuingEducationRegistration#status. EXISTS subqueries so a registration with several CE registrations matches if any one qualifies.

# registrant's (first) CE registration via a found-or-created license, mirrors
# it onto the registration's form answer, then returns to the callout. Plain
# full-page POST — no Turbo.
def update_ce_license

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 From Claude: Public license entry, authorized by the registration slug (the same bearer-token model as the other callouts). Plain full-page POST (no Turbo) — it sets the number on the first CE registration via find-or-create and mirrors it onto the form answer.

@maebeale
maebeale marked this pull request as ready for review June 25, 2026 20:18
@maebeale
maebeale force-pushed the maebeale/continuing-education-model branch from a752773 to 043d5ae Compare June 29, 2026 01:18
@maebeale
maebeale force-pushed the maebeale/continuing-education-cutover branch from bc77080 to a96a90d Compare June 29, 2026 01:46
@maebeale
maebeale force-pushed the maebeale/continuing-education-cutover branch from 1c1c1b4 to 97c480c Compare June 29, 2026 03:19
@maebeale
maebeale force-pushed the maebeale/continuing-education-model branch 2 times, most recently from abaee56 to a7e7d3f Compare June 29, 2026 10:40
Base automatically changed from maebeale/continuing-education-model to main June 29, 2026 12:18
@maebeale
maebeale force-pushed the maebeale/continuing-education-cutover branch 4 times, most recently from 326da62 to 7aef3fa Compare June 29, 2026 17:06
@maebeale
maebeale marked this pull request as draft June 29, 2026 17:41
@maebeale maebeale changed the title CE cutover: reroute intake/display, drop ce_* columns (PR 2) HOLD: CE cutover: reroute intake/display, drop ce_* columns (PR 2) Jun 29, 2026
@maebeale
maebeale force-pushed the maebeale/continuing-education-cutover branch from d596879 to d16bc05 Compare June 30, 2026 11:27
@maebeale
maebeale marked this pull request as ready for review June 30, 2026 22:23
@maebeale
maebeale force-pushed the maebeale/continuing-education-cutover branch 2 times, most recently from 4db3dc2 to 0b29237 Compare June 30, 2026 22:50
@maebeale maebeale changed the title HOLD: CE cutover: reroute intake/display, drop ce_* columns (PR 2) IN-PROGRESS: CE cutover: reroute intake/display, drop ce_* columns (PR 2) Jul 4, 2026
maebeale and others added 7 commits July 10, 2026 19:57
…umns

Completes the continuing-education cutover started by the foundation models
(#1916). Reroutes registration intake, the public callout, and every read
site (callouts, onboarding, reminders, CSV) off the flat
EventRegistration#ce_* columns and onto ContinuingEducationRegistration /
ProfessionalLicense, then drops the ce_credit_requested / ce_hours_requested /
ce_license_number columns.

Reconciled with #1833 (registrants-index CE column + filter), which merged to
main while this branch was in flight and built the same scaffolding on the
old columns:

- Kept #1833's scaffolding (CE column, dropdown filter, column toggles,
  layout, onboarding columns, bulk-reminder filter, CSV columns, eyebrow nav)
  and repointed every CE data read to the new models.
- EventRegistration.ce_status is now a derived scope over the CE registrations
  (needs_license / requested / paid / issued / not_issued), replacing the old
  ce_credit_requested-based buckets; the roster + reminder dropdowns follow it.
- EventRegistration#ce_status_label and the aggregators (ce_requested?,
  ce_hours_total, ce_amount_owed_cents, ce_license_provided?, ce_paid_in_full?)
  read the new models; the registrants column gates on a CE record existing
  rather than the dropped boolean (the "requested, no record yet" state is gone
  now that intake creates the record).
- ReminderRecipientFilter's CE matching flows through the shared ce_status
  scope (its per-record matchers were redundant with main's scope approach).

Migration timestamped to run after the foundation migrations now in main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make continuing education mirror scholarships:
- Add a stored ce_requested flag on event_registration (joins the other
  *_requested flags); intake sets it; the registration edit form drives it.
- The CE card shows a Requested toggle only when no CE registration exists;
  saving it on creates the record (against the selected/only license, else a
  placeholder), with a flash. Once a record exists the card shows a summary +
  Issued/Not-issued pill + an Edit link, gated on the event being ce_eligible?.
- New ContinuingEducationRegistrationsController + policy + edit page: edit
  license (promoting a placeholder in place), hours, cost; a Certificate
  issued toggle (certificate_sent_at); and removal guarded against payments.
- Rename the derived ce_requested? (record-exists) to ce_registered? to free
  the name for the column; readers/views key off whichever they mean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These configure whether an event grants CE and at what cost, so they belong
with the event's details rather than buried in the registration-ticket
callouts panel. The CE-hours ticket card (its label + details text) stays in
the callouts panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CE card always has three rows (cost·hrs / License / chip) but the
scholarship card showed its funder line only when a grant existed, so its
status chip sat a row higher and the two orange chips didn't line up. Always
render the scholarship card's middle row — "Grantor: <name>" when funded,
an empty same-height spacer (padding) when not — so both chips align.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single-license registrant was still shown a one-option license dropdown.
Only render the selector when the registrant has more than one license on
file; with one the controller uses it, with none it creates an empty
(placeholder) license to fill in later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fter

The create state is now just the Requested toggle. Saving creates the CE
registration against the registrant's existing license (or an empty
placeholder), and the selected license is shown in the card summary and
editable on the CE registration's own page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 11, 2026 13:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Copilot AI review requested due to automatic review settings July 11, 2026 14:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Copilot AI review requested due to automatic review settings July 11, 2026 20:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Copilot AI review requested due to automatic review settings July 11, 2026 20:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Copilot AI review requested due to automatic review settings July 11, 2026 22:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Copilot AI review requested due to automatic review settings July 11, 2026 22:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Copilot AI review requested due to automatic review settings July 11, 2026 23:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

@jmilljr24 jmilljr24 changed the title IN-PROGRESS: CE cutover: reroute intake/display, drop ce_* columns (PR 2) CE cutover: reroute intake/display, drop ce_* columns (PR 2) Jul 11, 2026
@jmilljr24
jmilljr24 merged commit 1016b33 into main Jul 11, 2026
3 checks passed
@jmilljr24
jmilljr24 deleted the maebeale/continuing-education-cutover branch July 11, 2026 23:27
jmilljr24 added a commit that referenced this pull request Jul 12, 2026
* Fix 500 in rich text asset mentions for decorated records (#1961)

* Fix 500 in rich text asset mentions for decorated records

The rhino editor embedded the *decorator's* signed GlobalID, so the
mentions endpoint located an EventDecorator and called .rich_text_assets
on a record type that doesn't own the association, raising NoMethodError.
Emit the model's sgid and guard the association lookup so unsupported
records return an empty list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Use Draper.undecorate to unwrap decorated records

Per review: Draper.undecorate is the built-in for this and reads cleaner
than the inline decorated? check. Also apply it in the controller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add signed-in user to Honeybadger fault context (#1962)

Honeybadger faults didn't identify the affected user, so triage relied
on decoding request params. Attach user_id/user_email as context on
every authenticated request.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* use remote select on variation author (#1964)

* JM: Surface onboarding day-attendance checkmarks on the registration edit form (#1932)

* Surface onboarding day-attendance checkmarks on the registration edit form

The per-day attendance checkboxes only lived on the Onboarding matrix, so an
admin editing a single registration couldn't mark days without leaving the page.
Surface the same checkmarks beside the status dropdown, and mirror onboarding's
behavior so toggling a day rolls the attendance status forward/back (client-side,
respecting deliberate inactive states). DAY_FIELDS were already permitted params,
so the boxes save with the form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Move the days-attended checkmarks into the registration meta strip

Group them inline with the "View ticket" / "View submission" links instead of
under the status dropdown. The attendance-status controller now spans the whole
card so the checkmarks still drive the status select.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Right-align the days-attended group with its label stacked above

Push it to the right edge of the meta strip and stack the label over the day
chips, per the requested layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Wait for the save redirect before asserting the persisted status

The derivation spec reloaded the record immediately after clicking Save, racing
the submit round-trip and occasionally reading the pre-save status. Assert the
redirect landed first, matching the sibling persistence spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* add author to select if present (#1965)

* CE cutover: reroute intake/display, drop ce_* columns (PR 2) (#1917)

* CE cutover: reroute intake/display onto the new models, drop ce_* columns

Completes the continuing-education cutover started by the foundation models
(#1916). Reroutes registration intake, the public callout, and every read
site (callouts, onboarding, reminders, CSV) off the flat
EventRegistration#ce_* columns and onto ContinuingEducationRegistration /
ProfessionalLicense, then drops the ce_credit_requested / ce_hours_requested /
ce_license_number columns.

Reconciled with #1833 (registrants-index CE column + filter), which merged to
main while this branch was in flight and built the same scaffolding on the
old columns:

- Kept #1833's scaffolding (CE column, dropdown filter, column toggles,
  layout, onboarding columns, bulk-reminder filter, CSV columns, eyebrow nav)
  and repointed every CE data read to the new models.
- EventRegistration.ce_status is now a derived scope over the CE registrations
  (needs_license / requested / paid / issued / not_issued), replacing the old
  ce_credit_requested-based buckets; the roster + reminder dropdowns follow it.
- EventRegistration#ce_status_label and the aggregators (ce_requested?,
  ce_hours_total, ce_amount_owed_cents, ce_license_provided?, ce_paid_in_full?)
  read the new models; the registrants column gates on a CE record existing
  rather than the dropped boolean (the "requested, no record yet" state is gone
  now that intake creates the record).
- ReminderRecipientFilter's CE matching flows through the shared ce_status
  scope (its per-record matchers were redundant with main's scope approach).

Migration timestamped to run after the foundation migrations now in main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CE: scholarship-style card, ce_requested flag, and a CE edit page

Make continuing education mirror scholarships:
- Add a stored ce_requested flag on event_registration (joins the other
  *_requested flags); intake sets it; the registration edit form drives it.
- The CE card shows a Requested toggle only when no CE registration exists;
  saving it on creates the record (against the selected/only license, else a
  placeholder), with a flash. Once a record exists the card shows a summary +
  Issued/Not-issued pill + an Edit link, gated on the event being ce_eligible?.
- New ContinuingEducationRegistrationsController + policy + edit page: edit
  license (promoting a placeholder in place), hours, cost; a Certificate
  issued toggle (certificate_sent_at); and removal guarded against payments.
- Rename the derived ce_requested? (record-exists) to ce_registered? to free
  the name for the column; readers/views key off whichever they mean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Move CE hours-offered/cost fields under the calendar description

These configure whether an event grants CE and at what cost, so they belong
with the event's details rather than buried in the registration-ticket
callouts panel. The CE-hours ticket card (its label + details text) stays in
the callouts panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Align scholarship/CE card status chips with a consistent grantor row

The CE card always has three rows (cost·hrs / License / chip) but the
scholarship card showed its funder line only when a grant existed, so its
status chip sat a row higher and the two orange chips didn't line up. Always
render the scholarship card's middle row — "Grantor: <name>" when funded,
an empty same-height spacer (padding) when not — so both chips align.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CE card: only show the license selector when there's a real choice

A single-license registrant was still shown a one-option license dropdown.
Only render the selector when the registrant has more than one license on
file; with one the controller uses it, with none it creates an empty
(placeholder) license to fill in later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CE card: drop the license dropdown; pick license on create, show it after

The create state is now just the Requested toggle. Saving creates the CE
registration against the registrant's existing license (or an empty
placeholder), and the selected license is shown in the card summary and
editable on the CE registration's own page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CE card: drop the 'Saving creates the CE registration' hint

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CE toggle: amber-while-pending/teal track + teal card highlight when on

Reuse (and simplify) the ce-credit-requested Stimulus controller so the CE
Requested toggle behaves like scholarship's: the track turns amber while the
choice is pending, teal once stored on, gray when off — and the whole card
gets a teal ring while it's on. Drops the controller's dead targets (license/
hours/details fields were removed earlier), so it's no longer orphaned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Restyle the CE registration edit page to match the scholarship form

Wrap it in the continuing-education-themed (teal) outer card, use the shared
centered event page header (icon + title + event + registrant), and split the
fields, certificate, and removal into teal section cards with icon chips —
mirroring the scholarship edit layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Align the org/scholarship/CE card bottom actions on one row

The three cards in the registration row are equal-height (grid), but their
bottom elements floated at different heights. Make each card a flex column and
pin its bottom action (Connect organization link, scholarship chip, CE chip)
with mt-auto so they line up. Drops the now-unneeded scholarship grantor spacer
since mt-auto handles the alignment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Block deleting a registration whose CE registration has payments

Mirrors the CE-registration removal guard: deleting the registration would
cascade away a paid CE registration and its allocations, so refuse it with a
flash directing the admin to revert the payment first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Let a registrant request CE from the public callout

A registrant who didn't opt into CE at registration can now request it from
the CE callout: a Request CE credit button sets ce_requested and creates the
CE registration (placeholder license; the number is entered next).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Manage a person's professional licenses on the person edit page

Add a Professional licenses section (cocoon nested fields: number, type,
issuing state, expiry) with add/remove rows, mirroring affiliations/contact
methods. A license whose CE registrations carry payments can't be removed —
the remove control is replaced with a note, and a prepended before_destroy
guard backstops it so the dependent: :destroy cascade can't wipe paid CE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Move professional licenses below Background; drop the credentials field

Place the Professional licenses section right after Background on the person
edit form, with the fields and Add license button inside one bordered card.
Remove the now-redundant free-text Credentials input (licenses capture this);
the credentials column and its profile display toggle are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Show license type(s) as the profile credential suffix

Replace the removed free-text credentials field as the source of the profile
name suffix: when 'Show credentials' is on, append the person's distinct
professional-license types (e.g. ', LMFT, LCSW') after their name. Keeps the
profile_show_credentials toggle; the credentials column is now unused for display.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Group the registrant CE callout into labeled sections

The callout body was an unanchored stack of bordered blocks. Split it
into "Your CE credit", "Your professional license", and "Details"
sections so it reads top-to-bottom, lead with Hours/Cost and trail the
status pill, and align the Save button to the input height.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Flip the registrant license to read-only with an edit link

Once a license number is on file, show it read-only with an Edit link
rather than always exposing the form. The link reloads with
?editing=license to flip in the form (a plain full-page nav, no JS),
and saving redirects back to the read-only view. Surface a clearer
"Needs license #" status and a prompt when no number is on file yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix unclosed filter div and read CE hours from the registration

The CE-status filter rewrite accidentally dropped the closing tag for the
registrants search Row 2 wrapper, so the browser auto-closed it at the form
boundary and distorted the filter row. Restore it.

The ticket card subtitle read hours from the event default, diverging from
every other registrant-facing surface (CE page, onboarding, CSV) once an admin
customizes a registrant's hours. Read the registration's own total instead.

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

* Render the license Expires field through simple_form

The Expires date field was hand-rolled with f.label + f.date_field while the
sibling fields use simple_form (wrapper: false), so its label weight and the
raw native date input didn't match the rest of the row. Render it as an HTML5
date input via simple_form so the label and control line up with the others.

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

* Add issuing state + expiry to the CE license forms

The CE callout (registrant-facing) and CE registration edit (admin) pages only
captured the license type and number, while the person edit form captures the
full license (type, number, issuing state, expiry). Surface the same two extra
fields on both CE surfaces so a registrant or admin can complete the license in
the place they're already working, instead of bouncing to the person page.

assign_license now persists issuing_state and expires_on alongside number/kind.

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

* Match the card prompts and retire the CE-credit Stimulus controller

- Add scholarship link was fuchsia/text-sm; recolor to grey/text-xs so it
  matches the sibling Connect organization prompt.
- The CE card's empty (toggle-only) state had no bottom prompt, leaving it
  barren next to the other two — add a grey hint pinned to the bottom. CE has
  no separate award page (saving with Requested on creates the record), so it's
  a hint rather than a link.
- The CE box no longer needs JS: drop the ce-credit-requested Stimulus
  controller (and its index.js registration / AGENTS.md count) along with the
  data-controller wiring, and recolor the Requested toggle's checked state.

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

* Add deliberate "Add CE registration" flow + universal number formatter

Add CE registration flow (mirrors scholarship's new/create):
- New/create actions + routes + policy; a "+ Add CE registration" link on the
  registration card opens a prefilled new form in a new tab and returns to the
  registration. The Requested toggle still auto-creates a stub on save; this is
  the alternative where the admin fills license/hours/cost up front.
- Extract the CE-details form fields into a shared _details_section partial used
  by both new and edit.

Universal number formatter:
- Replace ContinuingEducationRegistration.format_hours (CE-specific) and the
  EventsHelper#ce_hours_display wrapper with a NumberFormatter PORO + a generic
  plain_number helper, mirroring MoneyFormatter/dollars_from_cents. Any model,
  service, or view can now format a trailing-zero-free number one way.

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

* Add a license picker to the CE registration form

When the registrant already holds licenses, the new/edit CE form now shows a
License dropdown: pick an existing one to use it as-is, or "Create new license"
to add one from the typed fields. Plain server-rendered select (no JS), so the
fields stay the source of truth for a new/edited license while the dropdown only
chooses which record to write — a CE registration still never exists without a
license. With no license on file there's no picker and the fields create the
first one, as before.

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

* Tidy the CE registration form: one-line license row, optional markers, atomic create

- Lay the four license fields in a single 4-col grid row (widen the form to
  max-w-3xl) so Expires no longer wraps; mark Issuing state + Expires "(optional)".
- Make create atomic: a brand-new license is a build that persists with the CE
  registration in one transaction, so opening (then abandoning) the new form
  never leaves a stray placeholder license, and a failed save rolls back both.
  update is likewise transactional.

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

* Unify the CE status badge across every surface

The registrants index, CE callout, and CE card each computed their own CE
status pill with drifting labels and colors. Consolidate onto one decorator
method so the lifecycle reads identically everywhere:
Requested -> License # needed -> $X due -> Pending -> Issued, with Pending
blue, Issued green, and every actionable/in-progress state amber.

The "$X due" state shows the real outstanding balance (cost net of payments),
so it persists until paid rather than flipping on the first dollar. ?admin=true
on the callout previews the post-payment Pending state without recording a
payment, gated on edit access.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Tint registration-edit card icons by content + retire scholarship-requested controller

Color each card's section icon via section_icon_class(domain, active?) so a card
with content reads as filled and an empty one stays muted — applied across the
organizations, scholarship, shout-out, comments, payments, and communications
cards. Drop the scholarship-requested Stimulus controller in favor of a plain
peer-checked toggle, and align card chips/min-heights so the cards line up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Live-populate the CE license fields from the picked license

Selecting a license in the CE form's dropdown now loads that license's type,
number, state, and expiry into the fields (a ce-license-picker Stimulus
controller reads each option's data attributes); "Create new license" clears
them. Saving therefore edits whichever license is selected in place, so
assign_license now writes the typed fields onto the picked existing license
rather than just linking to it.

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

* Load picked CE license into the fields + add scholarship edit↔callout cross-links

Selecting a license in the CE picker now populates the type/number/state/expiry
fields from that license (new ce-license-picker Stimulus controller), so the
fields always describe the selected license — and saving corrects that license in
place rather than only switching to it. Mirrors the CE edit↔callout pattern onto
scholarships: an admin-only sky jump-link each way, plus a return_to=scholarship
eyebrow so an admin lands back on the edit page (registrants keep the ticket).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Drop the unused credentials column from people

The free-text credentials field is no longer edited (form input removed) or
displayed — the profile credential suffix now derives from professional-license
types via Person#license_credentials. Drop the column and its permit param; the
separate profile_show_credentials toggle is kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Scope inline PR comments to reviewer-worthy flags, not every push

Commenting on every push produced noise on routine changes. Reserve inline diff
comments for things a reviewer genuinely needs flagged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Mirror the saved CE license number into the form answer

The public license POST stripped the number twice — once inside
assign_license and again to feed record_ce_license_answer. assign_license
already normalizes it, so read the saved value back instead of re-stripping
the raw param.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* List the ce_license_picker Stimulus controller in AGENTS

It was added in the CE cutover but missing from the controller list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Make CE license issuing-state a US-states dropdown

Match the public registration form's state field on both the CE callout and
the person license fields, and mark issuing state / expiry optional.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Person license form: US states dropdown + grey date placeholder

- Issuing state is now a US-states <select> (shared us_states helper, blank
  option since it's optional), matching the address form, instead of free text.
- Grey the Expires native date input's "mm/dd/yyyy" placeholder while empty via a
  small reusable date-placeholder Stimulus controller — native date inputs have no
  ::placeholder and render it in the input's text colour, and CSS can't tell an
  empty optional date from a filled one, so it toggles a grey class on value.

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

* Reword the timezone hint to be user-facing

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

* Refresh AGENTS.md directory and spec counts to current

The approximate file counts had drifted repo-wide (views ~507→~632,
controllers ~71→~77, specs, etc.). True them up to actual, bump the Stimulus
controller count to 74, and list the four previously-omitted rake tasks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Lock CE-tied professional licenses to admins; never removable

Tighten license management: a license can be removed only when it has no CE
registrations at all (not just no paid ones), and add ProfessionalLicensePolicy
so a license is editable by an admin or its holder, but locks to admins once any
CE registration exists — a registrant must not alter the credentials a CE
certificate was issued under. The person form shows CE-tied licenses read-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Server-side backstop for per-license edit gating on person update

Drop submitted changes to a CE-tied license that the current user isn't allowed
to edit/destroy (a non-admin owner), so a crafted request can't bypass the
read-only view. No-op for admins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Test the per-license edit guard via an owner-reachable person form

The person form is admin-only today, so the server-side backstop in
PeopleController#update is dormant. Stub PersonPolicy to simulate the future
owner-access state and prove the guard holds against the real
ProfessionalLicensePolicy: CE-tied license edits/deletes are dropped while an
unlocked license still updates — so flipping PersonPolicy later is safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* License picker only when >1; identify licenses by kind+number; conditional timezone hint

- License picker shows only when the registrant holds more than one license; with
  zero or one, the fields edit/create it directly (no picker).
- Identify a license by kind + number, not number alone: widen the per-person
  unique index to (kind, number) via migration, update the model uniqueness scope,
  and match on kind + number in assign_license. So "LMFT 12345" and "LCSW 12345"
  are distinct licenses.
- Timezone hint is now second-person ("You will see…") when you're editing your own
  settings and third-person ("User will see…") when an admin edits someone else.

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

* Simplify the license uniqueness migration

Drop the idempotency guards and explicit up/down for a plain reversible change —
the index swap is trivially invertible and the local DB is disposable.

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

* Align org chip with $100 and use a US-states dropdown for admin CE issuing state

The registration org card's title wraps to two lines, making its header taller
than the scholarship card's, so its first chip sat below the "$100"; nudge the
chips up by that one-line delta to line them up. Bring the admin CE details
issuing-state field in line with the callout and person forms — a US-states
dropdown instead of a free-text box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Match schema.rb to the real dump order for the professional_licenses indexes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Lock the public CE license once the certificate is issued

After issuance the license is the credential the certificate was issued under, so
the registrant-facing CE callout shows it read-only (no Edit link, lock note) and
update_ce_license refuses changes. Admins can still correct it on the admin CE
edit page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Test that an issued CE certificate locks the public license edit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Correct AGENTS.md counts after rebasing onto main

Rebase conflicts left the Stimulus-controller and spec directory tallies at
intermediate values; refresh them to the actual rebased-tree counts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Reconcile main's revenue report + readiness filters with the CE cutover

Two features that merged from main during the rebase still used the removed
ce_* columns and CE_HOURLY_RATE_DOLLARS constant, breaking CI:
- EventRevenueReport projected CE off ce_hours_requested × $25/hr; repoint it
  to sum each active registrant's ContinuingEducationRegistration cost_cents.
- EventRegistrationReadiness gated CE checks on the dropped ce_credit_requested?
  column (a NoMethodError on the registrants roster); use the ce_requested flag.
Port both services' specs to set up CE via the new models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* add ce requested if a reg is updated

* gate edit cost on admin present

* add payment history to ce reg

* refactor duplicate allocation conditional code

* remove extra js

* remove anchor

* edit license with turbo frame

* show payment history always

* clean up policies

* clean up

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Justin Miller <16829344+jmilljr24@users.noreply.github.com>

* Ce part 3 (#1966)

* remove ce requested

any request for ce was followed by an automatic ce record creation so no
need for the boolean.

* add ce reg form role

* do not guard on 1

* add ce form role to edit/new

* remove check for ce offered, just use form

* add basic ce form selection

* css toggle cost and hour fields for one form

* add spec

* add ce payment via stripe

* add payment/discount info to callout

* add specs

* rubocop

* brakeman

* make index unique

* show/hidde callout edit on publish checkbox

* ui- add callout name to checkbox row

* allow all callouts to be used on any event - let admin decide vs hardcoding

* rubocop

* clean up ce callout

* guard

---------

Co-authored-by: maebeale <maebeale@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants