From 90e0d04dfda1e0aea83b9c98980de875350d7f21 Mon Sep 17 00:00:00 2001 From: brusnitsyn Date: Fri, 8 May 2026 17:04:56 +0900 Subject: [PATCH] =?UTF-8?q?*=20=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BB=20=D0=B7=D0=B0=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D1=8B=20=D0=B2=D1=8B=D0=B4=D0=B0=D1=87=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B0=D1=86=D0=B8=D0=B5=D0=BD=D1=82=D0=BE=D0=B2,=20?= =?UTF-8?q?=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D0=BD=D0=B0=D0=BF=D1=88=D0=BE=D1=82=D0=BE=D0=B2=20*=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=BB=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=83=20=D0=BE=D1=82=D1=87?= =?UTF-8?q?=D0=B5=D1=82=D0=B0=20=D0=B4=D0=B5=D0=B6=D1=83=D1=80=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20*=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BB=20"=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8?= =?UTF-8?q?=D1=8F"=20=D0=BD=D0=B0=D0=B4=20=D0=BF=D0=B0=D1=86=D0=B8=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=BE=D0=BC=20*=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B8=D0=BB=20=D0=B2=D0=B8=D0=B4=D0=B6=D0=B5=D1=82?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D0=B5=20=D0=BE=D1=82=D1=87=D0=B5=D1=82=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=B5=D0=B6=D1=83=D1=80=D0=BD=D0=BE=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Web/DutyReportController.php | 53 +--- .../Controllers/Web/NurseReportController.php | 46 ++- app/Models/MigrationPatient.php | 2 +- app/Models/UnifiedMigrationPatient.php | 20 ++ .../PatientStatusClassifier.php | 84 +++++ app/Services/DutyReportService.php | 16 +- app/Services/MedicalHistoryService.php | 64 ++++ app/Services/UnifiedMedicalHistoryService.php | 87 ++++-- package-lock.json | 6 - .../js/Composables/useDropdownActions.js | 40 +++ resources/js/Composables/usePatientColumns.js | 200 ++++++++++++ resources/js/Pages/Nurse/Report/Index.vue | 65 ++-- .../DataTableColumns/ActionsColumn.vue | 124 +++++--- .../DataTableColumns/OperationsColumn.vue | 2 +- .../Pages/Report/Components/ReportHeader.vue | 1 - resources/js/Pages/Report/Index.vue | 293 ++++++++++-------- routes/web.php | 7 + 17 files changed, 818 insertions(+), 292 deletions(-) create mode 100644 app/Services/Classification/PatientStatusClassifier.php create mode 100644 resources/js/Composables/useDropdownActions.js create mode 100644 resources/js/Composables/usePatientColumns.js diff --git a/app/Http/Controllers/Web/DutyReportController.php b/app/Http/Controllers/Web/DutyReportController.php index 05373ad..12cb37c 100644 --- a/app/Http/Controllers/Web/DutyReportController.php +++ b/app/Http/Controllers/Web/DutyReportController.php @@ -52,52 +52,31 @@ class DutyReportController extends Controller if ($hasReport) { $inDepartmentHistories = $this->dutyMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id); + $plannedHistories = collect([ 'data' => [] ]); + $emergencyHistories = collect([ 'data' => [] ]); $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); + $reanimationHistories = collect([ 'data' => [] ]); } else if ($this->dateRangeService->isPastPeriod($dateRange)) { - $inDepartmentHistories = collect([]); - $recipientHistories = collect([]); - $dischargedHistories = collect([]); - $deceasedHistories = collect([]); - $transferredHistories = collect([]); + $inDepartmentHistories = collect([ 'data' => [] ]); + $plannedHistories = collect([ 'data' => [] ]); + $emergencyHistories = collect([ 'data' => [] ]); + $recipientHistories = collect([ 'data' => [] ]); + $dischargedHistories = collect([ 'data' => [] ]); + $deceasedHistories = collect([ 'data' => [] ]); + $transferredHistories = collect([ 'data' => [] ]); + $reanimationHistories = collect([ 'data' => [] ]); } 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) - ); - $reanimationHistories = MedicalHistoryResource::collection( - $this->medicalHistoryService->getReanimationHistories($dateRange, $department->rf_mis_department_id) - ); + $patients = $this->medicalHistoryService->getGroupedHistories($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, - 'reanimationHistories' => $reanimationHistories, + 'patients' => $patients, + 'departmentInfo' => [ + // TODO: Добавить вывод информации из шапки + ], 'dates' => [ $dateRange->startDate->getTimestampMs(), $dateRange->endDate->getTimestampMs(), diff --git a/app/Http/Controllers/Web/NurseReportController.php b/app/Http/Controllers/Web/NurseReportController.php index 53df40f..4c2cfad 100644 --- a/app/Http/Controllers/Web/NurseReportController.php +++ b/app/Http/Controllers/Web/NurseReportController.php @@ -44,32 +44,30 @@ class NurseReportController extends Controller $hasReport = $existsReport && $isPastPeriod; - if ($hasReport) { - $inDepartmentHistories = $this->nurseMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id); - $recipientHistories = $this->nurseMedicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id); - $dischargedHistories = $this->nurseMedicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id); - $deceasedHistories = $this->nurseMedicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id); - $transferredHistories = $this->nurseMedicalHistoryService->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 = $this->unifiedMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id); - $recipientHistories = $this->unifiedMedicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id); - $dischargedHistories = $this->unifiedMedicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id); - $deceasedHistories = $this->unifiedMedicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id); - $transferredHistories = $this->unifiedMedicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id); - } +// if ($hasReport) { +// $inDepartmentHistories = $this->nurseMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id); +// $recipientHistories = $this->nurseMedicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id); +// $dischargedHistories = $this->nurseMedicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id); +// $deceasedHistories = $this->nurseMedicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id); +// $transferredHistories = $this->nurseMedicalHistoryService->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 = $this->unifiedMedicalHistoryService->getDepartmentHistories($dateRange, $department->rf_mis_department_id); +// $recipientHistories = $this->unifiedMedicalHistoryService->getRecipientHistories($dateRange, $department->rf_mis_department_id); +// $dischargedHistories = $this->unifiedMedicalHistoryService->getDischargedHistories($dateRange, $department->rf_mis_department_id); +// $deceasedHistories = $this->unifiedMedicalHistoryService->getDeceasedHistories($dateRange, $department->rf_mis_department_id); +// $transferredHistories = $this->unifiedMedicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id); +// } + + $data = $this->unifiedMedicalHistoryService->getGroupedHistories($dateRange, $department->rf_mis_department_id); return Inertia::render('Nurse/Report/Index', [ - 'inDepartmentHistories' => $inDepartmentHistories, - 'recipientHistories' => $recipientHistories, - 'dischargedHistories' => $dischargedHistories, - 'deceasedHistories' => $deceasedHistories, - 'transferredHistories' => $transferredHistories, + 'patients' => $data, 'dates' => [ $dateRange->startDate->getTimestampMs(), $dateRange->endDate->getTimestampMs(), diff --git a/app/Models/MigrationPatient.php b/app/Models/MigrationPatient.php index 01170a9..2115e76 100644 --- a/app/Models/MigrationPatient.php +++ b/app/Models/MigrationPatient.php @@ -104,7 +104,7 @@ class MigrationPatient extends MaterializedViewModel ->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()) diff --git a/app/Models/UnifiedMigrationPatient.php b/app/Models/UnifiedMigrationPatient.php index 1c7935d..bdc60a3 100644 --- a/app/Models/UnifiedMigrationPatient.php +++ b/app/Models/UnifiedMigrationPatient.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 UnifiedMigrationPatient extends Model @@ -89,4 +90,23 @@ class UnifiedMigrationPatient extends Model ->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/Services/Classification/PatientStatusClassifier.php b/app/Services/Classification/PatientStatusClassifier.php new file mode 100644 index 0000000..511b888 --- /dev/null +++ b/app/Services/Classification/PatientStatusClassifier.php @@ -0,0 +1,84 @@ + self::URGENCY_URGENT, + 2 => self::URGENCY_PLANNED, + default => self::URGENCY_UNKNOWN, + }; + } + + /** + * Определяет поступил ли пациент в диапазоне. + */ + public static function classifyAdmitted(Carbon|string|null $ingoingDate): bool + { + if (is_null($ingoingDate)) {return false;} + + $ingoingLocal = Carbon::parse($ingoingDate); + $now = Carbon::now(); + + // Окно смены: вчера 09:00 → сегодня 09:00 + $shiftStart = $now->copy()->subDay()->setTime(9, 0); + $shiftEnd = $now->copy()->setTime(9, 0); + + return $ingoingLocal->between($shiftStart, $shiftEnd); + } + + /** + * Определяет статус пациента на основе "сырых" полей из БД. + * Логика изолирована и может быть легко протестирована. + */ + public static function classify(UnifiedMedicalHistory|MedicalHistory $history, DateRange $dateRange): string + { + // 1. Смерть — приоритет №1 (не зависит от дат) + if (!empty($history->death_date)) { + return self::STATUS_DECEASED; + } + + // 2. Если есть дата выбытия + if (!empty($history->extract_date)) { + // Переведён (коды 3, 4) + if (in_array($history->visit_result_id, [3, 4], true)) { + return self::STATUS_TRANSFERRED; + } + // Выписан домой/иное (исключаем коды 3-6) + if (in_array($history->visit_result_id, [1], true)) { + return self::STATUS_DISCHARGED; + } + } + + // 3. Поступившие + if ($history->latestMigration?->getAdmittedInCurrentAttribute()) { + return self::STATUS_RECIPIENT; + } + + // 4. В отделении + if (empty($history->latestMigration?->out_date) && $history->latestMigration?->ingoing_date < $dateRange->startDate) { + return self::STATUS_IN_DEPARTMENT; + } + + return 'unknown'; + } +} diff --git a/app/Services/DutyReportService.php b/app/Services/DutyReportService.php index 6fe2dc2..c2f05ce 100644 --- a/app/Services/DutyReportService.php +++ b/app/Services/DutyReportService.php @@ -64,10 +64,10 @@ class DutyReportService /** * Сохранить снимок пациентов за период */ - public function saveSnapshot(DateRange $dateRange, ReportNurse $reportNurse, ?int $departmentId = null, ?int $userId = null): array + public function saveSnapshot(DateRange $dateRange, ReportDuty $reportDuty, ?int $departmentId = null, ?int $userId = null): array { - $departmentId = $departmentId ?? $reportNurse->department->rf_mis_department_id; - $userId = $userId ?? $reportNurse->rf_user_id; + $departmentId = $departmentId ?? $reportDuty->department->rf_mis_department_id; + $userId = $userId ?? $reportDuty->rf_user_id; $startYear = Carbon::now()->startOfYear()->format('Y-m-d'); $query = MedicalHistory::query() @@ -87,7 +87,7 @@ class DutyReportService // Получаем данные (chunk для памяти, если пациентов > 1000) $patients = $query->cursor(); - $savedStats = $this->saveReportSnapshot($reportNurse->id, $patients, $userId); + $savedStats = $this->saveReportSnapshot($reportDuty->id, $patients, $userId); return [ ...$savedStats, @@ -109,9 +109,9 @@ class DutyReportService foreach ($patients as $patient) { // Подготовка данных пациента $patientBatch[] = [ - 'report_nurse_id' => $reportDutyId, - 'source_type' => $patient->source_type, - 'original_id' => $patient->original_id, + 'report_duty_id' => $reportDutyId, + 'source_type' => 'mis', + 'original_id' => $patient->id, 'medical_card_number' => $patient->medical_card_number, 'full_name' => $patient->full_name, 'birth_date' => $patient->birth_date, @@ -184,7 +184,7 @@ class DutyReportService $patientUniqueBy = ['report_duty_id', 'source_type', 'original_id']; $patientUpdateColumns = array_diff(array_keys($patientBatch[0]), $patientUniqueBy); - DB::table('report_nurse_patients')->upsert( + DB::table('report_duty_patients')->upsert( $patientBatch, $patientUniqueBy, $patientUpdateColumns diff --git a/app/Services/MedicalHistoryService.php b/app/Services/MedicalHistoryService.php index be8f23e..bc0a6d1 100644 --- a/app/Services/MedicalHistoryService.php +++ b/app/Services/MedicalHistoryService.php @@ -4,10 +4,74 @@ namespace App\Services; use App\Models\MedicalHistory; use App\Models\MigrationPatient; +use App\Services\Classification\PatientStatusClassifier; use Illuminate\Support\Carbon; class MedicalHistoryService { + public function getGroupedHistories(DateRange $dateRange, int $departmentId): array + { + $startYear = Carbon::now()->startOfYear()->format('Y-m-d'); + + // 1. Один запрос: получаем "сырые" данные (без вычисляемых статусов) + $all = MedicalHistory::query()->whereHas('latestMigration', 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()); + }); + })->with(['latestMigration' => function ($q) use ($departmentId) { + $q->where('department_id', $departmentId); + }, 'latestMigration.operations', 'latestMigration.reanimations'])->get(); + + + // 2. Добавляем вычисляемые поля и превращаем в плоский массив + $prepared = $all->map(function (MedicalHistory $h) use ($dateRange) { + $patientStatus = PatientStatusClassifier::classify($h, $dateRange); + $patientUrgency = null; + if (!in_array($patientStatus, [ + PatientStatusClassifier::STATUS_DECEASED, + PatientStatusClassifier::STATUS_DISCHARGED, + PatientStatusClassifier::STATUS_TRANSFERRED + ])) $patientUrgency = PatientStatusClassifier::classifyUrgency($h->urgency_id); + return [ + // Все исходные поля модели (автоматически через toArray) + ...$h->toArray(), + + // + вычисляемые мета-поля для фронтенда + 'patient_status' => $patientStatus, + 'patient_urgency' => $patientUrgency, + 'admitted_today' => PatientStatusClassifier::classifyAdmitted($h->latestMigration?->ingoing_date), + ]; + }); + + // 3. Сортировка + $sortBy = 'recipient_date'; + $sortOrder = 'desc'; + $sorted = $prepared->sortBy($sortBy, SORT_REGULAR, $sortOrder === 'desc')->values(); + + // 4. Возвращаем плоский массив + метаданные для фронтенда + return [ + 'data' => $sorted->toArray(), + 'meta' => [ + 'total' => $sorted->count(), + 'sortBy' => $sortBy, + 'sortOrder' => $sortOrder, + // Статистика для фильтров/бейджей (опционально) + 'counts' => [ + 'in_department' => $sorted->where('patient_status', 'in_department')->count(), + 'discharged' => $sorted->where('patient_status', 'discharged')->count(), + 'urgent' => $sorted->where('patient_urgency', 'urgent')->count(), + 'planned' => $sorted->where('patient_urgency', 'planned')->count(), + ] + ] + ]; + } + public function getHistories(DateRange $dateRange, int $departmentId) { $query = MedicalHistory::query(); diff --git a/app/Services/UnifiedMedicalHistoryService.php b/app/Services/UnifiedMedicalHistoryService.php index 8749095..044cae1 100644 --- a/app/Services/UnifiedMedicalHistoryService.php +++ b/app/Services/UnifiedMedicalHistoryService.php @@ -5,10 +5,67 @@ namespace App\Services; use App\Models\MedicalHistory; use App\Models\MigrationPatient; use App\Models\UnifiedMedicalHistory; +use App\Services\Classification\PatientStatusClassifier; use Illuminate\Support\Carbon; class UnifiedMedicalHistoryService { + public function getGroupedHistories(DateRange $dateRange, int $departmentId): array + { + // 1. Один запрос: получаем "сырые" данные (без вычисляемых статусов) + $all = UnifiedMedicalHistory::with([ + 'latestMigration' => fn($q) => $q->where(function ($q) use ($dateRange) { + // Вариант А: Пациент уже лежит (текущий) + $q->whereNull('out_date') + ->whereNotNull('medical_history_id') + ->where('ingoing_date', '<', $dateRange->startSql()); + }) + ->orWhere(function ($q) use ($dateRange) { + $q->where('ingoing_date', '<=', $dateRange->endSql()) + ->where('ingoing_date', '>', $dateRange->startSql()); + }) + ]) + ->whereNull('extract_date') + ->get() + // Фильтр по отделению в памяти (быстро для <1000 записей) + ->filter(fn($h) => $h->latestMigration?->department_id === $departmentId); + + // 2. Добавляем вычисляемые поля и превращаем в плоский массив + $prepared = $all->map(function (UnifiedMedicalHistory $h) use ($dateRange) { + return [ + // Все исходные поля модели (автоматически через toArray) + ...$h->toArray(), + + // + вычисляемые мета-поля для фронтенда + 'patient_status' => PatientStatusClassifier::classify($h, $dateRange), + 'patient_urgency' => PatientStatusClassifier::classifyUrgency($h->urgency_id), + 'admitted_today' => PatientStatusClassifier::classifyAdmitted($h->latestMigration?->ingoing_date), + ]; + }); + + // 3. Сортировка + $sortBy = 'recipient_date'; + $sortOrder = 'desc'; + $sorted = $prepared->sortBy($sortBy, SORT_REGULAR, $sortOrder === 'desc')->values(); + + // 4. Возвращаем плоский массив + метаданные для фронтенда + return [ + 'data' => $sorted->toArray(), + 'meta' => [ + 'total' => $sorted->count(), + 'sortBy' => $sortBy, + 'sortOrder' => $sortOrder, + // Статистика для фильтров/бейджей (опционально) + 'counts' => [ + 'in_department' => $sorted->where('patient_status', 'in_department')->count(), + 'discharged' => $sorted->where('patient_status', 'discharged')->count(), + 'urgent' => $sorted->where('patient_urgency', 'urgent')->count(), + 'planned' => $sorted->where('patient_urgency', 'planned')->count(), + ] + ] + ]; + } + public function getHistories(DateRange $dateRange, int $departmentId) { $query = UnifiedMedicalHistory::query(); @@ -49,12 +106,10 @@ class UnifiedMedicalHistoryService public function getDepartmentHistories(DateRange $dateRange, int $departmentId) { return UnifiedMedicalHistory::query() - ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + ->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange) { $q->department($departmentId)->current($dateRange); }) - ->with(['latestMigration' => function ($q) use ($departmentId, $dateRange) { - $q->department($departmentId)->current($dateRange); // подгружаем только отфильтрованные движения - }]) + ->with(['latestMigration']) ->get() // Сортировка по дате поступления в отделение (поле дочерней таблицы) ->sortByDesc(fn ($mh) => $mh->latestMigration->ingoing_date ?? $mh->recipient_date) @@ -71,48 +126,40 @@ class UnifiedMedicalHistoryService $now = Carbon::now(); return UnifiedMedicalHistory::query() - ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + ->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange) { $q->department($departmentId)->admitted($dateRange->startSql(), $dateRange->endSql()); }) - ->with(['latestMigration' => function ($q) use ($departmentId) { - $q->department($departmentId); - }]) + ->with(['latestMigration']) ->get(); } public function getDischargedHistories(DateRange $dateRange, int $departmentId) { return UnifiedMedicalHistory::query() - ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + ->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange) { $q->department($departmentId)->discharged($dateRange->startSql(), $dateRange->endSql()); }) - ->with(['latestMigration' => function ($q) use ($departmentId) { - $q->department($departmentId); - }]) + ->with(['latestMigration']) ->get(); } public function getDeceasedHistories(DateRange $dateRange, int $departmentId) { return UnifiedMedicalHistory::query() - ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + ->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange) { $q->department($departmentId)->deceased($dateRange->startSql(), $dateRange->endSql()); }) - ->with(['latestMigration' => function ($q) use ($departmentId) { - $q->department($departmentId); - }]) + ->with(['latestMigration']) ->get(); } public function getTransferredHistories(DateRange $dateRange, int $departmentId) { return UnifiedMedicalHistory::query() - ->whereHas('migrations', function ($q) use ($departmentId, $dateRange) { + ->whereHas('latestMigration', function ($q) use ($departmentId, $dateRange) { $q->department($departmentId)->transferred($dateRange->startSql(), $dateRange->endSql()); }) - ->with(['latestMigration' => function ($q) use ($departmentId) { - $q->department($departmentId); - }]) + ->with(['latestMigration']) ->get(); } } diff --git a/package-lock.json b/package-lock.json index 2def047..6cdf5b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2112,7 +2112,6 @@ "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@emotion/hash": "~0.8.0", "csstype": "~3.0.5" @@ -2188,7 +2187,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -2242,7 +2240,6 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -3186,7 +3183,6 @@ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^7.7.7" }, @@ -3668,7 +3664,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4026,7 +4021,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", diff --git a/resources/js/Composables/useDropdownActions.js b/resources/js/Composables/useDropdownActions.js new file mode 100644 index 0000000..f94c7e6 --- /dev/null +++ b/resources/js/Composables/useDropdownActions.js @@ -0,0 +1,40 @@ +// composables/useDropdownActions.js +import { computed } from 'vue' +import { h } from 'vue' +import { NIcon } from 'naive-ui' + +export function useDropdownActions(groups, renderIcon) { + return computed(() => { + const result = [] + const visibleGroups = groups.filter(group => group.items?.some(item => item.if !== false)) + + visibleGroups.forEach((group, groupIndex) => { + // Фильтруем видимые пункты внутри группы + const visibleItems = group.items.filter(item => item.if !== false) + + if (visibleItems.length === 0) return + + // Добавляем пункты группы + visibleItems.forEach(item => { + const option = { ...item } + // Убираем служебное поле if + delete option.if + // Добавляем иконку через renderIcon если есть + if (item.icon && renderIcon) { + option.icon = renderIcon(item.icon) + } + result.push(option) + }) + + // Добавляем divider после группы, если это не последняя группа + if (groupIndex < visibleGroups.length - 1) { + result.push({ + type: 'divider', + key: `divider-${group.key || groupIndex}` + }) + } + }) + + return result + }) +} diff --git a/resources/js/Composables/usePatientColumns.js b/resources/js/Composables/usePatientColumns.js new file mode 100644 index 0000000..de2ad23 --- /dev/null +++ b/resources/js/Composables/usePatientColumns.js @@ -0,0 +1,200 @@ +import {format, formatDistanceStrict} from "date-fns"; +import {ru} from "date-fns/locale"; +import {h} from "vue"; +import TooltipColumn from "../Pages/Report/Components/DataTableColumns/TooltipColumn.vue"; +import OperationsColumn from "../Pages/Report/Components/DataTableColumns/OperationsColumn.vue"; +import ActionsColumn from "../Pages/Report/Components/DataTableColumns/ActionsColumn.vue"; + +export const usePatientColumns = (actionHandlers = {}) => { + const { onMisClick, onAddObservable, onAddObservableComment, onRemoveObservable, onShowOperationModal } = actionHandlers + + const defaultColumns = [ + { + title: 'ФИО', + key: 'full_name', + width: 280 + }, + { + title: 'Возраст', + key: 'birth_date', + render: (row) => formatDistanceStrict(new Date(row.birth_date), new Date(), { locale: ru }), + width: 75, + }, + { + title: 'Д/р', + key: 'birth_date', + minWidth: 94, + maxWidth: 100, + width: 94, + resizable: false, + render: (row) => format(new Date(row.birth_date), 'dd.MM.yyyy') + }, + { + title: 'Д/п', + key: 'latest_migration.ingoing_date', + minWidth: 134, + maxWidth: 144, + width: 134, + resizable: false, + render: (row) => format(new Date(row.latest_migration.ingoing_date), 'dd.MM.yyyy HH:mm') + }, + { + title: 'Диагноз', + key: 'latest_migration.diagnosis_code', + width: 75, + resizable: false, + render: (row) => h(TooltipColumn, { triggerText: row.latest_migration.diagnosis_code, contentText: row.latest_migration.diagnosis_name }) + }, + { + title: 'Операции', + key: 'latest_migration.operations', + width: 140, + className: 'relative', + render: (row) => h(OperationsColumn, { operations: row.latest_migration.operations, onClick: (operations) => onShowOperationModal(operations) }) + }, + ] + + const planOrEmergencyColumns = [ + ...defaultColumns, + { + title: '', + key: 'actions', + align: 'end', + render: (row) => { + return h( + ActionsColumn, + { + row: row, + isMis: true, + isAddObservable: true, + onMisClick, + onAddObservable, + onAddObservableComment, + onRemoveObservable, + } + ) + } + } + ] + + const observableColumns = [ + ...defaultColumns, + { + title: '', + key: 'actions', + align: 'end', + render: (row) => { + return h( + ActionsColumn, + { + row: row, + isMis: true, + isAddObservableComment: true, + isRemoveObservable: true, + onMisClick, + onAddObservable, + onAddObservableComment, + onRemoveObservable, + } + ) + } + } + ] + + const reanimationColumns = [ + ...defaultColumns, + { + title: '', + key: 'actions', + align: 'end', + render: (row) => { + return h( + ActionsColumn, + { + row: row, + isMis: true, + onMisClick, + onAddObservable, + onAddObservableComment, + onRemoveObservable, + } + ) + } + } + ] + + const deceasedColumns = [ + ...defaultColumns, + { + title: '', + key: 'actions', + align: 'end', + render: (row) => { + return h( + ActionsColumn, + { + row: row, + isMis: true, + onMisClick, + onAddObservable, + onAddObservableComment, + onRemoveObservable, + } + ) + } + } + ] + + const dischargedColumns = [ + ...defaultColumns, + { + title: '', + key: 'actions', + align: 'end', + render: (row) => { + return h( + ActionsColumn, + { + row: row, + isMis: true, + onMisClick, + onAddObservable, + onAddObservableComment, + onRemoveObservable, + } + ) + } + } + ] + + const transferredColumns = [ + ...defaultColumns, + { + title: '', + key: 'actions', + align: 'end', + render: (row) => { + return h( + ActionsColumn, + { + row: row, + isMis: true, + onMisClick, + onAddObservable, + onAddObservableComment, + onRemoveObservable, + } + ) + } + } + ] + + return { + planOrEmergencyColumns, + observableColumns, + reanimationColumns, + dischargedColumns, + deceasedColumns, + transferredColumns + } +} diff --git a/resources/js/Pages/Nurse/Report/Index.vue b/resources/js/Pages/Nurse/Report/Index.vue index b2d5004..a7754a9 100644 --- a/resources/js/Pages/Nurse/Report/Index.vue +++ b/resources/js/Pages/Nurse/Report/Index.vue @@ -5,7 +5,7 @@ import AppContainer from "../../../Components/AppContainer.vue"; import AppPanel from "../../../Components/AppPanel.vue"; import DatePickerQuery from "../../../Components/DatePickerQuery.vue"; import UrgencyBadge from "../../../Components/UrgencyBadge.vue"; -import {h, onMounted, ref, shallowRef} from "vue" +import {computed, h, onMounted, ref, shallowRef} from "vue" import {TbCirclePlus, TbPencil} from 'vue-icons-plus/tb' import {useAuthStore} from "../../../Stores/auth.js"; import AddMedicalHistoryModal from "../Components/AddMedicalHistoryModal.vue"; @@ -15,23 +15,7 @@ import ActionsColumnDataTable from "../Components/ActionsColumnDataTable.vue"; import {useAppDialog} from "../../../Composables/useAppDialog.js"; const props = defineProps({ - inDepartmentHistories: { - type: Array, - default: [] - }, - recipientHistories: { - type: Array, - default: [] - }, - dischargedHistories: { - type: Array, - default: [] - }, - deceasedHistories: { - type: Array, - default: [] - }, - transferredHistories: { + patients: { type: Array, default: [] }, @@ -48,6 +32,31 @@ const authStore = useAuthStore() const userDepartment = authStore.userDepartment const loading = ref(false) +const patientsByGroup = computed(() => { + const groups = { + urgent: [], + planned: [], + deceased: [], + in_department: [], + recipient: [], + discharged: [], + transferred: [], + }; + + for (const p of props.patients.data) { + // Группировка по срочности + if (p.patient_urgency === 'urgent') groups.urgent.push(p); + else if (p.patient_urgency === 'planned') groups.planned.push(p); + + // Группировка по статусу (дублирование нужно, если один пациент может быть в двух таблицах) + if (groups.hasOwnProperty(p.patient_status)) { + groups[p.patient_status].push(p); + } + } + + return groups; +}) + const columns = [ { title: 'ФИО', @@ -154,46 +163,46 @@ const formattedLabel = (word, count) => { - + - + - + - + -import {NSpace, NDropdown, NButton, NIcon} from 'naive-ui' -import {TbExternalLink, TbEyePlus, TbClick} from "vue-icons-plus/tb" -import {computed, h} from "vue"; +import { NSpace, NDropdown, NButton, NIcon } from 'naive-ui' +import {TbExternalLink, TbEyePlus, TbMessage, TbClick, TbEyeClosed} from "vue-icons-plus/tb" +import { computed, h } from "vue" +import { useDropdownActions } from '../../../../Composables/useDropdownActions.js' + const props = defineProps({ row: Object, isMis: Boolean, - isAddObservable: Boolean + isAddObservable: Boolean, + isRemoveObservable: Boolean, + isAddObservableComment: Boolean, + + onMisClick: Function, + onAddObservable: Function, + onAddObservableComment: Function, + onRemoveObservable: Function, }) +const emits = defineEmits(['addObservable', 'addObservableComment', 'removeObservable']) + const renderIcon = (icon) => { - return () => h( - NIcon, - {}, - { - default: () => h(icon) - } - ) + return () => h(NIcon, {}, { default: () => h(icon) }) } -const actions = computed(() => ([ +// Описываем группы действий +const actionGroups = computed(() => [ { - label: 'Просмотреть в МИС', - key: 'mis', - icon: renderIcon(TbExternalLink), - show: props.isMis + key: 'external', // ключ группы для идентификатора + items: [ + { + label: 'Просмотреть в МИС', + key: 'mis', + icon: TbExternalLink, + if: props.isMis + } + ] }, { - type: 'divider', - key: 'd1', - show: props.isMis && props.isAddObservable + key: 'actions', + items: [ + { + label: 'Добавить на контроль', + key: 'add-observable', + icon: TbEyePlus, + if: props.isAddObservable + }, + { + label: 'Добавить комментарий', + key: 'add-observable-comment', + icon: TbMessage, + if: props.isAddObservableComment + } + ] }, { - label: 'Добавить в наблюдение', - key: 'add-observable', - icon: renderIcon(TbEyePlus), - show: props.isAddObservable - }, -])) + key: 'danger', + items: [ + { + label: 'Снять с контроля', + key: 'remove-observable', + icon: TbEyeClosed, + if: props.isRemoveObservable + } + ] + } +]) + +// Получаем плоский массив опций с разделителями +const actions = useDropdownActions(actionGroups.value, renderIcon) const onOpenMis = () => { - const id = props.row.id - window.open(`https://stationar.amurzdrav.ru/prod/statist/edit/card/${id}`, '_blank') + if (props.onMisClick) { + props.onMisClick(props.row) + } else { + const id = props.row.id + window.open(`https://stationar.amurzdrav.ru/prod/statist/edit/card/${id}`, '_blank') + } } -const onSelectOption = (key, option) => { - switch (key) { - case 'mis': onOpenMis() - break +const onSelectOption = (key) => { + const handlers = { + 'mis': () => { + onOpenMis() + props.onMisClick?.(props.row) + }, + 'add-observable': () => { + // props.onAddObservable?.(props.row) + emits('addObservable', props.row) + }, + 'add-comment': () => { + // props.onAddObservableComment?.(props.row) + emits('onAddObservableComment', props.row) + }, + 'remove-observable': () => { + // props.onRemoveObservable?.(props.row) + emits('removeObservable', props.row) + }, } + handlers[key]?.() }