Materialize ticket callouts: editable, configurable, reorderable built-ins#1954
Materialize ticket callouts: editable, configurable, reorderable built-ins#1954maebeale wants to merge 26 commits into
Conversation
| t.references :registration_ticket_callout, null: false, foreign_key: { on_delete: :cascade }, | ||
| index: { name: "index_callout_resources_on_callout_id" } | ||
| # resources.id is a legacy `int` (not bigint), so the FK column must match. | ||
| t.integer :resource_id, null: false |
There was a problem hiding this comment.
🤖 From Claude: resource_id is :integer (not the default bigint) on purpose — resources.id is a legacy int, so the FK column has to match or the add_foreign_key fails.
| if foreign_key_exists?(:registration_ticket_callouts, :resources) | ||
| remove_foreign_key :registration_ticket_callouts, :resources | ||
| end | ||
| remove_column :registration_ticket_callouts, :resource_id |
There was a problem hiding this comment.
🤖 From Claude: Existing single links are copied into the join table (INSERT above) before this column is dropped, and down restores the lowest-positioned resource back into resource_id — so the migration is reversible without data loss.
| # definition order after whatever already exists. Returns the created rows. | ||
| def seed | ||
| existing_keys = @event.registration_ticket_callouts.magic.pluck(:magic_key).to_set | ||
| definitions.reject { |definition| existing_keys.include?(definition[:magic_key]) } |
There was a problem hiding this comment.
🤖 From Claude: Idempotency hinges here: any magic_key already on the event is skipped, so seeding on create and lazily on future edits never re-creates or overwrites a card an admin has edited/hidden.
| validates :color_class, inclusion: { in: DomainTheme::SWATCH_COLORS.map(&:to_s) }, allow_blank: true | ||
| validates :position, numericality: { only_integer: true, greater_than: 0, allow_nil: true } | ||
| validates :magic_key, inclusion: { in: MAGIC_KEYS }, allow_nil: true | ||
| validates :magic_key, uniqueness: { scope: :event_id }, allow_nil: true |
There was a problem hiding this comment.
🤖 From Claude: allow_nil is what lets many custom callouts (nil magic_key) coexist while guaranteeing at most one of each built-in per event — the invariant the seeder relies on.
| # A built-in card whose content has been materialized into an editable row for | ||
| # this event renders from that row (through the custom-callout loop) instead of | ||
| # here, so we skip it to avoid double-rendering. See DefaultTicketCallouts. | ||
| def materialized?(magic_key) |
There was a problem hiding this comment.
🤖 From Claude: This is the anti-double-render guard: once an event materializes a card into a row, the row renders it (via the ticket's callout loop) and the code path bows out. Memoized per instance, so it's one query per ticket render.
| and not-yet-dripped callouts are omitted. --> | ||
| <% payment_access = event_registration.payment_access_granted? || @checkout_payment_status == "paid" %> | ||
| <% event_registration.event.registration_ticket_callouts.each do |callout| %> | ||
| <% event_registration.event.registration_ticket_callouts.visible.each do |callout| %> |
There was a problem hiding this comment.
🤖 From Claude: .visible drops hidden (draft / opted-out) rows; the dripping? line below drops date-scheduled ones that haven't reached display_from. Custom and materialized magic callouts now share this one ordered loop, which is what makes cross-type drag-reorder work.
| Hidden (keep this built-in callout off the ticket) | ||
| </label> | ||
| <% if f.object.persisted? %> | ||
| <%# link (not button_to) so we don't nest a <form> inside the event form %> |
There was a problem hiding this comment.
🤖 From Claude: Restore uses a Turbo link_to POST rather than button_to on purpose — button_to emits a nested <form>, and this partial renders inside the event form, which would be invalid HTML.
| @event.event_forms.reset | ||
| if @event.update(event_params) | ||
| assign_associations(@event) | ||
| # Lazily materialize built-in callouts for events created before this |
There was a problem hiding this comment.
🤖 From Claude: Seeding on every update (idempotent) is the "no backfill" strategy — events created before this feature materialize their built-in callouts the first time an admin saves them.
| # when it shouldn't show for this registration (e.g. certificate not yet | ||
| # unlocked). The ticket calls this for a materialized behavioral row so the row | ||
| # governs visibility/order while the app still supplies the dynamic status. | ||
| def card_for(magic_key) |
There was a problem hiding this comment.
🤖 From Claude: card_for is the row-render path (no materialized? skip); #cards is the fallback/non-materialized path (with the skip). Both dispatch through the same CARD_BUILDERS map, so a behavioral card has exactly one source of truth for its live state whether it renders from a row or from code.
| # Reject only blank *new* callouts; existing rows (with an id) must always | ||
| # process so a control-only magic row's hidden/position toggle persists even | ||
| # though it submits no title. | ||
| reject_if: proc { |attrs| attrs["id"].blank? && attrs["title"].blank? } |
There was a problem hiding this comment.
🤖 From Claude: Loosened from title.blank? to also require id.blank? so a control-only magic row (which submits just id + hidden, no title) still processes its hide toggle. New blank rows are still rejected.
| # Drips onto the ticket a week before the event starts, replacing the old | ||
| # hard-coded "one week prior" rule with a stored, editable date. | ||
| display_from: ->(event) { event.start_date - 7.days if event.start_date }, | ||
| seed_if: ->(event) { event.videoconference_url.present? } |
There was a problem hiding this comment.
🤖 From Claude: Videoconference is only seeded once the event has a link — so a no-VC event never gets a stray control row. Because seeding runs on every edit, an event that adds a link later materializes the card on that save.
|
|
||
| # A behavioral magic callout that gets an editor control row (hide/reorder/ | ||
| # restore). Excludes the preview-edited ones, whose placement stays fixed. | ||
| def control_row? |
There was a problem hiding this comment.
🤖 From Claude: CE hours and Event details are behavioral (rendered via card_for) but excluded from control rows because their text is edited on the event itself (the built-in preview card). This keeps a single editing surface for them while still placing them in the correct ticket order.
| EditorCard.new(nil, "forms", "fa-solid fa-file-lines", "blue", "Forms", "W-9, invoice, and receipt", "On facilitator trainings and paid events", "Items link to their relevant resources."), | ||
| EditorCard.new(nil, "handouts", "fa-solid fa-folder-open", "blue", "Handouts", "Worksheets and resources for the training", "On facilitator trainings", "Items link to their relevant resources."), | ||
| EditorCard.new(nil, "faq", "fa-solid fa-circle-question", "blue", "Frequently asked questions", "Common questions about the 2-day training", "On facilitator trainings", nil) | ||
| ].reject { |card| card.key.nil? && materialized.include?(card.magic_key) } |
There was a problem hiding this comment.
🤖 From Claude: Only the greyed built-in previews (key nil) are dropped once materialized; the editable ones (CE hours, Event details — key present) always stay, since that preview is where their text is edited.
| # The forms callout's linked documents. Uses the materialized Forms row's | ||
| # resources when present (so admins can add/remove them), else the W-9 for | ||
| # events not yet materialized. | ||
| def form_document_resources |
There was a problem hiding this comment.
🤖 From Claude: Reads the materialized Forms row's resources so a removed W-9 disappears from the page; falls back to the hard-coded W-9 lookup for events not yet materialized (which have no Forms row). Invoice/receipt stay dynamic below.
54a2561 to
7f2a8bb
Compare
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the built-in/custom 'Hidden' checkbox with a 'Published' checkbox (the inverse of the stored hidden flag) via a virtual attribute, and move it to the right of the row footer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CE hours and event details are no longer special-cased: their title and text seed from the event's columns once (migrating existing content) and then live on the callout row, edited in the same shared row as every other built-in. They're now reorderable, link resources, and get Restore default. CE hours-offered/cost stays event config, edited inline in the CE row (ce_config?). The registrant CE and details pages read the row (falling back to the event columns before seed). Also: the built-in footer shows a static "Matches default" when a card is unedited and the clickable "Restore default" once it's customized — so every built-in (now including CE and event details) surfaces its restore state. Removed content_on_event?/restorable? and the event-column editor path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Restore default" is now a per-row checkbox (reset_to_default) applied during the normal event save via an after_save that resets the flagged callout to its template — so it's part of the full form submit. Removed the standalone restore route/action. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6ce56a2 to
173d5f4
Compare
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Payment (APP_COLORED_MAGIC_KEYS) keeps its dynamic card colour — orange while a balance is due, blue once paid — overriding the selected colour; the editor notes this under the colour picker. Other cards honour the row's colour, and the amber "action needed" badges (scholarship, CE, payment) were always retained. - Add a note in the callouts section telling admins to save a new event first and return to edit its built-in callouts (they're materialized on save). Refresh the now-stale section intro. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scholarship (award tasks outstanding) and CE hours (hours/license needed) now flip their card colour to amber — the app-controlled action colour — like Payment turns orange while a balance is due. Added both to APP_COLORED_MAGIC_KEYS so the app colour overrides the selected one, and generalized the colour-picker hint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
|
I created a branch of this and messed up something when trying to merge back in. This is included in #1969 |
🤖 suggested review level: 5 Inspect 🔬 schema migration with data move, seeder wired into create/edit, and ticket-render + editor changes across all built-in callouts (live registrant-facing behavior)
Why
Follow-up to #1945. Turns every code-defined ("magic") ticket callout into an editable / configurable per-event row admins can edit, hide/restore, reorder, and link resources to — one unified, admin-ordered callout list.
Model
DefaultTicketCalloutsseeds all nine built-ins in canonical order on event create and lazily on edit (idempotent, no backfill), each gated by the event's config (seed_if). Two kinds:MagicTicketCallouts#card_for.#cards(fallback / not-yet-seeded events) and#card_for(row path) share oneCARD_BUILDERSmap, so a card's live state has a single source of truth.Configurability highlights
display_from(replaces the hard-coded "one week prior").hidden_from_search.Schema
magic_key,hidden(draft/opt-out),display_from(drip); singleresource_id→ ordered join table (existing links migrated). Reversible, no backfill.Editor
Materialized cards live in the one sortable list — content cards as editable fields, behavioral cards as control-only rows (hide + drag + Restore default; Forms adds a linked-documents picker). Custom callouts gain a hidden (draft) checkbox.
reject_ifloosened so a control row's toggle persists without a title.Editable vs hard-coded (by design)
Row-editable everywhere: visibility, drip, order — plus, for content cards, title/subtitle/body/resources, and for Forms its linked documents. Hard-coded only for behavioral cards: live status/badge, per-registration visibility, and the functional destination (ledger, award page, certificate, join link, generated invoice/receipt).
Behavior changes (roll out gently — only on an event's next create/edit; untouched events keep today's behavior via the fallback)
Tests
~70 callout specs + ~370 related event/registration specs green; migration reversible (down→up verified); lint clean.