diff --git a/app/Application/Reports/ReportSavePathService.php b/app/Application/Reports/ReportSavePathService.php index e79d5eb..2007b43 100644 --- a/app/Application/Reports/ReportSavePathService.php +++ b/app/Application/Reports/ReportSavePathService.php @@ -3,11 +3,12 @@ namespace App\Application\Reports; use App\Application\Reports\DTO\GenerateReportResult; +use App\Infrastructure\Reports\Services\AutoFillReportPayloadBuilder; +use App\Infrastructure\Reports\Services\ReportSaveOrchestrator; use App\Models\Department; use App\Models\Report; use App\Models\User; use App\Services\DateRange; -use App\Services\ReportService; final readonly class ReportSavePathService { @@ -15,7 +16,8 @@ final readonly class ReportSavePathService private ReportFlowDecider $reportFlowDecider, private ReportInputFactory $reportInputFactory, private GenerateReportUseCase $generateReportUseCase, - private ReportService $reportService, + private ReportSaveOrchestrator $reportSaveOrchestrator, + private AutoFillReportPayloadBuilder $autoFillReportPayloadBuilder, ) {} public function usesNewArchitecture(string $reportType = 'daily'): bool @@ -29,7 +31,7 @@ final readonly class ReportSavePathService public function saveManual(User $actor, array $validated, string $reportType = 'daily'): GenerateReportResult|Report { if (! $this->usesNewArchitecture($reportType)) { - return $this->reportService->storeReport($validated, $actor, false); + return $this->reportSaveOrchestrator->storeReport($validated, $actor, false); } return $this->generateReportUseCase->handle( @@ -44,9 +46,9 @@ final readonly class ReportSavePathService string $reportType = 'daily', ): GenerateReportResult|Report { if (! $this->usesNewArchitecture($reportType)) { - $payload = $this->reportService->buildAutoFillReportPayload($scopedUser, $department, $dateRange); + $payload = $this->autoFillReportPayloadBuilder->build($scopedUser, $department, $dateRange); - return $this->reportService->storeReport($payload, $scopedUser, true); + return $this->reportSaveOrchestrator->storeReport($payload, $scopedUser, true); } return $this->generateReportUseCase->handle( diff --git a/app/Console/Commands/RecalculatePreoperativeMetric.php b/app/Console/Commands/RecalculatePreoperativeMetric.php index e0d1928..6ac7aaa 100644 --- a/app/Console/Commands/RecalculatePreoperativeMetric.php +++ b/app/Console/Commands/RecalculatePreoperativeMetric.php @@ -226,13 +226,16 @@ class RecalculatePreoperativeMetric extends Command $count = 0; foreach ($operations as $op) { - $days = Carbon::parse($op->first_admission) - ->diffInDays(Carbon::parse($op->first_operation)); + $admittedAt = Carbon::parse($op->first_admission); + $operationAt = Carbon::parse($op->first_operation); - if ($days >= 0) { - $totalDays += $days; - $count++; + if ($operationAt->lt($admittedAt)) { + continue; } + + $totalDays += $admittedAt->copy()->startOfDay() + ->diffInDays($operationAt->copy()->startOfDay()); + $count++; } $avgDays = $count > 0 ? round($totalDays / $count, 1) : 0; diff --git a/app/Domain/Reports/ValueObjects/MetrikaConfig.php b/app/Domain/Reports/ValueObjects/MetrikaConfig.php index 7c8d33d..b982c39 100644 --- a/app/Domain/Reports/ValueObjects/MetrikaConfig.php +++ b/app/Domain/Reports/ValueObjects/MetrikaConfig.php @@ -30,6 +30,8 @@ final class MetrikaConfig public const UNWANTED_EVENTS = 16; + public const STAFF_COUNT = 17; + public const AVERAGE_BED_DAYS = 18; public const PREOPERATIVE_AVERAGE_DAYS = 21; diff --git a/app/Http/Controllers/Api/NurseController.php b/app/Http/Controllers/Api/NurseController.php new file mode 100644 index 0000000..fbc5557 --- /dev/null +++ b/app/Http/Controllers/Api/NurseController.php @@ -0,0 +1,57 @@ +first(); + } + + public function searchPatients(Request $request) + { + $search = $request->search; + + return MedicalHistory::whereLike('full_name', $search . '%') + ->orderBy('recipient_date', 'desc') + ->get()->map(function ($item) { + return [ + 'label' => "$item->medical_card_number - $item->full_name", + 'value' => $item->id + ]; + }); + } + + public function storePatient(Request $request) + { + $data = $request->validate([ + 'source_type' => 'nullable', + 'medical_card_number' => 'nullable', + 'full_name' => 'required', + 'birth_date' => 'required', + 'recipient_date' => 'required', + 'extract_date' => 'nullable', + 'death_date' => 'nullable', + 'male' => 'nullable', + 'urgency_id' => 'required', + 'hospital_result_id' => 'nullable', + 'visit_result_id' => 'required', + 'mis_user_id' => 'nullable', + 'comment' => 'nullable', + ]); + + $data['user_id'] = auth()->user()->id; + + $result = MedicalHistoryNurse::create($data); + + return response()->json([ + 'data' => $result, + ], 201); + } +} diff --git a/app/Http/Controllers/Web/NurseReportController.php b/app/Http/Controllers/Web/NurseReportController.php new file mode 100644 index 0000000..0de11ed --- /dev/null +++ b/app/Http/Controllers/Web/NurseReportController.php @@ -0,0 +1,60 @@ +query('departmentId', $user->department->department_id); + $department = Department::where('department_id', $departmentId)->firstOrFail(); + $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); + + $inDepartmentHistories = $this->medicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id); + $recipientHistories = $this->medicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id); + $dischargedHistories = $this->medicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id); + $deceasedHistories = $this->medicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id); + $transferredHistories = $this->medicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id); + + return Inertia::render('Nurse/Report/Index', [ + 'inDepartmentHistories' => $inDepartmentHistories, + 'recipientHistories' => $recipientHistories, + 'dischargedHistories' => $dischargedHistories, + 'deceasedHistories' => $deceasedHistories, + 'transferredHistories' => $transferredHistories, + 'dates' => [ + $dateRange->startDate->getTimestampMs(), + $dateRange->endDate->getTimestampMs(), + ] + ]); + } + + /** + * Сохранение отчета от роли мед. сестра + * @return void + */ + public function store() + { + + } +} diff --git a/app/Infrastructure/Reports/Adapters/LegacyReportServiceAdapter.php b/app/Infrastructure/Reports/Adapters/LegacyReportServiceAdapter.php index 96c1958..0538253 100644 --- a/app/Infrastructure/Reports/Adapters/LegacyReportServiceAdapter.php +++ b/app/Infrastructure/Reports/Adapters/LegacyReportServiceAdapter.php @@ -5,26 +5,30 @@ namespace App\Infrastructure\Reports\Adapters; use App\Application\Reports\DTO\GenerateReportInput; use App\Domain\Reports\Models\ReportSnapshot; use App\Domain\Reports\ValueObjects\MetrikaConfig; +use App\Infrastructure\Reports\Services\AutoFillReportPayloadBuilder; +use App\Infrastructure\Reports\Services\ReportRuntimeService; +use App\Infrastructure\Reports\Services\ReportSaveOrchestrator; use App\Models\Department; use App\Models\Report; use App\Models\User; use App\Services\DateRange; use App\Services\DateRangeService; -use App\Services\ReportService; use App\Services\SnapshotService; use DateTimeImmutable; class LegacyReportServiceAdapter { public function __construct( - private readonly ReportService $reportService, private readonly SnapshotService $snapshotService, private readonly DateRangeService $dateRangeService, + private readonly AutoFillReportPayloadBuilder $autoFillReportPayloadBuilder, + private readonly ReportRuntimeService $reportRuntimeService, + private readonly ReportSaveOrchestrator $reportSaveOrchestrator, ) {} public function prepareMemoryForHeavySave(): void { - $this->reportService->prepareForHeavySave(); + $this->reportRuntimeService->prepareForHeavySave(); } /** @@ -82,7 +86,7 @@ class LegacyReportServiceAdapter public function syncCalculatedMetrics(Report $report, User $user, ReportSnapshot $snapshot): void { - $this->reportService->syncCalculatedMetricsForStoredReport( + $this->reportSaveOrchestrator->syncCalculatedMetrics( $report, $user, [ @@ -96,22 +100,22 @@ class LegacyReportServiceAdapter public function finalizeStoredReport(Report $report): void { - $this->reportService->finalizeStoredReport($report); + $this->reportSaveOrchestrator->finalizeStoredReport($report); } public function saveLethalMetricFromSnapshots(Report $report): void { - $this->reportService->saveLethalMetricForStoredReport($report); + $this->reportSaveOrchestrator->saveLethalMetricFromSnapshots($report); } public function clearCacheAfterReportCreation(User $user, Report $report): void { - $this->reportService->clearCacheAfterStoredReport($user, $report); + $this->reportRuntimeService->clearCacheAfterReportCreation($user, $report); } public function buildAutoFillPayload(User $user, Department $department, DateRange $dateRange): array { - return $this->reportService->buildAutoFillReportPayload($user, $department, $dateRange); + return $this->autoFillReportPayloadBuilder->build($user, $department, $dateRange); } /** diff --git a/app/Infrastructure/Reports/Services/ManualPatientManagementService.php b/app/Infrastructure/Reports/Services/ManualPatientManagementService.php new file mode 100644 index 0000000..4e9f515 --- /dev/null +++ b/app/Infrastructure/Reports/Services/ManualPatientManagementService.php @@ -0,0 +1,256 @@ + $data + */ + public function createManualPatient(Department $department, User $user, array $data): DepartmentPatient + { + $report = $this->resolveReportForManualPatient($department, $user, $data); + + return $this->unifiedPatientService->createManualPatient($department, $user, $data, $report->report_id); + } + + /** + * @param array $data + */ + public function setManualPatientOutcome(User $user, int $departmentPatientId, array $data): DepartmentPatient + { + $patient = DepartmentPatient::query() + ->where('department_patient_id', $departmentPatientId) + ->firstOrFail(); + + $updatedPatient = $this->unifiedPatientService->recordManualOutcome($patient, $data); + $this->syncManualPatientSnapshots($updatedPatient, $user, []); + + return $updatedPatient; + } + + /** + * @param array $data + */ + public function updateManualPatient(User $user, int $departmentPatientId, array $data): DepartmentPatient + { + $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); + + $updatedPatient = $this->unifiedPatientService->updateManualPatient($patient, $data); + $this->syncManualPatientSnapshots($updatedPatient, $user, $data); + + return $updatedPatient; + } + + public function linkManualPatientToMis(int $departmentPatientId, int $medicalHistoryId): DepartmentPatient + { + $patient = DepartmentPatient::query() + ->where('department_patient_id', $departmentPatientId) + ->firstOrFail(); + + return $this->unifiedPatientService->linkManualPatientToMis($patient, $medicalHistoryId); + } + + /** + * @return Collection + */ + public function getManualPatientOperations(User $user, int $departmentPatientId): Collection + { + $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); + + return $patient->operations() + ->with('serviceMedical') + ->orderByDesc('started_at') + ->get(); + } + + /** + * @param array $data + */ + public function createManualPatientOperation( + User $user, + int $departmentPatientId, + array $data + ): DepartmentPatientOperation { + $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); + $service = $this->resolveMedicalService((int) $data['service_id']); + + return $patient->operations()->create([ + 'rf_kl_service_medical_id' => $service->ServiceMedicalID, + 'service_code' => $service->ServiceMedicalCode, + 'service_name' => $service->ServiceMedicalName, + 'urgency' => $data['urgency'], + 'started_at' => $data['started_at'], + 'ended_at' => $data['ended_at'], + 'created_by' => $user->id, + ])->load('serviceMedical'); + } + + /** + * @param array $data + */ + public function updateManualPatientOperation( + User $user, + int $departmentPatientId, + int $operationId, + array $data + ): DepartmentPatientOperation { + $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); + $service = $this->resolveMedicalService((int) $data['service_id']); + + $operation = $patient->operations() + ->where('department_patient_operation_id', $operationId) + ->firstOrFail(); + + $operation->update([ + 'rf_kl_service_medical_id' => $service->ServiceMedicalID, + 'service_code' => $service->ServiceMedicalCode, + 'service_name' => $service->ServiceMedicalName, + 'urgency' => $data['urgency'], + 'started_at' => $data['started_at'], + 'ended_at' => $data['ended_at'], + ]); + + return $operation->fresh()->load('serviceMedical'); + } + + public function deleteManualPatientOperation(User $user, int $departmentPatientId, int $operationId): void + { + $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); + + $patient->operations() + ->where('department_patient_operation_id', $operationId) + ->firstOrFail() + ->delete(); + } + + private function resolveManageableManualPatient(User $user, int $departmentPatientId): DepartmentPatient + { + $query = DepartmentPatient::query() + ->where('department_patient_id', $departmentPatientId) + ->whereIn('source_type', ['manual', 'special']); + + if (! $user->isAdmin() && ! $user->isHeadOfDepartment()) { + $query->where('rf_department_id', $user->department->department_id); + } + + return $query->firstOrFail(); + } + + /** + * @param array $data + */ + private function syncManualPatientSnapshots(DepartmentPatient $patient, User $user, array $data): void + { + $reportIds = $patient->rf_report_id + ? [$patient->rf_report_id] + : (isset($data['startAt'], $data['endAt']) && $data['startAt'] && $data['endAt'] + ? $this->contextResolver + ->getReportsForDateRange( + $patient->rf_department_id, + $this->dateRangeService->getNormalizedDateRange( + $user, + (string) $data['startAt'], + (string) $data['endAt'] + ) + ) + ->pluck('report_id') + ->values() + ->all() + : []); + + if (empty($reportIds)) { + return; + } + + MedicalHistorySnapshot::query() + ->whereIn('rf_report_id', $reportIds) + ->where('rf_department_patient_id', $patient->department_patient_id) + ->update([ + 'patient_kind' => $patient->patient_kind, + 'full_name' => $patient->full_name, + 'birth_date' => $patient->birth_date, + 'diagnosis_code' => $patient->diagnosis_code, + 'diagnosis_name' => $patient->diagnosis_name, + 'admitted_at' => $patient->admitted_at, + 'outcome_type' => $patient->is_current ? null : $patient->outcome_type, + 'outcome_at' => $patient->is_current ? null : $patient->outcome_at, + 'updated_at' => now(), + ]); + } + + /** + * @param array $data + */ + private function resolveReportForManualPatient(Department $department, User $user, array $data): Report + { + $reportId = $data['report_id'] ?? null; + if ($reportId) { + return Report::query() + ->where('report_id', $reportId) + ->where('rf_department_id', $department->department_id) + ->firstOrFail(); + } + + if (! isset($data['startAt'], $data['endAt']) || ! $data['startAt'] || ! $data['endAt']) { + throw new InvalidArgumentException('Не указан отчет или диапазон для привязки спецконтингента'); + } + + $dateRange = $this->dateRangeService->getNormalizedDateRange( + $user, + (string) $data['startAt'], + (string) $data['endAt'] + ); + + $existingReport = Report::query() + ->where('rf_department_id', $department->department_id) + ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) + ->first(); + + if ($existingReport) { + return $existingReport; + } + + return Report::query()->create([ + 'rf_department_id' => $department->department_id, + 'rf_user_id' => $user->id, + 'rf_lpudoctor_id' => $data['user_id'] ?? $user->rf_lpudoctor_id, + 'sent_at' => $dateRange->endSql(), + 'created_at' => $dateRange->endSql(), + 'period_start' => $dateRange->startSql(), + 'period_end' => $dateRange->endSql(), + 'status' => 'draft', + ]); + } + + private function resolveMedicalService(int $serviceId): MisServiceMedical + { + return MisServiceMedical::query() + ->where('ServiceMedicalID', $serviceId) + ->firstOrFail(); + } +} diff --git a/app/Infrastructure/Reports/Services/ObservationPatientManagementService.php b/app/Infrastructure/Reports/Services/ObservationPatientManagementService.php new file mode 100644 index 0000000..56aee32 --- /dev/null +++ b/app/Infrastructure/Reports/Services/ObservationPatientManagementService.php @@ -0,0 +1,28 @@ +where('rf_department_patient_id', $id) + ->delete(); + + return; + } + + ObservationPatient::query() + ->where('rf_medicalhistory_id', $id) + ->delete(); + } +} diff --git a/app/Infrastructure/Reports/Services/ReanimationIndicatorService.php b/app/Infrastructure/Reports/Services/ReanimationIndicatorService.php new file mode 100644 index 0000000..9caf3d0 --- /dev/null +++ b/app/Infrastructure/Reports/Services/ReanimationIndicatorService.php @@ -0,0 +1,80 @@ +create([ + 'rf_department_id' => $departmentId, + 'rf_report_id' => $reportId, + 'rf_medicalhistory_id' => $medicalHistoryId, + 'indicator' => $indicator, + 'comment' => $comment, + 'created_by' => $user->id, + ]); + } + + /** + * @param array $medicalHistoryIds + * @return Collection + */ + public function latestByMedicalHistory(int $departmentId, array $medicalHistoryIds): Collection + { + if (empty($medicalHistoryIds)) { + return collect(); + } + + $subQuery = ReanimationPatientIndicator::query() + ->selectRaw('MAX(reanimation_patient_indicator_id) as max_id, rf_medicalhistory_id') + ->where('rf_department_id', $departmentId) + ->whereIn('rf_medicalhistory_id', $medicalHistoryIds) + ->groupBy('rf_medicalhistory_id'); + + return ReanimationPatientIndicator::query() + ->joinSub($subQuery, 'latest', function ($join) { + $join->on('reanimation_patient_indicators.reanimation_patient_indicator_id', '=', 'latest.max_id'); + }) + ->get([ + 'reanimation_patient_indicators.rf_medicalhistory_id', + 'reanimation_patient_indicators.indicator', + 'reanimation_patient_indicators.comment', + ]) + ->keyBy('rf_medicalhistory_id'); + } + + /** + * @return Collection + */ + public function history(int $departmentId, int $medicalHistoryId, int $limit = 50): Collection + { + return ReanimationPatientIndicator::query() + ->where('rf_department_id', $departmentId) + ->where('rf_medicalhistory_id', $medicalHistoryId) + ->orderByDesc('reanimation_patient_indicator_id') + ->limit($limit) + ->get([ + 'reanimation_patient_indicator_id', + 'rf_report_id', + 'rf_medicalhistory_id', + 'indicator', + 'comment', + 'created_by', + 'created_at', + ]); + } +} diff --git a/app/Infrastructure/Reports/Services/ReportClinicalSearchService.php b/app/Infrastructure/Reports/Services/ReportClinicalSearchService.php new file mode 100644 index 0000000..c1b07f7 --- /dev/null +++ b/app/Infrastructure/Reports/Services/ReportClinicalSearchService.php @@ -0,0 +1,22 @@ +unifiedPatientService->searchMisPatients($department, $query); + } +} diff --git a/app/Infrastructure/Reports/Services/ReportMetadataReadService.php b/app/Infrastructure/Reports/Services/ReportMetadataReadService.php new file mode 100644 index 0000000..c1d0613 --- /dev/null +++ b/app/Infrastructure/Reports/Services/ReportMetadataReadService.php @@ -0,0 +1,168 @@ +contextResolver->resolveReportForPeriod($department->department_id, $dateRange); + + $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); + $useSnapshots = $isHeadOrAdmin || ! $dateRange->isEndDateToday() || $reportToday; + + if ($useSnapshots && $isHeadOrAdmin && $reportToday) { + $fillableUserId = $reportToday->rf_lpudoctor_id ?? null; + } else { + $fillableUserId = request()->query('userId', $user->rf_lpudoctor_id); + } + + $unwantedEvents = $this->getUnwantedEvents($department, $dateRange); + $isActiveSendButton = $this->isSendButtonActive($user, $dateRange, $reportToday); + + $message = null; + if ($reportToday) { + $reportDoctor = $reportToday->lpuDoctor; + $message = "Отчет создан пользователем: $reportDoctor->FAM_V $reportDoctor->IM_V $reportDoctor->OT_V"; + } + + $statusMessage = $reportToday + ? ($reportToday->status === 'submitted' + ? 'Этот отчет в статусе: опубликован' + : 'Этот отчет в статусе: черновик') + : null; + + $lpuDoctor = $this->getDoctorInfo($fillableUserId, $dateRange); + $date = $isHeadOrAdmin ? [ + $dateRange->startDate->getTimestampMs(), + $dateRange->endDate->getTimestampMs(), + ] : $dateRange->endDate->getTimestampMs(); + + return [ + 'report_id' => $reportToday?->report_id, + 'unwantedEvents' => $unwantedEvents, + 'isActiveSendButton' => $isActiveSendButton, + 'message' => $dateRange->isOneDay ? $message : null, + 'status' => $reportToday?->status ?? 'draft', + 'statusMessage' => $dateRange->isOneDay ? $statusMessage : null, + 'canPublish' => (bool) $reportToday && ($reportToday->status === 'draft') && $isActiveSendButton, + 'isOneDay' => $dateRange->isOneDay, + 'isHeadOrAdmin' => $isHeadOrAdmin, + 'dates' => $date, + 'userId' => $fillableUserId, + 'userName' => $lpuDoctor ? "$lpuDoctor->FAM_V $lpuDoctor->IM_V $lpuDoctor->OT_V" : null, + ]; + } + + /** + * @return Collection> + */ + public function getUnwantedEvents(Department $department, DateRange $dateRange): Collection + { + return UnwantedEvent::query() + ->whereHas('report', function ($query) use ($department, $dateRange) { + $query->where('rf_department_id', $department->department_id); + + if ($dateRange->isOneDay) { + $query->exactPeriod($dateRange->startSql(), $dateRange->endSql()); + } else { + $query->withinPeriod($dateRange->startSql(), $dateRange->endSql()); + } + }) + ->get() + ->map(function (UnwantedEvent $item) { + return [ + ...$item->toArray(), + 'created_at' => Carbon::parse($item->created_at)->format('Создано d.m.Y в H:i'), + ]; + }); + } + + /** + * @return Collection + */ + public function getReportsForDateRange(int $departmentId, DateRange $dateRange): Collection + { + return $this->contextResolver->getReportsForDateRange($departmentId, $dateRange); + } + + public function getRecipientPlanOfYear(Department $department, DateRange $dateRange): array + { + $periodPlanModel = $department->recipientPlanOfYear(); + $monthsInPeriod = ceil($dateRange->startDate->diffInMonths($dateRange->endDate)); + $annualPlan = $periodPlanModel ? (int) $periodPlanModel->value : 0; + $oneMonthPlan = ceil($annualPlan / 12); + $periodPlan = round($oneMonthPlan * $monthsInPeriod); + + $query = $department->reports() + ->with('metrikaResults') + ->where('period_start', '>', $dateRange->startSql()) + ->where('period_end', '<=', $dateRange->endSql()); + + if ($dateRange->isOneDay) { + $query->where('period_start', '>=', $dateRange->startFirstOfMonth()) + ->where('period_end', '<=', $dateRange->endSql()); + } else { + $query->where('period_start', '>', $dateRange->startSql()) + ->where('period_end', '<=', $dateRange->endSql()); + } + + $progress = 0; + + foreach ($query->get() as $report) { + $outcome = $report->metrikaResults() + ->where('rf_metrika_item_id', MetrikaConfig::OUTCOME) + ->first(); + + if ($outcome) { + $progress += (int) $outcome->value; + } + } + + return [ + 'plan' => $periodPlan, + 'progress' => $progress, + ]; + } + + private function isSendButtonActive(User $user, DateRange $dateRange, ?Report $reportToday): bool + { + if (! $user->isHeadOfDepartment() && ! $user->isAdmin()) { + if ($reportToday && $reportToday->status === 'submitted') { + return false; + } + + return $dateRange->isEndDateToday(); + } + + return (bool) $reportToday && $dateRange->isOneDay; + } + + private function getDoctorInfo(?int $doctorId, DateRange $dateRange): ?MisLpuDoctor + { + if (! $doctorId || ! $dateRange->isOneDay) { + return null; + } + + return MisLpuDoctor::query() + ->where('LPUDoctorID', $doctorId) + ->first(); + } +} diff --git a/app/Infrastructure/Reports/Services/ReportMetricsFinalizer.php b/app/Infrastructure/Reports/Services/ReportMetricsFinalizer.php index 3a4a73b..93239f1 100644 --- a/app/Infrastructure/Reports/Services/ReportMetricsFinalizer.php +++ b/app/Infrastructure/Reports/Services/ReportMetricsFinalizer.php @@ -10,6 +10,7 @@ use App\Domain\Reports\Models\StayInterval; use App\Domain\Reports\ValueObjects\MetrikaConfig; use App\Models\MedicalHistorySnapshot; use App\Models\Report; +use DateTimeInterface; use DateTimeImmutable; use Illuminate\Support\Facades\DB; @@ -82,12 +83,12 @@ class ReportMetricsFinalizer $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)) { + if ($this->isRealDate($history->DateDeath)) { $endRaw = $history->DateDeath; - } elseif ($history->DateExtract && ! in_array($history->DateExtract->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { + } elseif ($this->isRealDate($history->DateExtract)) { $endRaw = $history->DateExtract; } - } elseif ($history->DateExtract && ! in_array($history->DateExtract->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { + } elseif ($this->isRealDate($history->DateExtract)) { $endRaw = $history->DateExtract; } @@ -104,6 +105,19 @@ class ReportMetricsFinalizer return $intervals; } + private function isRealDate(mixed $value): bool + { + if (! $value) { + return false; + } + + $date = $value instanceof DateTimeInterface + ? $value->format('Y-m-d') + : (new DateTimeImmutable((string) $value))->format('Y-m-d'); + + return ! in_array($date, ['1900-01-01', '2222-01-01'], true); + } + /** * @return array */ diff --git a/app/Infrastructure/Reports/Services/ReportPatientsReadService.php b/app/Infrastructure/Reports/Services/ReportPatientsReadService.php index d43f333..ce05752 100644 --- a/app/Infrastructure/Reports/Services/ReportPatientsReadService.php +++ b/app/Infrastructure/Reports/Services/ReportPatientsReadService.php @@ -235,6 +235,7 @@ class ReportPatientsReadService $patients = $this->snapshotService->getPatientsFromSnapshots( $patientType, $reportIds, + $branchId, false, in_array($baseStatus, ['plan', 'emergency'], true), $recipientReportIds diff --git a/app/Infrastructure/Reports/Services/ReportReadContextResolver.php b/app/Infrastructure/Reports/Services/ReportReadContextResolver.php index 256e24c..9a22611 100644 --- a/app/Infrastructure/Reports/Services/ReportReadContextResolver.php +++ b/app/Infrastructure/Reports/Services/ReportReadContextResolver.php @@ -101,6 +101,14 @@ class ReportReadContextResolver * Найти отчёт, который определяет видимость снапшотов для запрошенного периода. */ private function getReportForPeriod(int $departmentId, DateRange $dateRange): ?Report + { + return $this->resolveReportForPeriod($departmentId, $dateRange); + } + + /** + * Найти отчёт для точного периода с теми же правилами, что использует legacy read-side. + */ + public function resolveReportForPeriod(int $departmentId, DateRange $dateRange): ?Report { $query = Report::query() ->where('rf_department_id', $departmentId) diff --git a/app/Infrastructure/Reports/Services/ReportRuntimeService.php b/app/Infrastructure/Reports/Services/ReportRuntimeService.php new file mode 100644 index 0000000..3009426 --- /dev/null +++ b/app/Infrastructure/Reports/Services/ReportRuntimeService.php @@ -0,0 +1,64 @@ +getConnectionName(), + (new MisMigrationPatient)->getConnectionName(), + (new MisStationarBranch)->getConnectionName(), + ])); + + foreach ($connectionNames as $connectionName) { + try { + $connection = DB::connection($connectionName); + $connection->disableQueryLog(); + $connection->flushQueryLog(); + } catch (\Throwable) { + // best-effort cleanup only + } + } + + if (function_exists('gc_collect_cycles')) { + gc_collect_cycles(); + } + } + + public function clearCacheAfterReportCreation(User $user, Report $report): void + { + $this->clearDailyCache($user, $report->created_at); + } + + private function clearDailyCache(User $user, mixed $reportDate): void + { + $datesToClear = [ + Carbon::parse($reportDate)->format('Y-m-d'), + Carbon::parse($reportDate)->subDay()->format('Y-m-d'), + ]; + + foreach ($datesToClear as $date) { + Cache::forget($this->generateDailyCacheKey($user, $date)); + } + } + + private function generateDailyCacheKey(User $user, string $date): string + { + return 'daily_stats:'.$user->rf_department_id.':'.$date; + } +} diff --git a/app/Infrastructure/Reports/Services/ReportSaveOrchestrator.php b/app/Infrastructure/Reports/Services/ReportSaveOrchestrator.php new file mode 100644 index 0000000..9f59492 --- /dev/null +++ b/app/Infrastructure/Reports/Services/ReportSaveOrchestrator.php @@ -0,0 +1,164 @@ + $data + */ + public function storeReport(array $data, User $user, bool $fillableAuto = false): Report + { + $this->reportRuntimeService->prepareForHeavySave(); + $snapshot = $this->buildSnapshot($data, $user, $fillableAuto); + + $report = DB::transaction(function () use ($snapshot, $user, $data, $fillableAuto) { + $report = $this->reportStorageService->createOrUpdateReport($snapshot, $user); + $this->reportStorageService->saveMetrics($report, $snapshot); + $this->reportStorageService->saveUnwantedEvents($report, $snapshot); + $this->reportStorageService->saveObservationPatients($report, $snapshot); + + $this->snapshotService->createPatientSnapshots( + $report, + $user, + [ + $snapshot->periodStart->getTimestamp(), + $snapshot->periodEnd->getTimestamp(), + ], + $fillableAuto + ); + + $this->syncCalculatedMetrics($report, $user, $data); + + return $report; + }); + + DB::transaction(function () use ($report) { + $this->finalizeStoredReport($report); + $this->saveLethalMetricFromSnapshots($report); + }); + + $this->reportRuntimeService->clearCacheAfterReportCreation($user, $report); + + return $report; + } + + public function syncCalculatedMetrics(Report $report, User $user, array $data): void + { + $this->calculatedMetricsSynchronizer->sync($report, $user, $data); + } + + public function finalizeStoredReport(Report $report): void + { + $this->reportMetricsFinalizer->finalize($report); + } + + public function saveLethalMetricFromSnapshots(Report $report): void + { + $snapshots = MedicalHistorySnapshot::query() + ->where('rf_report_id', $report->report_id) + ->whereIn('patient_type', ['discharged', 'deceased']) + ->with('medicalHistory') + ->get(); + + if ($snapshots->isNotEmpty()) { + return; + } + + MetrikaResult::query()->updateOrCreate( + [ + 'rf_report_id' => $report->report_id, + 'rf_metrika_item_id' => MetrikaConfig::AVERAGE_BED_DAYS, + ], + ['value' => 0] + ); + + \Log::info("No discharged patients in report {$report->report_id}, saved 0"); + } + + private function buildSnapshot(array $data, User $user, bool $fillableAuto): ReportSnapshot + { + $dateRange = $this->dateRangeService->getNormalizedDateRange( + $user, + (string) ($data['dates'][0] ?? null), + (string) ($data['dates'][1] ?? null) + ); + + $rangeEndAt = $dateRange->endSql(); + $createdAt = $data['created_at'] ?? $rangeEndAt; + $sentAt = $data['sent_at'] ?? $rangeEndAt; + + return new ReportSnapshot( + departmentId: (int) $data['departmentId'], + userId: (int) $data['userId'], + actorUserId: (int) $user->id, + periodStart: new DateTimeImmutable($dateRange->startSql()), + periodEnd: new DateTimeImmutable($dateRange->endSql()), + status: (string) ($data['status'] ?? 'draft'), + autoFill: $fillableAuto, + metrics: MetrikaConfig::normalizeMetrics((array) ($data['metrics'] ?? [])), + observationPatients: $this->normalizeObservationPatients((array) ($data['observationPatients'] ?? [])), + unwantedEvents: $this->normalizeUnwantedEvents((array) ($data['unwantedEvents'] ?? [])), + reportId: isset($data['reportId']) && $data['reportId'] ? (int) $data['reportId'] : null, + createdAt: new DateTimeImmutable((string) $createdAt), + sentAt: new DateTimeImmutable((string) $sentAt), + ); + } + + /** + * @param array> $patients + * @return array> + */ + private function normalizeObservationPatients(array $patients): array + { + return array_values(array_map(static function (array $patient): array { + return [ + 'medical_history_id' => $patient['medical_history_id'] ?? $patient['id'] ?? null, + 'department_patient_id' => $patient['department_patient_id'] ?? null, + 'comment' => $patient['comment'] ?? null, + ]; + }, $patients)); + } + + /** + * @param array> $events + * @return array> + */ + private function normalizeUnwantedEvents(array $events): array + { + return array_values(array_map(static function (array $event): array { + return [ + 'unwanted_event_id' => $event['unwanted_event_id'] ?? null, + 'title' => $event['title'] ?? '', + 'comment' => $event['comment'] ?? '', + 'is_visible' => (bool) ($event['is_visible'] ?? true), + ]; + }, $events)); + } +} diff --git a/app/Infrastructure/Reports/Services/ReportStatisticsReadService.php b/app/Infrastructure/Reports/Services/ReportStatisticsReadService.php new file mode 100644 index 0000000..f24a842 --- /dev/null +++ b/app/Infrastructure/Reports/Services/ReportStatisticsReadService.php @@ -0,0 +1,213 @@ +contextResolver->resolveBranchId($department); + + if (! $branchId) { + return $this->emptyStatistics(); + } + + if ($this->contextResolver->shouldUseSnapshots($department, $dateRange)) { + return $this->getStatisticsFromSnapshots($department, $dateRange); + } + + return $this->getStatisticsFromReplica($department, $user, $dateRange, $branchId); + } + + /** + * Получить статистику из сохранённых снапшотов submitted-отчётов. + */ + private function getStatisticsFromSnapshots(Department $department, DateRange $dateRange): array + { + $reports = $this->contextResolver->getReportsForDateRange( + $department->department_id, + $dateRange + ); + + $reportIds = $reports->pluck('report_id')->all(); + $lastReportId = $reportIds[0] ?? null; + $recipientReportIds = $this->contextResolver->getRecipientReportIds($reportIds); + + $snapshotStats = [ + 'plan' => $this->getMetrikaResultCount(MetrikaConfig::PLAN, $reportIds), + 'emergency' => $this->getMetrikaResultCount(MetrikaConfig::EMERGENCY, $reportIds), + 'outcome' => $this->getMetrikaResultCount(MetrikaConfig::OUTCOME, $reportIds), + 'deceased' => $this->getMetrikaResultCount(MetrikaConfig::DECEASED, $reportIds), + 'current' => $this->getMetrikaResultCount(MetrikaConfig::CURRENT, $reportIds, false), + 'transferred' => $this->getMetrikaResultCount(MetrikaConfig::TRANSFERRED, $reportIds), + 'recipient' => $this->getMetrikaResultCount(MetrikaConfig::RECIPIENT, $reportIds), + 'beds' => $this->getMetrikaResultCount(MetrikaConfig::BEDS, $reportIds, false), + 'countStaff' => $lastReportId + ? $this->getMetrikaResultCount(MetrikaConfig::STAFF_COUNT, [$lastReportId], false) + : 0, + ]; + + $recipientIds = $this->snapshotService + ->getPatientsFromSnapshots('recipient', $recipientReportIds) + ->pluck('id') + ->all(); + + $surgicalCount = [ + $this->getMetrikaResultCount(MetrikaConfig::EMERGENCY_SURGERY, $reportIds), + $this->getMetrikaResultCount(MetrikaConfig::PLAN_SURGERY, $reportIds), + ]; + + return [ + 'recipientCount' => $snapshotStats['recipient'] ?? 0, + 'extractCount' => $snapshotStats['outcome'] ?? 0, + 'currentCount' => $snapshotStats['current'] ?? 0, + 'deadCount' => $snapshotStats['deceased'] ?? 0, + 'countStaff' => $snapshotStats['countStaff'] ?? 0, + 'surgicalCount' => $surgicalCount, + 'recipientIds' => $recipientIds, + 'beds' => $snapshotStats['beds'] ?? 0, + 'percentDead' => $this->calculatePercentDead($snapshotStats['deceased'], $snapshotStats['outcome']), + ]; + } + + /** + * Получить статистику из live-реплики МИС и manual-источников. + */ + private function getStatisticsFromReplica( + Department $department, + User $user, + DateRange $dateRange, + int $branchId + ): array { + $planCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'plan', $dateRange, $branchId, true); + $emergencyCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'emergency', $dateRange, $branchId, true); + $currentCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'current', $dateRange, $branchId); + $recipientCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'recipient', $dateRange, $branchId); + $outcomeCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'outcome', $dateRange, $branchId); + $deadCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'outcome-deceased', $dateRange, $branchId); + + $misSurgicalCount = [ + $this->patientService->getSurgicalPatients('emergency', $branchId, $dateRange, true), + $this->patientService->getSurgicalPatients('plan', $branchId, $dateRange, true), + ]; + $manualSurgicalCount = $this->calculatedMetricsSynchronizer->getManualSurgicalCounts($department, $dateRange); + $surgicalCount = [ + ($misSurgicalCount[0] ?? 0) + ($manualSurgicalCount[0] ?? 0), + ($misSurgicalCount[1] ?? 0) + ($manualSurgicalCount[1] ?? 0), + ]; + + $recipientIds = $this->unifiedPatientService + ->getRecipientIdsForReport($department, $user, $dateRange, $branchId); + + return [ + 'recipientCount' => $recipientCount, + 'extractCount' => $outcomeCount, + 'currentCount' => $currentCount, + 'deadCount' => $deadCount, + 'surgicalCount' => $surgicalCount, + 'recipientIds' => $recipientIds, + 'planCount' => $planCount, + 'emergencyCount' => $emergencyCount, + 'percentDead' => $this->calculatePercentDead($deadCount, $outcomeCount), + 'beds' => (int) ($department->metrikaDefault + ->where('rf_metrika_item_id', MetrikaConfig::BEDS) + ->first() + ?->value ?? 0), + ]; + } + + /** + * Получить агрегированное значение метрики из набора отчётов. + * + * @param array $reportIds + */ + private function getMetrikaResultCount(int $metrikaItemId, array $reportIds, bool $sum = true): int + { + if (empty($reportIds)) { + return 0; + } + + $reports = Report::query() + ->whereIn('report_id', $reportIds) + ->with('metrikaResults') + ->orderBy('created_at', 'DESC') + ->get(); + + if (! $sum) { + foreach ($reports as $report) { + $metric = $report->metrikaResults + ->firstWhere('rf_metrika_item_id', $metrikaItemId); + + if ($metric) { + return (int) $metric->value; + } + } + + return 0; + } + + $count = 0; + + foreach ($reports as $report) { + foreach ($report->metrikaResults as $metrikaResult) { + if ((int) $metrikaResult->rf_metrika_item_id === $metrikaItemId) { + $count += (int) $metrikaResult->value; + } + } + } + + return $count; + } + + private function calculatePercentDead(int $deadCount, int $outcomeCount): float|int + { + if ($outcomeCount === 0) { + return 0; + } + + return round(($deadCount / $outcomeCount) * 100, 2); + } + + private function emptyStatistics(): array + { + return [ + 'recipientCount' => 0, + 'extractCount' => 0, + 'currentCount' => 0, + 'deadCount' => 0, + 'surgicalCount' => [0, 0], + 'recipientIds' => [], + 'planCount' => 0, + 'emergencyCount' => 0, + 'percentDead' => 0, + 'beds' => 0, + 'countStaff' => 0, + ]; + } +} diff --git a/app/Infrastructure/Reports/Sources/MisClinicalDataSource.php b/app/Infrastructure/Reports/Sources/MisClinicalDataSource.php index d2d71d0..930a98e 100644 --- a/app/Infrastructure/Reports/Sources/MisClinicalDataSource.php +++ b/app/Infrastructure/Reports/Sources/MisClinicalDataSource.php @@ -138,6 +138,10 @@ class MisClinicalDataSource $reanimationByMedicalHistory = MisReanimation::query() ->join('stt_migrationpatient as mp', 'mp.MigrationPatientID', '=', 'stt_reanimation.rf_MigrationPatientID') ->where('mp.rf_StationarBranchID', $branchId) + ->where(function ($q) use ($dateRange) { + $q->where('stt_reanimation.DateIn', '<=', $dateRange->endSql()) + ->where('stt_reanimation.DateOut', '>=', $dateRange->startSql()); + }) ->where('mp.rf_MedicalHistoryID', '<>', 0) ->whereIn('mp.rf_MedicalHistoryID', $reportCohortIds) ->selectRaw(' diff --git a/app/Models/MaterializedViewModel.php b/app/Models/MaterializedViewModel.php new file mode 100644 index 0000000..bab01f6 --- /dev/null +++ b/app/Models/MaterializedViewModel.php @@ -0,0 +1,22 @@ +hasMany(MigrationPatient::class, 'medical_history_id', 'id'); + } + + public function operations(): \Illuminate\Database\Eloquent\Relations\HasMany|MedicalHistory + { + return $this->hasMany(SurgicalOperation::class, 'medical_history_id', 'id'); + } + + public function latestMigration() + { + return $this->hasOne(MigrationPatient::class, 'medical_history_id', 'id') + ->latest('ingoing_date'); + } + + public function operationsInDepartment($query, $departmentId) + { + return $this->operations()->where('department_id', $departmentId); + } + + // Скоупы + public function scopeUrgency($query, $typeId) // 1 = Экстренно, 2 = Планово + { + return $query->where('urgency_id', $typeId); + } +} diff --git a/app/Models/MedicalHistoryCorrection.php b/app/Models/MedicalHistoryCorrection.php new file mode 100644 index 0000000..5c27897 --- /dev/null +++ b/app/Models/MedicalHistoryCorrection.php @@ -0,0 +1,34 @@ + 'datetime', + 'recipient_date' => 'datetime', + 'extract_date' => 'datetime', + 'death_date' => 'datetime', + ]; + + public function medicalHistory() + { + return $this->belongsTo(MedicalHistory::class); + } +} diff --git a/app/Models/MedicalHistoryNurse.php b/app/Models/MedicalHistoryNurse.php new file mode 100644 index 0000000..615be32 --- /dev/null +++ b/app/Models/MedicalHistoryNurse.php @@ -0,0 +1,30 @@ +belongsTo(User::class); + } +} diff --git a/app/Models/MetrikaItem.php b/app/Models/MetrikaItem.php index a98a77e..a29dd16 100644 --- a/app/Models/MetrikaItem.php +++ b/app/Models/MetrikaItem.php @@ -3,7 +3,9 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Spatie\Sluggable\Attributes\Sluggable; +#[Sluggable(from: 'name', to: 'code')] class MetrikaItem extends Model { protected $primaryKey = 'metrika_item_id'; @@ -16,5 +18,6 @@ class MetrikaItem extends Model 'description', 'data_type', 'is_active', + 'code' ]; } diff --git a/app/Models/MigrationPatient.php b/app/Models/MigrationPatient.php new file mode 100644 index 0000000..b59bdfc --- /dev/null +++ b/app/Models/MigrationPatient.php @@ -0,0 +1,105 @@ +belongsTo(MedicalHistory::class, 'medical_history_id', 'id'); + } + + public function operations() + { + return $this->hasMany(SurgicalOperation::class, 'migration_patient_id', 'id'); + } + + // Пересечение с отчетным периодом + public function scopeDateRange($query, string $from, string $to) + { + return $query->where('ingoing_date', '<=', $to) + ->where(function ($q) use ($from) { + $q->whereNull('out_date')->orWhere('out_date', '>=', $from); + }); + } + + // Фильтр по подразделению (получает ID отделений) + public function scopeDepartment($query, int $departmentId) + { + $branchIds = DB::table('stt_stationarbranch') + ->where('rf_DepartmentID', $departmentId) + ->pluck('StationarBranchID'); + + return $query->whereIn('stationar_branch_id', $branchIds); + } + + // Добавляет вычисляемый столбец `category` (только для отображения) + public function scopeWithCategory($query, string $from, string $to) + { + $sql = "CASE + WHEN ingoing_date BETWEEN ? AND ? THEN 'admitted' + WHEN out_date BETWEEN ? AND ? THEN + CASE + WHEN death_date IS NOT NULL AND death_date BETWEEN ? AND ? THEN 'deceased' + WHEN stat_cure_result_id IN (3,4,7) THEN 'transferred' + ELSE 'discharged' + END + WHEN ingoing_date < ? AND (out_date IS NULL OR out_date > ?) THEN 'current' + ELSE 'historical' + END as category"; + + return $query->selectRaw($sql, [$from, $to, $from, $to, $from, $to, $to, $to]); + } + + // Быстрые фильтры по статусам (используют индексы MV, а не computed column) + public function scopeAdmitted($query, string $from, string $to) + { + return $query->where('ingoing_date', '>', $from) + ->where('ingoing_date', '<=', $to); + } + + public function scopeDischarged($query, string $from, string $to) + { + return $query->where('out_date', '>', $from) + ->where('out_date', '<=', $to) + ->whereNotIn('stat_cure_result_id', [3, 4, 7]) + ->whereNull('death_date'); // умершие не считаются "выбывшими домой" + } + + public function scopeTransferred($query, string $from, string $to) + { + return $query->where('out_date', '>', $from) + ->where('out_date', '<=', $to) + ->whereIn('stat_cure_result_id', [3, 4, 7]); + } + + public function scopeDeceased($query, string $from, string $to) + { + return $query->whereNotNull('death_date') + ->where('death_date', '>', $from) + ->where('death_date', '<=', $to); + } + + public function scopeCurrent($query, DateRange $dateRange) + { +// return $query->where('ingoing_date', '<', $dateRange->startSql()) +// ->where(function ($q) use ($dateRange) { +// $q->whereNull('out_date')->orWhere('out_date', '>', $dateRange->endSql()); +// }); +// return $query->where('ingoing_date', '<=', $dateRange->endSql()) +// ->has('medicalHistory') +// ->where(function ($q) use ($dateRange) { +// $q->whereNull('out_date') +// ->orWhere('out_date', '>=', $dateRange->startSql()) +// ->where('out_date', '<=', $dateRange->endSql()); +// }); + return $query->where('is_actually_current', true); + } +} diff --git a/app/Models/ReportNurse.php b/app/Models/ReportNurse.php new file mode 100644 index 0000000..a58e518 --- /dev/null +++ b/app/Models/ReportNurse.php @@ -0,0 +1,27 @@ +hasMany(ReportNursePatient::class, 'report_nurse_id'); + } + + public function status() + { + return $this->belongsTo(ReportStatus::class); + } +} diff --git a/app/Models/ReportPatientStatus.php b/app/Models/ReportPatientStatus.php new file mode 100644 index 0000000..cedac39 --- /dev/null +++ b/app/Models/ReportPatientStatus.php @@ -0,0 +1,19 @@ +belongsTo(MedicalHistory::class, 'medical_history_id', 'id'); + } + + public function migration(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(MigrationPatient::class, 'migration_patient_id', 'id'); + } +} diff --git a/app/Models/UnifiedMedicalHistory.php b/app/Models/UnifiedMedicalHistory.php new file mode 100644 index 0000000..e182cd7 --- /dev/null +++ b/app/Models/UnifiedMedicalHistory.php @@ -0,0 +1,19 @@ + 'date', + 'recipient_date' => 'datetime', + 'extract_date' => 'datetime', + 'death_date' => 'datetime', + 'male' => 'boolean', + ]; +} diff --git a/app/Models/User.php b/app/Models/User.php index af71637..23dee59 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -12,11 +12,12 @@ use Illuminate\Notifications\Notifiable; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Laravel\Sanctum\HasApiTokens; +use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasApiTokens, HasFactory, Notifiable; + use HasApiTokens, HasFactory, Notifiable, HasRoles; /** * The attributes that are mass assignable. diff --git a/app/Services/AutoReportService.php b/app/Services/AutoReportService.php index dd39cb5..0d966c0 100644 --- a/app/Services/AutoReportService.php +++ b/app/Services/AutoReportService.php @@ -17,10 +17,28 @@ use Illuminate\Support\Facades\Log; class AutoReportService { public function __construct( - protected ReportService $reportService, - protected DateRangeService $dateRangeService, - protected ReportSavePathService $reportSavePathService, - ) {} + mixed $dateRangeService = null, + mixed $reportSavePathService = null, + ?ReportSavePathService $legacyReportSavePathService = null, + ) { + if ($dateRangeService instanceof ReportService) { + $this->dateRangeService = $reportSavePathService instanceof DateRangeService + ? $reportSavePathService + : app(DateRangeService::class); + $this->reportSavePathService = $legacyReportSavePathService ?? app(ReportSavePathService::class); + + return; + } + + $this->dateRangeService = $dateRangeService instanceof DateRangeService + ? $dateRangeService + : app(DateRangeService::class); + $this->reportSavePathService = $reportSavePathService ?? app(ReportSavePathService::class); + } + + protected DateRangeService $dateRangeService; + + protected ReportSavePathService $reportSavePathService; /** * Заполнить отчеты для пользователя за период @@ -34,16 +52,11 @@ class AutoReportService ): int { $createdCount = 0; - // Для многодневного диапазона расширяем конец на 1 день, - // чтобы покрыть последние сутки (07:00 -> 07:00) целиком. $start = \Carbon\Carbon::createFromFormat('Y-m-d', $startDate, 'Asia/Yakutsk'); $end = \Carbon\Carbon::createFromFormat('Y-m-d', $endDate, 'Asia/Yakutsk'); - $periodEnd = $start->equalTo($end) - ? $end->copy() - : $end->copy()->addDay(); // Создаем период по дням - $period = CarbonPeriod::create($start->toDateString(), $periodEnd->toDateString()); + $period = CarbonPeriod::create($start->toDateString(), $end->toDateString()); foreach ($period as $date) { $dateRange = $this->dateRangeService->getNormalizedDateRange($user, $date, $date); diff --git a/app/Services/DateRange.php b/app/Services/DateRange.php index c5b4576..5e15c8c 100644 --- a/app/Services/DateRange.php +++ b/app/Services/DateRange.php @@ -65,12 +65,12 @@ readonly class DateRange public function startFirstOfMonth() { - return $this->startDate->copy()->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s'); + return $this->startDate->copy()->firstOfMonth()->setHour(9)->format('Y-m-d H:i:s'); } public function endFirstOfMonth() { - return $this->endDate->copy()->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s'); + return $this->endDate->copy()->firstOfMonth()->setHour(9)->format('Y-m-d H:i:s'); } /** diff --git a/app/Services/MedicalHistoryService.php b/app/Services/MedicalHistoryService.php new file mode 100644 index 0000000..164fc32 --- /dev/null +++ b/app/Services/MedicalHistoryService.php @@ -0,0 +1,117 @@ +where('recipient_date', '>=', $dateRange->startSql()) + ->where('recipient_date', '<', $dateRange->endSql()) + // 1. Оставляем только тех пациентов, у которых БЫЛО движение в этом отделении + ->whereHas('latestMigration', fn($q) => $q->where('department_id', $departmentId)) + + // 2. Загружаем ТОЛЬКО последнее движение в этом отделении (не все миграции) + ->with(['latestMigration' => fn($q) => $q->where('department_id', $departmentId)]); + + $result = $query->paginate(); + + return $result; + } + + public function getUrgencyHistory(DateRange $dateRange, int $departmentId, int $urgencyId) + { + $query = MedicalHistory::query(); + + $query->where('recipient_date', '>=', $dateRange->startSql()) + ->where('recipient_date', '<', $dateRange->endSql()) + ->urgency($urgencyId) + ->whereHas('migrations', function ($m) use ($departmentId) { + $m->where('department_id', $departmentId); + }) + ->with([ + 'migrations' => fn ($m) => $m->where('department_id', $departmentId), + 'migrations.operations' + ]); + + $result = $query->paginate(); + + return $result; + } + + public function getDepartmentHistories(DateRange $dateRange, int $departmentId) + { + return MedicalHistory::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->current($dateRange); + }) + ->with(['latestMigration' => function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->current($dateRange); // подгружаем только отфильтрованные движения + }]) + ->get() + // Сортировка по дате поступления в отделение (поле дочерней таблицы) + ->sortByDesc(fn ($mh) => $mh->latestMigration->ingoing_date ?? $mh->recipient_date) + ->values(); + } + + /** + * Получить карты поступившие сегодня + * @param DateRange $dateRange + * @param int $departmentId + */ + public function getRecipientHistories(DateRange $dateRange, int $departmentId) + { + $now = Carbon::now(); + + return MedicalHistory::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->admitted($dateRange->startSql(), $dateRange->endSql()); + }) + ->with(['latestMigration' => function ($q) use ($departmentId) { + $q->department($departmentId); + }]) + ->get(); + } + + public function getDischargedHistories(DateRange $dateRange, int $departmentId) + { + return MedicalHistory::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->discharged($dateRange->startSql(), $dateRange->endSql()); + }) + ->with(['latestMigration' => function ($q) use ($departmentId) { + $q->department($departmentId); + }]) + ->get(); + } + + public function getDeceasedHistories(DateRange $dateRange, int $departmentId) + { + return MedicalHistory::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->deceased($dateRange->startSql(), $dateRange->endSql()); + }) + ->with(['latestMigration' => function ($q) use ($departmentId) { + $q->department($departmentId); + }]) + ->get(); + } + + public function getTransferredHistories(DateRange $dateRange, int $departmentId) + { + return MedicalHistory::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->transferred($dateRange->startSql(), $dateRange->endSql()); + }) + ->with(['latestMigration' => function ($q) use ($departmentId) { + $q->department($departmentId); + }]) + ->get(); + } +} diff --git a/app/Services/MetricCalculators/PreoperativeDaysCalculator.php b/app/Services/MetricCalculators/PreoperativeDaysCalculator.php index 1c30192..7073a9c 100644 --- a/app/Services/MetricCalculators/PreoperativeDaysCalculator.php +++ b/app/Services/MetricCalculators/PreoperativeDaysCalculator.php @@ -74,16 +74,21 @@ class PreoperativeDaysCalculator extends BaseMetricService implements MetricCalc } $op = $operations[$historyId]; - $days = Carbon::parse($op->first_admission) - ->diffInDays(Carbon::parse($op->first_operation)); + $admittedAt = Carbon::parse($op->first_admission); + $operationAt = Carbon::parse($op->first_operation); - if ($days >= 0) { - if (! isset($results[$deptId])) { - $results[$deptId] = ['total' => 0, 'count' => 0]; - } - $results[$deptId]['total'] += $days; - $results[$deptId]['count']++; + if ($operationAt->lt($admittedAt)) { + continue; } + + $days = $admittedAt->copy()->startOfDay() + ->diffInDays($operationAt->copy()->startOfDay()); + + if (! isset($results[$deptId])) { + $results[$deptId] = ['total' => 0, 'count' => 0]; + } + $results[$deptId]['total'] += $days; + $results[$deptId]['count']++; } // Усредняем по отделениям diff --git a/app/Services/ReportService.php b/app/Services/ReportService.php index 510465a..0b9dce1 100644 --- a/app/Services/ReportService.php +++ b/app/Services/ReportService.php @@ -2,29 +2,21 @@ namespace App\Services; -use App\Domain\Reports\ValueObjects\MetrikaConfig; use App\Infrastructure\Reports\Services\AutoFillReportPayloadBuilder; -use App\Infrastructure\Reports\Services\CalculatedMetricsSynchronizer; +use App\Infrastructure\Reports\Services\ManualPatientManagementService; +use App\Infrastructure\Reports\Services\ObservationPatientManagementService; +use App\Infrastructure\Reports\Services\ReanimationIndicatorService; +use App\Infrastructure\Reports\Services\ReportClinicalSearchService; +use App\Infrastructure\Reports\Services\ReportMetadataReadService; use App\Infrastructure\Reports\Services\ReportPatientsReadService; -use App\Infrastructure\Reports\Services\ReportMetricsFinalizer; +use App\Infrastructure\Reports\Services\ReportRuntimeService; +use App\Infrastructure\Reports\Services\ReportSaveOrchestrator; +use App\Infrastructure\Reports\Services\ReportStatisticsReadService; use App\Models\Department; -use App\Models\DepartmentPatient; use App\Models\DepartmentPatientOperation; -use App\Models\MedicalHistorySnapshot; -use App\Models\MetrikaResult; -use App\Models\MisLpuDoctor; -use App\Models\MisMedicalHistory; -use App\Models\MisMigrationPatient; -use App\Models\MisServiceMedical; -use App\Models\MisStationarBranch; -use App\Models\ObservationPatient; use App\Models\Report; use App\Models\ReanimationPatientIndicator; -use App\Models\UnwantedEvent; use App\Models\User; -use Carbon\Carbon; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\DB; class ReportService { @@ -34,41 +26,55 @@ class ReportService protected PatientService $patientQueryService, protected SnapshotService $snapshotService, protected StatisticsService $statisticsService, - ?ReportMetricsFinalizer $reportMetricsFinalizer = null, - ?CalculatedMetricsSynchronizer $calculatedMetricsSynchronizer = null, ?AutoFillReportPayloadBuilder $autoFillReportPayloadBuilder = null, ?ReportPatientsReadService $reportPatientsReadService = null, + ?ReportStatisticsReadService $reportStatisticsReadService = null, + ?ManualPatientManagementService $manualPatientManagementService = null, + ?ObservationPatientManagementService $observationPatientManagementService = null, + ?ReanimationIndicatorService $reanimationIndicatorService = null, + ?ReportClinicalSearchService $reportClinicalSearchService = null, + ?ReportMetadataReadService $reportMetadataReadService = null, + ?ReportSaveOrchestrator $reportSaveOrchestrator = null, + ?ReportRuntimeService $reportRuntimeService = null, ) { - $this->reportMetricsFinalizer = $reportMetricsFinalizer ?? app(ReportMetricsFinalizer::class); - $this->calculatedMetricsSynchronizer = $calculatedMetricsSynchronizer ?? app(CalculatedMetricsSynchronizer::class); $this->autoFillReportPayloadBuilder = $autoFillReportPayloadBuilder ?? app(AutoFillReportPayloadBuilder::class); $this->reportPatientsReadService = $reportPatientsReadService ?? app(ReportPatientsReadService::class); + $this->reportStatisticsReadService = $reportStatisticsReadService ?? app(ReportStatisticsReadService::class); + $this->manualPatientManagementService = $manualPatientManagementService ?? app(ManualPatientManagementService::class); + $this->observationPatientManagementService = $observationPatientManagementService ?? app(ObservationPatientManagementService::class); + $this->reanimationIndicatorService = $reanimationIndicatorService ?? app(ReanimationIndicatorService::class); + $this->reportClinicalSearchService = $reportClinicalSearchService ?? app(ReportClinicalSearchService::class); + $this->reportMetadataReadService = $reportMetadataReadService ?? app(ReportMetadataReadService::class); + $this->reportSaveOrchestrator = $reportSaveOrchestrator ?? app(ReportSaveOrchestrator::class); + $this->reportRuntimeService = $reportRuntimeService ?? app(ReportRuntimeService::class); } - protected ReportMetricsFinalizer $reportMetricsFinalizer; - - protected CalculatedMetricsSynchronizer $calculatedMetricsSynchronizer; - protected AutoFillReportPayloadBuilder $autoFillReportPayloadBuilder; protected ReportPatientsReadService $reportPatientsReadService; + protected ReportStatisticsReadService $reportStatisticsReadService; + + protected ManualPatientManagementService $manualPatientManagementService; + + protected ObservationPatientManagementService $observationPatientManagementService; + + protected ReanimationIndicatorService $reanimationIndicatorService; + + protected ReportClinicalSearchService $reportClinicalSearchService; + + protected ReportMetadataReadService $reportMetadataReadService; + + protected ReportSaveOrchestrator $reportSaveOrchestrator; + + protected ReportRuntimeService $reportRuntimeService; + /** * Получить статистику для отчета */ public function getReportStatistics(Department $department, User $user, DateRange $dateRange): array { - $misDepartmentId = $department->rf_mis_department_id; - $branchId = $this->getBranchId($misDepartmentId); - - // Определяем, используем ли мы снапшоты - $useSnapshots = $this->shouldUseSnapshots($department, $user, $dateRange); - - if ($useSnapshots) { - return $this->getStatisticsFromSnapshots($department, $dateRange, $branchId); - } - - return $this->getStatisticsFromReplica($department, $user, $dateRange, $branchId); + return $this->reportStatisticsReadService->getReportStatistics($department, $user, $dateRange); } /** @@ -76,86 +82,32 @@ class ReportService */ public function storeReport(array $data, User $user, $fillableAuto = false): Report { - $this->prepareMemoryForHeavySave(); - - try { - $report = DB::transaction(function () use ($data, $user, $fillableAuto) { - $report = $this->createOrUpdateReport($data, $user); - - // Сохраняем все, что НЕ зависит от других отчетов - $this->saveMetrics($report, $data['metrics'] ?? []); - - $this->saveUnwantedEvents($report, $data['unwantedEvents'] ?? []); - - $this->saveObservationPatients($report, $data['observationPatients'] ?? [], $user->rf_department_id); - - $this->snapshotService->createPatientSnapshots($report, $user, $data['dates'], $fillableAuto); - - $this->syncCalculatedMetrics($report, $user, $data); - - return $report; - }); - - DB::transaction(function () use ($report) { - $this->reportMetricsFinalizer->finalize($report); - $this->saveLethalMetricFromSnapshots($report); - }); - } catch (\Throwable $e) { - throw $e; - } - - $this->clearCacheAfterReportCreation($user, $report); - - return $report; + return $this->reportSaveOrchestrator->storeReport($data, $user, (bool) $fillableAuto); } public function prepareForHeavySave(): void { - $this->prepareMemoryForHeavySave(); + $this->reportRuntimeService->prepareForHeavySave(); } public function syncCalculatedMetricsForStoredReport(Report $report, User $user, array $data): void { - $this->calculatedMetricsSynchronizer->sync($report, $user, $data); + $this->reportSaveOrchestrator->syncCalculatedMetrics($report, $user, $data); } public function finalizeStoredReport(Report $report): void { - $this->reportMetricsFinalizer->finalize($report); + $this->reportSaveOrchestrator->finalizeStoredReport($report); } public function saveLethalMetricForStoredReport(Report $report): void { - $this->saveLethalMetricFromSnapshots($report); + $this->reportSaveOrchestrator->saveLethalMetricFromSnapshots($report); } public function clearCacheAfterStoredReport(User $user, Report $report): void { - $this->clearCacheAfterReportCreation($user, $report); - } - - private function prepareMemoryForHeavySave(): void - { - $connectionNames = array_unique(array_filter([ - DB::getDefaultConnection(), - (new MisMedicalHistory)->getConnectionName(), - (new MisMigrationPatient)->getConnectionName(), - (new MisStationarBranch)->getConnectionName(), - ])); - - foreach ($connectionNames as $connectionName) { - try { - $connection = DB::connection($connectionName); - $connection->disableQueryLog(); - $connection->flushQueryLog(); - } catch (\Throwable) { - // best-effort cleanup only - } - } - - if (function_exists('gc_collect_cycles')) { - gc_collect_cycles(); - } + $this->reportRuntimeService->clearCacheAfterReportCreation($user, $report); } public function buildAutoFillReportPayload(User $user, Department $department, DateRange $dateRange): array @@ -163,263 +115,6 @@ class ReportService return $this->autoFillReportPayloadBuilder->build($user, $department, $dateRange); } - /** - * Сохранить метрику койко-дня из снапшотов отчета - */ - protected function saveBedDaysMetric(Report $report): void - { - try { - $result = $this->calculateBedDaysFromSnapshots($report); - - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::TOTAL_BED_DAYS, - ], - ['value' => $result['total_days']] - ); - - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::AVERAGE_BED_DAYS, - ], - ['value' => $result['avg_days']] - ); - } catch (\Throwable $e) { - \Log::error('Failed to save bed days metric: '.$e->getMessage()); - } - } - - protected function calculateBedDaysFromSnapshots(Report $report): array - { - $snapshots = MedicalHistorySnapshot::where('rf_report_id', $report->report_id) - ->whereIn('patient_type', ['discharged', 'deceased']) - ->with('medicalHistory') - ->get(); - - $totalDays = 0; - $patientCount = 0; - - foreach ($snapshots as $snapshot) { - $history = $snapshot->medicalHistory; - - if (! $history) { - continue; - } - - $start = $history->DateRecipientHS ?? $history->DateRecipient ?? null; - - if (! $start) { - continue; - } - - $end = null; - - if ($snapshot->patient_type === 'deceased') { - if ($history->DateDeath && ! in_array($history->DateDeath->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { - $end = $history->DateDeath; - } elseif ($history->DateExtract && ! in_array($history->DateExtract->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { - $end = $history->DateExtract; - } - } else { - if ($history->DateExtract && ! in_array($history->DateExtract->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { - $end = $history->DateExtract; - } - } - - if (! $end) { - continue; - } - - $start = Carbon::parse($start); - $end = Carbon::parse($end); - - if ($end->lt($start)) { - continue; - } - - // Календарные койко-дни - $days = $start->startOfDay()->diffInDays($end->startOfDay()); - - $totalDays += $days; - $patientCount++; - } - - return [ - 'total_days' => $totalDays, - 'patient_count' => $patientCount, - 'avg_days' => $patientCount > 0 ? round($totalDays / $patientCount, 2) : 0, - ]; - } - - /** - * Рассчитать предоперационные койко-дни по снапшотам отчета - * - * Возвращает: - * - total_days: общее количество предоперационных койко-дней - * - patient_count: количество пациентов, вошедших в расчет - * - avg_days: средний предоперационный койко-день - */ - protected function calculatePreoperativeDaysFromSnapshots(Report $report): array - { - $patientIds = MedicalHistorySnapshot::where('rf_report_id', $report->report_id) - ->whereIn('patient_type', ['discharged', 'deceased']) - ->pluck('rf_medicalhistory_id') - ->unique() - ->values(); - - if ($patientIds->isEmpty()) { - return [ - 'total_days' => 0, - 'patient_count' => 0, - 'avg_days' => 0, - ]; - } - - $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(); - - if ($rows->isEmpty()) { - return [ - 'total_days' => 0, - 'patient_count' => 0, - 'avg_days' => 0, - ]; - } - - $totalDays = 0; - $patientCount = 0; - - foreach ($rows as $row) { - $startRaw = $row->DateRecipientHS ?? $row->DateRecipient ?? null; - $operationRaw = $row->first_operation ?? null; - - if (! $startRaw || ! $operationRaw) { - continue; - } - - $start = Carbon::parse($startRaw); - $operation = Carbon::parse($operationRaw); - - if ($operation->lt($start)) { - continue; - } - - // Разница календарных дат - $days = $start->startOfDay()->diffInDays($operation->startOfDay()); - - $totalDays += $days; - $patientCount++; - } - - return [ - 'total_days' => $totalDays, - 'patient_count' => $patientCount, - 'avg_days' => $patientCount > 0 ? round($totalDays / $patientCount, 1) : 0, - ]; - } - - protected function saveLethalMetricFromSnapshots(Report $report): void - { - // Получаем все снапшоты выписанных пациентов из этого отчета - $snapshots = MedicalHistorySnapshot::where('rf_report_id', $report->report_id) - ->whereIn('patient_type', ['discharged', 'deceased']) // выписанные и умершие - ->with('medicalHistory') - ->get(); - - if ($snapshots->isEmpty()) { - // Если нет выписанных, сохраняем 0 - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::AVERAGE_BED_DAYS, - ], - ['value' => 0] - ); - - \Log::info("No discharged patients in report {$report->report_id}, saved 0"); - - return; - } - } - - /** - * Сохранить предоперационный койко-день из снапшотов - */ - protected function savePreoperativeMetric(Report $report): void - { - try { - $result = $this->calculatePreoperativeDaysFromSnapshots($report); - - $this->saveMetric($report, MetrikaConfig::TOTAL_PREOPERATIVE_DAYS, $result['total_days']); - $this->saveMetric($report, MetrikaConfig::PREOPERATIVE_PATIENT_COUNT, $result['patient_count']); - $this->saveMetric($report, MetrikaConfig::PREOPERATIVE_AVERAGE_DAYS, $result['avg_days']); - } catch (\Throwable $e) { - \Log::error('Failed to save preoperative total metric: '.$e->getMessage()); - } - } - - /** - * Сохранить % загруженности - */ - protected function saveDepartmentLoadedMetric(Report $report): void - { - // Получаем все снапшоты выписанных пациентов из этого отчета - $currentCount = $report->metrikaResults()->where('rf_metrika_item_id', MetrikaConfig::CURRENT)->value('value'); - $bedsCount = $report->metrikaResults()->where('rf_metrika_item_id', MetrikaConfig::BEDS)->value('value'); - - $percentLoaded = $bedsCount > 0 ? round($currentCount * 100 / $bedsCount) : 0; - - $this->saveMetric($report, MetrikaConfig::DEPARTMENT_LOADED, $percentLoaded); - } - - /** - * Очистить кэш после создания отчета - */ - private function clearCacheAfterReportCreation(User $user, Report $report): void - { - // Очищаем кэш статистики для пользователя - // $this->statisticsService->clearStatisticsCache($user); - - // Также можно очистить кэш для всех пользователей отдела - // $this->statisticsService->clearDepartmentStatisticsCache($user->rf_department_id); - - // Очищаем кэш за сегодня и вчера (так как отчеты влияют на эти даты) - $this->clearDailyCache($user, $report->created_at); - } - - /** - * Очистить дневной кэш - */ - private function clearDailyCache(User $user, $reportDate): void - { - $datesToClear = [ - Carbon::parse($reportDate)->format('Y-m-d'), - Carbon::parse($reportDate)->subDay()->format('Y-m-d'), - ]; - - foreach ($datesToClear as $date) { - $cacheKey = $this->generateDailyCacheKey($user, $date); - Cache::forget($cacheKey); - } - } - - private function generateDailyCacheKey(User $user, string $date): string - { - return 'daily_stats:'.$user->rf_department_id.':'.$date; - } - /** * Получить пациентов по статусу */ @@ -460,265 +155,12 @@ class ReportService return $this->reportPatientsReadService->getPatientsCountsMap($department, $user, $dateRange); } - /** - * Получить ID отделения из стационарного отделения - */ - private function getBranchId(int $misDepartmentId): ?int - { - return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId) - ->value('StationarBranchID'); - } - - /** - * Определить, нужно ли использовать снапшоты - */ - private function shouldUseSnapshots(Department $department, User $user, DateRange $dateRange, bool $beforeCreate = false): bool - { - if ($beforeCreate) { - return false; - } - - $report = $this->getReportForPeriod($department->department_id, $dateRange); - if (! $report) { - return false; - } - - if ($report->status !== 'submitted') { - return false; - } - - return true; - } - - /** - * Создать или обновить отчет - */ - private function createOrUpdateReport(array $data, User $user): Report - { - $rangeStartAt = isset($data['dates'][0]) - ? $this->dateRangeService->toSqlFormat($data['dates'][0]) - : null; - $rangeEndAt = isset($data['dates'][1]) - ? $this->dateRangeService->toSqlFormat($data['dates'][1]) - : null; - - $dateRange = $this->dateRangeService->createDateRangeForDate($this->dateRangeService->toCarbon($data['dates'][1]), $user); - - $sentAt = $data['sent_at'] ?? $rangeEndAt ?? $this->dateRangeService->toSqlFormat(\Illuminate\Support\Carbon::now()); - $createdAt = $data['created_at'] ?? $rangeEndAt ?? $this->dateRangeService->toSqlFormat(\Illuminate\Support\Carbon::now()); - - $reportData = [ - 'rf_department_id' => $data['departmentId'], - 'rf_user_id' => $user->id, - 'rf_lpudoctor_id' => $data['userId'], - 'sent_at' => $sentAt, - 'period_start' => $dateRange->startSql(), - 'period_end' => $dateRange->endSql(), - 'created_at' => $createdAt, - 'status' => $data['status'] ?? 'draft', - ]; - - if (isset($data['reportId']) && $data['reportId']) { - $report = Report::updateOrCreate( - ['report_id' => $data['reportId']], - $reportData - ); - } else { - $report = Report::create($reportData); - $department = Department::where('department_id', $reportData['rf_department_id'])->first(); - $beds = $department->metrikaDefault->where('rf_metrika_item_id', 1)->first(); - MetrikaResult::create([ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => MetrikaConfig::BEDS, - 'value' => $beds->value, - ]); - } - - return $report; - } - - /** - * Сохранить метрики отчета - */ - private function saveMetrics(Report $report, array $metrics): void - { - foreach ($metrics as $key => $value) { - $metrikaId = (int) str_replace('metrika_item_', '', $key); - - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => $metrikaId, - ], - [ - 'value' => $value, - ] - ); - } - } - - /** - * Сохранить метрику отчета - */ - private function saveMetric(Report $report, int $metrikaId, float $value): void - { - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => $metrikaId, - ], - [ - 'value' => $value, - ] - ); - } - - /** - * Сохранить нежелательные события - */ - private function saveUnwantedEvents(Report $report, array $unwantedEvents): void - { - if (empty($unwantedEvents)) { - $report->unwantedEvents()->delete(); - $this->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, 0); - - return; - } - - foreach ($unwantedEvents as $event) { - if (isset($event['unwanted_event_id']) && $event['unwanted_event_id']) { - UnwantedEvent::updateOrCreate( - ['unwanted_event_id' => $event['unwanted_event_id']], - [ - 'rf_report_id' => $report->report_id, - 'comment' => $event['comment'] ?? '', - 'title' => $event['title'] ?? '', - 'is_visible' => $event['is_visible'] ?? true, - ] - ); - } else { - UnwantedEvent::create([ - 'rf_report_id' => $report->report_id, - 'comment' => $event['comment'] ?? '', - 'title' => $event['title'] ?? '', - 'is_visible' => $event['is_visible'] ?? true, - ]); - } - } - - // Обновить метрику - $this->saveMetric($report, MetrikaConfig::UNWANTED_EVENTS, count($unwantedEvents)); - } - - /** - * Сохранить пациентов под наблюдением - */ - private function saveObservationPatients( - Report $report, - array $observationPatients, - int $departmentId - ): void { - if (empty($observationPatients)) { - ObservationPatient::where('rf_department_id', $departmentId) - ->where('rf_report_id', $report->report_id) - ->delete(); - // Обновить метрику - $this->saveMetric($report, MetrikaConfig::OBSERVATION, 0); - - return; - } - - foreach ($observationPatients as $patient) { - ObservationPatient::updateOrCreate( - [ - 'rf_medicalhistory_id' => $patient['medical_history_id'] ?? null, - 'rf_department_patient_id' => $patient['department_patient_id'] ?? null, - 'rf_department_id' => $departmentId, - ], - [ - 'rf_report_id' => $report->report_id, - 'rf_mkab_id' => null, - 'comment' => $patient['comment'] ?? null, - ] - ); - } - - // Обновить метрику - $this->saveMetric($report, MetrikaConfig::OBSERVATION, count($observationPatients)); - } - - private function syncCalculatedMetrics(Report $report, User $user, array $data): void - { - $this->calculatedMetricsSynchronizer->sync($report, $user, $data); - } - /** * Получить информацию о текущем отчете */ public function getCurrentReportInfo(Department $department, User $user, DateRange $dateRange): array { - $reportToday = $this->getReportForPeriod($department->department_id, $dateRange); - - $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); - $useSnapshots = $isHeadOrAdmin || ! $dateRange->isEndDateToday() || $reportToday; - - // Получаем ID пользователя для заполнения отчета - if ($useSnapshots && $isHeadOrAdmin && $reportToday) { - $fillableUserId = $reportToday->rf_lpudoctor_id ?? null; - } else { - $fillableUserId = request()->query('userId', $user->rf_lpudoctor_id); - } - - // Получаем нежелательные события - $unwantedEvents = $this->getUnwantedEvents($department, $dateRange); - - // Определяем активность кнопки отправки - $isActiveSendButton = $this->isSendButtonActive($user, $dateRange, $reportToday, $fillableUserId); - - $message = null; - if ($reportToday) { - $reportDoctor = $reportToday->lpuDoctor; - $message = "Отчет создан пользователем: $reportDoctor->FAM_V $reportDoctor->IM_V $reportDoctor->OT_V"; - } - $statusMessage = $reportToday - ? ($reportToday->status === 'submitted' - ? 'Этот отчет в статусе: опубликован' - : 'Этот отчет в статусе: черновик') - : null; - - // Получаем информацию о враче - $lpuDoctor = $this->getDoctorInfo($fillableUserId, $dateRange); - - // Проверяем, является ли диапазон одним днем - // $isRangeOneDay = $this->dateRangeService->isRangeOneDay( - // $endDate->copy()->subDay()->format('Y-m-d H:i:s'), - // $endDate->format('Y-m-d H:i:s') - // ); - - // Формируем даты для ответа - // $date = $isHeadOrAdmin ? [ - // $endDate->copy()->subDay()->getTimestampMs(), - // $endDate->getTimestampMs() - // ] : $endDate->getTimestampMs(); - $date = $isHeadOrAdmin ? [ - $dateRange->startDate->getTimestampMs(), - $dateRange->endDate->getTimestampMs(), - ] : $dateRange->endDate->getTimestampMs(); - - return [ - 'report_id' => $reportToday?->report_id, - 'unwantedEvents' => $unwantedEvents, - 'isActiveSendButton' => $isActiveSendButton, - 'message' => $dateRange->isOneDay ? $message : null, - 'status' => $reportToday?->status ?? 'draft', - 'statusMessage' => $dateRange->isOneDay ? $statusMessage : null, - 'canPublish' => (bool) $reportToday && ($reportToday->status === 'draft') && $isActiveSendButton, - 'isOneDay' => $dateRange->isOneDay, - 'isHeadOrAdmin' => $isHeadOrAdmin, - 'dates' => $date, - 'userId' => $fillableUserId, - 'userName' => $lpuDoctor ? "$lpuDoctor->FAM_V $lpuDoctor->IM_V $lpuDoctor->OT_V" : null, - ]; + return $this->reportMetadataReadService->getCurrentReportInfo($department, $user, $dateRange); } /** @@ -726,109 +168,47 @@ class ReportService */ public function removeObservationPatient(string $patientId): void { - [$sourceType, $id] = explode(':', $patientId) + [null, null]; - - if ($sourceType === 'manual') { - ObservationPatient::where('rf_department_patient_id', $id)->delete(); - - return; - } - - ObservationPatient::where('rf_medicalhistory_id', $id)->delete(); + $this->observationPatientManagementService->removeObservationPatient($patientId); } public function createManualPatient(Department $department, User $user, array $data) { - $report = $this->resolveReportForManualPatient($department, $user, $data); - - return $this->unifiedPatientService->createManualPatient($department, $user, $data, $report->report_id); + return $this->manualPatientManagementService->createManualPatient($department, $user, $data); } public function setManualPatientOutcome(User $user, int $departmentPatientId, array $data) { - $patient = \App\Models\DepartmentPatient::where('department_patient_id', $departmentPatientId)->firstOrFail(); - $updatedPatient = $this->unifiedPatientService->recordManualOutcome($patient, $data); - $this->syncManualPatientSnapshots($updatedPatient, $user, []); - - return $updatedPatient; + return $this->manualPatientManagementService->setManualPatientOutcome($user, $departmentPatientId, $data); } public function updateManualPatient(User $user, int $departmentPatientId, array $data) { - $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); - - $updatedPatient = $this->unifiedPatientService->updateManualPatient($patient, $data); - $this->syncManualPatientSnapshots($updatedPatient, $user, $data); - - return $updatedPatient; + return $this->manualPatientManagementService->updateManualPatient($user, $departmentPatientId, $data); } public function linkManualPatientToMis(int $departmentPatientId, int $medicalHistoryId) { - $patient = \App\Models\DepartmentPatient::where('department_patient_id', $departmentPatientId)->firstOrFail(); - - return $this->unifiedPatientService->linkManualPatientToMis($patient, $medicalHistoryId); + return $this->manualPatientManagementService->linkManualPatientToMis($departmentPatientId, $medicalHistoryId); } public function getManualPatientOperations(User $user, int $departmentPatientId) { - $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); - - return $patient->operations() - ->with('serviceMedical') - ->orderByDesc('started_at') - ->get(); + return $this->manualPatientManagementService->getManualPatientOperations($user, $departmentPatientId); } public function createManualPatientOperation(User $user, int $departmentPatientId, array $data): DepartmentPatientOperation { - $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); - $service = MisServiceMedical::query() - ->where('ServiceMedicalID', $data['service_id']) - ->firstOrFail(); - - return $patient->operations()->create([ - 'rf_kl_service_medical_id' => $service->ServiceMedicalID, - 'service_code' => $service->ServiceMedicalCode, - 'service_name' => $service->ServiceMedicalName, - 'urgency' => $data['urgency'], - 'started_at' => $data['started_at'], - 'ended_at' => $data['ended_at'], - 'created_by' => $user->id, - ])->load('serviceMedical'); + return $this->manualPatientManagementService->createManualPatientOperation($user, $departmentPatientId, $data); } public function updateManualPatientOperation(User $user, int $departmentPatientId, int $operationId, array $data): DepartmentPatientOperation { - $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); - $service = MisServiceMedical::query() - ->where('ServiceMedicalID', $data['service_id']) - ->firstOrFail(); - - $operation = $patient->operations() - ->where('department_patient_operation_id', $operationId) - ->firstOrFail(); - - $operation->update([ - 'rf_kl_service_medical_id' => $service->ServiceMedicalID, - 'service_code' => $service->ServiceMedicalCode, - 'service_name' => $service->ServiceMedicalName, - 'urgency' => $data['urgency'], - 'started_at' => $data['started_at'], - 'ended_at' => $data['ended_at'], - ]); - - return $operation->fresh()->load('serviceMedical'); + return $this->manualPatientManagementService->updateManualPatientOperation($user, $departmentPatientId, $operationId, $data); } public function deleteManualPatientOperation(User $user, int $departmentPatientId, int $operationId): void { - $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); - - $patient->operations() - ->where('department_patient_operation_id', $operationId) - ->firstOrFail() - ->delete(); + $this->manualPatientManagementService->deleteManualPatientOperation($user, $departmentPatientId, $operationId); } public function saveReanimationIndicator( @@ -839,38 +219,19 @@ class ReportService ?string $comment = null, ?int $reportId = null ): ReanimationPatientIndicator { - return ReanimationPatientIndicator::create([ - 'rf_department_id' => $departmentId, - 'rf_report_id' => $reportId, - 'rf_medicalhistory_id' => $medicalHistoryId, - 'indicator' => $indicator, - 'comment' => $comment, - 'created_by' => $user->id, - ]); + return $this->reanimationIndicatorService->save( + $user, + $departmentId, + $medicalHistoryId, + $indicator, + $comment, + $reportId + ); } public function getLatestReanimationIndicators(int $departmentId, array $medicalHistoryIds) { - if (empty($medicalHistoryIds)) { - return collect(); - } - - $subQuery = ReanimationPatientIndicator::query() - ->selectRaw('MAX(reanimation_patient_indicator_id) as max_id, rf_medicalhistory_id') - ->where('rf_department_id', $departmentId) - ->whereIn('rf_medicalhistory_id', $medicalHistoryIds) - ->groupBy('rf_medicalhistory_id'); - - return ReanimationPatientIndicator::query() - ->joinSub($subQuery, 'latest', function ($join) { - $join->on('reanimation_patient_indicators.reanimation_patient_indicator_id', '=', 'latest.max_id'); - }) - ->get([ - 'reanimation_patient_indicators.rf_medicalhistory_id', - 'reanimation_patient_indicators.indicator', - 'reanimation_patient_indicators.comment', - ]) - ->keyBy('rf_medicalhistory_id'); + return $this->reanimationIndicatorService->latestByMedicalHistory($departmentId, $medicalHistoryIds); } public function getReanimationIndicatorsHistory( @@ -878,237 +239,12 @@ class ReportService int $medicalHistoryId, int $limit = 50 ) { - return ReanimationPatientIndicator::query() - ->where('rf_department_id', $departmentId) - ->where('rf_medicalhistory_id', $medicalHistoryId) - ->orderByDesc('reanimation_patient_indicator_id') - ->limit($limit) - ->get([ - 'reanimation_patient_indicator_id', - 'rf_report_id', - 'rf_medicalhistory_id', - 'indicator', - 'comment', - 'created_by', - 'created_at', - ]); + return $this->reanimationIndicatorService->history($departmentId, $medicalHistoryId, $limit); } public function searchMisPatientsForDepartment(Department $department, string $query) { - return $this->unifiedPatientService->searchMisPatients($department, $query); - } - - private function resolveManageableManualPatient(User $user, int $departmentPatientId): DepartmentPatient - { - $query = DepartmentPatient::query() - ->where('department_patient_id', $departmentPatientId) - ->whereIn('source_type', ['manual', 'special']); - - if (! $user->isAdmin() && ! $user->isHeadOfDepartment()) { - $query->where('rf_department_id', $user->department->department_id); - } - - return $query->firstOrFail(); - } - - private function syncManualPatientSnapshots(DepartmentPatient $patient, User $user, array $data): void - { - $reportIds = $patient->rf_report_id - ? [$patient->rf_report_id] - : (isset($data['startAt'], $data['endAt']) && $data['startAt'] && $data['endAt'] - ? $this->getReportsForDateRange( - $patient->rf_department_id, - $this->dateRangeService->getNormalizedDateRange( - $user, - (string) $data['startAt'], - (string) $data['endAt'] - ) - )->pluck('report_id')->values()->all() - : []); - - if (empty($reportIds)) { - return; - } - - MedicalHistorySnapshot::query() - ->whereIn('rf_report_id', $reportIds) - ->where('rf_department_patient_id', $patient->department_patient_id) - ->update([ - 'patient_kind' => $patient->patient_kind, - 'full_name' => $patient->full_name, - 'birth_date' => $patient->birth_date, - 'diagnosis_code' => $patient->diagnosis_code, - 'diagnosis_name' => $patient->diagnosis_name, - 'admitted_at' => $patient->admitted_at, - 'outcome_type' => $patient->is_current ? null : $patient->outcome_type, - 'outcome_at' => $patient->is_current ? null : $patient->outcome_at, - 'updated_at' => now(), - ]); - } - - private function resolveReportForManualPatient(Department $department, User $user, array $data): Report - { - $reportId = $data['report_id'] ?? null; - if ($reportId) { - return Report::query() - ->where('report_id', $reportId) - ->where('rf_department_id', $department->department_id) - ->firstOrFail(); - } - - if (! isset($data['startAt'], $data['endAt']) || ! $data['startAt'] || ! $data['endAt']) { - throw new \InvalidArgumentException('Не указан отчет или диапазон для привязки спецконтингента'); - } - - $dateRange = $this->dateRangeService->getNormalizedDateRange( - $user, - (string) $data['startAt'], - (string) $data['endAt'] - ); - - $existingReport = Report::query() - ->where('rf_department_id', $department->department_id) - ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) - ->first(); - - if ($existingReport) { - return $existingReport; - } - - return Report::query()->create([ - 'rf_department_id' => $department->department_id, - 'rf_user_id' => $user->id, - 'rf_lpudoctor_id' => $data['user_id'] ?? $user->rf_lpudoctor_id, - 'sent_at' => $dateRange->endSql(), - 'created_at' => $dateRange->endSql(), - 'period_start' => $dateRange->startSql(), - 'period_end' => $dateRange->endSql(), - 'status' => 'draft', - ]); - } - - /** - * Получить статистику из снапшотов - */ - private function getStatisticsFromSnapshots(Department $department, DateRange $dateRange, int $branchId): array - { - // Получаем отчеты за период - $reports = $this->getReportsForDateRange( - $department->department_id, - $dateRange - ); - - $reportIds = $reports->pluck('report_id')->toArray(); - $lastReport = array_first($reportIds); - $recipientReportIds = $this->getSnapshotRecipientReportIds($reportIds); - - // Получаем статистику из снапшотов - $snapshotStats = [ - 'plan' => $this->getMetrikaResultCount(4, $reportIds), - 'emergency' => $this->getMetrikaResultCount(12, $reportIds), - 'outcome' => $this->getMetrikaResultCount(7, $reportIds), - 'deceased' => $this->getMetrikaResultCount(9, $reportIds), - 'current' => $this->getMetrikaResultCount(8, $reportIds, false), - // 'discharged' => $this->getMetrikaResultCount('discharged', $reportIds), - 'transferred' => $this->getMetrikaResultCount(13, $reportIds), - 'recipient' => $this->getMetrikaResultCount(3, $reportIds), - 'beds' => $this->getMetrikaResultCount(1, $reportIds, false), - 'countStaff' => $this->getMetrikaResultCount(17, [$lastReport], false), - ]; - - // Получаем ID поступивших пациентов - $recipientIds = $this->snapshotService - ->getPatientsFromSnapshots('recipient', $recipientReportIds) - ->pluck('id') - ->all(); - - // Получаем количество операций из метрик - $surgicalCount = [ - $this->getMetrikaResultCount(10, $reportIds), // экстренные операции - $this->getMetrikaResultCount(11, $reportIds), // плановые операции - ]; - - if ($snapshotStats['outcome'] == 0) { - $percentDead = 0; - } else { - $percentDead = ($snapshotStats['deceased'] / $snapshotStats['outcome']) * 100; - $percentDead = round($percentDead, 2); - } - - return [ - 'recipientCount' => $snapshotStats['recipient'] ?? 0, - 'extractCount' => $snapshotStats['outcome'] ?? 0, - 'currentCount' => $snapshotStats['current'] ?? 0, // $this->calculateCurrentPatientsFromSnapshots($reportIds, $branchId), - 'deadCount' => $snapshotStats['deceased'] ?? 0, - 'countStaff' => $snapshotStats['countStaff'] ?? 0, - 'surgicalCount' => $surgicalCount, - 'recipientIds' => $recipientIds, - 'beds' => $snapshotStats['beds'] ?? 0, - 'percentDead' => $percentDead, - ]; - } - - /** - * Получить статистику из реплики БД - */ - private function getStatisticsFromReplica(Department $department, User $user, DateRange $dateRange, int $branchId): array - { - $planCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'plan', $dateRange, $branchId, true); - $emergencyCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'emergency', $dateRange, $branchId, true); - $currentCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'current', $dateRange, $branchId); - $recipientCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'recipient', $dateRange, $branchId); - $outcomeCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'outcome', $dateRange, $branchId); - $deadCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'outcome-deceased', $dateRange, $branchId); - - // Операции - $misSurgicalCount = [ - $this->patientQueryService->getSurgicalPatients( - 'emergency', - $branchId, - $dateRange, - true - ), - $this->patientQueryService->getSurgicalPatients( - 'plan', - $branchId, - $dateRange, - true - ), - ]; - $manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange); - $surgicalCount = [ - ($misSurgicalCount[0] ?? 0) + ($manualSurgicalCount[0] ?? 0), - ($misSurgicalCount[1] ?? 0) + ($manualSurgicalCount[1] ?? 0), - ]; - - // ID поступивших сегодня (для отметки в таблице) - $recipientIds = $this->unifiedPatientService - ->getRecipientIdsForReport($department, $user, $dateRange, $branchId); - - $misBranch = MisStationarBranch::where('StationarBranchID', $branchId)->first(); - $beds = Department::where('rf_mis_department_id', $misBranch->rf_DepartmentID) - ->first()->metrikaDefault->where('rf_metrika_item_id', 1)->first(); - - if ($outcomeCount == 0) { - $percentDead = 0; - } else { - $percentDead = ($deadCount / $outcomeCount) * 100; - $percentDead = round($percentDead, 2); - } - - return [ - 'recipientCount' => $recipientCount, // только поступившие сегодня - 'extractCount' => $outcomeCount, - 'currentCount' => $currentCount, // все в отделении - 'deadCount' => $deadCount, - 'surgicalCount' => $surgicalCount, - 'recipientIds' => $recipientIds, // ID поступивших сегодня - 'planCount' => $planCount, // плановые (поступившие + уже лечащиеся) - 'emergencyCount' => $emergencyCount, // экстренные (поступившие + уже лечащиеся) - 'percentDead' => $percentDead, - 'beds' => $beds->value, - ]; + return $this->reportClinicalSearchService->searchMisPatientsForDepartment($department, $query); } /** @@ -1130,92 +266,12 @@ class ReportService ); } - private function getSnapshotRecipientReportIds(array $reportIds): array - { - if (empty($reportIds)) { - return []; - } - - return [reset($reportIds)]; - } - /** * Получить нежелательные события за дату */ public function getUnwantedEvents(Department $department, DateRange $dateRange) { - return UnwantedEvent::whereHas('report', function ($query) use ($department, $dateRange) { - $query->where('rf_department_id', $department->department_id); - - if ($dateRange->isOneDay) { - $query->exactPeriod($dateRange->startSql(), $dateRange->endSql()); - } else { - $query->withinPeriod($dateRange->startSql(), $dateRange->endSql()); - } - }) - ->get() - ->map(function ($item) { - return [ - ...$item->toArray(), - 'created_at' => Carbon::parse($item->created_at)->format('Создано d.m.Y в H:i'), - ]; - }); - } - - /** - * Проверить активность кнопки отправки отчета - */ - private function isSendButtonActive(User $user, DateRange $dateRange, ?Report $reportToday, ?int $fillableUserId): bool - { - // Для врача: только сегодня и если отчета еще нет - if (! $user->isHeadOfDepartment() && ! $user->isAdmin()) { - if ($reportToday && $reportToday->status === 'submitted') { - return false; - } - - return $dateRange->isEndDateToday(); - } - - // Для заведующего/админа: можно редактировать любой отчет за сутки (включая submitted) - if ( - $reportToday && - $dateRange->isOneDay - ) { - return true; - } - - return false; - } - - 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(); - } else { - return $query->onlySubmitted()->first(); - } - } - - /** - * Получить информацию о враче - */ - private function getDoctorInfo(?int $doctorId, DateRange $dateRange): ?MisLpuDoctor - { - if (! $doctorId) { - return null; - } - - // Если дата это период, не показываем врача - if (! $dateRange->isOneDay) { - return null; - } - - return MisLpuDoctor::where('LPUDoctorID', $doctorId)->first(); + return $this->reportMetadataReadService->getUnwantedEvents($department, $dateRange); } /** @@ -1223,54 +279,7 @@ class ReportService */ public function getReportsForDateRange(int $departmentId, DateRange $dateRange) { - if ($dateRange->isOneDay) { - return Report::where('rf_department_id', $departmentId) - ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) - ->onlySubmitted() - ->orderBy('period_end', 'DESC') - ->get(); - } - - return Report::where('rf_department_id', $departmentId) - ->withinPeriod($dateRange->startSql(), $dateRange->endSql()) - ->onlySubmitted() - ->orderBy('period_end', 'DESC') - ->get(); - } - - /** - * Получить количество из метрик - */ - private function getMetrikaResultCount(int $metrikaItemId, array $reportIds, bool $sum = true): int - { - $count = 0; - $reports = Report::whereIn('report_id', $reportIds) - ->with('metrikaResults') - ->orderBy('created_at', 'DESC') - ->get(); - - if (! $sum) { - foreach ($reports as $report) { - $metric = $report->metrikaResults - ->firstWhere('rf_metrika_item_id', $metrikaItemId); - - if ($metric) { - return intval($metric->value) ?? 0; - } - } - - return 0; - } - - foreach ($reports as $report) { - foreach ($report->metrikaResults as $metrikaResult) { - if ($metrikaResult->rf_metrika_item_id === $metrikaItemId) { - $count += intval($metrikaResult->value) ?? 0; - } - } - } - - return $count; + return $this->reportMetadataReadService->getReportsForDateRange($departmentId, $dateRange); } /** @@ -1278,39 +287,6 @@ class ReportService */ public function getRecipientPlanOfYear(Department $department, DateRange $dateRange): array { - $periodPlanModel = $department->recipientPlanOfYear(); - // Рассчитываем коэффициент периода (округляем в большую сторону) - $monthsInPeriod = ceil($dateRange->startDate->diffInMonths($dateRange->endDate)); - $annualPlan = $periodPlanModel ? (int) $periodPlanModel->value : 0; - $oneMonthPlan = ceil($annualPlan / 12); - $periodPlan = round($oneMonthPlan * $monthsInPeriod); - - $progress = 0; - $query = $department->reports() - ->with('metrikaResults') - ->where('period_start', '>', $dateRange->startSql()) - ->where('period_end', '<=', $dateRange->endSql()); - - if ($dateRange->isOneDay) { - $query->where('period_start', '>=', $dateRange->startFirstOfMonth()) - ->where('period_end', '<=', $dateRange->endSql()); - } else { - $query->where('period_start', '>', $dateRange->startSql()) - ->where('period_end', '<=', $dateRange->endSql()); - } - - $reports = $query->get(); - - foreach ($reports as $report) { - $outcome = $report->metrikaResults()->where('rf_metrika_item_id', 7)->first(); - if ($outcome) { - $progress += (int) $outcome->value; - } - } - - return [ - 'plan' => $periodPlan, - 'progress' => $progress, - ]; + return $this->reportMetadataReadService->getRecipientPlanOfYear($department, $dateRange); } } diff --git a/app/Services/SnapshotService.php b/app/Services/SnapshotService.php index 0234767..cfbdcf7 100644 --- a/app/Services/SnapshotService.php +++ b/app/Services/SnapshotService.php @@ -10,7 +10,6 @@ use App\Models\MedicalHistorySnapshot; use App\Models\MisStationarBranch; use App\Models\Report; use App\Models\User; -use Carbon\Carbon; use Illuminate\Support\Collection; class SnapshotService @@ -280,8 +279,8 @@ class SnapshotService private function parseDates(array $dates): array { return [ - Carbon::createFromTimestampMs($dates[0])->setTimezone('Asia/Yakutsk'), - Carbon::createFromTimestampMs($dates[1])->setTimezone('Asia/Yakutsk'), + $this->dateRangeService->parseDate($dates[0]), + $this->dateRangeService->parseDate($dates[1]), ]; } } diff --git a/app/Services/StatisticsService.php b/app/Services/StatisticsService.php index 6e90953..ce2f953 100644 --- a/app/Services/StatisticsService.php +++ b/app/Services/StatisticsService.php @@ -135,6 +135,11 @@ class StatisticsService $bedDaysSum = 0; $avgBedDays = 0; $preoperativeSum = 0; + $preoperativePatientCount = 0; + $preoperativeTotalRecords = 0; + $preoperativePatientRecords = 0; + $preoperativeAverageSum = 0; + $preoperativeAverageCount = 0; if (isset($metrics[$deptId])) { foreach ($metrics[$deptId] as $item) { @@ -153,10 +158,24 @@ class StatisticsService 16 => $unwanted = (int) $value, 25 => $bedDaysSum += $value, 19 => $lethalitySum = $value, + 21 => $preoperativeAverageSum += $value, 26 => $preoperativeSum += $value, + 27 => $preoperativePatientCount += (int) $value, // 24 => $completePlanProgress = (int)$value, default => null }; + + if ((int) $item->rf_metrika_item_id === 21) { + $preoperativeAverageCount += (int) $item->records_count; + } + + if ((int) $item->rf_metrika_item_id === 26) { + $preoperativeTotalRecords += (int) $item->records_count; + } + + if ((int) $item->rf_metrika_item_id === 27) { + $preoperativePatientRecords += (int) $item->records_count; + } } } @@ -170,10 +189,15 @@ class StatisticsService $percentLoaded = $bedsCount > 0 ? round($currentCount * 100 / $bedsCount) : 0; // Средний койко-день - $avgBedDays = $outcome > 0 ? round($bedDaysSum / $outcome, 2) : 0; + $avgBedDays = $outcome > 0 ? round($bedDaysSum / $outcome, 1) : 0; // Предоперационный койко-день - $preoperativeValue = + $canUsePreoperativeTotals = $preoperativePatientCount > 0 + && $preoperativeTotalRecords > 0 + && $preoperativePatientRecords >= $preoperativeTotalRecords; + $preoperativeValue = $canUsePreoperativeTotals + ? round($preoperativeSum / $preoperativePatientCount, 1) + : ($preoperativeAverageCount > 0 ? round($preoperativeAverageSum / $preoperativeAverageCount, 1) : 0); // Летальность $lethality = $outcome > 0 ? round(($deceased / $outcome) * 100, 2) : 0; @@ -203,8 +227,14 @@ class StatisticsService 'countStaff' => $staff, 'countObservable' => $observable, 'countUnwanted' => $unwanted, + 'averageBedDays' => $avgBedDays, + 'bedDaysSum' => $bedDaysSum, + 'preoperativeDays' => $preoperativeValue, + 'preoperativeSum' => $preoperativeSum, + 'preoperativePatientCount' => $preoperativePatientCount, + 'progressPlanOfYear' => $periodPlan, 'percentPlanOfYear' => $percentPlanOfYear, 'lethality' => $lethality, @@ -248,6 +278,11 @@ class StatisticsService 'deceased_sum' => 0, 'percentLoadedBeds_total' => 0, 'percentLoadedBeds_count' => 0, + 'bedDaysSum' => 0, + + 'preoperativeSum' => 0, + 'preoperativePatientCount' => 0, + 'staff_sum' => 0, 'observable_sum' => 0, 'unwanted_sum' => 0, @@ -272,6 +307,9 @@ class StatisticsService $totals['deceased_sum'] += $data['deceased']; $totals['percentLoadedBeds_total'] += $data['percentLoadedBeds']; $totals['percentLoadedBeds_count']++; + $totals['bedDaysSum'] += $data['bedDaysSum']; + $totals['preoperativeSum'] += $data['preoperativeSum']; + $totals['preoperativePatientCount'] += $data['preoperativePatientCount']; $totals['staff_sum'] += $data['countStaff']; $totals['observable_sum'] += $data['countObservable']; $totals['unwanted_sum'] += $data['countUnwanted']; @@ -325,11 +363,13 @@ class StatisticsService */ private function createTotalRow(string $type, array $total, bool $isGrandTotal): array { + if ($total['preoperativePatientCount'] === 0) $total['preoperativePatientCount'] = 1; + if ($total['outcome_sum'] === 0) $total['outcome_sum'] = 1; return [ 'isTotalRow' => ! $isGrandTotal, 'isGrandTotal' => $isGrandTotal, 'department' => $isGrandTotal ? 'ОБЩИЕ ИТОГИ:' : 'ИТОГО:', - 'beds' => '—', + 'beds' => $total['beds_sum'], 'recipients' => [ 'all' => $total['recipients_all_sum'], 'plan' => $total['recipients_plan_sum'], @@ -344,9 +384,9 @@ class StatisticsService 'emergency' => $total['emergency_surgical_sum'], ], 'deceased' => $total['deceased_sum'], - 'averageBedDays' => '—', - 'preoperativeDays' => '—', - 'lethality' => '—', + 'averageBedDays' => round($total['bedDaysSum'] / $total['outcome_sum'], 1), + 'preoperativeDays' => round($total['preoperativeSum'] / $total['preoperativePatientCount'] < 0 ?? 1, 1), + 'lethality' => round(($total['deceased_sum'] / $total['outcome_sum']) * 100, 2), 'type' => $type, 'departments_count' => $total['departments_count'], 'countStaff' => $total['staff_sum'], diff --git a/composer.json b/composer.json index c94a175..8f6df1a 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,9 @@ "laravel/framework": "^12.0", "laravel/sanctum": "^4.0", "laravel/tinker": "^2.10.1", - "maatwebsite/excel": "^3.1" + "maatwebsite/excel": "^3.1", + "spatie/laravel-permission": "^7.4", + "spatie/laravel-sluggable": "^4.0" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index a9eec14..eb8fc98 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "402f4b0051ff4985e04537ae489f5c02", + "content-hash": "94c6e33df7f1b2b6d0edeb558af1441f", "packages": [ { "name": "brick/math", @@ -4014,6 +4014,231 @@ }, "time": "2025-12-14T04:43:48+00:00" }, + { + "name": "spatie/laravel-package-tools", + "version": "1.93.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-02-21T12:49:54+00:00" + }, + { + "name": "spatie/laravel-permission", + "version": "7.4.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "6f60746145bbdb1692021d44860d7850c1d2041a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/6f60746145bbdb1692021d44860d7850c1d2041a", + "reference": "6f60746145bbdb1692021d44860d7850c1d2041a", + "shasum": "" + }, + "require": { + "illuminate/auth": "^12.0|^13.0", + "illuminate/container": "^12.0|^13.0", + "illuminate/contracts": "^12.0|^13.0", + "illuminate/database": "^12.0|^13.0", + "php": "^8.3", + "spatie/laravel-package-tools": "^1.0" + }, + "require-dev": { + "larastan/larastan": "^3.9", + "laravel/passport": "^13.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^10.0|^11.0", + "pestphp/pest": "^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^3.0|^4.1", + "phpstan/phpstan": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "7.x-dev", + "dev-master": "7.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Permission\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 12 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-permission/issues", + "source": "https://github.com/spatie/laravel-permission/tree/7.4.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-04-28T14:02:19+00:00" + }, + { + "name": "spatie/laravel-sluggable", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-sluggable.git", + "reference": "483066bc8fc20f99f177b4e740e7b987ea8699f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-sluggable/zipball/483066bc8fc20f99f177b4e740e7b987ea8699f9", + "reference": "483066bc8fc20f99f177b4e740e7b987ea8699f9", + "shasum": "" + }, + "require": { + "illuminate/database": "^12.0|^13.0", + "illuminate/support": "^12.0|^13.0", + "php": "^8.3" + }, + "require-dev": { + "larastan/larastan": "^3.0", + "laravel/pint": "^1.24", + "orchestra/testbench": "^10.0|^11.0", + "pestphp/pest": "^4.0", + "spatie/laravel-translatable": "^6.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "SelfHealing": "Spatie\\Sluggable\\Facades\\SelfHealing" + }, + "providers": [ + "Spatie\\Sluggable\\SluggableServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\Sluggable\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Generate slugs when saving Eloquent models", + "homepage": "https://github.com/spatie/laravel-sluggable", + "keywords": [ + "eloquent", + "laravel", + "laravel-sluggable", + "self-healing", + "slug", + "slugs", + "spatie", + "translatable" + ], + "support": { + "source": "https://github.com/spatie/laravel-sluggable/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-04-27T13:55:13+00:00" + }, { "name": "symfony/clock", "version": "v7.4.0", diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 0000000..4331c3c --- /dev/null +++ b/config/permission.php @@ -0,0 +1,219 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Role::class, + + /* + * When using the "Teams" feature from this package, we need to know which + * Eloquent model should be used to retrieve your teams. Of course, it + * is often just the "Team" model but you may use whatever you like. + */ + 'team' => null, + + /* + * When using the "HasModels" trait and passing raw IDs to syncModels, + * attachModels, or detachModels, this model class will be used to + * resolve those IDs. If null, defaults to the guard's model. + */ + 'default_model' => null, + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'app_roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'app_permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'app_model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'app_model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'app_role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => 'app_role_id', // default 'role_id', + 'permission_pivot_key' => 'app_permission_id', // default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttachedEvent + * \Spatie\Permission\Events\RoleDetachedEvent + * \Spatie\Permission\Events\PermissionAttachedEvent + * \Spatie\Permission\Events\PermissionDetachedEvent + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => DefaultTeamResolver::class, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/database/migrations/0002_04_29_110118_create_report_patient_types_table.php b/database/migrations/0002_04_29_110118_create_report_patient_types_table.php new file mode 100644 index 0000000..b124419 --- /dev/null +++ b/database/migrations/0002_04_29_110118_create_report_patient_types_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->string('code')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('report_patient_types'); + } +}; diff --git a/database/migrations/0002_04_29_110124_create_report_patient_statuses_table.php b/database/migrations/0002_04_29_110124_create_report_patient_statuses_table.php new file mode 100644 index 0000000..18925fd --- /dev/null +++ b/database/migrations/0002_04_29_110124_create_report_patient_statuses_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->string('code')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('report_patient_statuses'); + } +}; diff --git a/database/migrations/0002_04_29_110848_create_report_statuses_table.php b/database/migrations/0002_04_29_110848_create_report_statuses_table.php new file mode 100644 index 0000000..ec26525 --- /dev/null +++ b/database/migrations/0002_04_29_110848_create_report_statuses_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->string('code')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('report_statuses'); + } +}; diff --git a/database/migrations/0002_04_29_115250_create_permission_tables.php b/database/migrations/0002_04_29_115250_create_permission_tables.php new file mode 100644 index 0000000..8986275 --- /dev/null +++ b/database/migrations/0002_04_29_115250_create_permission_tables.php @@ -0,0 +1,137 @@ +id(); // permission id + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + /** + * See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered. + */ + Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { + $table->id(); // role id + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->cascadeOnDelete(); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + }); + + Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->cascadeOnDelete(); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->cascadeOnDelete(); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->cascadeOnDelete(); + + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + + Schema::dropIfExists($tableNames['role_has_permissions']); + Schema::dropIfExists($tableNames['model_has_roles']); + Schema::dropIfExists($tableNames['model_has_permissions']); + Schema::dropIfExists($tableNames['roles']); + Schema::dropIfExists($tableNames['permissions']); + } +}; diff --git a/database/migrations/2026_04_29_102307_create_report_nurses_table.php b/database/migrations/2026_04_29_102307_create_report_nurses_table.php new file mode 100644 index 0000000..bd9eee0 --- /dev/null +++ b/database/migrations/2026_04_29_102307_create_report_nurses_table.php @@ -0,0 +1,47 @@ +id(); + $table->date('report_date'); + $table->dateTime('sent_at')->nullable(); + + $table->string('period_type')->default('day') // day|week|month|year + ->comment('Тип отчетного периода'); + $table->dateTime('period_start')->nullable() + ->comment('Начало отчетного периода'); + $table->dateTime('period_end')->nullable() + ->comment('Окончание отчетного периода'); + + $table->integer('report_month')->storedAs('EXTRACT(MONTH FROM created_at)::integer') + ->comment('Отчетный месяц'); + $table->integer('report_year')->storedAs('EXTRACT(YEAR FROM created_at)::integer') + ->comment('Отчетный год'); + + $table->foreignIdFor(\App\Models\ReportStatus::class, 'status_id'); + + $table->foreignIdFor(\App\Models\MisLpuDoctor::class, 'rf_lpudoctor_id')->nullable(); + $table->foreignIdFor(\App\Models\Department::class, 'rf_department_id')->default(1); + $table->foreignIdFor(\App\Models\User::class, 'rf_user_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('report_nurses'); + } +}; diff --git a/database/migrations/2026_05_04_131344_create_medical_history_corrections_table.php b/database/migrations/2026_05_04_131344_create_medical_history_corrections_table.php new file mode 100644 index 0000000..5b36e2b --- /dev/null +++ b/database/migrations/2026_05_04_131344_create_medical_history_corrections_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignIdFor(\App\Models\MedicalHistory::class, 'medical_history_id'); + $table->string('medical_card_number')->nullable(); + $table->string('full_name')->nullable(); + $table->dateTime('birth_date')->nullable(); + $table->dateTime('recipient_date')->nullable(); + $table->dateTime('extract_date')->nullable(); + $table->dateTime('death_date')->nullable(); + $table->boolean('male')->nullable(); + $table->integer('urgency_id')->nullable(); + $table->integer('hospital_result_id')->nullable(); + $table->integer('visit_result_id')->nullable(); + $table->foreignIdFor(\App\Models\User::class, 'user_id'); + $table->integer('mis_user_id'); + $table->text('comment')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('medical_history_corrections'); + } +}; diff --git a/database/migrations/2026_05_04_141225_create_medical_history_nurses_table.php b/database/migrations/2026_05_04_141225_create_medical_history_nurses_table.php new file mode 100644 index 0000000..99123b6 --- /dev/null +++ b/database/migrations/2026_05_04_141225_create_medical_history_nurses_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('source_type')->default('manual'); + $table->string('medical_card_number'); + $table->string('full_name'); + $table->date('birth_date'); + $table->dateTime('recipient_date'); + $table->dateTime('extract_date')->nullable(); + $table->dateTime('death_date')->nullable(); + $table->boolean('male')->default(false); + $table->integer('urgency_id'); + $table->integer('hospital_result_id')->nullable(); + $table->integer('visit_result_id')->nullable(); + $table->foreignIdFor(\App\Models\User::class, 'user_id'); + $table->integer('mis_user_id')->nullable(); + $table->text('comment')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('medical_history_nurses'); + } +}; diff --git a/database/seeders/PermissionAndRoleSeeder.php b/database/seeders/PermissionAndRoleSeeder.php new file mode 100644 index 0000000..fc2bf23 --- /dev/null +++ b/database/seeders/PermissionAndRoleSeeder.php @@ -0,0 +1,62 @@ + 'Создание отчета']); + Permission::create(['name' => 'Редактирование отчета']); + Permission::create(['name' => 'Просмотр статистики']); + + Permission::create(['name' => 'Создание и редактирование пользователей']); + Permission::create(['name' => 'Создание и редактирование прав и ролей']); + Permission::create(['name' => 'Создание и редактирование метрик']); + + $admin = Role::create(['name' => 'admin']); + $gv = Role::create(['name' => 'gv']); + $zam = Role::create(['name' => 'zam']); + $zav = Role::create(['name' => 'zav']); + $dej = Role::create(['name' => 'dej']); + $nurse = Role::create(['name' => 'nurse']); + + $admin->givePermissionTo([ + 'Создание отчета', + 'Редактирование отчета', + 'Просмотр статистики', + 'Создание и редактирование пользователей', + 'Создание и редактирование прав и ролей', + 'Создание и редактирование метрик', + ]); + $gv->givePermissionTo([ + 'Создание отчета', + 'Редактирование отчета', + 'Просмотр статистики', + ]); + $zam->givePermissionTo([ + 'Создание отчета', + 'Редактирование отчета', + 'Просмотр статистики', + ]); + $zav->givePermissionTo([ + 'Создание отчета', + 'Редактирование отчета', + 'Просмотр статистики', + ]); + $dej->givePermissionTo([ + 'Создание отчета', + ]); + $nurse->givePermissionTo([ + 'Создание отчета', + ]); + } +} diff --git a/database/seeders/ReportLibSeeder.php b/database/seeders/ReportLibSeeder.php new file mode 100644 index 0000000..7cfeca7 --- /dev/null +++ b/database/seeders/ReportLibSeeder.php @@ -0,0 +1,17 @@ + [props.minH, props.maxH], ([minH, maxH]) => {