where('organization_id', $organizationId); } /** * @param array $filters */ public function search(string $ownerRef, int $organizationId, array $filters = [], ?int $branchId = null): LengthAwarePaginator { $query = $this->queryForOrganization($ownerRef, $organizationId) ->with(['branch']) ->orderByDesc('created_at'); if ($branchId !== null) { $query->where('branch_id', $branchId); } if ($q = trim((string) ($filters['q'] ?? ''))) { $query->where(function (Builder $inner) use ($q) { $inner->where('patient_number', 'like', "%{$q}%") ->orWhere('phone', 'like', "%{$q}%") ->orWhere('national_id', 'like', "%{$q}%") ->orWhere('first_name', 'like', "%{$q}%") ->orWhere('last_name', 'like', "%{$q}%") ->orWhere('other_names', 'like', "%{$q}%") ->orWhereRaw("concat(first_name, ' ', coalesce(other_names, ''), ' ', last_name) like ?", ["%{$q}%"]); }); } if ($patientNumber = trim((string) ($filters['patient_number'] ?? ''))) { $query->where('patient_number', $patientNumber); } if ($phone = trim((string) ($filters['phone'] ?? ''))) { $query->where('phone', 'like', "%{$phone}%"); } if ($nationalId = trim((string) ($filters['national_id'] ?? ''))) { $query->where('national_id', $nationalId); } if ($dob = $filters['date_of_birth'] ?? null) { $query->whereDate('date_of_birth', Carbon::parse($dob)->toDateString()); } return $query->paginate((int) ($filters['per_page'] ?? 20))->withQueryString(); } /** * Exact patient_number match first, then fall back to broad search (first hit). */ public function findByScanCode(string $ownerRef, int $organizationId, string $code, ?int $branchId = null): ?Patient { $code = trim($code); if ($code === '') { return null; } $base = $this->queryForOrganization($ownerRef, $organizationId) ->when($branchId !== null, fn ($q) => $q->where('branch_id', $branchId)); $exact = (clone $base)->where('patient_number', $code)->first(); if ($exact) { return $exact; } // Case-insensitive exact patient number. $ci = (clone $base)->whereRaw('lower(patient_number) = ?', [strtolower($code)])->first(); if ($ci) { return $ci; } return (clone $base) ->where(function (Builder $inner) use ($code) { $inner->where('patient_number', 'like', "%{$code}%") ->orWhere('national_id', $code) ->orWhere('phone', 'like', "%{$code}%"); }) ->orderByDesc('created_at') ->first(); } /** * @param array $data */ public function create(Organization $organization, string $ownerRef, array $data, ?string $actorRef = null): Patient { $patient = Patient::create([ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $data['branch_id'] ?? null, 'patient_number' => $this->numbers->generate($organization), 'first_name' => $data['first_name'], 'last_name' => $data['last_name'], 'other_names' => $data['other_names'] ?? null, 'gender' => $data['gender'] ?? null, 'date_of_birth' => $data['date_of_birth'] ?? null, 'phone' => $data['phone'] ?? null, 'email' => $data['email'] ?? null, 'national_id' => $data['national_id'] ?? null, 'address' => $data['address'] ?? null, 'city' => $data['city'] ?? null, 'region' => $data['region'] ?? null, 'notes' => $data['notes'] ?? null, ]); $this->syncRelated($patient, $ownerRef, $data); $this->storeAttachments($patient, $ownerRef, $data['attachments'] ?? [], $actorRef); AuditLogger::record($ownerRef, 'patient.created', $organization->id, $actorRef, Patient::class, $patient->id); return $patient->fresh([ 'allergies', 'conditions', 'familyHistory', 'emergencyContacts', 'insurancePolicies', 'attachments', 'branch', ]); } /** * @param array $data */ public function update(Patient $patient, string $ownerRef, array $data, ?string $actorRef = null): Patient { $patient->update([ 'branch_id' => $data['branch_id'] ?? $patient->branch_id, 'first_name' => $data['first_name'], 'last_name' => $data['last_name'], 'other_names' => $data['other_names'] ?? null, 'gender' => $data['gender'] ?? null, 'date_of_birth' => $data['date_of_birth'] ?? null, 'phone' => $data['phone'] ?? null, 'email' => $data['email'] ?? null, 'national_id' => $data['national_id'] ?? null, 'address' => $data['address'] ?? null, 'city' => $data['city'] ?? null, 'region' => $data['region'] ?? null, 'notes' => $data['notes'] ?? null, ]); $this->syncRelated($patient, $ownerRef, $data); $this->storeAttachments($patient, $ownerRef, $data['attachments'] ?? [], $actorRef); AuditLogger::record($ownerRef, 'patient.updated', $patient->organization_id, $actorRef, Patient::class, $patient->id); return $patient->fresh([ 'allergies', 'conditions', 'familyHistory', 'emergencyContacts', 'insurancePolicies', 'attachments', 'branch', ]); } public function delete(Patient $patient, string $ownerRef, ?string $actorRef = null): void { $organizationId = $patient->organization_id; $patientId = $patient->id; foreach ($patient->attachments as $attachment) { Storage::disk('public')->delete($attachment->file_path); } $patient->delete(); AuditLogger::record($ownerRef, 'patient.deleted', $organizationId, $actorRef, Patient::class, $patientId); } /** * @return array */ public function dashboard(Patient $patient): array { $patient->load([ 'branch', 'allergies', 'conditions', 'familyHistory', 'emergencyContacts', 'insurancePolicies', 'attachments', ]); $visits = $patient->visits() ->with(['branch']) ->orderByDesc('checked_in_at') ->limit(10) ->get(); $consultations = Consultation::owned($patient->owner_ref) ->where('patient_id', $patient->id) ->with(['practitioner', 'diagnoses']) ->orderByDesc('started_at') ->limit(10) ->get(); $laboratory = $patient->investigationRequests() ->with(['investigationType', 'result']) ->whereIn('status', [ \App\Models\InvestigationRequest::STATUS_COMPLETED, \App\Models\InvestigationRequest::STATUS_DELIVERED, ]) ->orderByDesc('completed_at') ->limit(10) ->get(); $prescriptions = $patient->prescriptions() ->with(['practitioner', 'items']) ->orderByDesc('created_at') ->limit(10) ->get(); $invoices = $patient->bills() ->with(['branch', 'payments']) ->orderByDesc('created_at') ->limit(10) ->get(); return [ 'patient' => $patient, 'visits' => $visits, 'consultations' => $consultations, 'laboratory' => $laboratory, 'prescriptions' => $prescriptions, 'invoices' => $invoices, ]; } /** * @param array $data */ protected function syncRelated(Patient $patient, string $ownerRef, array $data): void { if (array_key_exists('allergies', $data)) { $patient->allergies()->delete(); foreach ($data['allergies'] as $row) { if (empty($row['allergen'])) { continue; } PatientAllergy::create([ 'owner_ref' => $ownerRef, 'patient_id' => $patient->id, 'allergen' => $row['allergen'], 'severity' => $row['severity'] ?? 'unknown', 'notes' => $row['notes'] ?? null, ]); } } if (array_key_exists('conditions', $data)) { $patient->conditions()->delete(); foreach ($data['conditions'] as $row) { if (empty($row['condition'])) { continue; } PatientCondition::create([ 'owner_ref' => $ownerRef, 'patient_id' => $patient->id, 'condition' => $row['condition'], 'onset_date' => $row['onset_date'] ?? null, 'is_chronic' => (bool) ($row['is_chronic'] ?? false), 'notes' => $row['notes'] ?? null, ]); } } if (array_key_exists('family_history', $data)) { $patient->familyHistory()->delete(); foreach ($data['family_history'] as $row) { if (empty($row['condition'])) { continue; } PatientFamilyHistory::create([ 'owner_ref' => $ownerRef, 'patient_id' => $patient->id, 'relation' => $row['relation'] ?? 'other', 'condition' => $row['condition'], 'notes' => $row['notes'] ?? null, ]); } } if (array_key_exists('emergency_contacts', $data)) { $patient->emergencyContacts()->delete(); foreach ($data['emergency_contacts'] as $row) { if (empty($row['name']) || empty($row['phone'])) { continue; } EmergencyContact::create([ 'owner_ref' => $ownerRef, 'patient_id' => $patient->id, 'name' => $row['name'], 'phone' => $row['phone'], 'relationship' => $row['relationship'] ?? null, 'is_primary' => (bool) ($row['is_primary'] ?? false), ]); } } if (array_key_exists('insurance', $data)) { $patient->insurancePolicies()->delete(); foreach ($data['insurance'] as $row) { if (empty($row['provider_name'])) { continue; } InsurancePolicy::create([ 'owner_ref' => $ownerRef, 'patient_id' => $patient->id, 'provider_name' => $row['provider_name'], 'policy_number' => $row['policy_number'] ?? null, 'coverage_type' => $row['coverage_type'] ?? null, 'expiry_date' => $row['expiry_date'] ?? null, 'notes' => $row['notes'] ?? null, ]); } } } /** * @param array $files */ protected function storeAttachments(Patient $patient, string $ownerRef, array $files, ?string $actorRef): void { foreach ($files as $file) { if (! $file instanceof UploadedFile) { continue; } $path = $file->store("care/patients/{$patient->id}/attachments", 'public'); PatientAttachment::create([ 'owner_ref' => $ownerRef, 'patient_id' => $patient->id, 'file_path' => $path, 'original_name' => $file->getClientOriginalName(), 'mime_type' => $file->getMimeType(), 'document_type' => 'other', 'uploaded_by' => $actorRef, ]); } } }