Use a specialty dropdown when adding practitioners.
Deploy Ladill Care / deploy (push) Successful in 26s
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:
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user