Перевод на доменную архитектуру
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace App\Infrastructure\Reports\Services;
|
||||
|
||||
use App\Domain\Reports\ValueObjects\MetrikaConfig;
|
||||
use App\Models\Department;
|
||||
use App\Models\MisMedicalHistory;
|
||||
use App\Models\MisStationarBranch;
|
||||
use App\Models\User;
|
||||
use App\Services\DateRange;
|
||||
use App\Services\PatientService;
|
||||
use App\Services\UnifiedPatientService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AutoFillReportPayloadBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UnifiedPatientService $unifiedPatientService,
|
||||
private readonly PatientService $patientService,
|
||||
private readonly CalculatedMetricsSynchronizer $calculatedMetricsSynchronizer,
|
||||
) {}
|
||||
|
||||
public function build(User $user, Department $department, DateRange $dateRange): array
|
||||
{
|
||||
$branchId = $this->getBranchId($department->rf_mis_department_id);
|
||||
$metrics = $this->buildMetrics($department, $user, $branchId, $dateRange);
|
||||
|
||||
return [
|
||||
'departmentId' => $department->department_id,
|
||||
'userId' => $user->rf_lpudoctor_id ?? $user->id,
|
||||
'dates' => [
|
||||
$dateRange->startTimestamp(),
|
||||
$dateRange->endTimestamp(),
|
||||
],
|
||||
'sent_at' => $dateRange->endSql(),
|
||||
'created_at' => $dateRange->endSql(),
|
||||
'status' => 'submitted',
|
||||
'metrics' => [
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::PLAN) => $metrics['plan'],
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::EMERGENCY) => $metrics['emergency'],
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::RECIPIENT) => $metrics['recipient'],
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::OUTCOME) => $metrics['discharged'] + $metrics['deceased'],
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::CURRENT) => $metrics['current'],
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::DECEASED) => $metrics['deceased'],
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::EMERGENCY_SURGERY) => $metrics['emergency_surgery'],
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::PLAN_SURGERY) => $metrics['plan_surgery'],
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::TRANSFERRED) => $metrics['transferred'],
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::OBSERVATION) => 0,
|
||||
MetrikaConfig::payloadKey(MetrikaConfig::DISCHARGED) => $metrics['discharged'],
|
||||
],
|
||||
'observationPatients' => [],
|
||||
'unwantedEvents' => [],
|
||||
];
|
||||
}
|
||||
|
||||
private function buildMetrics(Department $department, User $user, ?int $branchId, DateRange $dateRange): array
|
||||
{
|
||||
if (! $branchId) {
|
||||
return [
|
||||
'plan' => 0,
|
||||
'emergency' => 0,
|
||||
'recipient' => 0,
|
||||
'discharged' => 0,
|
||||
'transferred' => 0,
|
||||
'deceased' => 0,
|
||||
'current' => 0,
|
||||
'plan_surgery' => 0,
|
||||
'emergency_surgery' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$manualSurgicalCount = $this->calculatedMetricsSynchronizer->getManualSurgicalCounts($department, $dateRange);
|
||||
$recipientQuery = $this->buildRecipientMedicalHistoryQuery($branchId, $dateRange);
|
||||
$dischargeCodes = [1, 11, 2, 12, 7, 18, 48];
|
||||
$deceasedCodes = [5, 6, 15, 16];
|
||||
$transferCodes = [4, 14];
|
||||
|
||||
$planRecipient = (clone $recipientQuery)
|
||||
->where('rf_EmerSignID', 1)
|
||||
->distinct()
|
||||
->count('MedicalHistoryID');
|
||||
|
||||
$emergencyRecipient = (clone $recipientQuery)
|
||||
->whereIn('rf_EmerSignID', [2, 4])
|
||||
->distinct()
|
||||
->count('MedicalHistoryID');
|
||||
|
||||
$recipientTotal = (clone $recipientQuery)
|
||||
->distinct()
|
||||
->count('MedicalHistoryID');
|
||||
|
||||
$discharged = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $dischargeCodes);
|
||||
$deceased = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $deceasedCodes);
|
||||
$transferred = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $transferCodes);
|
||||
|
||||
return [
|
||||
'plan' => $planRecipient,
|
||||
'emergency' => $emergencyRecipient,
|
||||
'recipient' => $recipientTotal,
|
||||
'discharged' => $discharged,
|
||||
'transferred' => $transferred,
|
||||
'deceased' => $deceased,
|
||||
'current' => $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'current', $dateRange, $branchId, null, true),
|
||||
'plan_surgery' => $this->patientService->getSurgicalPatients('plan', $branchId, $dateRange, true) + ($manualSurgicalCount[1] ?? 0),
|
||||
'emergency_surgery' => $this->patientService->getSurgicalPatients('emergency', $branchId, $dateRange, true) + ($manualSurgicalCount[0] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function buildRecipientMedicalHistoryQuery(int $branchId, DateRange $dateRange)
|
||||
{
|
||||
$startAt = $dateRange->start()->copy()->subDay()->format('Y-m-d H:i:s');
|
||||
$endAt = $dateRange->end()->copy()->addDay()->format('Y-m-d H:i:s');
|
||||
|
||||
if ($dateRange->isOneDay) {
|
||||
$startAt = $dateRange->startSql();
|
||||
$endAt = $dateRange->endSql();
|
||||
}
|
||||
|
||||
return MisMedicalHistory::query()
|
||||
->where('MedicalHistoryID', '<>', 0)
|
||||
->whereExists(function ($query) use ($branchId, $startAt, $endAt) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('stt_migrationpatient as mp')
|
||||
->whereColumn('mp.rf_MedicalHistoryID', 'stt_medicalhistory.MedicalHistoryID')
|
||||
->where('mp.rf_StationarBranchID', $branchId)
|
||||
->where('mp.DateIngoing', '>', $startAt)
|
||||
->where('mp.DateIngoing', '<=', $endAt);
|
||||
});
|
||||
}
|
||||
|
||||
private function buildTreatedMedicalHistoryQuery(int $branchId, DateRange $dateRange)
|
||||
{
|
||||
$query = MisMedicalHistory::query()
|
||||
->where('MedicalHistoryID', '<>', 0)
|
||||
->whereExists(function ($query) use ($branchId) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('stt_migrationpatient as mp')
|
||||
->whereColumn('mp.rf_MedicalHistoryID', 'stt_medicalhistory.MedicalHistoryID')
|
||||
->where('mp.rf_StationarBranchID', $branchId);
|
||||
});
|
||||
|
||||
if ($dateRange->isOneDay) {
|
||||
return $query
|
||||
->where('DateExtract', '>', $dateRange->startSql())
|
||||
->where('DateExtract', '<=', $dateRange->endSql());
|
||||
}
|
||||
|
||||
$startAt = $dateRange->startSql();
|
||||
$endDate = $dateRange->end()->toDateString();
|
||||
|
||||
return $query
|
||||
->where('DateExtract', '>', $startAt)
|
||||
->whereDate('DateExtract', '<=', $endDate);
|
||||
}
|
||||
|
||||
private function countOutcomeByVisitResultIds(int $branchId, DateRange $dateRange, array $visitResultIds): int
|
||||
{
|
||||
return $this->buildTreatedMedicalHistoryQuery($branchId, $dateRange)
|
||||
->whereExists(function ($query) use ($branchId, $visitResultIds) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('stt_migrationpatient as mp')
|
||||
->whereColumn('mp.rf_MedicalHistoryID', 'stt_medicalhistory.MedicalHistoryID')
|
||||
->where('mp.rf_StationarBranchID', $branchId)
|
||||
->whereIn('mp.rf_kl_VisitResultID', $visitResultIds);
|
||||
})
|
||||
->distinct()
|
||||
->count('MedicalHistoryID');
|
||||
}
|
||||
|
||||
private function getBranchId(int $misDepartmentId): ?int
|
||||
{
|
||||
return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId)
|
||||
->value('StationarBranchID');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Infrastructure\Reports\Services;
|
||||
|
||||
use App\Domain\Reports\ValueObjects\MetrikaConfig;
|
||||
use App\Models\Department;
|
||||
use App\Models\DepartmentPatientOperation;
|
||||
use App\Models\MedicalHistorySnapshot;
|
||||
use App\Models\MisStationarBranch;
|
||||
use App\Models\ObservationPatient;
|
||||
use App\Models\Report;
|
||||
use App\Models\UnwantedEvent;
|
||||
use App\Models\User;
|
||||
use App\Services\DateRange;
|
||||
use App\Services\DateRangeService;
|
||||
use App\Services\PatientService;
|
||||
|
||||
class CalculatedMetricsSynchronizer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DateRangeService $dateRangeService,
|
||||
private readonly PatientService $patientService,
|
||||
private readonly ReportStorageService $reportStorageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function sync(Report $report, User $user, array $data): void
|
||||
{
|
||||
if (! isset($data['dates'][0], $data['dates'][1])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$department = Department::query()->where('department_id', $report->rf_department_id)->first();
|
||||
if (! $department) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateRange = $this->dateRangeService->getNormalizedDateRange(
|
||||
$user,
|
||||
(string) $data['dates'][0],
|
||||
(string) $data['dates'][1]
|
||||
);
|
||||
|
||||
$branchId = $this->getBranchId($department->rf_mis_department_id);
|
||||
|
||||
$planCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['plan']);
|
||||
$emergencyCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['emergency']);
|
||||
$recipientCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['recipient']);
|
||||
$dischargedCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['discharged']);
|
||||
$transferredCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['transferred']);
|
||||
$deceasedCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['deceased']);
|
||||
$currentCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['current']);
|
||||
$outcomeCount = $dischargedCount + $deceasedCount;
|
||||
|
||||
$manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange);
|
||||
$misEmergencySurgery = $branchId
|
||||
? $this->patientService->getSurgicalPatients('emergency', $branchId, $dateRange, true)
|
||||
: 0;
|
||||
$misPlanSurgery = $branchId
|
||||
? $this->patientService->getSurgicalPatients('plan', $branchId, $dateRange, true)
|
||||
: 0;
|
||||
|
||||
$observationCount = ObservationPatient::query()
|
||||
->where('rf_department_id', $department->department_id)
|
||||
->where('rf_report_id', $report->report_id)
|
||||
->count();
|
||||
|
||||
$unwantedEventsCount = UnwantedEvent::query()
|
||||
->where('rf_report_id', $report->report_id)
|
||||
->count();
|
||||
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::RECIPIENT, $recipientCount);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::PLAN, $planCount);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::OUTCOME, $outcomeCount);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::CURRENT, $currentCount);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::DECEASED, $deceasedCount);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::EMERGENCY_SURGERY, $misEmergencySurgery + ($manualSurgicalCount[0] ?? 0));
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::PLAN_SURGERY, $misPlanSurgery + ($manualSurgicalCount[1] ?? 0));
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::EMERGENCY, $emergencyCount);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::TRANSFERRED, $transferredCount);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::OBSERVATION, $observationCount);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::DISCHARGED, $dischargedCount);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, $unwantedEventsCount);
|
||||
}
|
||||
|
||||
private function getBranchId(int $misDepartmentId): ?int
|
||||
{
|
||||
return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId)
|
||||
->value('StationarBranchID');
|
||||
}
|
||||
|
||||
private function countUniqueSnapshotsForTypes(int $reportId, array $patientTypes): int
|
||||
{
|
||||
return MedicalHistorySnapshot::query()
|
||||
->where('rf_report_id', $reportId)
|
||||
->whereIn('patient_type', $patientTypes)
|
||||
->get(['medical_history_snapshot_id', 'patient_uid', 'rf_medicalhistory_id'])
|
||||
->map(function (MedicalHistorySnapshot $snapshot) {
|
||||
return $snapshot->patient_uid
|
||||
?: ($snapshot->rf_medicalhistory_id
|
||||
? "mis:{$snapshot->rf_medicalhistory_id}"
|
||||
: "snapshot:{$snapshot->medical_history_snapshot_id}");
|
||||
})
|
||||
->unique()
|
||||
->count();
|
||||
}
|
||||
|
||||
public function getManualSurgicalCounts(Department $department, DateRange $dateRange): array
|
||||
{
|
||||
$baseQuery = DepartmentPatientOperation::query()
|
||||
->whereBetween('started_at', [$dateRange->startSql(), $dateRange->endSql()])
|
||||
->whereHas('patient', function ($query) use ($department) {
|
||||
$query->where('rf_department_id', $department->department_id)
|
||||
->whereIn('source_type', ['manual', 'special']);
|
||||
});
|
||||
|
||||
$emergencyCount = (clone $baseQuery)
|
||||
->where(function ($query) {
|
||||
$query->where('urgency', 'emergency')
|
||||
->orWhere(function ($fallback) {
|
||||
$fallback->whereNull('urgency')
|
||||
->whereHas('patient', fn ($patientQuery) => $patientQuery->where('patient_kind', 'emergency'));
|
||||
});
|
||||
})
|
||||
->count();
|
||||
|
||||
$planCount = (clone $baseQuery)
|
||||
->where(function ($query) {
|
||||
$query->where('urgency', 'plan')
|
||||
->orWhere(function ($fallback) {
|
||||
$fallback->whereNull('urgency')
|
||||
->whereHas('patient', fn ($patientQuery) => $patientQuery->where('patient_kind', 'plan'));
|
||||
});
|
||||
})
|
||||
->count();
|
||||
|
||||
return [$emergencyCount, $planCount];
|
||||
}
|
||||
}
|
||||
154
app/Infrastructure/Reports/Services/ReportMetricsFinalizer.php
Normal file
154
app/Infrastructure/Reports/Services/ReportMetricsFinalizer.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Infrastructure\Reports\Services;
|
||||
|
||||
use App\Domain\Reports\Calculators\BedDaysCalculator;
|
||||
use App\Domain\Reports\Calculators\DepartmentLoadCalculator;
|
||||
use App\Domain\Reports\Calculators\PreoperativeDaysCalculator;
|
||||
use App\Domain\Reports\Models\OperationInterval;
|
||||
use App\Domain\Reports\Models\StayInterval;
|
||||
use App\Domain\Reports\ValueObjects\MetrikaConfig;
|
||||
use App\Models\MedicalHistorySnapshot;
|
||||
use App\Models\Report;
|
||||
use DateTimeImmutable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ReportMetricsFinalizer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BedDaysCalculator $bedDaysCalculator,
|
||||
private readonly PreoperativeDaysCalculator $preoperativeDaysCalculator,
|
||||
private readonly DepartmentLoadCalculator $departmentLoadCalculator,
|
||||
private readonly ReportStorageService $reportStorageService,
|
||||
) {}
|
||||
|
||||
public function finalize(Report $report): void
|
||||
{
|
||||
$this->saveBedDaysMetrics($report);
|
||||
$this->savePreoperativeMetrics($report);
|
||||
$this->saveDepartmentLoadMetric($report);
|
||||
}
|
||||
|
||||
private function saveBedDaysMetrics(Report $report): void
|
||||
{
|
||||
$result = $this->bedDaysCalculator->calculate($this->buildStayIntervals($report));
|
||||
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::TOTAL_BED_DAYS, $result->total);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::AVERAGE_BED_DAYS, $result->average);
|
||||
}
|
||||
|
||||
private function savePreoperativeMetrics(Report $report): void
|
||||
{
|
||||
$result = $this->preoperativeDaysCalculator->calculate($this->buildOperationIntervals($report));
|
||||
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::TOTAL_PREOPERATIVE_DAYS, $result->total);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::PREOPERATIVE_PATIENT_COUNT, $result->count);
|
||||
$this->reportStorageService->saveMetric($report, MetrikaConfig::PREOPERATIVE_AVERAGE_DAYS, $result->average);
|
||||
}
|
||||
|
||||
private function saveDepartmentLoadMetric(Report $report): void
|
||||
{
|
||||
$currentCount = (float) ($report->metrikaResults()->where('rf_metrika_item_id', MetrikaConfig::CURRENT)->value('value') ?? 0);
|
||||
$bedsCount = (float) ($report->metrikaResults()->where('rf_metrika_item_id', MetrikaConfig::BEDS)->value('value') ?? 0);
|
||||
|
||||
$this->reportStorageService->saveMetric(
|
||||
$report,
|
||||
MetrikaConfig::DEPARTMENT_LOADED,
|
||||
$this->departmentLoadCalculator->calculate($currentCount, $bedsCount),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, StayInterval>
|
||||
*/
|
||||
private function buildStayIntervals(Report $report): array
|
||||
{
|
||||
$snapshots = MedicalHistorySnapshot::query()
|
||||
->where('rf_report_id', $report->report_id)
|
||||
->whereIn('patient_type', ['discharged', 'deceased'])
|
||||
->with('medicalHistory')
|
||||
->get();
|
||||
|
||||
$intervals = [];
|
||||
|
||||
foreach ($snapshots as $snapshot) {
|
||||
$history = $snapshot->medicalHistory;
|
||||
|
||||
if (! $history) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$startRaw = $history->DateRecipientHS ?? $history->DateRecipient ?? null;
|
||||
$endRaw = null;
|
||||
|
||||
if ($snapshot->patient_type === 'deceased') {
|
||||
if ($history->DateDeath && ! in_array($history->DateDeath->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) {
|
||||
$endRaw = $history->DateDeath;
|
||||
} elseif ($history->DateExtract && ! in_array($history->DateExtract->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) {
|
||||
$endRaw = $history->DateExtract;
|
||||
}
|
||||
} elseif ($history->DateExtract && ! in_array($history->DateExtract->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) {
|
||||
$endRaw = $history->DateExtract;
|
||||
}
|
||||
|
||||
if (! $startRaw || ! $endRaw) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$intervals[] = new StayInterval(
|
||||
startAt: new DateTimeImmutable((string) $startRaw),
|
||||
endAt: new DateTimeImmutable((string) $endRaw),
|
||||
);
|
||||
}
|
||||
|
||||
return $intervals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, OperationInterval>
|
||||
*/
|
||||
private function buildOperationIntervals(Report $report): array
|
||||
{
|
||||
$patientIds = MedicalHistorySnapshot::query()
|
||||
->where('rf_report_id', $report->report_id)
|
||||
->whereIn('patient_type', ['discharged', 'deceased'])
|
||||
->pluck('rf_medicalhistory_id')
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($patientIds->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = DB::table('stt_medicalhistory as mh')
|
||||
->join('stt_surgicaloperation as so', 'so.rf_MedicalHistoryID', '=', 'mh.MedicalHistoryID')
|
||||
->whereIn('mh.MedicalHistoryID', $patientIds)
|
||||
->whereNotNull('so.Date')
|
||||
->select(
|
||||
'mh.MedicalHistoryID',
|
||||
DB::raw('MIN(so."Date") as first_operation'),
|
||||
'mh.DateRecipientHS',
|
||||
'mh.DateRecipient'
|
||||
)
|
||||
->groupBy('mh.MedicalHistoryID', 'mh.DateRecipientHS', 'mh.DateRecipient')
|
||||
->get();
|
||||
|
||||
$intervals = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$startRaw = $row->DateRecipientHS ?? $row->DateRecipient ?? null;
|
||||
$operationRaw = $row->first_operation ?? null;
|
||||
|
||||
if (! $startRaw || ! $operationRaw) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$intervals[] = new OperationInterval(
|
||||
admittedAt: new DateTimeImmutable((string) $startRaw),
|
||||
operationAt: new DateTimeImmutable((string) $operationRaw),
|
||||
);
|
||||
}
|
||||
|
||||
return $intervals;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
<?php
|
||||
|
||||
namespace App\Infrastructure\Reports\Services;
|
||||
|
||||
use App\Data\UnifiedPatientData;
|
||||
use App\Models\Department;
|
||||
use App\Models\MedicalHistorySnapshot;
|
||||
use App\Models\User;
|
||||
use App\Services\DateRange;
|
||||
use App\Services\SnapshotService;
|
||||
use App\Services\UnifiedPatientService;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Сервис чтения пациентских выборок для отчётов.
|
||||
*
|
||||
* Он решает, откуда обслуживать запрос: из submitted-снапшотов или из live-реплики,
|
||||
* и при этом сохраняет API близким к старым методам ReportService для
|
||||
* постепенной strangler-миграции.
|
||||
*/
|
||||
class ReportPatientsReadService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UnifiedPatientService $unifiedPatientService,
|
||||
private readonly SnapshotService $snapshotService,
|
||||
private readonly ReportReadContextResolver $contextResolver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Получить пациентов отчёта по запрошенному статусу и области источника.
|
||||
*/
|
||||
public function getPatientsByStatus(
|
||||
Department $department,
|
||||
User $user,
|
||||
string $status,
|
||||
DateRange $dateRange,
|
||||
bool $onlyIds = false,
|
||||
bool $beforeCreate = false,
|
||||
?bool $includeCurrentPatients = null
|
||||
) {
|
||||
[$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
|
||||
$branchId = $this->contextResolver->resolveBranchId($department);
|
||||
|
||||
if (! $branchId) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
if ($sourceScope === 'special' || $baseStatus === 'reanimation') {
|
||||
return $this->getPatientsFromReplica(
|
||||
$department,
|
||||
$user,
|
||||
$status,
|
||||
$dateRange,
|
||||
$branchId,
|
||||
$onlyIds,
|
||||
$includeCurrentPatients
|
||||
);
|
||||
}
|
||||
|
||||
$useSnapshots = ! $this->contextResolver->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange)
|
||||
&& $this->contextResolver->shouldUseSnapshots($department, $dateRange, $beforeCreate);
|
||||
|
||||
if ($useSnapshots) {
|
||||
return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId, $onlyIds);
|
||||
}
|
||||
|
||||
return $this->getPatientsFromReplica(
|
||||
$department,
|
||||
$user,
|
||||
$status,
|
||||
$dateRange,
|
||||
$branchId,
|
||||
$onlyIds,
|
||||
$includeCurrentPatients
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Посчитать пациентов отчёта по запрошенному статусу и области источника.
|
||||
*/
|
||||
public function getPatientsCountByStatus(
|
||||
Department $department,
|
||||
User $user,
|
||||
string $status,
|
||||
DateRange $dateRange
|
||||
): int {
|
||||
[$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
|
||||
$branchId = $this->contextResolver->resolveBranchId($department);
|
||||
|
||||
if (! $branchId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($sourceScope === 'special' || $baseStatus === 'reanimation') {
|
||||
return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId);
|
||||
}
|
||||
|
||||
$useSnapshots = ! $this->contextResolver->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange)
|
||||
&& $this->contextResolver->shouldUseSnapshots($department, $dateRange);
|
||||
|
||||
if ($useSnapshots) {
|
||||
return $this->getPatientsCountFromSnapshots($department, $status, $dateRange, $branchId);
|
||||
}
|
||||
|
||||
return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Построить карту счётчиков пациентов по scope для интерфейса.
|
||||
*/
|
||||
public function getPatientsCountsMap(Department $department, User $user, DateRange $dateRange): array
|
||||
{
|
||||
$baseStatuses = [
|
||||
'plan',
|
||||
'emergency',
|
||||
'observation',
|
||||
'reanimation',
|
||||
'outcome-discharged',
|
||||
'outcome-deceased',
|
||||
'outcome-transferred',
|
||||
];
|
||||
|
||||
$counts = [
|
||||
'mis-plan' => 0,
|
||||
'mis-emergency' => 0,
|
||||
'mis-observation' => 0,
|
||||
'mis-reanimation' => 0,
|
||||
'mis-outcome' => 0,
|
||||
'mis-outcome-discharged' => 0,
|
||||
'mis-outcome-deceased' => 0,
|
||||
'mis-outcome-transferred' => 0,
|
||||
'special-plan' => 0,
|
||||
'special-emergency' => 0,
|
||||
'special-observation' => 0,
|
||||
'special-reanimation' => 0,
|
||||
'special-outcome' => 0,
|
||||
'special-outcome-discharged' => 0,
|
||||
'special-outcome-deceased' => 0,
|
||||
'special-outcome-transferred' => 0,
|
||||
];
|
||||
|
||||
foreach ($baseStatuses as $baseStatus) {
|
||||
$counts["mis-{$baseStatus}"] = $this->getPatientsCountByStatus(
|
||||
$department,
|
||||
$user,
|
||||
"mis-{$baseStatus}",
|
||||
$dateRange
|
||||
);
|
||||
$counts["special-{$baseStatus}"] = $this->getPatientsCountByStatus(
|
||||
$department,
|
||||
$user,
|
||||
"special-{$baseStatus}",
|
||||
$dateRange
|
||||
);
|
||||
}
|
||||
|
||||
$counts['mis-outcome'] = ($counts['mis-outcome-discharged'] ?? 0) + ($counts['mis-outcome-deceased'] ?? 0);
|
||||
$counts['special-outcome'] = ($counts['special-outcome-discharged'] ?? 0) + ($counts['special-outcome-deceased'] ?? 0);
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить пациентскую выборку из submitted-снапшотов.
|
||||
*/
|
||||
public function getPatientsFromSnapshots(
|
||||
Department $department,
|
||||
string $status,
|
||||
DateRange $dateRange,
|
||||
int $branchId,
|
||||
bool $onlyIds = false
|
||||
) {
|
||||
[$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
|
||||
$reportIds = $this->contextResolver
|
||||
->getReportsForDateRange($department->department_id, $dateRange)
|
||||
->pluck('report_id')
|
||||
->all();
|
||||
$recipientReportIds = $this->contextResolver->getRecipientReportIds($reportIds);
|
||||
|
||||
$patientTypeMap = [
|
||||
'plan' => 'plan',
|
||||
'emergency' => 'emergency',
|
||||
'current' => 'current',
|
||||
'recipient' => 'recipient',
|
||||
'outcome-discharged' => 'discharged',
|
||||
'outcome-transferred' => 'transferred',
|
||||
'outcome-deceased' => 'deceased',
|
||||
'observation' => 'observation',
|
||||
];
|
||||
|
||||
$patientType = $patientTypeMap[$baseStatus] ?? null;
|
||||
|
||||
if ($patientType === 'observation') {
|
||||
return $this->unifiedPatientService->getObservationPatients($department, $onlyIds, $sourceScope);
|
||||
}
|
||||
|
||||
if ($baseStatus === 'outcome') {
|
||||
$discharged = $this->snapshotService->getPatientsFromSnapshots(
|
||||
'discharged',
|
||||
$reportIds,
|
||||
false,
|
||||
false,
|
||||
$recipientReportIds
|
||||
);
|
||||
$deceased = $this->snapshotService->getPatientsFromSnapshots(
|
||||
'deceased',
|
||||
$reportIds,
|
||||
false,
|
||||
false,
|
||||
$recipientReportIds
|
||||
);
|
||||
|
||||
$merged = UnifiedPatientData::unique($discharged->concat($deceased))
|
||||
->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')
|
||||
->values();
|
||||
|
||||
return $this->filterSnapshotPatientsByScope($merged, $sourceScope, $onlyIds);
|
||||
}
|
||||
|
||||
if (! $patientType) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
if ($dateRange->isOneDay && in_array($baseStatus, ['plan', 'emergency'], true)) {
|
||||
$patients = $this->snapshotService->getPatientsFromOneDayCurrentSnapshots(
|
||||
$patientType,
|
||||
$reportIds,
|
||||
false,
|
||||
$recipientReportIds
|
||||
);
|
||||
|
||||
return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds);
|
||||
}
|
||||
|
||||
$patients = $this->snapshotService->getPatientsFromSnapshots(
|
||||
$patientType,
|
||||
$reportIds,
|
||||
false,
|
||||
in_array($baseStatus, ['plan', 'emergency'], true),
|
||||
$recipientReportIds
|
||||
);
|
||||
|
||||
return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить пациентов напрямую из live-реплики и manual-источников.
|
||||
*/
|
||||
private function getPatientsFromReplica(
|
||||
Department $department,
|
||||
User $user,
|
||||
string $status,
|
||||
DateRange $dateRange,
|
||||
int $branchId,
|
||||
bool $onlyIds = false,
|
||||
?bool $includeCurrent = null
|
||||
) {
|
||||
[$baseStatus] = $this->parseScopedStatus($status);
|
||||
$includeCurrent ??= in_array($baseStatus, ['plan', 'emergency', 'reanimation'], true);
|
||||
|
||||
return $this->unifiedPatientService->getLivePatientsByStatus(
|
||||
$department,
|
||||
$user,
|
||||
$status,
|
||||
$dateRange,
|
||||
$branchId,
|
||||
$onlyIds,
|
||||
$includeCurrent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Посчитать пациентов в снапшотах с той же семантикой scope, что и в legacy-сервисе.
|
||||
*/
|
||||
private function getPatientsCountFromSnapshots(
|
||||
Department $department,
|
||||
string $status,
|
||||
DateRange $dateRange,
|
||||
int $branchId
|
||||
): int {
|
||||
[$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
|
||||
$reportIds = $this->contextResolver
|
||||
->getReportsForDateRange($department->department_id, $dateRange)
|
||||
->pluck('report_id')
|
||||
->all();
|
||||
|
||||
if ($baseStatus === 'outcome') {
|
||||
if ($sourceScope !== 'all') {
|
||||
return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId)->count();
|
||||
}
|
||||
|
||||
return MedicalHistorySnapshot::query()
|
||||
->whereIn('rf_report_id', $reportIds)
|
||||
->whereIn('patient_type', ['discharged', 'deceased'])
|
||||
->distinct('rf_medicalhistory_id')
|
||||
->count('rf_medicalhistory_id');
|
||||
}
|
||||
|
||||
$patientTypeMap = [
|
||||
'plan' => 'plan',
|
||||
'emergency' => 'emergency',
|
||||
'observation' => 'observation',
|
||||
'outcome-discharged' => 'discharged',
|
||||
'outcome-transferred' => 'transferred',
|
||||
'outcome-deceased' => 'deceased',
|
||||
];
|
||||
|
||||
$patientType = $patientTypeMap[$baseStatus] ?? null;
|
||||
|
||||
if (! $patientType) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($patientType === 'observation') {
|
||||
return $this->unifiedPatientService->getObservationPatients($department, false, $sourceScope)->count();
|
||||
}
|
||||
|
||||
if ($sourceScope !== 'all') {
|
||||
return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId)->count();
|
||||
}
|
||||
|
||||
return MedicalHistorySnapshot::query()
|
||||
->whereIn('rf_report_id', $reportIds)
|
||||
->where('patient_type', $patientType)
|
||||
->distinct('rf_medicalhistory_id')
|
||||
->count('rf_medicalhistory_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Посчитать пациентов из реплики и manual-источников с legacy-правилами include-current.
|
||||
*/
|
||||
private function getPatientsCountFromReplica(
|
||||
Department $department,
|
||||
User $user,
|
||||
string $status,
|
||||
DateRange $dateRange,
|
||||
int $branchId
|
||||
): int {
|
||||
[$baseStatus] = $this->parseScopedStatus($status);
|
||||
|
||||
return match ($status) {
|
||||
'plan', 'emergency', 'observation', 'reanimation', 'outcome', 'outcome-discharged', 'outcome-transferred', 'outcome-deceased', 'current', 'recipient' => $this->unifiedPatientService->getLivePatientCountByStatus(
|
||||
$department,
|
||||
$user,
|
||||
$status,
|
||||
$dateRange,
|
||||
$branchId,
|
||||
in_array($status, ['plan', 'emergency'], true)
|
||||
),
|
||||
default => $this->unifiedPatientService->getLivePatientCountByStatus(
|
||||
$department,
|
||||
$user,
|
||||
$status,
|
||||
$dateRange,
|
||||
$branchId,
|
||||
in_array($baseStatus, ['plan', 'emergency'], true)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Применить фильтрацию по MIS/manual scope к коллекции DTO из снапшотов.
|
||||
*/
|
||||
private function filterSnapshotPatientsByScope(Collection $patients, string $sourceScope, bool $onlyIds = false)
|
||||
{
|
||||
if ($sourceScope === 'all') {
|
||||
return $onlyIds ? $patients->pluck('id') : $patients;
|
||||
}
|
||||
|
||||
$filtered = $patients->filter(function ($patient) use ($sourceScope) {
|
||||
return match ($sourceScope) {
|
||||
'mis' => $patient->sourceType === 'mis',
|
||||
'special' => in_array($patient->sourceType, ['manual', 'special'], true),
|
||||
default => true,
|
||||
};
|
||||
})->values();
|
||||
|
||||
return $onlyIds ? $filtered->pluck('id') : $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Разбить scoped-статус вроде "mis-plan" на базовый статус и scope источника.
|
||||
*
|
||||
* @return array{0: string, 1: string}
|
||||
*/
|
||||
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'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Infrastructure\Reports\Services;
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\MisStationarBranch;
|
||||
use App\Models\Report;
|
||||
use App\Models\User;
|
||||
use App\Services\DateRange;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Разрешает контекст отчётного периода, необходимый для read-side сервисов.
|
||||
*
|
||||
* Класс хранит в одном месте правила поиска отчётов по периоду и решения
|
||||
* snapshot-vs-replica, чтобы не дублировать их по контроллерам и сервисам.
|
||||
*/
|
||||
class ReportReadContextResolver
|
||||
{
|
||||
/**
|
||||
* Определить MIS branch id для отчётного отделения.
|
||||
*/
|
||||
public function resolveBranchId(Department $department): ?int
|
||||
{
|
||||
return MisStationarBranch::query()
|
||||
->where('rf_DepartmentID', $department->rf_mis_department_id)
|
||||
->value('StationarBranchID');
|
||||
}
|
||||
|
||||
/**
|
||||
* Определить, нужно ли читать submitted-снапшоты вместо live-данных.
|
||||
*/
|
||||
public function shouldUseSnapshots(
|
||||
Department $department,
|
||||
DateRange $dateRange,
|
||||
bool $beforeCreate = false
|
||||
): bool {
|
||||
if ($beforeCreate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$report = $this->getReportForPeriod($department->department_id, $dateRange);
|
||||
|
||||
return $report?->status === 'submitted';
|
||||
}
|
||||
|
||||
/**
|
||||
* Для самых изменчивых статусов врачи должны продолжать видеть live-данные за текущие сутки.
|
||||
*/
|
||||
public function shouldUseReplicaForLiveStatus(User $user, string $status, DateRange $dateRange): bool
|
||||
{
|
||||
if ($user->isHeadOfDepartment() || $user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($status, ['plan', 'emergency', 'recipient', 'current', 'reanimation'], true)
|
||||
&& $dateRange->isOneDay
|
||||
&& $dateRange->isEndDateToday();
|
||||
}
|
||||
|
||||
/**
|
||||
* Вернуть submitted-отчёты, относящиеся к выбранному отчётному окну.
|
||||
*
|
||||
* @return Collection<int, Report>
|
||||
*/
|
||||
public function getReportsForDateRange(int $departmentId, DateRange $dateRange): Collection
|
||||
{
|
||||
if ($dateRange->isOneDay) {
|
||||
return Report::query()
|
||||
->where('rf_department_id', $departmentId)
|
||||
->exactPeriod($dateRange->startSql(), $dateRange->endSql())
|
||||
->onlySubmitted()
|
||||
->orderBy('period_end', 'DESC')
|
||||
->get();
|
||||
}
|
||||
|
||||
return Report::query()
|
||||
->where('rf_department_id', $departmentId)
|
||||
->withinPeriod($dateRange->startSql(), $dateRange->endSql())
|
||||
->onlySubmitted()
|
||||
->orderBy('period_end', 'DESC')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recipient-снапшоты читаются из последнего отчёта в выбранном окне.
|
||||
*
|
||||
* @param array<int, int> $reportIds
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getRecipientReportIds(array $reportIds): array
|
||||
{
|
||||
if (empty($reportIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [reset($reportIds)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти отчёт, который определяет видимость снапшотов для запрошенного периода.
|
||||
*/
|
||||
private function getReportForPeriod(int $departmentId, DateRange $dateRange): ?Report
|
||||
{
|
||||
$query = Report::query()
|
||||
->where('rf_department_id', $departmentId)
|
||||
->exactPeriod($dateRange->startSql(), $dateRange->endSql())
|
||||
->orderByDesc('report_id');
|
||||
|
||||
if ($dateRange->isOneDay) {
|
||||
return $query->first();
|
||||
}
|
||||
|
||||
return $query->onlySubmitted()->first();
|
||||
}
|
||||
}
|
||||
166
app/Infrastructure/Reports/Services/ReportStorageService.php
Normal file
166
app/Infrastructure/Reports/Services/ReportStorageService.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Infrastructure\Reports\Services;
|
||||
|
||||
use App\Domain\Reports\Models\ReportSnapshot;
|
||||
use App\Domain\Reports\ValueObjects\MetrikaConfig;
|
||||
use App\Models\Department;
|
||||
use App\Models\MetrikaResult;
|
||||
use App\Models\ObservationPatient;
|
||||
use App\Models\Report;
|
||||
use App\Models\UnwantedEvent;
|
||||
use App\Models\User;
|
||||
|
||||
class ReportStorageService
|
||||
{
|
||||
public function createOrUpdateReport(ReportSnapshot $snapshot, User $actor): Report
|
||||
{
|
||||
$reportData = [
|
||||
'rf_department_id' => $snapshot->departmentId,
|
||||
'rf_user_id' => $actor->id,
|
||||
'rf_lpudoctor_id' => $snapshot->userId,
|
||||
'sent_at' => $snapshot->sentAt?->format('Y-m-d H:i:s') ?? $snapshot->periodEnd->format('Y-m-d H:i:s'),
|
||||
'period_start' => $snapshot->periodStart->format('Y-m-d H:i:s'),
|
||||
'period_end' => $snapshot->periodEnd->format('Y-m-d H:i:s'),
|
||||
'created_at' => $snapshot->createdAt?->format('Y-m-d H:i:s') ?? $snapshot->periodEnd->format('Y-m-d H:i:s'),
|
||||
'status' => $snapshot->status,
|
||||
];
|
||||
|
||||
if ($snapshot->reportId) {
|
||||
return Report::query()->updateOrCreate(
|
||||
['report_id' => $snapshot->reportId],
|
||||
$reportData,
|
||||
);
|
||||
}
|
||||
|
||||
$report = Report::query()->create($reportData);
|
||||
$department = Department::query()->find($snapshot->departmentId);
|
||||
$beds = $department?->metrikaDefault->where('rf_metrika_item_id', MetrikaConfig::BEDS)->first();
|
||||
|
||||
if ($beds) {
|
||||
MetrikaResult::query()->updateOrCreate(
|
||||
[
|
||||
'rf_report_id' => $report->report_id,
|
||||
'rf_metrika_item_id' => MetrikaConfig::BEDS,
|
||||
],
|
||||
['value' => $beds->value]
|
||||
);
|
||||
}
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
public function saveMetrics(Report $report, ReportSnapshot $snapshot): void
|
||||
{
|
||||
foreach ($snapshot->normalizedMetrics() as $metricId => $value) {
|
||||
MetrikaResult::query()->updateOrCreate(
|
||||
[
|
||||
'rf_report_id' => $report->report_id,
|
||||
'rf_metrika_item_id' => $metricId,
|
||||
],
|
||||
['value' => $value]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function saveMetric(Report $report, int $metricId, int|float $value): void
|
||||
{
|
||||
MetrikaResult::query()->updateOrCreate(
|
||||
[
|
||||
'rf_report_id' => $report->report_id,
|
||||
'rf_metrika_item_id' => $metricId,
|
||||
],
|
||||
['value' => $value]
|
||||
);
|
||||
}
|
||||
|
||||
public function saveUnwantedEvents(Report $report, ReportSnapshot $snapshot): void
|
||||
{
|
||||
if ($snapshot->unwantedEvents === []) {
|
||||
$report->unwantedEvents()->delete();
|
||||
$this->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, 0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$report->unwantedEvents()
|
||||
->whereNotIn('unwanted_event_id', array_values(array_filter(array_map(
|
||||
static fn (array $event): ?int => isset($event['unwanted_event_id']) ? (int) $event['unwanted_event_id'] : null,
|
||||
$snapshot->unwantedEvents
|
||||
))))
|
||||
->delete();
|
||||
|
||||
foreach ($snapshot->unwantedEvents as $event) {
|
||||
if (! empty($event['unwanted_event_id'])) {
|
||||
UnwantedEvent::query()->updateOrCreate(
|
||||
['unwanted_event_id' => (int) $event['unwanted_event_id']],
|
||||
[
|
||||
'rf_report_id' => $report->report_id,
|
||||
'comment' => (string) ($event['comment'] ?? ''),
|
||||
'title' => (string) ($event['title'] ?? ''),
|
||||
'is_visible' => (bool) ($event['is_visible'] ?? true),
|
||||
]
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
UnwantedEvent::query()->create([
|
||||
'rf_report_id' => $report->report_id,
|
||||
'comment' => (string) ($event['comment'] ?? ''),
|
||||
'title' => (string) ($event['title'] ?? ''),
|
||||
'is_visible' => (bool) ($event['is_visible'] ?? true),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, count($snapshot->unwantedEvents));
|
||||
}
|
||||
|
||||
public function saveObservationPatients(Report $report, ReportSnapshot $snapshot): void
|
||||
{
|
||||
if ($snapshot->observationPatients === []) {
|
||||
ObservationPatient::query()
|
||||
->where('rf_department_id', $snapshot->departmentId)
|
||||
->where('rf_report_id', $report->report_id)
|
||||
->delete();
|
||||
$this->saveMetric($report, MetrikaConfig::OBSERVATION, 0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$observedKeys = [];
|
||||
|
||||
foreach ($snapshot->observationPatients as $patient) {
|
||||
$medicalHistoryId = isset($patient['medical_history_id']) ? (int) $patient['medical_history_id'] : null;
|
||||
$departmentPatientId = isset($patient['department_patient_id']) ? (int) $patient['department_patient_id'] : null;
|
||||
$observedKeys[] = $medicalHistoryId.'-'.$departmentPatientId;
|
||||
|
||||
ObservationPatient::query()->updateOrCreate(
|
||||
[
|
||||
'rf_medicalhistory_id' => $medicalHistoryId,
|
||||
'rf_department_patient_id' => $departmentPatientId,
|
||||
'rf_department_id' => $snapshot->departmentId,
|
||||
],
|
||||
[
|
||||
'rf_report_id' => $report->report_id,
|
||||
'rf_mkab_id' => null,
|
||||
'comment' => $patient['comment'] ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
ObservationPatient::query()
|
||||
->where('rf_department_id', $snapshot->departmentId)
|
||||
->where('rf_report_id', $report->report_id)
|
||||
->get()
|
||||
->filter(fn (ObservationPatient $patient) => ! in_array(
|
||||
($patient->rf_medicalhistory_id ?? '').'-'.($patient->rf_department_patient_id ?? ''),
|
||||
$observedKeys,
|
||||
true
|
||||
))
|
||||
->each
|
||||
->delete();
|
||||
|
||||
$this->saveMetric($report, MetrikaConfig::OBSERVATION, count($snapshot->observationPatients));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Infrastructure\Reports\Services;
|
||||
|
||||
use App\Data\UnifiedPatientData;
|
||||
use App\Domain\Reports\ValueObjects\MetrikaConfig;
|
||||
use App\Models\MedicalHistorySnapshot;
|
||||
use App\Models\MetrikaResult;
|
||||
use App\Models\Report;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SnapshotPersistenceService
|
||||
{
|
||||
/**
|
||||
* Сохранить метрики, полученные из снапшотов, с идемпотентным upsert.
|
||||
*
|
||||
* @param array<int, int|float|string|null> $metrics
|
||||
*/
|
||||
public function saveMetrics(Report $report, array $metrics): void
|
||||
{
|
||||
foreach ($metrics as $metrikaItemId => $value) {
|
||||
MetrikaResult::updateOrCreate(
|
||||
[
|
||||
'rf_report_id' => $report->report_id,
|
||||
'rf_metrika_item_id' => $metrikaItemId,
|
||||
],
|
||||
[
|
||||
'value' => $value,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function createSnapshotsForType(Report $report, string $type, Collection $patients): void
|
||||
{
|
||||
foreach ($patients as $patient) {
|
||||
if (! $patient instanceof UnifiedPatientData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MedicalHistorySnapshot::updateOrCreate(
|
||||
[
|
||||
'rf_report_id' => $report->report_id,
|
||||
'patient_uid' => $patient->patientUid,
|
||||
'patient_type' => $type,
|
||||
],
|
||||
[
|
||||
'rf_report_id' => $report->report_id,
|
||||
'patient_type' => $type,
|
||||
...$patient->toSnapshotPayload($type),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить ранее построенные снапшоты перед полной перестройкой состояния отчёта.
|
||||
*/
|
||||
public function clearReportSnapshots(Report $report): void
|
||||
{
|
||||
MedicalHistorySnapshot::query()
|
||||
->where('rf_report_id', $report->report_id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Сопоставить типы snapshot-пациентов с идентификаторами сохраняемых метрик.
|
||||
*
|
||||
* @return array<string, int>
|
||||
*/
|
||||
public function snapshotMetricMap(): array
|
||||
{
|
||||
return [
|
||||
'plan' => MetrikaConfig::PLAN,
|
||||
'emergency' => MetrikaConfig::EMERGENCY,
|
||||
'discharged' => MetrikaConfig::DISCHARGED,
|
||||
'transferred' => MetrikaConfig::TRANSFERRED,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user