From 2026a1ca9fc35dd1744ae29b8ebcd202619fb356 Mon Sep 17 00:00:00 2001 From: brusnitsyn Date: Wed, 6 May 2026 17:03:41 +0900 Subject: [PATCH] =?UTF-8?q?*=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA?= =?UTF-8?q?=D0=B0=D1=80=D1=82=D1=8B,=20=D0=B5=D1=81=D0=BB=D0=B8=20=D0=BE?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B1=D1=8B=D0=BB=D0=B0=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BD=D0=B5=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=9C=D0=98=D0=A1=20*=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20=D0=B4=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=B0=D1=80=D1=82=D1=8B=20*=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B2=D0=B8=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20*=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D1=8B=D0=B2=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=BD=D0=BE=D0=B3=D0=BE=20=D0=BE?= =?UTF-8?q?=D1=82=D1=87=D0=B5=D1=82=D0=B0=20*=20=D0=B8=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=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?=D0=BE=D1=82=D1=87=D0=B5=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Api/NurseController.php | 36 +++- .../Controllers/Web/NurseReportController.php | 36 +++- app/Models/MigrationPatient.php | 3 +- app/Models/MigrationPatientNurse.php | 24 +++ app/Models/ReportNurseMigrationPatient.php | 108 ++++++++++ app/Models/ReportNursePatient.php | 35 ++- app/Models/UnifiedMigrationPatient.php | 26 +-- app/Services/DateRangeService.php | 9 + app/Services/NurseMedicalHistoryService.php | 119 +++++++++++ app/Services/NurseReportService.php | 201 ++++++++++++++---- ..._report_nurse_migration_patients_table.php | 43 ++++ resources/js/Components/AppDialog.vue | 53 +++++ resources/js/Components/AppDialogManager.vue | 40 ++++ resources/js/Components/ReportSelectDate.vue | 75 ++++--- resources/js/Composables/useAppDialog.js | 37 ++++ resources/js/Layouts/AppLayout.vue | 144 ++++++------- .../Components/ActionsColumnDataTable.vue | 42 ++++ .../Components/AddMedicalHistoryModal.vue | 8 +- .../Components/EditMedicalHistoryModal.vue | 3 + resources/js/Pages/Nurse/Report/Index.vue | 49 ++++- resources/js/app.js | 31 ++- routes/api.php | 1 + 22 files changed, 928 insertions(+), 195 deletions(-) create mode 100644 app/Models/ReportNurseMigrationPatient.php create mode 100644 app/Services/NurseMedicalHistoryService.php create mode 100644 database/migrations/2026_05_06_152530_create_report_nurse_migration_patients_table.php create mode 100644 resources/js/Components/AppDialog.vue create mode 100644 resources/js/Components/AppDialogManager.vue create mode 100644 resources/js/Composables/useAppDialog.js create mode 100644 resources/js/Pages/Nurse/Components/ActionsColumnDataTable.vue diff --git a/app/Http/Controllers/Api/NurseController.php b/app/Http/Controllers/Api/NurseController.php index 9cfb750..2f0cc67 100644 --- a/app/Http/Controllers/Api/NurseController.php +++ b/app/Http/Controllers/Api/NurseController.php @@ -94,6 +94,9 @@ class NurseController extends Controller public function storeCorrection($id, Request $request) { + $sourceType = $request->patient_source; + $originalId = $request->original_id; + $data = $request->validate([ 'medical_card_number' => 'nullable', 'full_name' => 'nullable', @@ -111,8 +114,12 @@ class NurseController extends Controller $data['medical_history_id'] = $id; $data['user_id'] = auth()->user()->id; + $departmentId = auth()->user()->department->rf_mis_department_id; - $currentMigration = MigrationPatient::currentMigration($id); + if ($sourceType === 'mis') + $currentMigration = MigrationPatient::currentMigration($id, $departmentId)->first(); + else + $currentMigration = MigrationPatientNurse::currentMigration($originalId, $departmentId)->first(); $migrationData = [ 'migration_patient_id' => $currentMigration->id, @@ -125,8 +132,16 @@ class NurseController extends Controller DB::beginTransaction(); - $historyCorrection = MedicalHistoryCorrection::create($data); - $migrationCorrection = MigrationPatientCorrection::create($migrationData); + if ($sourceType === 'mis') { + $historyCorrection = MedicalHistoryCorrection::create($data); + $migrationCorrection = MigrationPatientCorrection::create($migrationData); + } else if ($sourceType === 'manual') { + unset($data['medical_history_id']); + unset($migrationData['migration_patient_id']); + unset($migrationData['medical_history_id']); + $historyCorrection = MedicalHistoryNurse::find($originalId)->update($data); + $migrationCorrection = MigrationPatientNurse::where('medical_history_id', $originalId)->update($migrationData); + } if ($historyCorrection && $migrationCorrection) { DB::commit(); @@ -141,4 +156,19 @@ class NurseController extends Controller ], 400); } } + + public function deleteHistory($id, Request $request) + { + $medicalHistory = UnifiedMedicalHistory::where('id', $id)->first(); + $medicalHistoryId = $medicalHistory->original_id; + $migrationPatientsIds = $medicalHistory->migrations()->pluck('original_id'); + + foreach ($migrationPatientsIds as $migrationPatientsId) { + MigrationPatientNurse::query()->where('id', $migrationPatientsId)->delete(); + } + + MedicalHistoryNurse::where('id', $medicalHistoryId)->delete(); + + return response()->json([]); + } } diff --git a/app/Http/Controllers/Web/NurseReportController.php b/app/Http/Controllers/Web/NurseReportController.php index fa38798..5783111 100644 --- a/app/Http/Controllers/Web/NurseReportController.php +++ b/app/Http/Controllers/Web/NurseReportController.php @@ -4,7 +4,9 @@ namespace App\Http\Controllers\Web; use App\Http\Controllers\Controller; use App\Models\Department; +use App\Models\ReportNurse; use App\Services\DateRangeService; +use App\Services\NurseMedicalHistoryService; use App\Services\NurseReportService; use App\Services\UnifiedMedicalHistoryService; use Illuminate\Http\Request; @@ -17,6 +19,7 @@ class NurseReportController extends Controller protected DateRangeService $dateRangeService, protected UnifiedMedicalHistoryService $unifiedMedicalHistoryService, protected NurseReportService $nurseReportService, + protected NurseMedicalHistoryService $nurseMedicalHistoryService ) {} @@ -32,11 +35,34 @@ class NurseReportController extends Controller $department = Department::where('department_id', $departmentId)->firstOrFail(); $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user); - $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); + // Проверяем, есть ли отчет за этот период + $isPastPeriod = $this->dateRangeService->isPastPeriod($dateRange); + $existsReport = ReportNurse::where('rf_department_id', $departmentId) + ->where('period_end', '>', $dateRange->startSql()) + ->where('period_end', '<=', $dateRange->endSql()) + ->exists(); + + $hasReport = $existsReport; + + 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); + } return Inertia::render('Nurse/Report/Index', [ 'inDepartmentHistories' => $inDepartmentHistories, diff --git a/app/Models/MigrationPatient.php b/app/Models/MigrationPatient.php index e0374e8..b790d87 100644 --- a/app/Models/MigrationPatient.php +++ b/app/Models/MigrationPatient.php @@ -92,9 +92,10 @@ class MigrationPatient extends MaterializedViewModel return $query->where('is_actually_current', true); } - public function scopeCurrentMigration($query, $historyId) + 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/MigrationPatientNurse.php b/app/Models/MigrationPatientNurse.php index 81f421b..81b12fd 100644 --- a/app/Models/MigrationPatientNurse.php +++ b/app/Models/MigrationPatientNurse.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\DB; class MigrationPatientNurse extends Model { @@ -22,4 +23,27 @@ class MigrationPatientNurse extends Model 'mis_user_id', 'comment', ]; + + protected $casts = [ + 'ingoing_date' => 'datetime:Y-m-d H:i:s', + 'out_date' => 'datetime:Y-m-d H:i:s', + ]; + + // Фильтр по подразделению (получает 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); + } + + 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/ReportNurseMigrationPatient.php b/app/Models/ReportNurseMigrationPatient.php new file mode 100644 index 0000000..0095b4e --- /dev/null +++ b/app/Models/ReportNurseMigrationPatient.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/ReportNursePatient.php b/app/Models/ReportNursePatient.php index 3abe62f..027cdfb 100644 --- a/app/Models/ReportNursePatient.php +++ b/app/Models/ReportNursePatient.php @@ -25,10 +25,37 @@ class ReportNursePatient extends Model ]; protected $casts = [ - 'birth_date' => 'date', - 'recipient_date' => 'datetime', - 'extract_date' => 'datetime', - 'death_date' => 'datetime', + '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 migrations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(ReportNurseMigrationPatient::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(ReportNurseMigrationPatient::class, 'medical_history_id', 'id') + ->latest('ingoing_date'); + } + + public function operationsInDepartment($query, $departmentId) + { + return $this->operations()->where('department_id', $departmentId); + } + + // Скоупы + public function scopeUrgency($query, $typeId) // 1 = Экстренно, 2 = Планово + { + return $query->where('urgency_id', $typeId); + } } diff --git a/app/Models/UnifiedMigrationPatient.php b/app/Models/UnifiedMigrationPatient.php index 458817a..1c7935d 100644 --- a/app/Models/UnifiedMigrationPatient.php +++ b/app/Models/UnifiedMigrationPatient.php @@ -45,24 +45,6 @@ class UnifiedMigrationPatient extends Model 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) { @@ -96,14 +78,14 @@ class UnifiedMigrationPatient extends Model { return $query->whereNull('out_date') ->whereNotNull('medical_history_id') - ->whereHas('medicalHistory', function ($q) use ($dateRange) { - $q->whereNull('extract_date'); - }); + ->where('ingoing_date', '<', $dateRange->startSql()) + ->whereNull('patient_extract_date'); } - public function scopeCurrentMigration($query, $historyId) + 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/Services/DateRangeService.php b/app/Services/DateRangeService.php index 396d187..092e4c2 100644 --- a/app/Services/DateRangeService.php +++ b/app/Services/DateRangeService.php @@ -208,4 +208,13 @@ class DateRangeService isOneDay: true ); } + + /** + * Проверяет, является ли запрошенный период полностью прошедшим + */ + public function isPastPeriod(DateRange $dateRange): bool + { + // Период считается "прошлым", если его конец строго раньше начала текущих суток + return $dateRange->end()->lt(now()->startOfDay()); + } } diff --git a/app/Services/NurseMedicalHistoryService.php b/app/Services/NurseMedicalHistoryService.php new file mode 100644 index 0000000..5a0dbc4 --- /dev/null +++ b/app/Services/NurseMedicalHistoryService.php @@ -0,0 +1,119 @@ +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 = ReportNursePatient::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 ReportNursePatient::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 ReportNursePatient::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 ReportNursePatient::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 ReportNursePatient::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 ReportNursePatient::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/NurseReportService.php b/app/Services/NurseReportService.php index ce0eeb9..f758f8f 100644 --- a/app/Services/NurseReportService.php +++ b/app/Services/NurseReportService.php @@ -3,10 +3,13 @@ namespace App\Services; use App\Models\ReportNurse; +use App\Models\ReportNurseMigrationPatient; +use App\Models\ReportNursePatient; use App\Models\UnifiedMedicalHistory; use App\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\DB; class NurseReportService { @@ -47,7 +50,13 @@ class NurseReportService 'rf_user_id' => $user->id, ]; - $report = ReportNurse::create($data); + $report = ReportNurse::updateOrCreate( + [ + 'report_date' => $data['report_date'], 'period_start' => $data['period_start'], + 'period_end' => $data['period_end'] + ], + $data + ); return $report; } @@ -92,52 +101,164 @@ class NurseReportService ->latest('ingoing_date'); // если несколько, берём последнее }]); - $rawSql = $query->toRawSql(); - // Получаем данные (chunk для памяти, если пациентов > 1000) $patients = $query->cursor(); - $savedCount = 0; - $reportData = []; - - foreach ($patients as $patient) { - // Подготовка данных для сохранения (денормализация) - $data = [ - 'report_nurse_id' => $reportNurse->id, - '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, - ]; - - // UPSERT: обновляем если запись с таким ключом уже есть, иначе вставляем - \DB::table('report_nurse_patients')->upsert( - [$data], - ['report_nurse_id', 'source_type', 'original_id'], // уникальные ключи - [ - 'medical_card_number', 'full_name', 'birth_date', 'recipient_date', 'extract_date', 'death_date', - 'male', 'urgency_id', 'hospital_result_id', 'visit_result_id', 'comment', 'user_id' - ] // обновляемые поля - ); - - $savedCount++; - $reportData[] = $data; - } + $savedStats = $this->saveReportSnapshot($reportNurse->id, $patients, $userId); return [ - 'saved_count' => $savedCount, + ...$savedStats, 'report_date' => $dateRange->startSql(), 'department_id' => $departmentId, ]; } + + public function saveReportSnapshot(int $reportNurseId, 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' => $reportNurseId, + '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->latestMigration)) { + $migrationBatch[] = [ + // Временный ключ для связи с пациентом (заполним после первого upsert) + '_temp_key' => [ + 'report_nurse_id' => $reportNurseId, + 'source_type' => $patient->source_type, + 'original_id' => $patient->original_id, + ], + 'ingoing_date' => $patient->latestMigration->ingoing_date, + 'out_date' => $patient->latestMigration->out_date, + 'diagnosis_id' => $patient->latestMigration->diagnosis_id, + 'diagnosis_code' => $patient->latestMigration->diagnosis_code, + 'diagnosis_name' => $patient->latestMigration->diagnosis_name, + 'interrupted_event_id' => $patient->latestMigration->interrupted_event_id, + 'stationar_branch_id' => $patient->latestMigration->stationar_branch_id, + 'department_id' => $patient->latestMigration->department_id, + 'visit_result_id' => $patient->latestMigration->visit_result_id, + 'stat_cure_result_id' => $patient->latestMigration->stat_cure_result_id, + 'user_id' => $patient->latestMigration->user_id, + 'mis_user_id' => $patient->latestMigration->mis_user_id, + 'comment' => $patient->latestMigration->comment, + ]; + } + + // Пакетная запись каждые $batchSize записей + if (count($patientBatch) >= $batchSize) { + [$savedP, $savedM] = $this->upsertBatches($reportNurseId, $patientBatch, $migrationBatch); + $patientBatch = []; + $migrationBatch = []; + } + } + + // Сохраняем остаток + [$savedP, $savedM] = $this->upsertBatches($reportNurseId, $patientBatch, $migrationBatch); + + return ['saved_patients' => $savedP, 'saved_migrations' => $savedM]; + } + + /** + * Вспомогательный метод: выполняет upsert для пациентов и миграций + */ + private function upsertBatches(int $reportNurseId, array $patientBatch, array $migrationBatch): array + { + if (empty($patientBatch)) { + return [0, 0]; + } + + $savedPatients = 0; + $savedMigrations = 0; + + DB::transaction(function () use ($reportNurseId, $patientBatch, $migrationBatch, &$savedPatients, &$savedMigrations) { + // UPSERT пациентов + $patientUniqueBy = ['report_nurse_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_nurse_patients') + ->whereIn('report_nurse_id', [$reportNurseId]) + ->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']; + $migrationUpdateColumns = array_diff(array_keys($finalMigrations[0]), $migrationUniqueBy); + + DB::table('report_nurse_migration_patients')->upsert( + $finalMigrations, + $migrationUniqueBy, + $migrationUpdateColumns + ); + $savedMigrations = count($finalMigrations); + } + } + }); + + return [$savedPatients, $savedMigrations]; + } } diff --git a/database/migrations/2026_05_06_152530_create_report_nurse_migration_patients_table.php b/database/migrations/2026_05_06_152530_create_report_nurse_migration_patients_table.php new file mode 100644 index 0000000..d1bff88 --- /dev/null +++ b/database/migrations/2026_05_06_152530_create_report_nurse_migration_patients_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignIdFor( \App\Models\ReportNursePatient::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', 'uniq_rnmp_medical_history_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('report_nurse_migration_patients'); + } +}; diff --git a/resources/js/Components/AppDialog.vue b/resources/js/Components/AppDialog.vue new file mode 100644 index 0000000..4fcc89e --- /dev/null +++ b/resources/js/Components/AppDialog.vue @@ -0,0 +1,53 @@ + + + diff --git a/resources/js/Components/AppDialogManager.vue b/resources/js/Components/AppDialogManager.vue new file mode 100644 index 0000000..13b94e8 --- /dev/null +++ b/resources/js/Components/AppDialogManager.vue @@ -0,0 +1,40 @@ + + + diff --git a/resources/js/Components/ReportSelectDate.vue b/resources/js/Components/ReportSelectDate.vue index d4e7017..7220097 100644 --- a/resources/js/Components/ReportSelectDate.vue +++ b/resources/js/Components/ReportSelectDate.vue @@ -120,37 +120,52 @@ const modelComputed = computed({ const formattedValue = computed(() => { const value = reportStore.timestampCurrentRange - if (authStore.isHeadOfDepartment || authStore.isAdmin) { - if (props.isOneDay) { - const dateToFormat = Array.isArray(value) ? value[1] : value - return formatRussianDate(dateToFormat) - } else if (Array.isArray(value) && value.length >= 2 && value[0] && value[1]) { // Для админа - диапазон дат - return formatRussianDateRange(value) - } - - // Если что-то пошло не так, форматируем как одиночную дату - if (value) { - const dateToFormat = Array.isArray(value) ? value[0] : value - return formatRussianDate(dateToFormat) - } - - return '' - } else { - // Для врача - одиночная дата - let dateToFormat - - if (Array.isArray(value)) { - dateToFormat = value[1] || value[0] - } else { - dateToFormat = value - } - - // Если выбрана сегодняшняя дата - показываем текущее время - if (dateToFormat) { - return formatRussianDate(dateToFormat) - } - return '' + if (props.isOneDay) { + const dateToFormat = Array.isArray(value) ? value[1] : value + return formatRussianDate(dateToFormat) + } else if (Array.isArray(value) && value.length >= 2 && value[0] && value[1]) { // Для админа - диапазон дат + return formatRussianDateRange(value) } + + // Если что-то пошло не так, форматируем как одиночную дату + if (value) { + const dateToFormat = Array.isArray(value) ? value[0] : value + return formatRussianDate(dateToFormat) + } + + return '' + + // if (authStore.isHeadOfDepartment || authStore.isAdmin) { + // if (props.isOneDay) { + // const dateToFormat = Array.isArray(value) ? value[1] : value + // return formatRussianDate(dateToFormat) + // } else if (Array.isArray(value) && value.length >= 2 && value[0] && value[1]) { // Для админа - диапазон дат + // return formatRussianDateRange(value) + // } + // + // // Если что-то пошло не так, форматируем как одиночную дату + // if (value) { + // const dateToFormat = Array.isArray(value) ? value[0] : value + // return formatRussianDate(dateToFormat) + // } + // + // return '' + // } else { + // // Для врача - одиночная дата + // let dateToFormat + // + // if (Array.isArray(value)) { + // dateToFormat = value[1] || value[0] + // } else { + // dateToFormat = value + // } + // + // // Если выбрана сегодняшняя дата - показываем текущее время + // if (dateToFormat) { + // return formatRussianDate(dateToFormat) + // } + // return '' + // } }) const classComputed = computed(() => { diff --git a/resources/js/Composables/useAppDialog.js b/resources/js/Composables/useAppDialog.js new file mode 100644 index 0000000..d3d8cf5 --- /dev/null +++ b/resources/js/Composables/useAppDialog.js @@ -0,0 +1,37 @@ +import { h, ref, render, nextTick } from 'vue' +import AppDialog from '../Components/AppDialog.vue' + +// Глобальная очередь диалогов +export const dialogQueue = ref([]) +let idCounter = 0 + +// Вызывается при клике на кнопку / Esc / клик по маске +export function closeDialog(id, confirmed = false) { + const dialog = dialogQueue.value.find(d => d.id === id) + if (dialog && dialog.show) { + dialog.show = false // Запускает leave-анимацию + dialog.resolve(confirmed) // Резолвим промис сразу для лучшего UX + } +} + +// Вызывается после завершения leave-анимации +export function cleanupDialog(id) { + dialogQueue.value = dialogQueue.value.filter(d => d.id !== id) +} + +export function useAppDialog({title, content, positiveProps, negativeProps, positiveText = 'Подтвердить', negativeText = 'Отмена', maskClosable = false, onConfirm } = {}) { + return new Promise((resolve) => { + const id = idCounter++ + // 1. Добавляем скрытым, чтобы сработала enter-анимация + dialogQueue.value.push({ + id, show: false, title, content, loading: false, onConfirm, + positiveText, negativeText, positiveProps, negativeProps, maskClosable, resolve + }) + + // 2. Переключаем в visible на следующем тике + nextTick(() => { + const dialog = dialogQueue.value.find(d => d.id === id) + if (dialog) dialog.show = true + }) + }) +} diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index 3e80d64..8edf1c7 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -1,99 +1,85 @@ diff --git a/resources/js/Pages/Nurse/Components/AddMedicalHistoryModal.vue b/resources/js/Pages/Nurse/Components/AddMedicalHistoryModal.vue index 0db3c5b..a1e9530 100644 --- a/resources/js/Pages/Nurse/Components/AddMedicalHistoryModal.vue +++ b/resources/js/Pages/Nurse/Components/AddMedicalHistoryModal.vue @@ -160,10 +160,10 @@ const onChangeSearch = (historyId) => { form.value.urgency_id = res.data.urgency_id form.value.visit_result_id = res.data.visit_result_id - form.value.birth_date = res.data.birth_date ? format(new Date(res.data.birth_date), 'yyyy-MM-dd HH:mm:ss') : null - form.value.death_date = res.data.death_date ? format(new Date(res.data.death_date), 'yyyy-MM-dd HH:mm:ss') : null - form.value.extract_date = res.data.extract_date ? format(new Date(res.data.extract_date), 'yyyy-MM-dd HH:mm:ss') : null - form.value.recipient_date = res.data.recipient_date ? format(new Date(res.data.recipient_date), 'yyyy-MM-dd HH:mm:ss') : null + form.value.birth_date = res.data.birth_date + form.value.death_date = res.data.death_date + form.value.extract_date = res.data.extract_date + form.value.recipient_date = res.data.recipient_date }) } diff --git a/resources/js/Pages/Nurse/Components/EditMedicalHistoryModal.vue b/resources/js/Pages/Nurse/Components/EditMedicalHistoryModal.vue index f9ece66..b680bc9 100644 --- a/resources/js/Pages/Nurse/Components/EditMedicalHistoryModal.vue +++ b/resources/js/Pages/Nurse/Components/EditMedicalHistoryModal.vue @@ -28,6 +28,7 @@ const props = defineProps({ const form = ref({ patient_source: 'mis', + original_id: null, patient_id: null, full_name: '', urgency_id: 1, @@ -128,7 +129,9 @@ const fetchPatient = async (historyId) => { loading.value = true await axios.get(`/api/nurse/patients/${historyId}`) .then(res => { + form.value.patient_source = res.data.source_type form.value.patient_id = historyId + form.value.original_id = res.data.original_id form.value.full_name = res.data.full_name form.value.urgency_id = res.data.urgency_id form.value.visit_result_id = res.data.visit_result_id diff --git a/resources/js/Pages/Nurse/Report/Index.vue b/resources/js/Pages/Nurse/Report/Index.vue index 33c76e8..b2d5004 100644 --- a/resources/js/Pages/Nurse/Report/Index.vue +++ b/resources/js/Pages/Nurse/Report/Index.vue @@ -5,12 +5,14 @@ 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, ref, shallowRef} from "vue" +import {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"; import EditMedicalHistoryModal from "../Components/EditMedicalHistoryModal.vue"; import {router} from "@inertiajs/vue3"; +import ActionsColumnDataTable from "../Components/ActionsColumnDataTable.vue"; +import {useAppDialog} from "../../../Composables/useAppDialog.js"; const props = defineProps({ inDepartmentHistories: { @@ -44,6 +46,7 @@ const showEditMedicalHistoryModal = shallowRef(false) const editHistoryId = ref(null) const authStore = useAuthStore() const userDepartment = authStore.userDepartment +const loading = ref(false) const columns = [ { @@ -74,8 +77,13 @@ const columns = [ align: 'end', render: (row) => { return h( - NButton, { size: 'tiny', type: 'default', secondary: true, onClick: () => onClickEditButton(row.id) }, - { default: () => 'Редактировать', icon: () => h(TbPencil, { size: '18px' }) }) + ActionsColumnDataTable, + { + row: row, + onClickDelete: (historyId) => onClickDeleteButton(historyId), + onClickEdit: (historyId) => onClickEditButton(historyId), + } + ) } } ] @@ -85,6 +93,32 @@ const onClickEditButton = (historyId) => { editHistoryId.value = historyId } +const onClickDeleteButton = async (historyId) => { + const confirmed = await useAppDialog({ + title: 'Удалить историю?', + content: 'Это действие необратимо', + onConfirm: async () => { + await axios.delete(`/api/nurse/patients/${historyId}`) + } + }) + + if (confirmed) { + loading.value = true + router.reload({ + only: [ + 'inDepartmentHistories', + 'recipientHistories', + 'dischargedHistories', + 'deceasedHistories', + 'transferredHistories' + ], + onSuccess: () => { + loading.value = false + } + }) + } +} + const submit = () => { router.post('/nurse/report/save', {}, { onSuccess: () => { @@ -111,7 +145,7 @@ const formattedLabel = (word, count) => {