From eb3ac66393897095c94ae52b3ae986e21e54eace Mon Sep 17 00:00:00 2001 From: brusnitsyn Date: Fri, 10 Jul 2026 17:14:08 +0900 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80?= =?UTF-8?q?=20=D0=BE=D1=82=D1=87=D0=B5=D1=82=D0=BE=D0=B2=20=D0=BC=D0=B5?= =?UTF-8?q?=D0=B4.=20=D1=81=D0=B5=D1=81=D1=82=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/GenerateNurseReport.php | 192 ++++++++++++++++++ .../Controllers/Web/NurseReportController.php | 1 - app/Services/NurseMedicalHistoryService.php | 70 +++++-- 3 files changed, 247 insertions(+), 16 deletions(-) create mode 100644 app/Console/Commands/GenerateNurseReport.php diff --git a/app/Console/Commands/GenerateNurseReport.php b/app/Console/Commands/GenerateNurseReport.php new file mode 100644 index 0000000..9b0973e --- /dev/null +++ b/app/Console/Commands/GenerateNurseReport.php @@ -0,0 +1,192 @@ +option('timezone') ?: config('app.timezone', 'Europe/Moscow'); + $shiftStartTime = $this->option('shift-start') ?: '09:00'; + + // 1. Валидация и парсинг дат + $startDate = Carbon::parse($this->option('start') ?: now($tz)->format('Y-m-d'), $tz)->setTimeFromTimeString($shiftStartTime); + $endDate = Carbon::parse($this->option('end') ?: $this->option('start') ?: now($tz)->format('Y-m-d'), $tz)->setTimeFromTimeString($shiftStartTime); + + if ($endDate->lt($startDate)) { + $this->error('Конечная дата не может быть раньше начальной.'); + return CommandAlias::FAILURE; + } + + // 2. Разрешение списка отделений + $departments = $this->resolveDepartments($this->option('departments')); + if ($departments->isEmpty()) { + $this->error('Отделения не найдены.'); + return CommandAlias::FAILURE; + } + + // 3. Генерация массива смен + $shifts = []; + $current = $startDate->copy(); + while ($current->lte($endDate)) { + $shifts[] = [ + 'start' => $current->copy(), + 'end' => $current->copy()->addDay(), // 09:00 → 09:00 следующего дня + ]; + $current->addDay(); + } + + // 4. Пользователь CLI + $userId = $this->option('user') ? (int) $this->option('user') : null; + if (!$userId && !$this->option('dry-run')) { + $this->error('Для записи в БД в CLI режиме укажите параметр --user='); + return CommandAlias::FAILURE; + } + + // 5. Вывод информации + $totalTasks = count($shifts) * $departments->count(); + if ($totalTasks === 0) { + $this->warn('Нет задач для выполнения.'); + return CommandAlias::SUCCESS; + } + + $this->info("Период: {$shifts[0]['start']->format('Y-m-d H:i')} → {$shifts[array_key_last($shifts)]['end']->format('Y-m-d H:i')} ({$tz})"); + $this->info("Отделений: {$departments->count()}"); + $this->info("Всего смен: {$totalTasks}"); + if ($this->option('dry-run')) $this->warn("Режим DRY RUN"); + if ($this->option('skip-existing')) $this->warn("Пропуск существующих отчётов включён"); + + $progressBar = $this->output->createProgressBar($totalTasks); + $progressBar->start(); + + $success = 0; $skipped = 0; $errors = 0; + + // 6. Основной цикл + foreach ($departments as $dept) { + foreach ($shifts as $shift) { + try { + $status = $this->processShift( + $shift['start'], $shift['end'], $dept, $userId, + $this->reportService, $this->dateRangeService + ); + + if ($status === 'skip' || $status === 'dry_run') { + $skipped++; + } else { + $success++; + } + } catch (Throwable $e) { + $errors++; + // Безопасное получение имени отделения + $deptName = $dept->name ?? $dept->department_name ?? "Отдел #{$dept->department_id}"; + $this->error("\n[{$deptName}] {$shift['start']->format('Y-m-d H:i')}: {$e->getMessage()}"); + Log::error('NurseReportShiftGeneration', [ + 'department_id' => $dept->department_id, + 'shift_start' => $shift['start']->format('Y-m-d H:i:s'), + 'shift_end' => $shift['end']->format('Y-m-d H:i:s'), + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + } finally { + // ✅ ГАРАНТИРУЕМ ровно 1 шаг прогресса на каждую итерацию + $progressBar->advance(); + } + } + } + + $progressBar->finish(); + $this->newLine(2); + $this->info("Завершено. Успешно: {$success} | Пропущено: {$skipped} | Ошибок: {$errors}"); + + return $errors > 0 ? CommandAlias::FAILURE : CommandAlias::SUCCESS; + } + + /** + * Получение списка отделений + */ + private function resolveDepartments($input) + { + if (!$input || strtolower($input) === 'all') { + return Department::orderBy('department_id')->get(); + } + + $ids = array_map('trim', explode(',', $input)); + return Department::whereIn('department_id', $ids)->get(); + } + + /** + * Обработка одной смены + * @return string 'success' | 'skip' | 'dry_run' + */ + private function processShift( + Carbon $shiftStart, + Carbon $shiftEnd, + $dept, + int $userId, + NurseReportService $reportService, + DateRangeService $dateRangeService + ): string { + $deptId = $dept->department_id; + $misDeptId = $dept->rf_mis_department_id; + + // Пропуск, если отчёт уже существует за ЭТУ ЖЕ СМЕНУ + if ($this->option('skip-existing')) { + $exists = ReportNurse::where('rf_department_id', $deptId) + ->where('period_start', $shiftStart->format('Y-m-d H:i:s')) + ->where('period_end', $shiftEnd->format('Y-m-d H:i:s')) + ->exists(); + if ($exists) { + return 'skip'; + } + } + + // Тестовый режим + if ($this->option('dry-run')) { + return 'dry_run'; + } + + // Формируем DateRange через ваш сервис (учёт смен, часовых поясов) + $user = User::find($userId); + $lpuDoctorId = 0; + $dateRange = $dateRangeService->createDateRangeForDate($shiftEnd, $user); + + // Цепочка из вашего контроллера + $report = $reportService->saveReport($dateRange, $userId, $lpuDoctorId, $deptId); + $stats = $reportService->saveSnapshot($dateRange, $report, $misDeptId, $userId); + + return 'success'; + } +} diff --git a/app/Http/Controllers/Web/NurseReportController.php b/app/Http/Controllers/Web/NurseReportController.php index 1839d76..dd58052 100644 --- a/app/Http/Controllers/Web/NurseReportController.php +++ b/app/Http/Controllers/Web/NurseReportController.php @@ -49,7 +49,6 @@ class NurseReportController extends Controller $reportsNurse = ReportNurse::where('rf_department_id', $departmentId) ->where('period_start', '>=', $dateRange->startSql()) ->where('period_end', '<=', $dateRange->endSql()) - ->where('rf_lpudoctor_id', $selectedUserId) ->orderBy('period_end', 'desc') ->with(['doctor']) ->get(); diff --git a/app/Services/NurseMedicalHistoryService.php b/app/Services/NurseMedicalHistoryService.php index 1ec1511..f0cd466 100644 --- a/app/Services/NurseMedicalHistoryService.php +++ b/app/Services/NurseMedicalHistoryService.php @@ -12,17 +12,23 @@ use Illuminate\Support\Collection; class NurseMedicalHistoryService { - public function getGroupedHistories(DateRange $dateRange, int $departmentId, ?array $reportIds = null): array + public function getGroupedHistories( + DateRange $dateRange, + int $departmentId, + ?array $reportIds = null, + ?string $search = null, + ): array { $startYear = $dateRange->startDate->copy()->startOfYear()->format('Y-m-d'); - $periodMigrationFilter = function ($q) use ($departmentId, $dateRange, $startYear) { $q->where('department_id', $departmentId) ->where('ingoing_date', '<=', $dateRange->endSql()) ->where(function ($sub) use ($dateRange, $startYear) { + // Миграции без out_date (еще лежат) $sub->whereNull('out_date') ->where('ingoing_date', '>', $startYear); + // Миграции с out_date (закрытые) $sub->orWhere(function ($sub2) use ($dateRange, $startYear) { $sub2->whereNotNull('out_date') ->where('out_date', '>', $dateRange->startSql()) @@ -30,7 +36,6 @@ class NurseMedicalHistoryService }); }); }; - $departmentMigrationFilter = function ($q) use ($departmentId) { $q->where('department_id', $departmentId) ->orderByDesc('ingoing_date'); @@ -38,16 +43,26 @@ class NurseMedicalHistoryService // 1. Один запрос: получаем "сырые" данные (без вычисляемых статусов) $all = ReportNursePatient::query() - ->when($reportIds, function ($query, $reportIds) { + ->when($reportIds !== null, function ($query) use ($reportIds) { return $query->whereIn('report_nurse_id', $reportIds); }) ->whereHas('migrations', $periodMigrationFilter) + ->when($search, function ($query, $search) { + // Поиск по ФИО (точное совпадение или LIKE) + return $query->where(function ($q) use ($search) { + $q->where('full_name', 'ilike', "%{$search}%"); // PostgreSQL + }); + }) ->with([ 'latestMigration' => $periodMigrationFilter, 'migrations' => $departmentMigrationFilter, + 'latestMigration.operations' => function ($q) use ($dateRange) { + $q->where('start_date', '>=', $dateRange->startSql()) + ->where('start_date', '<', $dateRange->endSql()); // по start_date + }, 'operations' => function ($q) use ($departmentId, $dateRange) { $q->where('department_id', $departmentId) - ->where('start_date', '>=', $dateRange->startSql()) + // ->where('start_date', '>=', $dateRange->startSql()) ->where('start_date', '<', $dateRange->endSql()) // Только операции пока пациент реально лежал в отделении ->whereExists(function ($sub) use ($departmentId) { @@ -62,15 +77,20 @@ class NurseMedicalHistoryService }); }); } - ]) + ]); + + $all = $all ->selectRaw('DISTINCT ON (original_id) report_nurse_patients.*') ->orderBy('original_id') ->orderBy('report_nurse_id', 'desc') ->get(); - // 2. Добавляем вычисляемые поля и превращаем в плоский массив + //dd($all->where('original_id', 334564)->first()); + + // Добавляем вычисляемые поля и превращаем в плоский массив $prepared = $all->map(function (ReportNursePatient $h) use ($dateRange) { $patientStatus = PatientStatusClassifier::classify($h, $dateRange); + $periodFlags = PatientStatusClassifier::classifyPeriodFlags($h, $dateRange); $patientUrgency = null; $patientReanimation = null; if (!in_array($patientStatus, [ @@ -79,16 +99,21 @@ class NurseMedicalHistoryService PatientStatusClassifier::STATUS_TRANSFERRED ])) { $patientUrgency = PatientStatusClassifier::classifyUrgency($h->urgency_id); + $patientReanimation = PatientStatusClassifier::classifyReanimation($h->latestMigration?->reanimations, $dateRange); } + return [ // Все исходные поля модели (автоматически через toArray) ...$h->toArray(), + 'operations' => $h->operations, // + вычисляемые мета-поля для фронтенда 'patient_status' => $patientStatus, 'patient_urgency' => $patientUrgency, + 'period_flags' => $periodFlags, 'in_reanimation' => $patientReanimation, 'admitted_today' => PatientStatusClassifier::classifyAdmitted($h->latestMigration?->ingoing_date, $dateRange), + 'in_observable' => PatientStatusClassifier::classifyObservable($h->observable, $dateRange), ]; }); @@ -97,14 +122,27 @@ class NurseMedicalHistoryService $sortOrder = 'desc'; $sorted = $prepared->sortBy($sortBy, SORT_REGULAR, $sortOrder === 'desc')->values(); + // Операции + $operations = $sorted->map(function ($h) { + return $h['latest_migration']['operations']; + })->flatten(1); + // 4. Возвращаем плоский массив + метаданные для фронтенда - $countInDepartment = $sorted->where('patient_status', 'in_department')->count(); - $countRecipient = $sorted->where('patient_status', 'recipient')->count(); - $countDischarged = $sorted->where('patient_status', 'discharged')->count(); - $countDeceased = $sorted->where('patient_status', 'deceased')->count(); - $countUrgent = $sorted->where('patient_urgency', 'urgent')->count(); - $countPlanned = $sorted->where('patient_urgency', 'planned')->count(); + $countInDepartment = $sorted->where('period_flags.current_at_end', true)->count(); + $countRecipient = $sorted->where('period_flags.recipient', true)->count(); + $countDischarged = $sorted->where('period_flags.discharged', true)->count(); + $countDeceased = $sorted->where('period_flags.deceased', true)->count(); + $countUrgent = $sorted + ->where('period_flags.current_at_end', true) + ->where('period_flags.urgent', true) + ->count(); + $countPlanned = $sorted + ->where('period_flags.current_at_end', true) + ->where('period_flags.planned', true) + ->count(); $countReanimations = $sorted->where('in_reanimation', true)->count(); + $countSurgPlanned = $operations->where('urgent_status', 6)->count(); + $countSurgUrgent = $operations->whereIn('urgent_status', [4, 5])->count(); // 4. Возвращаем плоский массив + метаданные для фронтенда return [ @@ -114,13 +152,15 @@ class NurseMedicalHistoryService 'sortBy' => $sortBy, 'sortOrder' => $sortOrder, 'counts' => [ - 'in_department' => $countInDepartment + $countRecipient, + 'in_department' => $countInDepartment, 'recipient' => $countRecipient, - 'discharged' => $countDischarged + $countDeceased, + 'discharged' => $countDischarged, 'deceased' => $countDeceased, 'urgent' => $countUrgent, 'planned' => $countPlanned, 'reanimations' => $countReanimations, + 'surgical_planned' => $countSurgPlanned, + 'surgical_urgent' => $countSurgUrgent, ] ] ];