Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8d45909
CE cutover: reroute intake/display onto the new models, drop ce_* col…
maebeale Jun 29, 2026
6721a02
CE: scholarship-style card, ce_requested flag, and a CE edit page
maebeale Jun 29, 2026
8405e4d
Move CE hours-offered/cost fields under the calendar description
maebeale Jun 29, 2026
5929603
Align scholarship/CE card status chips with a consistent grantor row
maebeale Jun 29, 2026
db8bb5d
CE card: only show the license selector when there's a real choice
maebeale Jun 29, 2026
7bf776b
CE card: drop the license dropdown; pick license on create, show it a…
maebeale Jun 29, 2026
660d0e1
CE card: drop the 'Saving creates the CE registration' hint
maebeale Jun 29, 2026
573fb9f
CE toggle: amber-while-pending/teal track + teal card highlight when on
maebeale Jun 29, 2026
9b71df6
Restyle the CE registration edit page to match the scholarship form
maebeale Jun 30, 2026
f63f81d
Align the org/scholarship/CE card bottom actions on one row
maebeale Jun 30, 2026
91bc47c
Block deleting a registration whose CE registration has payments
maebeale Jun 30, 2026
a335386
Let a registrant request CE from the public callout
maebeale Jun 30, 2026
a8ca39f
Manage a person's professional licenses on the person edit page
maebeale Jun 30, 2026
e9cd019
Move professional licenses below Background; drop the credentials field
maebeale Jun 30, 2026
609af48
Show license type(s) as the profile credential suffix
maebeale Jun 30, 2026
e032b50
Group the registrant CE callout into labeled sections
maebeale Jun 30, 2026
40ce096
Flip the registrant license to read-only with an edit link
maebeale Jun 30, 2026
24a0626
Fix unclosed filter div and read CE hours from the registration
maebeale Jun 30, 2026
b9a40cc
Render the license Expires field through simple_form
maebeale Jun 30, 2026
14c42ba
Add issuing state + expiry to the CE license forms
maebeale Jun 30, 2026
571b83d
Match the card prompts and retire the CE-credit Stimulus controller
maebeale Jun 30, 2026
e78cd10
Add deliberate "Add CE registration" flow + universal number formatter
maebeale Jun 30, 2026
fee5b08
Add a license picker to the CE registration form
maebeale Jun 30, 2026
595046b
Tidy the CE registration form: one-line license row, optional markers…
maebeale Jun 30, 2026
4d8db13
Unify the CE status badge across every surface
maebeale Jun 30, 2026
a6d71dd
Tint registration-edit card icons by content + retire scholarship-req…
maebeale Jun 30, 2026
05fd8d7
Live-populate the CE license fields from the picked license
maebeale Jun 30, 2026
8065071
Load picked CE license into the fields + add scholarship edit↔callout…
maebeale Jun 30, 2026
b982fda
Drop the unused credentials column from people
maebeale Jun 30, 2026
2aec563
Scope inline PR comments to reviewer-worthy flags, not every push
maebeale Jun 30, 2026
1071006
Mirror the saved CE license number into the form answer
maebeale Jun 30, 2026
029b3a5
List the ce_license_picker Stimulus controller in AGENTS
maebeale Jun 30, 2026
396584b
Make CE license issuing-state a US-states dropdown
maebeale Jun 30, 2026
66690c6
Person license form: US states dropdown + grey date placeholder
maebeale Jun 30, 2026
bb15b78
Reword the timezone hint to be user-facing
maebeale Jun 30, 2026
a2f769b
Refresh AGENTS.md directory and spec counts to current
maebeale Jun 30, 2026
edfecd6
Lock CE-tied professional licenses to admins; never removable
maebeale Jun 30, 2026
f58beaf
Server-side backstop for per-license edit gating on person update
maebeale Jun 30, 2026
624b82f
Test the per-license edit guard via an owner-reachable person form
maebeale Jun 30, 2026
476f65c
License picker only when >1; identify licenses by kind+number; condit…
maebeale Jun 30, 2026
224b74b
Simplify the license uniqueness migration
maebeale Jun 30, 2026
8725dea
Align org chip with $100 and use a US-states dropdown for admin CE is…
maebeale Jun 30, 2026
1d6ced1
Match schema.rb to the real dump order for the professional_licenses …
maebeale Jun 30, 2026
00c619e
Lock the public CE license once the certificate is issued
maebeale Jun 30, 2026
3e2f982
Test that an issued CE certificate locks the public license edit
maebeale Jun 30, 2026
0b29237
Bump css_parser and msgpack to patch new CVEs (fix bundler-audit CI)
maebeale Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ Follow the [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introducti
- Include screenshots for UI changes
- **On every push**, update the PR title and content to reflect the current diff β€” preserve any existing images/screenshots in the description
- **On every push**, update AI instruction files if the diff adds, removes, or renames anything tracked in AGENTS.md β€” specifically: Stimulus controllers, services, model/controller concerns, mailers, rake tasks, and directory file counts
- **On every push**, add PR review comments on notable lines of code β€” decisions, trade-offs, non-obvious logic, or anything a reviewer should understand. Use `gh api` to post line comments on the diff
- **Inline-comment only to flag what matters to the reviewer** β€” do NOT comment on every push, and don't annotate routine or self-explanatory changes. Add a `gh api` line comment on the diff only when a reviewer genuinely needs something flagged: a non-obvious decision or trade-off, a risky/surprising change, a load-bearing assumption, or something easy to miss. When nothing rises to that bar, post no inline comments
- **Attribute every AI-authored GitHub comment** β€” `gh` posts as the authenticated user, so any comment you create (PR review comments, issue/PR comments, replies) MUST be prefixed to identify the AI agent that wrote it. Begin the comment body with `πŸ€– _From <agent>:_` (e.g. `πŸ€– _From Claude:_` or `πŸ€– _From Copilot:_`) followed by the content
- **Keep GitHub comments short and to the point** β€” one or two sentences, stating the key insight directly. Skip preamble, restating the code, and hedging; if a comment needs more than a few lines, it usually belongs in the PR description instead

Expand Down
45 changes: 25 additions & 20 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,19 @@ This codebase (Rails 8.1)

| Directory | Purpose | Count |
|---|---|---|
| `app/models/` | ActiveRecord models | ~78 files |
| `app/services/` | Service objects and POROs (e.g. `MoneyFormatter` for currency display) | ~29 files |
| `app/jobs/` | SolidQueue background jobs | 3 files |
| `app/models/` | ActiveRecord models | ~80 files |
| `app/services/` | Service objects and POROs (e.g. `MoneyFormatter` for currency display) | ~30 files |
| `app/jobs/` | SolidQueue background jobs | 4 files |
| `app/models/concerns/` | Shared model modules | 16 concerns |

### Presentation

| Directory | Purpose | Count |
|---|---|---|
| `app/controllers/` | Rails controllers (admin/, events/) | ~70 files |
| `app/views/` | ERB templates | ~504 files |
| `app/decorators/` | Draper decorators for view logic | ~38 files |
| `app/policies/` | ActionPolicy authorization rules | ~49 files |
| `app/controllers/` | Rails controllers (admin/, events/) | ~77 files |
| `app/views/` | ERB templates | ~632 files |
| `app/decorators/` | Draper decorators for view logic | ~40 files |
| `app/policies/` | ActionPolicy authorization rules | ~55 files |
| `app/presenters/` | Presentation objects | 3 files |
| `app/helpers/` | View helpers | ~24 files |
| `app/mailers/` | ActionMailer classes | 5 files |
Expand All @@ -81,7 +81,7 @@ This codebase (Rails 8.1)
|---|---|
| `config/routes.rb` | All routes (single file) |
| `config/database.yml` | MySQL via Trilogy adapter |
| `config/initializers/` | ~28 initializer files |
| `config/initializers/` | ~30 initializer files |
| `.github/workflows/` | GitHub Actions CI |
| `Procfile.dev` | Dev services: `vite` + `web` |
| `ai/` | Shell script shortcuts for common dev tasks (see `ai/README.md`) |
Expand Down Expand Up @@ -147,7 +147,7 @@ This codebase (Rails 8.1)

### Namespaces

- **Root level** (~52 controllers): Workshops, stories, resources, events, people, organizations, registration ticket callouts, etc.
- **Root level** (~58 controllers): Workshops, stories, resources, events, people, organizations, registration ticket callouts, etc.
- **`admin/`**: HomeController, AnalyticsController, AhoyActivitiesController
- **`events/`**: Registrations sub-resource (create/destroy + slug-based show at `/registration/:slug`)
- **Devise overrides**: Registrations, Confirmations, Passwords
Expand Down Expand Up @@ -276,6 +276,7 @@ end
- `asset_picker` β€” Asset selection UI
- `autosave` β€” Auto-save form state
- `carousel` β€” Swiper-based carousels
- `ce_license_picker` β€” Fill the CE license type/number/state/expiry fields from the picked license (or clear them for a new one)
- `cocoon` β€” Nested form handling (cocoon gem)
- `collection` β€” Filter form auto-submit with debounce
- `column_toggle` β€” Toggle table column visibility
Expand Down Expand Up @@ -340,17 +341,17 @@ Custom colors defined in `app/frontend/stylesheets/application.tailwind.css`:

| Directory | Count | Purpose |
|---|---|---|
| `spec/models/` | ~58 | Model unit tests |
| `spec/views/` | ~73 | View template tests |
| `spec/requests/` | ~47 | HTTP request/integration tests |
| `spec/system/` | ~25 | End-to-end browser tests (Capybara) |
| `spec/routing/` | ~13 | Route definition tests |
| `spec/policies/` | ~9 | Authorization policy tests |
| `spec/decorators/` | ~10 | Decorator tests |
| `spec/services/` | ~12 | Service object tests |
| `spec/models/` | ~71 | Model unit tests |
| `spec/views/` | ~76 | View template tests |
| `spec/requests/` | ~79 | HTTP request/integration tests |
| `spec/system/` | ~20 | End-to-end browser tests (Capybara) |
| `spec/routing/` | ~15 | Route definition tests |
| `spec/policies/` | ~14 | Authorization policy tests |
| `spec/decorators/` | ~14 | Decorator tests |
| `spec/services/` | ~22 | Service object tests |
| `spec/mailers/` | ~5 | Mailer tests |
| `spec/helpers/` | ~1 | Helper tests |
| `spec/factories/` | ~53 | FactoryBot factory definitions |
| `spec/helpers/` | ~5 | Helper tests |
| `spec/factories/` | ~67 | FactoryBot factory definitions |

### Configuration

Expand Down Expand Up @@ -423,8 +424,12 @@ RuboCop linting on PRs and pushes to main.

## Rake Tasks

Located in `lib/tasks/` (4 files):
Located in `lib/tasks/` (8 files):
- `dev.rake` β€” Development database seeding from XML/CSV
- `rhino_migrator.rake` β€” Rich text editor migration
- `attachment_report.rake` β€” Attachment reporting
- `migrate_internal_id_to_filemaker_code.rake` β€” FileMaker code migration
- `convert_age_ranges.rake` β€” Age range data conversion
- `legacy_user_permissions_to_comments.rake` β€” Migrate legacy user permissions into comments
- `migrate_sectors.rake` β€” Sector data migration
- `migrate_workshop_logs.rake` β€” Workshop log migration
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ Follow the [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introducti
- Include screenshots for UI changes
- **On every push**, update the PR title and content to reflect the current diff β€” preserve any existing images/screenshots in the description
- **On every push**, update AI instruction files if the diff adds, removes, or renames anything tracked in AGENTS.md β€” specifically: Stimulus controllers, services, model/controller concerns, mailers, rake tasks, and directory file counts
- **On every push**, add PR review comments on notable lines of code β€” decisions, trade-offs, non-obvious logic, or anything a reviewer should understand. Use `gh api` to post line comments on the diff
- **Inline-comment only to flag what matters to the reviewer** β€” do NOT comment on every push, and don't annotate routine or self-explanatory changes. Add a `gh api` line comment on the diff only when a reviewer genuinely needs something flagged: a non-obvious decision or trade-off, a risky/surprising change, a load-bearing assumption, or something easy to miss. When nothing rises to that bar, post no inline comments
- **Attribute every AI-authored GitHub comment** β€” `gh` posts as the authenticated user, so any comment you create (PR review comments, issue/PR comments, replies) MUST be prefixed to identify the AI agent that wrote it. Begin the comment body with `πŸ€– _From <agent>:_` (e.g. `πŸ€– _From Claude:_` or `πŸ€– _From Copilot:_`) followed by the content
- **Keep GitHub comments short and to the point** β€” one or two sentences, stating the key insight directly. Skip preamble, restating the code, and hedging; if a comment needs more than a few lines, it usually belongs in the PR description instead

Expand Down
11 changes: 7 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,9 @@ GEM
country_select (11.0.0)
countries (> 6.0, < 9.0)
crass (1.0.7)
css_parser (2.2.0)
css_parser (3.0.0)
addressable
ssrf_filter (~> 1.5)
csv (3.3.5)
date (3.5.1)
debug (1.11.1)
Expand Down Expand Up @@ -311,7 +312,7 @@ GEM
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
msgpack (1.8.3)
multi_xml (0.8.1)
bigdecimal (>= 3.1, < 5)
mutex_m (0.3.0)
Expand Down Expand Up @@ -713,6 +714,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
ssrf_filter (1.5.0)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.2.0)
Expand Down Expand Up @@ -884,7 +886,7 @@ CHECKSUMS
countries (8.1.0) sha256=4d6b318b8e906f1f769d5c021c13a418d33e917dc96ceb625a91d8e7ab2d192e
country_select (11.0.0) sha256=0ab0a385fa70eadc71473af4b21b9b4d09f75660cc1c282a5bf6ec873719204b
crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295
css_parser (2.2.0) sha256=23d1b247d7bc78cb2f2fe54629fb755e68d3004f1d1bd5c66d5096d42bff6325
css_parser (3.0.0) sha256=eaf0e9283fd581d06e815235ceef4f0910c0b394c606355dbc69f93e84443885
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
Expand Down Expand Up @@ -952,7 +954,7 @@ CHECKSUMS
mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
msgpack (1.8.3) sha256=8bda4a6428d3244e50d6bd55854d354edbada88a4e1f4f5731a39a0f86bee6a1
multi_xml (0.8.1) sha256=addba0290bac34e9088bfe73dc4878530297a82a7bbd66cb44dcd0a4b86edf5a
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
net-imap (0.6.4.1) sha256=29f0360d75a7efd3539f16ac1957dea5c0a51ddeceb348db4553c3120914ea0d
Expand Down Expand Up @@ -1094,6 +1096,7 @@ CHECKSUMS
solid_queue (1.3.1) sha256=d9580111180c339804ff1a810a7768f69f5dc694d31e86cf1535ff2cd7a87428
sprockets (4.2.2) sha256=761e5a49f1c288704763f73139763564c845a8f856d52fba013458f8af1b59b1
sprockets-rails (3.2.2) sha256=62862bce136e31d7497eededde5f7730d4096bc8ef33ef7037c41423ccf89557
ssrf_filter (1.5.0) sha256=e03dcdb9d1730d7f6710532a606b3543df2a448a0293ce04a2d995523c5a97f6
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
stripe (18.4.2) sha256=fd08a73ab87fc0b0ad938ca71293e1051e593001b70de5e9fcf12d1097133831
Expand Down
117 changes: 117 additions & 0 deletions app/controllers/continuing_education_registrations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
class ContinuingEducationRegistrationsController < ApplicationController
before_action :set_ce_registration, except: [ :new, :create ]
before_action :set_event_registration, only: [ :new, :create ]

# Deliberate "Add CE registration" path, mirroring scholarship's new/create.
# The "Requested" toggle on the registration form still auto-creates a stub on
# save; this is the alternative where the admin fills in license/hours/cost up
# front. Hours/cost prefill from the event's offering.
def new
@ce_registration = @event_registration.continuing_education_registrations.build(
professional_license: @event_registration.registrant.professional_licenses.first,
hours: @event_registration.event.ce_hours_offered,
cost_cents: @event_registration.event.ce_hours_cost_cents
)
authorize! @ce_registration
end

def create
@ce_registration = @event_registration.continuing_education_registrations.build(professional_license: license_for_create)
authorize! @ce_registration

# One transaction so the new license (a build until now) and the CE registration
# persist together β€” a failed save leaves neither behind.
ActiveRecord::Base.transaction do
apply_ce_params(@ce_registration)
@ce_registration.save!
@event_registration.update_column(:ce_requested, true)
end
redirect_to registration_path, notice: "CE registration created.", status: :see_other
rescue ActiveRecord::RecordInvalid
flash.now[:alert] = @ce_registration.errors.full_messages.to_sentence
render :new, status: :unprocessable_content
end

def edit
authorize! @ce_registration
end

def update
authorize! @ce_registration

ActiveRecord::Base.transaction do
apply_ce_params(@ce_registration)
@ce_registration.save!
end
redirect_to registration_path, notice: "CE registration updated.", status: :see_other
rescue ActiveRecord::RecordInvalid
flash.now[:alert] = @ce_registration.errors.full_messages.to_sentence
render :edit, status: :unprocessable_content
end

# Removal mirrors scholarship's destroy but never cascades away a CE registration
# that carries payments β€” the admin must revert the allocation first.
def destroy
authorize! @ce_registration
if @ce_registration.allocations.exists?
redirect_to edit_continuing_education_registration_path(@ce_registration, return_to: params[:return_to]),
alert: "Can't remove CE β€” it has payments. Revert the payment first.", status: :see_other
return
end

registration = @ce_registration.event_registration
@ce_registration.destroy!
registration.update_column(:ce_requested, false)
redirect_to edit_event_registration_path(registration), notice: "CE registration removed.", status: :see_other
end

# Mark / unmark the CE certificate as issued (sets/clears certificate_sent_at),
# mirroring scholarship's toggle_tasks.
def toggle_certificate
authorize! @ce_registration
issued = @ce_registration.certificate_sent_at.present?
@ce_registration.update!(certificate_sent_at: issued ? nil : Time.current)
redirect_to edit_continuing_education_registration_path(@ce_registration, return_to: params[:return_to]),
notice: issued ? "Certificate marked not issued." : "Certificate marked issued.", status: :see_other
end

private

def set_ce_registration
@ce_registration = ContinuingEducationRegistration.find(params[:id])
end

# The registration a new CE record attaches to, located from the signed global
# id the "Add CE registration" link carries (mirrors scholarship's allocatable).
def set_event_registration
sgid = params[:allocatable_sgid] || params.dig(:continuing_education_registration, :allocatable_sgid)
@event_registration = GlobalID::Locator.locate_signed(sgid) if sgid
redirect_to root_path, alert: "Registration not found.", status: :see_other unless @event_registration
end

# License a brand-new CE registration attaches to: the registrant's existing
# license, else an unsaved build. assign_license fills it from the submitted
# type/number/state/expiry, and it persists with the CE registration in create's
# transaction β€” so abandoning the new form never leaves a stray license behind.
def license_for_create
@event_registration.registrant.professional_licenses.first ||
@event_registration.registrant.professional_licenses.build
end

# Apply the submitted license fields, hours, and cost to a CE registration.
# Shared by create and update so both read params the same way.
def apply_ce_params(ce_registration)
ce_registration.assign_license(number: params.dig(:continuing_education_registration, :license_number),
kind: params.dig(:continuing_education_registration, :license_kind),
issuing_state: params.dig(:continuing_education_registration, :license_issuing_state),
expires_on: params.dig(:continuing_education_registration, :license_expires_on),
license_id: params.dig(:continuing_education_registration, :professional_license_id))
ce_registration.hours = params.dig(:continuing_education_registration, :hours)
cost = params.dig(:continuing_education_registration, :cost_dollars)
ce_registration.cost_cents = (cost.to_d * 100).round if cost.present?
end

def registration_path
edit_event_registration_path(@ce_registration.event_registration)
end
end
Loading