Wire Care lists into Ladill Queue workflows instead of reception panels.
Deploy Ladill Care / deploy (push) Successful in 1m45s
Deploy Ladill Care / deploy (push) Successful in 1m45s
When Queue integration is on, role pages issue tickets into their own department queues and drive Call next → Serve → Complete on the native lists. Integration off leaves existing UI unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,7 +11,6 @@ use App\Models\Patient;
|
||||
use App\Models\Practitioner;
|
||||
use App\Services\Care\AppointmentService;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\ServiceQueuePresenter;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -22,7 +21,6 @@ class AppointmentController extends Controller
|
||||
|
||||
public function __construct(
|
||||
protected AppointmentService $appointments,
|
||||
protected ServiceQueuePresenter $serviceQueues,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
@@ -61,13 +59,6 @@ class AppointmentController extends Controller
|
||||
'practitioners' => $practitioners,
|
||||
'statuses' => config('care.appointment_statuses'),
|
||||
'heroStats' => $heroStats,
|
||||
'serviceQueuePanel' => $this->serviceQueues->panel(
|
||||
$request,
|
||||
$organization,
|
||||
$member,
|
||||
'clinical',
|
||||
$request->input('counter_uuid'),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Bill;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Care\BillService;
|
||||
use App\Services\Care\CareQueueBridge;
|
||||
use App\Services\Care\CareQueueContexts;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Payments\MerchantGatewayService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -21,13 +23,15 @@ class BillController extends Controller
|
||||
public function __construct(
|
||||
protected BillService $bills,
|
||||
protected MerchantGatewayService $gateway,
|
||||
protected CareQueueBridge $queueBridge,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.view');
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
$member = $this->member($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($member);
|
||||
|
||||
$bills = $this->bills->list(
|
||||
$this->ownerRef($request),
|
||||
@@ -36,13 +40,59 @@ class BillController extends Controller
|
||||
$branchScope,
|
||||
);
|
||||
|
||||
$branchId = (int) ($branchScope ?: \App\Models\Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->value('id'));
|
||||
|
||||
return view('care.bills.index', [
|
||||
'organization' => $organization,
|
||||
'bills' => $bills,
|
||||
'statuses' => config('care.bill_statuses'),
|
||||
'branchId' => $branchId,
|
||||
'queueIntegration' => $branchId
|
||||
? $this->queueBridge->listMeta($organization, CareQueueContexts::BILLING, $branchId)
|
||||
: ['enabled' => false],
|
||||
'canManageQueue' => app(\App\Services\Care\CarePermissions::class)
|
||||
->can($member, 'service_queues.console'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function callNext(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.manage');
|
||||
$organization = $this->organization($request);
|
||||
$member = $this->member($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($member);
|
||||
$branchId = (int) ($request->input('branch_id') ?: $branchScope);
|
||||
abort_unless($branchId > 0, 422);
|
||||
abort_unless($this->queueBridge->isEnabled($organization), 404);
|
||||
|
||||
$result = $this->queueBridge->callNext($organization, CareQueueContexts::BILLING, $branchId);
|
||||
$ticket = $result['ticket'] ?? null;
|
||||
if (! $ticket) {
|
||||
return back()->with('error', 'No bills waiting in the billing queue.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Called '.$ticket['ticket_number'].'.');
|
||||
}
|
||||
|
||||
public function serve(Request $request, Bill $bill): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.manage');
|
||||
$this->authorizeBill($request, $bill);
|
||||
$organization = $this->organization($request);
|
||||
abort_unless($this->queueBridge->isEnabled($organization), 404);
|
||||
|
||||
$ticket = $this->queueBridge->serve($organization, $bill);
|
||||
if (! $ticket) {
|
||||
return back()->with('error', 'Could not serve billing ticket.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Now serving '.$ticket['ticket_number'].'.');
|
||||
}
|
||||
|
||||
public function generate(Request $request, Visit $visit): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'bills.manage');
|
||||
|
||||
@@ -12,7 +12,6 @@ use App\Services\Care\AppointmentService;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\ReportService;
|
||||
use App\Services\Care\ServiceQueuePresenter;
|
||||
use App\Services\Care\SpecialtyModuleService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -26,7 +25,6 @@ class DashboardController extends Controller
|
||||
protected ReportService $reports,
|
||||
protected CarePermissions $permissions,
|
||||
protected AppointmentService $appointments,
|
||||
protected ServiceQueuePresenter $serviceQueues,
|
||||
protected SpecialtyModuleService $specialtyModules,
|
||||
) {}
|
||||
|
||||
@@ -80,12 +78,6 @@ class DashboardController extends Controller
|
||||
? $this->patientQueueForMember($request, $organization->id, $owner, $member, $branchScope)
|
||||
: [collect(), collect(), null];
|
||||
|
||||
$serviceQueueContext = match ($member?->role) {
|
||||
'pharmacist' => 'pharmacy',
|
||||
'lab_technician' => 'laboratory',
|
||||
default => 'clinical',
|
||||
};
|
||||
|
||||
return view('care.dashboard', [
|
||||
'organization' => $organization,
|
||||
'member' => $member,
|
||||
@@ -103,13 +95,6 @@ class DashboardController extends Controller
|
||||
'patientQueue' => $queue,
|
||||
'inConsultation' => $inConsultation,
|
||||
'practitionerId' => $practitionerId,
|
||||
'serviceQueuePanel' => $this->serviceQueues->panel(
|
||||
$request,
|
||||
$organization,
|
||||
$member,
|
||||
$serviceQueueContext,
|
||||
$request->input('counter_uuid'),
|
||||
),
|
||||
'specialtyModules' => $this->specialtyModules->enabledModules($organization),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ use App\Models\Consultation;
|
||||
use App\Models\InvestigationRequest;
|
||||
use App\Models\InvestigationType;
|
||||
use App\Models\Member;
|
||||
use App\Services\Care\CareQueueBridge;
|
||||
use App\Services\Care\CareQueueContexts;
|
||||
use App\Services\Care\InvestigationService;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\ServiceQueuePresenter;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -22,7 +23,7 @@ class InvestigationController extends Controller
|
||||
|
||||
public function __construct(
|
||||
protected InvestigationService $investigations,
|
||||
protected ServiceQueuePresenter $serviceQueues,
|
||||
protected CareQueueBridge $queueBridge,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
@@ -76,16 +77,46 @@ class InvestigationController extends Controller
|
||||
->where('organization_id', $organization->id)
|
||||
->orderBy('role')
|
||||
->get(),
|
||||
'serviceQueuePanel' => $this->serviceQueues->panel(
|
||||
$request,
|
||||
$organization,
|
||||
$this->member($request),
|
||||
'laboratory',
|
||||
$request->input('counter_uuid'),
|
||||
),
|
||||
'queueIntegration' => $branchId
|
||||
? $this->queueBridge->listMeta($organization, CareQueueContexts::LABORATORY, $branchId)
|
||||
: ['enabled' => false],
|
||||
]);
|
||||
}
|
||||
|
||||
public function callNext(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$organization = $this->organization($request);
|
||||
$member = $this->member($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($member);
|
||||
$branchId = (int) ($request->input('branch_id') ?: $branchScope);
|
||||
abort_unless($branchId > 0, 422);
|
||||
abort_unless($this->queueBridge->isEnabled($organization), 404);
|
||||
|
||||
$result = $this->queueBridge->callNext($organization, CareQueueContexts::LABORATORY, $branchId);
|
||||
$ticket = $result['ticket'] ?? null;
|
||||
if (! $ticket) {
|
||||
return back()->with('error', 'No lab work waiting in the laboratory queue.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Called '.$ticket['ticket_number'].'.');
|
||||
}
|
||||
|
||||
public function serve(Request $request, InvestigationRequest $investigation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'lab.manage');
|
||||
$this->authorizeInvestigation($request, $investigation);
|
||||
$organization = $this->organization($request);
|
||||
abort_unless($this->queueBridge->isEnabled($organization), 404);
|
||||
|
||||
$ticket = $this->queueBridge->serve($organization, $investigation);
|
||||
if (! $ticket) {
|
||||
return back()->with('error', 'Could not serve lab ticket.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Now serving '.$ticket['ticket_number'].'.');
|
||||
}
|
||||
|
||||
public function requestFromConsultation(Request $request, Consultation $consultation): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'investigations.request');
|
||||
|
||||
@@ -7,10 +7,11 @@ use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\Prescription;
|
||||
use App\Models\Practitioner;
|
||||
use App\Services\Care\CareQueueBridge;
|
||||
use App\Services\Care\CareQueueContexts;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\PharmacyService;
|
||||
use App\Services\Care\PrescriptionService;
|
||||
use App\Services\Care\ServiceQueuePresenter;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -22,7 +23,7 @@ class PrescriptionController extends Controller
|
||||
public function __construct(
|
||||
protected PrescriptionService $prescriptions,
|
||||
protected PharmacyService $pharmacy,
|
||||
protected ServiceQueuePresenter $serviceQueues,
|
||||
protected CareQueueBridge $queueBridge,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
@@ -49,8 +50,18 @@ class PrescriptionController extends Controller
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.view');
|
||||
$organization = $this->organization($request);
|
||||
$member = $this->member($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($member);
|
||||
$branchId = (int) ($branchScope ?: \App\Models\Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->value('id'));
|
||||
|
||||
$queue = $this->prescriptions->pharmacyQueue($this->ownerRef($request), $organization->id);
|
||||
if ($branchScope) {
|
||||
$queue = $queue->filter(fn (Prescription $rx) => (int) ($rx->visit?->branch_id) === $branchScope)->values();
|
||||
}
|
||||
|
||||
$drugs = \App\Models\Drug::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
@@ -60,23 +71,54 @@ class PrescriptionController extends Controller
|
||||
->get();
|
||||
|
||||
$canDispense = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'prescriptions.dispense');
|
||||
->can($member, 'prescriptions.dispense');
|
||||
|
||||
return view('care.prescriptions.queue', [
|
||||
'organization' => $organization,
|
||||
'queue' => $queue,
|
||||
'drugs' => $drugs,
|
||||
'canDispense' => $canDispense,
|
||||
'serviceQueuePanel' => $this->serviceQueues->panel(
|
||||
$request,
|
||||
$organization,
|
||||
$this->member($request),
|
||||
'pharmacy',
|
||||
$request->input('counter_uuid'),
|
||||
),
|
||||
'branchId' => $branchId,
|
||||
'queueIntegration' => $branchId
|
||||
? $this->queueBridge->listMeta($organization, CareQueueContexts::PHARMACY, $branchId)
|
||||
: ['enabled' => false],
|
||||
]);
|
||||
}
|
||||
|
||||
public function callNext(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.dispense');
|
||||
$organization = $this->organization($request);
|
||||
$member = $this->member($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($member);
|
||||
$branchId = (int) ($request->input('branch_id') ?: $branchScope);
|
||||
abort_unless($branchId > 0, 422);
|
||||
abort_unless($this->queueBridge->isEnabled($organization), 404);
|
||||
|
||||
$result = $this->queueBridge->callNext($organization, CareQueueContexts::PHARMACY, $branchId);
|
||||
$ticket = $result['ticket'] ?? null;
|
||||
if (! $ticket) {
|
||||
return back()->with('error', 'No prescriptions waiting in the pharmacy queue.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Called '.$ticket['ticket_number'].'.');
|
||||
}
|
||||
|
||||
public function serve(Request $request, Prescription $prescription): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.dispense');
|
||||
$this->authorizePrescription($request, $prescription);
|
||||
$organization = $this->organization($request);
|
||||
abort_unless($this->queueBridge->isEnabled($organization), 404);
|
||||
|
||||
$ticket = $this->queueBridge->serve($organization, $prescription);
|
||||
if (! $ticket) {
|
||||
return back()->with('error', 'Could not serve pharmacy ticket.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Now serving '.$ticket['ticket_number'].'.');
|
||||
}
|
||||
|
||||
public function create(Request $request, Consultation $consultation): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'prescriptions.manage');
|
||||
|
||||
@@ -8,9 +8,10 @@ use App\Models\Appointment;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Practitioner;
|
||||
use App\Services\Care\AppointmentService;
|
||||
use App\Services\Care\CareQueueBridge;
|
||||
use App\Services\Care\CareQueueContexts;
|
||||
use App\Services\Care\ConsultationService;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\ServiceQueuePresenter;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -22,7 +23,7 @@ class QueueController extends Controller
|
||||
public function __construct(
|
||||
protected AppointmentService $appointments,
|
||||
protected ConsultationService $consultations,
|
||||
protected ServiceQueuePresenter $serviceQueues,
|
||||
protected CareQueueBridge $queueBridge,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
@@ -85,16 +86,47 @@ class QueueController extends Controller
|
||||
'canManageQueue' => $canManageQueue,
|
||||
'canConsult' => $canConsult,
|
||||
'heroStats' => $heroStats,
|
||||
'serviceQueuePanel' => $this->serviceQueues->panel(
|
||||
$request,
|
||||
$organization,
|
||||
$member,
|
||||
'clinical',
|
||||
$request->input('counter_uuid'),
|
||||
),
|
||||
'queueIntegration' => $branchId
|
||||
? $this->queueBridge->listMeta($organization, CareQueueContexts::CONSULTATION, $branchId)
|
||||
: ['enabled' => false],
|
||||
]);
|
||||
}
|
||||
|
||||
public function callNext(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.view');
|
||||
$organization = $this->organization($request);
|
||||
abort_unless($this->queueBridge->isEnabled($organization), 404);
|
||||
|
||||
$member = $this->member($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($member);
|
||||
$branchId = (int) ($request->input('branch_id') ?: $branchScope);
|
||||
abort_unless($branchId > 0, 422);
|
||||
|
||||
$result = $this->queueBridge->callNext($organization, CareQueueContexts::CONSULTATION, $branchId);
|
||||
$ticket = $result['ticket'] ?? null;
|
||||
if (! $ticket) {
|
||||
return back()->with('error', 'No patients waiting in the consultation queue.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Called '.$ticket['ticket_number'].' — '.($ticket['customer_name'] ?? 'patient').'.');
|
||||
}
|
||||
|
||||
public function completeTicket(Request $request, Appointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->authorizeAppointment($request, $appointment);
|
||||
$organization = $this->organization($request);
|
||||
abort_unless($this->queueBridge->isEnabled($organization), 404);
|
||||
|
||||
$ticket = $this->queueBridge->complete($organization, $appointment);
|
||||
if (! $ticket) {
|
||||
return back()->with('error', 'Could not complete queue ticket.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Completed '.$ticket['ticket_number'].'.');
|
||||
}
|
||||
|
||||
public function start(Request $request, Appointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Services\Care\AppointmentPatientNotifier;
|
||||
use App\Services\Care\AuditLogger;
|
||||
use App\Services\Care\CareFeatures;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\CareQueueProvisioner;
|
||||
use App\Services\Care\PlanService;
|
||||
use App\Services\Billing\PlatformEmailClient;
|
||||
use App\Services\Billing\PlatformSmsClient;
|
||||
@@ -75,7 +76,7 @@ class SettingsController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, QueueClient $queue): RedirectResponse
|
||||
public function update(Request $request, QueueClient $queue, CareQueueProvisioner $provisioner): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'settings.manage');
|
||||
$organization = $this->organization($request);
|
||||
@@ -152,6 +153,14 @@ class SettingsController extends Controller
|
||||
'settings' => $settings,
|
||||
]);
|
||||
|
||||
if ($wantsQueue && $queue->configured()) {
|
||||
try {
|
||||
$provisioner->provisionOrganization($organization->fresh());
|
||||
} catch (\Throwable) {
|
||||
// Best-effort; role pages will lazy-ensure on next use.
|
||||
}
|
||||
}
|
||||
|
||||
AuditLogger::record($owner, 'organization.updated', $organization->id, $owner, \App\Models\Organization::class, $organization->id);
|
||||
|
||||
return back()->with('success', 'Settings saved.');
|
||||
|
||||
@@ -6,9 +6,10 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Department;
|
||||
use App\Services\Care\CareQueueBridge;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\ServiceQueuePresenter;
|
||||
use App\Services\Care\SpecialtyModuleService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
@@ -20,7 +21,7 @@ class SpecialtyModuleController extends Controller
|
||||
Request $request,
|
||||
string $module,
|
||||
SpecialtyModuleService $modules,
|
||||
ServiceQueuePresenter $presenter,
|
||||
CareQueueBridge $queueBridge,
|
||||
): View {
|
||||
$definition = $modules->definition($module);
|
||||
abort_unless($definition, 404);
|
||||
@@ -59,13 +60,7 @@ class SpecialtyModuleController extends Controller
|
||||
->limit(25)
|
||||
->get();
|
||||
|
||||
$serviceQueuePanel = $presenter->panel(
|
||||
$request,
|
||||
$organization,
|
||||
$member,
|
||||
$module,
|
||||
$request->input('counter_uuid'),
|
||||
);
|
||||
$branchId = (int) ($branchScope ?: $departments->first()?->branch_id);
|
||||
|
||||
return view('care.specialty.show', [
|
||||
'organization' => $organization,
|
||||
@@ -74,7 +69,37 @@ class SpecialtyModuleController extends Controller
|
||||
'departments' => $departments,
|
||||
'waiting' => $waiting,
|
||||
'queueStubs' => $modules->queueStubsFor($organization, $module),
|
||||
'serviceQueuePanel' => $serviceQueuePanel,
|
||||
'branchId' => $branchId,
|
||||
'queueIntegration' => $branchId
|
||||
? $queueBridge->listMeta($organization, $module, $branchId)
|
||||
: ['enabled' => false],
|
||||
]);
|
||||
}
|
||||
|
||||
public function callNext(
|
||||
Request $request,
|
||||
string $module,
|
||||
SpecialtyModuleService $modules,
|
||||
CareQueueBridge $queueBridge,
|
||||
): RedirectResponse {
|
||||
$definition = $modules->definition($module);
|
||||
abort_unless($definition, 404);
|
||||
|
||||
$organization = $this->organization($request);
|
||||
abort_unless($modules->isEnabled($organization, $module), 404);
|
||||
abort_unless($queueBridge->isEnabled($organization), 404);
|
||||
|
||||
$member = $this->member($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($member);
|
||||
$branchId = (int) ($request->input('branch_id') ?: $branchScope);
|
||||
abort_unless($branchId > 0, 422);
|
||||
|
||||
$result = $queueBridge->callNext($organization, $module, $branchId);
|
||||
$ticket = $result['ticket'] ?? null;
|
||||
if (! $ticket) {
|
||||
return back()->with('error', 'No patients waiting in this specialty queue.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Called '.$ticket['ticket_number'].'.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ class Appointment extends Model
|
||||
protected $table = 'care_appointments';
|
||||
|
||||
protected $fillable = [
|
||||
'uuid', 'owner_ref', 'organization_id', 'branch_id', 'patient_id',
|
||||
'uuid', 'queue_ticket_uuid', 'queue_ticket_number', 'queue_ticket_status',
|
||||
'owner_ref', 'organization_id', 'branch_id', 'patient_id',
|
||||
'practitioner_id', 'department_id', 'visit_id', 'type', 'status',
|
||||
'scheduled_at', 'checked_in_at', 'waiting_at', 'started_at',
|
||||
'completed_at', 'cancelled_at', 'queue_position', 'reason', 'notes',
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@ class Bill extends Model
|
||||
protected $table = 'care_bills';
|
||||
|
||||
protected $fillable = [
|
||||
'uuid', 'owner_ref', 'organization_id', 'branch_id', 'visit_id', 'patient_id',
|
||||
'uuid', 'queue_ticket_uuid', 'queue_ticket_number', 'queue_ticket_status',
|
||||
'owner_ref', 'organization_id', 'branch_id', 'visit_id', 'patient_id',
|
||||
'invoice_number', 'status', 'subtotal_minor', 'discount_minor', 'tax_minor',
|
||||
'total_minor', 'amount_paid_minor', 'balance_minor', 'notes', 'created_by', 'finalized_at',
|
||||
];
|
||||
|
||||
@@ -30,7 +30,8 @@ class InvestigationRequest extends Model
|
||||
protected $table = 'care_investigation_requests';
|
||||
|
||||
protected $fillable = [
|
||||
'uuid', 'owner_ref', 'organization_id', 'branch_id', 'visit_id', 'consultation_id',
|
||||
'uuid', 'queue_ticket_uuid', 'queue_ticket_number', 'queue_ticket_status',
|
||||
'owner_ref', 'organization_id', 'branch_id', 'visit_id', 'consultation_id',
|
||||
'patient_id', 'investigation_type_id', 'practitioner_id', 'status', 'priority',
|
||||
'clinical_notes', 'requested_by', 'assigned_member_id', 'sample_barcode',
|
||||
'sample_collected_at', 'sample_collected_by', 'completed_at', 'delivered_at',
|
||||
|
||||
@@ -24,7 +24,8 @@ class Prescription extends Model
|
||||
protected $table = 'care_prescriptions';
|
||||
|
||||
protected $fillable = [
|
||||
'uuid', 'owner_ref', 'organization_id', 'visit_id', 'consultation_id',
|
||||
'uuid', 'queue_ticket_uuid', 'queue_ticket_number', 'queue_ticket_status',
|
||||
'owner_ref', 'organization_id', 'visit_id', 'consultation_id',
|
||||
'patient_id', 'practitioner_id', 'status', 'notes',
|
||||
'prescribed_by', 'dispensed_by', 'dispensed_at',
|
||||
];
|
||||
|
||||
@@ -19,6 +19,7 @@ class AppointmentService
|
||||
public function __construct(
|
||||
protected VisitService $visits,
|
||||
protected AppointmentPatientNotifier $notifier,
|
||||
protected CareQueueBridge $queueBridge,
|
||||
) {}
|
||||
|
||||
public function queryForOrganization(string $ownerRef, int $organizationId): Builder
|
||||
@@ -142,7 +143,10 @@ class AppointmentService
|
||||
|
||||
AuditLogger::record($ownerRef, 'appointment.walk_in', $organization->id, $actorRef, Appointment::class, $appointment->id);
|
||||
|
||||
return $appointment->load(['patient', 'practitioner', 'branch', 'visit']);
|
||||
$appointment = $appointment->load(['patient', 'practitioner', 'branch', 'visit']);
|
||||
$this->queueBridge->issueForAppointment($organization, $appointment);
|
||||
|
||||
return $appointment->fresh(['patient', 'practitioner', 'branch', 'visit']);
|
||||
}
|
||||
|
||||
public function checkIn(Appointment $appointment, string $ownerRef, ?string $actorRef = null): Appointment
|
||||
@@ -167,6 +171,9 @@ class AppointmentService
|
||||
|
||||
AuditLogger::record($ownerRef, 'appointment.checked_in', $appointment->organization_id, $actorRef, Appointment::class, $appointment->id);
|
||||
|
||||
$appointment = $appointment->fresh(['patient', 'practitioner', 'visit', 'organization']);
|
||||
$this->queueBridge->issueForAppointment($appointment->organization, $appointment);
|
||||
|
||||
return $appointment->fresh(['patient', 'practitioner', 'visit']);
|
||||
}
|
||||
|
||||
@@ -188,6 +195,11 @@ class AppointmentService
|
||||
$appointment->update($updates);
|
||||
AuditLogger::record($ownerRef, 'appointment.waiting', $appointment->organization_id, $actorRef, Appointment::class, $appointment->id);
|
||||
|
||||
$appointment = $appointment->fresh(['patient', 'practitioner', 'organization']);
|
||||
if ($appointment->organization) {
|
||||
$this->queueBridge->issueForAppointment($appointment->organization, $appointment);
|
||||
}
|
||||
|
||||
return $appointment->fresh(['patient', 'practitioner']);
|
||||
}
|
||||
|
||||
@@ -216,6 +228,11 @@ class AppointmentService
|
||||
|
||||
AuditLogger::record($ownerRef, 'appointment.in_consultation', $appointment->organization_id, $actorRef, Appointment::class, $appointment->id);
|
||||
|
||||
$appointment = $appointment->fresh(['patient', 'practitioner', 'visit', 'organization']);
|
||||
if ($appointment->organization) {
|
||||
$this->queueBridge->serve($appointment->organization, $appointment);
|
||||
}
|
||||
|
||||
return $appointment->fresh(['patient', 'practitioner', 'visit']);
|
||||
}
|
||||
|
||||
@@ -234,6 +251,11 @@ class AppointmentService
|
||||
|
||||
AuditLogger::record($ownerRef, 'appointment.completed', $appointment->organization_id, $actorRef, Appointment::class, $appointment->id);
|
||||
|
||||
$appointment = $appointment->fresh(['patient', 'practitioner', 'visit', 'organization']);
|
||||
if ($appointment->organization) {
|
||||
$this->queueBridge->complete($appointment->organization, $appointment);
|
||||
}
|
||||
|
||||
return $appointment->fresh(['patient', 'practitioner', 'visit']);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class BillService
|
||||
public function __construct(
|
||||
protected InvoiceNumberGenerator $invoices,
|
||||
protected MerchantGatewayService $gateway,
|
||||
protected CareQueueBridge $queueBridge,
|
||||
) {}
|
||||
|
||||
public function queryForOrganization(string $ownerRef, int $organizationId): Builder
|
||||
@@ -81,6 +82,11 @@ class BillService
|
||||
|
||||
AuditLogger::record($ownerRef, 'bill.created', $visit->organization_id, $actorRef, Bill::class, $bill->id);
|
||||
|
||||
$bill = $bill->fresh(['lineItems', 'patient', 'payments']);
|
||||
if ($visit->organization) {
|
||||
$this->queueBridge->issueForBill($visit->organization, $bill);
|
||||
}
|
||||
|
||||
return $bill->fresh(['lineItems', 'patient', 'payments']);
|
||||
}
|
||||
|
||||
@@ -441,6 +447,7 @@ class BillService
|
||||
$bill->refresh();
|
||||
$paid = $this->paidSum($bill);
|
||||
$balance = max(0, $bill->total_minor - $paid);
|
||||
$wasPaid = $bill->status === Bill::STATUS_PAID;
|
||||
|
||||
$status = Bill::STATUS_OPEN;
|
||||
if ($paid > 0 && $balance > 0) {
|
||||
@@ -460,6 +467,13 @@ class BillService
|
||||
'balance_minor' => $balance,
|
||||
'status' => $status,
|
||||
]);
|
||||
|
||||
if (! $wasPaid && $status === Bill::STATUS_PAID) {
|
||||
$organization = Organization::query()->find($bill->organization_id);
|
||||
if ($organization) {
|
||||
$this->queueBridge->complete($organization, $bill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function paidSum(Bill $bill): int
|
||||
|
||||
@@ -40,6 +40,7 @@ class CarePermissions
|
||||
],
|
||||
'cashier' => [
|
||||
'dashboard.view', 'patients.view', 'bills.view', 'bills.manage', 'payments.manage',
|
||||
'service_queues.console',
|
||||
],
|
||||
'accountant' => [
|
||||
'dashboard.view', 'bills.view', 'reports.finance.view', 'reports.finance.export',
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care;
|
||||
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Bill;
|
||||
use App\Models\InvestigationRequest;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Patient;
|
||||
use App\Models\Prescription;
|
||||
use App\Services\Queue\QueueClient;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Bridges Care workflow entities to Ladill Queue tickets for the matching department queue.
|
||||
*/
|
||||
class CareQueueBridge
|
||||
{
|
||||
public function __construct(
|
||||
protected QueueClient $queue,
|
||||
protected CareQueueProvisioner $provisioner,
|
||||
protected PlanService $plans,
|
||||
protected SpecialtyModuleService $specialties,
|
||||
) {}
|
||||
|
||||
public function isEnabled(Organization $organization): bool
|
||||
{
|
||||
return $this->plans->canUseQueueIntegration($organization)
|
||||
&& (bool) data_get($organization->settings, 'queue_integration_enabled')
|
||||
&& $this->queue->configured();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve which queue context an appointment should use (specialty if applicable).
|
||||
*/
|
||||
public function contextForAppointment(Organization $organization, Appointment $appointment): string
|
||||
{
|
||||
$departmentId = $appointment->department_id;
|
||||
if (! $departmentId) {
|
||||
return CareQueueContexts::CONSULTATION;
|
||||
}
|
||||
|
||||
foreach ($this->specialties->enabledKeys($organization) as $key) {
|
||||
$deptIds = data_get($organization->settings, "specialty_module_provisioning.{$key}.department_ids", []);
|
||||
if (is_array($deptIds) && in_array($departmentId, array_map('intval', $deptIds), true)) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
return CareQueueContexts::CONSULTATION;
|
||||
}
|
||||
|
||||
public function issueForAppointment(Organization $organization, Appointment $appointment): ?Appointment
|
||||
{
|
||||
if (! $this->isEnabled($organization) || filled($appointment->queue_ticket_uuid)) {
|
||||
return $appointment;
|
||||
}
|
||||
|
||||
$context = $this->contextForAppointment($organization, $appointment);
|
||||
$ticket = $this->issue(
|
||||
$organization,
|
||||
$context,
|
||||
(int) $appointment->branch_id,
|
||||
$appointment->patient,
|
||||
[
|
||||
'care_entity' => 'appointment',
|
||||
'care_entity_uuid' => $appointment->uuid,
|
||||
'care_entity_id' => $appointment->id,
|
||||
],
|
||||
$appointment->type === Appointment::TYPE_WALK_IN ? 'walk_in' : 'appointment',
|
||||
'appointment',
|
||||
);
|
||||
|
||||
if (! $ticket) {
|
||||
return $appointment;
|
||||
}
|
||||
|
||||
$this->applyTicket($appointment, $ticket);
|
||||
|
||||
return $appointment->fresh();
|
||||
}
|
||||
|
||||
public function issueForPrescription(Organization $organization, Prescription $prescription): ?Prescription
|
||||
{
|
||||
if (! $this->isEnabled($organization) || filled($prescription->queue_ticket_uuid)) {
|
||||
return $prescription;
|
||||
}
|
||||
|
||||
$prescription->loadMissing(['patient', 'visit']);
|
||||
$branchId = (int) ($prescription->visit?->branch_id ?? 0);
|
||||
if ($branchId < 1) {
|
||||
return $prescription;
|
||||
}
|
||||
|
||||
$ticket = $this->issue(
|
||||
$organization,
|
||||
CareQueueContexts::PHARMACY,
|
||||
$branchId,
|
||||
$prescription->patient,
|
||||
[
|
||||
'care_entity' => 'prescription',
|
||||
'care_entity_uuid' => $prescription->uuid,
|
||||
'care_entity_id' => $prescription->id,
|
||||
],
|
||||
);
|
||||
|
||||
if (! $ticket) {
|
||||
return $prescription;
|
||||
}
|
||||
|
||||
$this->applyTicket($prescription, $ticket);
|
||||
|
||||
return $prescription->fresh();
|
||||
}
|
||||
|
||||
public function issueForInvestigation(Organization $organization, InvestigationRequest $request): ?InvestigationRequest
|
||||
{
|
||||
if (! $this->isEnabled($organization) || filled($request->queue_ticket_uuid)) {
|
||||
return $request;
|
||||
}
|
||||
|
||||
$request->loadMissing('patient');
|
||||
$ticket = $this->issue(
|
||||
$organization,
|
||||
CareQueueContexts::LABORATORY,
|
||||
(int) $request->branch_id,
|
||||
$request->patient,
|
||||
[
|
||||
'care_entity' => 'investigation_request',
|
||||
'care_entity_uuid' => $request->uuid,
|
||||
'care_entity_id' => $request->id,
|
||||
],
|
||||
);
|
||||
|
||||
if (! $ticket) {
|
||||
return $request;
|
||||
}
|
||||
|
||||
$this->applyTicket($request, $ticket);
|
||||
|
||||
return $request->fresh();
|
||||
}
|
||||
|
||||
public function issueForBill(Organization $organization, Bill $bill): ?Bill
|
||||
{
|
||||
if (! $this->isEnabled($organization) || filled($bill->queue_ticket_uuid)) {
|
||||
return $bill;
|
||||
}
|
||||
|
||||
if (in_array($bill->status, [Bill::STATUS_PAID, Bill::STATUS_VOID], true)) {
|
||||
return $bill;
|
||||
}
|
||||
|
||||
$bill->loadMissing('patient');
|
||||
$ticket = $this->issue(
|
||||
$organization,
|
||||
CareQueueContexts::BILLING,
|
||||
(int) $bill->branch_id,
|
||||
$bill->patient,
|
||||
[
|
||||
'care_entity' => 'bill',
|
||||
'care_entity_uuid' => $bill->uuid,
|
||||
'care_entity_id' => $bill->id,
|
||||
],
|
||||
);
|
||||
|
||||
if (! $ticket) {
|
||||
return $bill;
|
||||
}
|
||||
|
||||
$this->applyTicket($bill, $ticket);
|
||||
|
||||
return $bill->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the next waiting ticket for a Care page context + branch.
|
||||
*
|
||||
* @return array{ticket: ?array<string, mixed>, entity: ?Model, resources: ?array<string, mixed>}
|
||||
*/
|
||||
public function callNext(Organization $organization, string $context, int $branchId): array
|
||||
{
|
||||
$resources = $this->provisioner->ensure($organization, $context, $branchId);
|
||||
if (! $resources || empty($resources['queue_uuid']) || empty($resources['counter_uuid'])) {
|
||||
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
|
||||
}
|
||||
|
||||
$owner = (string) $organization->owner_ref;
|
||||
|
||||
try {
|
||||
$ticket = $this->queue->callNext(
|
||||
$owner,
|
||||
(string) $resources['queue_uuid'],
|
||||
(string) $resources['counter_uuid'],
|
||||
);
|
||||
} catch (RequestException $e) {
|
||||
Log::warning('care.queue_call_next_failed', [
|
||||
'context' => $context,
|
||||
'branch_id' => $branchId,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
|
||||
}
|
||||
|
||||
if (! $ticket) {
|
||||
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
|
||||
}
|
||||
|
||||
$entity = $this->findEntityByTicket($organization, $context, (string) ($ticket['uuid'] ?? ''));
|
||||
if ($entity) {
|
||||
$this->applyTicket($entity, $ticket);
|
||||
}
|
||||
|
||||
return ['ticket' => $ticket, 'entity' => $entity?->fresh(), 'resources' => $resources];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark ticket as serving (Queue "start").
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function serve(Organization $organization, Model $entity): ?array
|
||||
{
|
||||
return $this->ticketAction($organization, $entity, 'start');
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the Queue ticket linked to a Care entity.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function complete(Organization $organization, Model $entity): ?array
|
||||
{
|
||||
return $this->ticketAction($organization, $entity, 'complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Recall the Queue ticket linked to a Care entity.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function recall(Organization $organization, Model $entity): ?array
|
||||
{
|
||||
return $this->ticketAction($organization, $entity, 'recall');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled: bool, context: string, queue_uuid: ?string, counter_uuid: ?string}
|
||||
*/
|
||||
public function listMeta(Organization $organization, string $context, int $branchId): array
|
||||
{
|
||||
if (! $this->isEnabled($organization)) {
|
||||
return [
|
||||
'enabled' => false,
|
||||
'context' => $context,
|
||||
'queue_uuid' => null,
|
||||
'counter_uuid' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$resources = $this->provisioner->ensure($organization->fresh(), $context, $branchId);
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'context' => $context,
|
||||
'queue_uuid' => $resources['queue_uuid'] ?? null,
|
||||
'counter_uuid' => $resources['counter_uuid'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $metadata
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
protected function issue(
|
||||
Organization $organization,
|
||||
string $context,
|
||||
int $branchId,
|
||||
?Patient $patient,
|
||||
array $metadata = [],
|
||||
string $priority = 'walk_in',
|
||||
string $source = 'api',
|
||||
): ?array {
|
||||
$resources = $this->provisioner->ensure($organization, $context, $branchId);
|
||||
if (! $resources || empty($resources['queue_uuid'])) {
|
||||
Log::info('care.queue_issue_skipped_no_queue', [
|
||||
'context' => $context,
|
||||
'branch_id' => $branchId,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$owner = (string) $organization->owner_ref;
|
||||
|
||||
try {
|
||||
return $this->queue->issueTicket($owner, [
|
||||
'service_queue_id' => $resources['queue_uuid'],
|
||||
'customer_name' => $patient?->fullName(),
|
||||
'customer_phone' => $patient?->phone,
|
||||
'priority' => $priority,
|
||||
'source' => $source,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('care.queue_issue_failed', [
|
||||
'context' => $context,
|
||||
'branch_id' => $branchId,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $ticket
|
||||
*/
|
||||
protected function applyTicket(Model $entity, array $ticket): void
|
||||
{
|
||||
$entity->forceFill([
|
||||
'queue_ticket_uuid' => $ticket['uuid'] ?? $entity->getAttribute('queue_ticket_uuid'),
|
||||
'queue_ticket_number' => $ticket['ticket_number'] ?? $entity->getAttribute('queue_ticket_number'),
|
||||
'queue_ticket_status' => $ticket['status'] ?? $entity->getAttribute('queue_ticket_status'),
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
protected function ticketAction(Organization $organization, Model $entity, string $action): ?array
|
||||
{
|
||||
$uuid = (string) $entity->getAttribute('queue_ticket_uuid');
|
||||
if ($uuid === '' || ! $this->isEnabled($organization)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$ticket = $this->queue->ticketAction((string) $organization->owner_ref, $uuid, [
|
||||
'action' => $action,
|
||||
]);
|
||||
$this->applyTicket($entity, $ticket);
|
||||
|
||||
return $ticket;
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('care.queue_ticket_action_failed', [
|
||||
'action' => $action,
|
||||
'ticket_uuid' => $uuid,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function findEntityByTicket(Organization $organization, string $context, string $ticketUuid): ?Model
|
||||
{
|
||||
if ($ticketUuid === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$owner = (string) $organization->owner_ref;
|
||||
|
||||
return match (true) {
|
||||
$context === CareQueueContexts::PHARMACY => Prescription::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('queue_ticket_uuid', $ticketUuid)
|
||||
->first(),
|
||||
$context === CareQueueContexts::LABORATORY => InvestigationRequest::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('queue_ticket_uuid', $ticketUuid)
|
||||
->first(),
|
||||
$context === CareQueueContexts::BILLING => Bill::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('queue_ticket_uuid', $ticketUuid)
|
||||
->first(),
|
||||
default => Appointment::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('queue_ticket_uuid', $ticketUuid)
|
||||
->first(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care;
|
||||
|
||||
/**
|
||||
* Canonical Care → Ladill Queue department / specialty contexts.
|
||||
*/
|
||||
class CareQueueContexts
|
||||
{
|
||||
public const RECEPTION = 'reception';
|
||||
|
||||
public const CONSULTATION = 'consultation';
|
||||
|
||||
public const PHARMACY = 'pharmacy';
|
||||
|
||||
public const LABORATORY = 'laboratory';
|
||||
|
||||
public const BILLING = 'billing';
|
||||
|
||||
/**
|
||||
* Core department queues provisioned for every branch when Queue integration is on.
|
||||
*
|
||||
* @return array<string, array{name: string, prefix: string}>
|
||||
*/
|
||||
public static function departments(): array
|
||||
{
|
||||
return [
|
||||
self::RECEPTION => ['name' => 'Reception', 'prefix' => 'R'],
|
||||
self::CONSULTATION => ['name' => 'Consultation', 'prefix' => 'C'],
|
||||
self::PHARMACY => ['name' => 'Pharmacy', 'prefix' => 'P'],
|
||||
self::LABORATORY => ['name' => 'Laboratory', 'prefix' => 'L'],
|
||||
self::BILLING => ['name' => 'Billing', 'prefix' => 'B'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function isDepartment(string $context): bool
|
||||
{
|
||||
return array_key_exists($context, self::departments());
|
||||
}
|
||||
|
||||
public static function isSpecialty(string $context): bool
|
||||
{
|
||||
return array_key_exists($context, config('care.specialty_modules', []));
|
||||
}
|
||||
|
||||
public static function queueExternalKey(string $context, int|string $careBranchId): string
|
||||
{
|
||||
if (self::isSpecialty($context)) {
|
||||
return "care:specialty:{$context}:queue:{$careBranchId}";
|
||||
}
|
||||
|
||||
return "care:dept:{$context}:queue:{$careBranchId}";
|
||||
}
|
||||
|
||||
public static function counterExternalKey(string $context, int|string $careBranchId): string
|
||||
{
|
||||
if (self::isSpecialty($context)) {
|
||||
return "care:specialty:{$context}:counter:{$careBranchId}";
|
||||
}
|
||||
|
||||
return "care:dept:{$context}:counter:{$careBranchId}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care;
|
||||
|
||||
use App\Models\Branch;
|
||||
use App\Models\Organization;
|
||||
use App\Services\Queue\QueueClient;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Idempotently provisions Ladill Queue queues/counters for Care departments per branch.
|
||||
*/
|
||||
class CareQueueProvisioner
|
||||
{
|
||||
public function __construct(
|
||||
protected QueueClient $queue,
|
||||
protected PlanService $plans,
|
||||
) {}
|
||||
|
||||
public function shouldProvision(Organization $organization): bool
|
||||
{
|
||||
return $this->plans->canUseQueueIntegration($organization)
|
||||
&& (bool) data_get($organization->settings, 'queue_integration_enabled')
|
||||
&& $this->queue->configured();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provision core department queues for all org branches. Returns updated settings fragment.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function provisionOrganization(Organization $organization): array
|
||||
{
|
||||
if (! $this->shouldProvision($organization)) {
|
||||
return (array) data_get($organization->settings, 'department_queue_provisioning', []);
|
||||
}
|
||||
|
||||
$owner = (string) $organization->owner_ref;
|
||||
$branches = Branch::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$provisioning = (array) data_get($organization->settings, 'department_queue_provisioning', []);
|
||||
|
||||
foreach (CareQueueContexts::departments() as $context => $meta) {
|
||||
$queues = is_array($provisioning[$context]['queues'] ?? null)
|
||||
? $provisioning[$context]['queues']
|
||||
: [];
|
||||
$byBranch = collect($queues)->keyBy(fn ($q) => (string) ($q['branch_id'] ?? ''));
|
||||
|
||||
foreach ($branches as $branch) {
|
||||
$prior = $byBranch->get((string) $branch->id, []);
|
||||
$stub = [
|
||||
'name' => $meta['name'],
|
||||
'prefix' => $meta['prefix'],
|
||||
'module' => $context,
|
||||
'branch_id' => $branch->id,
|
||||
'branch_name' => $branch->name,
|
||||
'active' => true,
|
||||
'queue_external_key' => CareQueueContexts::queueExternalKey($context, $branch->id),
|
||||
'counter_external_key' => CareQueueContexts::counterExternalKey($context, $branch->id),
|
||||
'queue_uuid' => $prior['queue_uuid'] ?? null,
|
||||
'counter_uuid' => $prior['counter_uuid'] ?? null,
|
||||
'synced' => false,
|
||||
];
|
||||
|
||||
try {
|
||||
$synced = $this->queue->syncSpecialtyQueueStubs($owner, [$stub]);
|
||||
$stub = $synced[0] ?? $stub;
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('care.queue_department_provision_failed', [
|
||||
'context' => $context,
|
||||
'branch_id' => $branch->id,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$byBranch->put((string) $branch->id, $stub);
|
||||
}
|
||||
|
||||
$provisioning[$context] = [
|
||||
'queues' => $byBranch->values()->all(),
|
||||
];
|
||||
}
|
||||
|
||||
$settings = $organization->settings ?? [];
|
||||
$settings['department_queue_provisioning'] = $provisioning;
|
||||
$organization->update(['settings' => $settings]);
|
||||
|
||||
return $provisioning;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{queue_uuid: ?string, counter_uuid: ?string, queue_external_key: string, counter_external_key: string, synced: bool}|null
|
||||
*/
|
||||
public function resourcesFor(
|
||||
Organization $organization,
|
||||
string $context,
|
||||
int $branchId,
|
||||
): ?array {
|
||||
if (CareQueueContexts::isSpecialty($context)) {
|
||||
$queues = data_get($organization->settings, "specialty_module_provisioning.{$context}.queues", []);
|
||||
} else {
|
||||
$queues = data_get($organization->settings, "department_queue_provisioning.{$context}.queues", []);
|
||||
}
|
||||
|
||||
if (! is_array($queues)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$match = collect($queues)->first(
|
||||
fn ($q) => (int) ($q['branch_id'] ?? 0) === $branchId && ($q['active'] ?? true)
|
||||
);
|
||||
|
||||
if (! $match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'queue_uuid' => $match['queue_uuid'] ?? null,
|
||||
'counter_uuid' => $match['counter_uuid'] ?? null,
|
||||
'queue_external_key' => (string) ($match['queue_external_key']
|
||||
?? CareQueueContexts::queueExternalKey($context, $branchId)),
|
||||
'counter_external_key' => (string) ($match['counter_external_key']
|
||||
?? CareQueueContexts::counterExternalKey($context, $branchId)),
|
||||
'synced' => (bool) ($match['synced'] ?? false),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure resources exist for one context+branch (lazy provision).
|
||||
*
|
||||
* @return array{queue_uuid: ?string, counter_uuid: ?string, queue_external_key: string, counter_external_key: string, synced: bool}|null
|
||||
*/
|
||||
public function ensure(
|
||||
Organization $organization,
|
||||
string $context,
|
||||
int $branchId,
|
||||
): ?array {
|
||||
$existing = $this->resourcesFor($organization, $context, $branchId);
|
||||
if ($existing && ! empty($existing['queue_uuid']) && ! empty($existing['counter_uuid'])) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
if (! $this->shouldProvision($organization)) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
if (CareQueueContexts::isSpecialty($context)) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
if (! CareQueueContexts::isDepartment($context)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->provisionOrganization($organization->fresh());
|
||||
|
||||
return $this->resourcesFor($organization->fresh(), $context, $branchId);
|
||||
}
|
||||
}
|
||||
@@ -462,6 +462,14 @@ class DemoTenantSeeder
|
||||
} catch (\Throwable) {
|
||||
// Demo seed should not fail if Queue API is unreachable; settings flag remains.
|
||||
}
|
||||
|
||||
if (data_get($organization->fresh()->settings, 'queue_integration_enabled')) {
|
||||
try {
|
||||
app(CareQueueProvisioner::class)->provisionOrganization($organization->fresh());
|
||||
} catch (\Throwable) {
|
||||
// Best-effort department queue stubs for Pro demo ticket flow.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,10 @@ use InvalidArgumentException;
|
||||
|
||||
class InvestigationService
|
||||
{
|
||||
public function __construct(
|
||||
protected CareQueueBridge $queueBridge,
|
||||
) {}
|
||||
|
||||
public function queryForOrganization(string $ownerRef, int $organizationId): Builder
|
||||
{
|
||||
return InvestigationRequest::owned($ownerRef)->where('organization_id', $organizationId);
|
||||
@@ -108,7 +112,13 @@ class InvestigationService
|
||||
$request->id,
|
||||
);
|
||||
|
||||
$created->push($request->load('investigationType'));
|
||||
$request = $request->load(['investigationType', 'patient']);
|
||||
$organization = Organization::query()->find($request->organization_id);
|
||||
if ($organization) {
|
||||
$this->queueBridge->issueForInvestigation($organization, $request);
|
||||
}
|
||||
|
||||
$created->push($request->fresh(['investigationType', 'patient']));
|
||||
}
|
||||
|
||||
return $created;
|
||||
@@ -131,7 +141,13 @@ class InvestigationService
|
||||
|
||||
AuditLogger::record($ownerRef, 'investigation.sample_collected', $request->organization_id, $actorRef, InvestigationRequest::class, $request->id);
|
||||
|
||||
return $request->fresh(['patient', 'investigationType']);
|
||||
$request = $request->fresh(['patient', 'investigationType']);
|
||||
$organization = Organization::query()->find($request->organization_id);
|
||||
if ($organization) {
|
||||
$this->queueBridge->serve($organization, $request);
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function startProcessing(
|
||||
@@ -261,7 +277,13 @@ class InvestigationService
|
||||
|
||||
AuditLogger::record($ownerRef, 'investigation.approved', $request->organization_id, $actorRef, InvestigationRequest::class, $request->id);
|
||||
|
||||
return $request->fresh(['patient', 'investigationType', 'result.values']);
|
||||
$request = $request->fresh(['patient', 'investigationType', 'result.values']);
|
||||
$organization = Organization::query()->find($request->organization_id);
|
||||
if ($organization) {
|
||||
$this->queueBridge->complete($organization, $request);
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function deliver(InvestigationRequest $request, string $ownerRef, ?string $actorRef = null): InvestigationRequest
|
||||
|
||||
@@ -15,6 +15,10 @@ use InvalidArgumentException;
|
||||
|
||||
class PharmacyService
|
||||
{
|
||||
public function __construct(
|
||||
protected CareQueueBridge $queueBridge,
|
||||
) {}
|
||||
|
||||
public function queryDrugs(string $ownerRef, int $organizationId): Builder
|
||||
{
|
||||
return Drug::owned($ownerRef)->where('organization_id', $organizationId);
|
||||
@@ -197,6 +201,12 @@ class PharmacyService
|
||||
|
||||
AuditLogger::record($ownerRef, 'prescription.dispensed', $prescription->organization_id, $actorRef, Prescription::class, $prescription->id);
|
||||
|
||||
return $prescription->fresh(['items', 'patient']);
|
||||
$prescription = $prescription->fresh(['items', 'patient']);
|
||||
$organization = Organization::query()->find($prescription->organization_id);
|
||||
if ($organization) {
|
||||
$this->queueBridge->complete($organization, $prescription);
|
||||
}
|
||||
|
||||
return $prescription;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Services\Care;
|
||||
|
||||
use App\Models\Consultation;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Prescription;
|
||||
use App\Models\PrescriptionItem;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
@@ -12,6 +13,10 @@ use InvalidArgumentException;
|
||||
|
||||
class PrescriptionService
|
||||
{
|
||||
public function __construct(
|
||||
protected CareQueueBridge $queueBridge,
|
||||
) {}
|
||||
|
||||
public function queryForOrganization(string $ownerRef, int $organizationId): Builder
|
||||
{
|
||||
return Prescription::owned($ownerRef)->where('organization_id', $organizationId);
|
||||
@@ -89,6 +94,14 @@ class PrescriptionService
|
||||
$prescription->id,
|
||||
);
|
||||
|
||||
$prescription = $prescription->fresh(['items', 'patient', 'practitioner', 'visit']);
|
||||
if ($status === Prescription::STATUS_ACTIVE) {
|
||||
$organization = Organization::query()->find($prescription->organization_id);
|
||||
if ($organization) {
|
||||
$this->queueBridge->issueForPrescription($organization, $prescription);
|
||||
}
|
||||
}
|
||||
|
||||
return $prescription->fresh(['items', 'patient', 'practitioner']);
|
||||
}
|
||||
|
||||
@@ -122,6 +135,12 @@ class PrescriptionService
|
||||
$prescription->update(['status' => Prescription::STATUS_ACTIVE]);
|
||||
AuditLogger::record($ownerRef, 'prescription.activated', $prescription->organization_id, $actorRef, Prescription::class, $prescription->id);
|
||||
|
||||
$prescription = $prescription->fresh(['items', 'patient', 'visit']);
|
||||
$organization = Organization::query()->find($prescription->organization_id);
|
||||
if ($organization) {
|
||||
$this->queueBridge->issueForPrescription($organization, $prescription);
|
||||
}
|
||||
|
||||
return $prescription->fresh(['items', 'patient']);
|
||||
}
|
||||
|
||||
@@ -137,6 +156,12 @@ class PrescriptionService
|
||||
|
||||
AuditLogger::record($ownerRef, 'prescription.dispensed', $prescription->organization_id, $actorRef, Prescription::class, $prescription->id);
|
||||
|
||||
$prescription = $prescription->fresh(['items', 'patient', 'visit']);
|
||||
$organization = Organization::query()->find($prescription->organization_id);
|
||||
if ($organization) {
|
||||
$this->queueBridge->complete($organization, $prescription);
|
||||
}
|
||||
|
||||
return $prescription->fresh(['items', 'patient']);
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,45 @@ class QueueClient
|
||||
return (array) ($response->json('data') ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function issueTicket(string $owner, array $payload): array
|
||||
{
|
||||
$response = $this->http($owner)->post($this->base().'/tickets', $payload);
|
||||
$response->throw();
|
||||
|
||||
return (array) ($response->json('data') ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function callNext(string $owner, string $queueUuid, string $counterUuid): ?array
|
||||
{
|
||||
$response = $this->http($owner)->post($this->base().'/queues/'.$queueUuid.'/call-next', [
|
||||
'counter_id' => $counterUuid,
|
||||
]);
|
||||
$response->throw();
|
||||
|
||||
$data = $response->json('data');
|
||||
|
||||
return is_array($data) ? $data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function ticketAction(string $owner, string $ticketUuid, array $payload): array
|
||||
{
|
||||
$response = $this->http($owner)->post($this->base().'/tickets/'.$ticketUuid.'/action', $payload);
|
||||
$response->throw();
|
||||
|
||||
return (array) ($response->json('data') ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or link real Queue queues/counters for Care specialty stubs.
|
||||
* Idempotent via external_key (and Queue-side name+branch fallback).
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
foreach (['care_appointments', 'care_prescriptions', 'care_investigation_requests', 'care_bills'] as $table) {
|
||||
Schema::table($table, function (Blueprint $blueprint) {
|
||||
$blueprint->string('queue_ticket_uuid', 36)->nullable()->after('uuid');
|
||||
$blueprint->string('queue_ticket_number', 32)->nullable()->after('queue_ticket_uuid');
|
||||
$blueprint->string('queue_ticket_status', 32)->nullable()->after('queue_ticket_number');
|
||||
$blueprint->index('queue_ticket_uuid');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach (['care_appointments', 'care_prescriptions', 'care_investigation_requests', 'care_bills'] as $table) {
|
||||
Schema::table($table, function (Blueprint $blueprint) {
|
||||
$blueprint->dropIndex(['queue_ticket_uuid']);
|
||||
$blueprint->dropColumn(['queue_ticket_uuid', 'queue_ticket_number', 'queue_ticket_status']);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -85,7 +85,5 @@
|
||||
</div>
|
||||
|
||||
<div>{{ $appointments->links() }}</div>
|
||||
|
||||
@include('care.service-queues._panel')
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -15,23 +15,61 @@
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
@include('care.partials.queue-ops', [
|
||||
'queueIntegration' => $queueIntegration ?? null,
|
||||
'queueCallNextRoute' => 'care.bills.call-next',
|
||||
'queueCallNextParams' => [],
|
||||
'branchId' => $branchId ?? null,
|
||||
])
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr><th class="px-4 py-3">Invoice</th><th class="px-4 py-3">Patient</th><th class="px-4 py-3">Total</th><th class="px-4 py-3">Balance</th><th class="px-4 py-3">Status</th><th></th></tr>
|
||||
<tr>
|
||||
<th class="px-4 py-3">Ticket</th>
|
||||
<th class="px-4 py-3">Invoice</th>
|
||||
<th class="px-4 py-3">Patient</th>
|
||||
<th class="px-4 py-3">Total</th>
|
||||
<th class="px-4 py-3">Balance</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($bills as $bill)
|
||||
<tr>
|
||||
<td class="px-4 py-3">
|
||||
@if (! empty($queueIntegration['enabled']) && $bill->queue_ticket_number)
|
||||
@include('care.partials.queue-ticket', [
|
||||
'ticketNumber' => $bill->queue_ticket_number,
|
||||
'ticketStatus' => $bill->queue_ticket_status,
|
||||
])
|
||||
@else
|
||||
<span class="text-slate-400">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 font-mono text-xs">{{ $bill->invoice_number }}</td>
|
||||
<td class="px-4 py-3">{{ $bill->patient->fullName() }}</td>
|
||||
<td class="px-4 py-3">{{ $money($bill->total_minor) }}</td>
|
||||
<td class="px-4 py-3 {{ $bill->balance_minor > 0 ? 'text-amber-700' : '' }}">{{ $money($bill->balance_minor) }}</td>
|
||||
<td class="px-4 py-3">{{ $statuses[$bill->status] ?? $bill->status }}</td>
|
||||
<td class="px-4 py-3 text-right"><a href="{{ route('care.bills.show', $bill) }}" class="text-sky-600">View</a></td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@if (! empty($queueIntegration['enabled']) && $bill->queue_ticket_uuid && ($bill->queue_ticket_status ?? '') !== 'serving' && $bill->balance_minor > 0)
|
||||
<form method="POST" action="{{ route('care.bills.serve', $bill) }}">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg bg-sky-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-sky-700">Serve</button>
|
||||
</form>
|
||||
@endif
|
||||
<a href="{{ route('care.bills.show', $bill) }}" class="text-sky-600">View</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">No bills yet.</td></tr>
|
||||
<tr><td colspan="7" class="px-4 py-8 text-center text-slate-500">No bills yet.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -192,12 +192,6 @@
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if (! empty($serviceQueuePanel['enabled']))
|
||||
<div class="mt-6">
|
||||
@include('care.service-queues._panel')
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (! empty($specialtyModules))
|
||||
<section class="mt-6">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Specialty modules</h2>
|
||||
|
||||
@@ -29,21 +29,42 @@
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
@include('care.partials.queue-ops', [
|
||||
'queueIntegration' => $queueIntegration ?? null,
|
||||
'queueCallNextRoute' => 'care.lab.queue.call-next',
|
||||
'queueCallNextParams' => [],
|
||||
'branchId' => $branchId,
|
||||
])
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse ($queue as $item)
|
||||
<div class="flex items-center justify-between rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">{{ $item->patient->fullName() }} — {{ $item->investigationType->name }}</p>
|
||||
<p class="font-medium text-slate-900">
|
||||
@if (! empty($queueIntegration['enabled']) && $item->queue_ticket_number)
|
||||
@include('care.partials.queue-ticket', [
|
||||
'ticketNumber' => $item->queue_ticket_number,
|
||||
'ticketStatus' => $item->queue_ticket_status,
|
||||
])
|
||||
@endif
|
||||
{{ $item->patient->fullName() }} — {{ $item->investigationType->name }}
|
||||
</p>
|
||||
<p class="text-xs text-slate-500">{{ $statuses[$item->status] ?? $item->status }} · {{ $item->priority }} · {{ $item->created_at->diffForHumans() }}</p>
|
||||
</div>
|
||||
<a href="{{ route('care.lab.requests.show', $item) }}" class="text-sm text-sky-600">Open</a>
|
||||
<div class="flex items-center gap-2">
|
||||
@if (! empty($queueIntegration['enabled']) && $item->queue_ticket_uuid && ($item->queue_ticket_status ?? '') !== 'serving')
|
||||
<form method="POST" action="{{ route('care.lab.queue.serve', $item) }}">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg bg-sky-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-700">Serve</button>
|
||||
</form>
|
||||
@endif
|
||||
<a href="{{ route('care.lab.requests.show', $item) }}" class="text-sm text-sky-600">Open</a>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center text-sm text-slate-500">Queue is empty.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
@include('care.service-queues._panel')
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{{-- Inline Queue ops when Care↔Queue integration is enabled (not a bolted-on Service counter). --}}
|
||||
@php
|
||||
$qi = $queueIntegration ?? null;
|
||||
$callNextRoute = $queueCallNextRoute ?? null;
|
||||
$callNextParams = $queueCallNextParams ?? [];
|
||||
@endphp
|
||||
@if (! empty($qi['enabled']) && $callNextRoute)
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-sky-100 bg-sky-50/70 px-4 py-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-sky-700">Queue</p>
|
||||
<p class="text-sm text-slate-600">Call next, serve, and complete on this list — tickets are for this department only.</p>
|
||||
</div>
|
||||
<form method="POST" action="{{ route($callNextRoute, $callNextParams) }}">
|
||||
@csrf
|
||||
@foreach ($callNextParams as $key => $value)
|
||||
@if (is_scalar($value))
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
@if (! empty($branchId))
|
||||
<input type="hidden" name="branch_id" value="{{ $branchId }}">
|
||||
@endif
|
||||
<button type="submit" class="btn-primary text-sm">Call next</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,18 @@
|
||||
@php
|
||||
$status = $ticketStatus ?? null;
|
||||
$number = $ticketNumber ?? null;
|
||||
@endphp
|
||||
@if ($number)
|
||||
<span class="mr-2 inline-flex items-center gap-1.5">
|
||||
<span class="inline-flex h-7 min-w-[2.5rem] items-center justify-center rounded-md bg-sky-100 px-1.5 font-mono text-xs font-bold text-sky-800">{{ $number }}</span>
|
||||
@if ($status)
|
||||
<span @class([
|
||||
'rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
||||
'bg-amber-100 text-amber-800' => $status === 'waiting',
|
||||
'bg-indigo-100 text-indigo-800' => $status === 'called',
|
||||
'bg-emerald-100 text-emerald-800' => $status === 'serving',
|
||||
'bg-slate-100 text-slate-600' => ! in_array($status, ['waiting', 'called', 'serving'], true),
|
||||
])>{{ $status }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
@@ -1,12 +1,30 @@
|
||||
<x-app-layout title="Pharmacy queue">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Pharmacy queue</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Active prescriptions awaiting dispensing</p>
|
||||
|
||||
<div class="mt-4">
|
||||
@include('care.partials.queue-ops', [
|
||||
'queueIntegration' => $queueIntegration ?? null,
|
||||
'queueCallNextRoute' => 'care.prescriptions.call-next',
|
||||
'queueCallNextParams' => [],
|
||||
'branchId' => $branchId ?? null,
|
||||
])
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse ($queue as $rx)
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-medium text-slate-900">{{ $rx->patient->fullName() }}</p>
|
||||
<p class="font-medium text-slate-900">
|
||||
@if (! empty($queueIntegration['enabled']) && $rx->queue_ticket_number)
|
||||
@include('care.partials.queue-ticket', [
|
||||
'ticketNumber' => $rx->queue_ticket_number,
|
||||
'ticketStatus' => $rx->queue_ticket_status,
|
||||
])
|
||||
@endif
|
||||
{{ $rx->patient->fullName() }}
|
||||
</p>
|
||||
<p class="text-xs text-slate-500">{{ $rx->patient->patient_number }} · {{ $rx->visit->branch?->name }}</p>
|
||||
<ul class="mt-2 space-y-1 text-sm text-slate-700">
|
||||
@foreach ($rx->items as $item)
|
||||
@@ -14,6 +32,14 @@
|
||||
@endforeach
|
||||
</ul>
|
||||
@if ($canDispense)
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
@if (! empty($queueIntegration['enabled']) && $rx->queue_ticket_uuid && ($rx->queue_ticket_status ?? '') !== 'serving')
|
||||
<form method="POST" action="{{ route('care.prescriptions.serve', $rx) }}">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg bg-sky-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-700">Serve</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
<form method="POST" action="{{ route('care.prescriptions.dispense', $rx) }}" class="mt-4 border-t border-slate-100 pt-4">
|
||||
@csrf
|
||||
@php $allocIndex = 0; @endphp
|
||||
@@ -41,7 +67,9 @@
|
||||
@endif
|
||||
@endforeach
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button type="submit" class="rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-700">Dispense</button>
|
||||
<button type="submit" class="rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-700">
|
||||
{{ ! empty($queueIntegration['enabled']) ? 'Complete & dispense' : 'Dispense' }}
|
||||
</button>
|
||||
<a href="{{ route('care.prescriptions.show', $rx) }}" class="text-sm text-sky-600">View details</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -56,8 +84,4 @@
|
||||
<p class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center text-sm text-slate-500">No prescriptions in queue.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
@include('care.service-queues._panel')
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -31,15 +31,32 @@
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
</form>
|
||||
|
||||
@include('care.partials.queue-ops', [
|
||||
'queueIntegration' => $queueIntegration ?? null,
|
||||
'queueCallNextRoute' => 'care.queue.call-next',
|
||||
'queueCallNextParams' => [],
|
||||
'branchId' => $branchId,
|
||||
])
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Waiting ({{ $queue->count() }})</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
@forelse ($queue as $appointment)
|
||||
<div class="flex items-center justify-between rounded-xl border border-slate-100 bg-slate-50 p-4">
|
||||
<div @class([
|
||||
'flex items-center justify-between rounded-xl border p-4',
|
||||
'border-indigo-200 bg-indigo-50/60' => ($appointment->queue_ticket_status ?? null) === 'called',
|
||||
'border-emerald-200 bg-emerald-50' => ($appointment->queue_ticket_status ?? null) === 'serving',
|
||||
'border-slate-100 bg-slate-50' => ! in_array($appointment->queue_ticket_status ?? null, ['called', 'serving'], true),
|
||||
])>
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">
|
||||
@if ($appointment->queue_position)
|
||||
@if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_number)
|
||||
@include('care.partials.queue-ticket', [
|
||||
'ticketNumber' => $appointment->queue_ticket_number,
|
||||
'ticketStatus' => $appointment->queue_ticket_status,
|
||||
])
|
||||
@elseif ($appointment->queue_position)
|
||||
<span class="mr-2 inline-flex h-6 w-6 items-center justify-center rounded-full bg-sky-100 text-xs font-bold text-sky-700">{{ $appointment->queue_position }}</span>
|
||||
@endif
|
||||
{{ $appointment->patient->fullName() }}
|
||||
@@ -49,7 +66,9 @@
|
||||
@if ($canConsult)
|
||||
<form method="POST" action="{{ route('care.queue.start', $appointment) }}">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg bg-sky-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-700">Start</button>
|
||||
<button type="submit" class="rounded-lg bg-sky-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-700">
|
||||
{{ ! empty($queueIntegration['enabled']) && ($appointment->queue_ticket_status ?? '') === 'called' ? 'Serve & start' : 'Start' }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@@ -65,7 +84,15 @@
|
||||
@forelse ($inConsultation as $appointment)
|
||||
<div class="flex items-center justify-between rounded-xl border border-emerald-100 bg-emerald-50 p-4">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">{{ $appointment->patient->fullName() }}</p>
|
||||
<p class="font-medium text-slate-900">
|
||||
@if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_number)
|
||||
@include('care.partials.queue-ticket', [
|
||||
'ticketNumber' => $appointment->queue_ticket_number,
|
||||
'ticketStatus' => $appointment->queue_ticket_status,
|
||||
])
|
||||
@endif
|
||||
{{ $appointment->patient->fullName() }}
|
||||
</p>
|
||||
<p class="text-xs text-slate-500">{{ $appointment->practitioner?->name ?? 'Unassigned' }}</p>
|
||||
</div>
|
||||
@if ($appointment->consultation)
|
||||
@@ -78,7 +105,5 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@include('care.service-queues._panel')
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
</div>
|
||||
</x-settings.card>
|
||||
|
||||
<x-settings.card title="Integrations" description="Connect Ladill Queue for service counters on clinical, pharmacy, and lab pages. Requires Care Pro or Enterprise — and Care must also be enabled in Queue settings.">
|
||||
<x-settings.card title="Integrations" description="Connect Ladill Queue so existing Care lists (patient queue, pharmacy, lab, billing, specialties) issue ticket numbers and support Call next → Serve → Complete. Requires Care Pro or Enterprise — and Care must also be enabled in Queue settings.">
|
||||
@if (! empty($canUseQueueIntegration))
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" name="queue_integration_enabled" value="1" @checked(data_get($organization->settings, 'queue_integration_enabled'))>
|
||||
|
||||
@@ -42,6 +42,13 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@include('care.partials.queue-ops', [
|
||||
'queueIntegration' => $queueIntegration ?? null,
|
||||
'queueCallNextRoute' => 'care.specialty.call-next',
|
||||
'queueCallNextParams' => ['module' => $moduleKey],
|
||||
'branchId' => $branchId ?? null,
|
||||
])
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Waiting patients</h2>
|
||||
@@ -51,7 +58,15 @@
|
||||
@forelse ($waiting as $appointment)
|
||||
<div class="flex items-center justify-between rounded-xl border border-slate-100 bg-slate-50 px-3 py-3 text-sm">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">{{ $appointment->patient?->fullName() }}</p>
|
||||
<p class="font-medium text-slate-900">
|
||||
@if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_number)
|
||||
@include('care.partials.queue-ticket', [
|
||||
'ticketNumber' => $appointment->queue_ticket_number,
|
||||
'ticketStatus' => $appointment->queue_ticket_status,
|
||||
])
|
||||
@endif
|
||||
{{ $appointment->patient?->fullName() }}
|
||||
</p>
|
||||
<p class="text-xs text-slate-500">{{ $appointment->department?->name }} · {{ $appointment->practitioner?->name ?? 'Unassigned' }}</p>
|
||||
</div>
|
||||
<a href="{{ route('care.appointments.show', $appointment) }}" class="text-sky-600">View</a>
|
||||
@@ -61,7 +76,5 @@
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@include('care.service-queues._panel')
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -88,7 +88,9 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
|
||||
Route::post('/appointments/{appointment}/start-meet', [AppointmentMeetController::class, 'start'])->name('care.appointments.start-meet');
|
||||
|
||||
Route::get('/queue', [QueueController::class, 'index'])->name('care.queue.index');
|
||||
Route::post('/queue/call-next', [QueueController::class, 'callNext'])->name('care.queue.call-next');
|
||||
Route::post('/queue/{appointment}/start', [QueueController::class, 'start'])->name('care.queue.start');
|
||||
Route::post('/queue/{appointment}/complete-ticket', [QueueController::class, 'completeTicket'])->name('care.queue.complete-ticket');
|
||||
|
||||
Route::middleware('care.paid')->group(function () {
|
||||
Route::get('/service-queues', [ServiceQueueController::class, 'index'])->name('care.service-queues.index');
|
||||
@@ -96,9 +98,12 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
|
||||
Route::post('/service-queues/console/{counterUuid}/action', [ServiceQueueController::class, 'action'])->name('care.service-queues.action');
|
||||
|
||||
Route::get('/specialty/{module}', [SpecialtyModuleController::class, 'show'])->name('care.specialty.show');
|
||||
Route::post('/specialty/{module}/call-next', [SpecialtyModuleController::class, 'callNext'])->name('care.specialty.call-next');
|
||||
|
||||
Route::get('/lab/requests', [InvestigationController::class, 'index'])->name('care.lab.requests.index');
|
||||
Route::get('/lab/queue', [InvestigationController::class, 'queue'])->name('care.lab.queue.index');
|
||||
Route::post('/lab/queue/call-next', [InvestigationController::class, 'callNext'])->name('care.lab.queue.call-next');
|
||||
Route::post('/lab/queue/{investigation}/serve', [InvestigationController::class, 'serve'])->name('care.lab.queue.serve');
|
||||
Route::get('/lab/requests/{investigation}', [InvestigationController::class, 'show'])->name('care.lab.requests.show');
|
||||
Route::post('/consultations/{consultation}/investigations', [InvestigationController::class, 'requestFromConsultation'])->name('care.lab.requests.store');
|
||||
Route::post('/lab/requests/{investigation}/collect-sample', [InvestigationController::class, 'collectSample'])->name('care.lab.requests.collect-sample');
|
||||
@@ -116,6 +121,8 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
|
||||
|
||||
Route::get('/prescriptions', [PrescriptionController::class, 'index'])->name('care.prescriptions.index');
|
||||
Route::get('/prescriptions/queue', [PrescriptionController::class, 'queue'])->name('care.prescriptions.queue');
|
||||
Route::post('/prescriptions/queue/call-next', [PrescriptionController::class, 'callNext'])->name('care.prescriptions.call-next');
|
||||
Route::post('/prescriptions/{prescription}/serve', [PrescriptionController::class, 'serve'])->name('care.prescriptions.serve');
|
||||
Route::get('/consultations/{consultation}/prescriptions/create', [PrescriptionController::class, 'create'])->name('care.prescriptions.create');
|
||||
Route::post('/consultations/{consultation}/prescriptions', [PrescriptionController::class, 'store'])->name('care.prescriptions.store');
|
||||
Route::get('/prescriptions/{prescription}', [PrescriptionController::class, 'show'])->name('care.prescriptions.show');
|
||||
@@ -124,6 +131,8 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
|
||||
Route::post('/prescriptions/{prescription}/cancel', [PrescriptionController::class, 'cancel'])->name('care.prescriptions.cancel');
|
||||
|
||||
Route::get('/bills', [BillController::class, 'index'])->name('care.bills.index');
|
||||
Route::post('/bills/call-next', [BillController::class, 'callNext'])->name('care.bills.call-next');
|
||||
Route::post('/bills/{bill}/serve', [BillController::class, 'serve'])->name('care.bills.serve');
|
||||
Route::post('/visits/{visit}/bill', [BillController::class, 'generate'])->name('care.bills.generate');
|
||||
Route::get('/bills/payments/callback', [BillController::class, 'paymentCallback'])->name('care.bills.payments.callback');
|
||||
Route::get('/bills/{bill}', [BillController::class, 'show'])->name('care.bills.show');
|
||||
|
||||
@@ -8,9 +8,11 @@ use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Natural list UX: no bolted-on Service counter panels; Queue ops live on role lists when enabled.
|
||||
*/
|
||||
class CareNaturalQueueTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
@@ -45,6 +47,38 @@ class CareNaturalQueueTest extends TestCase
|
||||
'plan' => 'pro',
|
||||
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||
'queue_integration_enabled' => true,
|
||||
'department_queue_provisioning' => [
|
||||
'consultation' => [
|
||||
'queues' => [[
|
||||
'branch_id' => 1,
|
||||
'name' => 'Consultation',
|
||||
'active' => true,
|
||||
'synced' => true,
|
||||
'queue_uuid' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'counter_uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||
]],
|
||||
],
|
||||
'pharmacy' => [
|
||||
'queues' => [[
|
||||
'branch_id' => 1,
|
||||
'name' => 'Pharmacy',
|
||||
'active' => true,
|
||||
'synced' => true,
|
||||
'queue_uuid' => 'cccccccc-cccc-cccc-cccc-cccccccccccc',
|
||||
'counter_uuid' => 'dddddddd-dddd-dddd-dddd-dddddddddddd',
|
||||
]],
|
||||
],
|
||||
'laboratory' => [
|
||||
'queues' => [[
|
||||
'branch_id' => 1,
|
||||
'name' => 'Laboratory',
|
||||
'active' => true,
|
||||
'synced' => true,
|
||||
'queue_uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||
'counter_uuid' => 'ffffffff-ffff-ffff-ffff-ffffffffffff',
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -55,49 +89,23 @@ class CareNaturalQueueTest extends TestCase
|
||||
'role' => 'hospital_admin',
|
||||
]);
|
||||
|
||||
Branch::create([
|
||||
$branch = Branch::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Main',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function fakeQueueApi(): void
|
||||
{
|
||||
Http::fake(function (\Illuminate\Http\Client\Request $request) {
|
||||
$url = $request->url();
|
||||
if (str_contains($url, '/counters/') && str_contains($url, '/console')) {
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'counter' => ['name' => 'Reception desk', 'branch' => 'Main', 'status' => 'available'],
|
||||
'current_ticket' => null,
|
||||
'queues' => [['uuid' => '22222222-2222-2222-2222-222222222222', 'name' => 'Consultation']],
|
||||
'waiting_by_queue' => [],
|
||||
'transfer_queues' => [],
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
if (str_contains($url, '/counters')) {
|
||||
return Http::response([
|
||||
'data' => [[
|
||||
'uuid' => '11111111-1111-1111-1111-111111111111',
|
||||
'name' => 'Reception desk',
|
||||
'branch' => 'Main',
|
||||
'status' => 'available',
|
||||
'queues' => [['uuid' => '22222222-2222-2222-2222-222222222222', 'name' => 'Consultation']],
|
||||
]],
|
||||
], 200);
|
||||
}
|
||||
|
||||
return Http::response(['message' => 'unexpected '.$url], 500);
|
||||
});
|
||||
// Keep provisioning stubs aligned with the created branch id.
|
||||
$settings = $this->organization->settings;
|
||||
foreach (['consultation', 'pharmacy', 'laboratory'] as $ctx) {
|
||||
$settings['department_queue_provisioning'][$ctx]['queues'][0]['branch_id'] = $branch->id;
|
||||
}
|
||||
$this->organization->update(['settings' => $settings]);
|
||||
}
|
||||
|
||||
public function test_sidebar_does_not_show_service_queues_nav(): void
|
||||
{
|
||||
$this->fakeQueueApi();
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.dashboard'))
|
||||
->assertOk()
|
||||
@@ -111,89 +119,70 @@ class CareNaturalQueueTest extends TestCase
|
||||
->assertRedirect(route('care.queue.index'));
|
||||
}
|
||||
|
||||
public function test_clinical_queue_embeds_service_counter_panel(): void
|
||||
public function test_clinical_queue_shows_inline_ops_not_service_counter(): void
|
||||
{
|
||||
$this->fakeQueueApi();
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.queue.index'))
|
||||
->assertOk()
|
||||
->assertSee('Service counter')
|
||||
->assertSee('Reception desk')
|
||||
->assertDontSee('Service counter')
|
||||
->assertSee('Call next');
|
||||
}
|
||||
|
||||
public function test_pharmacist_sees_queue_panel_on_pharmacy_page(): void
|
||||
public function test_pharmacist_sees_call_next_on_pharmacy_page(): void
|
||||
{
|
||||
$this->fakeQueueApi();
|
||||
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'pharmacist']);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.prescriptions.queue'))
|
||||
->assertOk()
|
||||
->assertSee('Service counter');
|
||||
->assertDontSee('Service counter')
|
||||
->assertSee('Call next');
|
||||
}
|
||||
|
||||
public function test_lab_page_embeds_service_counter_when_integration_on(): void
|
||||
public function test_lab_page_shows_call_next_when_integration_on(): void
|
||||
{
|
||||
$this->fakeQueueApi();
|
||||
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'lab_technician']);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.lab.queue.index'))
|
||||
->assertOk()
|
||||
->assertSee('Service counter');
|
||||
->assertDontSee('Service counter')
|
||||
->assertSee('Call next');
|
||||
}
|
||||
|
||||
public function test_branch_scoped_member_panel_prefers_matching_branch_counter(): void
|
||||
public function test_integration_off_hides_queue_controls(): void
|
||||
{
|
||||
Http::fake(function (\Illuminate\Http\Client\Request $request) {
|
||||
$url = $request->url();
|
||||
if (str_contains($url, '/counters/') && str_contains($url, '/console')) {
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'counter' => ['name' => 'Main reception', 'branch' => 'Main'],
|
||||
'current_ticket' => null,
|
||||
'queues' => [['uuid' => 'cccccccc-cccc-cccc-cccc-cccccccccccc', 'name' => 'Consultation']],
|
||||
'waiting_by_queue' => [],
|
||||
'transfer_queues' => [],
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
if (str_contains($url, '/counters')) {
|
||||
return Http::response([
|
||||
'data' => [
|
||||
[
|
||||
'uuid' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'name' => 'Other site desk',
|
||||
'branch' => 'Other',
|
||||
'queues' => [['name' => 'Consultation']],
|
||||
],
|
||||
[
|
||||
'uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||
'name' => 'Main reception',
|
||||
'branch' => 'Main',
|
||||
'queues' => [['name' => 'Consultation']],
|
||||
],
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
return Http::response(['message' => 'unexpected '.$url], 500);
|
||||
});
|
||||
|
||||
$branch = Branch::query()->first();
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update([
|
||||
'role' => 'receptionist',
|
||||
'branch_id' => $branch->id,
|
||||
]);
|
||||
$settings = $this->organization->settings;
|
||||
$settings['queue_integration_enabled'] = false;
|
||||
$this->organization->update(['settings' => $settings]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.queue.index'))
|
||||
->assertOk()
|
||||
->assertSee('Main reception')
|
||||
->assertDontSee('Other site desk');
|
||||
->assertDontSee('Call next')
|
||||
->assertDontSee('Service counter');
|
||||
}
|
||||
|
||||
public function test_doctor_call_next_uses_consultation_queue_not_reception(): void
|
||||
{
|
||||
\Illuminate\Support\Facades\Http::fake([
|
||||
'*/queues/*/call-next*' => \Illuminate\Support\Facades\Http::response([
|
||||
'data' => [
|
||||
'uuid' => '77777777-7777-7777-7777-777777777777',
|
||||
'ticket_number' => 'C001',
|
||||
'status' => 'called',
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$branchId = Branch::query()->firstOrFail()->id;
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->post(route('care.queue.call-next'), ['branch_id' => $branchId])
|
||||
->assertRedirect();
|
||||
|
||||
\Illuminate\Support\Facades\Http::assertSent(function ($request) {
|
||||
return str_contains($request->url(), '/queues/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/call-next');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Patient;
|
||||
use App\Models\User;
|
||||
use App\Services\Care\AppointmentService;
|
||||
use App\Services\Care\CareQueueContexts;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CareQueueBridgeTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $owner;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected Branch $branch;
|
||||
|
||||
protected Patient $patient;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
config([
|
||||
'care.queue.api_url' => 'https://queue.test/api/v1',
|
||||
'care.queue.api_key' => 'care-test-key',
|
||||
]);
|
||||
|
||||
$this->owner = User::create([
|
||||
'public_id' => 'bridge-owner',
|
||||
'name' => 'Owner',
|
||||
'email' => 'bridge@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'name' => 'Bridge Clinic',
|
||||
'slug' => 'bridge-clinic',
|
||||
'settings' => [
|
||||
'onboarded' => true,
|
||||
'facility_type' => 'clinic',
|
||||
'plan' => 'pro',
|
||||
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||
'queue_integration_enabled' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->owner->public_id,
|
||||
'role' => 'hospital_admin',
|
||||
]);
|
||||
|
||||
$this->branch = Branch::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Main',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$settings = $this->organization->settings;
|
||||
$settings['department_queue_provisioning'] = [
|
||||
CareQueueContexts::CONSULTATION => [
|
||||
'queues' => [[
|
||||
'branch_id' => $this->branch->id,
|
||||
'branch_name' => 'Main',
|
||||
'name' => 'Consultation',
|
||||
'prefix' => 'C',
|
||||
'active' => true,
|
||||
'synced' => true,
|
||||
'queue_uuid' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'counter_uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||
'queue_external_key' => CareQueueContexts::queueExternalKey(CareQueueContexts::CONSULTATION, $this->branch->id),
|
||||
'counter_external_key' => CareQueueContexts::counterExternalKey(CareQueueContexts::CONSULTATION, $this->branch->id),
|
||||
]],
|
||||
],
|
||||
];
|
||||
$this->organization->update(['settings' => $settings]);
|
||||
|
||||
$this->patient = Patient::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_number' => 'P-100',
|
||||
'first_name' => 'Ada',
|
||||
'last_name' => 'Lovelace',
|
||||
'gender' => 'female',
|
||||
'date_of_birth' => '1990-01-01',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_check_in_issues_consultation_ticket_when_integration_on(): void
|
||||
{
|
||||
Http::fake(function (\Illuminate\Http\Client\Request $request) {
|
||||
if (str_contains($request->url(), '/tickets') && $request->method() === 'POST') {
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'uuid' => 'cccccccc-cccc-cccc-cccc-cccccccccccc',
|
||||
'ticket_number' => 'C001',
|
||||
'status' => 'waiting',
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
return Http::response(['message' => 'unexpected '.$request->url()], 500);
|
||||
});
|
||||
|
||||
$appointment = Appointment::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $this->patient->id,
|
||||
'type' => Appointment::TYPE_SCHEDULED,
|
||||
'status' => Appointment::STATUS_SCHEDULED,
|
||||
'scheduled_at' => now()->addHour(),
|
||||
]);
|
||||
|
||||
$result = app(AppointmentService::class)->checkIn($appointment, $this->owner->public_id, $this->owner->public_id);
|
||||
|
||||
$this->assertSame('C001', $result->queue_ticket_number);
|
||||
$this->assertSame('waiting', $result->queue_ticket_status);
|
||||
$this->assertSame('cccccccc-cccc-cccc-cccc-cccccccccccc', $result->queue_ticket_uuid);
|
||||
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/tickets')
|
||||
&& ($request['service_queue_id'] ?? null) === 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
&& ($request['customer_name'] ?? null) === 'Ada Lovelace');
|
||||
}
|
||||
|
||||
public function test_check_in_skips_ticket_when_integration_off(): void
|
||||
{
|
||||
Http::fake();
|
||||
|
||||
$settings = $this->organization->settings;
|
||||
$settings['queue_integration_enabled'] = false;
|
||||
$this->organization->update(['settings' => $settings]);
|
||||
|
||||
$appointment = Appointment::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $this->patient->id,
|
||||
'type' => Appointment::TYPE_SCHEDULED,
|
||||
'status' => Appointment::STATUS_SCHEDULED,
|
||||
'scheduled_at' => now()->addHour(),
|
||||
]);
|
||||
|
||||
$result = app(AppointmentService::class)->checkIn(
|
||||
$appointment->fresh(['organization', 'patient']),
|
||||
$this->owner->public_id,
|
||||
$this->owner->public_id,
|
||||
);
|
||||
|
||||
$this->assertNull($result->queue_ticket_uuid);
|
||||
Http::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_waiting_list_shows_ticket_number_when_integration_on(): void
|
||||
{
|
||||
Appointment::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $this->patient->id,
|
||||
'type' => Appointment::TYPE_WALK_IN,
|
||||
'status' => Appointment::STATUS_WAITING,
|
||||
'scheduled_at' => now(),
|
||||
'waiting_at' => now(),
|
||||
'queue_position' => 1,
|
||||
'queue_ticket_uuid' => 'dddddddd-dddd-dddd-dddd-dddddddddddd',
|
||||
'queue_ticket_number' => 'C042',
|
||||
'queue_ticket_status' => 'waiting',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.queue.index'))
|
||||
->assertOk()
|
||||
->assertSee('C042')
|
||||
->assertSee('Call next')
|
||||
->assertSee('Serve');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Patient;
|
||||
use App\Models\Prescription;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Care\AppointmentService;
|
||||
use App\Services\Care\CareQueueBridge;
|
||||
use App\Services\Care\CareQueueContexts;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CareQueueWorkflowTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $owner;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected Branch $branch;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
config([
|
||||
'care.queue.api_url' => 'https://queue.test/api/v1',
|
||||
'care.queue.api_key' => 'care-test-key',
|
||||
]);
|
||||
|
||||
$this->owner = User::create([
|
||||
'public_id' => 'care-queue-workflow-owner',
|
||||
'name' => 'Owner',
|
||||
'email' => 'queue-workflow@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'name' => 'Workflow Clinic',
|
||||
'slug' => 'workflow-clinic',
|
||||
'settings' => [
|
||||
'onboarded' => true,
|
||||
'facility_type' => 'clinic',
|
||||
'plan' => 'pro',
|
||||
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||
'queue_integration_enabled' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->owner->public_id,
|
||||
'role' => 'hospital_admin',
|
||||
]);
|
||||
|
||||
$this->branch = Branch::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Main',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function fakeQueueApi(): void
|
||||
{
|
||||
$consultationQueue = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
||||
$pharmacyQueue = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
|
||||
$consultationCounter = 'cccccccc-cccc-cccc-cccc-cccccccccccc';
|
||||
$pharmacyCounter = 'dddddddd-dddd-dddd-dddd-dddddddddddd';
|
||||
$seq = ['C' => 0, 'P' => 0];
|
||||
|
||||
Http::fake(function (\Illuminate\Http\Client\Request $request) use (
|
||||
$consultationQueue,
|
||||
$pharmacyQueue,
|
||||
$consultationCounter,
|
||||
$pharmacyCounter,
|
||||
&$seq,
|
||||
) {
|
||||
$url = $request->url();
|
||||
$method = $request->method();
|
||||
|
||||
if (str_contains($url, '/integrations/status')) {
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'provisioned' => true,
|
||||
'integrations' => ['care' => true],
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
if ($method === 'POST' && str_contains($url, '/branches')) {
|
||||
return Http::response(['data' => ['id' => 1, 'name' => 'Main']], 201);
|
||||
}
|
||||
|
||||
if ($method === 'POST' && str_contains($url, '/queues') && ! str_contains($url, 'call-next')) {
|
||||
$payload = $request->data();
|
||||
$key = (string) ($payload['external_key'] ?? '');
|
||||
$isPharmacy = str_contains($key, 'pharmacy');
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'uuid' => $isPharmacy ? $pharmacyQueue : $consultationQueue,
|
||||
'name' => $payload['name'] ?? 'Queue',
|
||||
'prefix' => $payload['prefix'] ?? 'X',
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
if ($method === 'POST' && str_contains($url, '/counters')) {
|
||||
$payload = $request->data();
|
||||
$key = (string) ($payload['external_key'] ?? '');
|
||||
$isPharmacy = str_contains($key, 'pharmacy');
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'uuid' => $isPharmacy ? $pharmacyCounter : $consultationCounter,
|
||||
'name' => $payload['name'] ?? 'Counter',
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
if ($method === 'POST' && preg_match('#/tickets$#', parse_url($url, PHP_URL_PATH) ?? '')) {
|
||||
$payload = $request->data();
|
||||
$queueId = (string) ($payload['service_queue_id'] ?? '');
|
||||
$prefix = $queueId === $pharmacyQueue ? 'P' : 'C';
|
||||
$seq[$prefix]++;
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'ticket_number' => $prefix.sprintf('%03d', $seq[$prefix]),
|
||||
'status' => 'waiting',
|
||||
'customer_name' => $payload['customer_name'] ?? null,
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
if ($method === 'POST' && str_contains($url, '/call-next')) {
|
||||
$path = parse_url($url, PHP_URL_PATH) ?? '';
|
||||
$isPharmacy = str_contains($path, $pharmacyQueue);
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'ticket_number' => $isPharmacy ? 'P001' : 'C001',
|
||||
'status' => 'called',
|
||||
'customer_name' => 'Called Patient',
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
if ($method === 'POST' && str_contains($url, '/tickets/') && str_contains($url, '/action')) {
|
||||
$payload = $request->data();
|
||||
return Http::response([
|
||||
'data' => [
|
||||
'uuid' => basename(dirname(parse_url($url, PHP_URL_PATH) ?? '/x/x')),
|
||||
'ticket_number' => 'C001',
|
||||
'status' => $payload['action'] === 'complete' ? 'completed' : ($payload['action'] === 'start' ? 'serving' : 'called'),
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
return Http::response(['message' => 'unexpected '.$method.' '.$url], 500);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_integration_off_does_not_issue_tickets_on_check_in(): void
|
||||
{
|
||||
$settings = $this->organization->settings;
|
||||
$settings['queue_integration_enabled'] = false;
|
||||
$this->organization->update(['settings' => $settings]);
|
||||
|
||||
Http::fake();
|
||||
|
||||
$patient = Patient::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_number' => 'P-1',
|
||||
'first_name' => 'Ada',
|
||||
'last_name' => 'Lovelace',
|
||||
]);
|
||||
|
||||
$appointment = Appointment::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $patient->id,
|
||||
'type' => Appointment::TYPE_SCHEDULED,
|
||||
'status' => Appointment::STATUS_SCHEDULED,
|
||||
'scheduled_at' => now(),
|
||||
]);
|
||||
|
||||
app(AppointmentService::class)->checkIn($appointment, $this->owner->public_id, $this->owner->public_id);
|
||||
|
||||
$appointment->refresh();
|
||||
$this->assertNull($appointment->queue_ticket_uuid);
|
||||
$this->assertNull($appointment->queue_ticket_number);
|
||||
Http::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_integration_on_issues_consultation_ticket_on_check_in(): void
|
||||
{
|
||||
$this->fakeQueueApi();
|
||||
|
||||
$patient = Patient::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_number' => 'P-2',
|
||||
'first_name' => 'Grace',
|
||||
'last_name' => 'Hopper',
|
||||
]);
|
||||
|
||||
$appointment = Appointment::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $patient->id,
|
||||
'type' => Appointment::TYPE_SCHEDULED,
|
||||
'status' => Appointment::STATUS_SCHEDULED,
|
||||
'scheduled_at' => now(),
|
||||
]);
|
||||
|
||||
app(AppointmentService::class)->checkIn($appointment, $this->owner->public_id, $this->owner->public_id);
|
||||
|
||||
$appointment->refresh();
|
||||
$this->assertNotNull($appointment->queue_ticket_uuid);
|
||||
$this->assertSame('C001', $appointment->queue_ticket_number);
|
||||
$this->assertSame('waiting', $appointment->queue_ticket_status);
|
||||
}
|
||||
|
||||
public function test_pharmacy_call_next_uses_pharmacy_queue_not_reception(): void
|
||||
{
|
||||
$this->fakeQueueApi();
|
||||
|
||||
$this->organization->update([
|
||||
'settings' => array_merge($this->organization->settings ?? [], [
|
||||
'department_queue_provisioning' => [
|
||||
CareQueueContexts::PHARMACY => [
|
||||
'queues' => [[
|
||||
'branch_id' => $this->branch->id,
|
||||
'branch_name' => 'Main',
|
||||
'name' => 'Pharmacy',
|
||||
'prefix' => 'P',
|
||||
'active' => true,
|
||||
'synced' => true,
|
||||
'queue_uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||
'counter_uuid' => 'dddddddd-dddd-dddd-dddd-dddddddddddd',
|
||||
'queue_external_key' => CareQueueContexts::queueExternalKey(CareQueueContexts::PHARMACY, $this->branch->id),
|
||||
'counter_external_key' => CareQueueContexts::counterExternalKey(CareQueueContexts::PHARMACY, $this->branch->id),
|
||||
]],
|
||||
],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'pharmacist']);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->post(route('care.prescriptions.call-next'), ['branch_id' => $this->branch->id])
|
||||
->assertRedirect();
|
||||
|
||||
Http::assertSent(function (\Illuminate\Http\Client\Request $request) {
|
||||
return $request->method() === 'POST'
|
||||
&& str_contains($request->url(), '/queues/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/call-next');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_complete_advances_ticket_status_on_appointment(): void
|
||||
{
|
||||
$this->fakeQueueApi();
|
||||
|
||||
$patient = Patient::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_number' => 'P-3',
|
||||
'first_name' => 'Alan',
|
||||
'last_name' => 'Turing',
|
||||
]);
|
||||
|
||||
$appointment = Appointment::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $patient->id,
|
||||
'type' => Appointment::TYPE_WALK_IN,
|
||||
'status' => Appointment::STATUS_WAITING,
|
||||
'queue_ticket_uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||
'queue_ticket_number' => 'C009',
|
||||
'queue_ticket_status' => 'serving',
|
||||
'scheduled_at' => now(),
|
||||
]);
|
||||
|
||||
app(CareQueueBridge::class)->complete($this->organization, $appointment);
|
||||
$appointment->refresh();
|
||||
|
||||
$this->assertSame('completed', $appointment->queue_ticket_status);
|
||||
}
|
||||
|
||||
public function test_patient_queue_shows_call_next_not_service_counter_panel(): void
|
||||
{
|
||||
$this->fakeQueueApi();
|
||||
|
||||
$this->organization->update([
|
||||
'settings' => array_merge($this->organization->settings ?? [], [
|
||||
'department_queue_provisioning' => [
|
||||
CareQueueContexts::CONSULTATION => [
|
||||
'queues' => [[
|
||||
'branch_id' => $this->branch->id,
|
||||
'branch_name' => 'Main',
|
||||
'name' => 'Consultation',
|
||||
'prefix' => 'C',
|
||||
'active' => true,
|
||||
'synced' => true,
|
||||
'queue_uuid' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'counter_uuid' => 'cccccccc-cccc-cccc-cccc-cccccccccccc',
|
||||
]],
|
||||
],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.queue.index'))
|
||||
->assertOk()
|
||||
->assertDontSee('Service counter')
|
||||
->assertSee('Call next')
|
||||
->assertSee('tickets are for this department only', false);
|
||||
}
|
||||
|
||||
public function test_integration_off_hides_queue_ops_on_pharmacy_page(): void
|
||||
{
|
||||
$settings = $this->organization->settings;
|
||||
$settings['queue_integration_enabled'] = false;
|
||||
$this->organization->update(['settings' => $settings]);
|
||||
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'pharmacist']);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.prescriptions.queue'))
|
||||
->assertOk()
|
||||
->assertDontSee('Call next')
|
||||
->assertDontSee('Service counter');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user