Files
onboard/app/Services/UnifiedPatientService.php
2026-04-24 16:46:10 +09:00

612 lines
23 KiB
PHP

<?php
namespace App\Services;
use App\Data\UnifiedPatientData;
use App\Models\Department;
use App\Models\DepartmentPatient;
use App\Models\MisMedicalHistory;
use App\Models\ObservationPatient;
use App\Models\Report;
use App\Models\User;
use Illuminate\Support\Collection;
class UnifiedPatientService
{
private const SPECIAL_SOURCE_TYPES = ['manual', 'special'];
public function __construct(
protected PatientService $patientService,
) {}
public function getLivePatientsByStatus(
Department $department,
User $user,
string $status,
DateRange $dateRange,
int $branchId,
bool $onlyIds = false,
?bool $includeCurrent = null,
bool $fillableAuto = false,
bool $forSnapshots = false
): Collection {
[$baseStatus, $sourceScope] = $this->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;
}
}