From 2041ab54ea4d5f5d69acfcb936281efe850f0f45 Mon Sep 17 00:00:00 2001 From: brusnitsyn Date: Tue, 21 Apr 2026 10:08:14 +0900 Subject: [PATCH] modified: .gitignore --- .gitignore | 1 + app/Console/Commands/FillReportsFromDate.php | 94 +- app/Data/UnifiedPatientData.php | 251 ++++ .../Controllers/Api/OperationController.php | 4 +- app/Http/Controllers/Api/ReportController.php | 583 ++++++---- app/Http/Controllers/TestController.php | 25 + app/Http/Controllers/Web/ReportController.php | 127 +- app/Http/Middleware/HandleInertiaRequests.php | 3 + .../DepartmentPatientOperationResource.php | 36 + .../Mis/FormattedPatientResource.php | 63 +- app/Models/DepartmentPatient.php | 55 + app/Models/DepartmentPatientOperation.php | 36 + app/Models/LifeMisMigrationPatient.php | 77 +- app/Models/MedicalHistorySnapshot.php | 24 + app/Models/MisMedicalHistory.php | 22 +- app/Models/MisMigrationPatient.php | 90 +- app/Models/MisOperationPurpose.php | 17 + app/Models/MisReanimation.php | 17 + app/Models/MisSurgicalOperation.php | 14 + app/Models/ObservationPatient.php | 6 + app/Models/Report.php | 8 +- app/Services/AutoReportService.php | 223 +--- app/Services/DateRange.php | 4 +- app/Services/DateRangeService.php | 36 +- app/Services/PatientMigrationService.php | 170 +++ app/Services/PatientService.php | 445 +++++-- app/Services/ReportPageService.php | 44 + app/Services/ReportService.php | 938 ++++++++++++--- app/Services/SnapshotService.php | 368 ++++-- app/Services/StationarBranchService.php | 18 +- app/Services/UnifiedPatientService.php | 391 +++++++ config/time.php | 5 + ...20000_create_department_patients_table.php | 39 + ...mns_to_medical_history_snapshots_table.php | 52 + ...eference_to_observation_patients_table.php | 24 + ...te_department_patient_operations_table.php | 31 + ...ble_in_medical_history_snapshots_table.php | 17 + ...to_department_patient_operations_table.php | 24 + ...09_add_period_columns_in_reports_table.php | 41 + ...add_code_column_in_metrika_items_table.php | 28 + docs/faq.md | 85 ++ docs/index.md | 79 ++ docs/reporting.md | 132 +++ docs/statistics.md | 108 ++ resources/css/app.css | 32 + resources/js/Components/StartButton.vue | 60 +- resources/js/Composables/useGlobalLoading.js | 45 + resources/js/Composables/useServerTime.js | 102 ++ resources/js/Layouts/AppLayout.vue | 79 +- .../js/Layouts/Components/AppUserButton.vue | 12 +- resources/js/Pages/Index.vue | 121 +- .../Components/ManualPatientEditModal.vue | 238 ++++ .../Components/ManualPatientLinkModal.vue | 83 ++ .../Report/Components/ManualPatientModal.vue | 191 +++ .../ManualPatientOperationsModal.vue | 412 +++++++ .../Components/ManualPatientOutcomeModal.vue | 65 ++ .../Report/Components/MoveModalComment.vue | 6 +- .../Report/Components/ReportFormInput.vue | 4 +- .../Pages/Report/Components/ReportHeader.vue | 3 +- .../Pages/Report/Components/ReportSection.vue | 277 +++-- .../Report/Components/ReportSectionHeader.vue | 51 +- .../Report/Components/ReportSectionItem.vue | 299 +++-- .../Report/Components/SelectUserModal.vue | 14 +- resources/js/Pages/Report/Index.vue | 78 +- .../Components/ModalObservablePatients.vue | 2 +- resources/js/Pages/Statistic/Index.vue | 31 +- resources/js/Pages/TestQuery.vue | 13 +- resources/js/Stores/report.js | 474 +++++--- resources/js/app.js | 19 +- resources/js/bootstrap.js | 9 +- routes/api.php | 13 +- routes/web.php | 11 +- tests/Feature/AutoFillReportsTest.php | 457 ++++++++ tests/Feature/ReportPatientsServicesTest.php | 1021 +++++++++++++++++ 74 files changed, 7533 insertions(+), 1544 deletions(-) create mode 100644 app/Data/UnifiedPatientData.php create mode 100644 app/Http/Controllers/TestController.php create mode 100644 app/Http/Resources/Api/DepartmentPatientOperationResource.php create mode 100644 app/Models/DepartmentPatient.php create mode 100644 app/Models/DepartmentPatientOperation.php create mode 100644 app/Models/MisOperationPurpose.php create mode 100644 app/Models/MisReanimation.php create mode 100644 app/Services/ReportPageService.php create mode 100644 app/Services/UnifiedPatientService.php create mode 100644 config/time.php create mode 100644 database/migrations/2026_04_09_120000_create_department_patients_table.php create mode 100644 database/migrations/2026_04_09_120100_add_unified_columns_to_medical_history_snapshots_table.php create mode 100644 database/migrations/2026_04_09_120200_add_department_patient_reference_to_observation_patients_table.php create mode 100644 database/migrations/2026_04_15_090000_create_department_patient_operations_table.php create mode 100644 database/migrations/2026_04_15_101000_make_rf_medicalhistory_id_nullable_in_medical_history_snapshots_table.php create mode 100644 database/migrations/2026_04_15_113000_add_urgency_to_department_patient_operations_table.php create mode 100644 database/migrations/2026_04_20_093409_add_period_columns_in_reports_table.php create mode 100644 database/migrations/2026_04_20_163507_add_code_column_in_metrika_items_table.php create mode 100644 docs/faq.md create mode 100644 docs/index.md create mode 100644 docs/reporting.md create mode 100644 docs/statistics.md create mode 100644 resources/js/Composables/useGlobalLoading.js create mode 100644 resources/js/Composables/useServerTime.js create mode 100644 resources/js/Pages/Report/Components/ManualPatientEditModal.vue create mode 100644 resources/js/Pages/Report/Components/ManualPatientLinkModal.vue create mode 100644 resources/js/Pages/Report/Components/ManualPatientModal.vue create mode 100644 resources/js/Pages/Report/Components/ManualPatientOperationsModal.vue create mode 100644 resources/js/Pages/Report/Components/ManualPatientOutcomeModal.vue create mode 100644 tests/Feature/AutoFillReportsTest.php create mode 100644 tests/Feature/ReportPatientsServicesTest.php diff --git a/.gitignore b/.gitignore index b71b1ea..714ca7b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ Homestead.json Homestead.yaml Thumbs.db +.codex diff --git a/app/Console/Commands/FillReportsFromDate.php b/app/Console/Commands/FillReportsFromDate.php index 3533502..ad27b0e 100644 --- a/app/Console/Commands/FillReportsFromDate.php +++ b/app/Console/Commands/FillReportsFromDate.php @@ -27,12 +27,25 @@ class FillReportsFromDate extends Command public function handle() { - $startDate = $this->option('date') ?? Carbon::now()->subDays(7)->format('Y-m-d'); - $endDate = $this->option('end-date') ?? Carbon::now()->format('Y-m-d'); + $startDate = $this->option('date') ?? Carbon::now('Asia/Yakutsk')->subDays(7)->format('Y-m-d'); + $endDate = $this->option('end-date') ?? Carbon::now('Asia/Yakutsk')->format('Y-m-d'); $departmentId = $this->option('department'); $userId = $this->option('user'); $force = $this->option('force'); + try { + $start = Carbon::createFromFormat('Y-m-d', $startDate, 'Asia/Yakutsk')->startOfDay(); + $end = Carbon::createFromFormat('Y-m-d', $endDate, 'Asia/Yakutsk')->startOfDay(); + } catch (\Throwable) { + $this->error('Неверный формат даты. Используйте YYYY-MM-DD.'); + return 1; + } + + if ($start->gt($end)) { + $this->error('Дата начала больше даты окончания.'); + return 1; + } + $this->info("Заполнение отчетов с {$startDate} по {$endDate}"); // Получаем отделения @@ -51,34 +64,27 @@ class FillReportsFromDate extends Command foreach ($departments as $department) { $this->info("Обработка отделения: {$department->name_short}"); - // Получаем пользователей отделения - $users = $userId - ? User::where('id', $userId)->whereHas('departments', function ($query) use ($department) { - $query->where('rf_department_id', $department->department_id); - })->get() - : $this->getDepartmentUsers($department); + $user = $this->resolveResponsibleUser($department, $userId); - if ($users->isEmpty()) { - $this->warn("В отделении {$department->name} нет пользователей"); + if (!$user) { + $this->warn("В отделении {$department->name_short} нет подходящего пользователя для автозаполнения"); continue; } - foreach ($users as $user) { - try { - $created = $this->autoReportService->fillReportsForUser( - $user, - $startDate, - $endDate, - $departmentId, - $force, - ); + try { + $created = $this->autoReportService->fillReportsForUser( + $user, + $startDate, + $endDate, + $department, + $force, + ); - $totalReports += $created; - $this->info("Для пользователя {$user->name} создано {$created} отчетов"); - } catch (\Exception $e) { - $totalErrors++; - $this->error("Ошибка для пользователя {$user->name}: {$e->getMessage()}"); - } + $totalReports += $created; + $this->info("Для пользователя {$user->name} создано {$created} отчетов"); + } catch (\Exception $e) { + $totalErrors++; + $this->error("Ошибка для пользователя {$user->name}: {$e->getMessage()}"); } } @@ -87,15 +93,37 @@ class FillReportsFromDate extends Command return 0; } - private function getDepartmentUsers(Department $department) + private function resolveResponsibleUser(Department $department, ?string $userId): ?User { - // Получаем пользователей, которые могут создавать отчеты - return User::where('rf_department_id', $department->department_id) - ->where(function ($query) { - $query->where('role', 'doctor') - ->orWhere('role', 'head_of_department'); - }) + $query = User::query() ->where('is_active', true) - ->get(); + ->whereHas('departments', function ($departmentQuery) use ($department) { + $departmentQuery->where('rf_department_id', $department->department_id); + }) + ->whereHas('roles', function ($roleQuery) { + $roleQuery->where('slug', 'doctor'); + }) + ->with(['roles', 'departments']); + + if ($userId) { + return $query->where('id', $userId)->first(); + } + + return $query->get() + ->sortBy(function (User $user) use ($department) { + $departmentLink = $user->departments->firstWhere('rf_department_id', $department->department_id); + $isDoctor = $user->roles->contains('slug', 'doctor'); + $isFavorite = (bool) optional($departmentLink)->is_favorite; + $order = optional($departmentLink)->order ?? PHP_INT_MAX; + + return sprintf( + '%d-%d-%010d-%010d', + $isDoctor ? 0 : 1, + $isFavorite ? 0 : 1, + $order, + $user->id + ); + }) + ->first(); } } diff --git a/app/Data/UnifiedPatientData.php b/app/Data/UnifiedPatientData.php new file mode 100644 index 0000000..f84fa44 --- /dev/null +++ b/app/Data/UnifiedPatientData.php @@ -0,0 +1,251 @@ +BD ? Carbon::parse($patient->BD) : null; + $birthDate = $birthDateValue?->format('Y-m-d'); + $manualId = $linkedManualPatient?->department_patient_id; + $outcomeMigration = $patient->relationLoaded('outcomeMigration') + ? $patient->outcomeMigration->first() + : null; + $migration = $patient->relationLoaded('migrations') + ? $patient->migrations->first() + : null; + $diagnosisMkb = $outcomeMigration?->mainDiagnosis?->mkb ?? $migration?->mainDiagnosis?->mkb; + $operations = $patient->relationLoaded('surgicalOperations') + ? $patient->surgicalOperations->map(fn ($operation) => [ + 'code' => $operation->serviceMedical?->ServiceMedicalCode, + 'name' => $operation->serviceMedical?->ServiceMedicalName, + ])->values()->all() + : []; + + return new self( + id: $manualId ? "manual:{$manualId}" : "mis:{$patient->MedicalHistoryID}", + patientUid: "mis:{$patient->MedicalHistoryID}", + sourceType: $manualId ? 'manual' : 'mis', + departmentPatientId: $manualId, + medicalHistoryId: $patient->MedicalHistoryID, + fullname: $linkedManualPatient?->full_name ?: mb_convert_case(trim("{$patient->FAMILY} {$patient->Name} {$patient->OT}"), MB_CASE_TITLE, 'UTF-8'), + birthDate: $linkedManualPatient?->birth_date?->format('Y-m-d') ?? $birthDate, + age: $birthDateValue?->age ?? $linkedManualPatient?->birth_date?->age, + mkb: [ + 'ds' => $linkedManualPatient?->diagnosis_code ?: $diagnosisMkb?->DS, + 'name' => $linkedManualPatient?->diagnosis_name ?: $diagnosisMkb?->NAME, + ], + operations: $operations, + patientKind: $linkedManualPatient?->patient_kind ?: self::resolvePatientKind($patient->rf_EmerSignID), + admittedAt: $linkedManualPatient?->admitted_at?->toIso8601String() ?? $patient->DateRecipient?->toIso8601String(), + outcomeType: $patient->outcome_type ?? $linkedManualPatient?->outcome_type, + outcomeDate: $patient->outcome_date ?? $linkedManualPatient?->outcome_at?->toIso8601String(), + comment: $comment, + isRecipientToday: $isRecipientToday, + isManual: (bool) $linkedManualPatient, + canManageManual: (bool) $linkedManualPatient, + ); + } + public static function fromMisMigrationPatient( + MisMigrationPatient $migration, + bool $isRecipientToday = false, + ?DepartmentPatient $linkedManualPatient = null, + ?string $comment = null + ): self { + $birthDateValue = $migration->medicalHistory->BD ? Carbon::parse($migration->medicalHistory->BD) : null; + $birthDate = $birthDateValue?->format('Y-m-d'); + $manualId = $linkedManualPatient?->department_patient_id; + $medicalHistory = $migration->medicalHistory; + $outcomeMigration = $medicalHistory->relationLoaded('outcomeMigration') + ? $medicalHistory->outcomeMigration->first() + : null; + $operations = $medicalHistory->relationLoaded('surgicalOperations') + ? $medicalHistory->surgicalOperations->map(fn ($operation) => [ + 'code' => $operation->serviceMedical?->ServiceMedicalCode, + 'name' => $operation->serviceMedical?->ServiceMedicalName, + ])->values()->all() + : []; + + return new self( + id: $manualId ? "manual:{$manualId}" : "mis:{$medicalHistory->MedicalHistoryID}", + patientUid: "mis:{$medicalHistory->MedicalHistoryID}", + sourceType: $manualId ? 'manual' : 'mis', + departmentPatientId: $manualId, + medicalHistoryId: $medicalHistory->MedicalHistoryID, + fullname: $linkedManualPatient?->full_name ?: mb_convert_case(trim("{$medicalHistory->FAMILY} {$medicalHistory->Name} {$medicalHistory->OT}"), MB_CASE_TITLE, 'UTF-8'), + birthDate: $linkedManualPatient?->birth_date?->format('Y-m-d') ?? $birthDate, + age: $birthDateValue?->age ?? $linkedManualPatient?->birth_date?->age, + mkb: [ + 'ds' => $linkedManualPatient?->diagnosis_code ?: $outcomeMigration?->mainDiagnosis?->mkb?->DS, + 'name' => $linkedManualPatient?->diagnosis_name ?: $outcomeMigration?->mainDiagnosis?->mkb?->NAME, + ], + operations: $operations, + patientKind: $linkedManualPatient?->patient_kind ?: self::resolvePatientKind($medicalHistory->rf_EmerSignID), + admittedAt: $linkedManualPatient?->admitted_at?->toIso8601String() ?? $medicalHistory->DateRecipient?->toIso8601String(), + outcomeType: $migration->outcome_type ?? $linkedManualPatient?->outcome_type, + outcomeDate: $migration->outcome_date ?? $linkedManualPatient?->outcome_at?->toIso8601String(), + comment: $comment, + isRecipientToday: $isRecipientToday, + isManual: (bool) $linkedManualPatient, + canManageManual: (bool) $linkedManualPatient, + ); + } + + public static function fromDepartmentPatient( + DepartmentPatient $patient, + bool $isRecipientToday = false, + ?array $operations = null, + ?string $comment = null + ): self { + return new self( + id: "manual:{$patient->department_patient_id}", + patientUid: $patient->rf_medicalhistory_id ? "mis:{$patient->rf_medicalhistory_id}" : "manual:{$patient->department_patient_id}", + sourceType: 'manual', + departmentPatientId: $patient->department_patient_id, + medicalHistoryId: $patient->rf_medicalhistory_id, + fullname: $patient->full_name, + birthDate: $patient->birth_date?->format('Y-m-d'), + age: $patient->birth_date?->age, + mkb: [ + 'ds' => $patient->diagnosis_code, + 'name' => $patient->diagnosis_name, + ], + operations: $operations ?? [], + patientKind: $patient->patient_kind, + admittedAt: $patient->admitted_at?->toIso8601String(), + outcomeType: $patient->outcome_type, + outcomeDate: $patient->outcome_at?->toIso8601String(), + comment: $comment, + isRecipientToday: $isRecipientToday, + isManual: true, + canManageManual: true, + ); + } + + public static function fromSnapshot( + MedicalHistorySnapshot $snapshot, + bool $isRecipientToday = false, + ?array $operations = null + ): self + { + $birthDate = self::normalizeDate($snapshot->birth_date); + + return new self( + id: $snapshot->rf_department_patient_id ? "manual:{$snapshot->rf_department_patient_id}" : ($snapshot->patient_uid ?: "mis:{$snapshot->rf_medicalhistory_id}"), + patientUid: $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}"), + sourceType: $snapshot->patient_source_type ?: ($snapshot->is_manual ? 'manual' : 'mis'), + departmentPatientId: $snapshot->rf_department_patient_id, + medicalHistoryId: $snapshot->rf_medicalhistory_id, + fullname: $snapshot->full_name ?: 'Пациент без имени', + birthDate: $birthDate, + age: $birthDate ? now()->diffInYears($birthDate) : null, + mkb: [ + 'ds' => $snapshot->diagnosis_code, + 'name' => $snapshot->diagnosis_name, + ], + operations: $operations ?? [], + patientKind: $snapshot->patient_kind, + admittedAt: self::normalizeDateTime($snapshot->admitted_at), + outcomeType: $snapshot->outcome_type, + outcomeDate: self::normalizeDateTime($snapshot->outcome_at), + comment: null, + isRecipientToday: $isRecipientToday, + isManual: (bool) $snapshot->is_manual, + canManageManual: false, + ); + } + + public function toSnapshotPayload(string $patientType): array + { + return [ + 'rf_medicalhistory_id' => $this->medicalHistoryId, + 'rf_department_patient_id' => $this->departmentPatientId, + 'patient_type' => $patientType, + 'patient_uid' => $this->patientUid, + 'patient_source_type' => $this->sourceType, + 'patient_kind' => $this->patientKind, + 'full_name' => $this->fullname, + 'birth_date' => $this->birthDate, + 'diagnosis_code' => $this->mkb['ds'] ?? null, + 'diagnosis_name' => $this->mkb['name'] ?? null, + 'admitted_at' => $this->admittedAt, + 'outcome_type' => $this->outcomeType, + 'outcome_at' => $this->outcomeDate, + 'is_manual' => $this->isManual, + ]; + } + + public static function unique(Collection $patients): Collection + { + return $patients->unique(fn (self $patient) => $patient->patientUid)->values(); + } + + private static function resolvePatientKind(?int $emerSignId): ?string + { + return match ($emerSignId) { + 1 => 'plan', + 2, 4 => 'emergency', + default => null, + }; + } + + private static function normalizeDateTime($value): ?string + { + if (!$value) { + return null; + } + + if ($value instanceof CarbonInterface) { + return $value->toIso8601String(); + } + + return (string) $value; + } + + private static function normalizeDate($value): ?string + { + if (!$value) { + return null; + } + + if ($value instanceof CarbonInterface) { + return $value->format('Y-m-d'); + } + + return (string) $value; + } +} diff --git a/app/Http/Controllers/Api/OperationController.php b/app/Http/Controllers/Api/OperationController.php index 480b80e..269c754 100644 --- a/app/Http/Controllers/Api/OperationController.php +++ b/app/Http/Controllers/Api/OperationController.php @@ -15,7 +15,9 @@ class OperationController extends Controller 'historyId' => 'required|integer' ]); - $operations = MisSurgicalOperation::where('rf_MedicalHistoryID', $request->historyId)->get(); + $operations = MisSurgicalOperation::where('rf_MedicalHistoryID', $request->historyId) + ->completed() + ->get(); return response()->json( OperationsResource::collection($operations) diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php index 975136b..4b2013a 100644 --- a/app/Http/Controllers/Api/ReportController.php +++ b/app/Http/Controllers/Api/ReportController.php @@ -3,12 +3,14 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; +use App\Http\Resources\Api\DepartmentPatientOperationResource; use App\Http\Resources\Mis\FormattedPatientResource; use App\Models\Department; use App\Models\MedicalHistorySnapshot; use App\Models\MetrikaGroup; use App\Models\MetrikaResult; use App\Models\MisLpuDoctor; +use App\Models\MisMKB; use App\Models\MisMedicalHistory; use App\Models\MisMigrationPatient; use App\Models\MisStationarBranch; @@ -23,6 +25,7 @@ use App\Services\ReportService; use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Session; use Illuminate\Support\Str; @@ -39,188 +42,29 @@ class ReportController extends Controller public function index(Request $request) { $user = Auth::user(); - $department = $user->department; - - $startDateCarbon = Carbon::now()->firstOfMonth(); - $endDateCarbon = Carbon::now(); - - // Определяем даты в зависимости от роли - [$startDate, $endDate] = $this->dateRangeService->getDateRangeForUser($user, $request->query('startAt'), $request->query('endAt')); - if (Carbon::parse($startDate)->isValid()) { - $startDateCarbon = Carbon::parse($startDate)->setTimeZone('Asia/Yakutsk'); - } - if (Carbon::parse($endDate)->isValid()) { - $endDateCarbon = Carbon::parse($endDate)->setTimeZone('Asia/Yakutsk'); - } - - $reportIds = []; - $reports = $this->getReportsForDateRange($user->rf_department_id, $startDate, $endDate); - $reportIds = $reports->pluck('report_id')->toArray(); - - // Определяем, используем ли мы снапшоты - $reportToday = Report::whereDate('sent_at', $endDate) - ->whereDate('created_at', $endDate) - ->first(); - $useSnapshots = ($user->isAdmin() || $user->isHeadOfDepartment()) || (Carbon::parse($endDate)->isToday() === false || $reportToday); - if ($useSnapshots && ($user->isHeadOfDepartment() || $user->isAdmin())) { - $report = Report::whereDate('sent_at', $endDate) - ->where('rf_department_id', $department->department_id) - ->first(); - $fillableUserId = $report->rf_lpudoctor_id ?? null; - } else { - $fillableUserId = $request->query('userId', $user->rf_lpudoctor_id); - } - - $beds = (int)$department->metrikaDefault()->where('rf_metrika_item_id', 1)->first()->value; - $occupiedBeds = optional(Report::where('rf_department_id', $user->rf_department_id) - ->join('metrika_results', 'reports.report_id', '=', 'metrika_results.rf_report_id') - ->where('metrika_results.rf_metrika_item_id', 8) - ->orderBy('sent_at', 'desc')->first())->value ?? 0; - - $percentLoadedBeds = round(intval($occupiedBeds) * 100 / $beds); //intval($occupiedBeds) * 100 / $beds; - + $departmentId = $request->query('departmentId', $user->department->department_id); + $department = Department::where('department_id', $departmentId)->firstOrFail(); + $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); + $statistics = $this->reportService->getReportStatistics($department, $user, $dateRange); + $reportInfo = $this->reportService->getCurrentReportInfo($department, $user, $dateRange); $metrikaGroup = MetrikaGroup::whereMetrikaGroupId(2)->first(); $metrikaItems = $metrikaGroup->metrikaItems; - $misDepartmentId = $request->user()->department->rf_mis_department_id; - - $branchId = MisStationarBranch::where('rf_DepartmentID', $misDepartmentId) - ->value('StationarBranchID'); - - $unwantedEvents = UnwantedEvent::whereHas('report', function ($query) use ($user, $startDate, $endDate) { - $query->where('rf_department_id', $user->rf_department_id) - ->whereDate('created_at', $endDate); - }) - ->get()->map(function ($item) { - return [ - ...$item->toArray(), - 'created_at' => Carbon::parse($item->created_at)->format('Создано d.m.Y в H:i'), - ]; - }); - - // Определяем, является ли пользователь заведующим/администратором - $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); - - // Получаем статистику в зависимости от источника данных - if ($useSnapshots || $reportToday) { - // Используем снапшоты для статистики -// $plan = $this->getCountFromSnapshots('plan', $reportIds); -// $emergency = $this->getCountFromSnapshots('emergency', $reportIds); - $recipientCount = $this->getPatientsCountFromSnapshot('recipient', $reportIds); - $outcomeCount = $this->getPatientsCountFromSnapshot('outcome', $reportIds); - $currentCount = $this->getPatientsCountFromSnapshot('current', $reportIds); - $deadCount = $this->getPatientsCountFromSnapshot('deceased', $reportIds); - - // Для операций все равно используем реплику с фильтрацией по датам - $surgicalCount = [ - $this->getSurgicalPatientsFromSnapshot('plan', $reportIds), - $this->getSurgicalPatientsFromSnapshot('emergency', $reportIds) - ]; - - $recipientIds = $this->getRecipientIdsFromSnapshots($reportIds); - } else { - // Используем реплику для статистики - $plan = $this->getPlanOrEmergencyPatients( - 'plan', - $isHeadOrAdmin, - $branchId, - $startDate, - $endDate, - true, - today: true - ); - $emergency = $this->getPlanOrEmergencyPatients( - 'emergency', - $isHeadOrAdmin, - $branchId, - $startDate, - $endDate, - true, - today: true - ); - $outcomeCount = $this->getAllOutcomePatients( - $branchId, - $startDate, - $endDate, - true - ); - $currentCount = $this->getCurrentPatients($branchId, true); - $deadCount = $this->getDeceasedOutcomePatients($branchId, $startDate, $endDate, true); - - $surgicalCount = [ - $this->getSurgicalPatients('plan', $isHeadOrAdmin, $branchId, $startDate, $endDate, true), - $this->getSurgicalPatients('emergency', $isHeadOrAdmin, $branchId, $startDate, $endDate, true) - ]; - - $recipientIds = $this->getPlanOrEmergencyPatients( - null, - $isHeadOrAdmin, - $branchId, - $startDate, - $endDate, - false, - true, - true, - today: true - ); - } - - $isActiveSendButton = Carbon::createFromFormat('Y-m-d H:i:s', $endDate)->isToday() && - (!$user->isHeadOfDepartment() && !$user->isAdmin()) && $reportToday == null; - - $reportDoctor = $reportToday?->lpuDoctor; - $message = null; - if ($reportToday) { - if ($reportDoctor && $reportDoctor->LPUDoctorID === intval($fillableUserId)) { - $isActiveSendButton = true; - } else { - $message = "Отчет уже создан пользователем: $reportDoctor->FAM_V $reportDoctor->IM_V $reportDoctor->OT_V"; - } - - $lpuDoctor = $reportDoctor; - } else { - if (Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate)) > 1.0) { - $lpuDoctor = null; - } else { - $lpuDoctor = MisLpuDoctor::where('LPUDoctorID', $fillableUserId)->first(); - } - } - - $isRangeOneDay = $this->dateRangeService->isRangeOneDay($startDate, $endDate); - - $date = $isHeadOrAdmin ? [ - $this->dateRangeService->parseDate($isRangeOneDay ? $endDate : $startDate)->getTimestampMs(), - $this->dateRangeService->parseDate($endDate)->getTimestampMs() - ] : $this->dateRangeService->parseDate($endDate)->getTimestampMs(); - return response()->json([ 'department' => [ - 'beds' => $beds, - 'percentLoadedBeds' => $percentLoadedBeds, - - 'recipientCount' => $useSnapshots ? $recipientCount : $plan + $emergency, //$recipientCount, - 'extractCount' => $outcomeCount, //$extractedCount, - 'currentCount' => $currentCount, - 'deadCount' => $deadCount, - 'surgicalCount' => $surgicalCount, - 'recipientIds' => $recipientIds, + 'department_id' => $department->department_id, + 'department_name' => $department->name_full, + 'beds' => $department->beds, + ...$statistics, ], 'dates' => [ - 'startAt' => $startDateCarbon->getTimestampMs(), - 'endAt' => $endDateCarbon->getTimestampMs() - ], - 'report' => [ - 'report_id' => $reportToday?->report_id, - 'unwantedEvents' => $unwantedEvents, - 'isActiveSendButton' => $isActiveSendButton, - 'message' => $message, - 'isOneDay' => $isRangeOneDay, - 'isHeadOrAdmin' => $isHeadOrAdmin, - 'dates' => $date + 'startAt' => $dateRange->startTimestamp(), + 'endAt' => $dateRange->endTimestamp(), ], + 'report' => $reportInfo, 'metrikaItems' => $metrikaItems, - 'userId' => $fillableUserId, - 'userName' => $lpuDoctor ? "$lpuDoctor->FAM_V $lpuDoctor->IM_V $lpuDoctor->OT_V" : null + 'userId' => $reportInfo['userId'], + 'userName' => $reportInfo['userName'], ]); } @@ -660,22 +504,53 @@ class ReportController extends Controller 'status' => 'required|string', 'startAt' => 'nullable', 'endAt' => 'nullable', - 'departmentId' => 'nullable' + 'departmentId' => 'nullable', + 'page' => 'nullable|integer|min:1', + 'perPage' => 'nullable|integer|min:1|max:1000', + 'search' => 'nullable|string|max:255', ]); $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); $departmentId = $request->get('departmentId', $user->department->department_id); $department = Department::where('department_id', $departmentId)->first(); + $page = (int) ($validated['page'] ?? 1); + $perPage = (int) ($validated['perPage'] ?? 20); + $search = trim((string) ($validated['search'] ?? '')); - $patients = $this->reportService->getPatientsByStatus( + $patients = collect($this->reportService->getPatientsByStatus( $department, Auth::user(), $validated['status'], $dateRange - ); + )); - return response()->json(FormattedPatientResource::collection($patients)); + if ($search !== '') { + $needle = mb_strtolower($search); + $patients = $patients->filter(function ($patient) use ($needle) { + $fullName = mb_strtolower(trim((string) ($patient->fullname ?? "{$patient->FAMILY} {$patient->Name} {$patient->OT}"))); + $diagnosisCode = mb_strtolower((string) ($patient->mkb['ds'] ?? '')); + $diagnosisName = mb_strtolower((string) ($patient->mkb['name'] ?? '')); + + return str_contains($fullName, $needle) + || str_contains($diagnosisCode, $needle) + || str_contains($diagnosisName, $needle); + })->values(); + } + + $total = $patients->count(); + $items = $patients->forPage($page, $perPage)->values(); + $data = FormattedPatientResource::collection($items)->resolve(); + + return response()->json([ + 'data' => $data, + 'meta' => [ + 'total' => $total, + 'page' => $page, + 'perPage' => $perPage, + 'lastPage' => max((int) ceil($total / max($perPage, 1)), 1), + ], + ]); } public function getPatientsCount(Request $request) @@ -704,6 +579,45 @@ class ReportController extends Controller return response()->json($count); } + public function getPatientsCounts(Request $request) + { + $user = Auth::user(); + + $request->validate([ + 'startAt' => 'nullable', + 'endAt' => 'nullable', + 'departmentId' => 'nullable', + 'force' => 'nullable|boolean', + ]); + + $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); + $departmentId = $request->get('departmentId', $user->department->department_id); + $department = Department::where('department_id', $departmentId)->firstOrFail(); + + $cacheKey = sprintf( + 'report:patients-counts:%d:%d:%s:%s', + $user->id, + $department->department_id, + $dateRange->startSql(), + $dateRange->endSql() + ); + + $force = (bool) $request->boolean('force', false); + + if ($force) { + $counts = $this->reportService->getPatientsCountsMap($department, $user, $dateRange); + Cache::put($cacheKey, $counts, now()->addSeconds(30)); + } else { + $counts = Cache::remember($cacheKey, now()->addSeconds(30), function () use ($department, $user, $dateRange) { + return $this->reportService->getPatientsCountsMap($department, $user, $dateRange); + }); + } + + return response()->json([ + 'counts' => $counts, + ]); + } + /** * Получить пациентов (плановых или экстренных) */ @@ -721,17 +635,35 @@ class ReportController extends Controller $isOutcomeStatus = in_array($status, ['outcome-transferred', 'outcome-discharged', 'outcome-deceased']); if ($isOutcomeStatus) { - switch ($status) { - case 'outcome-transferred': - $query = MisMigrationPatient::transferred($branchId, $startDate, $endDate); - break; - case 'outcome-discharged': - $query = MisMigrationPatient::discharged($branchId, $startDate, $endDate); - break; - case 'outcome-deceased': - $query = MisMigrationPatient::deceasedOutcome($branchId, $startDate, $endDate); - break; + $visitResultIds = match ($status) { + 'outcome-transferred' => [4, 14], + 'outcome-discharged' => [1, 11, 2, 12, 7, 18, 48], + 'outcome-deceased' => [5, 6, 15, 16], + default => [], + }; + + $historyQuery = $this->buildOutcomeMedicalHistoryQuery( + $branchId, + $startDate, + $endDate, + $visitResultIds + ); + + if ($onlyIds) { + return $historyQuery->pluck('MedicalHistoryID')->values(); } + + if ($returnedCount) { + return $historyQuery->count(); + } + + return $historyQuery + ->with(['surgicalOperations' => function ($query) use ($startDate, $endDate) { + $query->where('Date', '>=', $startDate) + ->where('Date', '<=', $endDate); + }]) + ->orderBy('DateRecipient', 'DESC') + ->get(); } else { // Разная логика для заведующего и врача if ($isHeadOrAdmin) { @@ -795,42 +727,16 @@ class ReportController extends Controller */ private function getAllOutcomePatients($branchId, $startDate, $endDate, bool $returnedCount = false) { - // Сначала получаем миграции с типами выбытия - $migrations = MisMigrationPatient::outcomePatients($branchId, $startDate, $endDate) - ->select('rf_MedicalHistoryID', 'rf_kl_VisitResultID', 'DateOut') - ->get() - ->groupBy('rf_MedicalHistoryID'); + $query = $this->buildOutcomeMedicalHistoryQuery($branchId, $startDate, $endDate); - if ($migrations->isEmpty()) { - if ($returnedCount) return 0; - return collect(); + if ($returnedCount) { + return $query->count(); } - $medicalHistoryIds = $migrations->keys()->toArray(); - - // Получаем истории - $patients = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds) + return $query ->with(['surgicalOperations']) - ->orderBy('DateRecipient', 'DESC'); - - if ($returnedCount) return $patients->count(); - else $patients = $patients->get(); - - // Добавляем информацию о типе выбытия - return $patients->map(function ($patient) use ($migrations) { - $patientMigrations = $migrations->get($patient->MedicalHistoryID, collect()); - - // Определяем основной тип выбытия (берем последнюю миграцию) - $latestMigration = $patientMigrations->sortByDesc('DateOut')->first(); - - if ($latestMigration) { - $patient->outcome_type = $this->getOutcomeTypeName($latestMigration->rf_kl_VisitResultID); - $patient->outcome_date = $latestMigration->DateOut; - $patient->visit_result_id = $latestMigration->rf_kl_VisitResultID; - } - - return $patient; - }); + ->orderBy('DateRecipient', 'DESC') + ->get(); } /** @@ -852,26 +758,45 @@ class ReportController extends Controller */ private function getDeceasedOutcomePatients($branchId, $startDate, $endDate, bool $returnedCount = false, bool $onlyIds = false) { - $medicalHistoryIds = MisMigrationPatient::deceasedOutcome($branchId, $startDate, $endDate) - ->pluck('rf_MedicalHistoryID') - ->unique() - ->toArray(); - - if (empty($medicalHistoryIds)) { - if ($returnedCount) return 0; - return collect(); - } + $query = $this->buildOutcomeMedicalHistoryQuery( + $branchId, + $startDate, + $endDate, + [5, 6, 15, 16] + ); if ($onlyIds) { - return $medicalHistoryIds; + return $query->pluck('MedicalHistoryID')->values(); } - $query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds) - ->with(['surgicalOperations']) - ->orderBy('DateRecipient', 'DESC'); - if ($returnedCount) return $query->count(); - else return $query->get(); + else return $query + ->with(['surgicalOperations']) + ->orderBy('DateRecipient', 'DESC') + ->get(); + } + + private function buildOutcomeMedicalHistoryQuery( + int $branchId, + string $startDate, + string $endDate, + ?array $visitResultIds = null + ) + { + $startDateOnly = Carbon::parse($startDate)->toDateString(); + $endDateOnly = Carbon::parse($endDate)->toDateString(); + + return MisMedicalHistory::query() + ->where('MedicalHistoryID', '<>', 0) + ->whereDate('DateExtract', '>', $startDateOnly) + ->whereDate('DateExtract', '<=', $endDateOnly) + ->whereHas('migrations', function ($migrationQuery) use ($branchId, $visitResultIds) { + $migrationQuery->where('rf_StationarBranchID', $branchId); + + if ($visitResultIds !== null && !empty($visitResultIds)) { + $migrationQuery->whereIn('rf_kl_VisitResultID', $visitResultIds); + } + }); } /** @@ -880,6 +805,7 @@ class ReportController extends Controller private function getSurgicalPatients(string $status, bool $isHeadOrAdmin, $branchId, $startDate, $endDate, bool $returnedCount = false) { $query = MisSurgicalOperation::where('rf_StationarBranchID', $branchId) + ->completed() // ->whereBetween('Date', [$startDate, $endDate]) ->where('Date', '>=', $startDate) ->where('Date', '<=', $endDate) @@ -930,14 +856,199 @@ class ReportController extends Controller Request $request, ) { $data = $request->validate([ - 'id' => 'required' + 'id' => 'required|string' ]); - ObservationPatient::where('rf_medicalhistory_id', $data['id'])->delete(); + $this->reportService->removeObservationPatient($data['id']); return response()->json()->setStatusCode(200); } + public function createManualPatient(Request $request) + { + $user = Auth::user(); + $data = $request->validate([ + 'departmentId' => 'required|integer', + 'full_name' => 'required|string|max:255', + 'birth_date' => 'required|date', + 'patient_kind' => 'required|in:plan,emergency', + 'diagnosis_code' => 'nullable|string|max:255', + 'diagnosis_name' => 'nullable|string|max:1000', + 'admitted_at' => 'nullable|date', + ]); + + $department = Department::where('department_id', $data['departmentId'])->firstOrFail(); + $patient = $this->reportService->createManualPatient($department, $user, $data); + + return response()->json($patient, 201); + } + + public function setManualPatientOutcome(Request $request, int $departmentPatientId) + { + $data = $request->validate([ + 'outcome_type' => 'required|in:discharged,transferred,deceased', + 'outcome_at' => 'nullable|date', + ]); + + return response()->json( + $this->reportService->setManualPatientOutcome($departmentPatientId, $data) + ); + } + + public function updateManualPatient(Request $request, int $departmentPatientId) + { + $user = Auth::user(); + + $data = $request->validate([ + 'full_name' => 'required|string|max:255', + 'birth_date' => 'required|date', + 'patient_kind' => 'required|in:plan,emergency', + 'diagnosis_code' => 'nullable|string|max:255', + 'diagnosis_name' => 'nullable|string|max:1000', + 'admitted_at' => 'nullable|date', + 'startAt' => 'nullable', + 'endAt' => 'nullable', + ]); + + return response()->json( + $this->reportService->updateManualPatient( + $user, + $departmentPatientId, + $data + ) + ); + } + + public function linkManualPatient(Request $request, int $departmentPatientId) + { + $data = $request->validate([ + 'medical_history_id' => 'required|integer', + ]); + + return response()->json( + $this->reportService->linkManualPatientToMis($departmentPatientId, $data['medical_history_id']) + ); + } + + public function getManualPatientOperations(int $departmentPatientId) + { + $user = Auth::user(); + $operations = $this->reportService->getManualPatientOperations($user, $departmentPatientId); + + return response()->json( + DepartmentPatientOperationResource::collection($operations) + ); + } + + public function createManualPatientOperation(Request $request, int $departmentPatientId) + { + $user = Auth::user(); + $data = $request->validate([ + 'service_id' => 'required|integer', + 'urgency' => 'required|in:plan,emergency', + 'started_at' => 'required|date', + 'ended_at' => 'required|date|after_or_equal:started_at', + ]); + + $operation = $this->reportService->createManualPatientOperation($user, $departmentPatientId, $data); + + return response()->json(new DepartmentPatientOperationResource($operation), 201); + } + + public function updateManualPatientOperation(Request $request, int $departmentPatientId, int $operationId) + { + $user = Auth::user(); + $data = $request->validate([ + 'service_id' => 'required|integer', + 'urgency' => 'required|in:plan,emergency', + 'started_at' => 'required|date', + 'ended_at' => 'required|date|after_or_equal:started_at', + ]); + + $operation = $this->reportService->updateManualPatientOperation($user, $departmentPatientId, $operationId, $data); + + return response()->json(new DepartmentPatientOperationResource($operation)); + } + + public function deleteManualPatientOperation(int $departmentPatientId, int $operationId) + { + $user = Auth::user(); + $this->reportService->deleteManualPatientOperation($user, $departmentPatientId, $operationId); + + return response()->json()->setStatusCode(204); + } + + public function searchMisPatients(Request $request) + { + $data = $request->validate([ + 'departmentId' => 'required|integer', + 'query' => 'required|string|min:2', + ]); + + $department = Department::where('department_id', $data['departmentId'])->firstOrFail(); + $patients = $this->reportService->searchMisPatientsForDepartment($department, $data['query']); + + return response()->json(FormattedPatientResource::collection($patients)); + } + + public function searchMkb(Request $request) + { + $data = $request->validate([ + 'query' => 'required|string|min:1|max:255', + ]); + + $query = trim($data['query']); + $needle = mb_strtolower($query, 'UTF-8'); + $like = "%{$needle}%"; + + $items = MisMKB::query() + ->select(['MKBID', 'DS', 'NAME']) + ->where(function ($builder) use ($like) { + $builder->whereRaw('LOWER("DS") LIKE ?', [$like]) + ->orWhereRaw('LOWER("NAME") LIKE ?', [$like]); + }) + ->orderBy('DS') + ->limit(30) + ->get() + ->map(fn (MisMKB $item) => [ + 'id' => $item->MKBID, + 'code' => $item->DS, + 'name' => $item->NAME, + 'label' => trim(($item->DS ? "{$item->DS} " : '') . ($item->NAME ?? '')), + ]) + ->values(); + + return response()->json($items); + } + + public function searchMedicalServices(Request $request) + { + $data = $request->validate([ + 'query' => 'required|string|min:2|max:255', + ]); + + $query = trim($data['query']); + + $items = \App\Models\MisServiceMedical::query() + ->select(['ServiceMedicalID', 'ServiceMedicalCode', 'ServiceMedicalName']) + ->where(function ($builder) use ($query) { + $builder->where('ServiceMedicalCode', 'like', "%{$query}%") + ->orWhere('ServiceMedicalName', 'like', "%{$query}%"); + }) + ->orderBy('ServiceMedicalCode') + ->limit(30) + ->get() + ->map(fn (\App\Models\MisServiceMedical $item) => [ + 'id' => $item->ServiceMedicalID, + 'code' => $item->ServiceMedicalCode, + 'name' => $item->ServiceMedicalName, + 'label' => trim(($item->ServiceMedicalCode ? "{$item->ServiceMedicalCode} " : '') . ($item->ServiceMedicalName ?? '')), + ]) + ->values(); + + return response()->json($items); + } + // api/report/unwanted-event diff --git a/app/Http/Controllers/TestController.php b/app/Http/Controllers/TestController.php new file mode 100644 index 0000000..9abeb7a --- /dev/null +++ b/app/Http/Controllers/TestController.php @@ -0,0 +1,25 @@ +format('Y-m-d H:i:s'); + $endAt = Carbon::parse('2026-03-31T23:59:00')->format('Y-m-d H:i:s'); + + $cacheKey = "branch_current_2"; + \Cache::tags(["migrations_in_branch_outcome"])->flush(); + + $data = $migrationService->getMigrationsInBranchOutcome(2, $startAt, $endAt); + return Inertia::render('TestQuery', [ + 'data' => $data + ]); + } +} diff --git a/app/Http/Controllers/Web/ReportController.php b/app/Http/Controllers/Web/ReportController.php index 51c11b3..9c1a901 100644 --- a/app/Http/Controllers/Web/ReportController.php +++ b/app/Http/Controllers/Web/ReportController.php @@ -3,23 +3,18 @@ namespace App\Http\Controllers\Web; use App\Http\Controllers\Controller; -use App\Http\Resources\Mis\FormattedPatientResource; use App\Models\Department; -use App\Models\MetrikaGroup; -use App\Models\MetrikaItem; -use App\Models\MisLpuDoctor; -use App\Models\Report; -use App\Models\UnwantedEvent; use App\Services\DateRangeService; +use App\Services\ReportPageService; use App\Services\ReportService; use Illuminate\Http\Request; -use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; use Inertia\Inertia; class ReportController extends Controller { public function __construct( + protected ReportPageService $reportPageService, protected ReportService $reportService, protected DateRangeService $dateRangeService ) {} @@ -28,40 +23,10 @@ class ReportController extends Controller { $user = Auth::user(); $departmentId = $request->query('departmentId', $user->department->department_id); - $department = Department::where('department_id', $departmentId)->first(); //$user->department; - + $department = Department::where('department_id', $departmentId)->firstOrFail(); $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); - // Получаем статистику - $statistics = $this->reportService->getReportStatistics($department, $user, $dateRange); - - // Получаем метрики -// $metrikaGroup = MetrikaGroup::whereMetrikaGroupId(2)->first(); -// $metrikaItems = $metrikaGroup->metrikaItems; - $metrikaItems = MetrikaItem::whereIn('metrika_item_id', [3, 7, 8, 17])->get(); - - // Получаем информацию о текущем отчете - $reportInfo = $this->reportService->getCurrentReportInfo($department, $user, $dateRange); - - return Inertia::render('Report/Index', [ - 'department' => [ - 'department_name' => $department->name_full, - 'department_id' => $department->department_id, - 'beds' => $department->beds, - 'percentLoadedBeds' => $this->calculateBedOccupancy($department, $user), - 'recipientPlanOfYear' => $this->reportService->getRecipientPlanOfYear($department, $dateRange)['plan'], - 'progressPlanOfYear' => $this->reportService->getRecipientPlanOfYear($department, $dateRange)['progress'], - ...$statistics, - ], - 'dates' => [ - 'startAt' => $dateRange->startTimestamp(), - 'endAt' => $dateRange->endTimestamp() - ], - 'report' => $reportInfo, - 'metrikaItems' => $metrikaItems, - 'userId' => $reportInfo['userId'], - 'userName' => $reportInfo['userName'] - ]); + return Inertia::render('Report/Index', $this->reportPageService->build($department, $user, $dateRange)); } public function store(Request $request) @@ -76,90 +41,8 @@ class ReportController extends Controller 'reportId' => 'nullable|integer' ]); - $report = $this->reportService->storeReport($validated, Auth::user(), false); + $this->reportService->storeReport($validated, Auth::user(), false); return redirect()->route('start'); } - - public function getPatients(Request $request) - { - $user = Auth::user(); - - $validated = $request->validate([ - 'status' => 'required|string', - 'startAt' => 'nullable', - 'endAt' => 'nullable', - ]); - - $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); - - $patients = $this->reportService->getPatientsByStatus( - Auth::user(), - $validated['status'], - $dateRange, - ); - - return response()->json(FormattedPatientResource::collection($patients)); - } - - public function getPatientsCount(Request $request) - { - $user = Auth::user(); - - $validated = $request->validate([ - 'status' => 'required|string', - 'startAt' => 'nullable', - 'endAt' => 'nullable', - ]); - - $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); - - $count = $this->reportService->getPatientsCountByStatus( - Auth::user(), - $validated['status'], - $dateRange, - ); - - return response()->json($count); - } - - public function removeObservation(Request $request) - { - $validated = $request->validate(['id' => 'required|integer']); - - $this->reportService->removeObservationPatient($validated['id']); - - return response()->json(['message' => 'Удалено'], 200); - } - - public function removeUnwantedEvent(UnwantedEvent $unwantedEvent) - { - $unwantedEvent->delete(); - return response()->json(['message' => 'Удалено'], 200); - } - - public function getDepartmentUsers() - { - $users = MisLpuDoctor::select(['LPUDoctorID', 'FAM_V', 'IM_V', 'OT_V']) - ->active() - ->inMyDepartment() - ->get(); - - return response()->json($users, 200); - } - - /** - * Рассчитать загруженность коек - */ - private function calculateBedOccupancy(Department $department, $user): int - { - $beds = (int)$department->metrikaDefault()->where('rf_metrika_item_id', 1)->first()->value; - $occupiedBeds = optional(Report::where('rf_department_id', $department->department_id) - ->join('metrika_results', 'reports.report_id', '=', 'metrika_results.rf_report_id') - ->where('metrika_results.rf_metrika_item_id', 8) - ->orderBy('sent_at', 'desc') - ->first())->value ?? 0; - - return $beds > 0 ? round(intval($occupiedBeds) * 100 / $beds) : 0; - } } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 131bef0..f65c0a9 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -44,6 +44,9 @@ class HandleInertiaRequests extends Middleware 'version' => config('app.version'), 'tag' => config('app.tag') ], + 'config' => [ + 'timeEventSourceUrl' => config('time.eventSourceUrl') + ], 'user' => $user ? [ 'name' => $user->name, 'token' => Session::get('token'), diff --git a/app/Http/Resources/Api/DepartmentPatientOperationResource.php b/app/Http/Resources/Api/DepartmentPatientOperationResource.php new file mode 100644 index 0000000..05f4b3d --- /dev/null +++ b/app/Http/Resources/Api/DepartmentPatientOperationResource.php @@ -0,0 +1,36 @@ +resource; + + $serviceCode = $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code; + $serviceName = $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name; + + return [ + 'id' => $operation->department_patient_operation_id, + 'urgency' => $operation->urgency, + 'service' => [ + 'id' => $operation->rf_kl_service_medical_id, + 'code' => $serviceCode, + 'name' => $serviceName, + 'label' => trim(($serviceCode ? "{$serviceCode} " : '') . ($serviceName ?? '')), + ], + 'startAt' => $operation->started_at?->toIso8601String(), + 'endAt' => $operation->ended_at?->toIso8601String(), + 'duration' => $operation->started_at && $operation->ended_at + ? Carbon::parse($operation->started_at)->diffInMinutes(Carbon::parse($operation->ended_at)) + : null, + ]; + } +} diff --git a/app/Http/Resources/Mis/FormattedPatientResource.php b/app/Http/Resources/Mis/FormattedPatientResource.php index c83f070..b7fccaf 100644 --- a/app/Http/Resources/Mis/FormattedPatientResource.php +++ b/app/Http/Resources/Mis/FormattedPatientResource.php @@ -2,7 +2,7 @@ namespace App\Http\Resources\Mis; -use App\Models\MisSurgicalOperation; +use App\Data\UnifiedPatientData; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Carbon; @@ -17,9 +17,33 @@ class FormattedPatientResource extends JsonResource */ public function toArray(Request $request): array { + if ($this->resource instanceof UnifiedPatientData) { + $age = $this->formatAge($this->age); + + return [ + 'id' => $this->id, + 'patient_uid' => $this->patientUid, + 'source_type' => $this->sourceType, + 'department_patient_id' => $this->departmentPatientId, + 'medical_history_id' => $this->medicalHistoryId, + 'mkb' => $this->mkb, + 'operations' => $this->operations, + 'fullname' => $this->fullname, + 'age' => $age, + 'birth_date' => $this->birthDate ? Carbon::parse($this->birthDate)->format('d.m.Y') : null, + 'patient_kind' => $this->patientKind, + 'admitted_at' => $this->admittedAt ? Carbon::parse($this->admittedAt)->format('d.m.Y H:i') : null, + 'outcome_type' => $this->outcomeType, + 'outcome_date' => $this->outcomeDate, + 'comment' => $this->comment, + 'is_recipient_today' => $this->isRecipientToday, + 'is_manual' => $this->isManual, + 'can_manage_manual' => $this->canManageManual, + ]; + } + return [ 'id' => $this->MedicalHistoryID, - 'num' => $this->num, 'mkb' => [ 'ds' => $this->outcomeMigration->first()->mainDiagnosis?->mkb?->DS, 'name' => $this->outcomeMigration->first()->mainDiagnosis?->mkb?->NAME @@ -31,10 +55,41 @@ class FormattedPatientResource extends JsonResource ]; }), 'fullname' => Str::ucwords(Str::lower("$this->FAMILY $this->Name $this->OT")), - 'age' => Carbon::parse($this->BD)->diff(Carbon::now())->format('%y'), + 'age' => $this->formatAge(Carbon::parse($this->BD)->diffInYears(Carbon::now(), false)), 'birth_date' => Carbon::parse($this->BD)->format('d.m.Y'), + 'admitted_at' => $this->DateRecipient ? Carbon::parse($this->DateRecipient)->format('d.m.Y H:i') : null, 'outcome_type' => $this->when($this->outcome_type, $this->outcome_type), - 'comment' => $this->when($this->comment, $this->comment) + 'outcome_date' => $this->when($this->outcome_date, $this->outcome_date), + 'comment' => $this->when($this->comment, $this->comment), + 'is_recipient_today' => (bool) ($this->is_recipient_today ?? false), + 'is_manual' => false, + 'can_manage_manual' => false, ]; } + + private function formatAge($value): ?string + { + if ($value === null || $value === '') { + return null; + } + + $age = abs((int) $value); + $suffix = $this->ageSuffix($age); + + return "{$age} {$suffix}"; + } + + private function ageSuffix(int $age): string + { + $mod100 = $age % 100; + if ($mod100 >= 11 && $mod100 <= 14) { + return 'лет'; + } + + return match ($age % 10) { + 1 => 'год', + 2, 3, 4 => 'года', + default => 'лет', + }; + } } diff --git a/app/Models/DepartmentPatient.php b/app/Models/DepartmentPatient.php new file mode 100644 index 0000000..31e8f1e --- /dev/null +++ b/app/Models/DepartmentPatient.php @@ -0,0 +1,55 @@ + 'date', + 'admitted_at' => 'datetime', + 'is_current' => 'boolean', + 'outcome_at' => 'datetime', + 'linked_to_mis_at' => 'datetime', + ]; + + public function scopeCurrent($query) + { + return $query->where('is_current', true); + } + + public function scopeManual($query) + { + return $query->where('source_type', 'manual'); + } + + public function observationPatients() + { + return $this->hasMany(ObservationPatient::class, 'rf_department_patient_id', 'department_patient_id'); + } + + public function operations() + { + return $this->hasMany(DepartmentPatientOperation::class, 'rf_department_patient_id', 'department_patient_id'); + } +} diff --git a/app/Models/DepartmentPatientOperation.php b/app/Models/DepartmentPatientOperation.php new file mode 100644 index 0000000..d5fdf2f --- /dev/null +++ b/app/Models/DepartmentPatientOperation.php @@ -0,0 +1,36 @@ + 'datetime', + 'ended_at' => 'datetime', + ]; + + public function patient() + { + return $this->belongsTo(DepartmentPatient::class, 'rf_department_patient_id', 'department_patient_id'); + } + + public function serviceMedical() + { + return $this->belongsTo(MisServiceMedical::class, 'rf_kl_service_medical_id', 'ServiceMedicalID'); + } +} diff --git a/app/Models/LifeMisMigrationPatient.php b/app/Models/LifeMisMigrationPatient.php index aac655a..5f9360f 100644 --- a/app/Models/LifeMisMigrationPatient.php +++ b/app/Models/LifeMisMigrationPatient.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Services\DateRange; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Carbon; class LifeMisMigrationPatient extends Model { @@ -72,18 +73,19 @@ class LifeMisMigrationPatient extends Model */ public function scopeOutcomePatients($query, $branchId = null, DateRange $dateRange = null) { - $query->where('rf_kl_VisitResultID', '<>', 0) // не активное лечение - ->whereDate('DateOut', '<>', '1900-01-01') // есть дата выбытия - ->where('rf_MedicalHistoryID', '<>', 0); + $query->where('rf_MedicalHistoryID', '<>', 0); if ($branchId) { $query->where('rf_StationarBranchID', $branchId); } if ($dateRange) { -// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]); - $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); } return $query; @@ -94,11 +96,10 @@ class LifeMisMigrationPatient extends Model */ public function scopeOutcomeDischarged($query, $branchId = null, DateRange $dateRange = null) { - // ID выписки - $dischargeCodes = [1, 7, 8, 9, 10, 11, 48, 49, 124]; + // По уточненному SQL: Выписано за период + $dischargeCodes = [1, 11, 2, 12, 7, 18, 48]; $query->whereIn('rf_kl_VisitResultID', $dischargeCodes) - ->whereDate('DateOut', '<>', '1900-01-01') ->where('rf_MedicalHistoryID', '<>', 0); if ($branchId) { @@ -106,9 +107,12 @@ class LifeMisMigrationPatient extends Model } if ($dateRange) { -// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]); - $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); } return $query; @@ -119,11 +123,10 @@ class LifeMisMigrationPatient extends Model */ public function scopeOutcomeTransferred($query, $branchId = null, DateRange $dateRange = null) { - // ID перевода - $transferCodes = [2, 3, 4, 12, 13, 14]; + // По заданному SQL: только эти коды перевода + $transferCodes = [4, 14]; $query->whereIn('rf_kl_VisitResultID', $transferCodes) - ->whereDate('DateOut', '<>', '1900-01-01') ->where('rf_MedicalHistoryID', '<>', 0); if ($branchId) { @@ -131,9 +134,12 @@ class LifeMisMigrationPatient extends Model } if ($dateRange) { -// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]); - $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); } return $query; @@ -148,7 +154,6 @@ class LifeMisMigrationPatient extends Model $deceasedCodes = [5, 6, 15, 16]; $query->whereIn('rf_kl_VisitResultID', $deceasedCodes) - ->whereDate('DateOut', '<>', '1900-01-01') ->where('rf_MedicalHistoryID', '<>', 0); if ($branchId) { @@ -156,9 +161,12 @@ class LifeMisMigrationPatient extends Model } if ($dateRange) { -// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]); - $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); } return $query; @@ -166,11 +174,8 @@ class LifeMisMigrationPatient extends Model public function scopeOutcomeWithoutTransferred($query, $branchId = null, DateRange $dateRange = null) { - // ID выписанных, без переводных - $outcomeWithoutTransferredIds = [5, 6, 15, 16, 1, 7, 8, 9, 10, 11, 48, 49, 124]; - - $query->whereIn('rf_kl_VisitResultID', $outcomeWithoutTransferredIds) - ->whereDate('DateOut', '<>', '1900-01-01') + $query->whereNotIn('rf_kl_VisitResultID', [4, 14]) + ->where('rf_kl_VisitResultID', '<>', 0) ->where('rf_MedicalHistoryID', '<>', 0); if ($branchId) { @@ -178,9 +183,12 @@ class LifeMisMigrationPatient extends Model } if ($dateRange) { -// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]); - $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); } return $query; @@ -194,9 +202,12 @@ class LifeMisMigrationPatient extends Model $query->where('rf_kl_VisitResultID', '<>', 0) ->where('rf_MedicalHistoryID', '<>', 0) ->when($dateRange, function($query) use ($dateRange) { -// return $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]); - return $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + return $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); }); if ($branchId) { diff --git a/app/Models/MedicalHistorySnapshot.php b/app/Models/MedicalHistorySnapshot.php index c433ef7..03c7667 100644 --- a/app/Models/MedicalHistorySnapshot.php +++ b/app/Models/MedicalHistorySnapshot.php @@ -11,7 +11,26 @@ class MedicalHistorySnapshot extends Model protected $fillable = [ 'rf_report_id', 'rf_medicalhistory_id', + 'rf_department_patient_id', 'patient_type', + 'patient_uid', + 'patient_source_type', + 'patient_kind', + 'full_name', + 'birth_date', + 'diagnosis_code', + 'diagnosis_name', + 'admitted_at', + 'outcome_type', + 'outcome_at', + 'is_manual', + ]; + + protected $casts = [ + 'birth_date' => 'date', + 'admitted_at' => 'datetime', + 'outcome_at' => 'datetime', + 'is_manual' => 'boolean', ]; /** @@ -30,6 +49,11 @@ class MedicalHistorySnapshot extends Model return $this->belongsTo(MisMedicalHistory::class, 'rf_medicalhistory_id', 'MedicalHistoryID'); } + public function departmentPatient() + { + return $this->belongsTo(DepartmentPatient::class, 'rf_department_patient_id', 'department_patient_id'); + } + // Скоупы для фильтрации public function scopeForReport($query, $reportId) { diff --git a/app/Models/MisMedicalHistory.php b/app/Models/MisMedicalHistory.php index 5440169..f04c0f8 100644 --- a/app/Models/MisMedicalHistory.php +++ b/app/Models/MisMedicalHistory.php @@ -87,13 +87,15 @@ class MisMedicalHistory extends Model public function surgicalOperations() { - return $this->hasMany(MisSurgicalOperation::class, 'rf_MedicalHistoryID', 'MedicalHistoryID'); + return $this->hasMany(MisSurgicalOperation::class, 'rf_MedicalHistoryID', 'MedicalHistoryID') + ->completed(); } public function surgicalOperationsInBranch($branchId) { $operations = MisSurgicalOperation::where('rf_MedicalHistoryID', $this->MedicalHistoryID) ->where('rf_StationarBranchID', $branchId) + ->completed() ->get(); return $operations; @@ -109,7 +111,10 @@ class MisMedicalHistory extends Model public function scopeCurrentlyHospitalized($query) { - return $query->whereDate('DateExtract', '1900-01-01') + return $query->where(function ($builder) { + $builder->whereDate('DateExtract', '1900-01-01') + ->orWhereDate('DateExtract', '2222-01-01'); + }) ->where('MedicalHistoryID', '<>', 0); } @@ -126,7 +131,7 @@ class MisMedicalHistory extends Model */ public function scopeEmergency($query) { - return $query->where('rf_EmerSignID', 2); + return $query->whereIn('rf_EmerSignID', [2, 4]); } /* @@ -152,6 +157,12 @@ class MisMedicalHistory extends Model ->orderBy('DateOut', 'desc'); } + public function latestMigration() + { + return $this->hasOne(MisMigrationPatient::class, 'rf_MedicalHistoryID', 'MedicalHistoryID') + ->ofMany('DateOut', 'max'); + } + /* * Движение по StationarBranch */ @@ -181,4 +192,9 @@ class MisMedicalHistory extends Model } }); } + + public function operationPurpose() + { + return $this->belongsTo(MisOperationPurpose::class, 'MedicalHistoryID', 'rf_MedicalHistoryID'); + } } diff --git a/app/Models/MisMigrationPatient.php b/app/Models/MisMigrationPatient.php index b649028..bd219f8 100644 --- a/app/Models/MisMigrationPatient.php +++ b/app/Models/MisMigrationPatient.php @@ -11,6 +11,11 @@ class MisMigrationPatient extends Model protected $table = 'stt_migrationpatient'; protected $primaryKey = 'MigrationPatientID'; + protected $casts = [ + 'DateIngoing' => 'datetime:Y-m-d H:i:s', + 'DateOut' => 'datetime:Y-m-d H:i:s', + ]; + public function branch() { return $this->hasOne(MisStationarBranch::class, 'StationarBranchID', 'rf_StationarBranchID'); @@ -31,18 +36,12 @@ class MisMigrationPatient extends Model return $this->hasOne(MisMKB::class, 'MKBID', 'rf_MKBID'); } - public function medicalHistory() - { - return $this->belongsTo(MisMedicalHistory::class, 'rf_MedicalHistoryID', 'MedicalHistoryID'); - } - /** * Находятся на лечении */ public function scopeCurrentlyInTreatment($query, $branchId = null, DateRange $dateRange = null) { - $query->where('rf_kl_VisitResultID', 0) - ->where('rf_kl_StatCureResultID', 0) + $query->whereNotIn('rf_kl_VisitResultID', [4]) ->whereHas('medicalHistory', function ($query) use ($branchId, $dateRange) { $query->whereDate('DateExtract', '1900-01-01'); }) @@ -77,18 +76,19 @@ class MisMigrationPatient extends Model */ public function scopeOutcomePatients($query, $branchId = null, DateRange $dateRange = null) { - $query->where('rf_kl_VisitResultID', '<>', 0) // не активное лечение - ->whereDate('DateOut', '<>', '1900-01-01') // есть дата выбытия - ->where('rf_MedicalHistoryID', '<>', 0); + $query->where('rf_MedicalHistoryID', '<>', 0); if ($branchId) { $query->where('rf_StationarBranchID', $branchId); } if ($dateRange) { - $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); -// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); } return $query; @@ -99,11 +99,10 @@ class MisMigrationPatient extends Model */ public function scopeOutcomeDischarged($query, $branchId = null, DateRange $dateRange = null) { - // ID выписки - $dischargeCodes = [1, 7, 8, 9, 10, 11, 48, 49, 124]; + // По уточненному SQL: Выписано за период + $dischargeCodes = [1, 11, 2, 12, 7, 18, 48]; $query->whereIn('rf_kl_VisitResultID', $dischargeCodes) - ->whereDate('DateOut', '<>', '1900-01-01') ->where('rf_MedicalHistoryID', '<>', 0); if ($branchId) { @@ -111,8 +110,12 @@ class MisMigrationPatient extends Model } if ($dateRange) { - $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); } return $query; @@ -123,11 +126,10 @@ class MisMigrationPatient extends Model */ public function scopeOutcomeTransferred($query, $branchId = null, DateRange $dateRange = null) { - // ID перевода - $transferCodes = [2, 3, 4, 12, 13, 14]; + // По заданному SQL: только эти коды перевода + $transferCodes = [4, 14]; $query->whereIn('rf_kl_VisitResultID', $transferCodes) - ->whereDate('DateOut', '<>', '1900-01-01') ->where('rf_MedicalHistoryID', '<>', 0); if ($branchId) { @@ -135,8 +137,12 @@ class MisMigrationPatient extends Model } if ($dateRange) { - $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); } return $query; @@ -151,7 +157,6 @@ class MisMigrationPatient extends Model $deceasedCodes = [5, 6, 15, 16]; $query->whereIn('rf_kl_VisitResultID', $deceasedCodes) - ->whereDate('DateOut', '<>', '1900-01-01') ->where('rf_MedicalHistoryID', '<>', 0); if ($branchId) { @@ -159,8 +164,12 @@ class MisMigrationPatient extends Model } if ($dateRange) { - $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); } return $query; @@ -168,11 +177,9 @@ class MisMigrationPatient extends Model public function scopeOutcomeWithoutTransferred($query, $branchId = null, DateRange $dateRange = null) { - // ID выписанных, без переводных - $outcomeWithoutTransferredIds = [5, 6, 15, 16, 1, 7, 8, 9, 10, 11, 48, 49, 124]; - - $query->whereIn('rf_kl_VisitResultID', $outcomeWithoutTransferredIds) - ->whereDate('DateOut', '<>', '1900-01-01') + // По заданной логике переводы только 4 и 14, исключаем их + $query->whereNotIn('rf_kl_VisitResultID', [4, 14]) + ->where('rf_kl_VisitResultID', '<>', 0) ->where('rf_MedicalHistoryID', '<>', 0); if ($branchId) { @@ -180,8 +187,12 @@ class MisMigrationPatient extends Model } if ($dateRange) { - $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); } return $query; @@ -195,8 +206,12 @@ class MisMigrationPatient extends Model $query->where('rf_kl_VisitResultID', '<>', 0) ->where('rf_MedicalHistoryID', '<>', 0) ->when($dateRange, function($query) use ($dateRange) { - return $query->where('DateOut', '>=', $dateRange->startSql()) - ->where('DateOut', '<=', $dateRange->endSql()); + $startDate = Carbon::parse($dateRange->startSql())->toDateString(); + $endDate = Carbon::parse($dateRange->endSql())->toDateString(); + return $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) { + $mhQuery->whereDate('DateExtract', '>', $startDate) + ->whereDate('DateExtract', '<=', $endDate); + }); }); if ($branchId) { @@ -205,4 +220,9 @@ class MisMigrationPatient extends Model return $query; } + + public function medicalHistory() + { + return $this->belongsTo(MisMedicalHistory::class, 'rf_MedicalHistoryID', 'MedicalHistoryID'); + } } diff --git a/app/Models/MisOperationPurpose.php b/app/Models/MisOperationPurpose.php new file mode 100644 index 0000000..f176941 --- /dev/null +++ b/app/Models/MisOperationPurpose.php @@ -0,0 +1,17 @@ +belongsTo(MisSurgicalOperation::class, 'rf_SurgicalOperationID', 'SurgicalOperationID'); + } +} \ No newline at end of file diff --git a/app/Models/MisReanimation.php b/app/Models/MisReanimation.php new file mode 100644 index 0000000..b7b8a90 --- /dev/null +++ b/app/Models/MisReanimation.php @@ -0,0 +1,17 @@ +belongsTo(MisMigrationPatient::class, 'rf_MigrationPatientID', 'MigrationPatientID'); + } +} diff --git a/app/Models/MisSurgicalOperation.php b/app/Models/MisSurgicalOperation.php index ef80dac..f0ad4de 100644 --- a/app/Models/MisSurgicalOperation.php +++ b/app/Models/MisSurgicalOperation.php @@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model; class MisSurgicalOperation extends Model { + private const COMPLETED_OPERATION_STATUS_ID = 3; + protected $table = 'stt_surgicaloperation'; protected $primaryKey = 'SurgicalOperationID'; @@ -18,4 +20,16 @@ class MisSurgicalOperation extends Model { return $this->belongsTo(MisMedicalHistory::class, 'rf_MedicalHistoryID', 'MedicalHistoryID'); } + + public function operationPurpose() + { + return $this->hasOne(MisOperationPurpose::class, 'rf_SurgicalOperationID', 'SurgicalOperationID'); + } + + public function scopeCompleted($query) + { + return $query->whereHas('operationPurpose', function ($purposeQuery) { + $purposeQuery->where('rf_OperationStatusID', self::COMPLETED_OPERATION_STATUS_ID); + }); + } } diff --git a/app/Models/ObservationPatient.php b/app/Models/ObservationPatient.php index 3e358ae..b0014c6 100644 --- a/app/Models/ObservationPatient.php +++ b/app/Models/ObservationPatient.php @@ -11,6 +11,7 @@ class ObservationPatient extends Model protected $fillable = [ 'rf_medicalhistory_id', + 'rf_department_patient_id', 'rf_mkab_id', 'rf_department_id', 'rf_report_id', @@ -21,4 +22,9 @@ class ObservationPatient extends Model { return $this->belongsTo(MisMedicalHistory::class, 'rf_medicalhistory_id', 'MedicalHistoryID'); } + + public function departmentPatient() + { + return $this->belongsTo(DepartmentPatient::class, 'rf_department_patient_id', 'department_patient_id'); + } } diff --git a/app/Models/Report.php b/app/Models/Report.php index 6e229b3..7b7fb91 100644 --- a/app/Models/Report.php +++ b/app/Models/Report.php @@ -20,7 +20,13 @@ class Report extends Model 'sent_at', 'rf_department_id', 'rf_user_id', - 'rf_lpudoctor_id' + 'rf_lpudoctor_id', + 'period_type', + 'period_start', + 'period_end', + 'report_month', + 'report_year', + 'status', ]; public function metrikaResults(): \Illuminate\Database\Eloquent\Relations\HasMany diff --git a/app/Services/AutoReportService.php b/app/Services/AutoReportService.php index 69d1883..ed229f5 100644 --- a/app/Services/AutoReportService.php +++ b/app/Services/AutoReportService.php @@ -3,14 +3,13 @@ namespace App\Services; use App\Models\Department; +use App\Models\DepartmentPatient; use App\Models\MedicalHistorySnapshot; use App\Models\MetrikaResult; -use App\Models\MisStationarBranch; use App\Models\ObservationPatient; use App\Models\Report; use App\Models\UnwantedEvent; use App\Models\User; -use Carbon\Carbon; use Carbon\CarbonPeriod; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -19,8 +18,7 @@ class AutoReportService { public function __construct( protected ReportService $reportService, - protected DateRangeService $dateRangeService, - protected PatientService $patientQueryService + protected DateRangeService $dateRangeService ) {} /** @@ -30,18 +28,26 @@ class AutoReportService User $user, string $startDate, string $endDate, - $departmentId, + Department $department, bool $force = false ): 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($startDate, $endDate); + $period = CarbonPeriod::create($start->toDateString(), $periodEnd->toDateString()); foreach ($period as $date) { $dateRange = $this->dateRangeService->getNormalizedDateRange($user, $date, $date); try { - $reportCreated = $this->createReportForDate($user, $dateRange, $departmentId, $force); + $reportCreated = $this->createReportForDate($user, $department, $dateRange, $force); if ($reportCreated) { $createdCount++; @@ -58,12 +64,12 @@ class AutoReportService /** * Создать отчет для конкретной даты */ - public function createReportForDate(User $user, DateRange $dateRange, $departmentId, bool $force = false): bool + public function createReportForDate(User $user, Department $department, DateRange $dateRange, bool $force = false): bool { - $user->rf_department_id = $departmentId; + $scopedUser = $this->scopeUserToDepartment($user, $department); + // Проверяем, существует ли уже отчет на эту дату - $existingReport = Report::where('rf_department_id', $departmentId) - ->whereDate('created_at', $dateRange->endSql()) + $existingReport = Report::where('rf_department_id', $department->department_id) ->whereDate('sent_at', $dateRange->endSql()) ->first(); @@ -73,19 +79,15 @@ class AutoReportService // Если есть существующий отчет и force=true - удаляем его if ($existingReport && $force) { - MetrikaResult::where('rf_report_id', $existingReport->report_id)->delete(); - MedicalHistorySnapshot::where('rf_report_id', $existingReport->report_id)->delete(); - UnwantedEvent::where('rf_report_id', $existingReport->report_id)->delete(); - ObservationPatient::where('rf_report_id', $existingReport->report_id)->delete(); - $existingReport->delete(); + $this->deleteExistingReport($existingReport); } // Получаем данные для отчета - $reportData = $this->prepareReportData($user, $dateRange, $departmentId); + $reportData = $this->prepareReportData($scopedUser, $department, $dateRange); // Создаем отчет - DB::transaction(function () use ($user, $reportData) { - $this->reportService->storeReport($reportData, $user); + DB::transaction(function () use ($scopedUser, $reportData) { + $this->reportService->storeReport($reportData, $scopedUser, true); }); return true; @@ -94,180 +96,29 @@ class AutoReportService /** * Подготовить данные для отчета */ - private function prepareReportData(User $user, DateRange $dateRange, $departmentId): array + private function prepareReportData(User $user, Department $department, DateRange $dateRange): array { - $department = Department::where('department_id', $departmentId)->first(); - $branchId = $this->getBranchId($department->rf_mis_department_id); - $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); - - // Получаем метрики - $metrics = $this->calculateMetrics( - $user, - $isHeadOrAdmin, - $branchId, - $dateRange - ); - - // Получаем количество коек - $beds = $this->getBedCount($department); - - // Формируем данные отчета - return [ - 'departmentId' => $department->department_id, - 'userId' => $user->rf_lpudoctor_id ?? $user->id, - 'dates' => [ - $dateRange->startTimestamp(), - $dateRange->endTimestamp() - ], - 'sent_at' => $dateRange->endSql(), - 'created_at' => $dateRange->endSql(), - 'metrics' => $this->formatMetrics($metrics), - 'observationPatients' => $this->getObservationPatients($departmentId, $dateRange), - 'unwantedEvents' => [], - ]; + return $this->reportService->buildAutoFillReportPayload($user, $department, $dateRange); } - /** - * Рассчитать метрики для отчета - */ - private function calculateMetrics( - User $user, - bool $isHeadOrAdmin, - int $branchId, - DateRange $dateRange - ): array { - $metrics = []; - - // 1. Плановые пациенты - $metrics['plan'] = $this->patientQueryService->getPlanOrEmergencyPatients( - 'plan', - $isHeadOrAdmin, - $branchId, - $dateRange, - true, - false, - true, - true - ); - - // 2. Экстренные пациенты - $metrics['emergency'] = $this->patientQueryService->getPlanOrEmergencyPatients( - 'emergency', - $isHeadOrAdmin, - $branchId, - $dateRange, - true, - false, - true, - true - ); - - // 3. Поступившие сегодня - $metrics['recipient'] = $this->patientQueryService->getPlanOrEmergencyPatients( - null, - $isHeadOrAdmin, - $branchId, - $dateRange, - true, - false, - false, - true - ); - - // 4. Выписанные - $metrics['discharged'] = $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'discharged' - )->count(); - - // 5. Переведенные - $metrics['transferred'] = $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'transferred' - )->count(); - - // 6. Умершие - $metrics['deceased'] = $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'deceased' - )->count(); - - // 7. Текущие пациенты - $metrics['current'] = $this->patientQueryService->getAllPatientsInDepartment( - $isHeadOrAdmin, - $branchId, - $dateRange, - true - ); - - // 8. Плановые операции - $metrics['plan_surgery'] = $this->patientQueryService->getSurgicalPatients( - 'plan', - $branchId, - $dateRange, - true - ); - - // 9. Экстренные операции - $metrics['emergency_surgery'] = $this->patientQueryService->getSurgicalPatients( - 'emergency', - $branchId, - $dateRange, - true - ); - - return $metrics; - } - - /** - * Форматировать метрики для сохранения - */ - private function formatMetrics(array $metrics): array + private function scopeUserToDepartment(User $user, Department $department): User { - return [ - 'metrika_item_4' => $metrics['plan'] ?? 0, // плановые - 'metrika_item_12' => $metrics['emergency'] ?? 0, // экстренные - 'metrika_item_3' => $metrics['recipient'] ?? 0, // поступившие - // 'metrika_item_6' => ($metrics['plan_surgery'] ?? 0) + ($metrics['emergency_surgery'] ?? 0), // всего операций - 'metrika_item_7' => $metrics['discharged'] + $metrics['deceased'], // выписанные - 'metrika_item_8' => $metrics['current'] ?? 0, // текущие - 'metrika_item_9' => $metrics['deceased'] ?? 0, // умершие - 'metrika_item_11' => $metrics['plan_surgery'] ?? 0, // плановые операции - 'metrika_item_10' => $metrics['emergency_surgery'] ?? 0, // экстренные операции - 'metrika_item_13' => $metrics['transferred'] ?? 0, // переведенные - 'metrika_item_14' => 0, // под наблюдением (будет заполнено отдельно) - 'metrika_item_15' => $metrics['discharged'] ?? 0, // выбыло - ]; + $scopedUser = clone $user; + $scopedUser->rf_department_id = $department->department_id; + $scopedUser->setRelation('department', $department); + + return $scopedUser; } - /** - * Получить пациентов под наблюдением на дату - */ - private function getObservationPatients(int $departmentId, DateRange $dateRange): array + private function deleteExistingReport(Report $report): void { - // Здесь нужно реализовать логику получения пациентов под наблюдением - // на конкретную дату. Возможно, из снапшотов или истории. - return []; // временно возвращаем пустой массив + DB::transaction(function () use ($report) { + MetrikaResult::where('rf_report_id', $report->report_id)->delete(); + MedicalHistorySnapshot::where('rf_report_id', $report->report_id)->delete(); + UnwantedEvent::where('rf_report_id', $report->report_id)->delete(); + ObservationPatient::where('rf_report_id', $report->report_id)->delete(); + DB::table('reports')->where('report_id', $report->report_id)->delete(); + }); } - /** - * Получить ID отделения - */ - private function getBranchId(int $misDepartmentId): ?int - { - return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId) - ->value('StationarBranchID'); - } - - /** - * Получить количество коек - */ - private function getBedCount(Department $department): int - { - $default = $department->metrikaDefault()->where('rf_metrika_item_id', 1)->first(); - return (int)($default->value ?? 0); - } } diff --git a/app/Services/DateRange.php b/app/Services/DateRange.php index a493af9..bcd3f76 100644 --- a/app/Services/DateRange.php +++ b/app/Services/DateRange.php @@ -65,12 +65,12 @@ readonly class DateRange public function startFirstOfMonth() { - return $this->startDate->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s'); + return $this->startDate->copy()->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s'); } public function endFirstOfMonth() { - return $this->endDate->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s'); + return $this->endDate->copy()->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s'); } /** diff --git a/app/Services/DateRangeService.php b/app/Services/DateRangeService.php index 80605f2..3263803 100644 --- a/app/Services/DateRangeService.php +++ b/app/Services/DateRangeService.php @@ -56,10 +56,10 @@ class DateRangeService // По умолчанию: с начала года до сегодня $startDate = Carbon::now('Asia/Yakutsk') ->startOfYear() // 1 января текущего года - ->setTime(6, 0); + ->setTime(7, 0); $endDate = Carbon::now('Asia/Yakutsk') - ->setTime(6, 0); + ->setTime(7, 0); return [ $startDate->format('Y-m-d H:i:s'), @@ -83,7 +83,7 @@ class DateRangeService $startDate = $this->parseDate($startAt); $endDate = $this->parseDate($endAt); - return $startDate->diffInDays($endDate) === 1.0; + return $startDate->isSameDay($endDate) || $startDate->diffInDays($endDate) === 1.0; } private function getCustomDateRange($startAt, $endAt, $user): array @@ -92,11 +92,11 @@ class DateRangeService $endDate = $this->parseDate($endAt); if ($startDate->isSameDay($endDate)) { - $startDate = $startDate->subDay()->setTime(6, 0); - $endDate = $endDate->setTime(6, 0); + $startDate = $startDate->subDay()->setTime(7, 0); + $endDate = $endDate->setTime(7, 0); } else { - $startDate = $startDate->setTime(6, 0); - $endDate = $endDate->setTime(6, 0); + $startDate = $startDate->setTime(7, 0); + $endDate = $endDate->setTime(7, 0); } return [ @@ -109,10 +109,10 @@ class DateRangeService { $startDate = Carbon::now('Asia/Yakutsk') ->subDay() - ->setTime(6, 0); + ->setTime(7, 0); $endDate = Carbon::now('Asia/Yakutsk') - ->setTime(6, 0); + ->setTime(7, 0); return [ $startDate->format('Y-m-d H:i:s'), @@ -123,8 +123,12 @@ class DateRangeService public function parseDate($dateInput): Carbon { if (is_numeric($dateInput)) { - return Carbon::createFromTimestampMs($dateInput) - ->setTimezone('Asia/Yakutsk'); + $timestamp = (string) $dateInput; + $isMilliseconds = strlen(ltrim($timestamp, '-')) > 10; + + return $isMilliseconds + ? Carbon::createFromTimestampMs((int) $dateInput)->setTimezone('Asia/Yakutsk') + : Carbon::createFromTimestamp((int) $dateInput)->setTimezone('Asia/Yakutsk'); } return Carbon::parse($dateInput, 'Asia/Yakutsk'); @@ -139,6 +143,10 @@ class DateRangeService return $date; } + if (is_numeric($date)) { + return $this->parseDate($date); + } + if (is_string($date)) { return Carbon::parse($date, 'Asia/Yakutsk'); } @@ -157,9 +165,9 @@ class DateRangeService public function createDateRangeForDate(Carbon $date, User $user): DateRange { // Для автоматического заполнения используем логику как для врача - // (вчера 06:00 - сегодня 06:00) - $startDate = $date->copy()->subDay()->setTime(6, 0); - $endDate = $date->copy()->setTime(6, 0); + // (вчера 07:00 - сегодня 07:00) + $startDate = $date->copy()->subDay()->setTime(7, 0); + $endDate = $date->copy()->setTime(7, 0); return new DateRange( startDate: $startDate, diff --git a/app/Services/PatientMigrationService.php b/app/Services/PatientMigrationService.php index a0c808f..0c00fed 100644 --- a/app/Services/PatientMigrationService.php +++ b/app/Services/PatientMigrationService.php @@ -2,7 +2,177 @@ namespace App\Services; +use App\Data\UnifiedPatientData; +use App\Models\MisMigrationPatient; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Cache; + class PatientMigrationService { + private const ACTIVE_DATE_OUT = '2222-01-01 00:00:00'; + const DEAD_VISIT_RESULT_IDS = [5, 6, 15, 16]; + const TRANSFER_VISIT_RESULT_IDS = [2, 3, 4, 12, 13, 14, 35]; + const OUTCOME_VISIT_RESULT_IDS = [1, 11]; + public function __construct( + protected StationarBranchService $branchService + ) { } + + + /** + * Получить всех пациентов в отделении за период + */ + public function getMigrationsInBranch(int $branchId, string $startAt, string $endAt): Collection + { + return $this->getMigrationsCached( + branchId: $branchId, + startAt: $startAt, + endAt: $endAt, + cacheTag: 'migrations_in_branch', + dateFilter: fn($q) => $q->where('DateOut', '>', $startAt)->where('DateOut', '<=', $endAt), + ); + } + + /** + * Получить текущих пациентов в отделении (активные) + */ + public function getMigrationsInBranchCurrent(int $branchId, string $startAt, string $endAt): Collection + { + return $this->getMigrationsCached( + branchId: $branchId, + startAt: $startAt, + endAt: $endAt, + cacheTag: 'migrations_in_branch_current', + dateFilter: fn($q) => $q->where('DateOut', self::ACTIVE_DATE_OUT), + ); + } + + /** + * Получить текущих пациентов в отделении (активные) + */ + public function getMigrationsInBranchDead(int $branchId, string $startAt, string $endAt): Collection + { + return $this->getMigrationsCached( + branchId: $branchId, + startAt: $startAt, + endAt: $endAt, + cacheTag: 'migrations_in_branch_dead', + dateFilter: fn($q) => $q->where('DateOut', '>', $startAt)->where('DateOut', '<=', $endAt), + additionalFilters: [ + [ + 'key' => 'visit_result:dead', + 'apply' => fn($q) => $q->whereIn('rf_kl_VisitResultID', self::DEAD_VISIT_RESULT_IDS) + ] + ] + ); + } + + public function getMigrationsInBranchTransfer(int $branchId, string $startAt, string $endAt): Collection + { + return $this->getMigrationsCached( + branchId: $branchId, + startAt: $startAt, + endAt: $endAt, + cacheTag: 'migrations_in_branch_transfer', + dateFilter: fn($q) => $q->where('DateOut', '>', $startAt)->where('DateOut', '<=', $endAt), + additionalFilters: [ + [ + 'key' => 'visit_result:transfer', + 'apply' => fn($q) => $q->whereIn('rf_kl_VisitResultID', self::TRANSFER_VISIT_RESULT_IDS) + ] + ] + ); + } + + public function getMigrationsInBranchOutcome(int $branchId, string $startAt, string $endAt): Collection + { + return $this->getMigrationsCached( + branchId: $branchId, + startAt: $startAt, + endAt: $endAt, + cacheTag: 'migrations_in_branch_outcome', + dateFilter: fn($q) => $q->where('DateOut', '>', $startAt)->where('DateOut', '<=', $endAt), + additionalFilters: [ + [ + 'key' => 'visit_result:outcome', + 'apply' => fn($q) => $q->whereIn('rf_kl_VisitResultID', self::OUTCOME_VISIT_RESULT_IDS) + ] + ] + ); + } + + /** + * Базовый метод с кешированием и общей логикой + */ + protected function getMigrationsCached( + int $branchId, + string $startAt, + string $endAt, + string $cacheTag, + \Closure $dateFilter, + array $additionalFilters = [] + ): Collection { + // Исключения + if (in_array($branchId, [0], true)) { + return collect(); + } + + // Нормализованный ключ кеша + $filterKeys = array_column($additionalFilters, 'key'); + $filterHash = substr(md5(implode('|', $filterKeys)), 0, 6); + $dateHash = substr(md5($startAt . '_' . $endAt), 0, 8); + $cacheKey = "{$cacheTag}_{$branchId}_{$dateHash}_f{$filterHash}"; + + return Cache::tags([$cacheTag, "migrations_branch_{$branchId}"]) + ->remember($cacheKey, now()->addHours(6), function () use ($branchId, $startAt, $endAt, $dateFilter, $additionalFilters) { + return $this->buildBaseQuery($branchId, $startAt, $endAt) + ->tap($dateFilter) + ->tap(function ($q) use ($additionalFilters) { + foreach ($additionalFilters as $filter) { + if (isset($filter['apply']) && $filter['apply'] instanceof \Closure) { + $filter['apply']($q); + } + } + }) + ->get() + ->each(function ($result) { + return UnifiedPatientData::fromMisMigrationPatient($result); + }); + }); + } + + /** + * Строит базовый запрос с общими условиями и связями + */ + protected function buildBaseQuery(int $branchId, string $startAt, string $endAt): Builder + { + return MisMigrationPatient::query() + ->select([ + 'MigrationPatientID', 'DateIngoing', 'DateOut', 'BedDays', 'rf_MedicalHistoryID', + 'rf_StationarBranchID', 'rf_DiagnosID', 'rf_kl_ProfitTypeID', 'rf_kl_StatCureResultID', + 'rf_kl_VisitResultID', 'rf_BedProfileID', 'rf_kl_BedProfileID' + ]) + ->where('rf_StationarBranchID', $branchId) + // Жадная загрузка с ограничениями + ->with([ + 'medicalHistory' => fn($q) => $q->select('MedicalHistoryID', 'FAMILY', 'Name', 'OT', 'BD'), + 'medicalHistory.operationPurpose' => fn($q) => $q + ->where('rf_StationarBranchID', $branchId) + ->select('OperationPurposeID', 'rf_MedicalHistoryID', 'Date', 'rf_OperationStatusID', + 'CancelDate', 'rf_StationarBranchID', 'PhysicalExam', 'Description', 'Indications', 'EpicrisDate', + 'rf_SurgicalOperationID' + ), + 'medicalHistory.operationPurpose.surgicalOperation' => fn($q) => $q + ->select('SurgicalOperationID', 'DataEnd', 'Date', 'Num', 'rf_kl_ServiceMedicalID', + 'rf_MedicalHistoryID', 'rf_StationarBranchID', 'Description', 'rf_OperationResultID' + ), + 'diagnosis' => fn($q) => $q + ->where('rf_DiagnosTypeID', 3) + ->select('DiagnosID', 'Date', 'rf_DiagnosTypeID', 'rf_MedicalHistoryID', 'rf_MKBID', + 'rf_MigrationPatientID', 'Description' + ), + 'diagnosis.mkb' => fn($q) => $q->select(['MKBID', 'DS', 'NAME']), + ]); + } } diff --git a/app/Services/PatientService.php b/app/Services/PatientService.php index 59fe762..617393b 100644 --- a/app/Services/PatientService.php +++ b/app/Services/PatientService.php @@ -2,12 +2,13 @@ namespace App\Services; -use App\Models\LifeMisMigrationPatient; use App\Models\MisMedicalHistory; use App\Models\MisMigrationPatient; +use App\Models\MisReanimation; use App\Models\MisSurgicalOperation; use App\Models\ObservationPatient; use Carbon\Carbon; +use Illuminate\Support\Facades\DB; class PatientService { @@ -34,10 +35,7 @@ class PatientService // Если нужно добавить уже находящихся в отделении if ($includeCurrent) { if ($fillableAuto) { - $currentIds = LifeMisMigrationPatient::currentlyInTreatment($branchId, $dateRange) - ->distinct() - ->pluck('rf_MedicalHistoryID') - ->toArray(); + $currentIds = $this->getHistoricalCurrentMedicalHistoryIds($branchId, $dateRange); } else { $currentIds = MisMigrationPatient::currentlyInTreatment($branchId) ->pluck('rf_MedicalHistoryID') @@ -57,19 +55,75 @@ class PatientService // Получаем истории $query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds) + ->select([ + 'MedicalHistoryID', + 'FAMILY', + 'Name', + 'OT', + 'BD', + 'DateRecipient', + 'DateExtract', + 'rf_EmerSignID', + 'rf_kl_VisitResultID', + ]) ->with([ - 'surgicalOperations' => function ($q) use ($dateRange) { -// $q->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]); - $q->where('Date', '>=', $dateRange->startSql()) - ->where('Date', '<=', $dateRange->endSql()); + 'surgicalOperations' => function ($q) { + $q->select([ + 'SurgicalOperationID', + 'rf_MedicalHistoryID', + 'rf_kl_ServiceMedicalID', + 'Date', + ])->with(['serviceMedical' => function ($serviceQuery) { + $serviceQuery->select([ + 'ServiceMedicalID', + 'ServiceMedicalCode', + 'ServiceMedicalName', + ]); + }]); + }, + 'outcomeMigration' => function ($q) { + $q->select([ + 'MigrationPatientID', + 'rf_MedicalHistoryID', + 'DateOut', + 'rf_DiagnosID', + ])->with(['mainDiagnosis' => function ($diagnosisQuery) { + $diagnosisQuery->select([ + 'DiagnosID', + 'rf_MKBID', + ])->with(['mkb' => function ($mkbQuery) { + $mkbQuery->select([ + 'MKBID', + 'DS', + 'NAME', + ]); + }]); + }]); }, 'migrations' => function ($q) use ($branchId) { $q->where('rf_StationarBranchID', $branchId) - ->take(1) // берем только одну последнюю - ->with(['mainDiagnosis' => function ($q) { - $q->with('mkb'); - }]); - } + ->select([ + 'MigrationPatientID', + 'rf_MedicalHistoryID', + 'rf_DiagnosID', + 'DateIngoing', + 'rf_StationarBranchID', + ]) + ->orderByDesc('DateIngoing') + ->with(['mainDiagnosis' => function ($diagnosisQuery) { + $diagnosisQuery->select([ + 'DiagnosID', + 'rf_MKBID', + 'rf_MigrationPatientID', + ])->with(['mkb' => function ($mkbQuery) { + $mkbQuery->select([ + 'MKBID', + 'DS', + 'NAME', + ]); + }]); + }]); + }, ]) ->orderBy('DateRecipient', 'DESC'); @@ -88,7 +142,7 @@ class PatientService return $query->pluck('MedicalHistoryID'); } - return $query->get()->map(function ($patient) use ($recipientIds, $branchId) { + return $query->get()->map(function ($patient) use ($recipientIds) { // Добавляем флаг "поступил сегодня" $patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds); return $patient; @@ -103,17 +157,22 @@ class PatientService int $branchId, DateRange $dateRange, bool $countOnly = false, - bool $onlyIds = false + bool $onlyIds = false, + bool $fillableAuto = false ) { // Поступившие сегодня - $recipientIds = $this->buildRecipientQuery(null, $isHeadOrAdmin, $branchId, $dateRange) + $recipientIds = $this->buildRecipientQuery(null, $isHeadOrAdmin, $branchId, $dateRange, $fillableAuto) ->pluck('rf_MedicalHistoryID') ->toArray(); // Уже находящиеся на лечении - $currentIds = MisMigrationPatient::currentlyInTreatment($branchId) - ->pluck('rf_MedicalHistoryID') - ->toArray(); + if ($fillableAuto) { + $currentIds = $this->getHistoricalCurrentMedicalHistoryIds($branchId, $dateRange); + } else { + $currentIds = MisMigrationPatient::currentlyInTreatment($branchId) + ->pluck('rf_MedicalHistoryID') + ->toArray(); + } // Объединяем и убираем дубли $allIds = array_unique(array_merge($recipientIds, $currentIds)); @@ -132,11 +191,76 @@ class PatientService } return MisMedicalHistory::whereIn('MedicalHistoryID', $allIds) - ->with(['surgicalOperations' => function ($q) use ($dateRange) { -// $q->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]); - $q->where('Date', '>=', $dateRange->startSql()) - ->where('Date', '<=', $dateRange->endSql()); - }]) + ->select([ + 'MedicalHistoryID', + 'FAMILY', + 'Name', + 'OT', + 'BD', + 'DateRecipient', + 'DateExtract', + 'rf_EmerSignID', + 'rf_kl_VisitResultID', + ]) + ->with([ + 'surgicalOperations' => function ($q) { + $q->select([ + 'SurgicalOperationID', + 'rf_MedicalHistoryID', + 'rf_kl_ServiceMedicalID', + 'Date', + ])->with(['serviceMedical' => function ($serviceQuery) { + $serviceQuery->select([ + 'ServiceMedicalID', + 'ServiceMedicalCode', + 'ServiceMedicalName', + ]); + }]); + }, + 'outcomeMigration' => function ($q) { + $q->select([ + 'MigrationPatientID', + 'rf_MedicalHistoryID', + 'DateOut', + 'rf_DiagnosID', + ])->with(['mainDiagnosis' => function ($diagnosisQuery) { + $diagnosisQuery->select([ + 'DiagnosID', + 'rf_MKBID', + ])->with(['mkb' => function ($mkbQuery) { + $mkbQuery->select([ + 'MKBID', + 'DS', + 'NAME', + ]); + }]); + }]); + }, + 'migrations' => function ($q) use ($branchId) { + $q->where('rf_StationarBranchID', $branchId) + ->select([ + 'MigrationPatientID', + 'rf_MedicalHistoryID', + 'rf_DiagnosID', + 'DateIngoing', + 'rf_StationarBranchID', + ]) + ->orderByDesc('DateIngoing') + ->with(['mainDiagnosis' => function ($diagnosisQuery) { + $diagnosisQuery->select([ + 'DiagnosID', + 'rf_MKBID', + 'rf_MigrationPatientID', + ])->with(['mkb' => function ($mkbQuery) { + $mkbQuery->select([ + 'MKBID', + 'DS', + 'NAME', + ]); + }]); + }]); + }, + ]) ->orderBy('DateRecipient', 'DESC') ->get() ->map(function ($patient) use ($recipientIds) { @@ -167,7 +291,6 @@ class PatientService // Загрузка отношений, необходимых для FormattedPatientResource $query->with([ 'outcomeMigration.mainDiagnosis.mkb', // mkb через диагноз - 'surgicalOperations.serviceMedical', // операции с услугами ]); $patients = $query->get(); } @@ -190,34 +313,156 @@ class PatientService string $outcomeType = 'all', bool $onlyIds = false ) { - $methodMap = [ - 'discharged' => 'outcomeDischarged', - 'transferred' => 'outcomeTransferred', - 'without-transferred' => 'outcomeWithoutTransferred', - 'deceased' => 'deceasedOutcome', - 'all' => 'outcomePatients', - ]; + $query = MisMedicalHistory::query() + ->where('MedicalHistoryID', '<>', 0) + ->whereHas('migrations', function ($migrationQuery) use ($branchId, $outcomeType) { + $migrationQuery->where('rf_StationarBranchID', $branchId); - $method = $methodMap[$outcomeType] ?? 'outcomePatients'; + if ($outcomeType === 'deceased') { + $migrationQuery->whereIn('rf_kl_VisitResultID', [5, 6, 15, 16]); + } elseif ($outcomeType === 'transferred') { + $migrationQuery->whereIn('rf_kl_VisitResultID', [4, 14]); + } elseif ($outcomeType === 'discharged') { + $migrationQuery->whereIn('rf_kl_VisitResultID', [1, 11, 2, 12, 7, 18, 48]); + } elseif ($outcomeType === 'without-transferred') { + $migrationQuery->whereNotIn('rf_kl_VisitResultID', [4, 14]) + ->where('rf_kl_VisitResultID', '<>', 0); + } + }); - $medicalHistoryIds = MisMigrationPatient::{$method}($branchId, $dateRange) - ->pluck('rf_MedicalHistoryID') - ->unique() + if ($dateRange->isOneDay) { + $query->where('DateExtract', '>', $dateRange->startSql()) + ->where('DateExtract', '<=', $dateRange->endSql()); + } else { + $startAt = $dateRange->startSql(); + $endDate = $dateRange->end()->toDateString(); + $query->where('DateExtract', '>', $startAt) + ->whereDate('DateExtract', '<=', $endDate); + } + + if ($onlyIds) { + return $query->pluck('MedicalHistoryID'); + } + + return $query + ->select([ + 'MedicalHistoryID', + 'FAMILY', + 'Name', + 'OT', + 'BD', + 'DateRecipient', + 'DateExtract', + 'rf_EmerSignID', + 'rf_kl_VisitResultID', + ]) + ->with([ + 'outcomeMigration' => function ($q) { + $q->select([ + 'MigrationPatientID', + 'rf_MedicalHistoryID', + 'DateOut', + 'rf_DiagnosID', + ])->with(['mainDiagnosis' => function ($diagnosisQuery) { + $diagnosisQuery->select([ + 'DiagnosID', + 'rf_MKBID', + ])->with(['mkb' => function ($mkbQuery) { + $mkbQuery->select([ + 'MKBID', + 'DS', + 'NAME', + ]); + }]); + }]); + }, + ]) + ->orderBy('DateRecipient', 'DESC') + ->get() + ->map(fn ($patient) => $this->addOutcomeInfo($patient)); + } + + /** + * Получить пациентов, находящихся в реанимации на конец периода + */ + public function getReanimationPatients( + int $branchId, + DateRange $dateRange, + bool $onlyIds = false + ) { + $medicalHistoryIds = MisReanimation::query() + ->join('stt_migrationpatient as mp', 'mp.MigrationPatientID', '=', 'stt_reanimation.rf_MigrationPatientID') + ->where('stt_reanimation.rf_StationarBranchID', $branchId) + ->where('mp.rf_StationarBranchID', $branchId) + ->where('mp.rf_MedicalHistoryID', '<>', 0) + ->where('stt_reanimation.DateIn', '<=', $dateRange->endSql()) + ->where(function ($query) use ($dateRange) { + $query->where('stt_reanimation.DateOut', '>=', $dateRange->endSql()) + ->orWhereNull('stt_reanimation.DateOut') + ->orWhereDate('stt_reanimation.DateOut', '1900-01-01') + ->orWhereDate('stt_reanimation.DateOut', '2222-01-01'); + }) + ->distinct() + ->pluck('mp.rf_MedicalHistoryID') ->toArray(); if (empty($medicalHistoryIds)) { return collect(); } - if ($onlyIds) return collect($medicalHistoryIds); + if ($onlyIds) { + return collect($medicalHistoryIds); + } return MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds) - ->with(['surgicalOperations']) + ->select([ + 'MedicalHistoryID', + 'FAMILY', + 'Name', + 'OT', + 'BD', + 'DateRecipient', + 'DateExtract', + 'rf_EmerSignID', + 'rf_kl_VisitResultID', + ]) + ->with([ + 'surgicalOperations' => function ($q) { + $q->select([ + 'SurgicalOperationID', + 'rf_MedicalHistoryID', + 'rf_kl_ServiceMedicalID', + 'Date', + ])->with(['serviceMedical' => function ($serviceQuery) { + $serviceQuery->select([ + 'ServiceMedicalID', + 'ServiceMedicalCode', + 'ServiceMedicalName', + ]); + }]); + }, + 'outcomeMigration' => function ($q) { + $q->select([ + 'MigrationPatientID', + 'rf_MedicalHistoryID', + 'DateOut', + 'rf_DiagnosID', + ])->with(['mainDiagnosis' => function ($diagnosisQuery) { + $diagnosisQuery->select([ + 'DiagnosID', + 'rf_MKBID', + ])->with(['mkb' => function ($mkbQuery) { + $mkbQuery->select([ + 'MKBID', + 'DS', + 'NAME', + ]); + }]); + }]); + }, + ]) ->orderBy('DateRecipient', 'DESC') - ->get() - ->map(function ($patient) { - return $this->addOutcomeInfo($patient); - }); + ->get(); } /** @@ -230,6 +475,7 @@ class PatientService bool $countOnly = false ) { $query = MisSurgicalOperation::where('rf_StationarBranchID', $branchId) + ->completed() ->where('Date', '>=', $dateRange->startSql()) ->where('Date', '<=', $dateRange->endSql()); // ->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]); @@ -325,36 +571,64 @@ class PatientService DateRange $dateRange, bool $fillableAuto = false ) { - // Разная логика для заведующего и врача - if ($isHeadOrAdmin) { - // Заведующий: все поступившие за период - if ($fillableAuto) { - $query = LifeMisMigrationPatient::whereInDepartment($branchId) - ->where('DateIngoing', '>=', $dateRange->startSql()) - ->where('DateIngoing', '<=', $dateRange->endSql()); -// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]); - } else { - $query = MisMigrationPatient::whereInDepartment($branchId) - ->where('DateIngoing', '>=', $dateRange->startSql()) - ->where('DateIngoing', '<=', $dateRange->endSql()); -// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]); - } - } else { - // Врач: только поступившие за сутки - if ($fillableAuto) { - $query = LifeMisMigrationPatient::whereInDepartment($branchId) - ->where('DateIngoing', '>=', $dateRange->startSql()) - ->where('DateIngoing', '<=', $dateRange->endSql()); -// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]); - } else { - $query = MisMigrationPatient::whereInDepartment($branchId) - ->where('DateIngoing', '>=', $dateRange->startSql()) - ->where('DateIngoing', '<=', $dateRange->endSql()); -// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]); - }; + $startAt = $dateRange->start()->copy()->subDay()->format('Y-m-d H:i:s'); + $endAt = $dateRange->end()->copy()->addDay()->format('Y-m-d H:i:s'); + + if ($dateRange->isOneDay) { + $startAt = $dateRange->startSql(); + $endAt = $dateRange->endSql(); } - return $query; + $query = DB::table('stt_medicalhistory as mh') + ->selectRaw('mh."MedicalHistoryID" as "rf_MedicalHistoryID"') + ->where('mh.MedicalHistoryID', '<>', 0); + + $query->whereExists(function ($subQuery) use ($branchId, $startAt, $endAt) { + $subQuery->select(DB::raw(1)) + ->from('stt_migrationpatient as mp') + ->whereColumn('mp.rf_MedicalHistoryID', 'mh.MedicalHistoryID') + ->where('mp.rf_StationarBranchID', $branchId) + ->where('mp.DateIngoing', '>', $startAt) + ->where('mp.DateIngoing', '<=', $endAt); + }); + + if ($type === 'plan') { + $query->where('mh.rf_EmerSignID', 1); + } elseif ($type === 'emergency') { + $query->whereIn('mh.rf_EmerSignID', [2, 4]); + } + + return $query->distinct(); + } + + private function getHistoricalCurrentMedicalHistoryIds(int $branchId, DateRange $dateRange): array + { + // Исторический срез по основной таблице миграций: + // для каждой истории болезни берём последнюю миграцию в отделении + // на момент конца периода и проверяем, что пациент числился в отделении. + $latestRows = DB::table('stt_migrationpatient') + ->select('rf_MedicalHistoryID', DB::raw('MAX("MigrationPatientID") as max_migration_patient_id')) + ->where('rf_StationarBranchID', $branchId) + ->where('rf_MedicalHistoryID', '<>', 0) + ->where('DateIngoing', '<=', $dateRange->endSql()) + ->groupBy('rf_MedicalHistoryID'); + + return DB::table('stt_migrationpatient as mp') + ->joinSub($latestRows, 'latest', function ($join) { + $join->on('mp.rf_MedicalHistoryID', '=', 'latest.rf_MedicalHistoryID') + ->on('mp.MigrationPatientID', '=', 'latest.max_migration_patient_id'); + }) + ->join('stt_medicalhistory as mh', 'mh.MedicalHistoryID', '=', 'mp.rf_MedicalHistoryID') + ->where('mp.rf_StationarBranchID', $branchId) + ->where('mh.DateRecipient', '<=', $dateRange->endSql()) + ->where('mp.DateOut', '>=', $dateRange->endSql()) + ->where(function ($query) use ($dateRange) { + $query->where('mh.DateExtract', '>', $dateRange->endSql()) + ->orWhereDate('mh.DateExtract', '1900-01-01'); + }) + ->distinct() + ->pluck('mp.rf_MedicalHistoryID') + ->toArray(); } /** @@ -386,26 +660,15 @@ class PatientService int $branchId, DateRange $dateRange ): int { - // Поступившие сегодня указанного типа - $recipientCount = $this->buildRecipientQuery($type, $isHeadOrAdmin, $branchId, $dateRange) - ->count(); - - // Если нужны плановые/экстренные среди уже лечащихся - $currentCount = 0; - if ($type === 'plan' || $type === 'emergency') { - $currentIds = MisMigrationPatient::currentlyInTreatment($branchId) - ->pluck('rf_MedicalHistoryID') - ->toArray(); - - if (!empty($currentIds)) { - $currentCount = MisMedicalHistory::whereIn('MedicalHistoryID', $currentIds) - ->when($type === 'plan', fn($q) => $q->plan()) - ->when($type === 'emergency', fn($q) => $q->emergency()) - ->count(); - } - } - - return $currentCount; + return $this->getPlanOrEmergencyPatients( + $type, + $isHeadOrAdmin, + $branchId, + $dateRange, + true, + false, + true + ); } /** diff --git a/app/Services/ReportPageService.php b/app/Services/ReportPageService.php new file mode 100644 index 0000000..72ef7f7 --- /dev/null +++ b/app/Services/ReportPageService.php @@ -0,0 +1,44 @@ +reportService->getReportStatistics($department, $user, $dateRange); + $reportInfo = $this->reportService->getCurrentReportInfo($department, $user, $dateRange); + $recipientPlanOfYear = $this->reportService->getRecipientPlanOfYear($department, $dateRange); + + return [ + 'department' => [ + 'department_name' => $department->name_full, + 'department_id' => $department->department_id, + 'beds' => $department->beds, + 'percentLoadedBeds' => ($statistics['beds'] ?? 0) > 0 + ? round((($statistics['currentCount'] ?? 0) * 100) / $statistics['beds']) + : 0, + 'recipientPlanOfYear' => $recipientPlanOfYear['plan'], + 'progressPlanOfYear' => $recipientPlanOfYear['progress'], + ...$statistics, + ], + 'dates' => [ + 'startAt' => $dateRange->startTimestamp(), + 'endAt' => $dateRange->endTimestamp(), + ], + 'report' => $reportInfo, + 'metrikaItems' => MetrikaItem::whereIn('metrika_item_id', [3, 7, 8, 17])->get(), + 'patients' => [], + 'userId' => $reportInfo['userId'], + 'userName' => $reportInfo['userName'], + ]; + } +} diff --git a/app/Services/ReportService.php b/app/Services/ReportService.php index 747690a..e6e6489 100644 --- a/app/Services/ReportService.php +++ b/app/Services/ReportService.php @@ -3,6 +3,9 @@ namespace App\Services; use App\Models\Department; +use App\Models\DepartmentPatient; +use App\Models\DepartmentPatientOperation; +use App\Models\MisServiceMedical; use App\Models\MedicalHistorySnapshot; use App\Models\MetrikaResult; use App\Models\MisLpuDoctor; @@ -22,6 +25,7 @@ class ReportService { public function __construct( protected DateRangeService $dateRangeService, + protected UnifiedPatientService $unifiedPatientService, protected PatientService $patientQueryService, protected SnapshotService $snapshotService, protected StatisticsService $statisticsService @@ -42,7 +46,79 @@ class ReportService return $this->getStatisticsFromSnapshots($department, $dateRange, $branchId); } - return $this->getStatisticsFromReplica($user, $dateRange, $branchId); + return $this->getStatisticsFromReplica($department, $user, $dateRange, $branchId); + } + + public function shouldUseSnapshotsForPage(Department $department, User $user, DateRange $dateRange): bool + { + return $this->shouldUseSnapshots($department, $user, $dateRange); + } + + public function getFastReplicaStatisticsFromPatientsPayload( + Department $department, + User $user, + DateRange $dateRange, + array $patientsPayload + ): array { + $branchId = $this->getBranchId($department->rf_mis_department_id); + + $planCount = count($patientsPayload['mis-plan'] ?? []) + count($patientsPayload['special-plan'] ?? []); + $emergencyCount = count($patientsPayload['mis-emergency'] ?? []) + count($patientsPayload['special-emergency'] ?? []); + $dischargedCount = count($patientsPayload['mis-outcome-discharged'] ?? []) + count($patientsPayload['special-outcome-discharged'] ?? []); + $deadCount = count($patientsPayload['mis-outcome-deceased'] ?? []) + count($patientsPayload['special-outcome-deceased'] ?? []); + $outcomeCount = $dischargedCount + $deadCount; + + $recipientPatients = $this->unifiedPatientService + ->getLivePatientsByStatus($department, $user, 'recipient', $dateRange, $branchId); + $recipientCount = $recipientPatients->count(); + $recipientIds = $recipientPatients->pluck('id')->all(); + + $currentCount = $this->unifiedPatientService + ->getLivePatientCountByStatus($department, $user, 'current', $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), + ]; + + $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, + 'planCount' => $planCount, + 'emergencyCount' => $emergencyCount, + 'percentDead' => $percentDead, + 'beds' => $beds->value + ]; } /** @@ -58,6 +134,7 @@ class ReportService $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; }); @@ -78,6 +155,149 @@ class ReportService return $report; } + public function buildAutoFillReportPayload(User $user, Department $department, DateRange $dateRange): array + { + $branchId = $this->getBranchId($department->rf_mis_department_id); + + $metrics = $this->buildAutoFillMetrics($department, $user, $branchId, $dateRange); + + return [ + 'departmentId' => $department->department_id, + 'userId' => $user->rf_lpudoctor_id ?? $user->id, + 'dates' => [ + $dateRange->startTimestamp(), + $dateRange->endTimestamp(), + ], + 'sent_at' => $dateRange->endSql(), + 'created_at' => $dateRange->endSql(), + 'metrics' => [ + 'metrika_item_4' => $metrics['plan'], + 'metrika_item_12' => $metrics['emergency'], + 'metrika_item_3' => $metrics['recipient'], + 'metrika_item_7' => $metrics['discharged'] + $metrics['deceased'], + 'metrika_item_8' => $metrics['current'], + 'metrika_item_9' => $metrics['deceased'], + 'metrika_item_10' => $metrics['emergency_surgery'], + 'metrika_item_11' => $metrics['plan_surgery'], + 'metrika_item_13' => $metrics['transferred'], + 'metrika_item_14' => 0, + 'metrika_item_15' => $metrics['discharged'], + ], + 'observationPatients' => [], + 'unwantedEvents' => [], + ]; + } + + private function buildAutoFillMetrics(Department $department, User $user, int $branchId, DateRange $dateRange): array + { + $manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange); + $recipientQuery = $this->buildRecipientMedicalHistoryQuery($branchId, $dateRange); + $dischargeCodes = [1, 11, 2, 12, 7, 18, 48]; + $deceasedCodes = [5, 6, 15, 16]; + $transferCodes = [4, 14]; + + $planRecipient = (clone $recipientQuery) + ->where('rf_EmerSignID', 1) + ->distinct() + ->count('MedicalHistoryID'); + + $emergencyRecipient = (clone $recipientQuery) + ->whereIn('rf_EmerSignID', [2, 4]) + ->distinct() + ->count('MedicalHistoryID'); + + $recipientTotal = (clone $recipientQuery) + ->distinct() + ->count('MedicalHistoryID'); + + $discharged = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $dischargeCodes); + $deceased = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $deceasedCodes); + $transferred = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $transferCodes); + + return [ + 'plan' => $planRecipient, + 'emergency' => $emergencyRecipient, + 'recipient' => $recipientTotal, + 'discharged' => $discharged, + 'transferred' => $transferred, + 'deceased' => $deceased, + 'current' => $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'current', $dateRange, $branchId, null, true), + 'plan_surgery' => $this->patientQueryService->getSurgicalPatients( + 'plan', + $branchId, + $dateRange, + true + ) + ($manualSurgicalCount[1] ?? 0), + 'emergency_surgery' => $this->patientQueryService->getSurgicalPatients( + 'emergency', + $branchId, + $dateRange, + true + ) + ($manualSurgicalCount[0] ?? 0), + ]; + } + + private function buildRecipientMedicalHistoryQuery(int $branchId, DateRange $dateRange) + { + $startAt = $dateRange->start()->copy()->subDay()->format('Y-m-d H:i:s'); + $endAt = $dateRange->end()->copy()->addDay()->format('Y-m-d H:i:s'); + + if ($dateRange->isOneDay) { + $startAt = $dateRange->startSql(); + $endAt = $dateRange->endSql(); + } + + return MisMedicalHistory::query() + ->where('MedicalHistoryID', '<>', 0) + ->whereExists(function ($query) use ($branchId, $startAt, $endAt) { + $query->select(DB::raw(1)) + ->from('stt_migrationpatient as mp') + ->whereColumn('mp.rf_MedicalHistoryID', 'stt_medicalhistory.MedicalHistoryID') + ->where('mp.rf_StationarBranchID', $branchId) + ->where('mp.DateIngoing', '>', $startAt) + ->where('mp.DateIngoing', '<=', $endAt); + }); + } + + private function buildTreatedMedicalHistoryQuery(int $branchId, DateRange $dateRange) + { + $query = MisMedicalHistory::query() + ->where('MedicalHistoryID', '<>', 0) + ->whereExists(function ($query) use ($branchId) { + $query->select(DB::raw(1)) + ->from('stt_migrationpatient as mp') + ->whereColumn('mp.rf_MedicalHistoryID', 'stt_medicalhistory.MedicalHistoryID') + ->where('mp.rf_StationarBranchID', $branchId); + }); + + if ($dateRange->isOneDay) { + return $query + ->where('DateExtract', '>', $dateRange->startSql()) + ->where('DateExtract', '<=', $dateRange->endSql()); + } + + $startAt = $dateRange->startSql(); + $endDate = $dateRange->end()->toDateString(); + + return $query + ->where('DateExtract', '>', $startAt) + ->whereDate('DateExtract', '<=', $endDate); + } + + private function countOutcomeByVisitResultIds(int $branchId, DateRange $dateRange, array $visitResultIds): int + { + return $this->buildTreatedMedicalHistoryQuery($branchId, $dateRange) + ->whereExists(function ($query) use ($branchId, $visitResultIds) { + $query->select(DB::raw(1)) + ->from('stt_migrationpatient as mp') + ->whereColumn('mp.rf_MedicalHistoryID', 'stt_medicalhistory.MedicalHistoryID') + ->where('mp.rf_StationarBranchID', $branchId) + ->whereIn('mp.rf_kl_VisitResultID', $visitResultIds); + }) + ->distinct() + ->count('MedicalHistoryID'); + } + /** * Сохранить метрику койко-дня из снапшотов отчета */ @@ -300,15 +520,37 @@ class ReportService bool $beforeCreate = false, ?bool $includeCurrentPatients = null ) { + [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); $branchId = $this->getBranchId($department->rf_mis_department_id); - $useSnapshots = $this->shouldUseSnapshots($department, $user, $dateRange, $beforeCreate); - - if ($useSnapshots) { - return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId); + if ($baseStatus === 'reanimation') { + return $this->getPatientsFromReplica( + $department, + $user, + $status, + $dateRange, + $branchId, + $onlyIds, + $includeCurrentPatients + ); } - return $this->getPatientsFromReplica($department, $user, $status, $dateRange, $branchId, $onlyIds, $includeCurrentPatients); + $useSnapshots = !$this->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange) + && $this->shouldUseSnapshots($department, $user, $dateRange, $beforeCreate); + + if ($useSnapshots) { + return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId, $onlyIds); + } + + return $this->getPatientsFromReplica( + $department, + $user, + $status, + $dateRange, + $branchId, + $onlyIds, + $includeCurrentPatients + ); } /** @@ -320,9 +562,15 @@ class ReportService string $status, DateRange $dateRange ): int { + [$baseStatus] = $this->parseScopedStatus($status); $branchId = $this->getBranchId($department->rf_mis_department_id); - $useSnapshots = $this->shouldUseSnapshots($department, $user, $dateRange); + if ($baseStatus === 'reanimation') { + return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId); + } + + $useSnapshots = !$this->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange) + && $this->shouldUseSnapshots($department, $user, $dateRange); if ($useSnapshots) { return $this->getPatientsCountFromSnapshots($department, $status, $dateRange); @@ -331,6 +579,62 @@ class ReportService return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId); } + public function getPatientsCountsMap(Department $department, User $user, DateRange $dateRange): array + { + $baseStatuses = [ + 'plan', + 'emergency', + 'observation', + 'reanimation', + 'outcome-discharged', + 'outcome-deceased', + 'outcome-transferred', + ]; + + $counts = [ + 'mis-plan' => 0, + 'mis-emergency' => 0, + 'mis-observation' => 0, + 'mis-reanimation' => 0, + 'mis-outcome' => 0, + 'mis-outcome-discharged' => 0, + 'mis-outcome-deceased' => 0, + 'mis-outcome-transferred' => 0, + 'special-plan' => 0, + 'special-emergency' => 0, + 'special-observation' => 0, + 'special-reanimation' => 0, + 'special-outcome' => 0, + 'special-outcome-discharged' => 0, + 'special-outcome-deceased' => 0, + 'special-outcome-transferred' => 0, + ]; + + foreach ($baseStatuses as $baseStatus) { + $patients = collect($this->getPatientsByStatus($department, $user, $baseStatus, $dateRange)); + + $misCount = 0; + $specialCount = 0; + + foreach ($patients as $patient) { + if ($this->isSpecialScopedPatient($patient)) { + $specialCount++; + } else { + $misCount++; + } + } + + $counts["mis-{$baseStatus}"] = $misCount; + $counts["special-{$baseStatus}"] = $specialCount; + } + + // Выбывшие = выписанные + умершие (без переведенных) + $counts['mis-outcome'] = ($counts['mis-outcome-discharged'] ?? 0) + ($counts['mis-outcome-deceased'] ?? 0); + $counts['special-outcome'] = ($counts['special-outcome-discharged'] ?? 0) + ($counts['special-outcome-deceased'] ?? 0); + + return $counts; + } + /** * Получить ID отделения из стационарного отделения */ @@ -358,17 +662,42 @@ class ReportService return !$dateRange->isEndDateToday() || $reportToday; } + private function shouldUseReplicaForLiveStatus(User $user, string $status, DateRange $dateRange): bool + { + if ($user->isHeadOfDepartment() || $user->isAdmin()) { + return false; + } + + return in_array($status, ['plan', 'emergency', 'recipient', 'current'], true) + && $dateRange->isOneDay + && $dateRange->isEndDateToday(); + } + /** * Создать или обновить отчет */ 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' => $data['sent_at'] ?? $this->dateRangeService->toSqlFormat(\Illuminate\Support\Carbon::now()), - 'created_at' => $data['created_at'] ?? $this->dateRangeService->toSqlFormat(\Illuminate\Support\Carbon::now()), + 'sent_at' => $sentAt, + 'period_start' => $dateRange->startSql(), + 'period_end' => $dateRange->endSql(), + 'created_at' => $createdAt, ]; if (isset($data['reportId']) && $data['reportId']) { @@ -433,7 +762,7 @@ class ReportService { if (empty($unwantedEvents)) { $report->unwantedEvents()->delete(); - $this->saveMetrics($report, [16 => 0]); + $this->saveMetric($report, 16, 0); return; } @@ -459,7 +788,7 @@ class ReportService } // Обновить метрику - $this->saveMetrics($report, [16 => count($unwantedEvents)]); + $this->saveMetric($report, 16, count($unwantedEvents)); } /** @@ -475,14 +804,15 @@ class ReportService ->where('rf_report_id', $report->report_id) ->delete(); // Обновить метрику - $this->saveMetrics($report, [14 => 0]); + $this->saveMetric($report, 14, 0); return; } foreach ($observationPatients as $patient) { ObservationPatient::updateOrCreate( [ - 'rf_medicalhistory_id' => $patient['id'], + 'rf_medicalhistory_id' => $patient['medical_history_id'] ?? null, + 'rf_department_patient_id' => $patient['department_patient_id'] ?? null, 'rf_department_id' => $departmentId, ], [ @@ -494,7 +824,82 @@ class ReportService } // Обновить метрику - $this->saveMetrics($report, [14 => count($observationPatients)]); + $this->saveMetric($report, 14, count($observationPatients)); + } + + private function syncCalculatedMetrics(Report $report, User $user, array $data): void + { + if (!isset($data['dates'][0], $data['dates'][1])) { + return; + } + + $department = Department::query()->where('department_id', $report->rf_department_id)->first(); + if (!$department) { + return; + } + + $dateRange = $this->dateRangeService->getNormalizedDateRange( + $user, + (string) $data['dates'][0], + (string) $data['dates'][1] + ); + + $branchId = $this->getBranchId($department->rf_mis_department_id); + + $planCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['plan']); + $emergencyCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['emergency']); + $recipientCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['recipient']); + $dischargedCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['discharged']); + $transferredCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['transferred']); + $deceasedCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['deceased']); + $currentCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['current']); + $outcomeCount = $dischargedCount + $deceasedCount; + + $manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange); + $misEmergencySurgery = $branchId + ? $this->patientQueryService->getSurgicalPatients('emergency', $branchId, $dateRange, true) + : 0; + $misPlanSurgery = $branchId + ? $this->patientQueryService->getSurgicalPatients('plan', $branchId, $dateRange, true) + : 0; + + $observationCount = ObservationPatient::query() + ->where('rf_department_id', $department->department_id) + ->where('rf_report_id', $report->report_id) + ->count(); + + $unwantedEventsCount = UnwantedEvent::query() + ->where('rf_report_id', $report->report_id) + ->count(); + + $this->saveMetric($report, 3, $recipientCount); + $this->saveMetric($report, 4, $planCount); + $this->saveMetric($report, 7, $outcomeCount); + $this->saveMetric($report, 8, $currentCount); + $this->saveMetric($report, 9, $deceasedCount); + $this->saveMetric($report, 10, $misEmergencySurgery + ($manualSurgicalCount[0] ?? 0)); + $this->saveMetric($report, 11, $misPlanSurgery + ($manualSurgicalCount[1] ?? 0)); + $this->saveMetric($report, 12, $emergencyCount); + $this->saveMetric($report, 13, $transferredCount); + $this->saveMetric($report, 14, $observationCount); + $this->saveMetric($report, 15, $dischargedCount); + $this->saveMetric($report, 16, $unwantedEventsCount); + } + + private function countUniqueSnapshotsForTypes(int $reportId, array $patientTypes): int + { + return MedicalHistorySnapshot::query() + ->where('rf_report_id', $reportId) + ->whereIn('patient_type', $patientTypes) + ->get(['medical_history_snapshot_id', 'patient_uid', 'rf_medicalhistory_id']) + ->map(function (MedicalHistorySnapshot $snapshot) { + return $snapshot->patient_uid + ?: ($snapshot->rf_medicalhistory_id + ? "mis:{$snapshot->rf_medicalhistory_id}" + : "snapshot:{$snapshot->medical_history_snapshot_id}"); + }) + ->unique() + ->count(); } /** @@ -564,9 +969,159 @@ class ReportService /** * Удалить пациента из наблюдения */ - public function removeObservationPatient(int $medicalHistoryId): void + public function removeObservationPatient(string $patientId): void { - ObservationPatient::where('rf_medicalhistory_id', $medicalHistoryId)->delete(); + [$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(); + } + + public function createManualPatient(Department $department, User $user, array $data) + { + return $this->unifiedPatientService->createManualPatient($department, $user, $data); + } + + public function setManualPatientOutcome(int $departmentPatientId, array $data) + { + $patient = \App\Models\DepartmentPatient::where('department_patient_id', $departmentPatientId)->firstOrFail(); + + return $this->unifiedPatientService->recordManualOutcome($patient, $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; + } + + public function linkManualPatientToMis(int $departmentPatientId, int $medicalHistoryId) + { + $patient = \App\Models\DepartmentPatient::where('department_patient_id', $departmentPatientId)->firstOrFail(); + + return $this->unifiedPatientService->linkManualPatientToMis($patient, $medicalHistoryId); + } + + public function getManualPatientOperations(User $user, int $departmentPatientId) + { + $patient = $this->resolveManageableManualPatient($user, $departmentPatientId); + + return $patient->operations() + ->with('serviceMedical') + ->orderByDesc('started_at') + ->get(); + } + + 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'); + } + + 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'); + } + + 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(); + } + + 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 + { + if (!isset($data['startAt'], $data['endAt']) || !$data['startAt'] || !$data['endAt']) { + return; + } + + $dateRange = $this->dateRangeService->getNormalizedDateRange( + $user, + (string) $data['startAt'], + (string) $data['endAt'] + ); + + $reportIds = $this->getReportsForDateRange($patient->rf_department_id, $dateRange) + ->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, + 'updated_at' => now(), + ]); } /** @@ -582,6 +1137,7 @@ class ReportService $reportIds = $reports->pluck('report_id')->toArray(); $lastReport = array_first($reportIds); + $recipientReportIds = $this->getSnapshotRecipientReportIds($reportIds); // Получаем статистику из снапшотов $snapshotStats = [ @@ -598,11 +1154,10 @@ class ReportService ]; // Получаем ID поступивших пациентов - $recipientIds = MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds) - ->where('patient_type', 'recipient') - ->pluck('rf_medicalhistory_id') - ->unique() - ->toArray(); + $recipientIds = $this->snapshotService + ->getPatientsFromSnapshots('recipient', $recipientReportIds) + ->pluck('id') + ->all(); // Получаем количество операций из метрик $surgicalCount = [ @@ -633,61 +1188,17 @@ class ReportService /** * Получить статистику из реплики БД */ - private function getStatisticsFromReplica(User $user, DateRange $dateRange, int $branchId): array + private function getStatisticsFromReplica(Department $department, User $user, DateRange $dateRange, int $branchId): array { - $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); - - // Плановые: поступившие сегодня + уже лечащиеся - $planCount = $this->patientQueryService->getPatientsCountWithCurrent( - 'plan', - $isHeadOrAdmin, - $branchId, - $dateRange - ); - - // Экстренные: поступившие сегодня + уже лечащиеся - $emergencyCount = $this->patientQueryService->getPatientsCountWithCurrent( - 'emergency', - $isHeadOrAdmin, - $branchId, - $dateRange - ); - - // Все пациенты в отделении: поступившие + лечащиеся - $currentCount = $this->patientQueryService->getAllPatientsInDepartment( - $isHeadOrAdmin, - $branchId, - $dateRange, - true - ); - - // Поступившие сегодня (только новые поступления) - $recipientCount = $this->patientQueryService->getPlanOrEmergencyPatients( - null, // все типы - $isHeadOrAdmin, - $branchId, - $dateRange, - true, - false, - false // не включаем уже лечащихся - ); - - // Выбывшие за период - $outcomeCount = $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'without-transferred' - )->count(); - - // Умершие за период - $deadCount = $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'deceased' - )->count(); + $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); // Операции - $surgicalCount = [ + $misSurgicalCount = [ $this->patientQueryService->getSurgicalPatients( 'emergency', $branchId, @@ -701,17 +1212,17 @@ class ReportService true ) ]; + $manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange); + $surgicalCount = [ + ($misSurgicalCount[0] ?? 0) + ($manualSurgicalCount[0] ?? 0), + ($misSurgicalCount[1] ?? 0) + ($manualSurgicalCount[1] ?? 0), + ]; // ID поступивших сегодня (для отметки в таблице) - $recipientIds = $this->patientQueryService->getPlanOrEmergencyPatients( - null, - $isHeadOrAdmin, - $branchId, - $dateRange, - false, - true, - false // только поступившие сегодня - ); + $recipientIds = $this->unifiedPatientService + ->getLivePatientsByStatus($department, $user, 'recipient', $dateRange, $branchId) + ->pluck('id') + ->all(); $misBranch = MisStationarBranch::where('StationarBranchID', $branchId)->first(); $beds = Department::where('rf_mis_department_id', $misBranch->rf_DepartmentID) @@ -738,6 +1249,38 @@ class ReportService ]; } + private function getManualSurgicalCounts(Department $department, DateRange $dateRange): array + { + $baseQuery = DepartmentPatientOperation::query() + ->whereBetween('started_at', [$dateRange->startSql(), $dateRange->endSql()]) + ->whereHas('patient', function ($query) use ($department) { + $query->where('rf_department_id', $department->department_id) + ->whereIn('source_type', ['manual', 'special']); + }); + + $emergencyCount = (clone $baseQuery) + ->where(function ($query) { + $query->where('urgency', 'emergency') + ->orWhere(function ($fallback) { + $fallback->whereNull('urgency') + ->whereHas('patient', fn ($patientQuery) => $patientQuery->where('patient_kind', 'emergency')); + }); + }) + ->count(); + + $planCount = (clone $baseQuery) + ->where(function ($query) { + $query->where('urgency', 'plan') + ->orWhere(function ($fallback) { + $fallback->whereNull('urgency') + ->whereHas('patient', fn ($patientQuery) => $patientQuery->where('patient_kind', 'plan')); + }); + }) + ->count(); + + return [$emergencyCount, $planCount]; + } + /** * Получить пациентов из снапшотов */ @@ -748,29 +1291,52 @@ class ReportService int $branchId, bool $onlyIds = false ) { + [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); $reports = $this->getReportsForDateRange( $department->department_id, $dateRange ); $reportIds = $reports->pluck('report_id')->toArray(); + $recipientReportIds = $this->getSnapshotRecipientReportIds($reportIds); $patientTypeMap = [ 'plan' => 'plan', 'emergency' => 'emergency', + 'recipient' => 'recipient', 'outcome-discharged' => 'discharged', 'outcome-transferred' => 'transferred', 'outcome-deceased' => 'deceased', 'observation' => 'observation' ]; - $patientType = $patientTypeMap[$status] ?? null; + $patientType = $patientTypeMap[$baseStatus] ?? null; if ($patientType === 'observation') { - return $this->patientQueryService->getObservationPatients($department->department_id, $onlyIds); //$this->getObservationPatientsFromSnapshots($user->rf_department_id, $reportIds, $onlyIds); + return $this->unifiedPatientService->getObservationPatients($department, $onlyIds, $sourceScope); } - return $this->snapshotService->getPatientsFromSnapshots($patientType, $reportIds, $branchId, $onlyIds); + if ($dateRange->isOneDay && in_array($baseStatus, ['plan', 'emergency'], true)) { + $patients = $this->snapshotService->getPatientsFromOneDayCurrentSnapshots( + $patientType, + $reportIds, + false, + $recipientReportIds + ); + + return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds); + } + + $patients = $this->snapshotService->getPatientsFromSnapshots( + $patientType, + $reportIds, + $branchId, + false, + in_array($baseStatus, ['plan', 'emergency'], true), + $recipientReportIds + ); + + return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds); } /** @@ -785,63 +1351,31 @@ class ReportService bool $onlyIds = false, ?bool $isIncludeCurrent = null ) { - $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); + [$baseStatus] = $this->parseScopedStatus($status); // Для плановых и экстренных включаем уже лечащихся - $includeCurrent = $isIncludeCurrent ?? in_array($status, ['plan', 'emergency']); + $includeCurrent = $isIncludeCurrent ?? in_array($baseStatus, ['plan', 'emergency'], true); return match($status) { - 'plan', 'emergency' => $this->patientQueryService->getPlanOrEmergencyPatients( + 'plan', 'emergency', 'observation', 'reanimation', 'outcome', 'outcome-discharged', 'outcome-transferred', 'outcome-deceased', 'current', 'recipient' => + $this->unifiedPatientService->getLivePatientsByStatus( + $department, + $user, + $status, + $dateRange, + $branchId, + $onlyIds, + $includeCurrent + ), + default => $this->unifiedPatientService->getLivePatientsByStatus( + $department, + $user, $status, - $isHeadOrAdmin, - $branchId, $dateRange, - false, + $branchId, $onlyIds, $includeCurrent - ), - 'observation' => $this->patientQueryService->getObservationPatients($department->department_id, $onlyIds), - 'outcome' => $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'without-transferred', - $onlyIds - ), // Выписанные без перевода - 'outcome-discharged' => $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'discharged', - $onlyIds - ), - 'outcome-transferred' => $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'transferred', - $onlyIds - ), - 'outcome-deceased' => $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'deceased', - $onlyIds - ), - 'current' => $this->patientQueryService->getAllPatientsInDepartment( - $isHeadOrAdmin, - $branchId, - $dateRange, - false, - $onlyIds - ), - 'recipient' => $this->patientQueryService->getPlanOrEmergencyPatients( - null, - $isHeadOrAdmin, - $branchId, - $dateRange, - false, - $onlyIds, - false // только поступившие - ), - default => collect() + ) }; } @@ -850,6 +1384,7 @@ class ReportService */ private function getPatientsCountFromSnapshots(Department $department, string $status, DateRange $dateRange): int { + [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); $reports = $this->getReportsForDateRange( $department->department_id, $dateRange @@ -857,7 +1392,17 @@ class ReportService $reportIds = $reports->pluck('report_id')->toArray(); - if ($status === 'outcome') { + if ($baseStatus === 'outcome') { + if ($sourceScope !== 'all') { + return $this->getPatientsFromSnapshots( + $department, + $status, + $dateRange, + $this->getBranchId($department->rf_mis_department_id), + false + )->count(); + } + return MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds) ->whereIn('patient_type', ['discharged', 'deceased']) ->distinct('rf_medicalhistory_id') @@ -873,16 +1418,24 @@ class ReportService 'outcome-deceased' => 'deceased' ]; - $patientType = $patientTypeMap[$status] ?? null; + $patientType = $patientTypeMap[$baseStatus] ?? null; if (!$patientType) { return 0; } if ($patientType === 'observation') { - return ObservationPatient::whereIn('rf_report_id', $reportIds) - ->distinct('rf_medicalhistory_id') - ->count('rf_medicalhistory_id'); + return $this->unifiedPatientService->getObservationPatients($department, false, $sourceScope)->count(); + } + + if ($sourceScope !== 'all') { + return $this->getPatientsFromSnapshots( + $department, + $status, + $dateRange, + $this->getBranchId($department->rf_mis_department_id), + false + )->count(); } return MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds) @@ -891,6 +1444,15 @@ class ReportService ->count('rf_medicalhistory_id'); } + private function getSnapshotRecipientReportIds(array $reportIds): array + { + if (empty($reportIds)) { + return []; + } + + return [reset($reportIds)]; + } + /** * Получить количество пациентов из реплики БД */ @@ -902,40 +1464,70 @@ class ReportService int $branchId ): int { - $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); + [$baseStatus] = $this->parseScopedStatus($status); return match($status) { - 'plan', 'emergency' => $this->patientQueryService->getPatientsCountWithCurrent( + 'plan', 'emergency', 'observation', 'reanimation', 'outcome', 'outcome-discharged', 'outcome-transferred', 'outcome-deceased', 'current', 'recipient' => + $this->unifiedPatientService->getLivePatientCountByStatus( + $department, + $user, + $status, + $dateRange, + $branchId, + in_array($status, ['plan', 'emergency'], true) + ), + default => $this->unifiedPatientService->getLivePatientCountByStatus( + $department, + $user, $status, - $isHeadOrAdmin, - $branchId, $dateRange, - ), - 'observation' => ObservationPatient::where('rf_department_id', $department->department_id)->count(), - 'outcome' => $this->patientQueryService->getOutcomePatients( $branchId, - $dateRange, - 'without-transferred' - )->count(), - 'outcome-discharged' => $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'discharged' - )->count(), - 'outcome-transferred' => $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'transferred' - )->count(), - 'outcome-deceased' => $this->patientQueryService->getOutcomePatients( - $branchId, - $dateRange, - 'deceased' - )->count(), - default => 0 + in_array($baseStatus, ['plan', 'emergency'], true) + ) }; } + private function filterSnapshotPatientsByScope($patients, string $sourceScope, bool $onlyIds = false) + { + if ($sourceScope === 'all') { + return $onlyIds ? $patients->pluck('id') : $patients; + } + + $filtered = $patients->filter(function ($patient) use ($sourceScope) { + return match ($sourceScope) { + 'mis' => $patient->sourceType === 'mis', + 'special' => in_array($patient->sourceType, ['manual', 'special'], true), + default => true, + }; + })->values(); + + return $onlyIds ? $filtered->pluck('id') : $filtered; + } + + private function parseScopedStatus(string $status): array + { + foreach (['mis', 'special'] as $scope) { + $prefix = "{$scope}-"; + + if (str_starts_with($status, $prefix)) { + return [substr($status, strlen($prefix)), $scope]; + } + } + + return [$status, 'all']; + } + + private function isSpecialScopedPatient($patient): bool + { + $sourceType = $patient->sourceType ?? $patient->source_type ?? null; + + if ($sourceType !== null) { + return in_array($sourceType, ['manual', 'special'], true); + } + + return str_starts_with((string) ($patient->id ?? ''), 'manual:'); + } + /** * Получить нежелательные события за дату */ @@ -1025,11 +1617,23 @@ class ReportService ->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) { - if ($sum) $count += intval($metrikaResult->value) ?? 0; - else $count = intval($metrikaResult->value) ?? 0; + $count += intval($metrikaResult->value) ?? 0; } } } diff --git a/app/Services/SnapshotService.php b/app/Services/SnapshotService.php index ed44aaa..cc57209 100644 --- a/app/Services/SnapshotService.php +++ b/app/Services/SnapshotService.php @@ -2,20 +2,22 @@ namespace App\Services; +use App\Data\UnifiedPatientData; +use App\Models\Department; +use App\Models\DepartmentPatientOperation; use App\Models\MedicalHistorySnapshot; use App\Models\MetrikaResult; use App\Models\MisMedicalHistory; use App\Models\MisStationarBranch; -use App\Models\ObservationPatient; use App\Models\Report; use App\Models\User; use Carbon\Carbon; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\DB; class SnapshotService { public function __construct( + protected UnifiedPatientService $unifiedPatientService, protected PatientService $patientService, protected DateRangeService $dateRangeService, ) {} @@ -25,118 +27,124 @@ class SnapshotService */ public function createPatientSnapshots(Report $report, User $user, array $dates, $fillableAuto = false): void { - $branchId = $this->getBranchId($user->department->rf_mis_department_id); + $department = Department::query()->where('department_id', $report->rf_department_id)->first() ?? $user->department; + $branchId = $department + ? $this->getBranchId($department->rf_mis_department_id) + : null; + + if (!$department || !$branchId) { + return; + } + + MedicalHistorySnapshot::query() + ->where('rf_report_id', $report->report_id) + ->delete(); + [$startDate, $endDate] = $this->parseDates($dates); $dateRange = $this->dateRangeService->getNormalizedDateRange($user, $startDate, $endDate); - $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); - - // Массив для хранения подсчитанных метрик $metrics = []; - // 1. Плановые пациенты - $planIds = $this->patientService->getPlanOrEmergencyPatients( + $planPatients = $this->unifiedPatientService->getLivePatientsByStatus( + $department, + $user, 'plan', - $isHeadOrAdmin, - $branchId, $dateRange, + $branchId, false, - true, - true, + !$fillableAuto, $fillableAuto ); - $this->createSnapshotsForType($report, 'plan', $planIds); - $metrics[4] = $this->patientService->getPlanOrEmergencyPatients( - 'plan', - $isHeadOrAdmin, - $branchId, - $dateRange, - true, - $fillableAuto - ); // metrika_item_3 - плановые + $this->createSnapshotsForType($report, 'plan', $planPatients); + $metrics[4] = $planPatients->count(); - // 2. Экстренные пациенты - $emergencyIds = $this->patientService->getPlanOrEmergencyPatients( + $emergencyPatients = $this->unifiedPatientService->getLivePatientsByStatus( + $department, + $user, 'emergency', - $isHeadOrAdmin, - $branchId, $dateRange, + $branchId, false, - true, - true, + !$fillableAuto, $fillableAuto ); - $this->createSnapshotsForType($report, 'emergency', $emergencyIds); - $metrics[12] = $this->patientService->getPlanOrEmergencyPatients( - 'emergency', - $isHeadOrAdmin, - $branchId, - $dateRange, - true, - $fillableAuto - );; // metrika_item_12 - экстренные + $this->createSnapshotsForType($report, 'emergency', $emergencyPatients); + $metrics[12] = $emergencyPatients->count(); - // 3. Выписанные - $dischargedIds = $this->patientService->getOutcomePatients( - $branchId, + $dischargedPatients = $this->unifiedPatientService->getLivePatientsByStatus( + $department, + $user, + 'outcome-discharged', $dateRange, - 'discharged', - true - ); - $this->createSnapshotsForType($report, 'discharged', $dischargedIds); - $metrics[15] = count($dischargedIds); // metrika_item_15 - выписанные - - // 4. Переведенные - $transferredIds = $this->patientService->getOutcomePatients( $branchId, - $dateRange, - 'transferred', - true - ); - $this->createSnapshotsForType($report, 'transferred', $transferredIds); - $metrics[13] = count($transferredIds); // metrika_item_13 - переведенные - - // 5. Умершие - $deceasedIds = $this->patientService->getOutcomePatients( - $branchId, - $dateRange, - 'deceased', - true - ); - $this->createSnapshotsForType($report, 'deceased', $deceasedIds); -// $metrics[9] = count($deceasedIds); // metrika_item_9 - умершие - - // 6. Поступившие (все новые поступления - плановые + экстренные) - $recipientIds = $this->patientService->getPlanOrEmergencyPatients( + false, null, - $isHeadOrAdmin, - $branchId, - $dateRange, - false, - true, - false // только поступившие сегодня + $fillableAuto ); - $this->createSnapshotsForType($report, 'recipient', $recipientIds); -// $metrics[3] = count($recipientIds); // metrika_item_3 - поступившие + $this->createSnapshotsForType($report, 'discharged', $dischargedPatients); + $metrics[15] = $dischargedPatients->count(); + + $transferredPatients = $this->unifiedPatientService->getLivePatientsByStatus( + $department, + $user, + 'outcome-transferred', + $dateRange, + $branchId, + false, + null, + $fillableAuto + ); + $this->createSnapshotsForType($report, 'transferred', $transferredPatients); + $metrics[13] = $transferredPatients->count(); + + $deceasedPatients = $this->unifiedPatientService->getLivePatientsByStatus( + $department, + $user, + 'outcome-deceased', + $dateRange, + $branchId, + false, + null, + $fillableAuto + ); + $this->createSnapshotsForType($report, 'deceased', $deceasedPatients); + + $recipientPatients = $this->unifiedPatientService->getLivePatientsByStatus( + $department, + $user, + 'recipient', + $dateRange, + $branchId, + false, + null, + $fillableAuto + ); + $this->createSnapshotsForType($report, 'recipient', $recipientPatients); + + $currentPatients = $this->unifiedPatientService->getLivePatientsByStatus( + $department, + $user, + 'current', + $dateRange, + $branchId, + false, + null, + $fillableAuto + ); + $this->createSnapshotsForType($report, 'current', $currentPatients); - // 8. Плановые операции $planSurgeryCount = $this->patientService->getSurgicalPatients( 'plan', $branchId, $dateRange, true ); -// $metrics[11] = $planSurgeryCount; // metrika_item_11 - плановые операции - - // 9. Экстренные операции $emergencySurgeryCount = $this->patientService->getSurgicalPatients( 'emergency', $branchId, $dateRange, true ); -// $metrics[10] = $emergencySurgeryCount; // metrika_item_10 - экстренные операции - // Сохраняем все метрики $this->saveMetrics($report, $metrics); } @@ -181,43 +189,136 @@ class SnapshotService string $type, array $reportIds, ?int $branchId = null, - bool $onlyIds = false + bool $onlyIds = false, + bool $markRecipients = false, + ?array $recipientReportIds = null ): Collection { - // Получаем ID историй болезни напрямую через DB::table() — это быстрее - $medicalHistoryIds = DB::table('medical_history_snapshots') - ->select('rf_medicalhistory_id') + $snapshots = MedicalHistorySnapshot::query() ->whereIn('rf_report_id', $reportIds) ->where('patient_type', $type) - ->distinct() - ->pluck('rf_medicalhistory_id'); + ->get() + ->unique(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}")) + ->values(); - if ($medicalHistoryIds->isEmpty()) { + if ($snapshots->isEmpty()) { return collect(); } - $query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds); - - if ($type === 'plan') { - $query->plan(); - } elseif ($type === 'emergency') { - $query->emergency(); + $recipientIds = []; + if ($markRecipients) { + $recipientReportIds ??= $reportIds; + $recipientIds = MedicalHistorySnapshot::query() + ->whereIn('rf_report_id', $recipientReportIds) + ->where('patient_type', 'recipient') + ->get() + ->map(fn (MedicalHistorySnapshot $snapshot) => UnifiedPatientData::fromSnapshot($snapshot)->id) + ->unique() + ->values() + ->all(); } - // Загрузка отношений, необходимых для FormattedPatientResource - $query->with([ - 'outcomeMigration.mainDiagnosis.mkb', // mkb через диагноз - 'surgicalOperations.serviceMedical', // операции с услугами - ]); + $operationsByHistoryId = $this->getOperationsByMedicalHistoryId($snapshots); + $operationsByDepartmentPatientId = $this->getOperationsByDepartmentPatientId($snapshots); - $query->orderBy('DateRecipient', 'DESC'); + $patients = $snapshots->map(function (MedicalHistorySnapshot $snapshot) use ($recipientIds, $operationsByHistoryId, $operationsByDepartmentPatientId) { + $patientId = $snapshot->rf_department_patient_id + ? "manual:{$snapshot->rf_department_patient_id}" + : ($snapshot->patient_uid ?: "mis:{$snapshot->rf_medicalhistory_id}"); - $results = $query->get(); + $misOperations = $snapshot->rf_medicalhistory_id + ? ($operationsByHistoryId[$snapshot->rf_medicalhistory_id] ?? []) + : []; + $manualOperations = $snapshot->rf_department_patient_id + ? ($operationsByDepartmentPatientId[$snapshot->rf_department_patient_id] ?? []) + : []; + $operations = collect($misOperations) + ->merge($manualOperations) + ->unique(fn (array $operation) => ($operation['code'] ?? '') . '|' . ($operation['name'] ?? '')) + ->values() + ->all(); + + return UnifiedPatientData::fromSnapshot( + $snapshot, + in_array($patientId, $recipientIds, true), + $operations + ); + })->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')->values(); if ($onlyIds) { - return $results->pluck('MedicalHistoryID'); + return $patients->pluck('id'); } - return $results; + return $patients; + } + + public function getPatientsFromOneDayCurrentSnapshots( + string $type, + array $reportIds, + bool $onlyIds = false, + ?array $recipientReportIds = null + ): Collection { + $snapshots = MedicalHistorySnapshot::query() + ->whereIn('rf_report_id', $reportIds) + ->where('patient_type', 'current') + ->get(); + + if ($snapshots->isEmpty()) { + return $this->getPatientsFromSnapshots( + $type, + $reportIds, + null, + $onlyIds, + true, + $recipientReportIds + ); + } + + $recipientReportIds ??= $reportIds; + $recipientIds = MedicalHistorySnapshot::query() + ->whereIn('rf_report_id', $recipientReportIds) + ->where('patient_type', 'recipient') + ->get() + ->map(fn (MedicalHistorySnapshot $snapshot) => UnifiedPatientData::fromSnapshot($snapshot)->id) + ->unique() + ->values() + ->all(); + + $operationsByHistoryId = $this->getOperationsByMedicalHistoryId($snapshots); + $operationsByDepartmentPatientId = $this->getOperationsByDepartmentPatientId($snapshots); + + $patients = $snapshots + ->filter(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_kind === $type) + ->unique(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}")) + ->map(function (MedicalHistorySnapshot $snapshot) use ($recipientIds, $operationsByHistoryId, $operationsByDepartmentPatientId) { + $misOperations = $snapshot->rf_medicalhistory_id + ? ($operationsByHistoryId[$snapshot->rf_medicalhistory_id] ?? []) + : []; + $manualOperations = $snapshot->rf_department_patient_id + ? ($operationsByDepartmentPatientId[$snapshot->rf_department_patient_id] ?? []) + : []; + $operations = collect($misOperations) + ->merge($manualOperations) + ->unique(fn (array $operation) => ($operation['code'] ?? '') . '|' . ($operation['name'] ?? '')) + ->values() + ->all(); + + $patient = UnifiedPatientData::fromSnapshot( + $snapshot, + false, + $operations + ); + $patient->isRecipientToday = in_array($patient->id, $recipientIds, true); + + return $patient; + }) + ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '') + ->values(); + + if ($onlyIds) { + return $patients->pluck('id'); + } + + return $patients; } /** @@ -233,26 +334,32 @@ class SnapshotService $query->where('patient_type', $type); } - return $query->distinct('rf_medicalhistory_id') - ->count('rf_medicalhistory_id'); + return $query->get() + ->map(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}")) + ->unique() + ->count(); } /** * Создать снапшоты для определенного типа пациентов */ - private function createSnapshotsForType(Report $report, string $type, Collection $medicalHistoryIds): void + private function createSnapshotsForType(Report $report, string $type, Collection $patients): void { - foreach ($medicalHistoryIds as $id) { + foreach ($patients as $patient) { + if (!$patient instanceof UnifiedPatientData) { + continue; + } + MedicalHistorySnapshot::updateOrCreate( [ 'rf_report_id' => $report->report_id, - 'rf_medicalhistory_id' => $id, - 'patient_type' => $type + 'patient_uid' => $patient->patientUid, + 'patient_type' => $type, ], [ 'rf_report_id' => $report->report_id, - 'rf_medicalhistory_id' => $id, 'patient_type' => $type, + ...$patient->toSnapshotPayload($type), ] ); } @@ -277,4 +384,47 @@ class SnapshotService Carbon::createFromTimestampMs($dates[1])->setTimezone('Asia/Yakutsk'), ]; } + + private function getOperationsByMedicalHistoryId(Collection $snapshots): array + { + $historyIds = $snapshots->pluck('rf_medicalhistory_id')->filter()->unique()->values(); + + if ($historyIds->isEmpty()) { + return []; + } + + return MisMedicalHistory::query() + ->whereIn('MedicalHistoryID', $historyIds) + ->with(['surgicalOperations.serviceMedical']) + ->get() + ->mapWithKeys(function (MisMedicalHistory $history) { + return [ + $history->MedicalHistoryID => $history->surgicalOperations->map(fn ($operation) => [ + 'code' => $operation->serviceMedical?->ServiceMedicalCode, + 'name' => $operation->serviceMedical?->ServiceMedicalName, + ])->values()->all() + ]; + }) + ->all(); + } + + private function getOperationsByDepartmentPatientId(Collection $snapshots): array + { + $departmentPatientIds = $snapshots->pluck('rf_department_patient_id')->filter()->unique()->values(); + + if ($departmentPatientIds->isEmpty()) { + return []; + } + + return DepartmentPatientOperation::query() + ->whereIn('rf_department_patient_id', $departmentPatientIds) + ->with('serviceMedical') + ->get() + ->groupBy('rf_department_patient_id') + ->map(fn (Collection $operations) => $operations->map(fn (DepartmentPatientOperation $operation) => [ + 'code' => $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code, + 'name' => $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name, + ])->values()->all()) + ->all(); + } } diff --git a/app/Services/StationarBranchService.php b/app/Services/StationarBranchService.php index c77b1ae..410514e 100644 --- a/app/Services/StationarBranchService.php +++ b/app/Services/StationarBranchService.php @@ -2,7 +2,23 @@ namespace App\Services; +use App\Models\MisStationarBranch; +use Illuminate\Support\Facades\Cache; + class StationarBranchService { - + /** + * Получение идентификаторов приемных отделений. Кешируется на 24ч + * @return array + */ + public function getWardIds(): array + { + return Cache::tags(['wards_ids']) + ->remember('branch_ward_ids', now()->addHours(24), function () { + return MisStationarBranch::query() + ->where('IsHospitalWard', 1) + ->pluck('StationarBranchID') + ->toArray(); + }); + } } diff --git a/app/Services/UnifiedPatientService.php b/app/Services/UnifiedPatientService.php new file mode 100644 index 0000000..294ce63 --- /dev/null +++ b/app/Services/UnifiedPatientService.php @@ -0,0 +1,391 @@ +parseScopedStatus($status); + + if ($baseStatus === 'observation') { + return $this->getObservationPatients($department, $onlyIds, $sourceScope); + } + + $patients = match ($sourceScope) { + 'mis' => $this->getMisPatientDtos($user, $baseStatus, $dateRange, $branchId, $includeCurrent, $fillableAuto), + 'special' => $this->getSpecialPatientDtos($department, $baseStatus, $dateRange), + default => $this->getAggregatedPatientDtos($department, $user, $baseStatus, $dateRange, $branchId, $includeCurrent, $fillableAuto), + }; + + if ($onlyIds) { + return $patients->pluck('id'); + } + + return $patients; + } + + public function getLivePatientCountByStatus( + Department $department, + User $user, + string $status, + DateRange $dateRange, + int $branchId, + ?bool $includeCurrent = null, + bool $fillableAuto = false + ): int { + return $this->getLivePatientsByStatus($department, $user, $status, $dateRange, $branchId, false, $includeCurrent, $fillableAuto)->count(); + } + + public function createManualPatient(Department $department, User $user, array $data): DepartmentPatient + { + return DepartmentPatient::create([ + 'rf_department_id' => $department->department_id, + 'source_type' => 'special', + 'full_name' => $data['full_name'], + 'birth_date' => $data['birth_date'], + 'patient_kind' => $data['patient_kind'], + 'diagnosis_code' => $data['diagnosis_code'] ?? null, + 'diagnosis_name' => $data['diagnosis_name'] ?? null, + 'admitted_at' => $data['admitted_at'] ?? now(), + 'is_current' => true, + 'created_by' => $user->id, + ]); + } + + public function recordManualOutcome(DepartmentPatient $patient, array $data): DepartmentPatient + { + $patient->update([ + 'is_current' => false, + 'outcome_type' => $data['outcome_type'], + 'outcome_at' => $data['outcome_at'] ?? now(), + ]); + + return $patient->fresh(); + } + + public function updateManualPatient(DepartmentPatient $patient, array $data): DepartmentPatient + { + $patient->update([ + 'full_name' => $data['full_name'], + 'birth_date' => $data['birth_date'], + 'patient_kind' => $data['patient_kind'], + 'diagnosis_code' => $data['diagnosis_code'] ?? null, + 'diagnosis_name' => $data['diagnosis_name'] ?? null, + 'admitted_at' => $data['admitted_at'] ?? $patient->admitted_at, + ]); + + return $patient->fresh(); + } + + public function linkManualPatientToMis(DepartmentPatient $patient, int $medicalHistoryId): DepartmentPatient + { + $misPatient = MisMedicalHistory::where('MedicalHistoryID', $medicalHistoryId)->firstOrFail(); + + $patient->update([ + 'rf_medicalhistory_id' => $misPatient->MedicalHistoryID, + 'linked_to_mis_at' => now(), + 'full_name' => $patient->full_name ?: trim("{$misPatient->FAMILY} {$misPatient->Name} {$misPatient->OT}"), + 'birth_date' => $patient->birth_date ?: $misPatient->BD, + ]); + + return $patient->fresh(); + } + + public function searchMisPatients(Department $department, string $query): Collection + { + $branchId = \App\Models\MisStationarBranch::where('rf_DepartmentID', $department->rf_mis_department_id) + ->value('StationarBranchID'); + + return MisMedicalHistory::query() + ->whereHas('migrations', fn ($builder) => $builder->where('rf_StationarBranchID', $branchId)) + ->where(function ($builder) use ($query) { + $builder->where('FAMILY', 'like', "%{$query}%") + ->orWhere('Name', 'like', "%{$query}%") + ->orWhere('OT', 'like', "%{$query}%"); + }) + ->with(['outcomeMigration.mainDiagnosis.mkb']) + ->limit(20) + ->get() + ->map(fn (MisMedicalHistory $patient) => UnifiedPatientData::fromMisMedicalHistory($patient)); + } + + public function getObservationPatients( + Department $department, + bool $onlyIds = false, + string $sourceScope = 'all' + ): Collection + { + $observationPatients = ObservationPatient::where('rf_department_id', $department->department_id)->get(); + + $misIds = $observationPatients->pluck('rf_medicalhistory_id')->filter()->unique()->values(); + $manualIds = $observationPatients->pluck('rf_department_patient_id')->filter()->unique()->values(); + + $misPatients = MisMedicalHistory::whereIn('MedicalHistoryID', $misIds) + ->with(['outcomeMigration.mainDiagnosis.mkb']) + ->get() + ->keyBy('MedicalHistoryID'); + + $manualPatients = DepartmentPatient::whereIn('department_patient_id', $manualIds)->get()->keyBy('department_patient_id'); + + $patients = $observationPatients->map(function (ObservationPatient $observation) use ($misPatients, $manualPatients, $sourceScope) { + if ($observation->rf_department_patient_id && $manualPatients->has($observation->rf_department_patient_id)) { + if ($sourceScope === 'mis') { + return null; + } + + return UnifiedPatientData::fromDepartmentPatient( + $manualPatients[$observation->rf_department_patient_id], + false, + [], + $observation->comment + ); + } + + if ($observation->rf_medicalhistory_id && $misPatients->has($observation->rf_medicalhistory_id)) { + if ($sourceScope === 'special') { + return null; + } + + return UnifiedPatientData::fromMisMedicalHistory( + $misPatients[$observation->rf_medicalhistory_id], + false, + null, + $observation->comment + ); + } + + return null; + })->filter()->values(); + + if ($onlyIds) { + return $patients->pluck('id'); + } + + return $patients; + } + + private function getAggregatedPatientDtos( + Department $department, + User $user, + string $status, + DateRange $dateRange, + int $branchId, + ?bool $includeCurrent = null, + bool $fillableAuto = false + ): Collection { + $misPatients = $this->getMisPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto); + $manualPatients = $this->getManualPatients($department, $status, $dateRange); + $linkedManualPatients = DepartmentPatient::where('rf_department_id', $department->department_id) + ->whereIn('source_type', self::SPECIAL_SOURCE_TYPES) + ->whereNotNull('rf_medicalhistory_id') + ->get() + ->keyBy('rf_medicalhistory_id'); + + $mergedMisPatients = $misPatients->map(function ($patient) use ($linkedManualPatients) { + $linkedManual = $linkedManualPatients->get($patient->MedicalHistoryID); + + return UnifiedPatientData::fromMisMedicalHistory( + $patient, + (bool) ($patient->is_recipient_today ?? false), + $linkedManual, + $this->resolveObservationComment($patient->MedicalHistoryID, null) + ); + }); + + $manualDtos = $this->mapManualPatients($manualPatients, $dateRange); + + return UnifiedPatientData::unique($mergedMisPatients->concat($manualDtos)) + ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '') + ->values(); + } + + private function getMisPatientDtos( + User $user, + string $status, + DateRange $dateRange, + int $branchId, + ?bool $includeCurrent = null, + bool $fillableAuto = false + ): Collection { + return $this->getMisPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto) + ->map(fn ($patient) => UnifiedPatientData::fromMisMedicalHistory( + $patient, + (bool) ($patient->is_recipient_today ?? false), + )) + ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '') + ->values(); + } + + private function getSpecialPatientDtos( + Department $department, + string $status, + DateRange $dateRange + ): Collection { + return $this->mapManualPatients( + $this->getManualPatients($department, $status, $dateRange, self::SPECIAL_SOURCE_TYPES), + $dateRange + ); + } + + private function getMisPatients( + User $user, + string $status, + DateRange $dateRange, + int $branchId, + ?bool $includeCurrent = null, + bool $fillableAuto = false + ): Collection { + $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); + $includeCurrent = $includeCurrent ?? in_array($status, ['plan', 'emergency'], true); + + return match ($status) { + 'plan', 'emergency' => $this->patientService->getPlanOrEmergencyPatients( + $status, + $isHeadOrAdmin, + $branchId, + $dateRange, + false, + false, + $includeCurrent, + $fillableAuto + ), + 'current' => $this->patientService->getAllPatientsInDepartment( + $isHeadOrAdmin, + $branchId, + $dateRange, + false, + false, + $fillableAuto + ), + 'recipient' => $this->patientService->getPlanOrEmergencyPatients( + null, + $isHeadOrAdmin, + $branchId, + $dateRange, + false, + false, + false, + $fillableAuto + ), + 'outcome' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'without-transferred'), + 'outcome-discharged' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'discharged'), + 'outcome-transferred' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'transferred'), + 'outcome-deceased' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'deceased'), + 'reanimation' => $this->patientService->getReanimationPatients($branchId, $dateRange), + default => collect(), + }; + } + + private function getManualPatients( + Department $department, + string $status, + DateRange $dateRange, + ?array $sourceTypes = self::SPECIAL_SOURCE_TYPES + ): Collection + { + $query = DepartmentPatient::where('rf_department_id', $department->department_id) + ->with(['operations.serviceMedical']); + + if ($sourceTypes !== null) { + $query->whereIn('source_type', $sourceTypes); + } + + return match ($status) { + 'plan', 'emergency' => $query + ->current() + ->where('patient_kind', $status) + ->get(), + 'current' => $query + ->current() + ->get(), + 'recipient' => $query + ->whereBetween('admitted_at', [$dateRange->startSql(), $dateRange->endSql()]) + ->get(), + 'outcome' => $query + ->whereNotNull('outcome_type') + ->whereIn('outcome_type', ['discharged', 'deceased']) + ->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()]) + ->get(), + 'outcome-discharged', 'outcome-transferred', 'outcome-deceased' => $query + ->where('outcome_type', str_replace('outcome-', '', $status)) + ->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()]) + ->get(), + 'reanimation' => collect(), + default => collect(), + }; + } + + private function mapManualPatients(Collection $manualPatients, DateRange $dateRange): Collection + { + return $manualPatients + ->map(function (DepartmentPatient $patient) use ($dateRange) { + $operations = $patient->operations->map(fn ($operation) => [ + 'id' => $operation->department_patient_operation_id, + 'code' => $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code, + 'name' => $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name, + 'startAt' => $operation->started_at?->toIso8601String(), + 'endAt' => $operation->ended_at?->toIso8601String(), + ])->filter(fn ($operation) => $operation['code'] || $operation['name'])->values()->all(); + + return UnifiedPatientData::fromDepartmentPatient( + $patient, + $patient->admitted_at?->betweenIncluded($dateRange->startDate, $dateRange->endDate) ?? false, + $operations, + $this->resolveObservationComment(null, $patient->department_patient_id) + ); + }) + ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '') + ->values(); + } + + private function parseScopedStatus(string $status): array + { + foreach (['mis', 'special'] as $scope) { + $prefix = "{$scope}-"; + + if (str_starts_with($status, $prefix)) { + return [substr($status, strlen($prefix)), $scope]; + } + } + + return [$status, 'all']; + } + + private function resolveObservationComment(?int $medicalHistoryId, ?int $departmentPatientId): ?string + { + $query = ObservationPatient::query(); + + if ($departmentPatientId) { + $query->where('rf_department_patient_id', $departmentPatientId); + } elseif ($medicalHistoryId) { + $query->where('rf_medicalhistory_id', $medicalHistoryId); + } else { + return null; + } + + return $query->pluck('comment')->filter()->implode('; ') ?: null; + } +} diff --git a/config/time.php b/config/time.php new file mode 100644 index 0000000..211267d --- /dev/null +++ b/config/time.php @@ -0,0 +1,5 @@ + env('TIME_EVENT_SOURCE_URL', null), +]; \ No newline at end of file diff --git a/database/migrations/2026_04_09_120000_create_department_patients_table.php b/database/migrations/2026_04_09_120000_create_department_patients_table.php new file mode 100644 index 0000000..3922046 --- /dev/null +++ b/database/migrations/2026_04_09_120000_create_department_patients_table.php @@ -0,0 +1,39 @@ +id('department_patient_id'); + $table->unsignedBigInteger('rf_department_id'); + $table->string('source_type')->default('manual'); + $table->unsignedBigInteger('rf_medicalhistory_id')->nullable(); + $table->string('full_name'); + $table->date('birth_date'); + $table->string('patient_kind'); + $table->string('diagnosis_code')->nullable(); + $table->text('diagnosis_name')->nullable(); + $table->dateTime('admitted_at'); + $table->boolean('is_current')->default(true); + $table->string('outcome_type')->nullable(); + $table->dateTime('outcome_at')->nullable(); + $table->unsignedBigInteger('created_by')->nullable(); + $table->dateTime('linked_to_mis_at')->nullable(); + $table->timestamps(); + + $table->index(['rf_department_id', 'patient_kind'], 'idx_department_patients_department_kind'); + $table->index(['rf_department_id', 'is_current'], 'idx_department_patients_department_current'); + $table->index(['rf_medicalhistory_id'], 'idx_department_patients_medical_history'); + }); + } + + public function down(): void + { + Schema::dropIfExists('department_patients'); + } +}; diff --git a/database/migrations/2026_04_09_120100_add_unified_columns_to_medical_history_snapshots_table.php b/database/migrations/2026_04_09_120100_add_unified_columns_to_medical_history_snapshots_table.php new file mode 100644 index 0000000..120cabc --- /dev/null +++ b/database/migrations/2026_04_09_120100_add_unified_columns_to_medical_history_snapshots_table.php @@ -0,0 +1,52 @@ +string('patient_uid')->nullable()->after('patient_type'); + $table->string('patient_source_type')->nullable()->after('patient_uid'); + $table->unsignedBigInteger('rf_department_patient_id')->nullable()->after('rf_medicalhistory_id'); + $table->string('patient_kind')->nullable()->after('rf_department_patient_id'); + $table->string('full_name')->nullable()->after('patient_kind'); + $table->date('birth_date')->nullable()->after('full_name'); + $table->string('diagnosis_code')->nullable()->after('birth_date'); + $table->text('diagnosis_name')->nullable()->after('diagnosis_code'); + $table->dateTime('admitted_at')->nullable()->after('diagnosis_name'); + $table->string('outcome_type')->nullable()->after('admitted_at'); + $table->dateTime('outcome_at')->nullable()->after('outcome_type'); + $table->boolean('is_manual')->default(false)->after('outcome_at'); + + $table->index(['rf_report_id', 'patient_uid'], 'idx_snapshots_report_uid'); + $table->index(['patient_uid'], 'idx_snapshots_patient_uid'); + }); + } + + public function down(): void + { + Schema::table('medical_history_snapshots', function (Blueprint $table) { + $table->dropIndex('idx_snapshots_report_uid'); + $table->dropIndex('idx_snapshots_patient_uid'); + + $table->dropColumn([ + 'patient_uid', + 'patient_source_type', + 'rf_department_patient_id', + 'patient_kind', + 'full_name', + 'birth_date', + 'diagnosis_code', + 'diagnosis_name', + 'admitted_at', + 'outcome_type', + 'outcome_at', + 'is_manual', + ]); + }); + } +}; diff --git a/database/migrations/2026_04_09_120200_add_department_patient_reference_to_observation_patients_table.php b/database/migrations/2026_04_09_120200_add_department_patient_reference_to_observation_patients_table.php new file mode 100644 index 0000000..7342916 --- /dev/null +++ b/database/migrations/2026_04_09_120200_add_department_patient_reference_to_observation_patients_table.php @@ -0,0 +1,24 @@ +unsignedBigInteger('rf_department_patient_id')->nullable()->after('rf_medicalhistory_id'); + $table->index(['rf_department_patient_id'], 'idx_observation_patients_department_patient'); + }); + } + + public function down(): void + { + Schema::table('observation_patients', function (Blueprint $table) { + $table->dropIndex('idx_observation_patients_department_patient'); + $table->dropColumn('rf_department_patient_id'); + }); + } +}; diff --git a/database/migrations/2026_04_15_090000_create_department_patient_operations_table.php b/database/migrations/2026_04_15_090000_create_department_patient_operations_table.php new file mode 100644 index 0000000..3d2e9b3 --- /dev/null +++ b/database/migrations/2026_04_15_090000_create_department_patient_operations_table.php @@ -0,0 +1,31 @@ +id('department_patient_operation_id'); + $table->unsignedBigInteger('rf_department_patient_id'); + $table->unsignedBigInteger('rf_kl_service_medical_id')->nullable(); + $table->string('service_code', 64)->nullable(); + $table->string('service_name', 500)->nullable(); + $table->dateTime('started_at'); + $table->dateTime('ended_at'); + $table->unsignedBigInteger('created_by')->nullable(); + $table->timestamps(); + + $table->index(['rf_department_patient_id'], 'idx_department_patient_operations_patient'); + $table->index(['started_at'], 'idx_department_patient_operations_started_at'); + }); + } + + public function down(): void + { + Schema::dropIfExists('department_patient_operations'); + } +}; diff --git a/database/migrations/2026_04_15_101000_make_rf_medicalhistory_id_nullable_in_medical_history_snapshots_table.php b/database/migrations/2026_04_15_101000_make_rf_medicalhistory_id_nullable_in_medical_history_snapshots_table.php new file mode 100644 index 0000000..e62962c --- /dev/null +++ b/database/migrations/2026_04_15_101000_make_rf_medicalhistory_id_nullable_in_medical_history_snapshots_table.php @@ -0,0 +1,17 @@ +string('urgency', 32)->nullable()->after('service_name'); + $table->index(['urgency'], 'idx_department_patient_operations_urgency'); + }); + } + + public function down(): void + { + Schema::table('department_patient_operations', function (Blueprint $table) { + $table->dropIndex('idx_department_patient_operations_urgency'); + $table->dropColumn('urgency'); + }); + } +}; diff --git a/database/migrations/2026_04_20_093409_add_period_columns_in_reports_table.php b/database/migrations/2026_04_20_093409_add_period_columns_in_reports_table.php new file mode 100644 index 0000000..d727e33 --- /dev/null +++ b/database/migrations/2026_04_20_093409_add_period_columns_in_reports_table.php @@ -0,0 +1,41 @@ +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->string('status')->default('draft') + ->comment('Статус отчета'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('reports', function (Blueprint $table) { + $table->dropColumn(['period_type', 'period_start', 'period_end', 'status', 'report_year', 'report_month']); + }); + } +}; diff --git a/database/migrations/2026_04_20_163507_add_code_column_in_metrika_items_table.php b/database/migrations/2026_04_20_163507_add_code_column_in_metrika_items_table.php new file mode 100644 index 0000000..1c1ef7e --- /dev/null +++ b/database/migrations/2026_04_20_163507_add_code_column_in_metrika_items_table.php @@ -0,0 +1,28 @@ +string('code')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('metrika_items', function (Blueprint $table) { + // + }); + } +}; diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..f3a822d --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,85 @@ +# Частые вопросы + +## Не получается войти в систему + +Проверьте: + +- правильность логина; +- правильность пароля; +- раскладку клавиатуры; +- не включен ли `Caps Lock`. + +Если проблема сохраняется, обратитесь к администратору системы. + +## Почему я не вижу нужный раздел + +Доступность разделов зависит от вашей роли. + +Например: + +- врач работает со сводной; +- заведующий видит статистику отделения; +- администратор имеет доступ к административным функциям. + +Если у вас несколько ролей, попробуйте переключить роль в верхней части интерфейса. + +## Почему кнопка «Заполнить сводную» недоступна + +Возможные причины: + +- для вашей роли действует ограничение по времени; +- у вас нет права на создание отчета; +- текущая роль не предназначена для заполнения сводной. + +## Почему система сообщает, что сводная уже создана + +Это означает, что за выбранную дату и отделение уже есть отчет. + +В этом случае: + +1. Откройте существующую сводную. +2. Проверьте, кто указан ответственным. +3. При необходимости уточните дальнейшие действия у заведующего или администратора. + +## Почему данные за период выглядят слишком большими или слишком маленькими + +Сначала проверьте: + +- дату; +- диапазон дат; +- отделение; +- выбранную роль. + +Чаще всего причина связана с тем, что выбран не тот период, например месяц вместо недели или текущий год вместо одного дня. + +## Почему я не могу сохранить отчет + +Проверьте: + +- заполнены ли обязательные поля; +- нет ли предупреждения на странице; +- доступна ли кнопка `Сохранить отчет`; +- открыт ли отчет в режиме редактирования, а не только просмотра. + +## Как понять, за какой период сейчас показаны данные + +Ориентируйтесь на блок выбора даты в верхней части страницы. + +Именно выбранная там дата или диапазон определяют: + +- содержимое сводной; +- содержимое статистики; +- выгрузку в Excel. + +## Что делать, если отображаются неверные данные + +Подготовьте для обращения в поддержку следующую информацию: + +- ваше ФИО; +- роль, под которой вы работаете; +- отделение; +- дата или период; +- краткое описание проблемы; +- что вы ожидали увидеть на экране. + +Это поможет быстрее найти причину и проверить данные. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..328f2b2 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,79 @@ +# Метрика + +Добро пожаловать в руководство пользователя системы **«Метрика»**. + +Система помогает: + +- формировать сводный отчет по отделению; +- просматривать статистику по отделению или по нескольким отделениям; +- контролировать нежелательные события; +- отслеживать пациентов на контроле; +- работать в пределах своей роли и доступных отделений. + +## Для кого предназначена система + +Система используется медицинским персоналом и сотрудниками, которые работают с оперативной отчетностью отделений. + +В зависимости от вашей роли в системе вам могут быть доступны разные разделы: + +- `Врач` — заполнение сводной; +- `Заведующий отделением` — просмотр статистики и работа со сводными по отделению; +- `Администратор` — полный доступ, включая административную панель. + +Если у вас несколько ролей, нужную роль можно переключить в верхней части интерфейса. + +## Как начать работу + +1. Откройте страницу входа в систему. +2. Введите логин и пароль. +3. После входа вы попадете на стартовую страницу. +4. Выберите нужный раздел: + - `Заполнить сводную`; + - `Статистика моего отделения`; + - `Панель администратора` — только для администратора. + +## Что находится на стартовой странице + +На стартовой странице отображаются: + +- имя пользователя; +- текущая дата и время сервера; +- кнопки перехода к основным разделам; +- кнопка выхода из системы. + +### Кнопка «Заполнить сводную» + +Назначение кнопки зависит от вашей роли: + +- для `врача` она открывает выбор ответственного и отделения перед переходом к отчету; +- для `администратора` она открывает форму сводной напрямую; +- для `заведующего` доступ к сводной определяется рабочим сценарием и выбранной ролью. + +Для врача кнопка доступна только в установленный интервал времени. Если кнопка заблокирована, поверх нее отображается подсказка с доступным временем. + +## Основные разделы документации + +- [Работа со сводной](./reporting.md) +- [Статистика](./statistics.md) +- [Частые вопросы](./faq.md) + +## Общая логика работы + +Система строится вокруг даты и отделения. + +Обычно работа выглядит так: + +1. Пользователь выбирает нужный раздел. +2. При необходимости выбирает отделение и ответственного. +3. Указывает дату или период. +4. Просматривает данные. +5. При наличии права сохраняет отчет. + +## Полезный совет + +Если данные на экране не совпадают с ожидаемыми, в первую очередь проверьте: + +- выбранную роль; +- выбранное отделение; +- дату или диапазон дат; +- наличие уже созданной сводной за нужный день. diff --git a/docs/reporting.md b/docs/reporting.md new file mode 100644 index 0000000..eb7d6f1 --- /dev/null +++ b/docs/reporting.md @@ -0,0 +1,132 @@ +# Работа со сводной + +Эта страница описывает, как открыть, проверить и сохранить сводный отчет. + +## Что такое сводная + +Сводная — это отчет по отделению за выбранную дату или период. + +В сводной отображаются: + +- показатели отделения; +- количество поступивших и выбывших пациентов; +- число пациентов, состоящих в отделении; +- данные по операциям; +- нежелательные события; +- пациенты на контроле. + +Состав и доступность полей могут зависеть от роли пользователя и от того, открыт ли отчет только для просмотра или для заполнения. + +## Как открыть сводную + +### Вариант 1. Со стартовой страницы + +1. Нажмите `Заполнить сводную`. +2. Если откроется окно выбора, укажите: + - ответственного; + - отделение. +3. Нажмите `Перейти к заполнению сводной`. + +Если сводная за текущую дату уже существует, система предупредит об этом и предложит перейти к уже созданному отчету. + +### Вариант 2. Из раздела статистики + +1. Откройте `Статистика моего отделения`. +2. Выберите нужную дату или период. +3. Нажмите на название отделения в таблице. +4. Откроется сводная по выбранному отделению и выбранному периоду. + +## Что находится на странице сводной + +На странице сводной обычно доступны: + +- название отделения; +- выбранная дата или диапазон дат; +- сводные числовые показатели; +- поля формы отчета; +- разделы с пациентами; +- кнопка `Сохранить отчет`, если редактирование разрешено. + +Также на странице может отображаться предупреждение, если отчет за этот день уже был создан другим пользователем. + +## Как выбрать дату + +В верхней части страницы находится выбор даты. + +В зависимости от роли можно выбрать: + +- одну дату; +- диапазон дат. + +После выбора периода система обновляет данные на странице. + +### Кнопка «Сегодня» и «Текущий год» + +Рядом с выбором даты могут быть доступны быстрые действия: + +- `Сегодня` — переход к текущей дате; +- `Текущий год` — переход к периоду с начала года до текущей даты. + +## Что проверять перед сохранением + +Перед тем как нажать `Сохранить отчет`, проверьте: + +- правильность отделения; +- правильность даты или периода; +- корректность числовых значений; +- заполнение обязательных полей; +- список нежелательных событий; +- пациентов на контроле, если они должны быть указаны. + +## Как сохранить отчет + +1. Заполните доступные поля. +2. Проверьте предупреждения на странице. +3. Нажмите `Сохранить отчет`. + +Если форма заполнена корректно, система сохранит отчет и покажет сообщение об успешном сохранении. + +## Когда кнопка сохранения может быть недоступна + +Кнопка `Сохранить отчет` может отсутствовать или быть недоступной, если: + +- у пользователя нет права на сохранение; +- отчет открыт только для просмотра; +- выбран период, для которого редактирование не предусмотрено; +- отчет уже создан и текущий пользователь не является ответственным за его заполнение. + +## Нежелательные события + +На странице сводной можно работать с нежелательными событиями. + +Обычно пользователь может: + +- просмотреть список событий; +- добавить новое событие; +- отредактировать существующее; +- удалить событие, если это разрешено. + +Количество нежелательных событий отображается рядом с соответствующей кнопкой. + +## Пациенты на контроле + +В системе предусмотрен раздел для пациентов, требующих дополнительного внимания. + +Пользователь может: + +- просматривать список пациентов на контроле; +- добавлять комментарии; +- удалять пациента из контроля, если необходимость отпала. + +## Если данные выглядят неверно + +Если в сводной отображаются неожиданные данные: + +1. Проверьте выбранный диапазон дат. +2. Убедитесь, что выбрано нужное отделение. +3. Проверьте, не открыта ли уже существующая сводная за другой день. +4. Обновите страницу и снова выберите дату. +5. Если проблема сохраняется, сообщите администратору, указав: + - отделение; + - дату или период; + - что именно отображается неверно. diff --git a/docs/statistics.md b/docs/statistics.md new file mode 100644 index 0000000..8757e5c --- /dev/null +++ b/docs/statistics.md @@ -0,0 +1,108 @@ +# Статистика + +Раздел статистики помогает просматривать показатели по отделениям за выбранную дату или период. + +## Для чего нужен раздел + +В разделе статистики можно: + +- оценить показатели работы отделений; +- сравнить данные по нескольким отделениям; +- открыть сводную конкретного отделения; +- просмотреть нежелательные события; +- посмотреть пациентов на контроле; +- выгрузить таблицу в Excel. + +## Как открыть статистику + +1. На стартовой странице нажмите `Статистика моего отделения`. +2. В верхней части страницы выберите дату или период. +3. Дождитесь обновления таблицы. + +## Как работает выбор периода + +В верхней части страницы расположен календарь. + +С его помощью можно выбрать: + +- одну дату; +- диапазон дат. + +После выбора периода система пересчитывает статистику по выбранному интервалу. + +### Быстрые действия + +Для быстрого перехода могут использоваться кнопки: + +- `Сегодня`; +- `Текущий год`. + +Они помогают быстро открыть самые частые сценарии просмотра данных. + +## Что отображается в таблице + +В таблице статистики могут присутствовать следующие показатели: + +- отделение; +- количество коек; +- поступило; +- выбыло; +- состоит; +- средний койко-день; +- предоперационный койко-день; +- процент загруженности; +- процент летальности; +- количество операций; +- число умерших; +- количество медицинского персонала. + +В отдельных строках система может показывать: + +- групповые заголовки; +- итоговые строки; +- индикатор наличия отчета по отделению. + +## Как открыть сводную из статистики + +1. Найдите нужное отделение в таблице. +2. Нажмите на его название. +3. Система откроет страницу сводной по выбранному отделению. + +При переходе сохраняется выбранный период, чтобы вы сразу видели данные за тот же интервал. + +## Нежелательные события и пациенты на контроле + +Напротив отделения могут отображаться дополнительные индикаторы: + +- значок нежелательных событий; +- значок пациентов на контроле. + +Нажатие на них открывает отдельное окно с подробностями. + +## Выгрузка в Excel + +Чтобы скачать статистику: + +1. Выберите нужную дату или период. +2. Нажмите `Сохранить в Excel`. +3. Дождитесь скачивания файла. + +Файл будет сформирован по текущему периоду, который выбран на странице в момент выгрузки. + +## Как правильно интерпретировать данные + +Перед анализом статистики убедитесь, что: + +- выбран корректный период; +- вы находитесь в нужной роли; +- данные относятся к нужному отделению; +- отчет за интересующую дату уже создан, если вы ожидаете увидеть итоговые значения по завершенному дню. + +## Когда данные могут отличаться от ожидаемых + +Чаще всего причина в одном из следующих факторов: + +- выбран не тот диапазон дат; +- открыта статистика по другому отделению; +- отчет за нужную дату еще не сформирован; +- пользователь работает под другой ролью. diff --git a/resources/css/app.css b/resources/css/app.css index 3281ba9..7ac934d 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -37,6 +37,38 @@ font-weight: 800; src: url("/fonts/Golos-Text_Black.woff2"); } + + body { + background-image: + radial-gradient( + circle at top center, + color-mix(in oklch, var(--primary) 10%, transparent) 0, + transparent 34rem + ), + radial-gradient( + circle at bottom left, + color-mix(in oklch, var(--chart-2) 12%, transparent) 0, + transparent 26rem + ); + min-height: 100vh; + } +} + +@layer utilities { + .grid-overlay { + background-image: + linear-gradient( + to right, + color-mix(in oklch, var(--color-neutral-800) 50%, transparent) 1px, + transparent 1px + ), + linear-gradient( + to bottom, + color-mix(in oklch, var(--color-neutral-800) 45%, transparent) 1px, + transparent 1px + ); + background-size: 40px 40px; + } } @theme { diff --git a/resources/js/Components/StartButton.vue b/resources/js/Components/StartButton.vue index f6d0270..12fbb72 100644 --- a/resources/js/Components/StartButton.vue +++ b/resources/js/Components/StartButton.vue @@ -1,5 +1,6 @@ diff --git a/resources/js/Layouts/Components/AppUserButton.vue b/resources/js/Layouts/Components/AppUserButton.vue index 163c002..a1ce493 100644 --- a/resources/js/Layouts/Components/AppUserButton.vue +++ b/resources/js/Layouts/Components/AppUserButton.vue @@ -2,7 +2,7 @@ import {useAuthStore} from "../../Stores/auth.js"; import {NSelect, NFlex, NText} from 'naive-ui' import {computed} from "vue"; -import {router, useForm} from "@inertiajs/vue3"; +import {useForm} from "@inertiajs/vue3"; const authStore = useAuthStore() const userOptions = computed(() => { @@ -18,15 +18,7 @@ const formRole = useForm({ role_id: authStore.user.role?.role_id }) const onChangeRole = (roleId) => { - formRole.post('/user/role/change', { - onSuccess: () => { - router.visit(window.location.pathname, { - preserveScroll: true, - preserveState: false, // Это важно - сбрасывает состояние - only: ['user', 'auth'], // Указываем какие данные обновить - }) - } - }) + formRole.post('/user/role/change') } const themeOverride = { diff --git a/resources/js/Pages/Index.vue b/resources/js/Pages/Index.vue index 925d6d9..ae731eb 100644 --- a/resources/js/Pages/Index.vue +++ b/resources/js/Pages/Index.vue @@ -1,31 +1,60 @@ + + diff --git a/resources/js/Pages/Report/Components/ManualPatientLinkModal.vue b/resources/js/Pages/Report/Components/ManualPatientLinkModal.vue new file mode 100644 index 0000000..e0f788a --- /dev/null +++ b/resources/js/Pages/Report/Components/ManualPatientLinkModal.vue @@ -0,0 +1,83 @@ + + + diff --git a/resources/js/Pages/Report/Components/ManualPatientModal.vue b/resources/js/Pages/Report/Components/ManualPatientModal.vue new file mode 100644 index 0000000..42e368a --- /dev/null +++ b/resources/js/Pages/Report/Components/ManualPatientModal.vue @@ -0,0 +1,191 @@ + + + diff --git a/resources/js/Pages/Report/Components/ManualPatientOperationsModal.vue b/resources/js/Pages/Report/Components/ManualPatientOperationsModal.vue new file mode 100644 index 0000000..2f99bd8 --- /dev/null +++ b/resources/js/Pages/Report/Components/ManualPatientOperationsModal.vue @@ -0,0 +1,412 @@ + + + + + diff --git a/resources/js/Pages/Report/Components/ManualPatientOutcomeModal.vue b/resources/js/Pages/Report/Components/ManualPatientOutcomeModal.vue new file mode 100644 index 0000000..8dce8df --- /dev/null +++ b/resources/js/Pages/Report/Components/ManualPatientOutcomeModal.vue @@ -0,0 +1,65 @@ + + + diff --git a/resources/js/Pages/Report/Components/MoveModalComment.vue b/resources/js/Pages/Report/Components/MoveModalComment.vue index d22952c..c25d579 100644 --- a/resources/js/Pages/Report/Components/MoveModalComment.vue +++ b/resources/js/Pages/Report/Components/MoveModalComment.vue @@ -5,7 +5,7 @@ import {ref, watch} from "vue"; const show = defineModel('show') const props = defineProps({ patientId: { - type: Number, + type: String, default: null } }) @@ -21,9 +21,7 @@ const rules = { } watch(() => props.patientId, (newPatientId) => { if (newPatientId) { - const patientIndex = reportStore.patientsData.observation.findIndex(itm => itm.id === newPatientId) - const patient = reportStore.patientsData.observation[patientIndex] - droppedPatient.value = patient + droppedPatient.value = reportStore.findPatientById(newPatientId) } }) diff --git a/resources/js/Pages/Report/Components/ReportFormInput.vue b/resources/js/Pages/Report/Components/ReportFormInput.vue index 4ffb65d..299b535 100644 --- a/resources/js/Pages/Report/Components/ReportFormInput.vue +++ b/resources/js/Pages/Report/Components/ReportFormInput.vue @@ -85,7 +85,7 @@ watch(() => formRef.value, (nv) => { style="--n-padding-top: 0; --n-padding-bottom: 0; --n-padding-left: 8px; --n-padding-right: 8px;">
- +
diff --git a/resources/js/Pages/Report/Components/ReportHeader.vue b/resources/js/Pages/Report/Components/ReportHeader.vue index 4d18c7b..9146892 100644 --- a/resources/js/Pages/Report/Components/ReportHeader.vue +++ b/resources/js/Pages/Report/Components/ReportHeader.vue @@ -70,8 +70,7 @@ const currentDate = computed(() => { + v-model:date="reportStore.timestampCurrentRange" /> diff --git a/resources/js/Pages/Report/Components/ReportSection.vue b/resources/js/Pages/Report/Components/ReportSection.vue index ee1e972..e51d592 100644 --- a/resources/js/Pages/Report/Components/ReportSection.vue +++ b/resources/js/Pages/Report/Components/ReportSection.vue @@ -1,10 +1,12 @@