Deploy Ladill Care / deploy (push) Successful in 1m39s
Branch-scoped Care devices with hashed agent tokens, patient barcode lookup/labels, and provenance-aware vitals from a minimal device agent (Pro for agent hardware; wedge free). Co-authored-by: Cursor <cursoragent@cursor.com>
182 lines
6.7 KiB
PHP
182 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Care;
|
|
|
|
use App\Models\Appointment;
|
|
use App\Models\Consultation;
|
|
use App\Models\ConsultationDocument;
|
|
use App\Models\Diagnosis;
|
|
use App\Models\VitalSign;
|
|
use App\Models\Visit;
|
|
use Illuminate\Http\UploadedFile;
|
|
|
|
class ConsultationService
|
|
{
|
|
public function __construct(
|
|
protected AppointmentService $appointments,
|
|
) {}
|
|
|
|
public function startFromAppointment(Appointment $appointment, string $ownerRef, ?string $actorRef = null): Consultation
|
|
{
|
|
if ($appointment->status !== Appointment::STATUS_IN_CONSULTATION) {
|
|
$this->appointments->startConsultation(
|
|
$appointment,
|
|
$ownerRef,
|
|
$appointment->practitioner_id,
|
|
$actorRef,
|
|
);
|
|
$appointment->refresh();
|
|
}
|
|
|
|
$existing = Consultation::where('appointment_id', $appointment->id)->first();
|
|
if ($existing) {
|
|
return $existing->load(['vitalSigns', 'diagnoses', 'documents', 'patient', 'practitioner']);
|
|
}
|
|
|
|
$consultation = Consultation::create([
|
|
'owner_ref' => $ownerRef,
|
|
'visit_id' => $appointment->visit_id,
|
|
'appointment_id' => $appointment->id,
|
|
'practitioner_id' => $appointment->practitioner_id,
|
|
'patient_id' => $appointment->patient_id,
|
|
'status' => Consultation::STATUS_DRAFT,
|
|
'started_at' => now(),
|
|
]);
|
|
|
|
AuditLogger::record($ownerRef, 'consultation.started', $appointment->organization_id, $actorRef, Consultation::class, $consultation->id);
|
|
|
|
return $consultation->load(['patient', 'practitioner', 'visit']);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
public function save(Consultation $consultation, string $ownerRef, array $data, ?string $actorRef = null): Consultation
|
|
{
|
|
$consultation->update([
|
|
'symptoms' => $data['symptoms'] ?? $consultation->symptoms,
|
|
'clinical_notes' => $data['clinical_notes'] ?? $consultation->clinical_notes,
|
|
'practitioner_id' => $data['practitioner_id'] ?? $consultation->practitioner_id,
|
|
]);
|
|
|
|
if (array_key_exists('vitals', $data) && is_array($data['vitals'])) {
|
|
$this->saveVitals($consultation, $ownerRef, $data['vitals'], $actorRef);
|
|
}
|
|
|
|
if (array_key_exists('diagnoses', $data)) {
|
|
$this->syncDiagnoses($consultation, $ownerRef, $data['diagnoses']);
|
|
}
|
|
|
|
if (! empty($data['documents'])) {
|
|
$this->storeDocuments($consultation, $ownerRef, $data['documents'], $actorRef);
|
|
}
|
|
|
|
AuditLogger::record($ownerRef, 'consultation.updated', $consultation->visit->organization_id, $actorRef, Consultation::class, $consultation->id);
|
|
|
|
return $consultation->fresh(['vitalSigns', 'diagnoses', 'documents', 'patient', 'practitioner', 'visit', 'appointment']);
|
|
}
|
|
|
|
public function complete(Consultation $consultation, string $ownerRef, ?string $actorRef = null): Consultation
|
|
{
|
|
$consultation->update([
|
|
'status' => Consultation::STATUS_COMPLETED,
|
|
'completed_at' => now(),
|
|
'completed_by' => $actorRef,
|
|
]);
|
|
|
|
if ($consultation->appointment) {
|
|
$this->appointments->complete($consultation->appointment, $ownerRef, $actorRef);
|
|
} elseif ($consultation->visit) {
|
|
app(VisitService::class)->complete($consultation->visit, $ownerRef, $actorRef);
|
|
}
|
|
|
|
AuditLogger::record($ownerRef, 'consultation.completed', $consultation->visit->organization_id, $actorRef, Consultation::class, $consultation->id);
|
|
|
|
return $consultation->fresh(['vitalSigns', 'diagnoses', 'documents', 'patient', 'practitioner']);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $vitals
|
|
* @param array{source?: string, device_id?: int|null, raw_payload?: array<string, mixed>|null} $provenance
|
|
*/
|
|
public function saveVitals(
|
|
Consultation $consultation,
|
|
string $ownerRef,
|
|
array $vitals,
|
|
?string $actorRef = null,
|
|
array $provenance = [],
|
|
): ?VitalSign {
|
|
$numeric = collect($vitals)->only([
|
|
'bp_systolic', 'bp_diastolic', 'pulse', 'temperature',
|
|
'weight_kg', 'height_cm', 'spo2', 'respiratory_rate',
|
|
])->filter(fn ($v) => $v !== null && $v !== '')->all();
|
|
|
|
if ($numeric === []) {
|
|
return null;
|
|
}
|
|
|
|
return VitalSign::create([
|
|
'owner_ref' => $ownerRef,
|
|
'consultation_id' => $consultation->id,
|
|
'bp_systolic' => $vitals['bp_systolic'] ?? null,
|
|
'bp_diastolic' => $vitals['bp_diastolic'] ?? null,
|
|
'pulse' => $vitals['pulse'] ?? null,
|
|
'temperature' => $vitals['temperature'] ?? null,
|
|
'weight_kg' => $vitals['weight_kg'] ?? null,
|
|
'height_cm' => $vitals['height_cm'] ?? null,
|
|
'spo2' => $vitals['spo2'] ?? null,
|
|
'respiratory_rate' => $vitals['respiratory_rate'] ?? null,
|
|
'recorded_by' => $actorRef,
|
|
'recorded_at' => now(),
|
|
'source' => $provenance['source'] ?? VitalSign::SOURCE_MANUAL,
|
|
'device_id' => $provenance['device_id'] ?? null,
|
|
'raw_payload' => $provenance['raw_payload'] ?? null,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $rows
|
|
*/
|
|
protected function syncDiagnoses(Consultation $consultation, string $ownerRef, array $rows): void
|
|
{
|
|
$consultation->diagnoses()->delete();
|
|
|
|
foreach ($rows as $row) {
|
|
if (empty($row['description'])) {
|
|
continue;
|
|
}
|
|
Diagnosis::create([
|
|
'owner_ref' => $ownerRef,
|
|
'consultation_id' => $consultation->id,
|
|
'code' => $row['code'] ?? null,
|
|
'description' => $row['description'],
|
|
'is_primary' => (bool) ($row['is_primary'] ?? false),
|
|
'notes' => $row['notes'] ?? null,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<int, UploadedFile> $files
|
|
*/
|
|
protected function storeDocuments(Consultation $consultation, string $ownerRef, array $files, ?string $actorRef): void
|
|
{
|
|
foreach ($files as $file) {
|
|
if (! $file instanceof UploadedFile) {
|
|
continue;
|
|
}
|
|
|
|
$path = $file->store("care/consultations/{$consultation->id}/documents", 'public');
|
|
|
|
ConsultationDocument::create([
|
|
'owner_ref' => $ownerRef,
|
|
'consultation_id' => $consultation->id,
|
|
'file_path' => $path,
|
|
'original_name' => $file->getClientOriginalName(),
|
|
'mime_type' => $file->getMimeType(),
|
|
'uploaded_by' => $actorRef,
|
|
]);
|
|
}
|
|
}
|
|
}
|