parseScopedStatus($status); if ($baseStatus === 'observation') { return $this->getObservationPatients($department, $onlyIds, $sourceScope); } $patients = match ($sourceScope) { 'mis' => $this->getMisPatientDtos($user, $baseStatus, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots), 'special' => $this->getSpecialPatientDtos($department, $baseStatus, $dateRange, $forSnapshots), default => $this->getAggregatedPatientDtos($department, $user, $baseStatus, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots), }; if ($onlyIds) { return $patients->pluck('id'); } return $patients; } public function getLivePatientCountByStatus( Department $department, User $user, string $status, DateRange $dateRange, int $branchId, ?bool $includeCurrent = null, bool $fillableAuto = false ): int { [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); if ($baseStatus === 'observation') { $query = ObservationPatient::query() ->where('rf_department_id', $department->department_id); if ($sourceScope === 'special') { return $query->whereNotNull('rf_department_patient_id')->count(); } if ($sourceScope === 'mis') { return $query->whereNull('rf_department_patient_id')->count(); } return $query->count(); } if ($sourceScope === 'special') { return $this->getManualPatientsCount($department, $baseStatus, $dateRange, self::SPECIAL_SOURCE_TYPES); } $misCount = $this->getMisPatientsCount( $user, $baseStatus, $dateRange, $branchId, $includeCurrent, $fillableAuto ); if ($sourceScope === 'mis') { return $misCount; } $specialCount = $this->getManualPatientsCount($department, $baseStatus, $dateRange, self::SPECIAL_SOURCE_TYPES); return $misCount + $specialCount; } public function getRecipientIdsForReport( Department $department, User $user, DateRange $dateRange, int $branchId, bool $fillableAuto = false ): array { $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); $misIds = $this->patientService->getPlanOrEmergencyPatients( null, $isHeadOrAdmin, $branchId, $dateRange, false, true, false, $fillableAuto ); $manualIds = $this->buildManualPatientsQuery($department, $dateRange, self::SPECIAL_SOURCE_TYPES, false) ->whereBetween('admitted_at', [$dateRange->startSql(), $dateRange->endSql()]) ->pluck('department_patient_id'); $misUids = collect($misIds)->map(fn ($id) => "mis:{$id}")->all(); $manualUids = collect($manualIds)->map(fn ($id) => "manual:{$id}")->all(); return array_values(array_unique(array_merge($misUids, $manualUids))); } public function createManualPatient(Department $department, User $user, array $data, int $reportId): DepartmentPatient { return DepartmentPatient::create([ 'rf_department_id' => $department->department_id, 'rf_report_id' => $reportId, 'source_type' => 'special', 'full_name' => $data['full_name'], 'birth_date' => $data['birth_date'], 'patient_kind' => $data['patient_kind'], 'diagnosis_code' => $data['diagnosis_code'] ?? null, 'diagnosis_name' => $data['diagnosis_name'] ?? null, 'admitted_at' => $data['admitted_at'] ?? now(), 'is_current' => true, 'created_by' => $user->id, ]); } public function recordManualOutcome(DepartmentPatient $patient, array $data): DepartmentPatient { $patient->update([ 'is_current' => false, 'outcome_type' => $data['outcome_type'], 'outcome_at' => $data['outcome_at'] ?? now(), ]); return $patient->fresh(); } public function updateManualPatient(DepartmentPatient $patient, array $data): DepartmentPatient { $manualStatus = $data['manual_status'] ?? null; $isCurrent = $manualStatus === 'current' || $manualStatus === null; $outcomeType = match ($manualStatus) { 'discharged', 'deceased', 'transferred' => $manualStatus, default => null, }; $patient->update([ 'full_name' => $data['full_name'], 'birth_date' => $data['birth_date'], 'patient_kind' => $data['patient_kind'], 'diagnosis_code' => $data['diagnosis_code'] ?? null, 'diagnosis_name' => $data['diagnosis_name'] ?? null, 'admitted_at' => $data['admitted_at'] ?? $patient->admitted_at, 'is_current' => $isCurrent, 'outcome_type' => $outcomeType, 'outcome_at' => $isCurrent ? null : ($data['outcome_at'] ?? now()), ]); return $patient->fresh(); } public function linkManualPatientToMis(DepartmentPatient $patient, int $medicalHistoryId): DepartmentPatient { $misPatient = MisMedicalHistory::where('MedicalHistoryID', $medicalHistoryId)->firstOrFail(); $patient->update([ 'rf_medicalhistory_id' => $misPatient->MedicalHistoryID, 'linked_to_mis_at' => now(), 'full_name' => $patient->full_name ?: trim("{$misPatient->FAMILY} {$misPatient->Name} {$misPatient->OT}"), 'birth_date' => $patient->birth_date ?: $misPatient->BD, ]); return $patient->fresh(); } public function searchMisPatients(Department $department, string $query): Collection { $branchId = \App\Models\MisStationarBranch::where('rf_DepartmentID', $department->rf_mis_department_id) ->value('StationarBranchID'); return MisMedicalHistory::query() ->whereHas('migrations', fn ($builder) => $builder->where('rf_StationarBranchID', $branchId)) ->where(function ($builder) use ($query) { $builder->where('FAMILY', 'like', "%{$query}%") ->orWhere('Name', 'like', "%{$query}%") ->orWhere('OT', 'like', "%{$query}%"); }) ->with(['outcomeMigration.mainDiagnosis.mkb']) ->limit(20) ->get() ->map(fn (MisMedicalHistory $patient) => UnifiedPatientData::fromMisMedicalHistory($patient)); } public function getObservationPatients( Department $department, bool $onlyIds = false, string $sourceScope = 'all' ): Collection { $observationPatients = ObservationPatient::where('rf_department_id', $department->department_id)->get(); $misIds = $observationPatients->pluck('rf_medicalhistory_id')->filter()->unique()->values(); $manualIds = $observationPatients->pluck('rf_department_patient_id')->filter()->unique()->values(); $misPatients = MisMedicalHistory::whereIn('MedicalHistoryID', $misIds) ->with(['outcomeMigration.mainDiagnosis.mkb']) ->get() ->keyBy('MedicalHistoryID'); $manualPatients = DepartmentPatient::whereIn('department_patient_id', $manualIds)->get()->keyBy('department_patient_id'); $patients = $observationPatients->map(function (ObservationPatient $observation) use ($misPatients, $manualPatients, $sourceScope) { if ($observation->rf_department_patient_id && $manualPatients->has($observation->rf_department_patient_id)) { if ($sourceScope === 'mis') { return null; } return UnifiedPatientData::fromDepartmentPatient( $manualPatients[$observation->rf_department_patient_id], false, [], $observation->comment ); } if ($observation->rf_medicalhistory_id && $misPatients->has($observation->rf_medicalhistory_id)) { if ($sourceScope === 'special') { return null; } return UnifiedPatientData::fromMisMedicalHistory( $misPatients[$observation->rf_medicalhistory_id], false, null, $observation->comment ); } return null; })->filter()->values(); if ($onlyIds) { return $patients->pluck('id'); } return $patients; } private function getAggregatedPatientDtos( Department $department, User $user, string $status, DateRange $dateRange, int $branchId, ?bool $includeCurrent = null, bool $fillableAuto = false, bool $forSnapshots = false ): Collection { $misPatients = $this->getMisPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots); $manualPatients = $this->getManualPatients($department, $status, $dateRange, self::SPECIAL_SOURCE_TYPES, !$forSnapshots); $reportIds = $this->getReportIdsForDepartmentPeriod($department, $dateRange); $linkedManualPatients = DepartmentPatient::query() ->where(function ($builder) use ($department, $reportIds) { if (!empty($reportIds)) { $builder->whereIn('rf_report_id', $reportIds); } $builder->orWhere(function ($legacyQuery) use ($department) { $legacyQuery->whereNull('rf_report_id') ->where('rf_department_id', $department->department_id); }); }) ->whereIn('source_type', self::SPECIAL_SOURCE_TYPES) ->whereNotNull('rf_medicalhistory_id') ->get() ->keyBy('rf_medicalhistory_id'); $mergedMisPatients = $misPatients->map(function ($patient) use ($linkedManualPatients) { $linkedManual = $linkedManualPatients->get($patient->MedicalHistoryID); return UnifiedPatientData::fromMisMedicalHistory( $patient, (bool) ($patient->is_recipient_today ?? false), $linkedManual, $this->resolveObservationComment($patient->MedicalHistoryID, null) ); }); $manualDtos = $this->mapManualPatients($manualPatients, $dateRange); return UnifiedPatientData::unique($mergedMisPatients->concat($manualDtos)) ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '') ->values(); } private function getMisPatientDtos( User $user, string $status, DateRange $dateRange, int $branchId, ?bool $includeCurrent = null, bool $fillableAuto = false, bool $forSnapshots = false ): Collection { return $this->getMisPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots) ->map(fn ($patient) => UnifiedPatientData::fromMisMedicalHistory( $patient, (bool) ($patient->is_recipient_today ?? false), )) ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '') ->values(); } private function getSpecialPatientDtos( Department $department, string $status, DateRange $dateRange, bool $forSnapshots = false ): Collection { return $this->mapManualPatients( $this->getManualPatients($department, $status, $dateRange, self::SPECIAL_SOURCE_TYPES, !$forSnapshots), $dateRange ); } private function getMisPatients( User $user, string $status, DateRange $dateRange, int $branchId, ?bool $includeCurrent = null, bool $fillableAuto = false, bool $forSnapshots = false ): Collection { $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); $includeCurrent = $includeCurrent ?? in_array($status, ['plan', 'emergency'], true); return match ($status) { 'plan', 'emergency' => $this->patientService->getPlanOrEmergencyPatients( $status, $isHeadOrAdmin, $branchId, $dateRange, false, false, $includeCurrent, $fillableAuto ), 'current' => $this->patientService->getAllPatientsInDepartment( $isHeadOrAdmin, $branchId, $dateRange, false, false, $fillableAuto ), 'recipient' => $this->patientService->getPlanOrEmergencyPatients( null, $isHeadOrAdmin, $branchId, $dateRange, false, false, false, $fillableAuto ), 'outcome' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'without-transferred'), 'outcome-discharged' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'discharged'), 'outcome-transferred' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'transferred'), 'outcome-deceased' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'deceased'), 'reanimation' => $this->patientService->getReanimationPatients($branchId, $dateRange), default => collect(), }; } private function getMisPatientsCount( User $user, string $status, DateRange $dateRange, int $branchId, ?bool $includeCurrent = null, bool $fillableAuto = false ): int { $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); $includeCurrent = $includeCurrent ?? in_array($status, ['plan', 'emergency'], true); return match ($status) { 'plan', 'emergency' => $includeCurrent ? $this->patientService->getPatientsCountWithCurrent($status, $isHeadOrAdmin, $branchId, $dateRange) : $this->patientService->getPlanOrEmergencyPatients( $status, $isHeadOrAdmin, $branchId, $dateRange, true, false, false, $fillableAuto ), 'current' => $this->patientService->getAllPatientsInDepartment( $isHeadOrAdmin, $branchId, $dateRange, true, false, $fillableAuto ), 'recipient' => $this->patientService->getPlanOrEmergencyPatients( null, $isHeadOrAdmin, $branchId, $dateRange, true, false, false, $fillableAuto ), 'outcome' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'without-transferred', true)->count(), 'outcome-discharged' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'discharged', true)->count(), 'outcome-transferred' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'transferred', true)->count(), 'outcome-deceased' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'deceased', true)->count(), 'reanimation' => $this->patientService->getReanimationPatients($branchId, $dateRange, true)->count(), default => 0, }; } private function getManualPatients( Department $department, string $status, DateRange $dateRange, ?array $sourceTypes = self::SPECIAL_SOURCE_TYPES, bool $withOperations = true ): Collection { $query = $this->buildManualPatientsQuery($department, $dateRange, $sourceTypes, $withOperations); return match ($status) { 'plan', 'emergency' => $query ->current() ->where('patient_kind', $status) ->get(), 'current' => $query ->current() ->get(), 'recipient' => $query ->whereBetween('admitted_at', [$dateRange->startSql(), $dateRange->endSql()]) ->get(), 'outcome' => $query ->whereNotNull('outcome_type') ->whereIn('outcome_type', ['discharged', 'deceased']) ->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()]) ->get(), 'outcome-discharged', 'outcome-transferred', 'outcome-deceased' => $query ->where('outcome_type', str_replace('outcome-', '', $status)) ->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()]) ->get(), 'reanimation' => collect(), default => collect(), }; } private function getManualPatientsCount( Department $department, string $status, DateRange $dateRange, ?array $sourceTypes = self::SPECIAL_SOURCE_TYPES ): int { $query = $this->buildManualPatientsQuery($department, $dateRange, $sourceTypes, false); return match ($status) { 'plan', 'emergency' => $query ->current() ->where('patient_kind', $status) ->count(), 'current' => $query ->current() ->count(), 'recipient' => $query ->whereBetween('admitted_at', [$dateRange->startSql(), $dateRange->endSql()]) ->count(), 'outcome' => $query ->whereNotNull('outcome_type') ->whereIn('outcome_type', ['discharged', 'deceased']) ->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()]) ->count(), 'outcome-discharged', 'outcome-transferred', 'outcome-deceased' => $query ->where('outcome_type', str_replace('outcome-', '', $status)) ->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()]) ->count(), default => 0, }; } private function buildManualPatientsQuery( Department $department, DateRange $dateRange, ?array $sourceTypes, bool $withOperations ) { $reportIds = $this->getReportIdsForDepartmentPeriod($department, $dateRange); $query = DepartmentPatient::query() ->where(function ($builder) use ($department, $reportIds) { if (!empty($reportIds)) { $builder->whereIn('rf_report_id', $reportIds); } $builder->orWhere(function ($legacyQuery) use ($department) { $legacyQuery->whereNull('rf_report_id') ->where('rf_department_id', $department->department_id); }); }); if ($withOperations) { $query->with(['operations.serviceMedical']); } if ($sourceTypes !== null) { $query->whereIn('source_type', $sourceTypes); } return $query; } private function getReportIdsForDepartmentPeriod(Department $department, DateRange $dateRange): array { return Report::query() ->where('rf_department_id', $department->department_id) ->when( $dateRange->isOneDay, fn ($query) => $query->exactPeriod($dateRange->startSql(), $dateRange->endSql()), fn ($query) => $query->withinPeriod($dateRange->startSql(), $dateRange->endSql()), ) ->pluck('report_id') ->all(); } private function mapManualPatients(Collection $manualPatients, DateRange $dateRange): Collection { return $manualPatients ->map(function (DepartmentPatient $patient) use ($dateRange) { $operationsRelation = $patient->relationLoaded('operations') ? $patient->operations : collect(); $operations = $operationsRelation->map(fn ($operation) => [ 'id' => $operation->department_patient_operation_id, 'code' => $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code, 'name' => $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name, 'startAt' => $operation->started_at?->toIso8601String(), 'endAt' => $operation->ended_at?->toIso8601String(), ])->filter(fn ($operation) => $operation['code'] || $operation['name'])->values()->all(); return UnifiedPatientData::fromDepartmentPatient( $patient, $patient->admitted_at?->betweenIncluded($dateRange->startDate, $dateRange->endDate) ?? false, $operations, $this->resolveObservationComment(null, $patient->department_patient_id) ); }) ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '') ->values(); } private function parseScopedStatus(string $status): array { foreach (['mis', 'special'] as $scope) { $prefix = "{$scope}-"; if (str_starts_with($status, $prefix)) { return [substr($status, strlen($prefix)), $scope]; } } return [$status, 'all']; } private function resolveObservationComment(?int $medicalHistoryId, ?int $departmentPatientId): ?string { $query = ObservationPatient::query(); if ($departmentPatientId) { $query->where('rf_department_patient_id', $departmentPatientId); } elseif ($medicalHistoryId) { $query->where('rf_medicalhistory_id', $medicalHistoryId); } else { return null; } return $query->pluck('comment')->filter()->implode('; ') ?: null; } }