Hide specialty mutate UI when staff lack manage access.
Deploy Ladill Care / deploy (push) Successful in 36s

View/refer users keep read surfaces and refer actions; zero-access
modules stay out of nav via existing enabledModulesForMember filtering.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 18:33:07 +00:00
co-authored by Cursor
parent 1e00c31b8b
commit 9e32eb32c5
17 changed files with 702 additions and 399 deletions
@@ -563,6 +563,7 @@ class SpecialtyModuleController extends Controller
abort_unless($modules->memberCanAccess($organization, $member, $module), 403);
$canManageSpecialty = $modules->memberCanManage($organization, $member, $module);
$canViewSpecialty = $modules->memberCanView($organization, $member, $module);
$canReferSpecialty = $modules->memberCanRefer($organization, $member, $module);
$specialtyAccessLevel = $modules->memberAccessLevel($organization, $member, $module);
@@ -881,6 +882,7 @@ class SpecialtyModuleController extends Controller
'canConsult' => $canConsult,
'canManageQueue' => $canManageQueue,
'canManageSpecialty' => $canManageSpecialty,
'canViewSpecialty' => $canViewSpecialty,
'canReferSpecialty' => $canReferSpecialty,
'specialtyAccessLevel' => $specialtyAccessLevel,
'queueIntegration' => $branchId
@@ -1,48 +1,52 @@
@php $canManage = $canManageSpecialty ?? true; @endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<h3 class="text-sm font-semibold text-slate-900">Dental imaging</h3>
<p class="mt-0.5 text-xs text-slate-500">Upload PA, bitewing, OPG, or clinical photos linked to teeth.</p>
<form method="POST" action="{{ route('care.specialty.dentistry.images', $workspaceVisit) }}" enctype="multipart/form-data" class="mt-4 space-y-3">
@csrf
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-xs font-medium text-slate-600">File</label>
<input type="file" name="attachment" required accept=".pdf,.jpg,.jpeg,.png,.webp" class="mt-1 block w-full text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Modality</label>
<select name="modality" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
@foreach ($dentalModalities as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Caption</label>
<input type="text" name="caption" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div class="sm:col-span-2">
<p class="text-xs font-medium text-slate-600">Teeth</p>
<div class="mt-2 flex max-h-28 flex-wrap gap-2 overflow-y-auto">
@foreach (array_merge($dentalRows['upper'] ?? [], $dentalRows['lower'] ?? []) as $code)
<label class="inline-flex items-center gap-1 rounded border border-slate-200 px-1.5 py-0.5 text-xs">
<input type="checkbox" name="tooth_codes[]" value="{{ $code }}" class="rounded border-slate-300 text-indigo-600">
{{ $code }}
</label>
@endforeach
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.images', $workspaceVisit) }}" enctype="multipart/form-data" class="mt-4 space-y-3">
@csrf
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-xs font-medium text-slate-600">File</label>
<input type="file" name="attachment" required accept=".pdf,.jpg,.jpeg,.png,.webp" class="mt-1 block w-full text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Modality</label>
<select name="modality" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
@foreach ($dentalModalities as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Caption</label>
<input type="text" name="caption" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div class="sm:col-span-2">
<p class="text-xs font-medium text-slate-600">Teeth</p>
<div class="mt-2 flex max-h-28 flex-wrap gap-2 overflow-y-auto">
@foreach (array_merge($dentalRows['upper'] ?? [], $dentalRows['lower'] ?? []) as $code)
<label class="inline-flex items-center gap-1 rounded border border-slate-200 px-1.5 py-0.5 text-xs">
<input type="checkbox" name="tooth_codes[]" value="{{ $code }}" class="rounded border-slate-300 text-indigo-600">
{{ $code }}
</label>
@endforeach
</div>
</div>
<div class="sm:col-span-2">
<label class="inline-flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" name="add_fee" value="1" class="rounded border-slate-300 text-indigo-600" checked>
Add imaging fee to bill
</label>
</div>
</div>
<div class="sm:col-span-2">
<label class="inline-flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" name="add_fee" value="1" class="rounded border-slate-300 text-indigo-600" checked>
Add imaging fee to bill
</label>
</div>
</div>
<button type="submit" class="btn-primary">Upload image</button>
</form>
<button type="submit" class="btn-primary">Upload image</button>
</form>
@endif
<div class="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<div @class(['mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-3' => true, 'mt-4' => ! $canManage])>
@forelse ($dentalImages as $image)
@php
$url = $image->attachment?->file_path
@@ -71,10 +75,12 @@
<p class="mt-0.5 text-slate-600">{{ $image->caption }}</p>
@endif
</div>
<form method="POST" action="{{ route('care.specialty.dentistry.images.void', [$workspaceVisit, $image]) }}" onsubmit="return confirm('Void this image?')">
@csrf
<button type="submit" class="text-rose-600 hover:underline">Void</button>
</form>
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.images.void', [$workspaceVisit, $image]) }}" onsubmit="return confirm('Void this image?')">
@csrf
<button type="submit" class="text-rose-600 hover:underline">Void</button>
</form>
@endif
</div>
</div>
</div>
@@ -1,3 +1,5 @@
@php $canManage = $canManageSpecialty ?? true; @endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<h3 class="text-sm font-semibold text-slate-900">Lab cases</h3>
<p class="mt-0.5 text-xs text-slate-500">Track crowns, bridges, dentures, and other lab work.</p>
@@ -17,7 +19,7 @@
@if ($case->due_at) · due {{ $case->due_at->format('d M Y') }} @endif
</p>
</div>
@if ($case->status !== 'cancelled' && $case->status !== 'fitted')
@if ($canManage && $case->status !== 'cancelled' && $case->status !== 'fitted')
<form method="POST" action="{{ route('care.specialty.dentistry.lab-cases.status', [$workspaceVisit, $case]) }}" class="flex gap-1">
@csrf
<select name="status" class="rounded-lg border border-slate-200 px-2 py-1 text-xs">
@@ -35,49 +37,51 @@
@endforelse
</ul>
<form method="POST" action="{{ route('care.specialty.dentistry.lab-cases', $workspaceVisit) }}" class="mt-6 space-y-3 border-t border-slate-100 pt-4">
@csrf
<h4 class="text-sm font-semibold text-slate-900">New lab case</h4>
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-xs font-medium text-slate-600">Type</label>
<select name="case_type" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
@foreach ($dentalLabCaseTypes ?? [] as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Lab name</label>
<input type="text" name="lab_name" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Due date</label>
<input type="date" name="due_at" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Status</label>
<select name="status" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value="draft">Draft</option>
<option value="ordered">Ordered</option>
</select>
</div>
<div class="sm:col-span-2">
<p class="text-xs font-medium text-slate-600">Teeth</p>
<div class="mt-2 flex max-h-24 flex-wrap gap-2 overflow-y-auto">
@foreach (array_merge($dentalRows['upper'] ?? [], $dentalRows['lower'] ?? []) as $code)
<label class="inline-flex items-center gap-1 rounded border border-slate-200 px-1.5 py-0.5 text-xs">
<input type="checkbox" name="tooth_codes[]" value="{{ $code }}" class="rounded border-slate-300 text-indigo-600">
{{ $code }}
</label>
@endforeach
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.lab-cases', $workspaceVisit) }}" class="mt-6 space-y-3 border-t border-slate-100 pt-4">
@csrf
<h4 class="text-sm font-semibold text-slate-900">New lab case</h4>
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-xs font-medium text-slate-600">Type</label>
<select name="case_type" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
@foreach ($dentalLabCaseTypes ?? [] as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Lab name</label>
<input type="text" name="lab_name" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Due date</label>
<input type="date" name="due_at" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Status</label>
<select name="status" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value="draft">Draft</option>
<option value="ordered">Ordered</option>
</select>
</div>
<div class="sm:col-span-2">
<p class="text-xs font-medium text-slate-600">Teeth</p>
<div class="mt-2 flex max-h-24 flex-wrap gap-2 overflow-y-auto">
@foreach (array_merge($dentalRows['upper'] ?? [], $dentalRows['lower'] ?? []) as $code)
<label class="inline-flex items-center gap-1 rounded border border-slate-200 px-1.5 py-0.5 text-xs">
<input type="checkbox" name="tooth_codes[]" value="{{ $code }}" class="rounded border-slate-300 text-indigo-600">
{{ $code }}
</label>
@endforeach
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Notes</label>
<input type="text" name="notes" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Notes</label>
<input type="text" name="notes" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
</div>
<button type="submit" class="btn-primary">Create lab case</button>
</form>
<button type="submit" class="btn-primary">Create lab case</button>
</form>
@endif
</section>
@@ -1,25 +1,48 @@
@php $canManage = $canManageSpecialty ?? true; @endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<h3 class="text-sm font-semibold text-slate-900">Dental visit notes</h3>
<p class="mt-0.5 text-xs text-slate-500">Short clinical notes for this visit not a GP SOAP form.</p>
<form method="POST" action="{{ route('care.specialty.dentistry.notes', $workspaceVisit) }}" class="mt-4 space-y-3">
@csrf
<div>
<label class="block text-xs font-medium text-slate-600">Chief complaint</label>
<textarea name="chief_complaint" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ old('chief_complaint', $dentalVisitNote?->chief_complaint) }}</textarea>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Soft tissue</label>
<textarea name="soft_tissue" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ old('soft_tissue', $dentalVisitNote?->soft_tissue) }}</textarea>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Occlusion / bite</label>
<textarea name="occlusion" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ old('occlusion', $dentalVisitNote?->occlusion) }}</textarea>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Other notes</label>
<textarea name="notes" rows="3" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ old('notes', $dentalVisitNote?->notes) }}</textarea>
</div>
<button type="submit" class="btn-primary">Save notes</button>
</form>
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.notes', $workspaceVisit) }}" class="mt-4 space-y-3">
@csrf
<div>
<label class="block text-xs font-medium text-slate-600">Chief complaint</label>
<textarea name="chief_complaint" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ old('chief_complaint', $dentalVisitNote?->chief_complaint) }}</textarea>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Soft tissue</label>
<textarea name="soft_tissue" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ old('soft_tissue', $dentalVisitNote?->soft_tissue) }}</textarea>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Occlusion / bite</label>
<textarea name="occlusion" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ old('occlusion', $dentalVisitNote?->occlusion) }}</textarea>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Other notes</label>
<textarea name="notes" rows="3" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ old('notes', $dentalVisitNote?->notes) }}</textarea>
</div>
<button type="submit" class="btn-primary">Save notes</button>
</form>
@else
<dl class="mt-4 grid gap-3 text-sm">
<div>
<dt class="text-slate-500">Chief complaint</dt>
<dd class="mt-0.5 font-medium text-slate-900 whitespace-pre-wrap">{{ $dentalVisitNote?->chief_complaint ?: '—' }}</dd>
</div>
<div>
<dt class="text-slate-500">Soft tissue</dt>
<dd class="mt-0.5 font-medium text-slate-900 whitespace-pre-wrap">{{ $dentalVisitNote?->soft_tissue ?: '—' }}</dd>
</div>
<div>
<dt class="text-slate-500">Occlusion / bite</dt>
<dd class="mt-0.5 font-medium text-slate-900 whitespace-pre-wrap">{{ $dentalVisitNote?->occlusion ?: '—' }}</dd>
</div>
<div>
<dt class="text-slate-500">Other notes</dt>
<dd class="mt-0.5 font-medium text-slate-900 whitespace-pre-wrap">{{ $dentalVisitNote?->notes ?: '—' }}</dd>
</div>
</dl>
@endif
</section>
@@ -3,24 +3,34 @@
$selected = old('tooth_code', request('tooth', ($dentalRows['upper'][0] ?? '16')));
$selectedData = $teethMap[$selected] ?? ['status' => 'present', 'surfaces' => [], 'conditions' => [], 'notes' => null];
$dentition = $dentalChart->dentition ?? 'adult';
$canManage = $canManageSpecialty ?? true;
@endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 class="text-sm font-semibold text-slate-900">Odontogram (FDI)</h3>
<p class="mt-0.5 text-xs text-slate-500">Click a tooth to chart status, surfaces, and conditions.</p>
<p class="mt-0.5 text-xs text-slate-500">
{{ $canManage ? 'Click a tooth to chart status, surfaces, and conditions.' : 'View tooth chart status, surfaces, and conditions.' }}
</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<form method="POST" action="{{ route('care.specialty.dentistry.dentition', $workspaceVisit) }}" class="flex items-center gap-2">
@csrf
<label class="text-xs font-medium text-slate-600">Dentition</label>
<select name="dentition" onchange="this.form.submit()" class="rounded-lg border border-slate-200 px-2 py-1.5 text-sm">
@foreach ($dentalDentitions ?? [] as $value => $label)
<option value="{{ $value }}" @selected($dentition === $value)>{{ $label }}</option>
@endforeach
</select>
</form>
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.dentition', $workspaceVisit) }}" class="flex items-center gap-2">
@csrf
<label class="text-xs font-medium text-slate-600">Dentition</label>
<select name="dentition" onchange="this.form.submit()" class="rounded-lg border border-slate-200 px-2 py-1.5 text-sm">
@foreach ($dentalDentitions ?? [] as $value => $label)
<option value="{{ $value }}" @selected($dentition === $value)>{{ $label }}</option>
@endforeach
</select>
</form>
@else
<p class="text-xs text-slate-600">
Dentition:
<span class="font-medium text-slate-900">{{ ($dentalDentitions ?? [])[$dentition] ?? $dentition }}</span>
</p>
@endif
<a href="{{ route('care.specialty.dentistry.print', $workspaceVisit) }}" target="_blank" class="text-sm font-medium text-indigo-600">Print</a>
</div>
</div>
@@ -60,51 +70,84 @@
@endforeach
</div>
<form method="POST" action="{{ route('care.specialty.dentistry.tooth', $workspaceVisit) }}" class="mt-6 grid gap-4 border-t border-slate-100 pt-4 sm:grid-cols-2">
@csrf
<input type="hidden" name="tooth_code" value="{{ $selected }}">
<div>
<label class="block text-xs font-medium text-slate-600">Selected tooth</label>
<p class="mt-1 text-lg font-semibold tabular-nums text-slate-900">{{ $selected }}</p>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Status</label>
<select name="status" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
@foreach ($dentalStatuses as $value => $label)
<option value="{{ $value }}" @selected(($selectedData['status'] ?? 'present') === $value)>{{ $label }}</option>
@endforeach
</select>
</div>
<div class="sm:col-span-2">
<p class="text-xs font-medium text-slate-600">Surfaces</p>
<div class="mt-2 flex flex-wrap gap-3">
@foreach ($dentalSurfaces as $surface)
<label class="inline-flex items-center gap-1.5 text-sm text-slate-700">
<input type="checkbox" name="surfaces[]" value="{{ $surface }}" class="rounded border-slate-300 text-indigo-600"
@checked(in_array($surface, $selectedData['surfaces'] ?? [], true))>
{{ $surface }}
</label>
@endforeach
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.tooth', $workspaceVisit) }}" class="mt-6 grid gap-4 border-t border-slate-100 pt-4 sm:grid-cols-2">
@csrf
<input type="hidden" name="tooth_code" value="{{ $selected }}">
<div>
<label class="block text-xs font-medium text-slate-600">Selected tooth</label>
<p class="mt-1 text-lg font-semibold tabular-nums text-slate-900">{{ $selected }}</p>
</div>
</div>
<div class="sm:col-span-2">
<p class="text-xs font-medium text-slate-600">Conditions</p>
<div class="mt-2 flex flex-wrap gap-3">
@foreach ($dentalConditions as $value => $label)
<label class="inline-flex items-center gap-1.5 text-sm text-slate-700">
<input type="checkbox" name="conditions[]" value="{{ $value }}" class="rounded border-slate-300 text-indigo-600"
@checked(in_array($value, $selectedData['conditions'] ?? [], true))>
{{ $label }}
</label>
@endforeach
<div>
<label class="block text-xs font-medium text-slate-600">Status</label>
<select name="status" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
@foreach ($dentalStatuses as $value => $label)
<option value="{{ $value }}" @selected(($selectedData['status'] ?? 'present') === $value)>{{ $label }}</option>
@endforeach
</select>
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Notes</label>
<textarea name="notes" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $selectedData['notes'] ?? '' }}</textarea>
</div>
<div class="sm:col-span-2">
<button type="submit" class="btn-primary">Save tooth</button>
</div>
</form>
<div class="sm:col-span-2">
<p class="text-xs font-medium text-slate-600">Surfaces</p>
<div class="mt-2 flex flex-wrap gap-3">
@foreach ($dentalSurfaces as $surface)
<label class="inline-flex items-center gap-1.5 text-sm text-slate-700">
<input type="checkbox" name="surfaces[]" value="{{ $surface }}" class="rounded border-slate-300 text-indigo-600"
@checked(in_array($surface, $selectedData['surfaces'] ?? [], true))>
{{ $surface }}
</label>
@endforeach
</div>
</div>
<div class="sm:col-span-2">
<p class="text-xs font-medium text-slate-600">Conditions</p>
<div class="mt-2 flex flex-wrap gap-3">
@foreach ($dentalConditions as $value => $label)
<label class="inline-flex items-center gap-1.5 text-sm text-slate-700">
<input type="checkbox" name="conditions[]" value="{{ $value }}" class="rounded border-slate-300 text-indigo-600"
@checked(in_array($value, $selectedData['conditions'] ?? [], true))>
{{ $label }}
</label>
@endforeach
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Notes</label>
<textarea name="notes" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $selectedData['notes'] ?? '' }}</textarea>
</div>
<div class="sm:col-span-2">
<button type="submit" class="btn-primary">Save tooth</button>
</div>
</form>
@else
<dl class="mt-6 grid gap-3 border-t border-slate-100 pt-4 text-sm sm:grid-cols-2">
<div>
<dt class="text-slate-500">Selected tooth</dt>
<dd class="mt-0.5 text-lg font-semibold tabular-nums text-slate-900">{{ $selected }}</dd>
</div>
<div>
<dt class="text-slate-500">Status</dt>
<dd class="mt-0.5 font-medium text-slate-900">{{ ($dentalStatuses ?? [])[$selectedData['status'] ?? 'present'] ?? ($selectedData['status'] ?? '—') }}</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-slate-500">Surfaces</dt>
<dd class="mt-0.5 font-medium text-slate-900">{{ ! empty($selectedData['surfaces']) ? implode(', ', $selectedData['surfaces']) : '—' }}</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-slate-500">Conditions</dt>
<dd class="mt-0.5 font-medium text-slate-900">
@php
$conditionLabels = collect($selectedData['conditions'] ?? [])
->map(fn ($c) => ($dentalConditions ?? [])[$c] ?? $c)
->filter()
->all();
@endphp
{{ $conditionLabels !== [] ? implode(', ', $conditionLabels) : '—' }}
</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-slate-500">Notes</dt>
<dd class="mt-0.5 font-medium text-slate-900 whitespace-pre-wrap">{{ $selectedData['notes'] ?: '—' }}</dd>
</div>
</dl>
@endif
</section>
@@ -2,6 +2,7 @@
$plan = $dentalPlan;
$openItems = $plan?->items?->whereIn('status', ['proposed', 'accepted']) ?? collect();
$currentStage = $workspaceVisit->specialty_stage;
$canManage = $canManageSpecialty ?? true;
$stageFlow = [
'waiting' => ['next' => 'chair', 'label' => 'Seat at chair'],
'chair' => ['next' => 'procedure', 'label' => 'Start procedure'],
@@ -26,20 +27,33 @@
</p>
<div class="mt-3 flex flex-wrap gap-2">
@foreach (['waiting', 'chair', 'procedure', 'recovery', 'completed'] as $stageCode)
<form method="POST" action="{{ route('care.specialty.dentistry.stage', $workspaceVisit) }}">
@csrf
<input type="hidden" name="stage" value="{{ $stageCode }}">
<button type="submit"
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.stage', $workspaceVisit) }}">
@csrf
<input type="hidden" name="stage" value="{{ $stageCode }}">
<button type="submit"
@class([
'rounded-lg px-2.5 py-1 text-xs font-semibold',
'bg-indigo-600 text-white' => $currentStage === $stageCode,
'border border-slate-200 bg-white text-slate-700 hover:bg-slate-50' => $currentStage !== $stageCode,
])>
{{ ucfirst($stageCode) }}
</button>
</form>
@else
<span
@class([
'rounded-lg px-2.5 py-1 text-xs font-semibold',
'bg-indigo-600 text-white' => $currentStage === $stageCode,
'border border-slate-200 bg-white text-slate-700 hover:bg-slate-50' => $currentStage !== $stageCode,
])>
'border border-slate-200 bg-white text-slate-400' => $currentStage !== $stageCode,
])
@if ($currentStage !== $stageCode) aria-disabled="true" title="View-only access" @endif
>
{{ ucfirst($stageCode) }}
</button>
</form>
</span>
@endif
@endforeach
@if ($currentStage && isset($stageFlow[$currentStage]))
@if ($canManage && $currentStage && isset($stageFlow[$currentStage]))
<form method="POST" action="{{ route('care.specialty.dentistry.stage', $workspaceVisit) }}">
@csrf
<input type="hidden" name="stage" value="{{ $stageFlow[$currentStage]['next'] }}">
@@ -6,6 +6,7 @@
}
$teeth = array_merge($dentalRows['upper'] ?? [], $dentalRows['lower'] ?? []);
$probeTeeth = array_slice($teeth, 0, 8);
$canManage = $canManageSpecialty ?? true;
@endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
@@ -16,9 +17,58 @@
<p class="mt-3 text-xs text-slate-500">Latest exam {{ $exam->exam_date?->format('d M Y') }} · {{ $exam->sites->count() }} sites</p>
@endif
<form method="POST" action="{{ route('care.specialty.dentistry.perio', $workspaceVisit) }}" class="mt-4 space-y-4">
@csrf
<div class="overflow-x-auto">
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.perio', $workspaceVisit) }}" class="mt-4 space-y-4">
@csrf
<div class="overflow-x-auto">
<table class="min-w-full text-left text-xs">
<thead>
<tr class="border-b border-slate-200 text-slate-500">
<th class="py-2 pr-2">Tooth</th>
@foreach ($dentalPerioSurfaces ?? [] as $surface)
<th class="px-1 py-2">{{ $surface }} mm</th>
<th class="px-1 py-2">B</th>
<th class="px-1 py-2">P</th>
@endforeach
</tr>
</thead>
<tbody>
@foreach ($probeTeeth as $idx => $tooth)
<tr class="border-b border-slate-100">
<td class="py-2 pr-2 font-semibold tabular-nums">{{ $tooth }}</td>
@foreach ($dentalPerioSurfaces ?? [] as $sIdx => $surface)
@php
$existing = $siteMap[$tooth][$surface] ?? null;
@endphp
<input type="hidden" name="sites[{{ $idx }}_{{ $sIdx }}][tooth_code]" value="{{ $tooth }}">
<input type="hidden" name="sites[{{ $idx }}_{{ $sIdx }}][surface]" value="{{ $surface }}">
<td class="px-1 py-1">
<input type="number" min="0" max="15" name="sites[{{ $idx }}_{{ $sIdx }}][probing_mm]"
value="{{ $existing?->probing_mm }}"
class="w-12 rounded border border-slate-200 px-1 py-1 tabular-nums">
</td>
<td class="px-1 py-1">
<input type="checkbox" name="sites[{{ $idx }}_{{ $sIdx }}][bleeding]" value="1" class="rounded border-slate-300"
@checked($existing?->bleeding)>
</td>
<td class="px-1 py-1">
<input type="checkbox" name="sites[{{ $idx }}_{{ $sIdx }}][plaque]" value="1" class="rounded border-slate-300"
@checked($existing?->plaque)>
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Notes</label>
<textarea name="notes" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $exam?->notes }}</textarea>
</div>
<button type="submit" class="btn-primary">Save perio exam</button>
</form>
@else
<div class="mt-4 overflow-x-auto">
<table class="min-w-full text-left text-xs">
<thead>
<tr class="border-b border-slate-200 text-slate-500">
@@ -31,39 +81,26 @@
</tr>
</thead>
<tbody>
@foreach ($probeTeeth as $idx => $tooth)
@forelse ($probeTeeth as $tooth)
<tr class="border-b border-slate-100">
<td class="py-2 pr-2 font-semibold tabular-nums">{{ $tooth }}</td>
@foreach ($dentalPerioSurfaces ?? [] as $sIdx => $surface)
@php
$existing = $siteMap[$tooth][$surface] ?? null;
$base = "sites.{$idx}_{$sIdx}";
@endphp
<input type="hidden" name="sites[{{ $idx }}_{{ $sIdx }}][tooth_code]" value="{{ $tooth }}">
<input type="hidden" name="sites[{{ $idx }}_{{ $sIdx }}][surface]" value="{{ $surface }}">
<td class="px-1 py-1">
<input type="number" min="0" max="15" name="sites[{{ $idx }}_{{ $sIdx }}][probing_mm]"
value="{{ $existing?->probing_mm }}"
class="w-12 rounded border border-slate-200 px-1 py-1 tabular-nums">
</td>
<td class="px-1 py-1">
<input type="checkbox" name="sites[{{ $idx }}_{{ $sIdx }}][bleeding]" value="1" class="rounded border-slate-300"
@checked($existing?->bleeding)>
</td>
<td class="px-1 py-1">
<input type="checkbox" name="sites[{{ $idx }}_{{ $sIdx }}][plaque]" value="1" class="rounded border-slate-300"
@checked($existing?->plaque)>
</td>
@foreach ($dentalPerioSurfaces ?? [] as $surface)
@php $existing = $siteMap[$tooth][$surface] ?? null; @endphp
<td class="px-1 py-1 tabular-nums">{{ $existing?->probing_mm ?? '—' }}</td>
<td class="px-1 py-1">{{ $existing?->bleeding ? '✓' : '—' }}</td>
<td class="px-1 py-1">{{ $existing?->plaque ? '✓' : '—' }}</td>
@endforeach
</tr>
@endforeach
@empty
<tr>
<td colspan="{{ 1 + count($dentalPerioSurfaces ?? []) * 3 }}" class="py-3 text-slate-500">No perio exam yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Notes</label>
<textarea name="notes" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $exam?->notes }}</textarea>
</div>
<button type="submit" class="btn-primary">Save perio exam</button>
</form>
@if ($exam?->notes)
<p class="mt-3 text-sm text-slate-700 whitespace-pre-wrap">{{ $exam->notes }}</p>
@endif
@endif
</section>
@@ -1,6 +1,7 @@
@php
$plan = $dentalPlan;
$history = ($dentalPlanHistory ?? collect())->filter(fn ($p) => ! $plan || $p->id !== $plan->id);
$canManage = $canManageSpecialty ?? true;
@endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
@@ -16,7 +17,7 @@
@endif
</p>
</div>
@if ($plan)
@if ($canManage && $plan)
<div class="flex flex-wrap gap-2">
@if ($plan->status === 'draft')
<form method="POST" action="{{ route('care.specialty.dentistry.plan.accept', $workspaceVisit) }}">
@@ -53,14 +54,14 @@
</p>
<p class="text-xs text-slate-500">Priority {{ $item->priority }} · {{ ucfirst($item->status) }} · {{ number_format($item->estimate_minor / 100, 2) }}</p>
</div>
@if (! in_array($item->status, ['done', 'cancelled'], true))
@if ($canManage && ! in_array($item->status, ['done', 'cancelled'], true))
<form method="POST" action="{{ route('care.specialty.dentistry.plan-items.cancel', [$workspaceVisit, $item]) }}" onsubmit="return confirm('Cancel this item?')">
@csrf
<button type="submit" class="text-xs font-medium text-rose-600">Cancel item</button>
</form>
@endif
</div>
@if (! in_array($item->status, ['done', 'cancelled'], true))
@if ($canManage && ! in_array($item->status, ['done', 'cancelled'], true))
<form method="POST" action="{{ route('care.specialty.dentistry.plan-items.update', [$workspaceVisit, $item]) }}" class="mt-3 grid gap-2 border-t border-slate-200/70 pt-3 sm:grid-cols-4">
@csrf
<select name="procedure_code" class="rounded-lg border border-slate-200 px-2 py-1.5 text-xs">
@@ -84,7 +85,7 @@
@endforelse
</ul>
@if (! $plan || ! in_array($plan->status, ['completed', 'rejected', 'cancelled'], true))
@if ($canManage && (! $plan || ! in_array($plan->status, ['completed', 'rejected', 'cancelled'], true)))
<form method="POST" action="{{ route('care.specialty.dentistry.plan-items', $workspaceVisit) }}" class="mt-6 space-y-3 border-t border-slate-100 pt-4">
@csrf
<h4 class="text-sm font-semibold text-slate-900">Add item</h4>
@@ -1,3 +1,5 @@
@php $canManage = $canManageSpecialty ?? true; @endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<h3 class="text-sm font-semibold text-slate-900">Recalls</h3>
<p class="mt-0.5 text-xs text-slate-500">Schedule follow-ups and mark them complete when the patient returns.</p>
@@ -11,7 +13,7 @@
Due {{ $recall->due_on?->format('d M Y') }} · {{ ucfirst($recall->status) }}
</p>
</div>
@if (in_array($recall->status, ['scheduled', 'due'], true))
@if ($canManage && in_array($recall->status, ['scheduled', 'due'], true))
<div class="flex gap-2">
<form method="POST" action="{{ route('care.specialty.dentistry.recalls.complete', [$workspaceVisit, $recall]) }}">
@csrf
@@ -29,24 +31,26 @@
@endforelse
</ul>
<form method="POST" action="{{ route('care.specialty.dentistry.recalls', $workspaceVisit) }}" class="mt-6 space-y-3 border-t border-slate-100 pt-4">
@csrf
<h4 class="text-sm font-semibold text-slate-900">Schedule recall</h4>
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-xs font-medium text-slate-600">Due date</label>
<input type="date" name="due_on" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm"
value="{{ now()->addMonths(6)->toDateString() }}">
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.recalls', $workspaceVisit) }}" class="mt-6 space-y-3 border-t border-slate-100 pt-4">
@csrf
<h4 class="text-sm font-semibold text-slate-900">Schedule recall</h4>
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-xs font-medium text-slate-600">Due date</label>
<input type="date" name="due_on" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm"
value="{{ now()->addMonths(6)->toDateString() }}">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Reason</label>
<input type="text" name="reason" placeholder="e.g. 6-month check" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Notes</label>
<input type="text" name="notes" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Reason</label>
<input type="text" name="reason" placeholder="e.g. 6-month check" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Notes</label>
<input type="text" name="notes" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
</div>
<button type="submit" class="btn-primary">Schedule recall</button>
</form>
<button type="submit" class="btn-primary">Schedule recall</button>
</form>
@endif
</section>
@@ -1,8 +1,12 @@
@php $canManage = $canManageSpecialty ?? true; @endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<h3 class="text-sm font-semibold text-slate-900">Treat today</h3>
<p class="mt-0.5 text-xs text-slate-500">Record a completed procedure. It posts to the visit bill automatically.</p>
<p class="mt-0.5 text-xs text-slate-500">
{{ $canManage ? 'Record a completed procedure. It posts to the visit bill automatically.' : 'Completed procedures for this visit.' }}
</p>
@if ($dentalPlan?->items)
@if ($canManage && $dentalPlan?->items)
<div class="mt-4">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">From plan</p>
<ul class="mt-2 space-y-2">
@@ -27,59 +31,73 @@
@endforeach
</ul>
</div>
@elseif (! $canManage && $dentalPlan?->items?->whereIn('status', ['proposed', 'accepted'])->isNotEmpty())
<div class="mt-4">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Open plan items</p>
<ul class="mt-2 space-y-2 text-sm">
@foreach ($dentalPlan->items->whereIn('status', ['proposed', 'accepted']) as $item)
<li class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-2">
<p class="font-medium text-slate-800">{{ $item->label }} @if($item->tooth_code)<span class="text-slate-500">· {{ $item->tooth_code }}</span>@endif</p>
<p class="text-xs text-slate-500">{{ number_format($item->estimate_minor / 100, 2) }}</p>
</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('care.specialty.dentistry.procedures', $workspaceVisit) }}" class="mt-6 space-y-3 border-t border-slate-100 pt-4">
@csrf
<h4 class="text-sm font-semibold text-slate-900">Ad-hoc procedure</h4>
<input type="hidden" name="bill" value="1">
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-xs font-medium text-slate-600">Procedure</label>
<select name="procedure_code" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value="">Select…</option>
@foreach ($dentalCatalog as $service)
<option value="{{ $service['code'] }}">{{ $service['label'] }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Tooth</label>
<select name="tooth_code" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value=""></option>
@foreach (array_merge($dentalRows['upper'] ?? [], $dentalRows['lower'] ?? []) as $code)
<option value="{{ $code }}">{{ $code }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Anesthesia</label>
<select name="anesthesia" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value=""></option>
<option value="None">None</option>
<option value="Local">Local</option>
<option value="Sedation">Sedation</option>
<option value="GA">GA</option>
</select>
</div>
<div class="sm:col-span-2">
<p class="text-xs font-medium text-slate-600">Surfaces</p>
<div class="mt-2 flex flex-wrap gap-3">
@foreach ($dentalSurfaces as $surface)
<label class="inline-flex items-center gap-1.5 text-sm">
<input type="checkbox" name="surfaces[]" value="{{ $surface }}" class="rounded border-slate-300 text-indigo-600">
{{ $surface }}
</label>
@endforeach
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.procedures', $workspaceVisit) }}" class="mt-6 space-y-3 border-t border-slate-100 pt-4">
@csrf
<h4 class="text-sm font-semibold text-slate-900">Ad-hoc procedure</h4>
<input type="hidden" name="bill" value="1">
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-xs font-medium text-slate-600">Procedure</label>
<select name="procedure_code" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value="">Select…</option>
@foreach ($dentalCatalog as $service)
<option value="{{ $service['code'] }}">{{ $service['label'] }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Tooth</label>
<select name="tooth_code" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value=""></option>
@foreach (array_merge($dentalRows['upper'] ?? [], $dentalRows['lower'] ?? []) as $code)
<option value="{{ $code }}">{{ $code }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Anesthesia</label>
<select name="anesthesia" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value=""></option>
<option value="None">None</option>
<option value="Local">Local</option>
<option value="Sedation">Sedation</option>
<option value="GA">GA</option>
</select>
</div>
<div class="sm:col-span-2">
<p class="text-xs font-medium text-slate-600">Surfaces</p>
<div class="mt-2 flex flex-wrap gap-3">
@foreach ($dentalSurfaces as $surface)
<label class="inline-flex items-center gap-1.5 text-sm">
<input type="checkbox" name="surfaces[]" value="{{ $surface }}" class="rounded border-slate-300 text-indigo-600">
{{ $surface }}
</label>
@endforeach
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Notes</label>
<input type="text" name="notes" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Notes</label>
<input type="text" name="notes" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
</div>
<button type="submit" class="btn-primary">Complete &amp; bill</button>
</form>
<button type="submit" class="btn-primary">Complete &amp; bill</button>
</form>
@endif
<div class="mt-6 border-t border-slate-100 pt-4">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Completed this visit</p>
@@ -1,6 +1,7 @@
@php
$record = $emergencyDisposition;
$payload = old('payload', $record?->payload ?? []);
$canManage = $canManageSpecialty ?? true;
@endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
@@ -12,57 +13,94 @@
<a href="{{ route('care.specialty.emergency.print', $workspaceVisit) }}" target="_blank" class="text-sm font-medium text-indigo-600">Print summary</a>
</div>
<form method="POST" action="{{ route('care.specialty.emergency.disposition', $workspaceVisit) }}" class="mt-4 space-y-3">
@csrf
<input type="hidden" name="tab" value="disposition">
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.emergency.disposition', $workspaceVisit) }}" class="mt-4 space-y-3">
@csrf
<input type="hidden" name="tab" value="disposition">
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-xs font-medium text-slate-600">Final disposition</label>
<select name="payload[disposition]" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value="">Select…</option>
@foreach (['Discharge home', 'Admit to ward', 'Admit to ICU', 'Transfer to facility', 'Left against advice', 'Died', 'Absconded'] as $opt)
<option value="{{ $opt }}" @selected(($payload['disposition'] ?? '') === $opt)>{{ $opt }}</option>
@endforeach
</select>
@error('payload.disposition')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
<div class="grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-xs font-medium text-slate-600">Final disposition</label>
<select name="payload[disposition]" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value="">Select…</option>
@foreach (['Discharge home', 'Admit to ward', 'Admit to ICU', 'Transfer to facility', 'Left against advice', 'Died', 'Absconded'] as $opt)
<option value="{{ $opt }}" @selected(($payload['disposition'] ?? '') === $opt)>{{ $opt }}</option>
@endforeach
</select>
@error('payload.disposition')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Condition</label>
<select name="payload[condition]" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value="">Select…</option>
@foreach (['Stable', 'Improving', 'Unstable', 'Critical', 'Deceased'] as $opt)
<option value="{{ $opt }}" @selected(($payload['condition'] ?? '') === $opt)>{{ $opt }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Destination</label>
<input type="text" name="payload[destination]" value="{{ $payload['destination'] ?? '' }}" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Disposition time</label>
<input type="text" name="payload[time]" value="{{ $payload['time'] ?? '' }}" placeholder="e.g. 14:30" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Diagnosis</label>
<input type="text" name="payload[diagnosis]" value="{{ $payload['diagnosis'] ?? '' }}" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
@error('payload.diagnosis')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Advice / follow-up</label>
<textarea name="payload[advice]" rows="3" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $payload['advice'] ?? '' }}</textarea>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Medications on discharge</label>
<textarea name="payload[medications]" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $payload['medications'] ?? '' }}</textarea>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Notes</label>
<textarea name="payload[notes]" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $payload['notes'] ?? '' }}</textarea>
</div>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Condition</label>
<select name="payload[condition]" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value="">Select…</option>
@foreach (['Stable', 'Improving', 'Unstable', 'Critical', 'Deceased'] as $opt)
<option value="{{ $opt }}" @selected(($payload['condition'] ?? '') === $opt)>{{ $opt }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Destination</label>
<input type="text" name="payload[destination]" value="{{ $payload['destination'] ?? '' }}" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Disposition time</label>
<input type="text" name="payload[time]" value="{{ $payload['time'] ?? '' }}" placeholder="e.g. 14:30" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Diagnosis</label>
<input type="text" name="payload[diagnosis]" value="{{ $payload['diagnosis'] ?? '' }}" required class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
@error('payload.diagnosis')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Advice / follow-up</label>
<textarea name="payload[advice]" rows="3" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $payload['advice'] ?? '' }}</textarea>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Medications on discharge</label>
<textarea name="payload[medications]" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $payload['medications'] ?? '' }}</textarea>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-slate-600">Notes</label>
<textarea name="payload[notes]" rows="2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $payload['notes'] ?? '' }}</textarea>
</div>
</div>
<button type="submit" class="btn-primary">Save disposition &amp; complete visit</button>
</form>
<button type="submit" class="btn-primary">Save disposition &amp; complete visit</button>
</form>
@else
<dl class="mt-4 grid gap-3 text-sm sm:grid-cols-2">
<div>
<dt class="text-slate-500">Final disposition</dt>
<dd class="mt-0.5 font-medium text-slate-900">{{ $payload['disposition'] ?? '—' }}</dd>
</div>
<div>
<dt class="text-slate-500">Condition</dt>
<dd class="mt-0.5 font-medium text-slate-900">{{ $payload['condition'] ?? '—' }}</dd>
</div>
<div>
<dt class="text-slate-500">Destination</dt>
<dd class="mt-0.5 font-medium text-slate-900">{{ $payload['destination'] ?? '—' }}</dd>
</div>
<div>
<dt class="text-slate-500">Disposition time</dt>
<dd class="mt-0.5 font-medium text-slate-900">{{ $payload['time'] ?? '—' }}</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-slate-500">Diagnosis</dt>
<dd class="mt-0.5 font-medium text-slate-900">{{ $payload['diagnosis'] ?? '—' }}</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-slate-500">Advice / follow-up</dt>
<dd class="mt-0.5 font-medium text-slate-900 whitespace-pre-wrap">{{ $payload['advice'] ?? '—' }}</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-slate-500">Medications on discharge</dt>
<dd class="mt-0.5 font-medium text-slate-900 whitespace-pre-wrap">{{ $payload['medications'] ?? '—' }}</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-slate-500">Notes</dt>
<dd class="mt-0.5 font-medium text-slate-900 whitespace-pre-wrap">{{ $payload['notes'] ?? '—' }}</dd>
</div>
</dl>
@endif
</section>
@@ -4,6 +4,7 @@
$currentStage = $workspaceVisit->specialty_stage;
$stageFlow = $emergencyStageFlow ?? [];
$latest = $emergencyLatestVitals;
$canManage = $canManageSpecialty ?? true;
@endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
@@ -55,20 +56,33 @@
</p>
<div class="mt-3 flex flex-wrap gap-2">
@foreach (($emergencyStageCodes ?: ['arrival', 'resus', 'treatment', 'observation', 'disposition']) as $stageCode)
<form method="POST" action="{{ route('care.specialty.emergency.stage', $workspaceVisit) }}">
@csrf
<input type="hidden" name="stage" value="{{ $stageCode }}">
<button type="submit"
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.emergency.stage', $workspaceVisit) }}">
@csrf
<input type="hidden" name="stage" value="{{ $stageCode }}">
<button type="submit"
@class([
'rounded-lg px-2.5 py-1 text-xs font-semibold',
'bg-indigo-600 text-white' => $currentStage === $stageCode,
'border border-slate-200 bg-white text-slate-700 hover:bg-slate-50' => $currentStage !== $stageCode,
])>
{{ ucfirst(str_replace('_', ' ', $stageCode)) }}
</button>
</form>
@else
<span
@class([
'rounded-lg px-2.5 py-1 text-xs font-semibold',
'bg-indigo-600 text-white' => $currentStage === $stageCode,
'border border-slate-200 bg-white text-slate-700 hover:bg-slate-50' => $currentStage !== $stageCode,
])>
'border border-slate-200 bg-white text-slate-400' => $currentStage !== $stageCode,
])
@if ($currentStage !== $stageCode) aria-disabled="true" title="View-only access" @endif
>
{{ ucfirst(str_replace('_', ' ', $stageCode)) }}
</button>
</form>
</span>
@endif
@endforeach
@if ($currentStage && isset($stageFlow[$currentStage]) && $stageFlow[$currentStage]['next'] !== $currentStage)
@if ($canManage && $currentStage && isset($stageFlow[$currentStage]) && $stageFlow[$currentStage]['next'] !== $currentStage)
<form method="POST" action="{{ route('care.specialty.emergency.stage', $workspaceVisit) }}">
@csrf
<input type="hidden" name="stage" value="{{ $stageFlow[$currentStage]['next'] }}">
@@ -1,3 +1,5 @@
@php $canManage = $canManageSpecialty ?? true; @endphp
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
@@ -6,46 +8,48 @@
</div>
</div>
<form method="POST" action="{{ route('care.specialty.emergency.vitals', $workspaceVisit) }}" class="mt-4 grid gap-3 sm:grid-cols-4">
@csrf
<div>
<label class="block text-xs font-medium text-slate-600">BP systolic</label>
<input type="number" name="bp_systolic" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">BP diastolic</label>
<input type="number" name="bp_diastolic" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Pulse</label>
<input type="number" name="pulse" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">SpO₂ %</label>
<input type="number" name="spo2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Temperature °C</label>
<input type="number" step="0.1" name="temperature" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Respiratory rate</label>
<input type="number" name="respiratory_rate" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Weight kg</label>
<input type="number" step="0.1" name="weight_kg" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Height cm</label>
<input type="number" step="0.1" name="height_cm" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div class="sm:col-span-4">
<button type="submit" class="btn-primary">Save vitals</button>
</div>
</form>
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.emergency.vitals', $workspaceVisit) }}" class="mt-4 grid gap-3 sm:grid-cols-4">
@csrf
<div>
<label class="block text-xs font-medium text-slate-600">BP systolic</label>
<input type="number" name="bp_systolic" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">BP diastolic</label>
<input type="number" name="bp_diastolic" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Pulse</label>
<input type="number" name="pulse" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">SpO₂ %</label>
<input type="number" name="spo2" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Temperature °C</label>
<input type="number" step="0.1" name="temperature" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Respiratory rate</label>
<input type="number" name="respiratory_rate" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Weight kg</label>
<input type="number" step="0.1" name="weight_kg" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Height cm</label>
<input type="number" step="0.1" name="height_cm" class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
</div>
<div class="sm:col-span-4">
<button type="submit" class="btn-primary">Save vitals</button>
</div>
</form>
@endif
<div class="mt-6 border-t border-slate-100 pt-4">
<div @class(['mt-6 border-t border-slate-100 pt-4' => $canManage, 'mt-4' => ! $canManage])>
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">History</p>
<ul class="mt-2 divide-y divide-slate-100 text-sm">
@forelse ($emergencyVitals as $row)
@@ -40,7 +40,7 @@
$currentStage = $workspaceVisit?->specialty_stage;
@endphp
@if ($onWorkspace)
@if ($onWorkspace && ($canManageSpecialty ?? true))
@include('care.partials.queue-ops', [
'queueIntegration' => $queueIntegration ?? null,
'queueCallNextRoute' => 'care.specialty.call-next',
@@ -50,7 +50,7 @@
])
@endif
@if ($ticketCalled && $appointment)
@if (($canManageSpecialty ?? true) && $ticketCalled && $appointment)
<form method="POST" action="{{ route('care.queue.recall', $appointment) }}" class="w-full" @click.stop>
@csrf
<button type="submit" class="{{ $btnAccent }}">Call again</button>
@@ -42,22 +42,26 @@
@include('care.specialty.emergency.workspace-'.$activeTab)
@elseif ($activeTab === 'billing')
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<h3 class="text-sm font-semibold text-slate-900">Add specialty service</h3>
<form method="POST" action="{{ route('care.specialty.services.add', ['module' => $moduleKey, 'visit' => $workspaceVisit]) }}" class="mt-3 space-y-3">
@csrf
<select name="service_code" required class="w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value="">Select service…</option>
@foreach ($services as $service)
<option value="{{ $service['code'] }}">
{{ $service['label'] }} {{ $currency ?? 'GHS' }} {{ number_format(((int) $service['amount_minor']) / 100, 2) }}
</option>
@endforeach
</select>
<button type="submit" class="btn-primary">{{ $actions['invoice'] ?? 'Add to invoice' }}</button>
</form>
<h3 class="text-sm font-semibold text-slate-900">
{{ ($canManageSpecialty ?? true) ? 'Add specialty service' : 'Visit billing' }}
</h3>
@if ($canManageSpecialty ?? true)
<form method="POST" action="{{ route('care.specialty.services.add', ['module' => $moduleKey, 'visit' => $workspaceVisit]) }}" class="mt-3 space-y-3">
@csrf
<select name="service_code" required class="w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
<option value="">Select service…</option>
@foreach ($services as $service)
<option value="{{ $service['code'] }}">
{{ $service['label'] }} {{ $currency ?? 'GHS' }} {{ number_format(((int) $service['amount_minor']) / 100, 2) }}
</option>
@endforeach
</select>
<button type="submit" class="btn-primary">{{ $actions['invoice'] ?? 'Add to invoice' }}</button>
</form>
@endif
@if ($workspaceVisit->bill)
<div class="mt-6 border-t border-slate-100 pt-4">
<div @class(['mt-6 border-t border-slate-100 pt-4' => ($canManageSpecialty ?? true), 'mt-3' => ! ($canManageSpecialty ?? true)])>
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Current invoice {{ $workspaceVisit->bill->invoice_number }}</p>
<ul class="mt-2 space-y-1 text-sm">
@forelse ($workspaceVisit->bill->lineItems as $line)
@@ -71,6 +75,8 @@
</ul>
<a href="{{ route('care.bills.show', $workspaceVisit->bill) }}" class="mt-3 inline-flex text-sm font-medium text-indigo-600">Open bill</a>
</div>
@elseif (! ($canManageSpecialty ?? true))
<p class="mt-3 text-sm text-slate-500">No invoice on this visit yet.</p>
@endif
</section>
@elseif (! empty($clinicalFields))
@@ -82,11 +88,13 @@
<h3 class="text-sm font-semibold text-slate-900">Documents</h3>
<p class="mt-1 text-xs text-slate-500">Specialty documents attach to the patient chart for this episode.</p>
<form method="POST" action="{{ route('care.specialty.documents.upload', ['module' => $moduleKey, 'visit' => $workspaceVisit]) }}" enctype="multipart/form-data" class="mt-4 space-y-3">
@csrf
<input type="file" name="attachment" required accept=".pdf,.jpg,.jpeg,.png,.webp" class="block w-full text-sm">
<button type="submit" class="btn-primary">Upload document</button>
</form>
@if ($canManageSpecialty ?? true)
<form method="POST" action="{{ route('care.specialty.documents.upload', ['module' => $moduleKey, 'visit' => $workspaceVisit]) }}" enctype="multipart/form-data" class="mt-4 space-y-3">
@csrf
<input type="file" name="attachment" required accept=".pdf,.jpg,.jpeg,.png,.webp" class="block w-full text-sm">
<button type="submit" class="btn-primary">Upload document</button>
</form>
@endif
<ul class="mt-4 space-y-2 text-sm">
@forelse ($visitDocuments ?? [] as $doc)
@@ -116,7 +116,9 @@
<div class="flex flex-wrap gap-3 text-sm">
<a href="{{ route('care.specialty.history', $moduleKey) }}" class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-50">History</a>
<a href="{{ route('care.specialty.billing', $moduleKey) }}" class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-50">Billing</a>
@if ($canManageSpecialty ?? true)
<a href="{{ route('care.specialty.billing', $moduleKey) }}" class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-50">Billing</a>
@endif
</div>
@include('care.partials.queue-board', [
@@ -128,7 +130,7 @@
'canConsult' => $canConsult ?? false,
'lockToPractitioner' => $lockToPractitioner ?? false,
'showFilters' => true,
'queueCallNextRoute' => 'care.specialty.call-next',
'queueCallNextRoute' => ($canManageSpecialty ?? true) ? 'care.specialty.call-next' : null,
'queueCallNextParams' => ['module' => $moduleKey],
'startRouteName' => 'care.queue.start',
'inCareLabel' => 'In care',
+87 -2
View File
@@ -235,9 +235,18 @@ class CareSpecialtyAccessTest extends TestCase
]);
$this->actingAs($doctor)
->get(route('care.specialty.workspace', ['module' => 'cardiology', 'visit' => $visit]))
->get(route('care.specialty.workspace', ['module' => 'cardiology', 'visit' => $visit, 'tab' => 'exam']))
->assertOk()
->assertSee('view + refer');
->assertSee('view + refer')
->assertDontSee('Save record')
->assertDontSee('Call next')
->assertDontSee('Add to invoice')
->assertDontSee('Upload document')
->assertSee('Refer to specialty queue')
->assertDontSee(
'action="'.route('care.specialty.consultation.start', ['module' => 'cardiology', 'visit' => $visit], false).'"',
false,
);
$this->actingAs($doctor)
->post(route('care.specialty.clinical.save', ['module' => 'cardiology', 'visit' => $visit]), [
@@ -249,6 +258,82 @@ class CareSpecialtyAccessTest extends TestCase
->assertForbidden();
}
public function test_sidebar_and_dashboard_exclude_modules_with_no_access(): void
{
[$pharmacistUser, $pharmacistMember] = $this->makeStaff('pharmacist', 'rx-nav');
$enabledForMember = collect($this->modules->enabledModulesForMember($this->organization, $pharmacistMember))
->pluck('key')
->all();
$this->assertContains('emergency', $enabledForMember);
$this->assertContains('infusion', $enabledForMember);
$this->assertNotContains('cardiology', $enabledForMember);
$this->assertNotContains('dentistry', $enabledForMember);
$this->actingAs($pharmacistUser)
->get(route('care.dashboard'))
->assertOk()
->assertSee('Emergency')
->assertDontSee('Cardiology');
}
public function test_manage_role_still_sees_mutate_actions_on_restricted_module(): void
{
[$doctor, $member] = $this->makeStaff('doctor', 'cardio-ui');
$dept = Department::query()->where('type', 'cardiology')->firstOrFail();
Practitioner::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'department_id' => $dept->id,
'member_id' => $member->id,
'user_ref' => $doctor->public_id,
'name' => 'Dr Cardio UI',
'specialty' => 'Cardiology',
'is_active' => true,
]);
$patient = Patient::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_number' => 'P-CARD-UI',
'first_name' => 'Abena',
'last_name' => 'Heart',
'gender' => 'female',
'date_of_birth' => '1985-01-01',
]);
$visit = \App\Models\Visit::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $patient->id,
'status' => \App\Models\Visit::STATUS_IN_PROGRESS,
'checked_in_at' => now(),
]);
Appointment::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $patient->id,
'department_id' => $dept->id,
'visit_id' => $visit->id,
'type' => Appointment::TYPE_WALK_IN,
'status' => Appointment::STATUS_WAITING,
'scheduled_at' => now(),
'checked_in_at' => now(),
]);
$this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'cardiology'));
$this->actingAs($doctor)
->get(route('care.specialty.workspace', ['module' => 'cardiology', 'visit' => $visit]))
->assertOk()
->assertDontSee('view + refer')
->assertSee('Start');
}
public function test_admin_keeps_full_manage_access(): void
{
$admin = Member::query()->where('user_ref', $this->owner->public_id)->firstOrFail();