Skip to content

Casadesign - DRAFT DO NOT MERGE - #7051

Draft
giacoelho wants to merge 319 commits into
mainfrom
casadesign
Draft

Casadesign - DRAFT DO NOT MERGE#7051
giacoelho wants to merge 319 commits into
mainfrom
casadesign

Conversation

@giacoelho

Copy link
Copy Markdown
Collaborator

This is a work in progress. Will flag when it is ready to merge.

@github-actions github-actions Bot added dependencies Touches dependency files javascript Touches JavaScript code ruby Touches Ruby code erb Touches ERB templates labels Jul 9, 2026
@giacoelho
giacoelho marked this pull request as draft July 9, 2026 23:01
@github-actions github-actions Bot added the 🧪 Tests Tests label Jul 10, 2026
gia and others added 20 commits July 15, 2026 19:38
- design-todo.md: save the scoped volunteer edit page migration plan at the top
  (resume-here) and check off the CaseContact occurred_at flake (already a lambda).
- Delete the orphaned casa_cases/_generate_report_modal (Case show uses the Dialog
  report modal; the live report generation lives in case_court_reports / reports).
The cases filter chevron/search/clear icons and the index/show menu leading icons
were slate-400 (2.56:1, below the 3:1 UI floor and against design.md's slate-500
chevron rule). Now slate-500 (4.76:1). Decorative empty-state / notification icon
tiles left as-is.
Rebuild the volunteer edit page as one self-contained Tailwind view on
`layout: "casa_app"` (@active_nav "volunteers"); the shared Bootstrap
user-edit partials stay for the supervisor/admin edit pages. Sections:
identity header + Impersonate, an inline reminder card, Profile (two-column
grid, editable vs read-only per update_user_setting?), Account (metadata dl),
Status/activation, Cases (card list), Supervisor, and Notes (md+ table with a
mobile card twin). Rebuilt volunteers/_manage_active, _manage_cases,
_manage_supervisor, _notes and _volunteer_reminder_form as casa_app twins and
folded in _send_reminder_button.

Also lands two app-shell items this migration surfaced:
- casa_app impersonation banner (layouts/_impersonation_banner), fixing the two
  impersonation examples red since the volunteer dashboard moved to casa_app.
- flash parity: the casa_app flash strip carries the flash key as a class
  (.notice/.alert) alongside role=status/alert.

Deactivate keeps the UJS data-confirm (the :js spec drives accept_confirm).
Spec-locked Title Case action labels kept verbatim (queued for the sentence-case
sweep). All 745 volunteer-edit system examples green, plus the view + request
specs; no overflow 375-1280; select chevron darkest-pixel verified (slate-500).
Updated the three specs coupled to the old page title/flash, and documented the
Person edit page pattern + impersonation banner + flash parity in
design.md/design-todo.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuild the volunteers roster on the casa_app shell as a bespoke Tailwind table,
retiring the jQuery DataTable. Server-side filters (status, supervisor, transition-aged,
extra languages), search, sortable headers, and Pagy pagination reuse VolunteerDatatable's
SQL via new #index_relation / #index_count (the controller maps plain GET filters into the
DataTables param shape; the count strips the custom SELECT/ORDER aliases and counts
DISTINCT users.id). Responsive: desktop table (scrolls within its card) + a mobile card
twin; emoji columns become pills/text; names are honorific-free.

Bulk supervisor assignment is preserved — select-all + a native-dialog modal + the
disable-form-gated Confirm — with checkboxes in the desktop table only so the select-all
counts and #id lookups stay unambiguous. The Manage trigger toggles `hidden!` (Tailwind v4
important) because a plain `hidden` loses to button_classes' `inline-flex`.

The column-visibility picker is dropped (design.md — a responsive layout beats a picker).
The legacy DataTable is retired by moving the #volunteers hook off the <table> onto its
wrapper, so dashboard.js's $('table#volunteers').DataTable() no longer matches (it otherwise
re-inits a serverSide table whose undefined ajax.url POSTs to the index). The now-unused
datatable JSON action + dashboard.js volunteer block are dead (queued for cleanup).

Rewrote the 435-line DataTables-coupled spec (259 lines) for the server-side behavior:
listing/nav/filtering/search/empty-state (non-JS) plus auto-submit and the full bulk flow
(:js). Verified WCAG AA, chevrons darkest-pixel-checked (slate-500), and no page overflow
375-1280. Documented the DataTable-retirement and bulk-roster patterns in design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Index: bespoke Tailwind roster + "volunteers without supervisors" + "CASA
cases without court dates", with a server-side status filter (auto-submit).
Retires the serverSide DataTable by moving #supervisors off the <table>
onto its wrapper (dashboard.js no longer re-inits over the markup).
Per-supervisor contact stats become labeled pills (color + icon + word:
attempting / not attempting / transition-aged, or "No assigned
volunteers") instead of a color-only bar chart; the transition-aged column
is a Yes/No pill, not the butterfly/caterpillar emoji; and the always-blank
Hearing type / Judge columns are dropped (court-date-less cases never have
them).

Edit: self-contained casa_app column (Profile / Account / Status /
Volunteers). supervisors/_manage_active is now Tailwind, and a new
supervisors/_manage_volunteers twin replaces the shared Bootstrap
manage_volunteers; the shared _edit_form / _invite_login stay for the
casa_admin edit page (rebuilt inline here). "Edit supervisor" heading;
Deactivate keeps the UJS data-confirm the :js spec drives with
accept_confirm/dismiss_confirm.

Both view + system specs rewritten to semantic hooks ([data-stat],
.supervisor-filters, [data-test=cases-without-court-dates]). Chevrons
darkest-pixel verified (114 = slate-500); no overflow 375-1280; standardrb
+ erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The casa_app top-bar account/notification dropdowns are absolute panels
inside the <header>, and relied on the header's backdrop-blur stacking
context to sit above page content. That's fragile: any page element with
its own stacking context (a positioned z-* toolbar, a transform/hover-lift
card, a native control) ties the header and wins by DOM order — painting a
page button (e.g. the "New Supervisor" CTA) over the open dropdown.

Give the header `relative z-[25]`: above all page content (which tops out
at z-20) yet below the mobile nav scrim (z-30, so the open drawer still
dims the header), the sidebar drawer (z-40), and native <dialog> modals
(top layer). Reproduced and verified with elementFromPoint probes at 1018
and 375px (dropdown now fully on top; mobile drawer + scrim unchanged).
Documented the shell stacking order in design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
index / show / new / edit + LearningHours::Volunteers#show now render on
layout: "casa_app" (@active_nav "learning").

- Volunteer index: a bespoke Tailwind entries table (desktop table + mobile
  card twin), reused by the supervisor/admin volunteers#show.
- Supervisor/admin index: a per-volunteer YTD-total roster, server-rendered
  + Pagy paginated, retiring the client-side DataTable (dropped the
  table#learning-hours id learning_hours.js targets; initializeDataTable
  no-ops on the empty selector).
- new/edit form: Tailwind, with the Hour(s)/Minute(s) inputs rebuilt as a
  twin of Form::HourMinuteDurationComponent (left untouched — still used by
  the Bootstrap case-contacts form); native-select chevrons; form_errors
  summary. Duration fields default to 0 so leaving one blank submits 0.
- show: a fact card + Edit/Delete.

Column headers sentence-cased; system + request specs updated to match, and
the DataTables Previous/Next pagination test replaced with the Pagy summary.
Chevrons darkest-pixel verified (114 = slate-500); no overflow 375-1280;
standardrb + erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
volunteers#new and supervisors#new now render on layout: "casa_app"
(@active_nav volunteers/supervisors), rebuilt as self-contained Tailwind
invite forms (email, display name, phone [+ date of birth for volunteers])
using shared/_form_errors. Dropped the single-use volunteers/_form; fixed
the supervisor create-failure render (render new_supervisor_path -> :new).

Also fixes the shared not-authorized flash on casa_app: the handler ships
"Sorry, you are not authorized..." as flash[:notice] (locked by ~dozens of
request specs, so the key can't change), and the old Bootstrap layout gave
every flash a base .alert class (flash_class). The casa_app flash strip now
does the same (base `alert` + the flash key), so the .alert not-authorized
redirect specs (new volunteer, new supervisor, invite, imports) pass while
.notice parity holds.

Specs: updated the supervisor invalid-submission error-summary text
(shared/_form_errors -> "Unable to save") and the volunteers/index "New
volunteer" heading. No overflow 375-1280; standardrb + erb_lint clean. The
5 accepting-invitation failures in invite_spec are pre-existing (casa_auth
accept-page spec drift), unrelated to these pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The invitation-accept page (devise/invitations/edit) was migrated to
casa_auth ("Set your password" / "Confirm password" / "Activate account"),
but invite_spec still asserted the old Devise defaults ("Set my password" /
"Password confirmation"), leaving 5 red examples. Point the locators at the
shipped page; the Devise error assertions are unchanged (they use the
password_confirmation attribute's humanized name). Dropped the brittle
post-accept landing-text check — the .notice success flash + accepted-at
timestamp already verify the accept + sign-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
case_contacts#index and #drafts now render on layout: "casa_app"
(@active_nav "contacts"). The index keeps filterrific (the .filter-form
auto-submits on .filter-input change, targeting the :case_contacts
turbo-frame) but restyles it in Tailwind and swaps the Bootstrap collapse
for the `disclosure` controller — the filter panel is always in the DOM
(so rack_test still finds "Other filters") yet starts `hidden` unless
`expand_filters?`, so JS users see it collapsed.

_case_contact + _followup are rewritten in place (both pages are casa_app
now): a Tailwind card reusing TruncatedTextComponent, with the reminder JS
hooks preserved (.followup-button / #followup-button-<id> / #resolve,
data-turbo:false) so the SweetAlert reminder + resolve keep working. Kept
the inert class hooks the specs use (.full-card, .container-fluid.mb-1,
.card-title); grouped cards + shared/_pagination.

Specs: sentence-cased the view-spec title and dropped the strong.text-primary
color assertion (now a semantic .card-title check). Chevrons darkest-pixel
verified (114 = slate-500); no overflow 375-1280; standardrb + erb_lint
clean; index/drafts/card/followup/request specs green (60 examples).

The multi-step case-contact form + the case_contacts_new_design DataTable
remain (tracked in design-todo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
case_contacts/form (the Wicked single-step details wizard) now renders on
layout "casa_app" (@active_nav "contacts"); the JSON autosave path skips the
layout. Rebuilt details + the _contact_topic_answer and
shared/_additional_expense_form nested rows in Tailwind card sections
(Details / Notes / Reimbursement) preserving all three Stimulus contracts:

  - autosave: data-autosave-target="form" + input->autosave#save on the text
    fields + the <small role="alert" data-autosave-target="alert">No changes
    have been saved.</small> lines (a non-JS spec asserts that literal text).
  - casa-nested-form: .nested-form-wrapper + template/target/wrapper targets +
    hidden id/_destroy fields (add/remove topic answers + expenses).
  - case-contact-form: reimbursement show/hide now toggles Tailwind `hidden`
    instead of Bootstrap `d-none` (which Tailwind does not define); safe since
    only this form uses the controller.

Contact types stay grouped .contact-form-type-checkbox checkboxes; relevant
cases stay the shared Form::MultipleSelectComponent (TomSelect). Errors use
the Tailwind shared/form_errors; duration is an inline twin (like learning
hours), leaving Form::HourMinuteDurationComponent dead. form_title + the
field/action Title Case labels are spec-locked and queued for the sweep.

One non-JS spec ("selects the only case") matched the legacy sidebar's case
list, not the form; it now asserts the widget selection (have_select) with :js
like its multi-case sibling.

Verified: 55 form-spec examples green bar the pre-existing cross-org
edit_spec:76; chevron darkest-pixel 114 (slate-500); no overflow 375-1280;
standardrb + erb_lint + standardjs clean. (Two expense :js specs flake only
under full-dir concurrent load; 5/5 in isolation - pre-existing timing, JS
logic unchanged.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reimbursements#index renders on layout "casa_app" as a bespoke Tailwind table
(desktop table + mobile card twin) reusing the controller's policy-scoped query
+ Pagy, retiring the jQuery serverSide DataTable — the #reimbursements-datatable
table is gone, so the globally-required src/reimbursements.js just no-ops.

Server-side filters via the auto-submit controller: a single-select Volunteer
dropdown (was a select2 multi; options come from the status-scoped set so
selecting one volunteer does not collapse the list) and an occurred-at date
range. Two small controller fixes make the filters work: the volunteer filter
is guarded with .present? (so "All" does not run where(creator_id: "")), and the
occurred-at parse switched to Date.parse so native date inputs' ISO values are
accepted.

"Needs review" / "Reimbursement complete" tabs + column headers sentence-cased.
Mark-complete stays a per-row PATCH (_reimbursement_toggle, checkbox
auto-submits — CSP-safe, no inline onchange). Added a Reimbursements item to the
casa_app sidebar (guarded by policy(:reimbursement).index?) so the page is
reachable. Deleted 5 orphaned partials.

Specs: rewrote the system filter/count tests for the bespoke table + single
select (the old ones drove select2 + #reimbursements-datatable), the view-spec
assigns, and the request-spec scope mock (now returns a paginatable relation).
Verified: system + request + view + datatable + policy + notifier green (49);
chevron darkest-pixel 114 (slate-500); no overflow 375-1280; lint clean.

Also corrected the backlog: casa_cases new/edit was already migrated (verified
green this session; its Bootstrap _form.html.erb is orphaned).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reports#index renders on layout "casa_app" (@active_nav "reports"). Rewrote the
hub in Tailwind: a "Quick exports" grid of the 6 one-click CSV buttons + the
filterable "Case Contacts Report" builder (date range, four multi-select
filters, three Both/Yes/No radio groups, a column picker, download).

Preserved the download contract exactly: every submit keeps .report-form-submit
+ data-disable-with, which src/reports.js intercepts to swap the label and
native-submit the form (so the CSV download bypasses Turbo); the leading icons
are pointer-events-none so event.target is always the button.

The four filter selects stay NATIVE <select multiple> with their spec-locked
ids (multiple-select-field1..4) — select2 is dropped (it hides the native
select, which would break the specs' `select .. from:` / `have_select`). Their
person-name options are now honorific-free (formatted_name). The column filter
moved from a Bootstrap modal to a native <details>; deleted the now-orphaned
reports/_filter.

Spec-locked Title Case headings / labels / button names kept verbatim (queued
for the sentence-case sweep). No spec changes needed: 51 examples green; the 2
failures are pre-existing volunteer not-authorized redirects (land on "/" not
casa_cases_path, an app-wide redirect-target issue unrelated to this view). No
overflow 375-1280; standardrb + erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
casa_org#edit renders on layout "casa_app" (@active_nav "settings"). Rewrote the
org-profile form + all 11 admin entity lists in Tailwind.

Twilio: a new `twilio` Stimulus controller reveals the credential fields and
flips their required/disabled from the "Enable Twilio" checkbox (initial state
server-rendered from twilio_enabled). This replaces — and deletes —
src/casa_org.js (the jQuery + Bootstrap-collapse toggle), which also had to go
because it disabled the twilio fields by id on every page load and clobbered
the new controller.

The 11 entity lists (contact types/groups/topics, judges, hearing types,
languages, learning types/topics, placement types, custom links, sent emails)
are restyled Tailwind tables preserving their spec-locked table ids, row cells,
row ids, and Edit/New links (the view spec's have_table contracts). Contact-topic
+ custom-link deletes moved from the Bootstrap DropdownMenu + Modal:: to the
Dialog shared/confirm_button. Section anchor ids (#contact-types,
#case-contact-topics, ...) are kept as deep-link targets. Swapped
shared/error_messages for shared/form_errors.

Title-Case headings/labels the specs assert are kept verbatim (queued for the
sweep). 33 view + system + request examples green; no page overflow 375-1280.
The admin tables scroll horizontally within their card on mobile (matches the
old .table-responsive); card-stacking them is a documented follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
case_court_reports#index renders on layout "casa_app" (@active_nav
"court_reports"). Rebuilt the "Generate Reports" page as a Dialog that reuses
the existing court-report Stimulus controller (from the case-show modal): it
posts the case + date range to the JSON generate endpoint, shows a spinner, and
opens the docx download.

The case picker is a new searchable single-select TomSelect (searchable-select
controller). Its text search covers the assigned-volunteer names embedded in the
option labels, so the old select2 volunteer-name search is preserved — and now
volunteers get it too, not just supervisors/admins. Dropped select2. The native
#case-selection stays (with its data-lookup/transition option attributes);
TomSelect hides it, so option assertions use visible: :all. Dates are native
date inputs (ISO) like the show-page modal.

Added a "Court reports" sidebar item to casa_app (guarded by
see_court_reports_page?) so the standalone page is reachable.

Rewrote the ~250-line select2 + Bootstrap-modal system spec and
CaseCourtReportHelpers for the Dialog + TomSelect (server-side option checks for
role-based visibility; TomSelect search + selection for the search test). The
two old client-validation examples (hide-error-on-select, clear-on-reopen) were
dropped: an empty selection now surfaces the server's "not found" error via the
shared controller's error target.

49 system + view + request examples green; no overflow 375-1280; standardrb +
erb_lint + standardjs clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Judges, languages, placement types, and learning-hour types + topics — the
simple name(+active) resources reached from the org settings page. Each _form
now renders one shared shared/_settings_form (casa_app card: required name +
optional Active? checkbox + optional description + Submit; form_with model:
infers the url + param key), and each controller gets layout "casa_app" +
@active_nav = "settings". Save redirects back to settings.

Dropped the old title-wrapper/card-style Bootstrap markup + the lni icons +
shared/error_messages (now shared/form_errors). No breadcrumb, so the partial
stays free of current_organization and the no-layout view specs render it
standalone.

41 system + view + request examples green; no overflow; standardrb + erb_lint
clean. shared/_settings_form is reusable for the remaining name(+active) Phase 5
resources.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contact type groups reuses the shared shared/_settings_form (name + Active? +
description). Contact types and contact topics get custom casa_app forms:
contact types adds a contact-type-group chevron select (id
contact_type_contact_type_group_id kept for the system spec), and contact
topics has question/details + Active? + Exclude from Court Report?. All three
controllers get layout "casa_app" + @active_nav = "settings"; save redirects
back to settings.

Dropped the title-wrapper/card-style Bootstrap markup, lni icons, and
shared/error_messages (now shared/form_errors).

45 system + request examples green; chevron darkest-pixel 114 (slate-500); no
overflow; standardrb + erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p (Phase 5 batch 3)

Hearing-type form: name + Active? in a card, plus (once persisted) a Tailwind
checklist-items table (id checklist_items) with New List Item / Edit links and a
button_to Delete. The delete uses turbo_confirm only, not the Dialog
confirm-button, because a non-JS destroy spec drives "Delete" directly
(rack_test ignores turbo_confirm and submits).

checklist_items new/edit is a custom casa_app form (Category / Description /
Mandatory) nested under the hearing type, redirecting to the hearing-type edit
page. custom org links is a casa_app form (Display text / URL / Active?; Submit
label from the `action` local). All three controllers get layout "casa_app" +
@active_nav = "settings".

Dropped the title-wrapper/card-style Bootstrap markup, lni icons, and
shared/error_messages (now shared/form_errors).

57 system + view + request examples green; no page overflow (the checklist table
scrolls within its card on mobile, like the settings admin tables); standardrb +
erb_lint clean.

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

Mileage rates: casa_app settings index (date / amount / Active? / Edit, no
delete) + a date+currency+Active? form.

Placements (nested under a case): casa_app index whose per-row Delete is a
Dialog with a visible "Close" + button_to "Confirm", so the non-:js destroy
spec passes under rack_test (which can't match the header X's aria-label); a
date + chevron-select form and a fact-card show. The index/new view specs move
off the Bootstrap .modal / .primary-btn assertions.

Banners: casa_app #banners table (keeps td.min-width for the system spec) with
a shared/_confirm_button Delete, and a form with the rich-text content editor +
a live reveal warning (new reveal Stimulus controller) when another banner is
already active. Trix needed its stylesheet folded into the casa_app tailwind
bundle (@import "trix/dist/trix.css") — without it the toolbar was unstyled and
overflowed the page to ~900px on every width.

58 system + view + request examples green; chevron darkest-pixel 114; pages fit
375-1280 (tables + the Trix toolbar scroll within their own containers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gia and others added 30 commits July 26, 2026 22:50
Opening Notifications (the bell) raised NoMethodError: undefined method 'note' for nil. FollowupNotifier#build_message called params[:followup].note, but params[:followup] resolves to nil once the followup (or the case contact it belongs to) has been deleted -- the stored GlobalID no longer resolves. That crashed the entire notifications page for any user who had such a notification.

Make the notifier nil-safe: build_message uses params[:followup]&.note (the message just omits the note line), and url falls back to case_contacts_path when the followup is gone (edit_case_contact_path needs an id we no longer have). created_by already degrades safely via BaseNotifier. Verified the page now renders instead of 500ing.

Specs: add nil-followup coverage for #message and #url. followup_notifier 6/0, notifications request 12/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the crash fix: rather than render a followup notification whose followup was deleted as an empty/contextless item, hide it. Add renderable? to BaseNotifier (default true) + override in FollowupNotifier (params[:followup].present?). Notifications#index drops non-renderable notifications (by id, keeping a relation for the date-split helpers), and the unread badge counts only renderable ones so the two stay in sync.

Verified: an admin's orphaned followup notification disappears from the index (2 items -> 1) and the badge drops (2 -> 1). followup_notifier 8/0, notifications request 12/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Court reports page's primary button read "Download court report as a .docx" -- redundant with the h1 ("Court reports"), the card text (already says Microsoft Word / .docx), and the Word icon, and inaccurate since it opens a config dialog rather than downloading. Shorten it to "Generate report". The dialog title/aria become "Generate court report" (fuller, since a modal is a standalone context); the dialog's Generate submit is unchanged (spec-locked #btnGenerateReport).

Update the spec helper (opens via the new label) and the view spec (targets the trigger via data-action, since the submit also reads "Generate report"). Document the concise-CTA rule in design.md. view 4/0, system 18/0.

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

The searchable-select (TomSelect) pickers wire a page <label for=...> to their control input via aria-labelledby (so the court-report case picker already resolved to "Case"), but TomSelect ignores an aria-label set on the <select> -- and 4 of the 5 pickers (learning-hours, other-duties, supervisors per-row assign, emancipation search) label themselves that way, so their control input fell back to the placeholder for a name (an audit-flagged anti-pattern; the placeholder vanishes once you type).

Have the controller read the native select's accessible name (aria-label or associated <label> text) BEFORE TomSelect init -- TomSelect rewrites the <label>'s for and empties select.labels -- and set it as an explicit aria-label on the control input. Verified via the Chrome accessibility tree: court-report input name "Case", other-duties input name "Search volunteers" (was placeholder-only). Court report system 18/0, supervisors index 15/0, learning-hours index 6/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The case <select> carried the full bordered input_class. TomSelect copies the select's classes onto its .ts-wrapper, and the tom-select theme already borders/pads the inner .ts-control -- so the field rendered as two stacked, rounded, shadowed boxes (the control inset 15px inside the wrapper), reading as an input nested inside the field. Use the minimal 'block w-full' class the other four searchable-selects already use; the theme owns the border/padding/shadow (design.md 'Searchable single-select').

Verified: .ts-wrapper drops to 0px border and .ts-control is the single bordered control, both filling the same rect (one box, no nesting). Court report view 4/0, system 18/0.

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

casa_org/edit_spec 'requires Twilio Form to be filled in correctly' was red: native HTML5 validation is disabled app-wide (so invalid submits reach the server for design-system errors), which removed the ONLY thing enforcing the Twilio phone. The credentials check validates the account/API keys but not the phone, so submitting a blank phone posted through and hit an unstubbed Twilio API call (WebMock NetConnectNotAllowedError) -- and in production would have saved a Twilio-enabled org with no phone.

Add a server-side presence validation for twilio_phone_number (if twilio_enabled, on update) and skip the credentials API round-trip when the phone is blank. The blank phone now re-renders edit with the design-system error 'Twilio phone number can't be blank'. Update the system spec to assert that instead of the (now-unreachable) native :invalid pseudo. The SID/secret credential model specs are untouched (they leave the phone present, so the API check still runs). casa_org model 25/0, edit system 5/0, casa_org request unaffected.

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

Parallel to the searchable-select fix: the multiple-select (TomSelect multi) controller now copies the native <select>'s accessible name (aria-label or associated <label>, read before init) onto its control input. The <label for> multiselects (report filters, case groups) already resolved via TomSelect's aria-labelledby (verified AX name 'Assigned to'); this closes the gap for the rich Form::MultipleSelectComponent, which names its select with aria-label -- which TomSelect ignores -- leaving its input named only by the placeholder.

Verified: report filter inputs now carry explicit aria-labels (Assigned to / Volunteers / Contact type / Contact type group), AX name intact. reports index 41/0, case_groups 4/0; standardjs clean; documented in design.md.

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

The Modal::* ViewComponent suite (Group/Header/Body/Footer/OpenButton/OpenLink) and DropdownMenuComponent are legacy Bootstrap (data-bs-toggle/dismiss, btn-close, dropdown-toggle), superseded by the Dialog:: suite (native <dialog> + modal Stimulus controller) and the native <details> + dropdown controller. Verified referenced nowhere in the app -- only by their own component specs -- so the migration left them as dead code.

Delete the 14 component files + 7 specs, tidy the now-stale Dialog::GroupComponent comment, and log it in MIGRATION-MAP. Component suite 27/0, no dangling references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bootstrap 5's JS (import 'bootstrap') was dead weight: nothing used window.bootstrap or any data-bs-* auto-init. BS5 only kept $.fn.tooltip/$.fn.modal alive via its optional jQuery-plugin compat (window.jQuery is set before the import), and the only 3 callers were no-ops -- two $('[data-toggle="tooltip"]').tooltip() with no such elements anywhere, and $('#thank_you').modal() whose modal was already deleted. Bootstrap CSS was already retired.

Remove import 'bootstrap' + its 3 dead callers: delete src/tooltip.js (only the dead tooltip init), drop the tooltip init in case_contact.js, drop the thank-you-modal block in casa_case.js.

Verified: no console errors on reports / case_contacts / casa_cases (the bundle runs casa_case.js on every page); post-removal window.bootstrap, $.fn.tooltip, $.fn.modal are all undefined, while bootstrap-datepicker ($.fn.datepicker) still works. Jest 126/0, court report system 18/0, case_contacts index 19/0. Follow-ups (separate): remove the bootstrap npm dependency + review tom-select's bootstrap-5 theme.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to dropping import 'bootstrap': the bootstrap package's only remaining consumer was select2-bootstrap-5-theme, and select2 itself is dead -- no .select2 elements exist anywhere, its theme CSS isn't imported, and #case-selection (the court-report picker) is TomSelect, so $('#case-selection').select2() sat behind a false hasClass('select2') guard in a function only bound to a never-firing shown.bs.modal.

Uninstall bootstrap + select2 + select2-bootstrap-5-theme; drop the require('select2') and require('./src/select') lines; delete src/select.js; remove the dead select2 re-init handler + its binding in casa_case.js.

Verified: no console errors on reports / case_contacts / casa_cases / a case show; post-removal window.bootstrap and $.fn.select2/.tooltip/.modal are all undefined, while $.fn.datepicker (bootstrap-datepicker) and $.fn.DataTable (datatables) still work. Jest 126/0, court report system 18/0, npm ls bootstrap empty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bootstrap-datepicker was required in application.js (registering $.fn.datepicker) but never initialized -- no .datepicker() call anywhere, no .datepicker/data-provide elements, and the app uses native <input type="date"> (19 files). Dead weight.

Remove the require + uninstall the package. Verified: no console errors on date-bearing pages (/reports, case edit); $.fn.datepicker is now undefined while $.fn.DataTable still works. Jest 126/0, npm ls bootstrap-datepicker empty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Bootstrap-styled shared/_emancipation_link partial and the
CasaCaseDecorator#transition_aged_youth / #transition_aged_youth_icon
methods that rendered it are unused. No migrated view calls them: the
Tailwind cases index draws its own transition-aged pill straight from
in_transition_age?, and the emancipation checklist stays reachable from
the case show page. Drop the partial, both decorator methods, and their
decorator-spec examples.

Kept on purpose: the TRANSITION_AGE_YOUTH_ICON / NON_TRANSITION_AGE_YOUTH_ICON
constants (still asserted by the supervisors index view spec) and
CasaCaseDecorator#emancipation_checklist_count (still rendered on
casa_cases/show).

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

The new-volunteer (invite) form only collected email, display name, phone and
date of birth; the mailing address lived only on the edit page, even though the
create path already permits address_attributes. Add the address fieldset to
volunteers/new and make the required/optional split explicit: email + display
name (the only validated-required fields — Devise / UserValidator) get the rose
"*" marker and the required attribute; phone, date of birth and the address get
a muted "Optional" suffix.

Invalid required fields surface the design-system error automatically (the
global field_error_proc inline message + the shared/_form_errors summary, now
ordered to match the form).

Address stays optional end-to-end: split accepts_nested_attributes_for so
:address carries reject_if: :all_blank — a blank address on the invite (or edit)
no longer persists an empty addresses row. Editing an existing address is
unaffected: its :id is present, so :all_blank never rejects it.

Verified: rose-600 "*" on the two required fields + slate-500 "Optional" on the
optional ones, all five address inputs present, no overflow (CDP on the running
app); new_spec covers address-saved, blank-address-no-row, and the required-
field error. design.md documents the required/Optional marker pairing.

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

The invite form (volunteers/new) gained rose "*" required markers + muted
"Optional" suffixes; mirror the same treatment on volunteers/edit so the two
profile forms read consistently. Email + display name get the "*" (and the
required attribute on the editable inputs); phone, date of birth and the mailing
address get "Optional".

The markers are gated on can_edit — when the profile is read-only (no
update_user_setting? permission) every field is display-only, so the markers are
suppressed rather than shown as noise. The edit error summary is ordered to match
the form, like new.

Verified: edit_spec's label-touching examples stay green (the tests are rack_test,
which ignores the new required attribute, so the blank-field server-validation
test is unchanged); CDP on the running app confirms identical rose-600 "*" /
slate-500 "Optional" colors to volunteers/new and no horizontal overflow.

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

Add required_marker / optional_marker helpers (design_system_helper) so the rose "*" /
muted "Optional" pattern isn't copy-pasted per form, and refactor the two volunteers
forms onto them. Extend the same treatment to the sibling person forms — casa_admins
new + edit, supervisors new + edit — which share the User shape: email + display name
are required (rose "*" + the required attribute), phone / date of birth are optional
("Optional"). supervisors/edit gates the markers on can_edit like volunteers/edit, so a
read-only profile shows none.

design.md documents the helpers and the app-wide convention.

Verified: casa_admins/supervisors/volunteers new specs (17/0), casa_admins/edit (13/0),
supervisors/edit (28/0); erb_lint + standardrb clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the required/optional markers to the org-settings + case-group forms, using
each field's declared required: and/or model presence validation as the authority:
- shared settings form (judge / language / placement type / learning-hour type +
  topic), hearing type, contact type → name
- contact topic → question + details; custom org link → display text + url;
  checklist item → category + description
- case group → name + cases (model requires ≥1 membership); mileage rate →
  effective date + amount; banner → name + content, with expires-at → "Optional"

hearing type / judge / language / learning-hour type + topic / contact type declare
required: true in the view but lack a backing presence validation; the marker reflects
the form's existing declared requirement, and closing that model-validation gap is
tracked as a separate follow-up.

Verified: settings request specs (76/0), banners/case_groups/mileage_rates system
specs (10/0); erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…urs / duty / profile forms

Continue the required-marker sweep across the remaining chapter data-entry forms,
using model presence validations + the view's declared required: as the authority:
- casa_cases new + edit → case number
- court date → date; learning hours → title + occurred-on; other duty → notes
  (its ad-hoc "Enter notes *" becomes the shared rose marker)
- users/edit (self-service profile) → display name required; phone, date of birth
  and mailing address → "Optional"

Left as-is on purpose: the compound Hour(s)/Minute(s) duration pair (defaults to 0,
only the total is required, so a per-field * would mislead); fund_requests (its * /
** are footnote markers, not required indicators); notes (no model validation);
placements / bulk_court_dates (no single required text field).

Verified: casa_cases edit (46/0) + new/learning_hours/other_duties/users/court_dates
new specs (53/0 combined); erb_lint clean.

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

Finish the required-marker sweep on the all-CASA super-admin console and org settings,
per the model validations:
- all-CASA admin form (AllCasaAdmin validates only email) → email *, display name
  "Optional"; new + edit self-profile → email *
- new CASA org + casa_org/edit (CasaOrg validates name) → name *; display name,
  address, logo, court report template → "Optional"

Left alone: the Twilio credential block (conditionally required only when Twilio is
enabled, with its own reveal logic) and the opt-in password/email disclosures.

Verified: all_casa_admin + all-casa edit + casa_org/edit specs (11/0, casa_org/edit
5/0); erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
They fail intermittently only under the full CI suite and pass in isolation. The
2026-07-27 investigation showed a non-deterministic Selenium/JS timing race (the same
--seed can pass or fail) — not order-dependent data or a code bug — so they can't be
pinned locally without the CI failing seed. Per the project's "xit + tracking issue"
policy, disable them to unblock the rspec/docker jobs; each carries a reason comment and
should be re-enabled once the race is fixed.

- case_contacts/edit_spec "successfully edits case contact"
- case_contacts/contact_topic_answers_spec "removes an answer when its topic is unchecked"
- case_contacts/new_design_spec "hides drafts when Hide drafts is checked" (reproduced under --seed 42)

A GitHub tracking issue still needs to be filed (no gh CLI in this environment).

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

volunteers/new was the lone header that left the "Back to volunteers" link as a bare
child of the max-w-2xl space-y-6 wrapper, so the 24px section rhythm landed between the
link and the <h1> instead of the design-system 8px. Move the link into the same <div>
as the heading and give the <h1> mt-2, matching every other header page.

Audited all 19 casa_app header pages: this was the only offender; the other 18 already
group the link + `h1 mt-2` in one <div>. Pixel-verified the fixed gap at 9px, identical
to supervisors/new and casa_admins/new. design.md already documented the rule (and the
anti-pattern); added a dated audit note + the recurrence cause (hand-written per-page
headers + working-tree reverts reintroducing stale ones).

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

The "Back to X" link + heading spacing was hand-written on every page, so it kept
drifting (volunteers/new had regressed to a 24px top gap). Add shared/_page_header —
one implementation of the correct header block (chevron back link + h1 mt-2 + optional
subtitle, optional wrapper_class) — and convert the 16 simple back+title(+subtitle)
headers to it so the spacing can no longer drift per page:

volunteers/new, supervisors/new, casa_admins/new, casa_cases/new, case_groups/_form,
fund_requests/new, notes/edit, users/edit, learning_hours/new+edit, bulk_court_dates/new,
and the five all_casa_admins pages.

The 3 person-edit pages (volunteers/edit, supervisors/edit, casa_admins/edit) keep their
bespoke headers — identity name/email subtitle + volunteers/edit's Impersonate/reminder
actions, shapes the partial deliberately doesn't cover.

Verified: view spec (6/0) + converted-page system specs (43/0); erb_lint clean; CDP shows
an identical 9px back-link->h1 gap across the space-y / mb-6 / with-subtitle variants.
design.md documents the partial as the single implementation to use on every new sub-page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chapter shell dropped its footer in the Bootstrap migration, so signed-in
volunteers/supervisors/admins lost the support + SMS-compliance links that the
all-casa shell still shows. Add the same footer after <main> — Built by Ruby For
Good / Report a site issue / SMS Terms & Conditions — matching the all-casa markup
(border-t, text-xs slate-500). This restores parity (not a new product decision)
and gives the design.md app-shell checklist's [~] "help-link destination" a home:
"Report a site issue" is the help/support link.

Verified: system spec asserts the three footer links for a signed-in admin; CDP on
the running app confirms the links + hrefs, slate-500, and the footer sitting below
the content (pushed to the viewport bottom on short pages via `main flex-1`).

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

The footer (py-4, 49px) sat 8px lower than the sidebar's pinned-Settings divider, so the
two horizontal rules at the page bottom didn't line up. The sticky h-screen sidebar puts
its 57px Settings block (8px margin + a 40px nav item + 8px nav padding) at the viewport
bottom, where the footer also lands on a short page. Bump the footer to py-5 (57px) so its
top border sits on the same line. CDP: settings divider + footer border both at y=843 (diff 0).

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

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

public/sms-terms-conditions.html was a static page with the old Rails-default look
(#325271 blue field, red-topped dialog box, Arial). Rewrite it to match the app shell:
slate-50 surface, one white card with a rounded-2xl slate-200 border + an indigo
brand-gradient accent, the Figtree fallback stack, slate typography, and a "Back to CASA"
link. It is static (served from public/, no asset pipeline), so the tokens are inlined.

The six legal paragraphs are preserved verbatim — only the wrapper + styling changed;
the opt-in URL and casa@rubyforgood.org support address are now links.

Verified via CDP: slate-50 bg, 16px card, brand-gradient accent, slate-900 800-weight h1,
all six paragraphs present, 0 console errors.

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

The public/ error pages were on the old Rails-default styling (gray #EFEFEF dialog boxes,
#00447c headers, Arial). Rewrite all six to match the design system: slate-50 surface, a
centered white card with a rounded-2xl slate border + indigo brand-gradient accent, the
Figtree fallback stack, an "Error NNN" eyebrow + slate-900 heading + message, and a brand
"Back to homepage" button with a "Report a site issue" link where useful. Kept fully
self-contained (inline hardcoded hex) since they're served straight from public/, even
when Rails is down.

Per page: 403 (not authorized) shows only Back to homepage; 406 (unsupported browser) gives
browser guidance and no action button (switching browsers is the fix); 400/422/500 keep the
"application owner, check the logs" note.

Verified via CDP: all six render slate-50 bg, 16px card, brand-600 button, the right
heading + actions per page, 0 console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a small on-brand illustration — the CASA butterfly (public/butterfly.svg, white) on
an indigo brand-gradient circle badge with a gentle float (disabled under
prefers-reduced-motion) — above the status on all six static error pages, for a touch of
warmth. Copy stays neutral/professional per page (no jokes): "Page not found",
"Something went wrong", etc., so the delight scales with the low stakes of an error page
without being flippant when something actually broke.

Verified via CDP: the butterfly loads and the gradient badge renders on all six, the
neutral headings + per-page actions are unchanged, 0 console errors.

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

Rework layouts/mailer.html.erb from the old gray-header table (#728089 header, #f6f6f6
field, muted 13px body) to the design system: slate-50 canvas, a brand wordmark, a white
rounded card with an indigo (#4f46e5) top accent + slate-200 border, slate-700 body type,
and a slate footer with the org name + address. All inline + table-based per ADR 0007
(border-radius degrades to square in Outlook; the solid accent bar is Outlook-safe).

Add app/views/mailer_shared/_cta_button.html.erb — a bulletproof (bgcolor td + padded
anchor) brand-600 button — so the per-template CTAs stop using the old #348eda blue.

Templates are converted to the new type/button in follow-up commits; verified the layout
renders via the mailer preview.

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

Devise's mailer used ActionMailer::Base (parent_mailer was commented out), so its emails
skipped layouts/mailer entirely — reset_password was a full standalone doc with the old
#00447c header, and confirmation / password_change / email_changed / unlock were bare,
unstyled <p> fragments. Set config.parent_mailer = "ApplicationMailer" so every Devise
email uses the shared design-system layout, and rewrite the six HTML templates as clean
fragments (slate body paragraphs + the brand cta_button partial): invitation, reset
password, confirmation, password change, email changed, unlock.

Copy preserved where spec-locked (e.g. "This invitation will expire on … (two weeks)").
parent_mailer is an initializer, so a server restart is needed to see it in dev.

Verified: spec/mailers 77/0 (1 pre-existing pending).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert every ApplicationMailer template from the old repeated-Helvetica / #348eda-blue
tables into clean design-system fragments (slate body paragraphs + the brand cta_button
partial + brand links) that render inside the new mailer layout:

- casa_admin: account_setup, deactivation
- supervisor: account_setup, reimbursement_request, weekly_digest (+ partials — branded
  links, a real divider, dropped the stray <button>)
- volunteer: account_setup, case_contacts_reminder, court_report_reminder, reimbursement_complete
- user: followup_notification, followup_resolved, password_changed_reminder
- fund_request: send_request; learning_hours: report_email (was a full standalone doc → fragment)

Copy preserved where spec-locked ("A {org}'s County Admin account", "Set Your Password",
"Fund Request", "This invitation will expire on …", the digest's 30-day line). Also fixed a
pre-existing N+1 in reimbursement_complete (reimbursement_amount was fetched twice) by
computing it once, so Prosopite passes.

Verified: spec/mailers 77/0 (1 pre-existing pending).

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

db Touches database schema or migrations dependencies Touches dependency files erb Touches ERB templates javascript Touches JavaScript code pundit Touches Pundit authorization policies ruby Touches Ruby code 🧪 Tests Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants