# ABA staff designation — replace role-name matching with `teams.is_aba_staff`

**Filed:** Batch 53, 2026-08-02.
**Status:** deferred to post-demo. Interim workaround in place.

## Problem

`ProgramAssignmentController::searchStaff` decides whether a staff member can
be assigned to an ABA session by pattern-matching their role name against a
keyword list — `aba`, `rbt`, `bcba`, `bcaba`, `behaviour/behavior therapist`,
`instructor therapist`, `clinical supervisor`, `therapist`, `special program`.

The org this ships to today calls every staff member "Staff". None of them
match any keyword. The picker returns 0 rows and no session can be recorded.

Three alternative signals exist (active `program_student_assignments` row,
teaching a Special Program class, holding an ABA/Program/SessionData/Special
permission) but each depends on setup that is itself blocked until the picker
works.

## Interim workaround (Batch 53)

Added a fifth signal: exact role-name match on `"ABA Staff"`. An admin
creates a role called `ABA Staff` and assigns it to therapists. Backwards
compatible — the four inferred signals stay for orgs that DO use RBT/BCBA
role naming.

Live DB was migrated with:

```sql
-- One-time role creation + Amanda's reassignment (Batch 53, user-approved).
INSERT INTO user_roles (roleName, privileges, isDelete, created_by, created_at, updated_at)
SELECT 'ABA Staff', privileges, 0, 1, NOW(), NOW() FROM user_roles WHERE id = 4;
SET @new_role := LAST_INSERT_ID();
INSERT INTO role_permissions (roleId, permissionId, isDelete, created_by, created_at, updated_at)
SELECT @new_role, permissionId, 0, 1, NOW(), NOW()
  FROM role_permissions WHERE roleId = 4 AND isDelete = 0;
UPDATE users SET user_role = @new_role WHERE id = 284;   -- Amanda Cruz
```

Reversal:
```sql
UPDATE users SET user_role = 4 WHERE id = 284;
DELETE FROM role_permissions WHERE roleId = (SELECT id FROM user_roles WHERE roleName = 'ABA Staff');
DELETE FROM user_roles WHERE roleName = 'ABA Staff';
```

## Field evidence for discoverability

On 2026-08-02 a real user, expecting to make a staff member ABA-eligible,
assigned them the "ABA Therapy" **subject** and then went to record a
session. The picker was still empty. Their mental model was reasonable:
the app has a subject literally called "ABA Therapy"; assigning it to a
staff member ought to mean something.

It does not. Subject assignment is an academic scoping pivot
(`staff_grades_subject_tables`), unrelated to clinical eligibility, and
~150 active staff — including sample/test rows — are already on the ABA
subjects. Making subject-assignment a qualifying signal would flip the
picker from "zero eligible" to "essentially the whole staff list" and
lose all discriminating value. So we didn't.

But the *expectation* is real, and the current design ("hold the exact
role name 'ABA Staff'") is not discoverable — nothing in the Staff edit
screen tells an admin the role name matters. An explicit
`teams.is_aba_staff` checkbox on the Staff edit form solves both problems
at once: it names what qualifies someone, and the field is visible in the
same place an admin would go looking. Add this incident as motivation
when scoping the post-demo change.

The recorder's staff-picker empty state was tightened in the same batch
to spell out that subject assignment does NOT confer eligibility —
compensating copy for the current design, not a substitute for the
proper fix.

## Post-demo target

Replace role-name matching (both keyword and exact) with a **`teams.is_aba_staff tinyint(1) NOT NULL DEFAULT 0`** boolean column, set from the Staff edit screen by an admin. Clinical capability should not be inferred from a role name — role names are for authorisation, not clinical designation. The two axes are orthogonal (an admin may hold every permission but never do sessions; a therapist may have limited permissions but be the person recording data).

Steps:
1. Migration adds `teams.is_aba_staff`.
2. Staff edit UI adds a checkbox "ABA session staff".
3. `ProgramAssignmentController::searchStaff` gate becomes `WHERE teams.is_aba_staff = 1` (drop keyword + exact-role + inferred signals).
4. Data backfill: `UPDATE teams SET is_aba_staff = 1 WHERE ... [criteria the org chooses]` — likely everyone currently on the `ABA Staff` role.
5. Retire the `ABA Staff` role, or leave it as a permission role only.
6. Update seeder to write `is_aba_staff = 1` on the demo staff row it designates, instead of relying on a role hint.

## Related

- `docs/post-demo/session-progress-access-check.md` — parallel ticket for
  scoped-role access checks on session progress reads.
- Batch 52 audit — six psa-union sites all now go through
  `DataScopeHelper::scopeStudentsViaProgramOwnership`.
