From 18c24077e3333b1de09d985350bef70e4f7ba88d Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 18 Jul 2026 17:39:29 +0000 Subject: [PATCH] Use a specialty dropdown when adding practitioners. Replace free-text specialty on practitioner and team-invite forms with a shared selectable list so clinicians pick a known specialty. Co-authored-by: Cursor --- .../Controllers/Care/MemberController.php | 7 +++- .../Care/PractitionerController.php | 14 +++++++- app/Models/Practitioner.php | 19 ++++++++++ config/care.php | 35 +++++++++++++++++++ .../views/care/admin/members/create.blade.php | 9 +++-- .../care/admin/practitioners/create.blade.php | 8 ++++- .../care/admin/practitioners/edit.blade.php | 8 ++++- .../Feature/CarePractitionerBranchesTest.php | 35 +++++++++++++++++-- 8 files changed, 127 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Care/MemberController.php b/app/Http/Controllers/Care/MemberController.php index 556d896..97a88b0 100644 --- a/app/Http/Controllers/Care/MemberController.php +++ b/app/Http/Controllers/Care/MemberController.php @@ -72,6 +72,7 @@ class MemberController extends Controller 'branches' => $branches, 'roles' => config('care.roles'), 'mailboxOptions' => $mailboxOptions, + 'specialties' => Practitioner::specialtyOptions(), ]); } @@ -92,9 +93,13 @@ class MemberController extends Controller ], 'create_practitioner' => ['sometimes', 'boolean'], 'practitioner_name' => ['nullable', 'string', 'max:255'], - 'specialty' => ['nullable', 'string', 'max:255'], + 'specialty' => ['nullable', 'string', 'max:255', Rule::in(Practitioner::specialtyOptions())], ]); + if (($validated['specialty'] ?? '') === '') { + $validated['specialty'] = null; + } + if (! empty($validated['branch_id'])) { $branch = Branch::owned($owner)->findOrFail($validated['branch_id']); abort_unless($branch->organization_id === $organization->id, 404); diff --git a/app/Http/Controllers/Care/PractitionerController.php b/app/Http/Controllers/Care/PractitionerController.php index e2583f4..b73c499 100644 --- a/app/Http/Controllers/Care/PractitionerController.php +++ b/app/Http/Controllers/Care/PractitionerController.php @@ -52,6 +52,7 @@ class PractitionerController extends Controller 'branches' => $this->activeBranches($owner, $organization->id), 'departments' => $this->activeDepartments($owner, $organization->id), 'members' => $this->linkableMembers($owner, $organization->id), + 'specialties' => Practitioner::specialtyOptions(), ]); } @@ -101,6 +102,7 @@ class PractitionerController extends Controller 'branches' => $this->activeBranches($owner, $organization->id), 'departments' => $this->activeDepartments($owner, $organization->id), 'members' => $this->linkableMembers($owner, $organization->id), + 'specialties' => Practitioner::specialtyOptions($practitioner->specialty), ]); } @@ -154,9 +156,15 @@ class PractitionerController extends Controller */ protected function validated(Request $request, string $owner, int $organizationId): array { + $allowedSpecialties = Practitioner::specialtyOptions( + $request->route('practitioner') instanceof Practitioner + ? $request->route('practitioner')->specialty + : null, + ); + $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], - 'specialty' => ['nullable', 'string', 'max:255'], + 'specialty' => ['nullable', 'string', 'max:255', Rule::in($allowedSpecialties)], 'branch_ids' => ['required', 'array', 'min:1'], 'branch_ids.*' => [ 'integer', @@ -166,6 +174,10 @@ class PractitionerController extends Controller 'member_id' => ['nullable', 'integer', 'exists:care_members,id'], ]); + if (($validated['specialty'] ?? '') === '') { + $validated['specialty'] = null; + } + if (! empty($validated['department_id'])) { $department = Department::owned($owner)->with('branch')->findOrFail($validated['department_id']); abort_unless($department->branch?->organization_id === $organizationId, 404); diff --git a/app/Models/Practitioner.php b/app/Models/Practitioner.php index 448f075..64011b4 100644 --- a/app/Models/Practitioner.php +++ b/app/Models/Practitioner.php @@ -64,6 +64,25 @@ class Practitioner extends Model return $this->hasMany(Appointment::class, 'practitioner_id'); } + /** + * Specialty dropdown options. Preserves a legacy/custom value when editing. + * + * @return list + */ + public static function specialtyOptions(?string $current = null): array + { + $options = array_values(array_filter( + config('care.practitioner_specialties', []), + fn ($label) => is_string($label) && $label !== '', + )); + + if ($current !== null && $current !== '' && ! in_array($current, $options, true)) { + $options[] = $current; + } + + return $options; + } + /** * Select option label — includes branch context when available. */ diff --git a/config/care.php b/config/care.php index 7b01dc2..0e05208 100644 --- a/config/care.php +++ b/config/care.php @@ -43,6 +43,41 @@ return [ 'child_welfare' => 'Child Welfare Clinic', ], + /* + | Practitioner specialty options (Admin → Practitioners / Team invite). + | Stored as free-text labels on care_practitioners.specialty. + */ + 'practitioner_specialties' => [ + 'General Practice', + 'Internal Medicine', + 'Nursing', + 'Outpatient', + 'Emergency', + 'Laboratory', + 'Radiology', + 'Pharmacy', + 'Maternity', + 'Dentistry', + 'Ophthalmology / Eye care', + 'Physiotherapy', + 'Blood Bank', + 'Cardiology', + 'Psychiatry', + 'Pediatrics', + 'Orthopedics', + 'ENT', + 'Oncology', + 'Renal Care', + 'Surgery', + 'Vaccination & Immunization', + 'Pathology', + 'Infusion Centre', + 'Dermatology', + 'Podiatry', + 'Fertility', + 'Child Welfare Clinic', + ], + /* | Specialty practice modules (Settings → Modules). | Pro-gated via plans.*.features specialty_modules — expansions beyond core diff --git a/resources/views/care/admin/members/create.blade.php b/resources/views/care/admin/members/create.blade.php index e6a2fbe..f01383b 100644 --- a/resources/views/care/admin/members/create.blade.php +++ b/resources/views/care/admin/members/create.blade.php @@ -78,8 +78,13 @@
- + + @error('specialty')

{{ $message }}

@enderror
diff --git a/resources/views/care/admin/practitioners/create.blade.php b/resources/views/care/admin/practitioners/create.blade.php index 8ae726c..7b7b7b0 100644 --- a/resources/views/care/admin/practitioners/create.blade.php +++ b/resources/views/care/admin/practitioners/create.blade.php @@ -12,7 +12,13 @@
- + + @error('specialty')

{{ $message }}

@enderror
Branches diff --git a/resources/views/care/admin/practitioners/edit.blade.php b/resources/views/care/admin/practitioners/edit.blade.php index 5ffa998..66cf238 100644 --- a/resources/views/care/admin/practitioners/edit.blade.php +++ b/resources/views/care/admin/practitioners/edit.blade.php @@ -11,7 +11,13 @@
- + + @error('specialty')

{{ $message }}

@enderror
Branches diff --git a/tests/Feature/CarePractitionerBranchesTest.php b/tests/Feature/CarePractitionerBranchesTest.php index 61a7bfc..e4543d2 100644 --- a/tests/Feature/CarePractitionerBranchesTest.php +++ b/tests/Feature/CarePractitionerBranchesTest.php @@ -24,14 +24,14 @@ class CarePractitionerBranchesTest extends TestCase $this->actingAs($owner) ->post(route('care.practitioners.store'), [ 'name' => 'Dr. Tele', - 'specialty' => 'General', + 'specialty' => 'General Practice', ]) ->assertSessionHasErrors('branch_ids'); $this->actingAs($owner) ->post(route('care.practitioners.store'), [ 'name' => 'Dr. Tele', - 'specialty' => 'General', + 'specialty' => 'General Practice', 'branch_ids' => [$east->id, $west->id], ]) ->assertRedirect(route('care.practitioners.index')); @@ -51,6 +51,37 @@ class CarePractitionerBranchesTest extends TestCase ->assertDontSee('All branches'); } + public function test_practitioner_create_form_uses_specialty_dropdown(): void + { + $this->withoutMiddleware(EnsurePlatformSession::class); + + [$owner] = $this->seedOrg(); + + $this->actingAs($owner) + ->get(route('care.practitioners.create')) + ->assertOk() + ->assertSee('name="specialty"', false) + ->assertSee('