Use a specialty dropdown when adding practitioners.
Deploy Ladill Care / deploy (push) Successful in 26s

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 <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 17:39:29 +00:00
co-authored by Cursor
parent 2ac84faf73
commit 18c24077e3
8 changed files with 127 additions and 8 deletions
@@ -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);
@@ -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);
+19
View File
@@ -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<string>
*/
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.
*/
+35
View File
@@ -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
@@ -78,8 +78,13 @@
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Specialty (optional)</label>
<input type="text" name="specialty" value="{{ old('specialty') }}"
class="mt-1 w-full rounded-lg border-slate-300 text-sm" placeholder="General practice">
<select name="specialty" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">None</option>
@foreach ($specialties as $specialty)
<option value="{{ $specialty }}" @selected(old('specialty') === $specialty)>{{ $specialty }}</option>
@endforeach
</select>
@error('specialty')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
</div>
</div>
@@ -12,7 +12,13 @@
</div>
<div>
<label class="block text-sm font-medium">Specialty (optional)</label>
<input type="text" name="specialty" value="{{ old('specialty') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm" placeholder="General practice">
<select name="specialty" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">None</option>
@foreach ($specialties as $specialty)
<option value="{{ $specialty }}" @selected(old('specialty') === $specialty)>{{ $specialty }}</option>
@endforeach
</select>
@error('specialty')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<fieldset>
<legend class="block text-sm font-medium">Branches</legend>
@@ -11,7 +11,13 @@
</div>
<div>
<label class="block text-sm font-medium">Specialty (optional)</label>
<input type="text" name="specialty" value="{{ old('specialty', $practitioner->specialty) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<select name="specialty" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">None</option>
@foreach ($specialties as $specialty)
<option value="{{ $specialty }}" @selected(old('specialty', $practitioner->specialty) === $specialty)>{{ $specialty }}</option>
@endforeach
</select>
@error('specialty')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<fieldset>
<legend class="block text-sm font-medium">Branches</legend>
+33 -2
View File
@@ -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('<select name="specialty"', false)
->assertSee('General Practice')
->assertSee('Dentistry')
->assertDontSee('placeholder="General practice"', false);
}
public function test_practitioner_rejects_unknown_specialty(): void
{
$this->withoutMiddleware(EnsurePlatformSession::class);
[$owner, $organization, $east] = $this->seedOrg();
$this->actingAs($owner)
->post(route('care.practitioners.store'), [
'name' => 'Dr. Unknown',
'specialty' => 'Not A Real Specialty',
'branch_ids' => [$east->id],
])
->assertSessionHasErrors('specialty');
}
public function test_doctor_with_multiple_assigned_branches_can_switch_among_them_only(): void
{
$this->withoutMiddleware(EnsurePlatformSession::class);