Add Meet video visit scheduling for Care appointments.
Deploy Ladill Care / deploy (push) Successful in 38s
Deploy Ladill Care / deploy (push) Successful in 38s
Schedule and start video visits via the Meet service API, persist join links on appointments, and propagate the shared copy-button component. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -40,6 +40,10 @@ BILLING_API_KEY_CARE=
|
||||
IDENTITY_API_URL=https://ladill.com/api
|
||||
IDENTITY_API_KEY_CARE=
|
||||
|
||||
# Ladill Meet (video visits)
|
||||
MEET_API_URL=https://meet.ladill.com/api/service/v1
|
||||
MEET_API_KEY_CARE=
|
||||
|
||||
# Afia in-app assistant (uses platform relay when AFIA_API_KEY is empty)
|
||||
AFIA_ENABLED=true
|
||||
AFIA_PRODUCT=care
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Appointment;
|
||||
use App\Services\Care\AuditLogger;
|
||||
use App\Services\Meet\MeetClient;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AppointmentMeetController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function schedule(Request $request, Appointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$this->authorizeOwner($request, $appointment);
|
||||
|
||||
if ($appointment->meet_join_url) {
|
||||
return redirect()->route('care.appointments.show', $appointment)
|
||||
->with('success', 'Video visit is already scheduled.');
|
||||
}
|
||||
|
||||
$appointment->loadMissing('patient');
|
||||
$patient = $appointment->patient;
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$response = MeetClient::for($owner)->createRoom([
|
||||
'title' => 'Video visit — '.$patient->fullName(),
|
||||
'description' => $appointment->reason,
|
||||
'scheduled_at' => ($appointment->scheduled_at ?? now()->addHour())->toIso8601String(),
|
||||
'duration_minutes' => 30,
|
||||
'branch_id' => $appointment->branch_id,
|
||||
'source' => [
|
||||
'app' => 'care',
|
||||
'entity_type' => 'appointment',
|
||||
'entity_id' => $appointment->uuid,
|
||||
],
|
||||
'invite_emails' => array_values(array_filter([$patient->email])),
|
||||
]);
|
||||
|
||||
$appointment->update([
|
||||
'meet_room_uuid' => data_get($response, 'room.uuid'),
|
||||
'meet_join_url' => data_get($response, 'join_url'),
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'appointment.meet_scheduled', $appointment->organization_id, $owner, Appointment::class, $appointment->id);
|
||||
|
||||
return redirect()->route('care.appointments.show', $appointment)
|
||||
->with('success', 'Video visit scheduled. The patient will receive a join link.');
|
||||
}
|
||||
|
||||
public function start(Request $request, Appointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->authorizeOwner($request, $appointment);
|
||||
|
||||
$owner = $this->ownerRef($request);
|
||||
$appointment->loadMissing('patient');
|
||||
|
||||
if (! $appointment->meet_room_uuid) {
|
||||
$response = MeetClient::for($owner)->createRoom([
|
||||
'title' => 'Video visit — '.$appointment->patient->fullName(),
|
||||
'description' => $appointment->reason,
|
||||
'source' => [
|
||||
'app' => 'care',
|
||||
'entity_type' => 'appointment',
|
||||
'entity_id' => $appointment->uuid,
|
||||
],
|
||||
'invite_emails' => array_values(array_filter([$appointment->patient->email])),
|
||||
]);
|
||||
|
||||
$appointment->update([
|
||||
'meet_room_uuid' => data_get($response, 'room.uuid'),
|
||||
'meet_join_url' => data_get($response, 'join_url'),
|
||||
]);
|
||||
}
|
||||
|
||||
$start = MeetClient::for($owner)->startRoom((string) $appointment->meet_room_uuid, $owner);
|
||||
|
||||
return redirect()->away((string) (data_get($start, 'join_url') ?: $appointment->meet_join_url));
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,8 @@ class Appointment extends Model
|
||||
'uuid', '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', 'created_by',
|
||||
'completed_at', 'cancelled_at', 'queue_position', 'reason', 'notes',
|
||||
'meet_room_uuid', 'meet_join_url', 'created_by',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Meet;
|
||||
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* Client for the Ladill Meet service API — schedule rooms linked to Care records.
|
||||
*/
|
||||
class MeetClient
|
||||
{
|
||||
public function __construct(private readonly string $ownerRef) {}
|
||||
|
||||
public static function for(string $ownerRef): self
|
||||
{
|
||||
return new self($ownerRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createRoom(array $data): array
|
||||
{
|
||||
return $this->post('rooms', array_merge($data, [
|
||||
'owner_ref' => $this->ownerRef,
|
||||
'host_user_ref' => $data['host_user_ref'] ?? $this->ownerRef,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function startRoom(string $roomUuid, ?string $hostUserRef = null): array
|
||||
{
|
||||
return $this->post("rooms/{$roomUuid}/start", [
|
||||
'host_user_ref' => $hostUserRef ?? $this->ownerRef,
|
||||
]);
|
||||
}
|
||||
|
||||
private function client(): PendingRequest
|
||||
{
|
||||
return Http::baseUrl((string) config('meet.url'))
|
||||
->withToken((string) config('meet.key'))
|
||||
->acceptJson()
|
||||
->asJson()
|
||||
->connectTimeout(10)
|
||||
->timeout(20);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function post(string $path, array $data): array
|
||||
{
|
||||
try {
|
||||
$response = $this->client()->post($path, $data);
|
||||
} catch (ConnectionException) {
|
||||
throw ValidationException::withMessages([
|
||||
'meet' => ['Could not reach the Meet service. Please try again in a moment.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($response->status() === 422) {
|
||||
throw ValidationException::withMessages(
|
||||
$response->json('errors') ?: ['meet' => [$response->json('message') ?: 'Request failed.']]
|
||||
);
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
abort($response->status() === 404 ? 404 : 502, 'Meet service error.');
|
||||
}
|
||||
|
||||
return (array) $response->json();
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ return [
|
||||
'appointment.completed' => 'Appointment completed',
|
||||
'appointment.cancelled' => 'Appointment cancelled',
|
||||
'appointment.no_show' => 'Patient marked no-show',
|
||||
'appointment.meet_scheduled' => 'Video visit scheduled',
|
||||
'visit.checked_in' => 'Visit opened',
|
||||
'visit.completed' => 'Visit completed',
|
||||
'consultation.started' => 'Consultation started',
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Ladill Meet service
|
||||
|--------------------------------------------------------------------------
|
||||
| Video meetings at meet.ladill.com. Care schedules video visits via the
|
||||
| service API (App\Services\Meet\MeetClient).
|
||||
*/
|
||||
|
||||
'url' => rtrim((string) env('MEET_API_URL', 'https://meet.ladill.com/api/service/v1'), '/'),
|
||||
'key' => env('MEET_API_KEY_CARE'),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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
|
||||
{
|
||||
Schema::table('care_appointments', function (Blueprint $table) {
|
||||
$table->uuid('meet_room_uuid')->nullable()->after('notes');
|
||||
$table->string('meet_join_url', 2048)->nullable()->after('meet_room_uuid');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('care_appointments', function (Blueprint $table) {
|
||||
$table->dropColumn(['meet_room_uuid', 'meet_join_url']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import Alpine from 'alpinejs';
|
||||
import { registerLadillClipboard } from './ladill-clipboard';
|
||||
import collapse from '@alpinejs/collapse';
|
||||
|
||||
Alpine.plugin(collapse);
|
||||
registerLadillClipboard(Alpine);
|
||||
|
||||
// In-app notification bell + dropdown.
|
||||
Alpine.data('notificationDropdown', (config = {}) => ({
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
const COPY_FEEDBACK_MS = 2000;
|
||||
|
||||
export async function writeClipboardText(text) {
|
||||
const value = String(text ?? '');
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// fall through to legacy copy
|
||||
}
|
||||
|
||||
try {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = value;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
|
||||
return ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerLadillClipboard(Alpine) {
|
||||
Alpine.data('copyButton', (text = '') => ({
|
||||
copied: false,
|
||||
text: text ?? '',
|
||||
resetTimer: null,
|
||||
async copy() {
|
||||
const ok = await writeClipboardText(this.text);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.copied = true;
|
||||
|
||||
if (this.resetTimer) {
|
||||
clearTimeout(this.resetTimer);
|
||||
}
|
||||
|
||||
this.resetTimer = setTimeout(() => {
|
||||
this.copied = false;
|
||||
}, COPY_FEEDBACK_MS);
|
||||
},
|
||||
}));
|
||||
}
|
||||
@@ -25,9 +25,27 @@
|
||||
<button type="submit" class="btn-primary">Start consultation</button>
|
||||
</form>
|
||||
@endif
|
||||
@if ($canConsult && in_array($appointment->status, [\App\Models\Appointment::STATUS_WAITING, \App\Models\Appointment::STATUS_CHECKED_IN, \App\Models\Appointment::STATUS_IN_CONSULTATION], true))
|
||||
@if ($appointment->meet_join_url)
|
||||
<a href="{{ $appointment->meet_join_url }}" target="_blank" rel="noopener" class="btn-primary">Join video visit</a>
|
||||
@else
|
||||
<form method="POST" action="{{ route('care.appointments.start-meet', $appointment) }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn-primary">Start video visit</button>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
@if ($canManage && $appointment->status === \App\Models\Appointment::STATUS_SCHEDULED && ! $appointment->meet_join_url)
|
||||
<form method="POST" action="{{ route('care.appointments.schedule-meet', $appointment) }}">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg border border-sky-200 px-4 py-2 text-sm text-sky-700 hover:bg-sky-50">Schedule video visit</button>
|
||||
</form>
|
||||
@endif
|
||||
@if ($canManage && in_array($appointment->status, [\App\Models\Appointment::STATUS_WAITING, \App\Models\Appointment::STATUS_CHECKED_IN], true))
|
||||
@if ($appointment->consultation)
|
||||
<a href="{{ route('care.consultations.show', $appointment->consultation) }}" class="rounded-lg border border-sky-200 px-4 py-2 text-sm text-sky-700 hover:bg-sky-50">View consultation</a>
|
||||
@endif
|
||||
@endif
|
||||
@if ($canManage && ! in_array($appointment->status, [\App\Models\Appointment::STATUS_COMPLETED, \App\Models\Appointment::STATUS_CANCELLED], true))
|
||||
<form method="POST" action="{{ route('care.appointments.cancel', $appointment) }}" onsubmit="return confirm('Cancel this appointment?')">
|
||||
@csrf
|
||||
@@ -45,6 +63,9 @@
|
||||
<div><dt class="text-slate-500">Branch</dt><dd class="font-medium">{{ $appointment->branch?->name ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Department</dt><dd class="font-medium">{{ $appointment->department?->name ?? '—' }}</dd></div>
|
||||
<div><dt class="text-slate-500">Type</dt><dd class="font-medium capitalize">{{ str_replace('_', ' ', $appointment->type) }}</dd></div>
|
||||
@if ($appointment->meet_join_url)
|
||||
<div><dt class="text-slate-500">Video visit</dt><dd class="font-medium flex flex-wrap items-center gap-2"><a href="{{ $appointment->meet_join_url }}" target="_blank" rel="noopener" class="text-sky-600">Join link</a><x-copy-button :text="$appointment->meet_join_url" icon copied-class="text-emerald-600" class="rounded p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600" /></dd></div>
|
||||
@endif
|
||||
<div><dt class="text-slate-500">Reason</dt><dd class="font-medium">{{ $appointment->reason ?? '—' }}</dd></div>
|
||||
@if ($appointment->notes)
|
||||
<div><dt class="text-slate-500">Notes</dt><dd class="font-medium">{{ $appointment->notes }}</dd></div>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
@props([
|
||||
'text' => '',
|
||||
'label' => 'Copy',
|
||||
'copiedLabel' => 'Copied!',
|
||||
'icon' => false,
|
||||
'copiedClass' => '',
|
||||
])
|
||||
|
||||
<button
|
||||
type="button"
|
||||
x-data="copyButton(@js($text))"
|
||||
@click="copy()"
|
||||
:title="copied ? @js($copiedLabel) : @js($icon ? 'Copy' : $label)"
|
||||
:class="copied ? @js($copiedClass) : ''"
|
||||
{{ $attributes->class($icon ? 'inline-flex shrink-0 items-center justify-center' : '') }}
|
||||
>
|
||||
@if ($icon)
|
||||
<svg x-show="!copied" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<svg x-show="copied" x-cloak class="h-4 w-4 text-emerald-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
|
||||
</svg>
|
||||
@elseif ($slot->isNotEmpty())
|
||||
{{ $slot }}
|
||||
@else
|
||||
<span x-text="copied ? @js($copiedLabel) : @js($label)"></span>
|
||||
@endif
|
||||
</button>
|
||||
@@ -5,6 +5,7 @@ use App\Http\Controllers\Care\AiController;
|
||||
use App\Http\Controllers\NotificationController;
|
||||
use App\Http\Controllers\Care\AuditLogController;
|
||||
use App\Http\Controllers\Care\AppointmentController;
|
||||
use App\Http\Controllers\Care\AppointmentMeetController;
|
||||
use App\Http\Controllers\Care\BillController;
|
||||
use App\Http\Controllers\Care\BranchController;
|
||||
use App\Http\Controllers\Care\ConsultationController;
|
||||
@@ -69,6 +70,8 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
|
||||
Route::post('/appointments/{appointment}/check-in', [AppointmentController::class, 'checkIn'])->name('care.appointments.check-in');
|
||||
Route::post('/appointments/{appointment}/cancel', [AppointmentController::class, 'cancel'])->name('care.appointments.cancel');
|
||||
Route::post('/appointments/{appointment}/no-show', [AppointmentController::class, 'noShow'])->name('care.appointments.no-show');
|
||||
Route::post('/appointments/{appointment}/schedule-meet', [AppointmentMeetController::class, 'schedule'])->name('care.appointments.schedule-meet');
|
||||
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/{appointment}/start', [QueueController::class, 'start'])->name('care.queue.start');
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Department;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Patient;
|
||||
use App\Models\Practitioner;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CareAppointmentMeetTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected Branch $branch;
|
||||
|
||||
protected Patient $patient;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
config([
|
||||
'meet.url' => 'https://meet.test/api/service/v1',
|
||||
'meet.key' => 'test-care-key',
|
||||
]);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'test-user-meet',
|
||||
'name' => 'Test User',
|
||||
'email' => 'meet@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Test Clinic',
|
||||
'slug' => 'test-clinic-meet',
|
||||
'timezone' => 'UTC',
|
||||
'settings' => ['onboarded' => true],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'receptionist',
|
||||
]);
|
||||
|
||||
$this->branch = Branch::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'name' => 'Main Branch',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
Department::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'name' => 'General Outpatient',
|
||||
'type' => 'outpatient',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->patient = Patient::create([
|
||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_number' => 'LC-2026-00099',
|
||||
'first_name' => 'Ama',
|
||||
'last_name' => 'Mensah',
|
||||
'email' => 'ama@example.com',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_can_schedule_video_visit_for_appointment(): void
|
||||
{
|
||||
Http::fake([
|
||||
'meet.test/*' => Http::response([
|
||||
'room' => ['uuid' => 'room-uuid-1'],
|
||||
'join_url' => 'https://meet.test/join/abc',
|
||||
], 201),
|
||||
]);
|
||||
|
||||
$appointment = $this->createAppointment();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.appointments.schedule-meet', $appointment))
|
||||
->assertRedirect(route('care.appointments.show', $appointment));
|
||||
|
||||
$appointment->refresh();
|
||||
$this->assertSame('room-uuid-1', $appointment->meet_room_uuid);
|
||||
$this->assertSame('https://meet.test/join/abc', $appointment->meet_join_url);
|
||||
$this->assertDatabaseHas('care_audit_logs', ['action' => 'appointment.meet_scheduled']);
|
||||
}
|
||||
|
||||
public function test_can_start_instant_video_visit(): void
|
||||
{
|
||||
Http::fake([
|
||||
'meet.test/*' => Http::sequence()
|
||||
->push([
|
||||
'room' => ['uuid' => 'room-uuid-2'],
|
||||
'join_url' => 'https://meet.test/join/live',
|
||||
], 201)
|
||||
->push([
|
||||
'join_url' => 'https://meet.test/join/live-now',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
Member::where('user_ref', $this->user->public_id)->update(['role' => 'doctor']);
|
||||
|
||||
$appointment = $this->createAppointment(Appointment::STATUS_WAITING);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.appointments.start-meet', $appointment))
|
||||
->assertRedirect('https://meet.test/join/live-now');
|
||||
|
||||
$appointment->refresh();
|
||||
$this->assertSame('room-uuid-2', $appointment->meet_room_uuid);
|
||||
}
|
||||
|
||||
protected function createAppointment(string $status = Appointment::STATUS_SCHEDULED): Appointment
|
||||
{
|
||||
return Appointment::create([
|
||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $this->patient->id,
|
||||
'type' => Appointment::TYPE_SCHEDULED,
|
||||
'status' => $status,
|
||||
'scheduled_at' => now()->addHour(),
|
||||
'reason' => 'Follow-up',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user