From bb9e67ab3d15df728ff1f7dfc96401f78f65e95c Mon Sep 17 00:00:00 2001 From: brusnitsyn Date: Thu, 7 May 2026 18:00:43 +0900 Subject: [PATCH] =?UTF-8?q?*=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8=D0=BB?= =?UTF-8?q?=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B=20=D0=B2=20=D0=BE?= =?UTF-8?q?=D1=81=D0=BD=D0=BE=D0=B2=D0=BD=D0=BE=D0=BC=20=D0=BE=D1=82=D1=87?= =?UTF-8?q?=D0=B5=D1=82=D0=B5=20*=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20=D1=81=D0=BE=D1=85?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=BE=D1=81=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BE=D1=82=D1=87=D0=B5=D1=82?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Web/DutyReportController.php | 118 +++++++++ app/Http/Resources/MedicalHistoryResource.php | 22 ++ .../Reports/Sources/SnapshotPatientSource.php | 2 +- app/Models/MigrationPatient.php | 73 +++-- app/Models/ReportDuty.php | 42 +++ app/Models/ReportDutyMigrationPatient.php | 108 ++++++++ app/Models/ReportDutyPatient.php | 61 +++++ app/Services/DutyMedicalHistoryService.php | 116 ++++++++ app/Services/DutyReportService.php | 249 ++++++++++++++++++ app/Services/MedicalHistoryService.php | 46 +++- ...5_07_090900_create_report_duties_table.php | 47 ++++ ...0909_create_report_duty_patients_table.php | 43 +++ ...e_report_duty_migration_patients_table.php | 43 +++ resources/js/Components/AppContainer.vue | 2 +- resources/js/Components/AppPanel.vue | 2 +- .../DataTableColumns/ActionsColumn.vue | 69 +++++ .../DataTableColumns/IndexColumn.vue | 16 ++ .../DataTableColumns/OperationsColumn.vue | 24 ++ .../DataTableColumns/TooltipColumn.vue | 22 ++ .../Report/Components/PatientDataTable.vue | 86 ++++++ .../Report/Components/PatientSection.vue | 16 ++ .../Report/Components/PatientSectionItem.vue | 27 ++ .../Pages/Report/Components/ReportWidget.vue | 26 ++ resources/js/Pages/Report/Index.vue | 228 ++++++++++++++-- routes/web.php | 2 +- 25 files changed, 1438 insertions(+), 52 deletions(-) create mode 100644 app/Http/Controllers/Web/DutyReportController.php create mode 100644 app/Http/Resources/MedicalHistoryResource.php create mode 100644 app/Models/ReportDuty.php create mode 100644 app/Models/ReportDutyMigrationPatient.php create mode 100644 app/Models/ReportDutyPatient.php create mode 100644 app/Services/DutyMedicalHistoryService.php create mode 100644 app/Services/DutyReportService.php create mode 100644 database/migrations/2026_05_07_090900_create_report_duties_table.php create mode 100644 database/migrations/2026_05_07_090909_create_report_duty_patients_table.php create mode 100644 database/migrations/2026_05_07_090915_create_report_duty_migration_patients_table.php create mode 100644 resources/js/Pages/Report/Components/DataTableColumns/ActionsColumn.vue create mode 100644 resources/js/Pages/Report/Components/DataTableColumns/IndexColumn.vue create mode 100644 resources/js/Pages/Report/Components/DataTableColumns/OperationsColumn.vue create mode 100644 resources/js/Pages/Report/Components/DataTableColumns/TooltipColumn.vue create mode 100644 resources/js/Pages/Report/Components/PatientDataTable.vue create mode 100644 resources/js/Pages/Report/Components/PatientSection.vue create mode 100644 resources/js/Pages/Report/Components/PatientSectionItem.vue create mode 100644 resources/js/Pages/Report/Components/ReportWidget.vue diff --git a/app/Http/Controllers/Web/DutyReportController.php b/app/Http/Controllers/Web/DutyReportController.php new file mode 100644 index 0000000..56843fb --- /dev/null +++ b/app/Http/Controllers/Web/DutyReportController.php @@ -0,0 +1,118 @@ +query('departmentId', $user->department->department_id); + $department = Department::where('department_id', $departmentId)->firstOrFail(); + $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); + + // Проверяем, есть ли отчет за этот период + $isPastPeriod = $this->dateRangeService->isPastPeriod($dateRange); + $existsReport = ReportDuty::where('rf_department_id', $departmentId) + ->where('period_end', '>', $dateRange->startSql()) + ->where('period_end', '<=', $dateRange->endSql()) + ->exists(); + + $hasReport = $existsReport && $isPastPeriod; + + if ($hasReport) { + $inDepartmentHistories = $this->dutyMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id); + $recipientHistories = $this->dutyMedicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id); + $dischargedHistories = $this->dutyMedicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id); + $deceasedHistories = $this->dutyMedicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id); + $transferredHistories = $this->dutyMedicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id); + } else if ($this->dateRangeService->isPastPeriod($dateRange)) { + $inDepartmentHistories = collect([]); + $recipientHistories = collect([]); + $dischargedHistories = collect([]); + $deceasedHistories = collect([]); + $transferredHistories = collect([]); + } else { + $inDepartmentHistories = MedicalHistoryResource::collection( + $this->medicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id) + ); + $plannedHistories = MedicalHistoryResource::collection( + $this->medicalHistoryService->getPlannedHistories($dateRange, $department->rf_mis_department_id) + ); + $emergencyHistories = MedicalHistoryResource::collection( + $this->medicalHistoryService->getEmergencyHistories($dateRange, $department->rf_mis_department_id) + ); + $recipientHistories = MedicalHistoryResource::collection( + $this->medicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id) + ); + $dischargedHistories = MedicalHistoryResource::collection( + $this->medicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id) + ); + $deceasedHistories = MedicalHistoryResource::collection( + $this->medicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id) + ); + $transferredHistories = MedicalHistoryResource::collection( + $this->medicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id) + ); + } + + return Inertia::render('Report/Index', [ + 'inDepartmentHistories' => $inDepartmentHistories, + 'plannedHistories' => $plannedHistories, + 'emergencyHistories' => $emergencyHistories, + 'recipientHistories' => $recipientHistories, + 'dischargedHistories' => $dischargedHistories, + 'deceasedHistories' => $deceasedHistories, + 'transferredHistories' => $transferredHistories, + 'dates' => [ + $dateRange->startDate->getTimestampMs(), + $dateRange->endDate->getTimestampMs(), + ] + ]); + } + + /** + * Сохранение отчета от роли мед. сестра + * @return \Illuminate\Http\RedirectResponse + */ + public function store(Request $request) + { + $user = auth()->user(); + + $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); + $report = $this->dutyReportService->saveReport($dateRange); + $this->dutyReportService->saveSnapshot($dateRange, $report); + + return redirect()->back(); + } +} diff --git a/app/Http/Resources/MedicalHistoryResource.php b/app/Http/Resources/MedicalHistoryResource.php new file mode 100644 index 0000000..6cfc588 --- /dev/null +++ b/app/Http/Resources/MedicalHistoryResource.php @@ -0,0 +1,22 @@ + + */ + public function toArray(Request $request): array + { + return [ + ...$this->resource->toArray(), + 'admitted_today' => $this->latestMigration->admittedInCurrent + ]; + } +} diff --git a/app/Infrastructure/Reports/Sources/SnapshotPatientSource.php b/app/Infrastructure/Reports/Sources/SnapshotPatientSource.php index 632a18a..d5cccfb 100644 --- a/app/Infrastructure/Reports/Sources/SnapshotPatientSource.php +++ b/app/Infrastructure/Reports/Sources/SnapshotPatientSource.php @@ -182,7 +182,7 @@ class SnapshotPatientSource } return MedicalHistory::query() - ->whereIn('original_id', $historyIds) + ->whereIn('id', $historyIds) ->with(['operations']) ->get() ->mapWithKeys(function (MedicalHistory $history) { diff --git a/app/Models/MigrationPatient.php b/app/Models/MigrationPatient.php index b790d87..b0c4af8 100644 --- a/app/Models/MigrationPatient.php +++ b/app/Models/MigrationPatient.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Services\DateRange; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class MigrationPatient extends MaterializedViewModel @@ -11,6 +12,14 @@ class MigrationPatient extends MaterializedViewModel protected $table = 'mv_migrationpatient_details'; protected $primaryKey = 'id'; + protected $casts = [ + 'birth_date' => 'date:Y-m-d', + 'recipient_date' => 'datetime:Y-m-d H:i:s', + 'extract_date' => 'datetime:Y-m-d H:i:s', + 'death_date' => 'datetime:Y-m-d H:i:s', + 'male' => 'boolean', + ]; + public function medicalHistory() { return $this->belongsTo(MedicalHistory::class, 'medical_history_id', 'id'); @@ -18,7 +27,8 @@ class MigrationPatient extends MaterializedViewModel public function operations() { - return $this->hasMany(SurgicalOperation::class, 'migration_patient_id', 'id'); + return $this->hasMany(SurgicalOperation::class, 'migration_patient_id', 'id') + ->orderBy('end_date', 'desc'); } // Пересечение с отчетным периодом @@ -40,24 +50,6 @@ class MigrationPatient extends MaterializedViewModel return $query->whereIn('stationar_branch_id', $branchIds); } - // Добавляет вычисляемый столбец `category` (только для отображения) - public function scopeWithCategory($query, string $from, string $to) - { - $sql = "CASE - WHEN ingoing_date BETWEEN ? AND ? THEN 'admitted' - WHEN out_date BETWEEN ? AND ? THEN - CASE - WHEN death_date IS NOT NULL AND death_date BETWEEN ? AND ? THEN 'deceased' - WHEN stat_cure_result_id IN (3,4,7) THEN 'transferred' - ELSE 'discharged' - END - WHEN ingoing_date < ? AND (out_date IS NULL OR out_date > ?) THEN 'current' - ELSE 'historical' - END as category"; - - return $query->selectRaw($sql, [$from, $to, $from, $to, $from, $to, $to, $to]); - } - // Быстрые фильтры по статусам (используют индексы MV, а не computed column) public function scopeAdmitted($query, string $from, string $to) { @@ -89,7 +81,29 @@ class MigrationPatient extends MaterializedViewModel public function scopeCurrent($query, DateRange $dateRange) { - return $query->where('is_actually_current', true); + return $query->whereNull('out_date') + ->whereNotNull('medical_history_id') + ->where('ingoing_date', '<', $dateRange->startSql()) + ->wherehas('medicalHistory', function ($query) use ($dateRange) { + $query->whereNull('extract_date'); + }); + } + + public function scopeCurrentOrAdmitted($query, DateRange $dateRange) + { + return $query->where(function ($q) use ($dateRange) { + // Вариант А: Пациент уже лежит (текущий) + $q->whereNull('out_date') + ->whereNotNull('medical_history_id') + ->where('ingoing_date', '<', $dateRange->startSql()) + ->wherehas('medicalHistory', function ($query) use ($dateRange) { + $query->whereNull('extract_date'); + }); // опционально: поступил не раньше 2 лет назад + }) + ->orWhere(function ($q) use ($dateRange) { + $q->where('ingoing_date', '<=', $dateRange->endSql()) + ->where('ingoing_date', '>', $dateRange->startSql()); + }); } public function scopeCurrentMigration($query, $historyId, $departmentId) @@ -99,4 +113,23 @@ class MigrationPatient extends MaterializedViewModel ->orderBy('ingoing_date', 'desc') ->limit(1)->first(); } + + public function getAdmittedInCurrentAttribute(): bool + { + // Получаем дату поступления из последнего движения + $ingoing = $this->ingoing_date; + + if (!$ingoing) { + return false; + } + + $ingoingLocal = Carbon::parse($ingoing)->setTimezone(config('app.timezone', 'Europe/Moscow')); + $now = Carbon::now(config('app.timezone', 'Europe/Moscow')); + + // Окно смены: вчера 09:00 → сегодня 09:00 + $shiftStart = $now->copy()->subDay()->setTime(9, 0); + $shiftEnd = $now->copy()->setTime(9, 0); + + return $ingoingLocal->between($shiftStart, $shiftEnd); + } } diff --git a/app/Models/ReportDuty.php b/app/Models/ReportDuty.php new file mode 100644 index 0000000..95170ac --- /dev/null +++ b/app/Models/ReportDuty.php @@ -0,0 +1,42 @@ + 'date:Y-m-d', + 'sent_at' => 'datetime:Y-m-d H:i:s', + 'period_start' => 'datetime:Y-m-d H:i:s', + 'period_end' => 'datetime:Y-m-d H:i:s', + ]; + + public function department() + { + return $this->belongsTo(Department::class, 'rf_department_id', 'department_id'); + } + + public function patients() + { + return $this->hasMany(ReportNursePatient::class, 'report_nurse_id'); + } + + public function status() + { + return $this->belongsTo(ReportStatus::class); + } +} diff --git a/app/Models/ReportDutyMigrationPatient.php b/app/Models/ReportDutyMigrationPatient.php new file mode 100644 index 0000000..8cf4d5a --- /dev/null +++ b/app/Models/ReportDutyMigrationPatient.php @@ -0,0 +1,108 @@ + 'datetime:Y-m-d H:i:s', + 'out_date' => 'datetime:Y-m-d H:i:s', + ]; + + public function medicalHistory() + { + return $this->belongsTo(ReportNursePatient::class, 'medical_history_id', 'id'); + } + + public function operations() + { + return $this->hasMany(SurgicalOperation::class, 'migration_patient_id', 'id'); + } + + // Пересечение с отчетным периодом + public function scopeDateRange($query, string $from, string $to) + { + return $query->where('ingoing_date', '<=', $to) + ->where(function ($q) use ($from) { + $q->whereNull('out_date')->orWhere('out_date', '>=', $from); + }); + } + + // Фильтр по подразделению (получает ID отделений) + public function scopeDepartment($query, int $departmentId) + { + $branchIds = DB::table('stt_stationarbranch') + ->where('rf_DepartmentID', $departmentId) + ->pluck('StationarBranchID'); + + return $query->whereIn('stationar_branch_id', $branchIds); + } + + // Быстрые фильтры по статусам (используют индексы MV, а не computed column) + public function scopeAdmitted($query, string $from, string $to) + { + return $query->where('ingoing_date', '>', $from) + ->where('ingoing_date', '<=', $to); + } + + public function scopeDischarged($query, string $from, string $to) + { + return $query->where('out_date', '>', $from) + ->where('out_date', '<=', $to) + ->whereNotIn('visit_result_id', [3, 4, 5, 6]) + ->whereNull('death_date'); // умершие не считаются "выбывшими домой" + } + + public function scopeTransferred($query, string $from, string $to) + { + return $query->where('out_date', '>', $from) + ->where('out_date', '<=', $to) + ->whereIn('visit_result_id', [3, 4]); + } + + public function scopeDeceased($query, string $from, string $to) + { + return $query->whereNotNull('death_date') + ->where('death_date', '>', $from) + ->where('death_date', '<=', $to); + } + + public function scopeCurrent($query, DateRange $dateRange) + { + return $query->whereNull('out_date') + ->whereNotNull('medical_history_id') + ->where('ingoing_date', '<', $dateRange->startSql()) + ->whereHas('medicalHistory', function ($query) use ($dateRange) { + $query->whereNull('extract_date'); + }); + } + + public function scopeCurrentMigration($query, $historyId, $departmentId) + { + return $query->where('medical_history_id', $historyId) + ->department($departmentId) + ->orderBy('ingoing_date', 'desc') + ->limit(1)->first(); + } +} diff --git a/app/Models/ReportDutyPatient.php b/app/Models/ReportDutyPatient.php new file mode 100644 index 0000000..9de90a2 --- /dev/null +++ b/app/Models/ReportDutyPatient.php @@ -0,0 +1,61 @@ + 'date:Y-m-d', + 'recipient_date' => 'datetime:Y-m-d H:i:s', + 'extract_date' => 'datetime:Y-m-d H:i:s', + 'death_date' => 'datetime:Y-m-d H:i:s', + 'male' => 'boolean', + ]; + + public function migrations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(ReportDutyMigrationPatient::class, 'medical_history_id', 'id'); + } + + public function operations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(SurgicalOperation::class, 'medical_history_id', 'id'); + } + + public function latestMigration() + { + return $this->hasOne(ReportDutyMigrationPatient::class, 'medical_history_id', 'id') + ->latest('ingoing_date'); + } + + public function operationsInDepartment($query, $departmentId) + { + return $this->operations()->where('department_id', $departmentId); + } + + // Скоупы + public function scopeUrgency($query, $typeId) // 1 = Экстренно, 2 = Планово + { + return $query->where('urgency_id', $typeId); + } +} diff --git a/app/Services/DutyMedicalHistoryService.php b/app/Services/DutyMedicalHistoryService.php new file mode 100644 index 0000000..fa0f496 --- /dev/null +++ b/app/Services/DutyMedicalHistoryService.php @@ -0,0 +1,116 @@ +where('recipient_date', '>=', $dateRange->startSql()) + ->where('recipient_date', '<', $dateRange->endSql()) + // 1. Оставляем только тех пациентов, у которых БЫЛО движение в этом отделении + ->whereHas('latestMigration', fn($q) => $q->where('department_id', $departmentId)) + + // 2. Загружаем ТОЛЬКО последнее движение в этом отделении (не все миграции) + ->with(['latestMigration' => fn($q) => $q->where('department_id', $departmentId)]); + + $result = $query->paginate(); + + return $result; + } + + public function getUrgencyHistory(DateRange $dateRange, int $departmentId, int $urgencyId) + { + $query = ReportDutyPatient::query(); + + $query->where('recipient_date', '>=', $dateRange->startSql()) + ->where('recipient_date', '<', $dateRange->endSql()) + ->urgency($urgencyId) + ->whereHas('migrations', function ($m) use ($departmentId) { + $m->where('department_id', $departmentId); + }) + ->with([ + 'migrations' => fn ($m) => $m->where('department_id', $departmentId), + 'migrations.operations' + ]); + + $result = $query->paginate(); + + return $result; + } + + public function getDepartmentHistories(DateRange $dateRange, int $departmentId) + { + return ReportDutyPatient::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->current($dateRange); + }) + ->with(['latestMigration' => function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->current($dateRange); // подгружаем только отфильтрованные движения + }]) + ->get() + // Сортировка по дате поступления в отделение (поле дочерней таблицы) + ->sortByDesc(fn ($mh) => $mh->latestMigration->ingoing_date ?? $mh->recipient_date) + ->values(); + } + + /** + * Получить карты поступившие сегодня + * @param DateRange $dateRange + * @param int $departmentId + */ + public function getRecipientHistories(DateRange $dateRange, int $departmentId) + { + $now = Carbon::now(); + + return ReportDutyPatient::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->admitted($dateRange->startSql(), $dateRange->endSql()); + }) + ->with(['latestMigration' => function ($q) use ($departmentId) { + $q->department($departmentId); + }]) + ->get(); + } + + public function getDischargedHistories(DateRange $dateRange, int $departmentId) + { + return ReportDutyPatient::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->discharged($dateRange->startSql(), $dateRange->endSql()); + }) + ->with(['latestMigration' => function ($q) use ($departmentId) { + $q->department($departmentId); + }]) + ->get(); + } + + public function getDeceasedHistories(DateRange $dateRange, int $departmentId) + { + return ReportDutyPatient::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->deceased($dateRange->startSql(), $dateRange->endSql()); + }) + ->with(['latestMigration' => function ($q) use ($departmentId) { + $q->department($departmentId); + }]) + ->get(); + } + + public function getTransferredHistories(DateRange $dateRange, int $departmentId) + { + return ReportDutyPatient::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->transferred($dateRange->startSql(), $dateRange->endSql()); + }) + ->with(['latestMigration' => function ($q) use ($departmentId) { + $q->department($departmentId); + }]) + ->get(); + } +} diff --git a/app/Services/DutyReportService.php b/app/Services/DutyReportService.php new file mode 100644 index 0000000..6fe2dc2 --- /dev/null +++ b/app/Services/DutyReportService.php @@ -0,0 +1,249 @@ +whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->dateRange($dateRange->startSql(), $dateRange->endSql()); + }) + ->with(['latestMigration' => function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->dateRange($dateRange->startSql(), $dateRange->endSql()); + }]); + } + + /** + * Сохранить отчет + */ + public function saveReport(DateRange $dateRange, ?int $userId = null, ?int $lpuDoctorId = null, ?int $departmentId = null) + { + $user = $userId ? User::find($userId) : auth()->user(); + + $lpuDoctorId = $lpuDoctorId ?? $user->rf_lpudoctor_id; + $departmentId = $departmentId ?? $user->rf_department_id; + + $data = [ + 'report_date' => Carbon::now()->format('Y-m-d'), + 'sent_at' => Carbon::now()->format('Y-m-d H:i:s'), + 'period_type' => 'day', + 'period_start' => $dateRange->startSql(), + 'period_end' => $dateRange->endSql(), + 'status_id' => 2, // опубликован + 'rf_lpudoctor_id' => $lpuDoctorId, + 'rf_department_id' => $departmentId, + 'rf_user_id' => $user->id, + ]; + + $report = ReportDuty::updateOrCreate( + [ + 'report_date' => $data['report_date'], 'period_start' => $data['period_start'], + 'period_end' => $data['period_end'] + ], + $data + ); + + return $report; + } + + /** + * Сохранить снимок пациентов за период + */ + public function saveSnapshot(DateRange $dateRange, ReportNurse $reportNurse, ?int $departmentId = null, ?int $userId = null): array + { + $departmentId = $departmentId ?? $reportNurse->department->rf_mis_department_id; + $userId = $userId ?? $reportNurse->rf_user_id; + $startYear = Carbon::now()->startOfYear()->format('Y-m-d'); + + $query = MedicalHistory::query() + // Фильтруем движения по отделению + пересечение дат + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange, $startYear) { + $q->where('department_id', $departmentId) + // пребывание пересекается с отчётным периодом + ->where('ingoing_date', '<=', $dateRange->endSql()) + ->where('ingoing_date', '>=', $startYear) + ->where(function ($sub) use ($dateRange) { + $sub->whereNull('out_date') + ->orWhere('out_date', '>=', $dateRange->startSql()) + ->where('out_date', '<=', $dateRange->endSql()); + }); + }); + + // Получаем данные (chunk для памяти, если пациентов > 1000) + $patients = $query->cursor(); + + $savedStats = $this->saveReportSnapshot($reportNurse->id, $patients, $userId); + + return [ + ...$savedStats, + 'report_date' => $dateRange->startSql(), + 'department_id' => $departmentId, + ]; + } + + public function saveReportSnapshot(int $reportDutyId, iterable $patients, int $userId): array + { + if (empty($patients)) { + return ['saved_patients' => 0, 'saved_migrations' => 0]; + } + + $patientBatch = []; + $migrationBatch = []; + $batchSize = 100; + + foreach ($patients as $patient) { + // Подготовка данных пациента + $patientBatch[] = [ + 'report_nurse_id' => $reportDutyId, + 'source_type' => $patient->source_type, + 'original_id' => $patient->original_id, + 'medical_card_number' => $patient->medical_card_number, + 'full_name' => $patient->full_name, + 'birth_date' => $patient->birth_date, + 'recipient_date' => $patient->recipient_date, + 'extract_date' => $patient->extract_date, + 'death_date' => $patient->death_date, + 'male' => $patient->male, + 'urgency_id' => $patient->urgency_id, + 'hospital_result_id' => $patient->hospital_result_id, + 'visit_result_id' => $patient->visit_result_id, + 'comment' => $patient->comment, + 'user_id' => $userId, + ]; + + // Подготовка данных миграции (если есть) + if (!empty($patient->migrations)) { + foreach ($patient->migrations as $migration) { + $migrationBatch[] = [ + // Временный ключ для связи с пациентом (заполним после первого upsert) + '_temp_key' => [ + 'report_duty_id' => $reportDutyId, + 'source_type' => $patient->source_type, + 'original_id' => $patient->original_id, + ], + 'ingoing_date' => $migration->ingoing_date, + 'out_date' => $migration->out_date, + 'diagnosis_id' => $migration->diagnosis_id, + 'diagnosis_code' => $migration->diagnosis_code, + 'diagnosis_name' => $migration->diagnosis_name, + 'interrupted_event_id' => $migration->interrupted_event_id, + 'stationar_branch_id' => $migration->stationar_branch_id, + 'department_id' => $migration->department_id, + 'visit_result_id' => $migration->visit_result_id, + 'stat_cure_result_id' => $migration->stat_cure_result_id, + 'user_id' => $migration->user_id, + 'mis_user_id' => $migration->mis_user_id, + 'comment' => $migration->comment, + ]; + } + } + + // Пакетная запись каждые $batchSize записей + if (count($patientBatch) >= $batchSize) { + [$savedP, $savedM] = $this->upsertBatches($reportDutyId, $patientBatch, $migrationBatch); + $patientBatch = []; + $migrationBatch = []; + } + } + + // Сохраняем остаток + [$savedP, $savedM] = $this->upsertBatches($reportDutyId, $patientBatch, $migrationBatch); + + return ['saved_patients' => $savedP, 'saved_migrations' => $savedM]; + } + + /** + * Вспомогательный метод: выполняет upsert для пациентов и миграций + */ + private function upsertBatches(int $reportDutyId, array $patientBatch, array $migrationBatch): array + { + if (empty($patientBatch)) { + return [0, 0]; + } + + $savedPatients = 0; + $savedMigrations = 0; + + DB::transaction(function () use ($reportDutyId, $patientBatch, $migrationBatch, &$savedPatients, &$savedMigrations) { + // UPSERT пациентов + $patientUniqueBy = ['report_duty_id', 'source_type', 'original_id']; + $patientUpdateColumns = array_diff(array_keys($patientBatch[0]), $patientUniqueBy); + + DB::table('report_nurse_patients')->upsert( + $patientBatch, + $patientUniqueBy, + $patientUpdateColumns + ); + $savedPatients = count($patientBatch); + + // Получаем ID сохранённых пациентов для связи с миграциями + if (!empty($migrationBatch)) { + // Извлекаем уникальные ключи для поиска + $tempKeys = array_map(fn($m) => $m['_temp_key'], $migrationBatch); + + // Получаем реальные ID из БД + $patientIds = DB::table('report_duty_patients') + ->whereIn('report_duty_id', [$reportDutyId]) + ->get() + ->pluck('id', 'original_id') // key=original_id, value=id + ->toArray(); + + // Формируем финальный массив миграций с реальными medical_history_id + $finalMigrations = []; + foreach ($migrationBatch as $m) { + $tempKey = $m['_temp_key']; + $originalId = $tempKey['original_id']; + + if (isset($patientIds[$originalId])) { + $finalMigrations[] = [ + 'medical_history_id' => $patientIds[$originalId], // Реальный ID + 'ingoing_date' => $m['ingoing_date'], + 'out_date' => $m['out_date'], + 'diagnosis_id' => $m['diagnosis_id'], + 'diagnosis_code' => $m['diagnosis_code'], + 'diagnosis_name' => $m['diagnosis_name'], + 'interrupted_event_id' => $m['interrupted_event_id'], + 'stationar_branch_id' => $m['stationar_branch_id'], + 'department_id' => $m['department_id'], + 'visit_result_id' => $m['visit_result_id'], + 'stat_cure_result_id' => $m['stat_cure_result_id'], + 'user_id' => $m['user_id'], + 'mis_user_id' => $m['mis_user_id'], + 'comment' => $m['comment'], + ]; + } + } + + if (!empty($finalMigrations)) { + // UPSERT миграций + $migrationUniqueBy = ['medical_history_id', 'ingoing_date']; + $migrationUpdateColumns = array_diff(array_keys($finalMigrations[0]), $migrationUniqueBy); + + DB::table('report_duty_migration_patients')->upsert( + $finalMigrations, + $migrationUniqueBy, + $migrationUpdateColumns + ); + $savedMigrations = count($finalMigrations); + } + } + }); + + return [$savedPatients, $savedMigrations]; + } +} diff --git a/app/Services/MedicalHistoryService.php b/app/Services/MedicalHistoryService.php index 164fc32..fb5000a 100644 --- a/app/Services/MedicalHistoryService.php +++ b/app/Services/MedicalHistoryService.php @@ -49,11 +49,43 @@ class MedicalHistoryService { return MedicalHistory::query() ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { - $q->department($departmentId)->current($dateRange); + $q->department($departmentId)->currentOrAdmitted($dateRange); }) ->with(['latestMigration' => function ($q) use ($departmentId, $dateRange) { - $q->department($departmentId)->current($dateRange); // подгружаем только отфильтрованные движения - }]) + $q->department($departmentId)->currentOrAdmitted($dateRange)->latest('ingoing_date'); // подгружаем только отфильтрованные движения + }, 'latestMigration.operations']) + ->get() + // Сортировка по дате поступления в отделение (поле дочерней таблицы) + ->sortByDesc(fn ($mh) => $mh->latestMigration->ingoing_date ?? $mh->recipient_date) + ->values(); + } + + public function getPlannedHistories(DateRange $dateRange, int $departmentId) + { + return MedicalHistory::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->currentOrAdmitted($dateRange); + }) + ->with(['latestMigration' => function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->currentOrAdmitted($dateRange) + ->latest('ingoing_date'); // подгружаем только отфильтрованные движения + }, 'latestMigration.operations']) + ->urgency(1) + ->get() + // Сортировка по дате поступления в отделение (поле дочерней таблицы) + ->sortByDesc(fn ($mh) => $mh->latestMigration->ingoing_date ?? $mh->recipient_date) + ->values(); + } + public function getEmergencyHistories(DateRange $dateRange, int $departmentId) + { + return MedicalHistory::query() + ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->currentOrAdmitted($dateRange); + }) + ->with(['latestMigration' => function ($q) use ($departmentId, $dateRange) { + $q->department($departmentId)->currentOrAdmitted($dateRange)->latest('ingoing_date'); // подгружаем только отфильтрованные движения + }, 'latestMigration.operations']) + ->urgency(2) ->get() // Сортировка по дате поступления в отделение (поле дочерней таблицы) ->sortByDesc(fn ($mh) => $mh->latestMigration->ingoing_date ?? $mh->recipient_date) @@ -75,7 +107,7 @@ class MedicalHistoryService }) ->with(['latestMigration' => function ($q) use ($departmentId) { $q->department($departmentId); - }]) + }, 'latestMigration.operations']) ->get(); } @@ -87,7 +119,7 @@ class MedicalHistoryService }) ->with(['latestMigration' => function ($q) use ($departmentId) { $q->department($departmentId); - }]) + }, 'latestMigration.operations']) ->get(); } @@ -99,7 +131,7 @@ class MedicalHistoryService }) ->with(['latestMigration' => function ($q) use ($departmentId) { $q->department($departmentId); - }]) + }, 'latestMigration.operations']) ->get(); } @@ -111,7 +143,7 @@ class MedicalHistoryService }) ->with(['latestMigration' => function ($q) use ($departmentId) { $q->department($departmentId); - }]) + }, 'latestMigration.operations']) ->get(); } } diff --git a/database/migrations/2026_05_07_090900_create_report_duties_table.php b/database/migrations/2026_05_07_090900_create_report_duties_table.php new file mode 100644 index 0000000..8f0f1af --- /dev/null +++ b/database/migrations/2026_05_07_090900_create_report_duties_table.php @@ -0,0 +1,47 @@ +id(); + $table->date('report_date'); + $table->dateTime('sent_at')->nullable(); + + $table->string('period_type')->default('day') // day|week|month|year + ->comment('Тип отчетного периода'); + $table->dateTime('period_start')->nullable() + ->comment('Начало отчетного периода'); + $table->dateTime('period_end')->nullable() + ->comment('Окончание отчетного периода'); + + $table->integer('report_month')->storedAs('EXTRACT(MONTH FROM created_at)::integer') + ->comment('Отчетный месяц'); + $table->integer('report_year')->storedAs('EXTRACT(YEAR FROM created_at)::integer') + ->comment('Отчетный год'); + + $table->foreignIdFor(\App\Models\ReportStatus::class, 'status_id')->default(1); + + $table->foreignIdFor(\App\Models\MisLpuDoctor::class, 'rf_lpudoctor_id')->nullable(); + $table->foreignIdFor(\App\Models\Department::class, 'rf_department_id')->default(1); + $table->foreignIdFor(\App\Models\User::class, 'rf_user_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('report_duties'); + } +}; diff --git a/database/migrations/2026_05_07_090909_create_report_duty_patients_table.php b/database/migrations/2026_05_07_090909_create_report_duty_patients_table.php new file mode 100644 index 0000000..6f0e66e --- /dev/null +++ b/database/migrations/2026_05_07_090909_create_report_duty_patients_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignIdFor(\App\Models\ReportDuty::class, 'report_duty_id'); + $table->string('source_type'); + $table->bigInteger('original_id'); + $table->string('medical_card_number')->nullable(); + $table->string('full_name'); + $table->date('birth_date')->nullable(); + $table->dateTime('recipient_date'); + $table->dateTime('extract_date')->nullable(); + $table->dateTime('death_date')->nullable(); + $table->boolean('male')->default(true); + $table->integer('urgency_id')->nullable(); + $table->integer('hospital_result_id')->nullable(); + $table->integer('visit_result_id')->nullable(); + $table->text('comment')->nullable(); + $table->foreignIdFor(\App\Models\User::class, 'user_id'); + $table->unique(['report_duty_id', 'source_type', 'original_id']); // защита от дублей + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('report_duty_patients'); + } +}; diff --git a/database/migrations/2026_05_07_090915_create_report_duty_migration_patients_table.php b/database/migrations/2026_05_07_090915_create_report_duty_migration_patients_table.php new file mode 100644 index 0000000..41014ec --- /dev/null +++ b/database/migrations/2026_05_07_090915_create_report_duty_migration_patients_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignIdFor( \App\Models\ReportDutyPatient::class, 'medical_history_id'); + $table->dateTime('ingoing_date')->nullable(); + $table->dateTime('out_date')->nullable(); + $table->integer('diagnosis_id')->nullable(); + $table->string('diagnosis_code')->nullable(); + $table->string('diagnosis_name')->nullable(); + $table->integer('interrupted_event_id')->nullable(); + $table->integer('stationar_branch_id')->nullable(); + $table->integer('department_id')->nullable(); + $table->integer('visit_result_id')->nullable(); + $table->integer('stat_cure_result_id')->nullable(); + $table->foreignIdFor(\App\Models\User::class, 'user_id')->nullable(); + $table->integer('mis_user_id')->nullable(); + $table->text('comment')->nullable(); + $table->timestamps(); + + $table->unique(['medical_history_id', 'ingoing_date'], 'uniq_rdmp_medical_history_id_and_ingoing_date'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('report_duty_migration_patients'); + } +}; diff --git a/resources/js/Components/AppContainer.vue b/resources/js/Components/AppContainer.vue index 0153f3a..cd19f63 100644 --- a/resources/js/Components/AppContainer.vue +++ b/resources/js/Components/AppContainer.vue @@ -3,7 +3,7 @@ diff --git a/resources/js/Components/AppPanel.vue b/resources/js/Components/AppPanel.vue index 9331b39..eeeb82f 100644 --- a/resources/js/Components/AppPanel.vue +++ b/resources/js/Components/AppPanel.vue @@ -61,7 +61,7 @@ watch(() => [props.minH, props.maxH], ([minH, maxH]) => {