diff --git a/app/Console/Commands/FillAverageBedDaysMetric.php b/app/Console/Commands/FillAverageBedDaysMetric.php index 2aae887..2396c4b 100644 --- a/app/Console/Commands/FillAverageBedDaysMetric.php +++ b/app/Console/Commands/FillAverageBedDaysMetric.php @@ -45,12 +45,12 @@ class FillAverageBedDaysMetric extends Command // Filter by date range if ($from = $this->option('from')) { - $query->whereDate('created_at', '>=', $from); + $query->where('period_start', '>=', Carbon::parse($from, 'Asia/Yakutsk')->startOfDay()->format('Y-m-d H:i:s')); $this->info("Filter: from {$from}"); } if ($to = $this->option('to')) { - $query->whereDate('created_at', '<=', $to); + $query->where('period_end', '<', Carbon::parse($to, 'Asia/Yakutsk')->addDay()->startOfDay()->format('Y-m-d H:i:s')); $this->info("Filter: to {$to}"); } @@ -147,10 +147,10 @@ class FillAverageBedDaysMetric extends Command // Apply same filters to sample if ($from = $this->option('from')) { - $sampleQuery->whereDate('created_at', '>=', $from); + $sampleQuery->where('period_start', '>=', Carbon::parse($from, 'Asia/Yakutsk')->startOfDay()->format('Y-m-d H:i:s')); } if ($to = $this->option('to')) { - $sampleQuery->whereDate('created_at', '<=', $to); + $sampleQuery->where('period_end', '<', Carbon::parse($to, 'Asia/Yakutsk')->addDay()->startOfDay()->format('Y-m-d H:i:s')); } if ($departmentId = $this->option('department')) { $sampleQuery->where('rf_department_id', $departmentId); diff --git a/app/Console/Commands/RecalculatePreoperativeMetric.php b/app/Console/Commands/RecalculatePreoperativeMetric.php index dab0231..08df419 100644 --- a/app/Console/Commands/RecalculatePreoperativeMetric.php +++ b/app/Console/Commands/RecalculatePreoperativeMetric.php @@ -56,12 +56,12 @@ class RecalculatePreoperativeMetric extends Command // Фильтр по дате if ($from = $this->option('from')) { - $query->whereDate('created_at', '>=', $from); + $query->where('period_start', '>=', Carbon::parse($from, 'Asia/Yakutsk')->startOfDay()->format('Y-m-d H:i:s')); $this->info("📅 Фильтр: с {$from}"); } if ($to = $this->option('to')) { - $query->whereDate('created_at', '<=', $to); + $query->where('period_end', '<', Carbon::parse($to, 'Asia/Yakutsk')->addDay()->startOfDay()->format('Y-m-d H:i:s')); $this->info("📅 Фильтр: по {$to}"); } diff --git a/app/Data/UnifiedPatientData.php b/app/Data/UnifiedPatientData.php index f84fa44..d5e9f49 100644 --- a/app/Data/UnifiedPatientData.php +++ b/app/Data/UnifiedPatientData.php @@ -43,11 +43,15 @@ class UnifiedPatientData $birthDate = $birthDateValue?->format('Y-m-d'); $manualId = $linkedManualPatient?->department_patient_id; $outcomeMigration = $patient->relationLoaded('outcomeMigration') - ? $patient->outcomeMigration->first() + ? $patient->outcomeMigration()->first() : null; + $migration = $patient->relationLoaded('migrations') ? $patient->migrations->first() : null; + if (!$migration && $patient->relationLoaded('latestMigration')) { + $migration = $patient->latestMigration; + } $diagnosisMkb = $outcomeMigration?->mainDiagnosis?->mkb ?? $migration?->mainDiagnosis?->mkb; $operations = $patient->relationLoaded('surgicalOperations') ? $patient->surgicalOperations->map(fn ($operation) => [ @@ -91,7 +95,7 @@ class UnifiedPatientData $manualId = $linkedManualPatient?->department_patient_id; $medicalHistory = $migration->medicalHistory; $outcomeMigration = $medicalHistory->relationLoaded('outcomeMigration') - ? $medicalHistory->outcomeMigration->first() + ? $medicalHistory->outcomeMigration()->first() : null; $operations = $medicalHistory->relationLoaded('surgicalOperations') ? $medicalHistory->surgicalOperations->map(fn ($operation) => [ @@ -164,6 +168,9 @@ class UnifiedPatientData { $birthDate = self::normalizeDate($snapshot->birth_date); +// if ($snapshot->rf_medicalhistory_id === 334148) +// dd($snapshot); + return new self( id: $snapshot->rf_department_patient_id ? "manual:{$snapshot->rf_department_patient_id}" : ($snapshot->patient_uid ?: "mis:{$snapshot->rf_medicalhistory_id}"), patientUid: $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}"), diff --git a/app/Http/Controllers/Api/MetrikaFormController.php b/app/Http/Controllers/Api/MetrikaFormController.php index 14ceee9..87e216c 100644 --- a/app/Http/Controllers/Api/MetrikaFormController.php +++ b/app/Http/Controllers/Api/MetrikaFormController.php @@ -10,6 +10,7 @@ use App\Models\MetrikaResult; use App\Models\MetrikaResultValue; use App\Models\Report; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; @@ -66,11 +67,14 @@ class MetrikaFormController extends Controller } // Создаем или обновляем отчет + [$periodStart, $periodEnd] = $this->getTodayPeriodBounds(); $report = Report::create( [ 'rf_user_id' => $user->id, - 'created_at' => now()->toDateString(), - 'sent_at' => now()->toDateString(), + 'created_at' => $periodEnd, + 'sent_at' => $periodEnd, + 'period_start' => $periodStart, + 'period_end' => $periodEnd, 'rf_department_id' => $user->department->departmentid ] ); @@ -150,7 +154,7 @@ class MetrikaFormController extends Controller // Находим последний отчет пользователя за сегодня $report = Report::where('rf_user_id', $user->id) - ->whereDate('created_at', now()->toDateString()) + ->exactPeriod(...$this->getTodayPeriodBounds()) ->orderBy('created_at', 'desc') ->first(); @@ -252,6 +256,7 @@ class MetrikaFormController extends Controller $dateStart = date('Y-m-d', $startAt) . ' 00:00:00'; $dateEnd = date('Y-m-d', $endAt) . ' 23:59:59'; + $dateEndExclusive = Carbon::parse($dateEnd, 'Asia/Yakutsk')->addSecond()->format('Y-m-d H:i:s'); $group = MetrikaGroup::findOrFail($groupId); @@ -260,9 +265,8 @@ class MetrikaFormController extends Controller ->join('metrika_result_values as mv', 'mr.metrika_result_id', '=', 'mv.rf_metrika_result_id') ->join('reports as r', 'mr.rf_report_id', '=', 'r.report_id') ->where('mr.rf_metrika_group_id', $groupId) -// ->whereBetween('r.sent_at', [$dateStart, $dateEnd]) - ->where('r.sent_at', '>', $dateStart) - ->where('r.sent_at', '<=', $dateEnd) + ->where('r.period_start', '>=', $dateStart) + ->where('r.period_end', '<', $dateEndExclusive) ->when(!$user->isAdmin() && !$user->isHeadOfDepartment(), function ($query) use ($user) { return $query->where('r.rf_user_id', $user->id); }) @@ -355,9 +359,8 @@ class MetrikaFormController extends Controller $endDate = date("{$year}-{$month}-t", strtotime($startDate)); $reports = Report::where('rf_user_id', $user->id) -// ->whereBetween('sent_at', [$startDate, $endDate]) - ->where('sent_at', '>', $startDate) - ->where('sent_at', '<=', $endDate) + ->where('period_start', '>=', $startDate) + ->where('period_end', '<', Carbon::parse($endDate, 'Asia/Yakutsk')->addDay()->startOfDay()->format('Y-m-d H:i:s')) ->get(); // Создаем календарь @@ -406,7 +409,7 @@ class MetrikaFormController extends Controller // Дни месяца for ($day = 1; $day <= $daysInMonth; $day++) { $date = date("{$year}-{$month}-" . sprintf('%02d', $day)); - $report = $reports->firstWhere('sent_at', $date); + $report = $reports->first(fn (Report $item) => $item->period_end && $item->period_end->toDateString() === $date); $timestamp = strtotime($date) * 1000; // В миллисекундах $dayData = [ @@ -420,7 +423,7 @@ class MetrikaFormController extends Controller 'is_weekend' => date('N', strtotime($date)) >= 6, 'has_report' => !is_null($report), 'report_status' => $report ? $report->status : null, - 'sent_at' => $report && $report->sent_at ? $report->sent_at->getTimestamp() * 1000 : $timestamp + 'sent_at' => $report && $report->period_end ? $report->period_end->getTimestamp() * 1000 : $timestamp ]; $calendar['days'][] = $dayData; @@ -564,7 +567,7 @@ class MetrikaFormController extends Controller $user = Auth::user(); $report = Report::where('rf_user_id', $user->id) - ->whereDate('created_at', now()->toDateString()) + ->exactPeriod(...$this->getTodayPeriodBounds()) ->first(); if (!$report) { @@ -593,4 +596,14 @@ class MetrikaFormController extends Controller ]; } + private function getTodayPeriodBounds(): array + { + $now = now('Asia/Yakutsk'); + + return [ + $now->copy()->subDay()->setTime(7, 0)->format('Y-m-d H:i:s'), + $now->copy()->setTime(7, 0)->format('Y-m-d H:i:s'), + ]; + } + } diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php index 4b2013a..cc7e12b 100644 --- a/app/Http/Controllers/Api/ReportController.php +++ b/app/Http/Controllers/Api/ReportController.php @@ -244,8 +244,10 @@ class ReportController extends Controller 'rf_department_id' => $data['departmentId'], 'rf_user_id' => Auth::user()->id, 'rf_lpudoctor_id' => $data['userId'], - 'created_at' => now(), - 'sent_at' => now() + 'created_at' => $dateRange->endSql(), + 'sent_at' => $dateRange->endSql(), + 'period_start' => $dateRange->startSql(), + 'period_end' => $dateRange->endSql(), ] ); } else { @@ -253,8 +255,10 @@ class ReportController extends Controller 'rf_department_id' => $data['departmentId'], 'rf_user_id' => Auth::user()->id, 'rf_lpudoctor_id' => $data['userId'], - 'created_at' => now(), - 'sent_at' => now() + 'created_at' => $dateRange->endSql(), + 'sent_at' => $dateRange->endSql(), + 'period_start' => $dateRange->startSql(), + 'period_end' => $dateRange->endSql(), ]); } @@ -869,6 +873,10 @@ class ReportController extends Controller $user = Auth::user(); $data = $request->validate([ 'departmentId' => 'required|integer', + 'report_id' => 'nullable|integer', + 'startAt' => 'required_without:report_id', + 'endAt' => 'required_without:report_id', + 'user_id' => 'nullable|integer', 'full_name' => 'required|string|max:255', 'birth_date' => 'required|date', 'patient_kind' => 'required|in:plan,emergency', @@ -880,18 +888,22 @@ class ReportController extends Controller $department = Department::where('department_id', $data['departmentId'])->firstOrFail(); $patient = $this->reportService->createManualPatient($department, $user, $data); - return response()->json($patient, 201); + return response()->json([ + 'patient' => $patient, + 'report_id' => $patient->rf_report_id, + ], 201); } public function setManualPatientOutcome(Request $request, int $departmentPatientId) { + $user = Auth::user(); $data = $request->validate([ 'outcome_type' => 'required|in:discharged,transferred,deceased', 'outcome_at' => 'nullable|date', ]); return response()->json( - $this->reportService->setManualPatientOutcome($departmentPatientId, $data) + $this->reportService->setManualPatientOutcome($user, $departmentPatientId, $data) ); } @@ -903,6 +915,8 @@ class ReportController extends Controller 'full_name' => 'required|string|max:255', 'birth_date' => 'required|date', 'patient_kind' => 'required|in:plan,emergency', + 'manual_status' => 'nullable|in:current,discharged,deceased', + 'outcome_at' => 'nullable|date', 'diagnosis_code' => 'nullable|string|max:255', 'diagnosis_name' => 'nullable|string|max:1000', 'admitted_at' => 'nullable|date', @@ -1087,15 +1101,13 @@ class ReportController extends Controller { if (Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate)) > 1.0) return Report::where('rf_department_id', $departmentId) -// ->whereBetween('created_at', [$startDate, $endDate]) - ->where('sent_at', '>=', $startDate) - ->where('sent_at', '<=', $endDate) - ->orderBy('sent_at', 'ASC') + ->withinPeriod($startDate, $endDate) + ->orderBy('period_end', 'ASC') ->get(); else return Report::where('rf_department_id', $departmentId) - ->whereDate('created_at', $endDate) - ->orderBy('created_at', 'ASC') + ->exactPeriod($startDate, $endDate) + ->orderBy('period_end', 'ASC') ->get(); } @@ -1106,12 +1118,12 @@ class ReportController extends Controller ]); $report = Report::where('rf_department_id', $request->department_id) - ->whereDate('created_at', now()->toDateString()) + ->exactPeriod(now('Asia/Yakutsk')->subDay()->setTime(7, 0)->format('Y-m-d H:i:s'), now('Asia/Yakutsk')->setTime(7, 0)->format('Y-m-d H:i:s')) ->first(); return response()->json([ 'report_id' => $report?->report_id, - 'exists' => $report->exists + 'exists' => (bool) $report ]); } } diff --git a/app/Http/Controllers/TestController.php b/app/Http/Controllers/TestController.php index 9abeb7a..76d0ae7 100644 --- a/app/Http/Controllers/TestController.php +++ b/app/Http/Controllers/TestController.php @@ -22,4 +22,11 @@ class TestController extends Controller 'data' => $data ]); } + + public function testIndex() + { + return Inertia::render('TestIndex', [ + + ]); + } } diff --git a/app/Http/Controllers/Web/ReportController.php b/app/Http/Controllers/Web/ReportController.php index 9c1a901..70621b4 100644 --- a/app/Http/Controllers/Web/ReportController.php +++ b/app/Http/Controllers/Web/ReportController.php @@ -38,7 +38,8 @@ class ReportController extends Controller 'unwantedEvents' => 'nullable|array', 'dates' => 'required|array', 'userId' => 'required|integer', - 'reportId' => 'nullable|integer' + 'reportId' => 'nullable|integer', + 'status' => 'nullable|in:draft,submitted', ]); $this->reportService->storeReport($validated, Auth::user(), false); diff --git a/app/Models/DepartmentPatient.php b/app/Models/DepartmentPatient.php index 31e8f1e..611b1fa 100644 --- a/app/Models/DepartmentPatient.php +++ b/app/Models/DepartmentPatient.php @@ -10,6 +10,7 @@ class DepartmentPatient extends Model protected $fillable = [ 'rf_department_id', + 'rf_report_id', 'source_type', 'rf_medicalhistory_id', 'full_name', @@ -52,4 +53,9 @@ class DepartmentPatient extends Model { return $this->hasMany(DepartmentPatientOperation::class, 'rf_department_patient_id', 'department_patient_id'); } + + public function report() + { + return $this->belongsTo(Report::class, 'rf_report_id', 'report_id'); + } } diff --git a/app/Models/MisMedicalHistory.php b/app/Models/MisMedicalHistory.php index f04c0f8..42b86ea 100644 --- a/app/Models/MisMedicalHistory.php +++ b/app/Models/MisMedicalHistory.php @@ -152,15 +152,15 @@ class MisMedicalHistory extends Model public function outcomeMigration() { - return $this->migrations() - ->whereDate('DateOut', '<>', '2222-01-01') - ->orderBy('DateOut', 'desc'); + return $this->hasOne(MisMigrationPatient::class, 'rf_MedicalHistoryID', 'MedicalHistoryID') + ->whereDate('DateOut', '=', '2222-01-01'); } public function latestMigration() { return $this->hasOne(MisMigrationPatient::class, 'rf_MedicalHistoryID', 'MedicalHistoryID') - ->ofMany('DateOut', 'max'); + ->whereDate('DateOut', '=', '2222-01-01') + ->orderBy('DateOut', 'desc'); } /* diff --git a/app/Models/MisMigrationPatient.php b/app/Models/MisMigrationPatient.php index bd219f8..9262f5e 100644 --- a/app/Models/MisMigrationPatient.php +++ b/app/Models/MisMigrationPatient.php @@ -28,7 +28,11 @@ class MisMigrationPatient extends Model public function mainDiagnosis() { - return $this->hasOne(MisDiagnos::class, 'rf_MigrationPatientID', 'MigrationPatientID')->where('rf_DiagnosTypeID', 3); + return $this->hasOne(MisDiagnos::class, 'rf_MigrationPatientID', 'MigrationPatientID') + ->where(function ($query) { + $query->where('rf_DiagnosTypeID', 3) + ->orWhere('rf_DiagnosTypeID', 7); + }); } public function mkb() diff --git a/app/Models/MisSurgicalOperation.php b/app/Models/MisSurgicalOperation.php index f0ad4de..a68fc36 100644 --- a/app/Models/MisSurgicalOperation.php +++ b/app/Models/MisSurgicalOperation.php @@ -3,10 +3,12 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use LaravelIdea\Helper\App\Models\_IH_MisSurgicalOperation_QB; class MisSurgicalOperation extends Model { private const COMPLETED_OPERATION_STATUS_ID = 3; + private const CANCELLED_OPERATION_STATUS_ID = 4; protected $table = 'stt_surgicaloperation'; protected $primaryKey = 'SurgicalOperationID'; @@ -26,10 +28,47 @@ class MisSurgicalOperation extends Model return $this->hasOne(MisOperationPurpose::class, 'rf_SurgicalOperationID', 'SurgicalOperationID'); } + + /** + * Получить операции у которых исход <> 0 + * @param $query + * @return _IH_MisSurgicalOperation_QB|_IH_MisSurgicalOperation_QB + */ public function scopeCompleted($query) + { + return $query->whereNot('rf_OperationResultID', 0); + } + + /** + * Получить операции с благоприятным исходом + * @param $query + * @return _IH_MisSurgicalOperation_QB|_IH_MisSurgicalOperation_QB + */ + public function scopePositiveResult($query) + { + return $query->where('rf_OperationResultID', 1); + } + + /** + * Получить операции с неблагоприятным исходом + * @param $query + * @return _IH_MisSurgicalOperation_QB|_IH_MisSurgicalOperation_QB + */ + public function scopeNotPositiveResult($query) + { + return $query->where('rf_OperationResultID', 2); + } + + + /** + * Получить отмененные операции через назначение + * @param $query + * @return _IH_MisSurgicalOperation_QB + */ + public function scopeCanceled($query) { return $query->whereHas('operationPurpose', function ($purposeQuery) { - $purposeQuery->where('rf_OperationStatusID', self::COMPLETED_OPERATION_STATUS_ID); + $purposeQuery->where('rf_OperationStatusID', self::CANCELLED_OPERATION_STATUS_ID); }); } } diff --git a/app/Models/Report.php b/app/Models/Report.php index 7b7fb91..797bd8e 100644 --- a/app/Models/Report.php +++ b/app/Models/Report.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; @@ -29,6 +30,32 @@ class Report extends Model 'status', ]; + protected $casts = [ + 'created_at' => 'datetime', + 'sent_at' => 'datetime', + 'period_start' => 'datetime', + 'period_end' => 'datetime', + ]; + + public function scopeWithinPeriod(Builder $query, string $startAt, string $endAt): Builder + { + return $query + ->where('period_start', '>=', $startAt) + ->where('period_start', '<=', $endAt); + } + + public function scopeExactPeriod(Builder $query, string $startAt, string $endAt): Builder + { + return $query + ->where('period_start', '>=', $startAt) + ->where('period_end', '<=', $endAt); + } + + public function scopeOnlySubmitted(Builder $query): Builder + { + return $query->where('status', 'submitted'); + } + public function metrikaResults(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(MetrikaResult::class, 'rf_report_id', 'report_id'); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..56f4373 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use App\Services\Cache\CacheInvalidator; +use App\Services\Cache\CacheKeyBuilder; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -11,7 +13,11 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton(CacheKeyBuilder::class, function () { + return new CacheKeyBuilder(version: 'v1'); + }); + + $this->app->singleton(CacheInvalidator::class); } /** diff --git a/app/Services/AutoReportService.php b/app/Services/AutoReportService.php index ed229f5..0ef475a 100644 --- a/app/Services/AutoReportService.php +++ b/app/Services/AutoReportService.php @@ -70,7 +70,7 @@ class AutoReportService // Проверяем, существует ли уже отчет на эту дату $existingReport = Report::where('rf_department_id', $department->department_id) - ->whereDate('sent_at', $dateRange->endSql()) + ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) ->first(); if ($existingReport && !$force) { diff --git a/app/Services/BedDayService.php b/app/Services/BedDayService.php index a9a00a0..ed44364 100644 --- a/app/Services/BedDayService.php +++ b/app/Services/BedDayService.php @@ -41,9 +41,8 @@ class BedDayService // Находим отчеты за период $reports = Report::where('rf_department_id', $departmentId) -// ->whereBetween('created_at', [$actualStartDate, $endDate]) - ->where('sent_at', '>=', $actualStartDate) - ->where('sent_at', '<=', $endDate) + ->where('period_start', '>=', $actualStartDate) + ->where('period_end', '<', $endDate) ->pluck('report_id'); if ($reports->isEmpty()) { @@ -109,9 +108,8 @@ class BedDayService // Находим все отчеты за период по отделениям $reportsByDepartment = Report::whereIn('rf_department_id', $departmentIds) -// ->whereBetween('created_at', [$actualStartDate, $endDate]) - ->where('sent_at', '>=', $actualStartDate) - ->where('sent_at', '<=', $endDate) + ->where('period_start', '>=', $actualStartDate) + ->where('period_end', '<', $endDate) ->select('report_id', 'rf_department_id') ->get() ->groupBy('rf_department_id'); @@ -190,9 +188,8 @@ class BedDayService public function getDetailedStatsFromSnapshots(int $departmentId, string $startDate, string $endDate): array { $reports = Report::where('rf_department_id', $departmentId) -// ->whereBetween('created_at', [$startDate, $endDate]) - ->where('sent_at', '>', $startDate) - ->where('sent_at', '<=', $endDate) + ->where('period_start', '>=', $startDate) + ->where('period_end', '<', $endDate) ->pluck('report_id'); if ($reports->isEmpty()) { @@ -296,7 +293,7 @@ class BedDayService foreach ($reports as $report) { // Для каждого отчета считаем средний койко-день за последние 30 дней до даты отчета - $endDate = $report->created_at; + $endDate = $report->period_end; $startDate = Carbon::startOfYear(); $avg = $this->getAverageBedDaysFromSnapshots( diff --git a/app/Services/CurrentPatientService.php b/app/Services/CurrentPatientService.php new file mode 100644 index 0000000..86cfdbf --- /dev/null +++ b/app/Services/CurrentPatientService.php @@ -0,0 +1,81 @@ +getHistoricalCurrentMedicalHistoryIds($type, $branchId, $dateRange); + } + + $typeIds = match($type) { + 'plan' => self::PLAN_STATUSES, + 'emergency' => self::EMERGENCY_STATUSES, + default => null + }; + + return MisMigrationPatient::currentlyInTreatment($branchId) + ->whereHas('medicalHistory', function ($q) use ($typeIds) { + $q->whereIn('rf_EmerSignID', $typeIds); + }) + ->pluck('rf_MedicalHistoryID') + ->toArray(); + } + + public function getHistoricalCurrentMedicalHistoryIds(?string $type, int $branchId, DateRange $dateRange): array + { + $endAt = $dateRange->endSql(); + + $typeIds = match($type) { + 'plan' => self::PLAN_STATUSES, + 'emergency' => self::EMERGENCY_STATUSES, + default => null + }; + + $lastMovementSubquery = DB::table('stt_migrationpatient as mp') + ->selectRaw(' + mp."rf_MedicalHistoryID", + mp."rf_StationarBranchID", + mp."DateIngoing", + mp."SystemDate", + mp."MigrationPatientID", + row_number() over ( + partition by mp."rf_MedicalHistoryID" + order by mp."DateIngoing" desc, mp."SystemDate" desc, mp."MigrationPatientID" desc + ) as rn + ') + ->where('mp.rf_MedicalHistoryID', '<>', 0) + ->where('mp.DateIngoing', '<', $endAt); + + return DB::query() + ->fromSub($lastMovementSubquery, 'last_mp') + ->leftJoin('stt_medicalhistory as mh', 'mh.MedicalHistoryID', '=', 'last_mp.rf_MedicalHistoryID') + ->where('last_mp.rn', 1) + ->where('last_mp.rf_StationarBranchID', $branchId) + ->where(function ($q) use ($endAt) { + $q->whereNull('mh.DateExtract') + ->orWhere('mh.DateExtract', '>=', $endAt) + ->orWhereDate('mh.DateExtract', '1900-01-01') + ->orWhereDate('mh.DateExtract', '2222-01-01'); + }) + ->where(function ($q) use ($endAt) { + $q->whereNull('mh.DateDeath') + ->orWhere('mh.DateDeath', '>=', $endAt) + ->orWhereDate('mh.DateDeath', '1900-01-01') + ->orWhereDate('mh.DateDeath', '2222-01-01'); + }) + ->when($typeIds, function ($q) use ($typeIds) { + $q->whereIn('mh.rf_EmerSignID', $typeIds); + }) + ->pluck('last_mp.rf_MedicalHistoryID') + ->toArray(); + } +} diff --git a/app/Services/DateRangeService.php b/app/Services/DateRangeService.php index 3263803..66369e6 100644 --- a/app/Services/DateRangeService.php +++ b/app/Services/DateRangeService.php @@ -39,6 +39,34 @@ class DateRangeService ); } + /** + * Получить диапазон дат для страницы /report: + * всегда сутки (07:00 предыдущего дня -> 07:00 выбранного дня) + */ + public function getReportDateRangeFromRequest(Request $request, User $user): DateRange + { + $endAtInput = $request->query('endAt', $request->get('endAt')); + $startAtInput = $request->query('startAt', $request->get('startAt')); + $endDate = $endAtInput + ? $this->parseDate($endAtInput) + : Carbon::now('Asia/Yakutsk'); + + $startDate = $startAtInput + ? $this->parseDate($startAtInput) + : Carbon::now('Asia/Yakutsk'); + + $endDate = $endDate->copy()->setTime(7, 0); + $startDate = $startDate->copy()->subDay()->setTime(7, 0); + + return new DateRange( + startDate: $startDate, + endDate: $endDate, + startDateRaw: $startDate->format('Y-m-d H:i:s'), + endDateRaw: $endDate->format('Y-m-d H:i:s'), + isOneDay: true + ); + } + /** * Получить диапазон дат для статистики (по умолчанию с начала года) */ @@ -56,6 +84,7 @@ class DateRangeService // По умолчанию: с начала года до сегодня $startDate = Carbon::now('Asia/Yakutsk') ->startOfYear() // 1 января текущего года + ->subDay() ->setTime(7, 0); $endDate = Carbon::now('Asia/Yakutsk') diff --git a/app/Services/MetricCalculators/AverageBedDaysCalculator.php b/app/Services/MetricCalculators/AverageBedDaysCalculator.php index 209cd57..5066243 100644 --- a/app/Services/MetricCalculators/AverageBedDaysCalculator.php +++ b/app/Services/MetricCalculators/AverageBedDaysCalculator.php @@ -24,9 +24,8 @@ class AverageBedDaysCalculator extends BaseMetricService implements MetricCalcul ->join('metrika_results as mr', 'r.report_id', '=', 'mr.rf_report_id') ->whereIn('r.rf_department_id', $departmentIds) ->where('mr.rf_metrika_item_id', 18) -// ->whereBetween('r.created_at', [$startDate, $endDate]) - ->where('r.sent_at', '>', $startDate) - ->where('r.sent_at', '<=', $endDate) + ->where('r.period_start', '>=', $startDate) + ->where('r.period_end', '<', $endDate) ->select( 'r.rf_department_id', DB::raw('AVG(CAST(mr.value AS DECIMAL)) as avg_value') diff --git a/app/Services/MetricCalculators/LethalityCalculator.php b/app/Services/MetricCalculators/LethalityCalculator.php index f8f9a25..146e546 100644 --- a/app/Services/MetricCalculators/LethalityCalculator.php +++ b/app/Services/MetricCalculators/LethalityCalculator.php @@ -24,9 +24,8 @@ class LethalityCalculator extends BaseMetricService implements MetricCalculatorI ->join('metrika_results as mr', 'r.report_id', '=', 'mr.rf_report_id') ->whereIn('r.rf_department_id', $departmentIds) ->whereIn('mr.rf_metrika_item_id', [7, 9]) -// ->whereBetween('r.created_at', [$startDate, $endDate]) - ->where('r.sent_at', '>', $startDate) - ->where('r.sent_at', '<=', $endDate) + ->where('r.period_start', '>=', $startDate) + ->where('r.period_end', '<', $endDate) ->select( 'r.rf_department_id', 'mr.rf_metrika_item_id', diff --git a/app/Services/MetricCalculators/PreoperativeDaysCalculator.php b/app/Services/MetricCalculators/PreoperativeDaysCalculator.php index 454c6d5..014c20c 100644 --- a/app/Services/MetricCalculators/PreoperativeDaysCalculator.php +++ b/app/Services/MetricCalculators/PreoperativeDaysCalculator.php @@ -25,9 +25,8 @@ class PreoperativeDaysCalculator extends BaseMetricService implements MetricCalc // Получаем отчеты за период $reports = Report::whereIn('rf_department_id', $departmentIds) - ->where('sent_at', '>', $startDate) - ->where('sent_at', '<=', $endDate) -// ->whereBetween('created_at', [$startDate, $endDate]) + ->where('period_start', '>=', $startDate) + ->where('period_end', '<', $endDate) ->get(['report_id', 'rf_department_id']) ->keyBy('report_id'); diff --git a/app/Services/MetrikaService.php b/app/Services/MetrikaService.php index 157992b..fd07623 100644 --- a/app/Services/MetrikaService.php +++ b/app/Services/MetrikaService.php @@ -28,8 +28,8 @@ class MetrikaService ->join('stt_migrationpatient as mp', 'mhs.rf_medicalhistory_id', '=', 'mp.rf_MedicalHistoryID') ->join('stt_surgicaloperation as so', 'mhs.rf_medicalhistory_id', '=', 'so.rf_MedicalHistoryID') ->whereIn('r.rf_department_id', $departmentIds) - ->whereDate('r.sent_at', '>=', $startDate) - ->whereDate('r.sent_at', '<=', $endDate) + ->where('r.period_start', '>=', $startDate) + ->where('r.period_end', '<', $endDate) ->whereIn('mhs.patient_type', ['discharged', 'deceased']) ->select( 'r.rf_department_id', diff --git a/app/Services/PatientService.php b/app/Services/PatientService.php index 617393b..d4c246d 100644 --- a/app/Services/PatientService.php +++ b/app/Services/PatientService.php @@ -7,11 +7,16 @@ use App\Models\MisMigrationPatient; use App\Models\MisReanimation; use App\Models\MisSurgicalOperation; use App\Models\ObservationPatient; -use Carbon\Carbon; -use Illuminate\Support\Facades\DB; class PatientService { + public function __construct( + protected OutcomePatientService $outcomePatientService, + protected RecipientPatientService $recipientPatientService, + protected CurrentPatientService $currentPatientService + ) + { } + /** * Получить плановых или экстренных пациентов */ @@ -25,36 +30,83 @@ class PatientService bool $includeCurrent = false, bool $fillableAuto = false ) { - // Получаем поступивших сегодня - $recipientQuery = $this->buildRecipientQuery($type, $isHeadOrAdmin, $branchId, $dateRange, $fillableAuto); - if ($fillableAuto) - $recipientIds = $recipientQuery->distinct()->pluck('rf_MedicalHistoryID')->toArray(); - else - $recipientIds = $recipientQuery->pluck('rf_MedicalHistoryID')->toArray(); - - // Если нужно добавить уже находящихся в отделении - if ($includeCurrent) { - if ($fillableAuto) { - $currentIds = $this->getHistoricalCurrentMedicalHistoryIds($branchId, $dateRange); - } else { - $currentIds = MisMigrationPatient::currentlyInTreatment($branchId) - ->pluck('rf_MedicalHistoryID') - ->toArray(); - } - - $medicalHistoryIds = array_unique(array_merge($recipientIds, $currentIds)); - } else { - $medicalHistoryIds = $recipientIds; - } + $medicalHistoryIds = $this->recipientPatientService->resolvePlanOrEmergencyMedicalHistoryIds( + $type, + $isHeadOrAdmin, + $branchId, + $dateRange, + $includeCurrent, + $fillableAuto + ); if (empty($medicalHistoryIds)) { + return $countOnly ? 0 : collect(); + } + + if ($countOnly) { + return count($medicalHistoryIds); + } + + if ($onlyIds) { + return collect($medicalHistoryIds); + } + + $recipientIds = $this->recipientPatientService->getRecipientMedicalHistoryIds( + $type, + $isHeadOrAdmin, + $branchId, + $dateRange + ); + + $res = $this->buildPatientCardsQuery($medicalHistoryIds, $branchId); + + return $res->get() + ->map(function ($patient) use ($recipientIds) { + $patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds, true); + return $patient; + }); + + } + + /** + * Получить всех пациентов в отделении (поступившие сегодня + уже лечащиеся) + */ + public function getAllPatientsInDepartment( + bool $isHeadOrAdmin, + int $branchId, + DateRange $dateRange, + bool $countOnly = false, + bool $onlyIds = false, + bool $fillableAuto = false + ) { + $recipientIds = $this->recipientPatientService->buildRecipientQuery(null, $isHeadOrAdmin, $branchId, $dateRange, $fillableAuto) + ->pluck('rf_MedicalHistoryID') + ->toArray(); + + if ($fillableAuto) { + $currentIds = $this->currentPatientService->getHistoricalCurrentMedicalHistoryIds(null, $branchId, $dateRange); + } else { + $currentIds = MisMigrationPatient::currentlyInTreatment($branchId) + ->pluck('rf_MedicalHistoryID') + ->toArray(); + } + + $allIds = array_unique(array_merge($recipientIds, $currentIds)); + + if (empty($allIds)) { if ($countOnly) return 0; - if ($onlyIds) return collect(); return collect(); } - // Получаем истории - $query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds) + if ($countOnly) { + return count($allIds); + } + + if ($onlyIds) { + return collect($allIds); + } + + $res = MisMedicalHistory::whereIn('MedicalHistoryID', $allIds) ->select([ 'MedicalHistoryID', 'FAMILY', @@ -83,10 +135,10 @@ class PatientService }, 'outcomeMigration' => function ($q) { $q->select([ - 'MigrationPatientID', - 'rf_MedicalHistoryID', - 'DateOut', - 'rf_DiagnosID', + 'stt_migrationpatient.MigrationPatientID', + 'stt_migrationpatient.rf_MedicalHistoryID', + 'stt_migrationpatient.DateOut', + 'stt_migrationpatient.rf_DiagnosID', ])->with(['mainDiagnosis' => function ($diagnosisQuery) { $diagnosisQuery->select([ 'DiagnosID', @@ -127,145 +179,10 @@ class PatientService ]) ->orderBy('DateRecipient', 'DESC'); - // Фильтруем по типу (план/экстренные) - if ($type === 'plan') { - $query->plan(); - } elseif ($type === 'emergency') { - $query->emergency(); - } - - if ($countOnly) { - return $query->count(); - } - - if ($onlyIds) { - return $query->pluck('MedicalHistoryID'); - } - - return $query->get()->map(function ($patient) use ($recipientIds) { - // Добавляем флаг "поступил сегодня" - $patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds); - return $patient; - }); - } - - /** - * Получить всех пациентов в отделении (поступившие сегодня + уже лечащиеся) - */ - public function getAllPatientsInDepartment( - bool $isHeadOrAdmin, - int $branchId, - DateRange $dateRange, - bool $countOnly = false, - bool $onlyIds = false, - bool $fillableAuto = false - ) { - // Поступившие сегодня - $recipientIds = $this->buildRecipientQuery(null, $isHeadOrAdmin, $branchId, $dateRange, $fillableAuto) - ->pluck('rf_MedicalHistoryID') - ->toArray(); - - // Уже находящиеся на лечении - if ($fillableAuto) { - $currentIds = $this->getHistoricalCurrentMedicalHistoryIds($branchId, $dateRange); - } else { - $currentIds = MisMigrationPatient::currentlyInTreatment($branchId) - ->pluck('rf_MedicalHistoryID') - ->toArray(); - } - - // Объединяем и убираем дубли - $allIds = array_unique(array_merge($recipientIds, $currentIds)); - - if (empty($allIds)) { - if ($countOnly) return 0; - return collect(); - } - - if ($countOnly) { - return count($allIds); - } - - if ($onlyIds) { - return collect($allIds); - } - - return MisMedicalHistory::whereIn('MedicalHistoryID', $allIds) - ->select([ - 'MedicalHistoryID', - 'FAMILY', - 'Name', - 'OT', - 'BD', - 'DateRecipient', - 'DateExtract', - 'rf_EmerSignID', - 'rf_kl_VisitResultID', - ]) - ->with([ - 'surgicalOperations' => function ($q) { - $q->select([ - 'SurgicalOperationID', - 'rf_MedicalHistoryID', - 'rf_kl_ServiceMedicalID', - 'Date', - ])->with(['serviceMedical' => function ($serviceQuery) { - $serviceQuery->select([ - 'ServiceMedicalID', - 'ServiceMedicalCode', - 'ServiceMedicalName', - ]); - }]); - }, - 'outcomeMigration' => function ($q) { - $q->select([ - 'MigrationPatientID', - 'rf_MedicalHistoryID', - 'DateOut', - 'rf_DiagnosID', - ])->with(['mainDiagnosis' => function ($diagnosisQuery) { - $diagnosisQuery->select([ - 'DiagnosID', - 'rf_MKBID', - ])->with(['mkb' => function ($mkbQuery) { - $mkbQuery->select([ - 'MKBID', - 'DS', - 'NAME', - ]); - }]); - }]); - }, - 'migrations' => function ($q) use ($branchId) { - $q->where('rf_StationarBranchID', $branchId) - ->select([ - 'MigrationPatientID', - 'rf_MedicalHistoryID', - 'rf_DiagnosID', - 'DateIngoing', - 'rf_StationarBranchID', - ]) - ->orderByDesc('DateIngoing') - ->with(['mainDiagnosis' => function ($diagnosisQuery) { - $diagnosisQuery->select([ - 'DiagnosID', - 'rf_MKBID', - 'rf_MigrationPatientID', - ])->with(['mkb' => function ($mkbQuery) { - $mkbQuery->select([ - 'MKBID', - 'DS', - 'NAME', - ]); - }]); - }]); - }, - ]) - ->orderBy('DateRecipient', 'DESC') - ->get() - ->map(function ($patient) use ($recipientIds) { - $patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds); - return $patient; + return $res->get() + ->map(function ($patient) use ($recipientIds) { + $patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds); + return $patient; }); } @@ -288,9 +205,8 @@ class PatientService if ($onlyIds) $patients = $query->pluck('MedicalHistoryID'); else { - // Загрузка отношений, необходимых для FormattedPatientResource $query->with([ - 'outcomeMigration.mainDiagnosis.mkb', // mkb через диагноз + 'outcomeMigration.mainDiagnosis.mkb', ]); $patients = $query->get(); } @@ -313,73 +229,7 @@ class PatientService string $outcomeType = 'all', bool $onlyIds = false ) { - $query = MisMedicalHistory::query() - ->where('MedicalHistoryID', '<>', 0) - ->whereHas('migrations', function ($migrationQuery) use ($branchId, $outcomeType) { - $migrationQuery->where('rf_StationarBranchID', $branchId); - - if ($outcomeType === 'deceased') { - $migrationQuery->whereIn('rf_kl_VisitResultID', [5, 6, 15, 16]); - } elseif ($outcomeType === 'transferred') { - $migrationQuery->whereIn('rf_kl_VisitResultID', [4, 14]); - } elseif ($outcomeType === 'discharged') { - $migrationQuery->whereIn('rf_kl_VisitResultID', [1, 11, 2, 12, 7, 18, 48]); - } elseif ($outcomeType === 'without-transferred') { - $migrationQuery->whereNotIn('rf_kl_VisitResultID', [4, 14]) - ->where('rf_kl_VisitResultID', '<>', 0); - } - }); - - if ($dateRange->isOneDay) { - $query->where('DateExtract', '>', $dateRange->startSql()) - ->where('DateExtract', '<=', $dateRange->endSql()); - } else { - $startAt = $dateRange->startSql(); - $endDate = $dateRange->end()->toDateString(); - $query->where('DateExtract', '>', $startAt) - ->whereDate('DateExtract', '<=', $endDate); - } - - if ($onlyIds) { - return $query->pluck('MedicalHistoryID'); - } - - return $query - ->select([ - 'MedicalHistoryID', - 'FAMILY', - 'Name', - 'OT', - 'BD', - 'DateRecipient', - 'DateExtract', - 'rf_EmerSignID', - 'rf_kl_VisitResultID', - ]) - ->with([ - 'outcomeMigration' => function ($q) { - $q->select([ - 'MigrationPatientID', - 'rf_MedicalHistoryID', - 'DateOut', - 'rf_DiagnosID', - ])->with(['mainDiagnosis' => function ($diagnosisQuery) { - $diagnosisQuery->select([ - 'DiagnosID', - 'rf_MKBID', - ])->with(['mkb' => function ($mkbQuery) { - $mkbQuery->select([ - 'MKBID', - 'DS', - 'NAME', - ]); - }]); - }]); - }, - ]) - ->orderBy('DateRecipient', 'DESC') - ->get() - ->map(fn ($patient) => $this->addOutcomeInfo($patient)); + return $this->outcomePatientService->getOutcomePatients($branchId, $dateRange, $outcomeType, $onlyIds); } /** @@ -443,10 +293,10 @@ class PatientService }, 'outcomeMigration' => function ($q) { $q->select([ - 'MigrationPatientID', - 'rf_MedicalHistoryID', - 'DateOut', - 'rf_DiagnosID', + 'stt_migrationpatient.MigrationPatientID', + 'stt_migrationpatient.rf_MedicalHistoryID', + 'stt_migrationpatient.DateOut', + 'stt_migrationpatient.rf_DiagnosID', ])->with(['mainDiagnosis' => function ($diagnosisQuery) { $diagnosisQuery->select([ 'DiagnosID', @@ -478,7 +328,6 @@ class PatientService ->completed() ->where('Date', '>=', $dateRange->startSql()) ->where('Date', '<=', $dateRange->endSql()); -// ->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]); if ($type === 'plan') { $query->where('rf_TypeSurgOperationInTimeID', 6); @@ -493,164 +342,6 @@ class PatientService return $query->get(); } - /** - * Получить текущих пациентов - */ - public function getCurrentPatients(int $branchId, bool $countOnly = false) - { - $medicalHistoryIds = MisMigrationPatient::currentlyInTreatment($branchId) - ->pluck('rf_MedicalHistoryID') - ->unique() - ->toArray(); - - if (empty($medicalHistoryIds)) { - return $countOnly ? 0 : collect(); - } - - $query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds) - ->currentlyHospitalized() - ->with(['surgicalOperations']) - ->orderBy('DateRecipient', 'DESC'); - - if ($countOnly) { - return $query->count(); - } - - return $query->get(); - } - - /** - * Собрать базовый запрос для пациентов - */ - private function buildPatientQuery( - ?string $type, - bool $isHeadOrAdmin, - int $branchId, - DateRange $dateRange - ) { - if ($isHeadOrAdmin) { - $query = MisMigrationPatient::whereInDepartment($branchId) - ->where('DateIngoing', '>=', $dateRange->startSql()) - ->where('DateIngoing', '<=', $dateRange->endSql()); -// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]); - } else { - $query = MisMigrationPatient::currentlyInTreatment($branchId) - ->where('DateIngoing', '>=', $dateRange->startSql()) - ->where('DateIngoing', '<=', $dateRange->endSql()); -// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]); - } - - $medicalHistoryIds = $query->pluck('rf_MedicalHistoryID')->toArray(); - - if (empty($medicalHistoryIds)) { - return MisMedicalHistory::whereRaw('1 = 0'); - } - - $query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds); - - if ($type === 'plan') { - $query->plan(); - } elseif ($type === 'emergency') { - $query->emergency(); - } - - if (!$isHeadOrAdmin && !in_array($type, ['discharged', 'transferred', 'deceased'])) { - $query->currentlyHospitalized(); - } - - return $query; - } - - /** - * Построить запрос для поступивших пациентов - */ - private function buildRecipientQuery( - ?string $type, - bool $isHeadOrAdmin, - int $branchId, - DateRange $dateRange, - bool $fillableAuto = false - ) { - $startAt = $dateRange->start()->copy()->subDay()->format('Y-m-d H:i:s'); - $endAt = $dateRange->end()->copy()->addDay()->format('Y-m-d H:i:s'); - - if ($dateRange->isOneDay) { - $startAt = $dateRange->startSql(); - $endAt = $dateRange->endSql(); - } - - $query = DB::table('stt_medicalhistory as mh') - ->selectRaw('mh."MedicalHistoryID" as "rf_MedicalHistoryID"') - ->where('mh.MedicalHistoryID', '<>', 0); - - $query->whereExists(function ($subQuery) use ($branchId, $startAt, $endAt) { - $subQuery->select(DB::raw(1)) - ->from('stt_migrationpatient as mp') - ->whereColumn('mp.rf_MedicalHistoryID', 'mh.MedicalHistoryID') - ->where('mp.rf_StationarBranchID', $branchId) - ->where('mp.DateIngoing', '>', $startAt) - ->where('mp.DateIngoing', '<=', $endAt); - }); - - if ($type === 'plan') { - $query->where('mh.rf_EmerSignID', 1); - } elseif ($type === 'emergency') { - $query->whereIn('mh.rf_EmerSignID', [2, 4]); - } - - return $query->distinct(); - } - - private function getHistoricalCurrentMedicalHistoryIds(int $branchId, DateRange $dateRange): array - { - // Исторический срез по основной таблице миграций: - // для каждой истории болезни берём последнюю миграцию в отделении - // на момент конца периода и проверяем, что пациент числился в отделении. - $latestRows = DB::table('stt_migrationpatient') - ->select('rf_MedicalHistoryID', DB::raw('MAX("MigrationPatientID") as max_migration_patient_id')) - ->where('rf_StationarBranchID', $branchId) - ->where('rf_MedicalHistoryID', '<>', 0) - ->where('DateIngoing', '<=', $dateRange->endSql()) - ->groupBy('rf_MedicalHistoryID'); - - return DB::table('stt_migrationpatient as mp') - ->joinSub($latestRows, 'latest', function ($join) { - $join->on('mp.rf_MedicalHistoryID', '=', 'latest.rf_MedicalHistoryID') - ->on('mp.MigrationPatientID', '=', 'latest.max_migration_patient_id'); - }) - ->join('stt_medicalhistory as mh', 'mh.MedicalHistoryID', '=', 'mp.rf_MedicalHistoryID') - ->where('mp.rf_StationarBranchID', $branchId) - ->where('mh.DateRecipient', '<=', $dateRange->endSql()) - ->where('mp.DateOut', '>=', $dateRange->endSql()) - ->where(function ($query) use ($dateRange) { - $query->where('mh.DateExtract', '>', $dateRange->endSql()) - ->orWhereDate('mh.DateExtract', '1900-01-01'); - }) - ->distinct() - ->pluck('mp.rf_MedicalHistoryID') - ->toArray(); - } - - /** - * Добавить информацию об исходе пациенту - */ - private function addOutcomeInfo(MisMedicalHistory $patient) - { - $latestMigration = $patient->migrations - ->whereNotNull('DateOut') - ->where('DateOut', '<>', '1900-01-01') - ->sortByDesc('DateOut') - ->first(); - - if ($latestMigration) { - $patient->outcome_type = $this->getOutcomeTypeName($latestMigration->rf_kl_VisitResultID); - $patient->outcome_date = $latestMigration->DateOut; - $patient->visit_result_id = $latestMigration->rf_kl_VisitResultID; - } - - return $patient; - } - /** * Получить количество пациентов по типу с учетом уже находящихся в отделении */ @@ -671,16 +362,81 @@ class PatientService ); } - /** - * Получить название типа исхода - */ - private function getOutcomeTypeName(int $visitResultId): string + private function buildPatientCardsQuery(array $medicalHistoryIds, int $branchId) { - return match($visitResultId) { - 1, 7, 8, 9, 10, 11, 48, 49, 124 => 'Выписка', - 2, 3, 4, 12, 13, 14 => 'Перевод', - 5, 6, 15, 16 => 'Умер', - default => 'Другое (' . $visitResultId . ')' - }; + return MisMedicalHistory::query() + ->whereIn('MedicalHistoryID', $medicalHistoryIds) + ->select([ + 'MedicalHistoryID', + 'FAMILY', + 'Name', + 'OT', + 'BD', + 'DateRecipient', + 'DateExtract', + 'DateDeath', + 'rf_EmerSignID', + 'rf_kl_VisitResultID', + ]) + ->with([ + 'surgicalOperations' => function ($q) { + $q->select([ + 'SurgicalOperationID', + 'rf_MedicalHistoryID', + 'rf_kl_ServiceMedicalID', + 'Date', + ])->with(['serviceMedical' => function ($serviceQuery) { + $serviceQuery->select([ + 'ServiceMedicalID', + 'ServiceMedicalCode', + 'ServiceMedicalName', + ]); + }]); + }, + 'outcomeMigration' => function ($q) { + $q->select([ + 'stt_migrationpatient.MigrationPatientID', + 'stt_migrationpatient.rf_MedicalHistoryID', + 'stt_migrationpatient.DateOut', + 'stt_migrationpatient.rf_DiagnosID', + ])->with(['mainDiagnosis' => function ($diagnosisQuery) { + $diagnosisQuery->select([ + 'DiagnosID', + 'rf_MKBID', + ])->with(['mkb' => function ($mkbQuery) { + $mkbQuery->select([ + 'MKBID', + 'DS', + 'NAME', + ]); + }]); + }]); + }, + 'migrations' => function ($q) use ($branchId) { + $q->where('rf_StationarBranchID', $branchId) + ->select([ + 'MigrationPatientID', + 'rf_MedicalHistoryID', + 'rf_DiagnosID', + 'DateIngoing', + 'rf_StationarBranchID', + ]) + ->orderByDesc('DateIngoing') + ->with(['mainDiagnosis' => function ($diagnosisQuery) { + $diagnosisQuery->select([ + 'DiagnosID', + 'rf_MKBID', + 'rf_MigrationPatientID', + ])->with(['mkb' => function ($mkbQuery) { + $mkbQuery->select([ + 'MKBID', + 'DS', + 'NAME', + ]); + }]); + }]); + }, + ]) + ->orderBy('DateRecipient', 'DESC'); } } diff --git a/app/Services/RecipientPatientService.php b/app/Services/RecipientPatientService.php new file mode 100644 index 0000000..12cc653 --- /dev/null +++ b/app/Services/RecipientPatientService.php @@ -0,0 +1,101 @@ +buildRecipientQuery($type, $isHeadOrAdmin, $branchId, $dateRange, false) + ->distinct() + ->pluck('rf_MedicalHistoryID') + ->toArray(); + } + + /** + * Построить запрос для поступивших пациентов + */ + public function buildRecipientQuery( + ?string $type, + bool $isHeadOrAdmin, + int $branchId, + DateRange $dateRange, + bool $fillableAuto = false + ) { + $startAt = $dateRange->start()->copy()->format('Y-m-d H:i:s'); + $endAt = $dateRange->end()->copy()->format('Y-m-d H:i:s'); + + if ($dateRange->isOneDay) { + $startAt = $dateRange->startSql(); + $endAt = $dateRange->endSql(); + } + + $query = DB::table('stt_medicalhistory as mh') + ->selectRaw('mh."MedicalHistoryID" as "rf_MedicalHistoryID"') + ->where('mh.MedicalHistoryID', '<>', 0); + + $query->whereExists(function ($subQuery) use ($branchId, $startAt, $endAt) { + $subQuery->select(DB::raw(1)) + ->from('stt_migrationpatient as mp') + ->whereColumn('mp.rf_MedicalHistoryID', 'mh.MedicalHistoryID') + ->where('mp.rf_StationarBranchID', $branchId) + ->where('mp.DateIngoing', '>', $startAt) + ->where('mp.DateIngoing', '<=', $endAt); + }); + + if ($type === 'plan') { + $query->where('mh.rf_EmerSignID', 1); + } elseif ($type === 'emergency') { + $query->whereIn('mh.rf_EmerSignID', [2, 4]); + } + + return $query->distinct(); + } + + // Получить IDs состоящих + private function getCurrentMedicalHistoryIds( + string $type, + int $branchId, + DateRange $dateRange, + bool $fillableAuto, + ): array { + return $this->currentPatientService->getCurrentMedicalHistoryIds($type, $branchId, $dateRange, $fillableAuto); + } + + public function resolvePlanOrEmergencyMedicalHistoryIds( + ?string $type, + bool $isHeadOrAdmin, + int $branchId, + DateRange $dateRange, + bool $includeCurrent, + bool $fillableAuto + ): array { + $recipientIds = $this->getRecipientMedicalHistoryIds( + $type, + $isHeadOrAdmin, + $branchId, + $dateRange + ); + + if (!$includeCurrent) { + return array_values(array_unique($recipientIds)); + } + + $currentIds = $this->getCurrentMedicalHistoryIds($type, $branchId, $dateRange, $fillableAuto); + + return array_values(array_unique(array_merge($recipientIds, $currentIds))); + } +} diff --git a/app/Services/ReportService.php b/app/Services/ReportService.php index e6e6489..5dfe7fd 100644 --- a/app/Services/ReportService.php +++ b/app/Services/ReportService.php @@ -126,35 +126,79 @@ class ReportService */ public function storeReport(array $data, User $user, $fillableAuto = false): Report { - $report = DB::transaction(function () use ($data, $user, $fillableAuto) { - $report = $this->createOrUpdateReport($data, $user); + $this->prepareMemoryForHeavySave(); - // Сохраняем все, что НЕ зависит от других отчетов - $this->saveMetrics($report, $data['metrics'] ?? []); - $this->saveUnwantedEvents($report, $data['unwantedEvents'] ?? []); - $this->saveObservationPatients($report, $data['observationPatients'] ?? [], $user->rf_department_id); - $this->snapshotService->createPatientSnapshots($report, $user, $data['dates'], $fillableAuto); - $this->syncCalculatedMetrics($report, $user, $data); + try { + $report = DB::transaction(function () use ($data, $user, $fillableAuto) { + $report = $this->createOrUpdateReport($data, $user); - return $report; - }); + // Сохраняем все, что НЕ зависит от других отчетов + $this->saveMetrics($report, $data['metrics'] ?? []); - DB::transaction(function () use ($report) { - // Сохраняем метрику среднего койко-дня из снапшотов - $this->saveAverageBedDaysMetricFromSnapshots($report); + $this->saveUnwantedEvents($report, $data['unwantedEvents'] ?? []); - $this->saveLethalMetricFromSnapshots($report); + $this->saveObservationPatients($report, $data['observationPatients'] ?? [], $user->rf_department_id); - $this->savePreoperativeMetric($report); + $shouldBuildSnapshots = (bool) $fillableAuto; + if ($shouldBuildSnapshots) { + $this->snapshotService->createPatientSnapshots($report, $user, $data['dates'], $fillableAuto); - $this->saveDepartmentLoadedMetric($report); - }); + $this->syncCalculatedMetrics($report, $user, $data); + } else { + MedicalHistorySnapshot::query() + ->where('rf_report_id', $report->report_id) + ->delete(); + } + + return $report; + }); + + $shouldBuildDerivedMetrics = (bool) $fillableAuto; + if ($shouldBuildDerivedMetrics) { + DB::transaction(function () use ($report) { + // Сохраняем метрику койко-дня + среднего койко-дня из снапшотов + $this->saveBedDaysMetric($report); + + $this->saveLethalMetricFromSnapshots($report); + + $this->savePreoperativeMetric($report); + + $this->saveDepartmentLoadedMetric($report); + }); + } + } catch (\Throwable $e) { + throw $e; + } $this->clearCacheAfterReportCreation($user, $report); return $report; } + private function prepareMemoryForHeavySave(): void + { + $connectionNames = array_unique(array_filter([ + DB::getDefaultConnection(), + (new MisMedicalHistory)->getConnectionName(), + (new MisMigrationPatient)->getConnectionName(), + (new MisStationarBranch)->getConnectionName(), + ])); + + foreach ($connectionNames as $connectionName) { + try { + $connection = DB::connection($connectionName); + $connection->disableQueryLog(); + $connection->flushQueryLog(); + } catch (\Throwable) { + // best-effort cleanup only + } + } + + if (function_exists('gc_collect_cycles')) { + gc_collect_cycles(); + } + } + public function buildAutoFillReportPayload(User $user, Department $department, DateRange $dateRange): array { $branchId = $this->getBranchId($department->rf_mis_department_id); @@ -170,6 +214,7 @@ class ReportService ], 'sent_at' => $dateRange->endSql(), 'created_at' => $dateRange->endSql(), + 'status' => 'submitted', 'metrics' => [ 'metrika_item_4' => $metrics['plan'], 'metrika_item_12' => $metrics['emergency'], @@ -301,73 +346,170 @@ class ReportService /** * Сохранить метрику койко-дня из снапшотов отчета */ - protected function saveAverageBedDaysMetricFromSnapshots(Report $report): void + protected function saveBedDaysMetric(Report $report): void { try { - // Получаем все снапшоты выписанных пациентов из этого отчета - $snapshots = MedicalHistorySnapshot::where('rf_report_id', $report->report_id) - ->whereIn('patient_type', ['discharged', 'deceased']) // выписанные и умершие - ->with('medicalHistory') - ->get(); + $result = $this->calculateBedDaysFromSnapshots($report); - if ($snapshots->isEmpty()) { - // Если нет выписанных, сохраняем 0 - MetrikaResult::updateOrCreate( - [ - 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => 18, - ], - ['value' => 0] - ); - - \Log::info("No discharged patients in report {$report->report_id}, saved 0"); - return; - } - - // Рассчитываем средний койко-день по снапшотам - $totalDays = 0; - $validCount = 0; - - foreach ($snapshots as $snapshot) { - $history = $snapshot->medicalHistory; - - if ($history && $history->DateRecipient && $history->DateExtract) { - // Проверяем что дата выписки не специальная - if ($history->DateExtract->format('Y-m-d') === '2222-01-01') { - continue; // пропускаем текущих пациентов - } - - $start = Carbon::parse($history->DateRecipient); - $end = Carbon::parse($history->DateExtract); - - // Проверяем что дата выписки позже даты поступления - if ($end->gt($start)) { - $days = $start->diffInDays($end); - $totalDays += $days; - $validCount++; - } - } - } - - $bedDays = $validCount > 0 ? $totalDays: 0; - - // Сохраняем метрику MetrikaResult::updateOrCreate( [ 'rf_report_id' => $report->report_id, - 'rf_metrika_item_id' => 18, + 'rf_metrika_item_id' => 25, // койко-дни ], - ['value' => $bedDays] + ['value' => $result['total_days']] ); - //\Log::info("Saved average bed days metric for report {$report->report_id}: {$avgBedDays} (from {$validCount} patients)"); - - } catch (\Exception $e) { - \Log::error("Failed to save average bed days metric: " . $e->getMessage()); - // Не прерываем выполнение, если метрика не сохранилась + MetrikaResult::updateOrCreate( + [ + 'rf_report_id' => $report->report_id, + 'rf_metrika_item_id' => 18, // средний койко-день + ], + ['value' => $result['avg_days']] + ); + } catch (\Throwable $e) { + \Log::error('Failed to save bed days metric: ' . $e->getMessage()); } } + protected function calculateBedDaysFromSnapshots(Report $report): array + { + $snapshots = MedicalHistorySnapshot::where('rf_report_id', $report->report_id) + ->whereIn('patient_type', ['discharged', 'deceased']) + ->with('medicalHistory') + ->get(); + + $totalDays = 0; + $patientCount = 0; + + foreach ($snapshots as $snapshot) { + $history = $snapshot->medicalHistory; + + if (!$history) { + continue; + } + + $start = $history->DateRecipientHS ?? $history->DateRecipient ?? null; + + if (!$start) { + continue; + } + + $end = null; + + if ($snapshot->patient_type === 'deceased') { + if ($history->DateDeath && !in_array($history->DateDeath->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { + $end = $history->DateDeath; + } elseif ($history->DateExtract && !in_array($history->DateExtract->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { + $end = $history->DateExtract; + } + } else { + if ($history->DateExtract && !in_array($history->DateExtract->format('Y-m-d'), ['1900-01-01', '2222-01-01'], true)) { + $end = $history->DateExtract; + } + } + + if (!$end) { + continue; + } + + $start = Carbon::parse($start); + $end = Carbon::parse($end); + + if ($end->lt($start)) { + continue; + } + + // Календарные койко-дни + $days = $start->startOfDay()->diffInDays($end->startOfDay()); + + $totalDays += $days; + $patientCount++; + } + + return [ + 'total_days' => $totalDays, + 'patient_count' => $patientCount, + 'avg_days' => $patientCount > 0 ? round($totalDays / $patientCount, 2) : 0, + ]; + } + + /** + * Рассчитать предоперационные койко-дни по снапшотам отчета + * + * Возвращает: + * - total_days: общее количество предоперационных койко-дней + * - patient_count: количество пациентов, вошедших в расчет + * - avg_days: средний предоперационный койко-день + */ + protected function calculatePreoperativeDaysFromSnapshots(Report $report): array + { + $patientIds = MedicalHistorySnapshot::where('rf_report_id', $report->report_id) + ->whereIn('patient_type', ['discharged', 'deceased']) + ->pluck('rf_medicalhistory_id') + ->unique() + ->values(); + + if ($patientIds->isEmpty()) { + return [ + 'total_days' => 0, + 'patient_count' => 0, + 'avg_days' => 0, + ]; + } + + $rows = DB::table('stt_medicalhistory as mh') + ->join('stt_surgicaloperation as so', 'so.rf_MedicalHistoryID', '=', 'mh.MedicalHistoryID') + ->whereIn('mh.MedicalHistoryID', $patientIds) + ->whereNotNull('so.Date') + ->select( + 'mh.MedicalHistoryID', + DB::raw('MIN(so."Date") as first_operation'), + 'mh.DateRecipientHS', + 'mh.DateRecipient' + ) + ->groupBy('mh.MedicalHistoryID', 'mh.DateRecipientHS', 'mh.DateRecipient') + ->get(); + + if ($rows->isEmpty()) { + return [ + 'total_days' => 0, + 'patient_count' => 0, + 'avg_days' => 0, + ]; + } + + $totalDays = 0; + $patientCount = 0; + + foreach ($rows as $row) { + $startRaw = $row->DateRecipientHS ?? $row->DateRecipient ?? null; + $operationRaw = $row->first_operation ?? null; + + if (!$startRaw || !$operationRaw) { + continue; + } + + $start = Carbon::parse($startRaw); + $operation = Carbon::parse($operationRaw); + + if ($operation->lt($start)) { + continue; + } + + // Разница календарных дат + $days = $start->startOfDay()->diffInDays($operation->startOfDay()); + + $totalDays += $days; + $patientCount++; + } + + return [ + 'total_days' => $totalDays, + 'patient_count' => $patientCount, + 'avg_days' => $patientCount > 0 ? round($totalDays / $patientCount, 1) : 0, + ]; + } + protected function saveLethalMetricFromSnapshots(Report $report): void { // Получаем все снапшоты выписанных пациентов из этого отчета @@ -396,70 +538,18 @@ class ReportService */ protected function savePreoperativeMetric(Report $report): void { - // 1. Получаем ВСЕ предыдущие отчеты этого отделения - $allPreviousReports = Report::where('rf_department_id', $report->rf_department_id) - ->where('sent_at', '<=', $report->sent_at) - ->orderBy('sent_at') - ->pluck('report_id'); + try { + $result = $this->calculatePreoperativeDaysFromSnapshots($report); - if ($allPreviousReports->isEmpty()) { - $this->saveMetric($report, 21, 0); - return; + $this->saveMetric($report, 26, $result['total_days']); + $this->saveMetric($report, 21, $result['avg_days']); + } catch (\Throwable $e) { + \Log::error('Failed to save preoperative total metric: ' . $e->getMessage()); } - - // 2. Получаем ВСЕХ пациентов из всех отчетов (discharged + deceased) - $allPatients = MedicalHistorySnapshot::whereIn('rf_report_id', $allPreviousReports) - ->whereIn('patient_type', ['discharged', 'deceased']) - ->pluck('rf_medicalhistory_id') - ->unique(); - - if ($allPatients->isEmpty()) { - $this->saveMetric($report, 21, 0); - return; - } - - // 3. Получаем операции для ВСЕХ пациентов - $operations = DB::table('stt_surgicaloperation as so') - ->join('stt_migrationpatient as mp', 'so.rf_MedicalHistoryID', '=', 'mp.rf_MedicalHistoryID') - ->whereIn('so.rf_MedicalHistoryID', $allPatients) - ->whereNotNull('so.Date') - ->whereNotNull('mp.DateIngoing') - ->select( - 'so.rf_MedicalHistoryID', - DB::raw('MIN(so."Date") as first_operation'), - DB::raw('MIN(mp."DateIngoing") as first_admission') - ) - ->groupBy('so.rf_MedicalHistoryID') - ->get(); - - if ($operations->isEmpty()) { - $this->saveMetric($report, 21, 0); - return; - } - - // 4. Считаем общее количество дней и пациентов - $totalDays = 0; - $patientCount = 0; - - foreach ($operations as $op) { - $days = Carbon::parse($op->first_admission) - ->diffInDays(Carbon::parse($op->first_operation)); - - if ($days >= 0) { - $totalDays += $days; - $patientCount++; - } - } - - // 5. Нарастающий итог = общее количество дней / общее количество пациентов - $avgDays = $patientCount > 0 ? round($totalDays / $patientCount, 1) : 0; - - // 6. Сохраняем метрику - $this->saveMetric($report, 21, $avgDays); } /** - * Сохранить предоперационный койко-день из снапшотов + * Сохранить % загруженности */ protected function saveDepartmentLoadedMetric(Report $report): void { @@ -523,6 +613,18 @@ class ReportService [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); $branchId = $this->getBranchId($department->rf_mis_department_id); + if ($sourceScope === 'special') { + return $this->getPatientsFromReplica( + $department, + $user, + $status, + $dateRange, + $branchId, + $onlyIds, + $includeCurrentPatients + ); + } + if ($baseStatus === 'reanimation') { return $this->getPatientsFromReplica( $department, @@ -562,9 +664,13 @@ class ReportService string $status, DateRange $dateRange ): int { - [$baseStatus] = $this->parseScopedStatus($status); + [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); $branchId = $this->getBranchId($department->rf_mis_department_id); + if ($sourceScope === 'special') { + return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId); + } + if ($baseStatus === 'reanimation') { return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId); } @@ -611,21 +717,18 @@ class ReportService ]; foreach ($baseStatuses as $baseStatus) { - $patients = collect($this->getPatientsByStatus($department, $user, $baseStatus, $dateRange)); - - $misCount = 0; - $specialCount = 0; - - foreach ($patients as $patient) { - if ($this->isSpecialScopedPatient($patient)) { - $specialCount++; - } else { - $misCount++; - } - } - - $counts["mis-{$baseStatus}"] = $misCount; - $counts["special-{$baseStatus}"] = $specialCount; + $counts["mis-{$baseStatus}"] = $this->getPatientsCountByStatus( + $department, + $user, + "mis-{$baseStatus}", + $dateRange + ); + $counts["special-{$baseStatus}"] = $this->getPatientsCountByStatus( + $department, + $user, + "special-{$baseStatus}", + $dateRange + ); } // Выбывшие = выписанные + умершие (без переведенных) @@ -649,17 +752,20 @@ class ReportService */ private function shouldUseSnapshots(Department $department, User $user, DateRange $dateRange, bool $beforeCreate = false): bool { - if (($user->isAdmin() || $user->isHeadOfDepartment()) && !$beforeCreate) { - return true; + if ($beforeCreate) { + return false; } - // Проверяем, есть ли отчет на сегодня - $reportToday = Report::whereDate('sent_at', $dateRange->end()) - ->whereDate('created_at', $dateRange->end()) - ->where('rf_department_id', $department->department_id) - ->first(); + $report = $this->getReportForPeriod($department->department_id, $dateRange); + if (!$report) { + return false; + } - return !$dateRange->isEndDateToday() || $reportToday; + if ($report->status !== 'submitted') { + return false; + } + + return true; } private function shouldUseReplicaForLiveStatus(User $user, string $status, DateRange $dateRange): bool @@ -698,6 +804,7 @@ class ReportService 'period_start' => $dateRange->startSql(), 'period_end' => $dateRange->endSql(), 'created_at' => $createdAt, + 'status' => $data['status'] ?? 'draft', ]; if (isset($data['reportId']) && $data['reportId']) { @@ -907,10 +1014,7 @@ class ReportService */ public function getCurrentReportInfo(Department $department, User $user, DateRange $dateRange): array { - $reportToday = Report::whereDate('sent_at', $dateRange->endSql()) - ->whereDate('created_at', $dateRange->endSql()) - ->where('rf_department_id', $department->department_id) - ->first(); + $reportToday = $this->getReportForPeriod($department->department_id, $dateRange); $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); $useSnapshots = $isHeadOrAdmin || !$dateRange->isEndDateToday() || $reportToday; @@ -931,8 +1035,13 @@ class ReportService $message = null; if ($reportToday) { $reportDoctor = $reportToday->lpuDoctor; - $message = "Отчет уже создан пользователем: $reportDoctor->FAM_V $reportDoctor->IM_V $reportDoctor->OT_V"; + $message = "Отчет создан пользователем: $reportDoctor->FAM_V $reportDoctor->IM_V $reportDoctor->OT_V"; } + $statusMessage = $reportToday + ? ($reportToday->status === 'submitted' + ? 'Этот отчет в статусе: опубликован' + : 'Этот отчет в статусе: черновик') + : null; // Получаем информацию о враче $lpuDoctor = $this->getDoctorInfo($fillableUserId, $dateRange); @@ -957,7 +1066,10 @@ class ReportService 'report_id' => $reportToday?->report_id, 'unwantedEvents' => $unwantedEvents, 'isActiveSendButton' => $isActiveSendButton, - 'message' => $message, + 'message' => $dateRange->isOneDay ? $message : null, + 'status' => $reportToday?->status ?? 'draft', + 'statusMessage' => $dateRange->isOneDay ? $statusMessage : null, + 'canPublish' => (bool) $reportToday && ($reportToday->status === 'draft') && $isActiveSendButton, 'isOneDay' => $dateRange->isOneDay, 'isHeadOrAdmin' => $isHeadOrAdmin, 'dates' => $date, @@ -983,14 +1095,18 @@ class ReportService public function createManualPatient(Department $department, User $user, array $data) { - return $this->unifiedPatientService->createManualPatient($department, $user, $data); + $report = $this->resolveReportForManualPatient($department, $user, $data); + + return $this->unifiedPatientService->createManualPatient($department, $user, $data, $report->report_id); } - public function setManualPatientOutcome(int $departmentPatientId, array $data) + public function setManualPatientOutcome(User $user, int $departmentPatientId, array $data) { $patient = \App\Models\DepartmentPatient::where('department_patient_id', $departmentPatientId)->firstOrFail(); + $updatedPatient = $this->unifiedPatientService->recordManualOutcome($patient, $data); + $this->syncManualPatientSnapshots($updatedPatient, $user, []); - return $this->unifiedPatientService->recordManualOutcome($patient, $data); + return $updatedPatient; } public function updateManualPatient(User $user, int $departmentPatientId, array $data) @@ -1091,20 +1207,18 @@ class ReportService private function syncManualPatientSnapshots(DepartmentPatient $patient, User $user, array $data): void { - if (!isset($data['startAt'], $data['endAt']) || !$data['startAt'] || !$data['endAt']) { - return; - } - - $dateRange = $this->dateRangeService->getNormalizedDateRange( - $user, - (string) $data['startAt'], - (string) $data['endAt'] - ); - - $reportIds = $this->getReportsForDateRange($patient->rf_department_id, $dateRange) - ->pluck('report_id') - ->values() - ->all(); + $reportIds = $patient->rf_report_id + ? [$patient->rf_report_id] + : (isset($data['startAt'], $data['endAt']) && $data['startAt'] && $data['endAt'] + ? $this->getReportsForDateRange( + $patient->rf_department_id, + $this->dateRangeService->getNormalizedDateRange( + $user, + (string) $data['startAt'], + (string) $data['endAt'] + ) + )->pluck('report_id')->values()->all() + : []); if (empty($reportIds)) { return; @@ -1120,10 +1234,53 @@ class ReportService 'diagnosis_code' => $patient->diagnosis_code, 'diagnosis_name' => $patient->diagnosis_name, 'admitted_at' => $patient->admitted_at, + 'outcome_type' => $patient->is_current ? null : $patient->outcome_type, + 'outcome_at' => $patient->is_current ? null : $patient->outcome_at, 'updated_at' => now(), ]); } + private function resolveReportForManualPatient(Department $department, User $user, array $data): Report + { + $reportId = $data['report_id'] ?? null; + if ($reportId) { + return Report::query() + ->where('report_id', $reportId) + ->where('rf_department_id', $department->department_id) + ->firstOrFail(); + } + + if (!isset($data['startAt'], $data['endAt']) || !$data['startAt'] || !$data['endAt']) { + throw new \InvalidArgumentException('Не указан отчет или диапазон для привязки спецконтингента'); + } + + $dateRange = $this->dateRangeService->getNormalizedDateRange( + $user, + (string) $data['startAt'], + (string) $data['endAt'] + ); + + $existingReport = Report::query() + ->where('rf_department_id', $department->department_id) + ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) + ->first(); + + if ($existingReport) { + return $existingReport; + } + + return Report::query()->create([ + 'rf_department_id' => $department->department_id, + 'rf_user_id' => $user->id, + 'rf_lpudoctor_id' => $data['user_id'] ?? $user->rf_lpudoctor_id, + 'sent_at' => $dateRange->endSql(), + 'created_at' => $dateRange->endSql(), + 'period_start' => $dateRange->startSql(), + 'period_end' => $dateRange->endSql(), + 'status' => 'draft', + ]); + } + /** * Получить статистику из снапшотов */ @@ -1220,9 +1377,7 @@ class ReportService // ID поступивших сегодня (для отметки в таблице) $recipientIds = $this->unifiedPatientService - ->getLivePatientsByStatus($department, $user, 'recipient', $dateRange, $branchId) - ->pluck('id') - ->all(); + ->getRecipientIdsForReport($department, $user, $dateRange, $branchId); $misBranch = MisStationarBranch::where('StationarBranchID', $branchId)->first(); $beds = Department::where('rf_mis_department_id', $misBranch->rf_DepartmentID) @@ -1356,27 +1511,15 @@ class ReportService // Для плановых и экстренных включаем уже лечащихся $includeCurrent = $isIncludeCurrent ?? in_array($baseStatus, ['plan', 'emergency'], true); - return match($status) { - 'plan', 'emergency', 'observation', 'reanimation', 'outcome', 'outcome-discharged', 'outcome-transferred', 'outcome-deceased', 'current', 'recipient' => - $this->unifiedPatientService->getLivePatientsByStatus( - $department, - $user, - $status, - $dateRange, - $branchId, - $onlyIds, - $includeCurrent - ), - default => $this->unifiedPatientService->getLivePatientsByStatus( - $department, - $user, - $status, - $dateRange, - $branchId, - $onlyIds, - $includeCurrent - ) - }; + return $this->unifiedPatientService->getLivePatientsByStatus( + $department, + $user, + $status, + $dateRange, + $branchId, + $onlyIds, + $includeCurrent + ); } /** @@ -1534,9 +1677,13 @@ class ReportService public function getUnwantedEvents(Department $department, DateRange $dateRange) { return UnwantedEvent::whereHas('report', function ($query) use ($department, $dateRange) { - $query->where('rf_department_id', $department->department_id) - ->whereDate('sent_at', '>=', $dateRange->startSql()) - ->whereDate('sent_at', '<=', $dateRange->endSql()); + $query->where('rf_department_id', $department->department_id); + + if ($dateRange->isOneDay) { + $query->exactPeriod($dateRange->startSql(), $dateRange->endSql()); + } else { + $query->withinPeriod($dateRange->startSql(), $dateRange->endSql()); + } }) ->get() ->map(function ($item) { @@ -1554,13 +1701,16 @@ class ReportService { // Для врача: только сегодня и если отчета еще нет if (!$user->isHeadOfDepartment() && !$user->isAdmin()) { - return $dateRange->isEndDateToday() && !$reportToday; + if ($reportToday && $reportToday->status === 'submitted') { + return false; + } + + return $dateRange->isEndDateToday(); } - // Для заведующего/админа: если есть отчет & он заполнен текущим пользователем & диапазон дат = 1 дню + // Для заведующего/админа: можно редактировать любой отчет за сутки (включая submitted) if ( $reportToday && - $reportToday->rf_lpudoctor_id === intval($fillableUserId) && $dateRange->isOneDay ) { return true; @@ -1569,6 +1719,19 @@ class ReportService return false; } + private function getReportForPeriod(int $departmentId, DateRange $dateRange): ?Report + { + $query = Report::query() + ->where('rf_department_id', $departmentId) + ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) + ->orderByDesc('report_id'); + + if ($dateRange->isOneDay) + return $query->first(); + else + return $query->onlySubmitted()->first(); + } + /** * Получить информацию о враче */ @@ -1593,16 +1756,16 @@ class ReportService { if ($dateRange->isOneDay) { return Report::where('rf_department_id', $departmentId) - ->whereDate('sent_at', $dateRange->endSql()) - ->orderBy('sent_at', 'DESC') + ->exactPeriod($dateRange->startSql(), $dateRange->endSql()) + ->onlySubmitted() + ->orderBy('period_end', 'DESC') ->get(); } return Report::where('rf_department_id', $departmentId) -// ->whereBetween('created_at', [$dateRange->startSql(), $dateRange->endSql()]) - ->where('sent_at', '>', $dateRange->startSql()) - ->where('sent_at', '<=', $dateRange->endSql()) - ->orderBy('sent_at', 'DESC') + ->withinPeriod($dateRange->startSql(), $dateRange->endSql()) + ->onlySubmitted() + ->orderBy('period_end', 'DESC') ->get(); } @@ -1719,15 +1882,15 @@ class ReportService $progress = 0; $query = $department->reports() ->with('metrikaResults') - ->where('sent_at', '>=', $dateRange->startSql()) - ->where('sent_at', '<=', $dateRange->endSql()); + ->where('period_start', '>', $dateRange->startSql()) + ->where('period_end', '<=', $dateRange->endSql()); if ($dateRange->isOneDay) { - $query->where('sent_at', '>=', $dateRange->startFirstOfMonth()) - ->where('sent_at', '<=', $dateRange->endSql()); + $query->where('period_start', '>=', $dateRange->startFirstOfMonth()) + ->where('period_end', '<=', $dateRange->endSql()); } else { - $query->where('sent_at', '>=', $dateRange->startSql()) - ->where('sent_at', '<=', $dateRange->endSql()); + $query->where('period_start', '>', $dateRange->startSql()) + ->where('period_end', '<=', $dateRange->endSql()); } $reports = $query->get(); diff --git a/app/Services/SnapshotService.php b/app/Services/SnapshotService.php index cc57209..f970d76 100644 --- a/app/Services/SnapshotService.php +++ b/app/Services/SnapshotService.php @@ -27,6 +27,12 @@ class SnapshotService */ public function createPatientSnapshots(Report $report, User $user, array $dates, $fillableAuto = false): void { + $this->logSnapshotMemory('snapshots:start', [ + 'report_id' => $report->report_id, + 'department_id' => $report->rf_department_id, + 'fillable_auto' => (bool) $fillableAuto, + ]); + $department = Department::query()->where('department_id', $report->rf_department_id)->first() ?? $user->department; $branchId = $department ? $this->getBranchId($department->rf_mis_department_id) @@ -39,11 +45,15 @@ class SnapshotService MedicalHistorySnapshot::query() ->where('rf_report_id', $report->report_id) ->delete(); + $this->logSnapshotMemory('snapshots:after_delete_old', [ + 'report_id' => $report->report_id, + ]); [$startDate, $endDate] = $this->parseDates($dates); $dateRange = $this->dateRangeService->getNormalizedDateRange($user, $startDate, $endDate); $metrics = []; + $this->logSnapshotMemory('snapshots:before_plan_load', ['report_id' => $report->report_id]); $planPatients = $this->unifiedPatientService->getLivePatientsByStatus( $department, $user, @@ -52,11 +62,18 @@ class SnapshotService $branchId, false, !$fillableAuto, - $fillableAuto + $fillableAuto, + true ); + $this->logSnapshotMemory('snapshots:after_plan_load', [ + 'report_id' => $report->report_id, + 'count' => $planPatients->count(), + ]); $this->createSnapshotsForType($report, 'plan', $planPatients); + $this->logSnapshotMemory('snapshots:after_plan_save', ['report_id' => $report->report_id]); $metrics[4] = $planPatients->count(); + $this->logSnapshotMemory('snapshots:before_emergency_load', ['report_id' => $report->report_id]); $emergencyPatients = $this->unifiedPatientService->getLivePatientsByStatus( $department, $user, @@ -65,11 +82,18 @@ class SnapshotService $branchId, false, !$fillableAuto, - $fillableAuto + $fillableAuto, + true ); + $this->logSnapshotMemory('snapshots:after_emergency_load', [ + 'report_id' => $report->report_id, + 'count' => $emergencyPatients->count(), + ]); $this->createSnapshotsForType($report, 'emergency', $emergencyPatients); + $this->logSnapshotMemory('snapshots:after_emergency_save', ['report_id' => $report->report_id]); $metrics[12] = $emergencyPatients->count(); + $this->logSnapshotMemory('snapshots:before_discharged_load', ['report_id' => $report->report_id]); $dischargedPatients = $this->unifiedPatientService->getLivePatientsByStatus( $department, $user, @@ -78,11 +102,18 @@ class SnapshotService $branchId, false, null, - $fillableAuto + $fillableAuto, + true ); + $this->logSnapshotMemory('snapshots:after_discharged_load', [ + 'report_id' => $report->report_id, + 'count' => $dischargedPatients->count(), + ]); $this->createSnapshotsForType($report, 'discharged', $dischargedPatients); + $this->logSnapshotMemory('snapshots:after_discharged_save', ['report_id' => $report->report_id]); $metrics[15] = $dischargedPatients->count(); + $this->logSnapshotMemory('snapshots:before_transferred_load', ['report_id' => $report->report_id]); $transferredPatients = $this->unifiedPatientService->getLivePatientsByStatus( $department, $user, @@ -91,11 +122,18 @@ class SnapshotService $branchId, false, null, - $fillableAuto + $fillableAuto, + true ); + $this->logSnapshotMemory('snapshots:after_transferred_load', [ + 'report_id' => $report->report_id, + 'count' => $transferredPatients->count(), + ]); $this->createSnapshotsForType($report, 'transferred', $transferredPatients); + $this->logSnapshotMemory('snapshots:after_transferred_save', ['report_id' => $report->report_id]); $metrics[13] = $transferredPatients->count(); + $this->logSnapshotMemory('snapshots:before_deceased_load', ['report_id' => $report->report_id]); $deceasedPatients = $this->unifiedPatientService->getLivePatientsByStatus( $department, $user, @@ -104,10 +142,17 @@ class SnapshotService $branchId, false, null, - $fillableAuto + $fillableAuto, + true ); + $this->logSnapshotMemory('snapshots:after_deceased_load', [ + 'report_id' => $report->report_id, + 'count' => $deceasedPatients->count(), + ]); $this->createSnapshotsForType($report, 'deceased', $deceasedPatients); + $this->logSnapshotMemory('snapshots:after_deceased_save', ['report_id' => $report->report_id]); + $this->logSnapshotMemory('snapshots:before_recipient_load', ['report_id' => $report->report_id]); $recipientPatients = $this->unifiedPatientService->getLivePatientsByStatus( $department, $user, @@ -116,10 +161,17 @@ class SnapshotService $branchId, false, null, - $fillableAuto + $fillableAuto, + true ); + $this->logSnapshotMemory('snapshots:after_recipient_load', [ + 'report_id' => $report->report_id, + 'count' => $recipientPatients->count(), + ]); $this->createSnapshotsForType($report, 'recipient', $recipientPatients); + $this->logSnapshotMemory('snapshots:after_recipient_save', ['report_id' => $report->report_id]); + $this->logSnapshotMemory('snapshots:before_current_load', ['report_id' => $report->report_id]); $currentPatients = $this->unifiedPatientService->getLivePatientsByStatus( $department, $user, @@ -128,9 +180,15 @@ class SnapshotService $branchId, false, null, - $fillableAuto + $fillableAuto, + true ); + $this->logSnapshotMemory('snapshots:after_current_load', [ + 'report_id' => $report->report_id, + 'count' => $currentPatients->count(), + ]); $this->createSnapshotsForType($report, 'current', $currentPatients); + $this->logSnapshotMemory('snapshots:after_current_save', ['report_id' => $report->report_id]); $planSurgeryCount = $this->patientService->getSurgicalPatients( 'plan', @@ -146,6 +204,17 @@ class SnapshotService ); $this->saveMetrics($report, $metrics); + $this->logSnapshotMemory('snapshots:after_save_metrics', ['report_id' => $report->report_id]); + } + + private function logSnapshotMemory(string $stage, array $context = []): void + { + \Log::info('report.snapshot.memory', [ + 'stage' => $stage, + 'memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), + 'peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2), + ...$context, + ]); } /** diff --git a/app/Services/StatisticsService.php b/app/Services/StatisticsService.php index 6806a93..50247e2 100644 --- a/app/Services/StatisticsService.php +++ b/app/Services/StatisticsService.php @@ -63,8 +63,9 @@ class StatisticsService $metrics = DB::table('reports as r') ->join('metrika_results as mr', 'r.report_id', '=', 'mr.rf_report_id') ->whereIn('r.rf_department_id', $allDeptIds) - ->whereDate('r.sent_at', '>=', $startDate) - ->whereDate('r.sent_at', '<=', $endDate) + ->where('r.period_start', '>=', $startDate) + ->where('r.period_start', '<=', $endDate) + ->where('r.status', 'submitted') ->select( 'r.rf_department_id', 'mr.rf_metrika_item_id', @@ -81,10 +82,10 @@ class StatisticsService ->join('metrika_results as mr', 'r.report_id', '=', 'mr.rf_report_id') ->whereIn('r.rf_department_id', $allDeptIds) ->where('mr.rf_metrika_item_id', 8) - ->where('r.sent_at', '<=', $endDate) + ->where('r.period_start', '<=', $endDate) ->select('r.rf_department_id', 'mr.value', 'r.created_at') ->orderBy('r.rf_department_id') // Сначала поле из DISTINCT ON - ->orderBy('r.sent_at', 'desc') // Потом остальные + ->orderBy('r.period_end', 'desc') // Потом остальные ->distinct('r.rf_department_id') ->get() ->keyBy('rf_department_id'); @@ -109,10 +110,16 @@ class StatisticsService foreach ($deptList as $dept) { $deptId = $dept->department_id; - $lastReport = Report::where('rf_department_id', $deptId) - ->whereDate('sent_at', '>=', Carbon::parse($startDate)->format('Y-m-d')) - ->whereDate('sent_at', '<=', Carbon::parse($endDate)->format('Y-m-d')) - ->orderBy('sent_at', 'desc') + $lastReportQuery = Report::where('rf_department_id', $deptId); + if ($isRangeOneDay) { + $lastReportQuery->exactPeriod($startDate, $endDate); + } else { + $lastReportQuery->withinPeriod($startDate, $endDate); + } + + $lastReport = $lastReportQuery + ->onlySubmitted() + ->orderBy('period_end', 'desc') ->first(); // Базовые показатели @@ -141,6 +148,7 @@ class StatisticsService $observable = 0; $unwanted = 0; $bedDaysSum = 0; + $avgBedDays = 0; if (isset($metrics[$deptId])) { foreach ($metrics[$deptId] as $item) { @@ -219,7 +227,7 @@ class StatisticsService 'lethality' => $lethality, 'type' => $typeName, 'isDepartment' => true, - 'isReportToday' => $lastReport ? Carbon::parse($lastReport->sent_at)->isSameDay($endDate) : null, + 'isReportToday' => $lastReport ? Carbon::parse($lastReport->period_end)->isSameDay($endDate) : null, ]; $groupedData[$typeName][] = $data; diff --git a/app/Services/UnifiedPatientService.php b/app/Services/UnifiedPatientService.php index 294ce63..4c5e02a 100644 --- a/app/Services/UnifiedPatientService.php +++ b/app/Services/UnifiedPatientService.php @@ -7,6 +7,7 @@ use App\Models\Department; use App\Models\DepartmentPatient; use App\Models\MisMedicalHistory; use App\Models\ObservationPatient; +use App\Models\Report; use App\Models\User; use Illuminate\Support\Collection; @@ -26,7 +27,8 @@ class UnifiedPatientService int $branchId, bool $onlyIds = false, ?bool $includeCurrent = null, - bool $fillableAuto = false + bool $fillableAuto = false, + bool $forSnapshots = false ): Collection { [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); @@ -35,9 +37,9 @@ class UnifiedPatientService } $patients = match ($sourceScope) { - 'mis' => $this->getMisPatientDtos($user, $baseStatus, $dateRange, $branchId, $includeCurrent, $fillableAuto), - 'special' => $this->getSpecialPatientDtos($department, $baseStatus, $dateRange), - default => $this->getAggregatedPatientDtos($department, $user, $baseStatus, $dateRange, $branchId, $includeCurrent, $fillableAuto), + 'mis' => $this->getMisPatientDtos($user, $baseStatus, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots), + 'special' => $this->getSpecialPatientDtos($department, $baseStatus, $dateRange, $forSnapshots), + default => $this->getAggregatedPatientDtos($department, $user, $baseStatus, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots), }; if ($onlyIds) { @@ -56,13 +58,79 @@ class UnifiedPatientService ?bool $includeCurrent = null, bool $fillableAuto = false ): int { - return $this->getLivePatientsByStatus($department, $user, $status, $dateRange, $branchId, false, $includeCurrent, $fillableAuto)->count(); + [$baseStatus, $sourceScope] = $this->parseScopedStatus($status); + + if ($baseStatus === 'observation') { + $query = ObservationPatient::query() + ->where('rf_department_id', $department->department_id); + + if ($sourceScope === 'special') { + return $query->whereNotNull('rf_department_patient_id')->count(); + } + + if ($sourceScope === 'mis') { + return $query->whereNull('rf_department_patient_id')->count(); + } + + return $query->count(); + } + + if ($sourceScope === 'special') { + return $this->getManualPatientsCount($department, $baseStatus, $dateRange, self::SPECIAL_SOURCE_TYPES); + } + + $misCount = $this->getMisPatientsCount( + $user, + $baseStatus, + $dateRange, + $branchId, + $includeCurrent, + $fillableAuto + ); + + if ($sourceScope === 'mis') { + return $misCount; + } + + $specialCount = $this->getManualPatientsCount($department, $baseStatus, $dateRange, self::SPECIAL_SOURCE_TYPES); + return $misCount + $specialCount; } - public function createManualPatient(Department $department, User $user, array $data): DepartmentPatient + public function getRecipientIdsForReport( + Department $department, + User $user, + DateRange $dateRange, + int $branchId, + bool $fillableAuto = false + ): array { + $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); + + $misIds = $this->patientService->getPlanOrEmergencyPatients( + null, + $isHeadOrAdmin, + $branchId, + $dateRange, + false, + true, + false, + $fillableAuto + ); + + $manualIds = $this->buildManualPatientsQuery($department, $dateRange, self::SPECIAL_SOURCE_TYPES, false) + ->whereBetween('admitted_at', [$dateRange->startSql(), $dateRange->endSql()]) + ->pluck('department_patient_id'); + + $misUids = collect($misIds)->map(fn ($id) => "mis:{$id}")->all(); + $manualUids = collect($manualIds)->map(fn ($id) => "manual:{$id}")->all(); + + return array_values(array_unique(array_merge($misUids, $manualUids))); + } + + public function createManualPatient(Department $department, User $user, array $data, int $reportId): DepartmentPatient { return DepartmentPatient::create([ 'rf_department_id' => $department->department_id, + 'rf_report_id' => $reportId, 'source_type' => 'special', 'full_name' => $data['full_name'], 'birth_date' => $data['birth_date'], @@ -88,6 +156,13 @@ class UnifiedPatientService public function updateManualPatient(DepartmentPatient $patient, array $data): DepartmentPatient { + $manualStatus = $data['manual_status'] ?? null; + $isCurrent = $manualStatus === 'current' || $manualStatus === null; + $outcomeType = match ($manualStatus) { + 'discharged', 'deceased', 'transferred' => $manualStatus, + default => null, + }; + $patient->update([ 'full_name' => $data['full_name'], 'birth_date' => $data['birth_date'], @@ -95,6 +170,9 @@ class UnifiedPatientService 'diagnosis_code' => $data['diagnosis_code'] ?? null, 'diagnosis_name' => $data['diagnosis_name'] ?? null, 'admitted_at' => $data['admitted_at'] ?? $patient->admitted_at, + 'is_current' => $isCurrent, + 'outcome_type' => $outcomeType, + 'outcome_at' => $isCurrent ? null : ($data['outcome_at'] ?? now()), ]); return $patient->fresh(); @@ -194,11 +272,24 @@ class UnifiedPatientService DateRange $dateRange, int $branchId, ?bool $includeCurrent = null, - bool $fillableAuto = false + bool $fillableAuto = false, + bool $forSnapshots = false ): Collection { - $misPatients = $this->getMisPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto); - $manualPatients = $this->getManualPatients($department, $status, $dateRange); - $linkedManualPatients = DepartmentPatient::where('rf_department_id', $department->department_id) + $misPatients = $this->getMisPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots); + $manualPatients = $this->getManualPatients($department, $status, $dateRange, self::SPECIAL_SOURCE_TYPES, !$forSnapshots); + $reportIds = $this->getReportIdsForDepartmentPeriod($department, $dateRange); + + $linkedManualPatients = DepartmentPatient::query() + ->where(function ($builder) use ($department, $reportIds) { + if (!empty($reportIds)) { + $builder->whereIn('rf_report_id', $reportIds); + } + + $builder->orWhere(function ($legacyQuery) use ($department) { + $legacyQuery->whereNull('rf_report_id') + ->where('rf_department_id', $department->department_id); + }); + }) ->whereIn('source_type', self::SPECIAL_SOURCE_TYPES) ->whereNotNull('rf_medicalhistory_id') ->get() @@ -228,9 +319,10 @@ class UnifiedPatientService DateRange $dateRange, int $branchId, ?bool $includeCurrent = null, - bool $fillableAuto = false + bool $fillableAuto = false, + bool $forSnapshots = false ): Collection { - return $this->getMisPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto) + return $this->getMisPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto, $forSnapshots) ->map(fn ($patient) => UnifiedPatientData::fromMisMedicalHistory( $patient, (bool) ($patient->is_recipient_today ?? false), @@ -242,10 +334,11 @@ class UnifiedPatientService private function getSpecialPatientDtos( Department $department, string $status, - DateRange $dateRange + DateRange $dateRange, + bool $forSnapshots = false ): Collection { return $this->mapManualPatients( - $this->getManualPatients($department, $status, $dateRange, self::SPECIAL_SOURCE_TYPES), + $this->getManualPatients($department, $status, $dateRange, self::SPECIAL_SOURCE_TYPES, !$forSnapshots), $dateRange ); } @@ -256,7 +349,8 @@ class UnifiedPatientService DateRange $dateRange, int $branchId, ?bool $includeCurrent = null, - bool $fillableAuto = false + bool $fillableAuto = false, + bool $forSnapshots = false ): Collection { $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); $includeCurrent = $includeCurrent ?? in_array($status, ['plan', 'emergency'], true); @@ -299,19 +393,66 @@ class UnifiedPatientService }; } + private function getMisPatientsCount( + User $user, + string $status, + DateRange $dateRange, + int $branchId, + ?bool $includeCurrent = null, + bool $fillableAuto = false + ): int { + $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin(); + $includeCurrent = $includeCurrent ?? in_array($status, ['plan', 'emergency'], true); + + return match ($status) { + 'plan', 'emergency' => $includeCurrent + ? $this->patientService->getPatientsCountWithCurrent($status, $isHeadOrAdmin, $branchId, $dateRange) + : $this->patientService->getPlanOrEmergencyPatients( + $status, + $isHeadOrAdmin, + $branchId, + $dateRange, + true, + false, + false, + $fillableAuto + ), + 'current' => $this->patientService->getAllPatientsInDepartment( + $isHeadOrAdmin, + $branchId, + $dateRange, + true, + false, + $fillableAuto + ), + 'recipient' => $this->patientService->getPlanOrEmergencyPatients( + null, + $isHeadOrAdmin, + $branchId, + $dateRange, + true, + false, + false, + $fillableAuto + ), + 'outcome' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'without-transferred', true)->count(), + 'outcome-discharged' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'discharged', true)->count(), + 'outcome-transferred' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'transferred', true)->count(), + 'outcome-deceased' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'deceased', true)->count(), + 'reanimation' => $this->patientService->getReanimationPatients($branchId, $dateRange, true)->count(), + default => 0, + }; + } + private function getManualPatients( Department $department, string $status, DateRange $dateRange, - ?array $sourceTypes = self::SPECIAL_SOURCE_TYPES + ?array $sourceTypes = self::SPECIAL_SOURCE_TYPES, + bool $withOperations = true ): Collection { - $query = DepartmentPatient::where('rf_department_id', $department->department_id) - ->with(['operations.serviceMedical']); - - if ($sourceTypes !== null) { - $query->whereIn('source_type', $sourceTypes); - } + $query = $this->buildManualPatientsQuery($department, $dateRange, $sourceTypes, $withOperations); return match ($status) { 'plan', 'emergency' => $query @@ -338,11 +479,91 @@ class UnifiedPatientService }; } + private function getManualPatientsCount( + Department $department, + string $status, + DateRange $dateRange, + ?array $sourceTypes = self::SPECIAL_SOURCE_TYPES + ): int { + $query = $this->buildManualPatientsQuery($department, $dateRange, $sourceTypes, false); + + return match ($status) { + 'plan', 'emergency' => $query + ->current() + ->where('patient_kind', $status) + ->count(), + 'current' => $query + ->current() + ->count(), + 'recipient' => $query + ->whereBetween('admitted_at', [$dateRange->startSql(), $dateRange->endSql()]) + ->count(), + 'outcome' => $query + ->whereNotNull('outcome_type') + ->whereIn('outcome_type', ['discharged', 'deceased']) + ->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()]) + ->count(), + 'outcome-discharged', 'outcome-transferred', 'outcome-deceased' => $query + ->where('outcome_type', str_replace('outcome-', '', $status)) + ->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()]) + ->count(), + default => 0, + }; + } + + private function buildManualPatientsQuery( + Department $department, + DateRange $dateRange, + ?array $sourceTypes, + bool $withOperations + ) { + $reportIds = $this->getReportIdsForDepartmentPeriod($department, $dateRange); + + $query = DepartmentPatient::query() + ->where(function ($builder) use ($department, $reportIds) { + if (!empty($reportIds)) { + $builder->whereIn('rf_report_id', $reportIds); + } + + $builder->orWhere(function ($legacyQuery) use ($department) { + $legacyQuery->whereNull('rf_report_id') + ->where('rf_department_id', $department->department_id); + }); + }); + + if ($withOperations) { + $query->with(['operations.serviceMedical']); + } + + if ($sourceTypes !== null) { + $query->whereIn('source_type', $sourceTypes); + } + + return $query; + } + + private function getReportIdsForDepartmentPeriod(Department $department, DateRange $dateRange): array + { + return Report::query() + ->where('rf_department_id', $department->department_id) + ->when( + $dateRange->isOneDay, + fn ($query) => $query->exactPeriod($dateRange->startSql(), $dateRange->endSql()), + fn ($query) => $query->withinPeriod($dateRange->startSql(), $dateRange->endSql()), + ) + ->pluck('report_id') + ->all(); + } + private function mapManualPatients(Collection $manualPatients, DateRange $dateRange): Collection { return $manualPatients ->map(function (DepartmentPatient $patient) use ($dateRange) { - $operations = $patient->operations->map(fn ($operation) => [ + $operationsRelation = $patient->relationLoaded('operations') + ? $patient->operations + : collect(); + + $operations = $operationsRelation->map(fn ($operation) => [ 'id' => $operation->department_patient_operation_id, 'code' => $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code, 'name' => $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name, diff --git a/database/migrations/2026_04_20_163507_add_code_column_in_metrika_items_table.php b/database/migrations/2026_04_20_163507_add_code_column_in_metrika_items_table.php index 1c1ef7e..789d46f 100644 --- a/database/migrations/2026_04_20_163507_add_code_column_in_metrika_items_table.php +++ b/database/migrations/2026_04_20_163507_add_code_column_in_metrika_items_table.php @@ -22,7 +22,7 @@ return new class extends Migration public function down(): void { Schema::table('metrika_items', function (Blueprint $table) { - // + $table->dropColumn('code'); }); } }; diff --git a/resources/js/Components/AppGrid.vue b/resources/js/Components/AppGrid.vue new file mode 100644 index 0000000..8f6692f --- /dev/null +++ b/resources/js/Components/AppGrid.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/resources/js/Components/AppGridItem.vue b/resources/js/Components/AppGridItem.vue new file mode 100644 index 0000000..103013a --- /dev/null +++ b/resources/js/Components/AppGridItem.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/resources/js/Components/AppPanel.vue b/resources/js/Components/AppPanel.vue index 8adfa78..9e68a07 100644 --- a/resources/js/Components/AppPanel.vue +++ b/resources/js/Components/AppPanel.vue @@ -6,6 +6,10 @@ const props = defineProps({ type: String, default: '' }, + headerIncludeBody: { + type: Boolean, + default: false + }, feedback: { type: String, default: '' @@ -25,6 +29,7 @@ const props = defineProps({ }) const hasHeader = computed(() => props.header.trim().length > 0) +const hasHeaderInOutside = computed(() => hasHeader.value && !props.headerIncludeBody) const hasFeedback = computed(() => props.feedback.trim().length > 0) const hasMinH = computed(() => props.minH.trim().length > 0 || Number.isInteger(props.minH)) const hasMaxH = computed(() => props.maxH.trim().length > 0 || Number.isInteger(props.maxH)) @@ -55,8 +60,11 @@ watch(() => [props.minH, props.maxH], ([minH, maxH]) => {