345 lines
12 KiB
PHP
345 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Data\UnifiedPatientData;
|
|
use App\Infrastructure\Reports\Sources\MisPatientSource;
|
|
use App\Infrastructure\Reports\Sources\SpecialPatientSource;
|
|
use App\Models\Department;
|
|
use App\Models\DepartmentPatient;
|
|
use App\Models\MedicalHistory;
|
|
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,
|
|
?MisPatientSource $misPatientSource = null,
|
|
?SpecialPatientSource $specialPatientSource = null,
|
|
) {
|
|
$this->misPatientSource = $misPatientSource ?? app(MisPatientSource::class);
|
|
$this->specialPatientSource = $specialPatientSource ?? app(SpecialPatientSource::class);
|
|
}
|
|
|
|
protected MisPatientSource $misPatientSource;
|
|
|
|
protected SpecialPatientSource $specialPatientSource;
|
|
|
|
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->misPatientSource->getDtos($user, $baseStatus, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots),
|
|
'special' => $this->specialPatientSource->getDtos($department, $baseStatus, $dateRange, self::SPECIAL_SOURCE_TYPES, $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->specialPatientSource->getCount($department, $baseStatus, $dateRange, self::SPECIAL_SOURCE_TYPES);
|
|
}
|
|
|
|
$misCount = $this->misPatientSource->getCount(
|
|
$user,
|
|
$baseStatus,
|
|
$dateRange,
|
|
$branchId,
|
|
$includeCurrent,
|
|
$fillableAuto
|
|
);
|
|
|
|
if ($sourceScope === 'mis') {
|
|
return $misCount;
|
|
}
|
|
|
|
$specialCount = $this->specialPatientSource->getCount($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->specialPatientSource->getPatients($department, 'recipient', $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
|
|
{
|
|
$history = MedicalHistory::query()
|
|
->where('original_id', $medicalHistoryId)
|
|
->firstOrFail();
|
|
|
|
$patient->update([
|
|
'rf_medicalhistory_id' => $history->original_id,
|
|
'linked_to_mis_at' => now(),
|
|
'full_name' => $patient->full_name ?: $history->full_name,
|
|
'birth_date' => $patient->birth_date ?: $history->birth_date,
|
|
]);
|
|
|
|
return $patient->fresh();
|
|
}
|
|
|
|
public function searchMisPatients(Department $department, string $query): Collection
|
|
{
|
|
return MedicalHistory::query()
|
|
->whereLike('full_name', "%{$query}%")
|
|
->whereHas('migrations', fn ($builder) => $builder->department($department->rf_mis_department_id))
|
|
->with(['latestMigration', 'operations'])
|
|
->limit(20)
|
|
->get()
|
|
->map(fn (MedicalHistory $patient) => UnifiedPatientData::fromMedicalHistory($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 = MedicalHistory::query()
|
|
->whereIn('original_id', $misIds)
|
|
->with(['latestMigration', 'operations'])
|
|
->get()
|
|
->keyBy('original_id');
|
|
|
|
$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::fromMedicalHistory(
|
|
$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->misPatientSource->getPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots);
|
|
$linkedManualPatients = $this->specialPatientSource->getLinkedManualPatientsForPeriod($department, $dateRange);
|
|
|
|
$mergedMisPatients = $misPatients->map(function ($patient) use ($linkedManualPatients) {
|
|
$medicalHistoryId = $patient instanceof MedicalHistory
|
|
? ($patient->original_id ?? $patient->id)
|
|
: $patient->MedicalHistoryID;
|
|
$linkedManual = $linkedManualPatients->get($medicalHistoryId);
|
|
|
|
return $patient instanceof MedicalHistory
|
|
? UnifiedPatientData::fromMedicalHistory(
|
|
$patient,
|
|
(bool) ($patient->is_recipient_today ?? false),
|
|
$linkedManual,
|
|
$this->resolveObservationComment($medicalHistoryId, null)
|
|
)
|
|
: UnifiedPatientData::fromMisMedicalHistory(
|
|
$patient,
|
|
(bool) ($patient->is_recipient_today ?? false),
|
|
$linkedManual,
|
|
$this->resolveObservationComment($medicalHistoryId, null)
|
|
);
|
|
});
|
|
|
|
$manualDtos = $this->specialPatientSource->getDtos($department, $status, $dateRange, self::SPECIAL_SOURCE_TYPES, $forSnapshots);
|
|
|
|
return UnifiedPatientData::unique($mergedMisPatients->concat($manualDtos))
|
|
->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;
|
|
}
|
|
}
|