Label multi-branch department picks and add searchable patients.
Deploy Ladill Care / deploy (push) Successful in 27s

Make appointment/practitioner department (and practitioner) options show branch names so duplicates are distinguishable, and replace the patient select with a searchable dropdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 17:31:32 +00:00
co-authored by Cursor
parent 5a1d47b9cc
commit 2ac84faf73
10 changed files with 304 additions and 14 deletions
+43
View File
@@ -172,5 +172,48 @@ Alpine.data('dealForm', (initialLines = [], products = []) => ({
},
}));
// Searchable select for long option lists (patients, etc.).
Alpine.data('searchableSelect', (config = {}) => ({
options: Array.isArray(config.options) ? config.options : [],
selected: config.selected == null ? '' : String(config.selected),
placeholder: config.placeholder || 'Search…',
emptyLabel: config.emptyLabel || 'Select…',
query: '',
open: false,
toggle() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.$refs.query?.focus());
}
},
selectedLabel() {
if (! this.selected) {
return this.emptyLabel;
}
const match = this.options.find((o) => String(o.value) === String(this.selected));
return match?.label || this.emptyLabel;
},
get filtered() {
const q = this.query.trim().toLowerCase();
if (! q) {
return this.options;
}
return this.options.filter((o) => {
const hay = `${o.search || ''} ${o.label || ''}`.toLowerCase();
return hay.includes(q);
});
},
choose(value) {
this.selected = value == null ? '' : String(value);
this.open = false;
this.query = '';
},
selectFirst() {
if (this.filtered.length > 0) {
this.choose(this.filtered[0].value);
}
},
}));
window.Alpine = Alpine;
Alpine.start();
@@ -37,7 +37,7 @@
<select name="department_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">None</option>
@foreach ($departments as $department)
<option value="{{ $department->id }}" @selected(old('department_id') == $department->id)>{{ $department->name }}</option>
<option value="{{ $department->id }}" @selected(old('department_id') == $department->id)>{{ $department->labelForSelect() }}</option>
@endforeach
</select>
</div>
@@ -39,7 +39,7 @@
<select name="department_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">None</option>
@foreach ($departments as $department)
<option value="{{ $department->id }}" @selected(old('department_id', $practitioner->department_id) == $department->id)>{{ $department->name }}</option>
<option value="{{ $department->id }}" @selected(old('department_id', $practitioner->department_id) == $department->id)>{{ $department->labelForSelect() }}</option>
@endforeach
</select>
</div>
@@ -1,5 +1,10 @@
@php
$isWalkIn = $isWalkIn ?? false;
$patientOptions = collect($patients)->map(fn ($patient) => [
'value' => (string) $patient->id,
'label' => $patient->fullName().' ('.$patient->patient_number.')',
'search' => trim($patient->fullName().' '.$patient->patient_number.' '.($patient->phone ?? '').' '.($patient->email ?? '')),
])->all();
@endphp
<div class="grid gap-4 sm:grid-cols-2">
@@ -13,19 +18,21 @@
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Patient</label>
<select name="patient_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">Select patient…</option>
@foreach ($patients as $patient)
<option value="{{ $patient->id }}" @selected(old('patient_id') == $patient->id)>{{ $patient->fullName() }} ({{ $patient->patient_number }})</option>
@endforeach
</select>
<x-searchable-select
name="patient_id"
:options="$patientOptions"
:selected="old('patient_id')"
:required="true"
placeholder="Search name, number, phone…"
empty-label="Select patient…"
/>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Practitioner</label>
<select name="practitioner_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">Any / unassigned</option>
@foreach ($practitioners as $practitioner)
<option value="{{ $practitioner->id }}" @selected(old('practitioner_id') == $practitioner->id)>{{ $practitioner->name }}</option>
<option value="{{ $practitioner->id }}" @selected(old('practitioner_id') == $practitioner->id)>{{ $practitioner->labelForSelect() }}</option>
@endforeach
</select>
</div>
@@ -34,7 +41,7 @@
<select name="department_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value=""></option>
@foreach ($departments as $department)
<option value="{{ $department->id }}" @selected(old('department_id') == $department->id)>{{ $department->name }}</option>
<option value="{{ $department->id }}" @selected(old('department_id') == $department->id)>{{ $department->labelForSelect() }}</option>
@endforeach
</select>
</div>
@@ -0,0 +1,90 @@
@props([
'name',
'options' => [],
'selected' => null,
'placeholder' => 'Search…',
'required' => false,
'emptyLabel' => 'Select…',
])
@php
$optionList = collect($options)->map(function ($option) {
if (is_array($option)) {
return [
'value' => (string) ($option['value'] ?? ''),
'label' => (string) ($option['label'] ?? ''),
'search' => (string) ($option['search'] ?? $option['label'] ?? ''),
];
}
return [
'value' => (string) data_get($option, 'value', data_get($option, 'id', '')),
'label' => (string) data_get($option, 'label', data_get($option, 'name', '')),
'search' => (string) data_get($option, 'search', data_get($option, 'label', data_get($option, 'name', ''))),
];
})->values()->all();
$selectedValue = old($name, $selected);
$selectedValue = $selectedValue === null || $selectedValue === '' ? '' : (string) $selectedValue;
@endphp
<div
class="relative"
x-data="searchableSelect({
options: {{ \Illuminate\Support\Js::from($optionList) }},
selected: {{ \Illuminate\Support\Js::from($selectedValue) }},
placeholder: {{ \Illuminate\Support\Js::from($placeholder) }},
emptyLabel: {{ \Illuminate\Support\Js::from($emptyLabel) }},
})"
@keydown.escape.window="open = false"
@click.outside="open = false"
>
<input type="hidden" name="{{ $name }}" x-model="selected" @if ($required) required @endif {{ $attributes->except(['class']) }}>
<button
type="button"
class="mt-1 flex w-full items-center justify-between rounded-lg border border-slate-300 bg-white px-3 py-2 text-left text-sm text-slate-900 shadow-sm hover:border-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
@click="toggle()"
:aria-expanded="open.toString()"
>
<span class="truncate" x-text="selectedLabel()"></span>
<svg class="ml-2 h-4 w-4 shrink-0 text-slate-400" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg>
</button>
<div
x-show="open"
x-cloak
x-transition.opacity
class="absolute z-30 mt-1 w-full overflow-hidden rounded-xl border border-slate-200 bg-white shadow-lg"
>
<div class="border-b border-slate-100 p-2">
<input
type="search"
x-ref="query"
x-model="query"
@keydown.enter.prevent="selectFirst()"
placeholder="{{ $placeholder }}"
class="w-full rounded-lg border-slate-300 text-sm"
>
</div>
<ul class="max-h-56 overflow-y-auto py-1 text-sm">
<li>
<button type="button" class="flex w-full px-3 py-2 text-left text-slate-500 hover:bg-slate-50" @click="choose('')">
{{ $emptyLabel }}
</button>
</li>
<template x-for="option in filtered" :key="option.value">
<li>
<button
type="button"
class="flex w-full px-3 py-2 text-left hover:bg-indigo-50"
:class="selected === option.value ? 'bg-indigo-50 font-medium text-indigo-900' : 'text-slate-800'"
@click="choose(option.value)"
x-text="option.label"
></button>
</li>
</template>
<li x-show="filtered.length === 0" class="px-3 py-2 text-slate-400">No matches</li>
</ul>
</div>
</div>