diff --git a/.gitignore b/.gitignore
index b71b1ea..714ca7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@
Homestead.json
Homestead.yaml
Thumbs.db
+.codex
diff --git a/app/Console/Commands/FillReportsFromDate.php b/app/Console/Commands/FillReportsFromDate.php
index 3533502..ad27b0e 100644
--- a/app/Console/Commands/FillReportsFromDate.php
+++ b/app/Console/Commands/FillReportsFromDate.php
@@ -27,12 +27,25 @@ class FillReportsFromDate extends Command
public function handle()
{
- $startDate = $this->option('date') ?? Carbon::now()->subDays(7)->format('Y-m-d');
- $endDate = $this->option('end-date') ?? Carbon::now()->format('Y-m-d');
+ $startDate = $this->option('date') ?? Carbon::now('Asia/Yakutsk')->subDays(7)->format('Y-m-d');
+ $endDate = $this->option('end-date') ?? Carbon::now('Asia/Yakutsk')->format('Y-m-d');
$departmentId = $this->option('department');
$userId = $this->option('user');
$force = $this->option('force');
+ try {
+ $start = Carbon::createFromFormat('Y-m-d', $startDate, 'Asia/Yakutsk')->startOfDay();
+ $end = Carbon::createFromFormat('Y-m-d', $endDate, 'Asia/Yakutsk')->startOfDay();
+ } catch (\Throwable) {
+ $this->error('Неверный формат даты. Используйте YYYY-MM-DD.');
+ return 1;
+ }
+
+ if ($start->gt($end)) {
+ $this->error('Дата начала больше даты окончания.');
+ return 1;
+ }
+
$this->info("Заполнение отчетов с {$startDate} по {$endDate}");
// Получаем отделения
@@ -51,34 +64,27 @@ class FillReportsFromDate extends Command
foreach ($departments as $department) {
$this->info("Обработка отделения: {$department->name_short}");
- // Получаем пользователей отделения
- $users = $userId
- ? User::where('id', $userId)->whereHas('departments', function ($query) use ($department) {
- $query->where('rf_department_id', $department->department_id);
- })->get()
- : $this->getDepartmentUsers($department);
+ $user = $this->resolveResponsibleUser($department, $userId);
- if ($users->isEmpty()) {
- $this->warn("В отделении {$department->name} нет пользователей");
+ if (!$user) {
+ $this->warn("В отделении {$department->name_short} нет подходящего пользователя для автозаполнения");
continue;
}
- foreach ($users as $user) {
- try {
- $created = $this->autoReportService->fillReportsForUser(
- $user,
- $startDate,
- $endDate,
- $departmentId,
- $force,
- );
+ try {
+ $created = $this->autoReportService->fillReportsForUser(
+ $user,
+ $startDate,
+ $endDate,
+ $department,
+ $force,
+ );
- $totalReports += $created;
- $this->info("Для пользователя {$user->name} создано {$created} отчетов");
- } catch (\Exception $e) {
- $totalErrors++;
- $this->error("Ошибка для пользователя {$user->name}: {$e->getMessage()}");
- }
+ $totalReports += $created;
+ $this->info("Для пользователя {$user->name} создано {$created} отчетов");
+ } catch (\Exception $e) {
+ $totalErrors++;
+ $this->error("Ошибка для пользователя {$user->name}: {$e->getMessage()}");
}
}
@@ -87,15 +93,37 @@ class FillReportsFromDate extends Command
return 0;
}
- private function getDepartmentUsers(Department $department)
+ private function resolveResponsibleUser(Department $department, ?string $userId): ?User
{
- // Получаем пользователей, которые могут создавать отчеты
- return User::where('rf_department_id', $department->department_id)
- ->where(function ($query) {
- $query->where('role', 'doctor')
- ->orWhere('role', 'head_of_department');
- })
+ $query = User::query()
->where('is_active', true)
- ->get();
+ ->whereHas('departments', function ($departmentQuery) use ($department) {
+ $departmentQuery->where('rf_department_id', $department->department_id);
+ })
+ ->whereHas('roles', function ($roleQuery) {
+ $roleQuery->where('slug', 'doctor');
+ })
+ ->with(['roles', 'departments']);
+
+ if ($userId) {
+ return $query->where('id', $userId)->first();
+ }
+
+ return $query->get()
+ ->sortBy(function (User $user) use ($department) {
+ $departmentLink = $user->departments->firstWhere('rf_department_id', $department->department_id);
+ $isDoctor = $user->roles->contains('slug', 'doctor');
+ $isFavorite = (bool) optional($departmentLink)->is_favorite;
+ $order = optional($departmentLink)->order ?? PHP_INT_MAX;
+
+ return sprintf(
+ '%d-%d-%010d-%010d',
+ $isDoctor ? 0 : 1,
+ $isFavorite ? 0 : 1,
+ $order,
+ $user->id
+ );
+ })
+ ->first();
}
}
diff --git a/app/Data/UnifiedPatientData.php b/app/Data/UnifiedPatientData.php
new file mode 100644
index 0000000..f84fa44
--- /dev/null
+++ b/app/Data/UnifiedPatientData.php
@@ -0,0 +1,251 @@
+BD ? Carbon::parse($patient->BD) : null;
+ $birthDate = $birthDateValue?->format('Y-m-d');
+ $manualId = $linkedManualPatient?->department_patient_id;
+ $outcomeMigration = $patient->relationLoaded('outcomeMigration')
+ ? $patient->outcomeMigration->first()
+ : null;
+ $migration = $patient->relationLoaded('migrations')
+ ? $patient->migrations->first()
+ : null;
+ $diagnosisMkb = $outcomeMigration?->mainDiagnosis?->mkb ?? $migration?->mainDiagnosis?->mkb;
+ $operations = $patient->relationLoaded('surgicalOperations')
+ ? $patient->surgicalOperations->map(fn ($operation) => [
+ 'code' => $operation->serviceMedical?->ServiceMedicalCode,
+ 'name' => $operation->serviceMedical?->ServiceMedicalName,
+ ])->values()->all()
+ : [];
+
+ return new self(
+ id: $manualId ? "manual:{$manualId}" : "mis:{$patient->MedicalHistoryID}",
+ patientUid: "mis:{$patient->MedicalHistoryID}",
+ sourceType: $manualId ? 'manual' : 'mis',
+ departmentPatientId: $manualId,
+ medicalHistoryId: $patient->MedicalHistoryID,
+ fullname: $linkedManualPatient?->full_name ?: mb_convert_case(trim("{$patient->FAMILY} {$patient->Name} {$patient->OT}"), MB_CASE_TITLE, 'UTF-8'),
+ birthDate: $linkedManualPatient?->birth_date?->format('Y-m-d') ?? $birthDate,
+ age: $birthDateValue?->age ?? $linkedManualPatient?->birth_date?->age,
+ mkb: [
+ 'ds' => $linkedManualPatient?->diagnosis_code ?: $diagnosisMkb?->DS,
+ 'name' => $linkedManualPatient?->diagnosis_name ?: $diagnosisMkb?->NAME,
+ ],
+ operations: $operations,
+ patientKind: $linkedManualPatient?->patient_kind ?: self::resolvePatientKind($patient->rf_EmerSignID),
+ admittedAt: $linkedManualPatient?->admitted_at?->toIso8601String() ?? $patient->DateRecipient?->toIso8601String(),
+ outcomeType: $patient->outcome_type ?? $linkedManualPatient?->outcome_type,
+ outcomeDate: $patient->outcome_date ?? $linkedManualPatient?->outcome_at?->toIso8601String(),
+ comment: $comment,
+ isRecipientToday: $isRecipientToday,
+ isManual: (bool) $linkedManualPatient,
+ canManageManual: (bool) $linkedManualPatient,
+ );
+ }
+ public static function fromMisMigrationPatient(
+ MisMigrationPatient $migration,
+ bool $isRecipientToday = false,
+ ?DepartmentPatient $linkedManualPatient = null,
+ ?string $comment = null
+ ): self {
+ $birthDateValue = $migration->medicalHistory->BD ? Carbon::parse($migration->medicalHistory->BD) : null;
+ $birthDate = $birthDateValue?->format('Y-m-d');
+ $manualId = $linkedManualPatient?->department_patient_id;
+ $medicalHistory = $migration->medicalHistory;
+ $outcomeMigration = $medicalHistory->relationLoaded('outcomeMigration')
+ ? $medicalHistory->outcomeMigration->first()
+ : null;
+ $operations = $medicalHistory->relationLoaded('surgicalOperations')
+ ? $medicalHistory->surgicalOperations->map(fn ($operation) => [
+ 'code' => $operation->serviceMedical?->ServiceMedicalCode,
+ 'name' => $operation->serviceMedical?->ServiceMedicalName,
+ ])->values()->all()
+ : [];
+
+ return new self(
+ id: $manualId ? "manual:{$manualId}" : "mis:{$medicalHistory->MedicalHistoryID}",
+ patientUid: "mis:{$medicalHistory->MedicalHistoryID}",
+ sourceType: $manualId ? 'manual' : 'mis',
+ departmentPatientId: $manualId,
+ medicalHistoryId: $medicalHistory->MedicalHistoryID,
+ fullname: $linkedManualPatient?->full_name ?: mb_convert_case(trim("{$medicalHistory->FAMILY} {$medicalHistory->Name} {$medicalHistory->OT}"), MB_CASE_TITLE, 'UTF-8'),
+ birthDate: $linkedManualPatient?->birth_date?->format('Y-m-d') ?? $birthDate,
+ age: $birthDateValue?->age ?? $linkedManualPatient?->birth_date?->age,
+ mkb: [
+ 'ds' => $linkedManualPatient?->diagnosis_code ?: $outcomeMigration?->mainDiagnosis?->mkb?->DS,
+ 'name' => $linkedManualPatient?->diagnosis_name ?: $outcomeMigration?->mainDiagnosis?->mkb?->NAME,
+ ],
+ operations: $operations,
+ patientKind: $linkedManualPatient?->patient_kind ?: self::resolvePatientKind($medicalHistory->rf_EmerSignID),
+ admittedAt: $linkedManualPatient?->admitted_at?->toIso8601String() ?? $medicalHistory->DateRecipient?->toIso8601String(),
+ outcomeType: $migration->outcome_type ?? $linkedManualPatient?->outcome_type,
+ outcomeDate: $migration->outcome_date ?? $linkedManualPatient?->outcome_at?->toIso8601String(),
+ comment: $comment,
+ isRecipientToday: $isRecipientToday,
+ isManual: (bool) $linkedManualPatient,
+ canManageManual: (bool) $linkedManualPatient,
+ );
+ }
+
+ public static function fromDepartmentPatient(
+ DepartmentPatient $patient,
+ bool $isRecipientToday = false,
+ ?array $operations = null,
+ ?string $comment = null
+ ): self {
+ return new self(
+ id: "manual:{$patient->department_patient_id}",
+ patientUid: $patient->rf_medicalhistory_id ? "mis:{$patient->rf_medicalhistory_id}" : "manual:{$patient->department_patient_id}",
+ sourceType: 'manual',
+ departmentPatientId: $patient->department_patient_id,
+ medicalHistoryId: $patient->rf_medicalhistory_id,
+ fullname: $patient->full_name,
+ birthDate: $patient->birth_date?->format('Y-m-d'),
+ age: $patient->birth_date?->age,
+ mkb: [
+ 'ds' => $patient->diagnosis_code,
+ 'name' => $patient->diagnosis_name,
+ ],
+ operations: $operations ?? [],
+ patientKind: $patient->patient_kind,
+ admittedAt: $patient->admitted_at?->toIso8601String(),
+ outcomeType: $patient->outcome_type,
+ outcomeDate: $patient->outcome_at?->toIso8601String(),
+ comment: $comment,
+ isRecipientToday: $isRecipientToday,
+ isManual: true,
+ canManageManual: true,
+ );
+ }
+
+ public static function fromSnapshot(
+ MedicalHistorySnapshot $snapshot,
+ bool $isRecipientToday = false,
+ ?array $operations = null
+ ): self
+ {
+ $birthDate = self::normalizeDate($snapshot->birth_date);
+
+ 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}"),
+ sourceType: $snapshot->patient_source_type ?: ($snapshot->is_manual ? 'manual' : 'mis'),
+ departmentPatientId: $snapshot->rf_department_patient_id,
+ medicalHistoryId: $snapshot->rf_medicalhistory_id,
+ fullname: $snapshot->full_name ?: 'Пациент без имени',
+ birthDate: $birthDate,
+ age: $birthDate ? now()->diffInYears($birthDate) : null,
+ mkb: [
+ 'ds' => $snapshot->diagnosis_code,
+ 'name' => $snapshot->diagnosis_name,
+ ],
+ operations: $operations ?? [],
+ patientKind: $snapshot->patient_kind,
+ admittedAt: self::normalizeDateTime($snapshot->admitted_at),
+ outcomeType: $snapshot->outcome_type,
+ outcomeDate: self::normalizeDateTime($snapshot->outcome_at),
+ comment: null,
+ isRecipientToday: $isRecipientToday,
+ isManual: (bool) $snapshot->is_manual,
+ canManageManual: false,
+ );
+ }
+
+ public function toSnapshotPayload(string $patientType): array
+ {
+ return [
+ 'rf_medicalhistory_id' => $this->medicalHistoryId,
+ 'rf_department_patient_id' => $this->departmentPatientId,
+ 'patient_type' => $patientType,
+ 'patient_uid' => $this->patientUid,
+ 'patient_source_type' => $this->sourceType,
+ 'patient_kind' => $this->patientKind,
+ 'full_name' => $this->fullname,
+ 'birth_date' => $this->birthDate,
+ 'diagnosis_code' => $this->mkb['ds'] ?? null,
+ 'diagnosis_name' => $this->mkb['name'] ?? null,
+ 'admitted_at' => $this->admittedAt,
+ 'outcome_type' => $this->outcomeType,
+ 'outcome_at' => $this->outcomeDate,
+ 'is_manual' => $this->isManual,
+ ];
+ }
+
+ public static function unique(Collection $patients): Collection
+ {
+ return $patients->unique(fn (self $patient) => $patient->patientUid)->values();
+ }
+
+ private static function resolvePatientKind(?int $emerSignId): ?string
+ {
+ return match ($emerSignId) {
+ 1 => 'plan',
+ 2, 4 => 'emergency',
+ default => null,
+ };
+ }
+
+ private static function normalizeDateTime($value): ?string
+ {
+ if (!$value) {
+ return null;
+ }
+
+ if ($value instanceof CarbonInterface) {
+ return $value->toIso8601String();
+ }
+
+ return (string) $value;
+ }
+
+ private static function normalizeDate($value): ?string
+ {
+ if (!$value) {
+ return null;
+ }
+
+ if ($value instanceof CarbonInterface) {
+ return $value->format('Y-m-d');
+ }
+
+ return (string) $value;
+ }
+}
diff --git a/app/Http/Controllers/Api/OperationController.php b/app/Http/Controllers/Api/OperationController.php
index 480b80e..269c754 100644
--- a/app/Http/Controllers/Api/OperationController.php
+++ b/app/Http/Controllers/Api/OperationController.php
@@ -15,7 +15,9 @@ class OperationController extends Controller
'historyId' => 'required|integer'
]);
- $operations = MisSurgicalOperation::where('rf_MedicalHistoryID', $request->historyId)->get();
+ $operations = MisSurgicalOperation::where('rf_MedicalHistoryID', $request->historyId)
+ ->completed()
+ ->get();
return response()->json(
OperationsResource::collection($operations)
diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php
index 975136b..4b2013a 100644
--- a/app/Http/Controllers/Api/ReportController.php
+++ b/app/Http/Controllers/Api/ReportController.php
@@ -3,12 +3,14 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
+use App\Http\Resources\Api\DepartmentPatientOperationResource;
use App\Http\Resources\Mis\FormattedPatientResource;
use App\Models\Department;
use App\Models\MedicalHistorySnapshot;
use App\Models\MetrikaGroup;
use App\Models\MetrikaResult;
use App\Models\MisLpuDoctor;
+use App\Models\MisMKB;
use App\Models\MisMedicalHistory;
use App\Models\MisMigrationPatient;
use App\Models\MisStationarBranch;
@@ -23,6 +25,7 @@ use App\Services\ReportService;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
@@ -39,188 +42,29 @@ class ReportController extends Controller
public function index(Request $request)
{
$user = Auth::user();
- $department = $user->department;
-
- $startDateCarbon = Carbon::now()->firstOfMonth();
- $endDateCarbon = Carbon::now();
-
- // Определяем даты в зависимости от роли
- [$startDate, $endDate] = $this->dateRangeService->getDateRangeForUser($user, $request->query('startAt'), $request->query('endAt'));
- if (Carbon::parse($startDate)->isValid()) {
- $startDateCarbon = Carbon::parse($startDate)->setTimeZone('Asia/Yakutsk');
- }
- if (Carbon::parse($endDate)->isValid()) {
- $endDateCarbon = Carbon::parse($endDate)->setTimeZone('Asia/Yakutsk');
- }
-
- $reportIds = [];
- $reports = $this->getReportsForDateRange($user->rf_department_id, $startDate, $endDate);
- $reportIds = $reports->pluck('report_id')->toArray();
-
- // Определяем, используем ли мы снапшоты
- $reportToday = Report::whereDate('sent_at', $endDate)
- ->whereDate('created_at', $endDate)
- ->first();
- $useSnapshots = ($user->isAdmin() || $user->isHeadOfDepartment()) || (Carbon::parse($endDate)->isToday() === false || $reportToday);
- if ($useSnapshots && ($user->isHeadOfDepartment() || $user->isAdmin())) {
- $report = Report::whereDate('sent_at', $endDate)
- ->where('rf_department_id', $department->department_id)
- ->first();
- $fillableUserId = $report->rf_lpudoctor_id ?? null;
- } else {
- $fillableUserId = $request->query('userId', $user->rf_lpudoctor_id);
- }
-
- $beds = (int)$department->metrikaDefault()->where('rf_metrika_item_id', 1)->first()->value;
- $occupiedBeds = optional(Report::where('rf_department_id', $user->rf_department_id)
- ->join('metrika_results', 'reports.report_id', '=', 'metrika_results.rf_report_id')
- ->where('metrika_results.rf_metrika_item_id', 8)
- ->orderBy('sent_at', 'desc')->first())->value ?? 0;
-
- $percentLoadedBeds = round(intval($occupiedBeds) * 100 / $beds); //intval($occupiedBeds) * 100 / $beds;
-
+ $departmentId = $request->query('departmentId', $user->department->department_id);
+ $department = Department::where('department_id', $departmentId)->firstOrFail();
+ $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user);
+ $statistics = $this->reportService->getReportStatistics($department, $user, $dateRange);
+ $reportInfo = $this->reportService->getCurrentReportInfo($department, $user, $dateRange);
$metrikaGroup = MetrikaGroup::whereMetrikaGroupId(2)->first();
$metrikaItems = $metrikaGroup->metrikaItems;
- $misDepartmentId = $request->user()->department->rf_mis_department_id;
-
- $branchId = MisStationarBranch::where('rf_DepartmentID', $misDepartmentId)
- ->value('StationarBranchID');
-
- $unwantedEvents = UnwantedEvent::whereHas('report', function ($query) use ($user, $startDate, $endDate) {
- $query->where('rf_department_id', $user->rf_department_id)
- ->whereDate('created_at', $endDate);
- })
- ->get()->map(function ($item) {
- return [
- ...$item->toArray(),
- 'created_at' => Carbon::parse($item->created_at)->format('Создано d.m.Y в H:i'),
- ];
- });
-
- // Определяем, является ли пользователь заведующим/администратором
- $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
-
- // Получаем статистику в зависимости от источника данных
- if ($useSnapshots || $reportToday) {
- // Используем снапшоты для статистики
-// $plan = $this->getCountFromSnapshots('plan', $reportIds);
-// $emergency = $this->getCountFromSnapshots('emergency', $reportIds);
- $recipientCount = $this->getPatientsCountFromSnapshot('recipient', $reportIds);
- $outcomeCount = $this->getPatientsCountFromSnapshot('outcome', $reportIds);
- $currentCount = $this->getPatientsCountFromSnapshot('current', $reportIds);
- $deadCount = $this->getPatientsCountFromSnapshot('deceased', $reportIds);
-
- // Для операций все равно используем реплику с фильтрацией по датам
- $surgicalCount = [
- $this->getSurgicalPatientsFromSnapshot('plan', $reportIds),
- $this->getSurgicalPatientsFromSnapshot('emergency', $reportIds)
- ];
-
- $recipientIds = $this->getRecipientIdsFromSnapshots($reportIds);
- } else {
- // Используем реплику для статистики
- $plan = $this->getPlanOrEmergencyPatients(
- 'plan',
- $isHeadOrAdmin,
- $branchId,
- $startDate,
- $endDate,
- true,
- today: true
- );
- $emergency = $this->getPlanOrEmergencyPatients(
- 'emergency',
- $isHeadOrAdmin,
- $branchId,
- $startDate,
- $endDate,
- true,
- today: true
- );
- $outcomeCount = $this->getAllOutcomePatients(
- $branchId,
- $startDate,
- $endDate,
- true
- );
- $currentCount = $this->getCurrentPatients($branchId, true);
- $deadCount = $this->getDeceasedOutcomePatients($branchId, $startDate, $endDate, true);
-
- $surgicalCount = [
- $this->getSurgicalPatients('plan', $isHeadOrAdmin, $branchId, $startDate, $endDate, true),
- $this->getSurgicalPatients('emergency', $isHeadOrAdmin, $branchId, $startDate, $endDate, true)
- ];
-
- $recipientIds = $this->getPlanOrEmergencyPatients(
- null,
- $isHeadOrAdmin,
- $branchId,
- $startDate,
- $endDate,
- false,
- true,
- true,
- today: true
- );
- }
-
- $isActiveSendButton = Carbon::createFromFormat('Y-m-d H:i:s', $endDate)->isToday() &&
- (!$user->isHeadOfDepartment() && !$user->isAdmin()) && $reportToday == null;
-
- $reportDoctor = $reportToday?->lpuDoctor;
- $message = null;
- if ($reportToday) {
- if ($reportDoctor && $reportDoctor->LPUDoctorID === intval($fillableUserId)) {
- $isActiveSendButton = true;
- } else {
- $message = "Отчет уже создан пользователем: $reportDoctor->FAM_V $reportDoctor->IM_V $reportDoctor->OT_V";
- }
-
- $lpuDoctor = $reportDoctor;
- } else {
- if (Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate)) > 1.0) {
- $lpuDoctor = null;
- } else {
- $lpuDoctor = MisLpuDoctor::where('LPUDoctorID', $fillableUserId)->first();
- }
- }
-
- $isRangeOneDay = $this->dateRangeService->isRangeOneDay($startDate, $endDate);
-
- $date = $isHeadOrAdmin ? [
- $this->dateRangeService->parseDate($isRangeOneDay ? $endDate : $startDate)->getTimestampMs(),
- $this->dateRangeService->parseDate($endDate)->getTimestampMs()
- ] : $this->dateRangeService->parseDate($endDate)->getTimestampMs();
-
return response()->json([
'department' => [
- 'beds' => $beds,
- 'percentLoadedBeds' => $percentLoadedBeds,
-
- 'recipientCount' => $useSnapshots ? $recipientCount : $plan + $emergency, //$recipientCount,
- 'extractCount' => $outcomeCount, //$extractedCount,
- 'currentCount' => $currentCount,
- 'deadCount' => $deadCount,
- 'surgicalCount' => $surgicalCount,
- 'recipientIds' => $recipientIds,
+ 'department_id' => $department->department_id,
+ 'department_name' => $department->name_full,
+ 'beds' => $department->beds,
+ ...$statistics,
],
'dates' => [
- 'startAt' => $startDateCarbon->getTimestampMs(),
- 'endAt' => $endDateCarbon->getTimestampMs()
- ],
- 'report' => [
- 'report_id' => $reportToday?->report_id,
- 'unwantedEvents' => $unwantedEvents,
- 'isActiveSendButton' => $isActiveSendButton,
- 'message' => $message,
- 'isOneDay' => $isRangeOneDay,
- 'isHeadOrAdmin' => $isHeadOrAdmin,
- 'dates' => $date
+ 'startAt' => $dateRange->startTimestamp(),
+ 'endAt' => $dateRange->endTimestamp(),
],
+ 'report' => $reportInfo,
'metrikaItems' => $metrikaItems,
- 'userId' => $fillableUserId,
- 'userName' => $lpuDoctor ? "$lpuDoctor->FAM_V $lpuDoctor->IM_V $lpuDoctor->OT_V" : null
+ 'userId' => $reportInfo['userId'],
+ 'userName' => $reportInfo['userName'],
]);
}
@@ -660,22 +504,53 @@ class ReportController extends Controller
'status' => 'required|string',
'startAt' => 'nullable',
'endAt' => 'nullable',
- 'departmentId' => 'nullable'
+ 'departmentId' => 'nullable',
+ 'page' => 'nullable|integer|min:1',
+ 'perPage' => 'nullable|integer|min:1|max:1000',
+ 'search' => 'nullable|string|max:255',
]);
$dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user);
$departmentId = $request->get('departmentId', $user->department->department_id);
$department = Department::where('department_id', $departmentId)->first();
+ $page = (int) ($validated['page'] ?? 1);
+ $perPage = (int) ($validated['perPage'] ?? 20);
+ $search = trim((string) ($validated['search'] ?? ''));
- $patients = $this->reportService->getPatientsByStatus(
+ $patients = collect($this->reportService->getPatientsByStatus(
$department,
Auth::user(),
$validated['status'],
$dateRange
- );
+ ));
- return response()->json(FormattedPatientResource::collection($patients));
+ if ($search !== '') {
+ $needle = mb_strtolower($search);
+ $patients = $patients->filter(function ($patient) use ($needle) {
+ $fullName = mb_strtolower(trim((string) ($patient->fullname ?? "{$patient->FAMILY} {$patient->Name} {$patient->OT}")));
+ $diagnosisCode = mb_strtolower((string) ($patient->mkb['ds'] ?? ''));
+ $diagnosisName = mb_strtolower((string) ($patient->mkb['name'] ?? ''));
+
+ return str_contains($fullName, $needle)
+ || str_contains($diagnosisCode, $needle)
+ || str_contains($diagnosisName, $needle);
+ })->values();
+ }
+
+ $total = $patients->count();
+ $items = $patients->forPage($page, $perPage)->values();
+ $data = FormattedPatientResource::collection($items)->resolve();
+
+ return response()->json([
+ 'data' => $data,
+ 'meta' => [
+ 'total' => $total,
+ 'page' => $page,
+ 'perPage' => $perPage,
+ 'lastPage' => max((int) ceil($total / max($perPage, 1)), 1),
+ ],
+ ]);
}
public function getPatientsCount(Request $request)
@@ -704,6 +579,45 @@ class ReportController extends Controller
return response()->json($count);
}
+ public function getPatientsCounts(Request $request)
+ {
+ $user = Auth::user();
+
+ $request->validate([
+ 'startAt' => 'nullable',
+ 'endAt' => 'nullable',
+ 'departmentId' => 'nullable',
+ 'force' => 'nullable|boolean',
+ ]);
+
+ $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user);
+ $departmentId = $request->get('departmentId', $user->department->department_id);
+ $department = Department::where('department_id', $departmentId)->firstOrFail();
+
+ $cacheKey = sprintf(
+ 'report:patients-counts:%d:%d:%s:%s',
+ $user->id,
+ $department->department_id,
+ $dateRange->startSql(),
+ $dateRange->endSql()
+ );
+
+ $force = (bool) $request->boolean('force', false);
+
+ if ($force) {
+ $counts = $this->reportService->getPatientsCountsMap($department, $user, $dateRange);
+ Cache::put($cacheKey, $counts, now()->addSeconds(30));
+ } else {
+ $counts = Cache::remember($cacheKey, now()->addSeconds(30), function () use ($department, $user, $dateRange) {
+ return $this->reportService->getPatientsCountsMap($department, $user, $dateRange);
+ });
+ }
+
+ return response()->json([
+ 'counts' => $counts,
+ ]);
+ }
+
/**
* Получить пациентов (плановых или экстренных)
*/
@@ -721,17 +635,35 @@ class ReportController extends Controller
$isOutcomeStatus = in_array($status, ['outcome-transferred', 'outcome-discharged', 'outcome-deceased']);
if ($isOutcomeStatus) {
- switch ($status) {
- case 'outcome-transferred':
- $query = MisMigrationPatient::transferred($branchId, $startDate, $endDate);
- break;
- case 'outcome-discharged':
- $query = MisMigrationPatient::discharged($branchId, $startDate, $endDate);
- break;
- case 'outcome-deceased':
- $query = MisMigrationPatient::deceasedOutcome($branchId, $startDate, $endDate);
- break;
+ $visitResultIds = match ($status) {
+ 'outcome-transferred' => [4, 14],
+ 'outcome-discharged' => [1, 11, 2, 12, 7, 18, 48],
+ 'outcome-deceased' => [5, 6, 15, 16],
+ default => [],
+ };
+
+ $historyQuery = $this->buildOutcomeMedicalHistoryQuery(
+ $branchId,
+ $startDate,
+ $endDate,
+ $visitResultIds
+ );
+
+ if ($onlyIds) {
+ return $historyQuery->pluck('MedicalHistoryID')->values();
}
+
+ if ($returnedCount) {
+ return $historyQuery->count();
+ }
+
+ return $historyQuery
+ ->with(['surgicalOperations' => function ($query) use ($startDate, $endDate) {
+ $query->where('Date', '>=', $startDate)
+ ->where('Date', '<=', $endDate);
+ }])
+ ->orderBy('DateRecipient', 'DESC')
+ ->get();
} else {
// Разная логика для заведующего и врача
if ($isHeadOrAdmin) {
@@ -795,42 +727,16 @@ class ReportController extends Controller
*/
private function getAllOutcomePatients($branchId, $startDate, $endDate, bool $returnedCount = false)
{
- // Сначала получаем миграции с типами выбытия
- $migrations = MisMigrationPatient::outcomePatients($branchId, $startDate, $endDate)
- ->select('rf_MedicalHistoryID', 'rf_kl_VisitResultID', 'DateOut')
- ->get()
- ->groupBy('rf_MedicalHistoryID');
+ $query = $this->buildOutcomeMedicalHistoryQuery($branchId, $startDate, $endDate);
- if ($migrations->isEmpty()) {
- if ($returnedCount) return 0;
- return collect();
+ if ($returnedCount) {
+ return $query->count();
}
- $medicalHistoryIds = $migrations->keys()->toArray();
-
- // Получаем истории
- $patients = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
+ return $query
->with(['surgicalOperations'])
- ->orderBy('DateRecipient', 'DESC');
-
- if ($returnedCount) return $patients->count();
- else $patients = $patients->get();
-
- // Добавляем информацию о типе выбытия
- return $patients->map(function ($patient) use ($migrations) {
- $patientMigrations = $migrations->get($patient->MedicalHistoryID, collect());
-
- // Определяем основной тип выбытия (берем последнюю миграцию)
- $latestMigration = $patientMigrations->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;
- });
+ ->orderBy('DateRecipient', 'DESC')
+ ->get();
}
/**
@@ -852,26 +758,45 @@ class ReportController extends Controller
*/
private function getDeceasedOutcomePatients($branchId, $startDate, $endDate, bool $returnedCount = false, bool $onlyIds = false)
{
- $medicalHistoryIds = MisMigrationPatient::deceasedOutcome($branchId, $startDate, $endDate)
- ->pluck('rf_MedicalHistoryID')
- ->unique()
- ->toArray();
-
- if (empty($medicalHistoryIds)) {
- if ($returnedCount) return 0;
- return collect();
- }
+ $query = $this->buildOutcomeMedicalHistoryQuery(
+ $branchId,
+ $startDate,
+ $endDate,
+ [5, 6, 15, 16]
+ );
if ($onlyIds) {
- return $medicalHistoryIds;
+ return $query->pluck('MedicalHistoryID')->values();
}
- $query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
- ->with(['surgicalOperations'])
- ->orderBy('DateRecipient', 'DESC');
-
if ($returnedCount) return $query->count();
- else return $query->get();
+ else return $query
+ ->with(['surgicalOperations'])
+ ->orderBy('DateRecipient', 'DESC')
+ ->get();
+ }
+
+ private function buildOutcomeMedicalHistoryQuery(
+ int $branchId,
+ string $startDate,
+ string $endDate,
+ ?array $visitResultIds = null
+ )
+ {
+ $startDateOnly = Carbon::parse($startDate)->toDateString();
+ $endDateOnly = Carbon::parse($endDate)->toDateString();
+
+ return MisMedicalHistory::query()
+ ->where('MedicalHistoryID', '<>', 0)
+ ->whereDate('DateExtract', '>', $startDateOnly)
+ ->whereDate('DateExtract', '<=', $endDateOnly)
+ ->whereHas('migrations', function ($migrationQuery) use ($branchId, $visitResultIds) {
+ $migrationQuery->where('rf_StationarBranchID', $branchId);
+
+ if ($visitResultIds !== null && !empty($visitResultIds)) {
+ $migrationQuery->whereIn('rf_kl_VisitResultID', $visitResultIds);
+ }
+ });
}
/**
@@ -880,6 +805,7 @@ class ReportController extends Controller
private function getSurgicalPatients(string $status, bool $isHeadOrAdmin, $branchId, $startDate, $endDate, bool $returnedCount = false)
{
$query = MisSurgicalOperation::where('rf_StationarBranchID', $branchId)
+ ->completed()
// ->whereBetween('Date', [$startDate, $endDate])
->where('Date', '>=', $startDate)
->where('Date', '<=', $endDate)
@@ -930,14 +856,199 @@ class ReportController extends Controller
Request $request,
) {
$data = $request->validate([
- 'id' => 'required'
+ 'id' => 'required|string'
]);
- ObservationPatient::where('rf_medicalhistory_id', $data['id'])->delete();
+ $this->reportService->removeObservationPatient($data['id']);
return response()->json()->setStatusCode(200);
}
+ public function createManualPatient(Request $request)
+ {
+ $user = Auth::user();
+ $data = $request->validate([
+ 'departmentId' => 'required|integer',
+ 'full_name' => 'required|string|max:255',
+ 'birth_date' => 'required|date',
+ 'patient_kind' => 'required|in:plan,emergency',
+ 'diagnosis_code' => 'nullable|string|max:255',
+ 'diagnosis_name' => 'nullable|string|max:1000',
+ 'admitted_at' => 'nullable|date',
+ ]);
+
+ $department = Department::where('department_id', $data['departmentId'])->firstOrFail();
+ $patient = $this->reportService->createManualPatient($department, $user, $data);
+
+ return response()->json($patient, 201);
+ }
+
+ public function setManualPatientOutcome(Request $request, int $departmentPatientId)
+ {
+ $data = $request->validate([
+ 'outcome_type' => 'required|in:discharged,transferred,deceased',
+ 'outcome_at' => 'nullable|date',
+ ]);
+
+ return response()->json(
+ $this->reportService->setManualPatientOutcome($departmentPatientId, $data)
+ );
+ }
+
+ public function updateManualPatient(Request $request, int $departmentPatientId)
+ {
+ $user = Auth::user();
+
+ $data = $request->validate([
+ 'full_name' => 'required|string|max:255',
+ 'birth_date' => 'required|date',
+ 'patient_kind' => 'required|in:plan,emergency',
+ 'diagnosis_code' => 'nullable|string|max:255',
+ 'diagnosis_name' => 'nullable|string|max:1000',
+ 'admitted_at' => 'nullable|date',
+ 'startAt' => 'nullable',
+ 'endAt' => 'nullable',
+ ]);
+
+ return response()->json(
+ $this->reportService->updateManualPatient(
+ $user,
+ $departmentPatientId,
+ $data
+ )
+ );
+ }
+
+ public function linkManualPatient(Request $request, int $departmentPatientId)
+ {
+ $data = $request->validate([
+ 'medical_history_id' => 'required|integer',
+ ]);
+
+ return response()->json(
+ $this->reportService->linkManualPatientToMis($departmentPatientId, $data['medical_history_id'])
+ );
+ }
+
+ public function getManualPatientOperations(int $departmentPatientId)
+ {
+ $user = Auth::user();
+ $operations = $this->reportService->getManualPatientOperations($user, $departmentPatientId);
+
+ return response()->json(
+ DepartmentPatientOperationResource::collection($operations)
+ );
+ }
+
+ public function createManualPatientOperation(Request $request, int $departmentPatientId)
+ {
+ $user = Auth::user();
+ $data = $request->validate([
+ 'service_id' => 'required|integer',
+ 'urgency' => 'required|in:plan,emergency',
+ 'started_at' => 'required|date',
+ 'ended_at' => 'required|date|after_or_equal:started_at',
+ ]);
+
+ $operation = $this->reportService->createManualPatientOperation($user, $departmentPatientId, $data);
+
+ return response()->json(new DepartmentPatientOperationResource($operation), 201);
+ }
+
+ public function updateManualPatientOperation(Request $request, int $departmentPatientId, int $operationId)
+ {
+ $user = Auth::user();
+ $data = $request->validate([
+ 'service_id' => 'required|integer',
+ 'urgency' => 'required|in:plan,emergency',
+ 'started_at' => 'required|date',
+ 'ended_at' => 'required|date|after_or_equal:started_at',
+ ]);
+
+ $operation = $this->reportService->updateManualPatientOperation($user, $departmentPatientId, $operationId, $data);
+
+ return response()->json(new DepartmentPatientOperationResource($operation));
+ }
+
+ public function deleteManualPatientOperation(int $departmentPatientId, int $operationId)
+ {
+ $user = Auth::user();
+ $this->reportService->deleteManualPatientOperation($user, $departmentPatientId, $operationId);
+
+ return response()->json()->setStatusCode(204);
+ }
+
+ public function searchMisPatients(Request $request)
+ {
+ $data = $request->validate([
+ 'departmentId' => 'required|integer',
+ 'query' => 'required|string|min:2',
+ ]);
+
+ $department = Department::where('department_id', $data['departmentId'])->firstOrFail();
+ $patients = $this->reportService->searchMisPatientsForDepartment($department, $data['query']);
+
+ return response()->json(FormattedPatientResource::collection($patients));
+ }
+
+ public function searchMkb(Request $request)
+ {
+ $data = $request->validate([
+ 'query' => 'required|string|min:1|max:255',
+ ]);
+
+ $query = trim($data['query']);
+ $needle = mb_strtolower($query, 'UTF-8');
+ $like = "%{$needle}%";
+
+ $items = MisMKB::query()
+ ->select(['MKBID', 'DS', 'NAME'])
+ ->where(function ($builder) use ($like) {
+ $builder->whereRaw('LOWER("DS") LIKE ?', [$like])
+ ->orWhereRaw('LOWER("NAME") LIKE ?', [$like]);
+ })
+ ->orderBy('DS')
+ ->limit(30)
+ ->get()
+ ->map(fn (MisMKB $item) => [
+ 'id' => $item->MKBID,
+ 'code' => $item->DS,
+ 'name' => $item->NAME,
+ 'label' => trim(($item->DS ? "{$item->DS} " : '') . ($item->NAME ?? '')),
+ ])
+ ->values();
+
+ return response()->json($items);
+ }
+
+ public function searchMedicalServices(Request $request)
+ {
+ $data = $request->validate([
+ 'query' => 'required|string|min:2|max:255',
+ ]);
+
+ $query = trim($data['query']);
+
+ $items = \App\Models\MisServiceMedical::query()
+ ->select(['ServiceMedicalID', 'ServiceMedicalCode', 'ServiceMedicalName'])
+ ->where(function ($builder) use ($query) {
+ $builder->where('ServiceMedicalCode', 'like', "%{$query}%")
+ ->orWhere('ServiceMedicalName', 'like', "%{$query}%");
+ })
+ ->orderBy('ServiceMedicalCode')
+ ->limit(30)
+ ->get()
+ ->map(fn (\App\Models\MisServiceMedical $item) => [
+ 'id' => $item->ServiceMedicalID,
+ 'code' => $item->ServiceMedicalCode,
+ 'name' => $item->ServiceMedicalName,
+ 'label' => trim(($item->ServiceMedicalCode ? "{$item->ServiceMedicalCode} " : '') . ($item->ServiceMedicalName ?? '')),
+ ])
+ ->values();
+
+ return response()->json($items);
+ }
+
// api/report/unwanted-event
diff --git a/app/Http/Controllers/TestController.php b/app/Http/Controllers/TestController.php
new file mode 100644
index 0000000..9abeb7a
--- /dev/null
+++ b/app/Http/Controllers/TestController.php
@@ -0,0 +1,25 @@
+format('Y-m-d H:i:s');
+ $endAt = Carbon::parse('2026-03-31T23:59:00')->format('Y-m-d H:i:s');
+
+ $cacheKey = "branch_current_2";
+ \Cache::tags(["migrations_in_branch_outcome"])->flush();
+
+ $data = $migrationService->getMigrationsInBranchOutcome(2, $startAt, $endAt);
+ return Inertia::render('TestQuery', [
+ 'data' => $data
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Web/ReportController.php b/app/Http/Controllers/Web/ReportController.php
index 51c11b3..9c1a901 100644
--- a/app/Http/Controllers/Web/ReportController.php
+++ b/app/Http/Controllers/Web/ReportController.php
@@ -3,23 +3,18 @@
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
-use App\Http\Resources\Mis\FormattedPatientResource;
use App\Models\Department;
-use App\Models\MetrikaGroup;
-use App\Models\MetrikaItem;
-use App\Models\MisLpuDoctor;
-use App\Models\Report;
-use App\Models\UnwantedEvent;
use App\Services\DateRangeService;
+use App\Services\ReportPageService;
use App\Services\ReportService;
use Illuminate\Http\Request;
-use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
class ReportController extends Controller
{
public function __construct(
+ protected ReportPageService $reportPageService,
protected ReportService $reportService,
protected DateRangeService $dateRangeService
) {}
@@ -28,40 +23,10 @@ class ReportController extends Controller
{
$user = Auth::user();
$departmentId = $request->query('departmentId', $user->department->department_id);
- $department = Department::where('department_id', $departmentId)->first(); //$user->department;
-
+ $department = Department::where('department_id', $departmentId)->firstOrFail();
$dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user);
- // Получаем статистику
- $statistics = $this->reportService->getReportStatistics($department, $user, $dateRange);
-
- // Получаем метрики
-// $metrikaGroup = MetrikaGroup::whereMetrikaGroupId(2)->first();
-// $metrikaItems = $metrikaGroup->metrikaItems;
- $metrikaItems = MetrikaItem::whereIn('metrika_item_id', [3, 7, 8, 17])->get();
-
- // Получаем информацию о текущем отчете
- $reportInfo = $this->reportService->getCurrentReportInfo($department, $user, $dateRange);
-
- return Inertia::render('Report/Index', [
- 'department' => [
- 'department_name' => $department->name_full,
- 'department_id' => $department->department_id,
- 'beds' => $department->beds,
- 'percentLoadedBeds' => $this->calculateBedOccupancy($department, $user),
- 'recipientPlanOfYear' => $this->reportService->getRecipientPlanOfYear($department, $dateRange)['plan'],
- 'progressPlanOfYear' => $this->reportService->getRecipientPlanOfYear($department, $dateRange)['progress'],
- ...$statistics,
- ],
- 'dates' => [
- 'startAt' => $dateRange->startTimestamp(),
- 'endAt' => $dateRange->endTimestamp()
- ],
- 'report' => $reportInfo,
- 'metrikaItems' => $metrikaItems,
- 'userId' => $reportInfo['userId'],
- 'userName' => $reportInfo['userName']
- ]);
+ return Inertia::render('Report/Index', $this->reportPageService->build($department, $user, $dateRange));
}
public function store(Request $request)
@@ -76,90 +41,8 @@ class ReportController extends Controller
'reportId' => 'nullable|integer'
]);
- $report = $this->reportService->storeReport($validated, Auth::user(), false);
+ $this->reportService->storeReport($validated, Auth::user(), false);
return redirect()->route('start');
}
-
- public function getPatients(Request $request)
- {
- $user = Auth::user();
-
- $validated = $request->validate([
- 'status' => 'required|string',
- 'startAt' => 'nullable',
- 'endAt' => 'nullable',
- ]);
-
- $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user);
-
- $patients = $this->reportService->getPatientsByStatus(
- Auth::user(),
- $validated['status'],
- $dateRange,
- );
-
- return response()->json(FormattedPatientResource::collection($patients));
- }
-
- public function getPatientsCount(Request $request)
- {
- $user = Auth::user();
-
- $validated = $request->validate([
- 'status' => 'required|string',
- 'startAt' => 'nullable',
- 'endAt' => 'nullable',
- ]);
-
- $dateRange = $this->dateRangeService->getDateRangeFromRequest($request, $user);
-
- $count = $this->reportService->getPatientsCountByStatus(
- Auth::user(),
- $validated['status'],
- $dateRange,
- );
-
- return response()->json($count);
- }
-
- public function removeObservation(Request $request)
- {
- $validated = $request->validate(['id' => 'required|integer']);
-
- $this->reportService->removeObservationPatient($validated['id']);
-
- return response()->json(['message' => 'Удалено'], 200);
- }
-
- public function removeUnwantedEvent(UnwantedEvent $unwantedEvent)
- {
- $unwantedEvent->delete();
- return response()->json(['message' => 'Удалено'], 200);
- }
-
- public function getDepartmentUsers()
- {
- $users = MisLpuDoctor::select(['LPUDoctorID', 'FAM_V', 'IM_V', 'OT_V'])
- ->active()
- ->inMyDepartment()
- ->get();
-
- return response()->json($users, 200);
- }
-
- /**
- * Рассчитать загруженность коек
- */
- private function calculateBedOccupancy(Department $department, $user): int
- {
- $beds = (int)$department->metrikaDefault()->where('rf_metrika_item_id', 1)->first()->value;
- $occupiedBeds = optional(Report::where('rf_department_id', $department->department_id)
- ->join('metrika_results', 'reports.report_id', '=', 'metrika_results.rf_report_id')
- ->where('metrika_results.rf_metrika_item_id', 8)
- ->orderBy('sent_at', 'desc')
- ->first())->value ?? 0;
-
- return $beds > 0 ? round(intval($occupiedBeds) * 100 / $beds) : 0;
- }
}
diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php
index 131bef0..f65c0a9 100644
--- a/app/Http/Middleware/HandleInertiaRequests.php
+++ b/app/Http/Middleware/HandleInertiaRequests.php
@@ -44,6 +44,9 @@ class HandleInertiaRequests extends Middleware
'version' => config('app.version'),
'tag' => config('app.tag')
],
+ 'config' => [
+ 'timeEventSourceUrl' => config('time.eventSourceUrl')
+ ],
'user' => $user ? [
'name' => $user->name,
'token' => Session::get('token'),
diff --git a/app/Http/Resources/Api/DepartmentPatientOperationResource.php b/app/Http/Resources/Api/DepartmentPatientOperationResource.php
new file mode 100644
index 0000000..05f4b3d
--- /dev/null
+++ b/app/Http/Resources/Api/DepartmentPatientOperationResource.php
@@ -0,0 +1,36 @@
+resource;
+
+ $serviceCode = $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code;
+ $serviceName = $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name;
+
+ return [
+ 'id' => $operation->department_patient_operation_id,
+ 'urgency' => $operation->urgency,
+ 'service' => [
+ 'id' => $operation->rf_kl_service_medical_id,
+ 'code' => $serviceCode,
+ 'name' => $serviceName,
+ 'label' => trim(($serviceCode ? "{$serviceCode} " : '') . ($serviceName ?? '')),
+ ],
+ 'startAt' => $operation->started_at?->toIso8601String(),
+ 'endAt' => $operation->ended_at?->toIso8601String(),
+ 'duration' => $operation->started_at && $operation->ended_at
+ ? Carbon::parse($operation->started_at)->diffInMinutes(Carbon::parse($operation->ended_at))
+ : null,
+ ];
+ }
+}
diff --git a/app/Http/Resources/Mis/FormattedPatientResource.php b/app/Http/Resources/Mis/FormattedPatientResource.php
index c83f070..b7fccaf 100644
--- a/app/Http/Resources/Mis/FormattedPatientResource.php
+++ b/app/Http/Resources/Mis/FormattedPatientResource.php
@@ -2,7 +2,7 @@
namespace App\Http\Resources\Mis;
-use App\Models\MisSurgicalOperation;
+use App\Data\UnifiedPatientData;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Carbon;
@@ -17,9 +17,33 @@ class FormattedPatientResource extends JsonResource
*/
public function toArray(Request $request): array
{
+ if ($this->resource instanceof UnifiedPatientData) {
+ $age = $this->formatAge($this->age);
+
+ return [
+ 'id' => $this->id,
+ 'patient_uid' => $this->patientUid,
+ 'source_type' => $this->sourceType,
+ 'department_patient_id' => $this->departmentPatientId,
+ 'medical_history_id' => $this->medicalHistoryId,
+ 'mkb' => $this->mkb,
+ 'operations' => $this->operations,
+ 'fullname' => $this->fullname,
+ 'age' => $age,
+ 'birth_date' => $this->birthDate ? Carbon::parse($this->birthDate)->format('d.m.Y') : null,
+ 'patient_kind' => $this->patientKind,
+ 'admitted_at' => $this->admittedAt ? Carbon::parse($this->admittedAt)->format('d.m.Y H:i') : null,
+ 'outcome_type' => $this->outcomeType,
+ 'outcome_date' => $this->outcomeDate,
+ 'comment' => $this->comment,
+ 'is_recipient_today' => $this->isRecipientToday,
+ 'is_manual' => $this->isManual,
+ 'can_manage_manual' => $this->canManageManual,
+ ];
+ }
+
return [
'id' => $this->MedicalHistoryID,
- 'num' => $this->num,
'mkb' => [
'ds' => $this->outcomeMigration->first()->mainDiagnosis?->mkb?->DS,
'name' => $this->outcomeMigration->first()->mainDiagnosis?->mkb?->NAME
@@ -31,10 +55,41 @@ class FormattedPatientResource extends JsonResource
];
}),
'fullname' => Str::ucwords(Str::lower("$this->FAMILY $this->Name $this->OT")),
- 'age' => Carbon::parse($this->BD)->diff(Carbon::now())->format('%y'),
+ 'age' => $this->formatAge(Carbon::parse($this->BD)->diffInYears(Carbon::now(), false)),
'birth_date' => Carbon::parse($this->BD)->format('d.m.Y'),
+ 'admitted_at' => $this->DateRecipient ? Carbon::parse($this->DateRecipient)->format('d.m.Y H:i') : null,
'outcome_type' => $this->when($this->outcome_type, $this->outcome_type),
- 'comment' => $this->when($this->comment, $this->comment)
+ 'outcome_date' => $this->when($this->outcome_date, $this->outcome_date),
+ 'comment' => $this->when($this->comment, $this->comment),
+ 'is_recipient_today' => (bool) ($this->is_recipient_today ?? false),
+ 'is_manual' => false,
+ 'can_manage_manual' => false,
];
}
+
+ private function formatAge($value): ?string
+ {
+ if ($value === null || $value === '') {
+ return null;
+ }
+
+ $age = abs((int) $value);
+ $suffix = $this->ageSuffix($age);
+
+ return "{$age} {$suffix}";
+ }
+
+ private function ageSuffix(int $age): string
+ {
+ $mod100 = $age % 100;
+ if ($mod100 >= 11 && $mod100 <= 14) {
+ return 'лет';
+ }
+
+ return match ($age % 10) {
+ 1 => 'год',
+ 2, 3, 4 => 'года',
+ default => 'лет',
+ };
+ }
}
diff --git a/app/Models/DepartmentPatient.php b/app/Models/DepartmentPatient.php
new file mode 100644
index 0000000..31e8f1e
--- /dev/null
+++ b/app/Models/DepartmentPatient.php
@@ -0,0 +1,55 @@
+ 'date',
+ 'admitted_at' => 'datetime',
+ 'is_current' => 'boolean',
+ 'outcome_at' => 'datetime',
+ 'linked_to_mis_at' => 'datetime',
+ ];
+
+ public function scopeCurrent($query)
+ {
+ return $query->where('is_current', true);
+ }
+
+ public function scopeManual($query)
+ {
+ return $query->where('source_type', 'manual');
+ }
+
+ public function observationPatients()
+ {
+ return $this->hasMany(ObservationPatient::class, 'rf_department_patient_id', 'department_patient_id');
+ }
+
+ public function operations()
+ {
+ return $this->hasMany(DepartmentPatientOperation::class, 'rf_department_patient_id', 'department_patient_id');
+ }
+}
diff --git a/app/Models/DepartmentPatientOperation.php b/app/Models/DepartmentPatientOperation.php
new file mode 100644
index 0000000..d5fdf2f
--- /dev/null
+++ b/app/Models/DepartmentPatientOperation.php
@@ -0,0 +1,36 @@
+ 'datetime',
+ 'ended_at' => 'datetime',
+ ];
+
+ public function patient()
+ {
+ return $this->belongsTo(DepartmentPatient::class, 'rf_department_patient_id', 'department_patient_id');
+ }
+
+ public function serviceMedical()
+ {
+ return $this->belongsTo(MisServiceMedical::class, 'rf_kl_service_medical_id', 'ServiceMedicalID');
+ }
+}
diff --git a/app/Models/LifeMisMigrationPatient.php b/app/Models/LifeMisMigrationPatient.php
index aac655a..5f9360f 100644
--- a/app/Models/LifeMisMigrationPatient.php
+++ b/app/Models/LifeMisMigrationPatient.php
@@ -4,6 +4,7 @@ namespace App\Models;
use App\Services\DateRange;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Carbon;
class LifeMisMigrationPatient extends Model
{
@@ -72,18 +73,19 @@ class LifeMisMigrationPatient extends Model
*/
public function scopeOutcomePatients($query, $branchId = null, DateRange $dateRange = null)
{
- $query->where('rf_kl_VisitResultID', '<>', 0) // не активное лечение
- ->whereDate('DateOut', '<>', '1900-01-01') // есть дата выбытия
- ->where('rf_MedicalHistoryID', '<>', 0);
+ $query->where('rf_MedicalHistoryID', '<>', 0);
if ($branchId) {
$query->where('rf_StationarBranchID', $branchId);
}
if ($dateRange) {
-// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]);
- $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
}
return $query;
@@ -94,11 +96,10 @@ class LifeMisMigrationPatient extends Model
*/
public function scopeOutcomeDischarged($query, $branchId = null, DateRange $dateRange = null)
{
- // ID выписки
- $dischargeCodes = [1, 7, 8, 9, 10, 11, 48, 49, 124];
+ // По уточненному SQL: Выписано за период
+ $dischargeCodes = [1, 11, 2, 12, 7, 18, 48];
$query->whereIn('rf_kl_VisitResultID', $dischargeCodes)
- ->whereDate('DateOut', '<>', '1900-01-01')
->where('rf_MedicalHistoryID', '<>', 0);
if ($branchId) {
@@ -106,9 +107,12 @@ class LifeMisMigrationPatient extends Model
}
if ($dateRange) {
-// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]);
- $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
}
return $query;
@@ -119,11 +123,10 @@ class LifeMisMigrationPatient extends Model
*/
public function scopeOutcomeTransferred($query, $branchId = null, DateRange $dateRange = null)
{
- // ID перевода
- $transferCodes = [2, 3, 4, 12, 13, 14];
+ // По заданному SQL: только эти коды перевода
+ $transferCodes = [4, 14];
$query->whereIn('rf_kl_VisitResultID', $transferCodes)
- ->whereDate('DateOut', '<>', '1900-01-01')
->where('rf_MedicalHistoryID', '<>', 0);
if ($branchId) {
@@ -131,9 +134,12 @@ class LifeMisMigrationPatient extends Model
}
if ($dateRange) {
-// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]);
- $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
}
return $query;
@@ -148,7 +154,6 @@ class LifeMisMigrationPatient extends Model
$deceasedCodes = [5, 6, 15, 16];
$query->whereIn('rf_kl_VisitResultID', $deceasedCodes)
- ->whereDate('DateOut', '<>', '1900-01-01')
->where('rf_MedicalHistoryID', '<>', 0);
if ($branchId) {
@@ -156,9 +161,12 @@ class LifeMisMigrationPatient extends Model
}
if ($dateRange) {
-// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]);
- $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
}
return $query;
@@ -166,11 +174,8 @@ class LifeMisMigrationPatient extends Model
public function scopeOutcomeWithoutTransferred($query, $branchId = null, DateRange $dateRange = null)
{
- // ID выписанных, без переводных
- $outcomeWithoutTransferredIds = [5, 6, 15, 16, 1, 7, 8, 9, 10, 11, 48, 49, 124];
-
- $query->whereIn('rf_kl_VisitResultID', $outcomeWithoutTransferredIds)
- ->whereDate('DateOut', '<>', '1900-01-01')
+ $query->whereNotIn('rf_kl_VisitResultID', [4, 14])
+ ->where('rf_kl_VisitResultID', '<>', 0)
->where('rf_MedicalHistoryID', '<>', 0);
if ($branchId) {
@@ -178,9 +183,12 @@ class LifeMisMigrationPatient extends Model
}
if ($dateRange) {
-// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]);
- $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
}
return $query;
@@ -194,9 +202,12 @@ class LifeMisMigrationPatient extends Model
$query->where('rf_kl_VisitResultID', '<>', 0)
->where('rf_MedicalHistoryID', '<>', 0)
->when($dateRange, function($query) use ($dateRange) {
-// return $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]);
- return $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ return $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
});
if ($branchId) {
diff --git a/app/Models/MedicalHistorySnapshot.php b/app/Models/MedicalHistorySnapshot.php
index c433ef7..03c7667 100644
--- a/app/Models/MedicalHistorySnapshot.php
+++ b/app/Models/MedicalHistorySnapshot.php
@@ -11,7 +11,26 @@ class MedicalHistorySnapshot extends Model
protected $fillable = [
'rf_report_id',
'rf_medicalhistory_id',
+ 'rf_department_patient_id',
'patient_type',
+ 'patient_uid',
+ 'patient_source_type',
+ 'patient_kind',
+ 'full_name',
+ 'birth_date',
+ 'diagnosis_code',
+ 'diagnosis_name',
+ 'admitted_at',
+ 'outcome_type',
+ 'outcome_at',
+ 'is_manual',
+ ];
+
+ protected $casts = [
+ 'birth_date' => 'date',
+ 'admitted_at' => 'datetime',
+ 'outcome_at' => 'datetime',
+ 'is_manual' => 'boolean',
];
/**
@@ -30,6 +49,11 @@ class MedicalHistorySnapshot extends Model
return $this->belongsTo(MisMedicalHistory::class, 'rf_medicalhistory_id', 'MedicalHistoryID');
}
+ public function departmentPatient()
+ {
+ return $this->belongsTo(DepartmentPatient::class, 'rf_department_patient_id', 'department_patient_id');
+ }
+
// Скоупы для фильтрации
public function scopeForReport($query, $reportId)
{
diff --git a/app/Models/MisMedicalHistory.php b/app/Models/MisMedicalHistory.php
index 5440169..f04c0f8 100644
--- a/app/Models/MisMedicalHistory.php
+++ b/app/Models/MisMedicalHistory.php
@@ -87,13 +87,15 @@ class MisMedicalHistory extends Model
public function surgicalOperations()
{
- return $this->hasMany(MisSurgicalOperation::class, 'rf_MedicalHistoryID', 'MedicalHistoryID');
+ return $this->hasMany(MisSurgicalOperation::class, 'rf_MedicalHistoryID', 'MedicalHistoryID')
+ ->completed();
}
public function surgicalOperationsInBranch($branchId)
{
$operations = MisSurgicalOperation::where('rf_MedicalHistoryID', $this->MedicalHistoryID)
->where('rf_StationarBranchID', $branchId)
+ ->completed()
->get();
return $operations;
@@ -109,7 +111,10 @@ class MisMedicalHistory extends Model
public function scopeCurrentlyHospitalized($query)
{
- return $query->whereDate('DateExtract', '1900-01-01')
+ return $query->where(function ($builder) {
+ $builder->whereDate('DateExtract', '1900-01-01')
+ ->orWhereDate('DateExtract', '2222-01-01');
+ })
->where('MedicalHistoryID', '<>', 0);
}
@@ -126,7 +131,7 @@ class MisMedicalHistory extends Model
*/
public function scopeEmergency($query)
{
- return $query->where('rf_EmerSignID', 2);
+ return $query->whereIn('rf_EmerSignID', [2, 4]);
}
/*
@@ -152,6 +157,12 @@ class MisMedicalHistory extends Model
->orderBy('DateOut', 'desc');
}
+ public function latestMigration()
+ {
+ return $this->hasOne(MisMigrationPatient::class, 'rf_MedicalHistoryID', 'MedicalHistoryID')
+ ->ofMany('DateOut', 'max');
+ }
+
/*
* Движение по StationarBranch
*/
@@ -181,4 +192,9 @@ class MisMedicalHistory extends Model
}
});
}
+
+ public function operationPurpose()
+ {
+ return $this->belongsTo(MisOperationPurpose::class, 'MedicalHistoryID', 'rf_MedicalHistoryID');
+ }
}
diff --git a/app/Models/MisMigrationPatient.php b/app/Models/MisMigrationPatient.php
index b649028..bd219f8 100644
--- a/app/Models/MisMigrationPatient.php
+++ b/app/Models/MisMigrationPatient.php
@@ -11,6 +11,11 @@ class MisMigrationPatient extends Model
protected $table = 'stt_migrationpatient';
protected $primaryKey = 'MigrationPatientID';
+ protected $casts = [
+ 'DateIngoing' => 'datetime:Y-m-d H:i:s',
+ 'DateOut' => 'datetime:Y-m-d H:i:s',
+ ];
+
public function branch()
{
return $this->hasOne(MisStationarBranch::class, 'StationarBranchID', 'rf_StationarBranchID');
@@ -31,18 +36,12 @@ class MisMigrationPatient extends Model
return $this->hasOne(MisMKB::class, 'MKBID', 'rf_MKBID');
}
- public function medicalHistory()
- {
- return $this->belongsTo(MisMedicalHistory::class, 'rf_MedicalHistoryID', 'MedicalHistoryID');
- }
-
/**
* Находятся на лечении
*/
public function scopeCurrentlyInTreatment($query, $branchId = null, DateRange $dateRange = null)
{
- $query->where('rf_kl_VisitResultID', 0)
- ->where('rf_kl_StatCureResultID', 0)
+ $query->whereNotIn('rf_kl_VisitResultID', [4])
->whereHas('medicalHistory', function ($query) use ($branchId, $dateRange) {
$query->whereDate('DateExtract', '1900-01-01');
})
@@ -77,18 +76,19 @@ class MisMigrationPatient extends Model
*/
public function scopeOutcomePatients($query, $branchId = null, DateRange $dateRange = null)
{
- $query->where('rf_kl_VisitResultID', '<>', 0) // не активное лечение
- ->whereDate('DateOut', '<>', '1900-01-01') // есть дата выбытия
- ->where('rf_MedicalHistoryID', '<>', 0);
+ $query->where('rf_MedicalHistoryID', '<>', 0);
if ($branchId) {
$query->where('rf_StationarBranchID', $branchId);
}
if ($dateRange) {
- $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
-// $query->whereBetween('DateOut', [$dateRange->startSql(), $dateRange->endSql()]);
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
}
return $query;
@@ -99,11 +99,10 @@ class MisMigrationPatient extends Model
*/
public function scopeOutcomeDischarged($query, $branchId = null, DateRange $dateRange = null)
{
- // ID выписки
- $dischargeCodes = [1, 7, 8, 9, 10, 11, 48, 49, 124];
+ // По уточненному SQL: Выписано за период
+ $dischargeCodes = [1, 11, 2, 12, 7, 18, 48];
$query->whereIn('rf_kl_VisitResultID', $dischargeCodes)
- ->whereDate('DateOut', '<>', '1900-01-01')
->where('rf_MedicalHistoryID', '<>', 0);
if ($branchId) {
@@ -111,8 +110,12 @@ class MisMigrationPatient extends Model
}
if ($dateRange) {
- $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
}
return $query;
@@ -123,11 +126,10 @@ class MisMigrationPatient extends Model
*/
public function scopeOutcomeTransferred($query, $branchId = null, DateRange $dateRange = null)
{
- // ID перевода
- $transferCodes = [2, 3, 4, 12, 13, 14];
+ // По заданному SQL: только эти коды перевода
+ $transferCodes = [4, 14];
$query->whereIn('rf_kl_VisitResultID', $transferCodes)
- ->whereDate('DateOut', '<>', '1900-01-01')
->where('rf_MedicalHistoryID', '<>', 0);
if ($branchId) {
@@ -135,8 +137,12 @@ class MisMigrationPatient extends Model
}
if ($dateRange) {
- $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
}
return $query;
@@ -151,7 +157,6 @@ class MisMigrationPatient extends Model
$deceasedCodes = [5, 6, 15, 16];
$query->whereIn('rf_kl_VisitResultID', $deceasedCodes)
- ->whereDate('DateOut', '<>', '1900-01-01')
->where('rf_MedicalHistoryID', '<>', 0);
if ($branchId) {
@@ -159,8 +164,12 @@ class MisMigrationPatient extends Model
}
if ($dateRange) {
- $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
}
return $query;
@@ -168,11 +177,9 @@ class MisMigrationPatient extends Model
public function scopeOutcomeWithoutTransferred($query, $branchId = null, DateRange $dateRange = null)
{
- // ID выписанных, без переводных
- $outcomeWithoutTransferredIds = [5, 6, 15, 16, 1, 7, 8, 9, 10, 11, 48, 49, 124];
-
- $query->whereIn('rf_kl_VisitResultID', $outcomeWithoutTransferredIds)
- ->whereDate('DateOut', '<>', '1900-01-01')
+ // По заданной логике переводы только 4 и 14, исключаем их
+ $query->whereNotIn('rf_kl_VisitResultID', [4, 14])
+ ->where('rf_kl_VisitResultID', '<>', 0)
->where('rf_MedicalHistoryID', '<>', 0);
if ($branchId) {
@@ -180,8 +187,12 @@ class MisMigrationPatient extends Model
}
if ($dateRange) {
- $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
}
return $query;
@@ -195,8 +206,12 @@ class MisMigrationPatient extends Model
$query->where('rf_kl_VisitResultID', '<>', 0)
->where('rf_MedicalHistoryID', '<>', 0)
->when($dateRange, function($query) use ($dateRange) {
- return $query->where('DateOut', '>=', $dateRange->startSql())
- ->where('DateOut', '<=', $dateRange->endSql());
+ $startDate = Carbon::parse($dateRange->startSql())->toDateString();
+ $endDate = Carbon::parse($dateRange->endSql())->toDateString();
+ return $query->whereHas('medicalHistory', function ($mhQuery) use ($startDate, $endDate) {
+ $mhQuery->whereDate('DateExtract', '>', $startDate)
+ ->whereDate('DateExtract', '<=', $endDate);
+ });
});
if ($branchId) {
@@ -205,4 +220,9 @@ class MisMigrationPatient extends Model
return $query;
}
+
+ public function medicalHistory()
+ {
+ return $this->belongsTo(MisMedicalHistory::class, 'rf_MedicalHistoryID', 'MedicalHistoryID');
+ }
}
diff --git a/app/Models/MisOperationPurpose.php b/app/Models/MisOperationPurpose.php
new file mode 100644
index 0000000..f176941
--- /dev/null
+++ b/app/Models/MisOperationPurpose.php
@@ -0,0 +1,17 @@
+belongsTo(MisSurgicalOperation::class, 'rf_SurgicalOperationID', 'SurgicalOperationID');
+ }
+}
\ No newline at end of file
diff --git a/app/Models/MisReanimation.php b/app/Models/MisReanimation.php
new file mode 100644
index 0000000..b7b8a90
--- /dev/null
+++ b/app/Models/MisReanimation.php
@@ -0,0 +1,17 @@
+belongsTo(MisMigrationPatient::class, 'rf_MigrationPatientID', 'MigrationPatientID');
+ }
+}
diff --git a/app/Models/MisSurgicalOperation.php b/app/Models/MisSurgicalOperation.php
index ef80dac..f0ad4de 100644
--- a/app/Models/MisSurgicalOperation.php
+++ b/app/Models/MisSurgicalOperation.php
@@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class MisSurgicalOperation extends Model
{
+ private const COMPLETED_OPERATION_STATUS_ID = 3;
+
protected $table = 'stt_surgicaloperation';
protected $primaryKey = 'SurgicalOperationID';
@@ -18,4 +20,16 @@ class MisSurgicalOperation extends Model
{
return $this->belongsTo(MisMedicalHistory::class, 'rf_MedicalHistoryID', 'MedicalHistoryID');
}
+
+ public function operationPurpose()
+ {
+ return $this->hasOne(MisOperationPurpose::class, 'rf_SurgicalOperationID', 'SurgicalOperationID');
+ }
+
+ public function scopeCompleted($query)
+ {
+ return $query->whereHas('operationPurpose', function ($purposeQuery) {
+ $purposeQuery->where('rf_OperationStatusID', self::COMPLETED_OPERATION_STATUS_ID);
+ });
+ }
}
diff --git a/app/Models/ObservationPatient.php b/app/Models/ObservationPatient.php
index 3e358ae..b0014c6 100644
--- a/app/Models/ObservationPatient.php
+++ b/app/Models/ObservationPatient.php
@@ -11,6 +11,7 @@ class ObservationPatient extends Model
protected $fillable = [
'rf_medicalhistory_id',
+ 'rf_department_patient_id',
'rf_mkab_id',
'rf_department_id',
'rf_report_id',
@@ -21,4 +22,9 @@ class ObservationPatient extends Model
{
return $this->belongsTo(MisMedicalHistory::class, 'rf_medicalhistory_id', 'MedicalHistoryID');
}
+
+ public function departmentPatient()
+ {
+ return $this->belongsTo(DepartmentPatient::class, 'rf_department_patient_id', 'department_patient_id');
+ }
}
diff --git a/app/Models/Report.php b/app/Models/Report.php
index 6e229b3..7b7fb91 100644
--- a/app/Models/Report.php
+++ b/app/Models/Report.php
@@ -20,7 +20,13 @@ class Report extends Model
'sent_at',
'rf_department_id',
'rf_user_id',
- 'rf_lpudoctor_id'
+ 'rf_lpudoctor_id',
+ 'period_type',
+ 'period_start',
+ 'period_end',
+ 'report_month',
+ 'report_year',
+ 'status',
];
public function metrikaResults(): \Illuminate\Database\Eloquent\Relations\HasMany
diff --git a/app/Services/AutoReportService.php b/app/Services/AutoReportService.php
index 69d1883..ed229f5 100644
--- a/app/Services/AutoReportService.php
+++ b/app/Services/AutoReportService.php
@@ -3,14 +3,13 @@
namespace App\Services;
use App\Models\Department;
+use App\Models\DepartmentPatient;
use App\Models\MedicalHistorySnapshot;
use App\Models\MetrikaResult;
-use App\Models\MisStationarBranch;
use App\Models\ObservationPatient;
use App\Models\Report;
use App\Models\UnwantedEvent;
use App\Models\User;
-use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -19,8 +18,7 @@ class AutoReportService
{
public function __construct(
protected ReportService $reportService,
- protected DateRangeService $dateRangeService,
- protected PatientService $patientQueryService
+ protected DateRangeService $dateRangeService
) {}
/**
@@ -30,18 +28,26 @@ class AutoReportService
User $user,
string $startDate,
string $endDate,
- $departmentId,
+ Department $department,
bool $force = false
): int {
$createdCount = 0;
+ // Для многодневного диапазона расширяем конец на 1 день,
+ // чтобы покрыть последние сутки (07:00 -> 07:00) целиком.
+ $start = \Carbon\Carbon::createFromFormat('Y-m-d', $startDate, 'Asia/Yakutsk');
+ $end = \Carbon\Carbon::createFromFormat('Y-m-d', $endDate, 'Asia/Yakutsk');
+ $periodEnd = $start->equalTo($end)
+ ? $end->copy()
+ : $end->copy()->addDay();
+
// Создаем период по дням
- $period = CarbonPeriod::create($startDate, $endDate);
+ $period = CarbonPeriod::create($start->toDateString(), $periodEnd->toDateString());
foreach ($period as $date) {
$dateRange = $this->dateRangeService->getNormalizedDateRange($user, $date, $date);
try {
- $reportCreated = $this->createReportForDate($user, $dateRange, $departmentId, $force);
+ $reportCreated = $this->createReportForDate($user, $department, $dateRange, $force);
if ($reportCreated) {
$createdCount++;
@@ -58,12 +64,12 @@ class AutoReportService
/**
* Создать отчет для конкретной даты
*/
- public function createReportForDate(User $user, DateRange $dateRange, $departmentId, bool $force = false): bool
+ public function createReportForDate(User $user, Department $department, DateRange $dateRange, bool $force = false): bool
{
- $user->rf_department_id = $departmentId;
+ $scopedUser = $this->scopeUserToDepartment($user, $department);
+
// Проверяем, существует ли уже отчет на эту дату
- $existingReport = Report::where('rf_department_id', $departmentId)
- ->whereDate('created_at', $dateRange->endSql())
+ $existingReport = Report::where('rf_department_id', $department->department_id)
->whereDate('sent_at', $dateRange->endSql())
->first();
@@ -73,19 +79,15 @@ class AutoReportService
// Если есть существующий отчет и force=true - удаляем его
if ($existingReport && $force) {
- MetrikaResult::where('rf_report_id', $existingReport->report_id)->delete();
- MedicalHistorySnapshot::where('rf_report_id', $existingReport->report_id)->delete();
- UnwantedEvent::where('rf_report_id', $existingReport->report_id)->delete();
- ObservationPatient::where('rf_report_id', $existingReport->report_id)->delete();
- $existingReport->delete();
+ $this->deleteExistingReport($existingReport);
}
// Получаем данные для отчета
- $reportData = $this->prepareReportData($user, $dateRange, $departmentId);
+ $reportData = $this->prepareReportData($scopedUser, $department, $dateRange);
// Создаем отчет
- DB::transaction(function () use ($user, $reportData) {
- $this->reportService->storeReport($reportData, $user);
+ DB::transaction(function () use ($scopedUser, $reportData) {
+ $this->reportService->storeReport($reportData, $scopedUser, true);
});
return true;
@@ -94,180 +96,29 @@ class AutoReportService
/**
* Подготовить данные для отчета
*/
- private function prepareReportData(User $user, DateRange $dateRange, $departmentId): array
+ private function prepareReportData(User $user, Department $department, DateRange $dateRange): array
{
- $department = Department::where('department_id', $departmentId)->first();
- $branchId = $this->getBranchId($department->rf_mis_department_id);
- $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
-
- // Получаем метрики
- $metrics = $this->calculateMetrics(
- $user,
- $isHeadOrAdmin,
- $branchId,
- $dateRange
- );
-
- // Получаем количество коек
- $beds = $this->getBedCount($department);
-
- // Формируем данные отчета
- return [
- 'departmentId' => $department->department_id,
- 'userId' => $user->rf_lpudoctor_id ?? $user->id,
- 'dates' => [
- $dateRange->startTimestamp(),
- $dateRange->endTimestamp()
- ],
- 'sent_at' => $dateRange->endSql(),
- 'created_at' => $dateRange->endSql(),
- 'metrics' => $this->formatMetrics($metrics),
- 'observationPatients' => $this->getObservationPatients($departmentId, $dateRange),
- 'unwantedEvents' => [],
- ];
+ return $this->reportService->buildAutoFillReportPayload($user, $department, $dateRange);
}
- /**
- * Рассчитать метрики для отчета
- */
- private function calculateMetrics(
- User $user,
- bool $isHeadOrAdmin,
- int $branchId,
- DateRange $dateRange
- ): array {
- $metrics = [];
-
- // 1. Плановые пациенты
- $metrics['plan'] = $this->patientQueryService->getPlanOrEmergencyPatients(
- 'plan',
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- true,
- false,
- true,
- true
- );
-
- // 2. Экстренные пациенты
- $metrics['emergency'] = $this->patientQueryService->getPlanOrEmergencyPatients(
- 'emergency',
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- true,
- false,
- true,
- true
- );
-
- // 3. Поступившие сегодня
- $metrics['recipient'] = $this->patientQueryService->getPlanOrEmergencyPatients(
- null,
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- true,
- false,
- false,
- true
- );
-
- // 4. Выписанные
- $metrics['discharged'] = $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'discharged'
- )->count();
-
- // 5. Переведенные
- $metrics['transferred'] = $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'transferred'
- )->count();
-
- // 6. Умершие
- $metrics['deceased'] = $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'deceased'
- )->count();
-
- // 7. Текущие пациенты
- $metrics['current'] = $this->patientQueryService->getAllPatientsInDepartment(
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- true
- );
-
- // 8. Плановые операции
- $metrics['plan_surgery'] = $this->patientQueryService->getSurgicalPatients(
- 'plan',
- $branchId,
- $dateRange,
- true
- );
-
- // 9. Экстренные операции
- $metrics['emergency_surgery'] = $this->patientQueryService->getSurgicalPatients(
- 'emergency',
- $branchId,
- $dateRange,
- true
- );
-
- return $metrics;
- }
-
- /**
- * Форматировать метрики для сохранения
- */
- private function formatMetrics(array $metrics): array
+ private function scopeUserToDepartment(User $user, Department $department): User
{
- return [
- 'metrika_item_4' => $metrics['plan'] ?? 0, // плановые
- 'metrika_item_12' => $metrics['emergency'] ?? 0, // экстренные
- 'metrika_item_3' => $metrics['recipient'] ?? 0, // поступившие
- // 'metrika_item_6' => ($metrics['plan_surgery'] ?? 0) + ($metrics['emergency_surgery'] ?? 0), // всего операций
- 'metrika_item_7' => $metrics['discharged'] + $metrics['deceased'], // выписанные
- 'metrika_item_8' => $metrics['current'] ?? 0, // текущие
- 'metrika_item_9' => $metrics['deceased'] ?? 0, // умершие
- 'metrika_item_11' => $metrics['plan_surgery'] ?? 0, // плановые операции
- 'metrika_item_10' => $metrics['emergency_surgery'] ?? 0, // экстренные операции
- 'metrika_item_13' => $metrics['transferred'] ?? 0, // переведенные
- 'metrika_item_14' => 0, // под наблюдением (будет заполнено отдельно)
- 'metrika_item_15' => $metrics['discharged'] ?? 0, // выбыло
- ];
+ $scopedUser = clone $user;
+ $scopedUser->rf_department_id = $department->department_id;
+ $scopedUser->setRelation('department', $department);
+
+ return $scopedUser;
}
- /**
- * Получить пациентов под наблюдением на дату
- */
- private function getObservationPatients(int $departmentId, DateRange $dateRange): array
+ private function deleteExistingReport(Report $report): void
{
- // Здесь нужно реализовать логику получения пациентов под наблюдением
- // на конкретную дату. Возможно, из снапшотов или истории.
- return []; // временно возвращаем пустой массив
+ DB::transaction(function () use ($report) {
+ MetrikaResult::where('rf_report_id', $report->report_id)->delete();
+ MedicalHistorySnapshot::where('rf_report_id', $report->report_id)->delete();
+ UnwantedEvent::where('rf_report_id', $report->report_id)->delete();
+ ObservationPatient::where('rf_report_id', $report->report_id)->delete();
+ DB::table('reports')->where('report_id', $report->report_id)->delete();
+ });
}
- /**
- * Получить ID отделения
- */
- private function getBranchId(int $misDepartmentId): ?int
- {
- return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId)
- ->value('StationarBranchID');
- }
-
- /**
- * Получить количество коек
- */
- private function getBedCount(Department $department): int
- {
- $default = $department->metrikaDefault()->where('rf_metrika_item_id', 1)->first();
- return (int)($default->value ?? 0);
- }
}
diff --git a/app/Services/DateRange.php b/app/Services/DateRange.php
index a493af9..bcd3f76 100644
--- a/app/Services/DateRange.php
+++ b/app/Services/DateRange.php
@@ -65,12 +65,12 @@ readonly class DateRange
public function startFirstOfMonth()
{
- return $this->startDate->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s');
+ return $this->startDate->copy()->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s');
}
public function endFirstOfMonth()
{
- return $this->endDate->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s');
+ return $this->endDate->copy()->firstOfMonth()->setHour(6)->format('Y-m-d H:i:s');
}
/**
diff --git a/app/Services/DateRangeService.php b/app/Services/DateRangeService.php
index 80605f2..3263803 100644
--- a/app/Services/DateRangeService.php
+++ b/app/Services/DateRangeService.php
@@ -56,10 +56,10 @@ class DateRangeService
// По умолчанию: с начала года до сегодня
$startDate = Carbon::now('Asia/Yakutsk')
->startOfYear() // 1 января текущего года
- ->setTime(6, 0);
+ ->setTime(7, 0);
$endDate = Carbon::now('Asia/Yakutsk')
- ->setTime(6, 0);
+ ->setTime(7, 0);
return [
$startDate->format('Y-m-d H:i:s'),
@@ -83,7 +83,7 @@ class DateRangeService
$startDate = $this->parseDate($startAt);
$endDate = $this->parseDate($endAt);
- return $startDate->diffInDays($endDate) === 1.0;
+ return $startDate->isSameDay($endDate) || $startDate->diffInDays($endDate) === 1.0;
}
private function getCustomDateRange($startAt, $endAt, $user): array
@@ -92,11 +92,11 @@ class DateRangeService
$endDate = $this->parseDate($endAt);
if ($startDate->isSameDay($endDate)) {
- $startDate = $startDate->subDay()->setTime(6, 0);
- $endDate = $endDate->setTime(6, 0);
+ $startDate = $startDate->subDay()->setTime(7, 0);
+ $endDate = $endDate->setTime(7, 0);
} else {
- $startDate = $startDate->setTime(6, 0);
- $endDate = $endDate->setTime(6, 0);
+ $startDate = $startDate->setTime(7, 0);
+ $endDate = $endDate->setTime(7, 0);
}
return [
@@ -109,10 +109,10 @@ class DateRangeService
{
$startDate = Carbon::now('Asia/Yakutsk')
->subDay()
- ->setTime(6, 0);
+ ->setTime(7, 0);
$endDate = Carbon::now('Asia/Yakutsk')
- ->setTime(6, 0);
+ ->setTime(7, 0);
return [
$startDate->format('Y-m-d H:i:s'),
@@ -123,8 +123,12 @@ class DateRangeService
public function parseDate($dateInput): Carbon
{
if (is_numeric($dateInput)) {
- return Carbon::createFromTimestampMs($dateInput)
- ->setTimezone('Asia/Yakutsk');
+ $timestamp = (string) $dateInput;
+ $isMilliseconds = strlen(ltrim($timestamp, '-')) > 10;
+
+ return $isMilliseconds
+ ? Carbon::createFromTimestampMs((int) $dateInput)->setTimezone('Asia/Yakutsk')
+ : Carbon::createFromTimestamp((int) $dateInput)->setTimezone('Asia/Yakutsk');
}
return Carbon::parse($dateInput, 'Asia/Yakutsk');
@@ -139,6 +143,10 @@ class DateRangeService
return $date;
}
+ if (is_numeric($date)) {
+ return $this->parseDate($date);
+ }
+
if (is_string($date)) {
return Carbon::parse($date, 'Asia/Yakutsk');
}
@@ -157,9 +165,9 @@ class DateRangeService
public function createDateRangeForDate(Carbon $date, User $user): DateRange
{
// Для автоматического заполнения используем логику как для врача
- // (вчера 06:00 - сегодня 06:00)
- $startDate = $date->copy()->subDay()->setTime(6, 0);
- $endDate = $date->copy()->setTime(6, 0);
+ // (вчера 07:00 - сегодня 07:00)
+ $startDate = $date->copy()->subDay()->setTime(7, 0);
+ $endDate = $date->copy()->setTime(7, 0);
return new DateRange(
startDate: $startDate,
diff --git a/app/Services/PatientMigrationService.php b/app/Services/PatientMigrationService.php
index a0c808f..0c00fed 100644
--- a/app/Services/PatientMigrationService.php
+++ b/app/Services/PatientMigrationService.php
@@ -2,7 +2,177 @@
namespace App\Services;
+use App\Data\UnifiedPatientData;
+use App\Models\MisMigrationPatient;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
+
class PatientMigrationService
{
+ private const ACTIVE_DATE_OUT = '2222-01-01 00:00:00';
+ const DEAD_VISIT_RESULT_IDS = [5, 6, 15, 16];
+ const TRANSFER_VISIT_RESULT_IDS = [2, 3, 4, 12, 13, 14, 35];
+ const OUTCOME_VISIT_RESULT_IDS = [1, 11];
+ public function __construct(
+ protected StationarBranchService $branchService
+ ) { }
+
+
+ /**
+ * Получить всех пациентов в отделении за период
+ */
+ public function getMigrationsInBranch(int $branchId, string $startAt, string $endAt): Collection
+ {
+ return $this->getMigrationsCached(
+ branchId: $branchId,
+ startAt: $startAt,
+ endAt: $endAt,
+ cacheTag: 'migrations_in_branch',
+ dateFilter: fn($q) => $q->where('DateOut', '>', $startAt)->where('DateOut', '<=', $endAt),
+ );
+ }
+
+ /**
+ * Получить текущих пациентов в отделении (активные)
+ */
+ public function getMigrationsInBranchCurrent(int $branchId, string $startAt, string $endAt): Collection
+ {
+ return $this->getMigrationsCached(
+ branchId: $branchId,
+ startAt: $startAt,
+ endAt: $endAt,
+ cacheTag: 'migrations_in_branch_current',
+ dateFilter: fn($q) => $q->where('DateOut', self::ACTIVE_DATE_OUT),
+ );
+ }
+
+ /**
+ * Получить текущих пациентов в отделении (активные)
+ */
+ public function getMigrationsInBranchDead(int $branchId, string $startAt, string $endAt): Collection
+ {
+ return $this->getMigrationsCached(
+ branchId: $branchId,
+ startAt: $startAt,
+ endAt: $endAt,
+ cacheTag: 'migrations_in_branch_dead',
+ dateFilter: fn($q) => $q->where('DateOut', '>', $startAt)->where('DateOut', '<=', $endAt),
+ additionalFilters: [
+ [
+ 'key' => 'visit_result:dead',
+ 'apply' => fn($q) => $q->whereIn('rf_kl_VisitResultID', self::DEAD_VISIT_RESULT_IDS)
+ ]
+ ]
+ );
+ }
+
+ public function getMigrationsInBranchTransfer(int $branchId, string $startAt, string $endAt): Collection
+ {
+ return $this->getMigrationsCached(
+ branchId: $branchId,
+ startAt: $startAt,
+ endAt: $endAt,
+ cacheTag: 'migrations_in_branch_transfer',
+ dateFilter: fn($q) => $q->where('DateOut', '>', $startAt)->where('DateOut', '<=', $endAt),
+ additionalFilters: [
+ [
+ 'key' => 'visit_result:transfer',
+ 'apply' => fn($q) => $q->whereIn('rf_kl_VisitResultID', self::TRANSFER_VISIT_RESULT_IDS)
+ ]
+ ]
+ );
+ }
+
+ public function getMigrationsInBranchOutcome(int $branchId, string $startAt, string $endAt): Collection
+ {
+ return $this->getMigrationsCached(
+ branchId: $branchId,
+ startAt: $startAt,
+ endAt: $endAt,
+ cacheTag: 'migrations_in_branch_outcome',
+ dateFilter: fn($q) => $q->where('DateOut', '>', $startAt)->where('DateOut', '<=', $endAt),
+ additionalFilters: [
+ [
+ 'key' => 'visit_result:outcome',
+ 'apply' => fn($q) => $q->whereIn('rf_kl_VisitResultID', self::OUTCOME_VISIT_RESULT_IDS)
+ ]
+ ]
+ );
+ }
+
+ /**
+ * Базовый метод с кешированием и общей логикой
+ */
+ protected function getMigrationsCached(
+ int $branchId,
+ string $startAt,
+ string $endAt,
+ string $cacheTag,
+ \Closure $dateFilter,
+ array $additionalFilters = []
+ ): Collection {
+ // Исключения
+ if (in_array($branchId, [0], true)) {
+ return collect();
+ }
+
+ // Нормализованный ключ кеша
+ $filterKeys = array_column($additionalFilters, 'key');
+ $filterHash = substr(md5(implode('|', $filterKeys)), 0, 6);
+ $dateHash = substr(md5($startAt . '_' . $endAt), 0, 8);
+ $cacheKey = "{$cacheTag}_{$branchId}_{$dateHash}_f{$filterHash}";
+
+ return Cache::tags([$cacheTag, "migrations_branch_{$branchId}"])
+ ->remember($cacheKey, now()->addHours(6), function () use ($branchId, $startAt, $endAt, $dateFilter, $additionalFilters) {
+ return $this->buildBaseQuery($branchId, $startAt, $endAt)
+ ->tap($dateFilter)
+ ->tap(function ($q) use ($additionalFilters) {
+ foreach ($additionalFilters as $filter) {
+ if (isset($filter['apply']) && $filter['apply'] instanceof \Closure) {
+ $filter['apply']($q);
+ }
+ }
+ })
+ ->get()
+ ->each(function ($result) {
+ return UnifiedPatientData::fromMisMigrationPatient($result);
+ });
+ });
+ }
+
+ /**
+ * Строит базовый запрос с общими условиями и связями
+ */
+ protected function buildBaseQuery(int $branchId, string $startAt, string $endAt): Builder
+ {
+ return MisMigrationPatient::query()
+ ->select([
+ 'MigrationPatientID', 'DateIngoing', 'DateOut', 'BedDays', 'rf_MedicalHistoryID',
+ 'rf_StationarBranchID', 'rf_DiagnosID', 'rf_kl_ProfitTypeID', 'rf_kl_StatCureResultID',
+ 'rf_kl_VisitResultID', 'rf_BedProfileID', 'rf_kl_BedProfileID'
+ ])
+ ->where('rf_StationarBranchID', $branchId)
+ // Жадная загрузка с ограничениями
+ ->with([
+ 'medicalHistory' => fn($q) => $q->select('MedicalHistoryID', 'FAMILY', 'Name', 'OT', 'BD'),
+ 'medicalHistory.operationPurpose' => fn($q) => $q
+ ->where('rf_StationarBranchID', $branchId)
+ ->select('OperationPurposeID', 'rf_MedicalHistoryID', 'Date', 'rf_OperationStatusID',
+ 'CancelDate', 'rf_StationarBranchID', 'PhysicalExam', 'Description', 'Indications', 'EpicrisDate',
+ 'rf_SurgicalOperationID'
+ ),
+ 'medicalHistory.operationPurpose.surgicalOperation' => fn($q) => $q
+ ->select('SurgicalOperationID', 'DataEnd', 'Date', 'Num', 'rf_kl_ServiceMedicalID',
+ 'rf_MedicalHistoryID', 'rf_StationarBranchID', 'Description', 'rf_OperationResultID'
+ ),
+ 'diagnosis' => fn($q) => $q
+ ->where('rf_DiagnosTypeID', 3)
+ ->select('DiagnosID', 'Date', 'rf_DiagnosTypeID', 'rf_MedicalHistoryID', 'rf_MKBID',
+ 'rf_MigrationPatientID', 'Description'
+ ),
+ 'diagnosis.mkb' => fn($q) => $q->select(['MKBID', 'DS', 'NAME']),
+ ]);
+ }
}
diff --git a/app/Services/PatientService.php b/app/Services/PatientService.php
index 59fe762..617393b 100644
--- a/app/Services/PatientService.php
+++ b/app/Services/PatientService.php
@@ -2,12 +2,13 @@
namespace App\Services;
-use App\Models\LifeMisMigrationPatient;
use App\Models\MisMedicalHistory;
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
{
@@ -34,10 +35,7 @@ class PatientService
// Если нужно добавить уже находящихся в отделении
if ($includeCurrent) {
if ($fillableAuto) {
- $currentIds = LifeMisMigrationPatient::currentlyInTreatment($branchId, $dateRange)
- ->distinct()
- ->pluck('rf_MedicalHistoryID')
- ->toArray();
+ $currentIds = $this->getHistoricalCurrentMedicalHistoryIds($branchId, $dateRange);
} else {
$currentIds = MisMigrationPatient::currentlyInTreatment($branchId)
->pluck('rf_MedicalHistoryID')
@@ -57,19 +55,75 @@ class PatientService
// Получаем истории
$query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
+ ->select([
+ 'MedicalHistoryID',
+ 'FAMILY',
+ 'Name',
+ 'OT',
+ 'BD',
+ 'DateRecipient',
+ 'DateExtract',
+ 'rf_EmerSignID',
+ 'rf_kl_VisitResultID',
+ ])
->with([
- 'surgicalOperations' => function ($q) use ($dateRange) {
-// $q->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
- $q->where('Date', '>=', $dateRange->startSql())
- ->where('Date', '<=', $dateRange->endSql());
+ '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)
- ->take(1) // берем только одну последнюю
- ->with(['mainDiagnosis' => function ($q) {
- $q->with('mkb');
- }]);
- }
+ ->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');
@@ -88,7 +142,7 @@ class PatientService
return $query->pluck('MedicalHistoryID');
}
- return $query->get()->map(function ($patient) use ($recipientIds, $branchId) {
+ return $query->get()->map(function ($patient) use ($recipientIds) {
// Добавляем флаг "поступил сегодня"
$patient->is_recipient_today = in_array($patient->MedicalHistoryID, $recipientIds);
return $patient;
@@ -103,17 +157,22 @@ class PatientService
int $branchId,
DateRange $dateRange,
bool $countOnly = false,
- bool $onlyIds = false
+ bool $onlyIds = false,
+ bool $fillableAuto = false
) {
// Поступившие сегодня
- $recipientIds = $this->buildRecipientQuery(null, $isHeadOrAdmin, $branchId, $dateRange)
+ $recipientIds = $this->buildRecipientQuery(null, $isHeadOrAdmin, $branchId, $dateRange, $fillableAuto)
->pluck('rf_MedicalHistoryID')
->toArray();
// Уже находящиеся на лечении
- $currentIds = MisMigrationPatient::currentlyInTreatment($branchId)
- ->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));
@@ -132,11 +191,76 @@ class PatientService
}
return MisMedicalHistory::whereIn('MedicalHistoryID', $allIds)
- ->with(['surgicalOperations' => function ($q) use ($dateRange) {
-// $q->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
- $q->where('Date', '>=', $dateRange->startSql())
- ->where('Date', '<=', $dateRange->endSql());
- }])
+ ->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) {
@@ -167,7 +291,6 @@ class PatientService
// Загрузка отношений, необходимых для FormattedPatientResource
$query->with([
'outcomeMigration.mainDiagnosis.mkb', // mkb через диагноз
- 'surgicalOperations.serviceMedical', // операции с услугами
]);
$patients = $query->get();
}
@@ -190,34 +313,156 @@ class PatientService
string $outcomeType = 'all',
bool $onlyIds = false
) {
- $methodMap = [
- 'discharged' => 'outcomeDischarged',
- 'transferred' => 'outcomeTransferred',
- 'without-transferred' => 'outcomeWithoutTransferred',
- 'deceased' => 'deceasedOutcome',
- 'all' => 'outcomePatients',
- ];
+ $query = MisMedicalHistory::query()
+ ->where('MedicalHistoryID', '<>', 0)
+ ->whereHas('migrations', function ($migrationQuery) use ($branchId, $outcomeType) {
+ $migrationQuery->where('rf_StationarBranchID', $branchId);
- $method = $methodMap[$outcomeType] ?? 'outcomePatients';
+ 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);
+ }
+ });
- $medicalHistoryIds = MisMigrationPatient::{$method}($branchId, $dateRange)
- ->pluck('rf_MedicalHistoryID')
- ->unique()
+ 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));
+ }
+
+ /**
+ * Получить пациентов, находящихся в реанимации на конец периода
+ */
+ public function getReanimationPatients(
+ int $branchId,
+ DateRange $dateRange,
+ bool $onlyIds = false
+ ) {
+ $medicalHistoryIds = MisReanimation::query()
+ ->join('stt_migrationpatient as mp', 'mp.MigrationPatientID', '=', 'stt_reanimation.rf_MigrationPatientID')
+ ->where('stt_reanimation.rf_StationarBranchID', $branchId)
+ ->where('mp.rf_StationarBranchID', $branchId)
+ ->where('mp.rf_MedicalHistoryID', '<>', 0)
+ ->where('stt_reanimation.DateIn', '<=', $dateRange->endSql())
+ ->where(function ($query) use ($dateRange) {
+ $query->where('stt_reanimation.DateOut', '>=', $dateRange->endSql())
+ ->orWhereNull('stt_reanimation.DateOut')
+ ->orWhereDate('stt_reanimation.DateOut', '1900-01-01')
+ ->orWhereDate('stt_reanimation.DateOut', '2222-01-01');
+ })
+ ->distinct()
+ ->pluck('mp.rf_MedicalHistoryID')
->toArray();
if (empty($medicalHistoryIds)) {
return collect();
}
- if ($onlyIds) return collect($medicalHistoryIds);
+ if ($onlyIds) {
+ return collect($medicalHistoryIds);
+ }
return MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
- ->with(['surgicalOperations'])
+ ->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',
+ ]);
+ }]);
+ }]);
+ },
+ ])
->orderBy('DateRecipient', 'DESC')
- ->get()
- ->map(function ($patient) {
- return $this->addOutcomeInfo($patient);
- });
+ ->get();
}
/**
@@ -230,6 +475,7 @@ class PatientService
bool $countOnly = false
) {
$query = MisSurgicalOperation::where('rf_StationarBranchID', $branchId)
+ ->completed()
->where('Date', '>=', $dateRange->startSql())
->where('Date', '<=', $dateRange->endSql());
// ->whereBetween('Date', [$dateRange->startSql(), $dateRange->endSql()]);
@@ -325,36 +571,64 @@ class PatientService
DateRange $dateRange,
bool $fillableAuto = false
) {
- // Разная логика для заведующего и врача
- if ($isHeadOrAdmin) {
- // Заведующий: все поступившие за период
- if ($fillableAuto) {
- $query = LifeMisMigrationPatient::whereInDepartment($branchId)
- ->where('DateIngoing', '>=', $dateRange->startSql())
- ->where('DateIngoing', '<=', $dateRange->endSql());
-// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
- } else {
- $query = MisMigrationPatient::whereInDepartment($branchId)
- ->where('DateIngoing', '>=', $dateRange->startSql())
- ->where('DateIngoing', '<=', $dateRange->endSql());
-// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
- }
- } else {
- // Врач: только поступившие за сутки
- if ($fillableAuto) {
- $query = LifeMisMigrationPatient::whereInDepartment($branchId)
- ->where('DateIngoing', '>=', $dateRange->startSql())
- ->where('DateIngoing', '<=', $dateRange->endSql());
-// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
- } else {
- $query = MisMigrationPatient::whereInDepartment($branchId)
- ->where('DateIngoing', '>=', $dateRange->startSql())
- ->where('DateIngoing', '<=', $dateRange->endSql());
-// ->whereBetween('DateIngoing', [$dateRange->startSql(), $dateRange->endSql()]);
- };
+ $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();
}
- return $query;
+ $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();
}
/**
@@ -386,26 +660,15 @@ class PatientService
int $branchId,
DateRange $dateRange
): int {
- // Поступившие сегодня указанного типа
- $recipientCount = $this->buildRecipientQuery($type, $isHeadOrAdmin, $branchId, $dateRange)
- ->count();
-
- // Если нужны плановые/экстренные среди уже лечащихся
- $currentCount = 0;
- if ($type === 'plan' || $type === 'emergency') {
- $currentIds = MisMigrationPatient::currentlyInTreatment($branchId)
- ->pluck('rf_MedicalHistoryID')
- ->toArray();
-
- if (!empty($currentIds)) {
- $currentCount = MisMedicalHistory::whereIn('MedicalHistoryID', $currentIds)
- ->when($type === 'plan', fn($q) => $q->plan())
- ->when($type === 'emergency', fn($q) => $q->emergency())
- ->count();
- }
- }
-
- return $currentCount;
+ return $this->getPlanOrEmergencyPatients(
+ $type,
+ $isHeadOrAdmin,
+ $branchId,
+ $dateRange,
+ true,
+ false,
+ true
+ );
}
/**
diff --git a/app/Services/ReportPageService.php b/app/Services/ReportPageService.php
new file mode 100644
index 0000000..72ef7f7
--- /dev/null
+++ b/app/Services/ReportPageService.php
@@ -0,0 +1,44 @@
+reportService->getReportStatistics($department, $user, $dateRange);
+ $reportInfo = $this->reportService->getCurrentReportInfo($department, $user, $dateRange);
+ $recipientPlanOfYear = $this->reportService->getRecipientPlanOfYear($department, $dateRange);
+
+ return [
+ 'department' => [
+ 'department_name' => $department->name_full,
+ 'department_id' => $department->department_id,
+ 'beds' => $department->beds,
+ 'percentLoadedBeds' => ($statistics['beds'] ?? 0) > 0
+ ? round((($statistics['currentCount'] ?? 0) * 100) / $statistics['beds'])
+ : 0,
+ 'recipientPlanOfYear' => $recipientPlanOfYear['plan'],
+ 'progressPlanOfYear' => $recipientPlanOfYear['progress'],
+ ...$statistics,
+ ],
+ 'dates' => [
+ 'startAt' => $dateRange->startTimestamp(),
+ 'endAt' => $dateRange->endTimestamp(),
+ ],
+ 'report' => $reportInfo,
+ 'metrikaItems' => MetrikaItem::whereIn('metrika_item_id', [3, 7, 8, 17])->get(),
+ 'patients' => [],
+ 'userId' => $reportInfo['userId'],
+ 'userName' => $reportInfo['userName'],
+ ];
+ }
+}
diff --git a/app/Services/ReportService.php b/app/Services/ReportService.php
index 747690a..e6e6489 100644
--- a/app/Services/ReportService.php
+++ b/app/Services/ReportService.php
@@ -3,6 +3,9 @@
namespace App\Services;
use App\Models\Department;
+use App\Models\DepartmentPatient;
+use App\Models\DepartmentPatientOperation;
+use App\Models\MisServiceMedical;
use App\Models\MedicalHistorySnapshot;
use App\Models\MetrikaResult;
use App\Models\MisLpuDoctor;
@@ -22,6 +25,7 @@ class ReportService
{
public function __construct(
protected DateRangeService $dateRangeService,
+ protected UnifiedPatientService $unifiedPatientService,
protected PatientService $patientQueryService,
protected SnapshotService $snapshotService,
protected StatisticsService $statisticsService
@@ -42,7 +46,79 @@ class ReportService
return $this->getStatisticsFromSnapshots($department, $dateRange, $branchId);
}
- return $this->getStatisticsFromReplica($user, $dateRange, $branchId);
+ return $this->getStatisticsFromReplica($department, $user, $dateRange, $branchId);
+ }
+
+ public function shouldUseSnapshotsForPage(Department $department, User $user, DateRange $dateRange): bool
+ {
+ return $this->shouldUseSnapshots($department, $user, $dateRange);
+ }
+
+ public function getFastReplicaStatisticsFromPatientsPayload(
+ Department $department,
+ User $user,
+ DateRange $dateRange,
+ array $patientsPayload
+ ): array {
+ $branchId = $this->getBranchId($department->rf_mis_department_id);
+
+ $planCount = count($patientsPayload['mis-plan'] ?? []) + count($patientsPayload['special-plan'] ?? []);
+ $emergencyCount = count($patientsPayload['mis-emergency'] ?? []) + count($patientsPayload['special-emergency'] ?? []);
+ $dischargedCount = count($patientsPayload['mis-outcome-discharged'] ?? []) + count($patientsPayload['special-outcome-discharged'] ?? []);
+ $deadCount = count($patientsPayload['mis-outcome-deceased'] ?? []) + count($patientsPayload['special-outcome-deceased'] ?? []);
+ $outcomeCount = $dischargedCount + $deadCount;
+
+ $recipientPatients = $this->unifiedPatientService
+ ->getLivePatientsByStatus($department, $user, 'recipient', $dateRange, $branchId);
+ $recipientCount = $recipientPatients->count();
+ $recipientIds = $recipientPatients->pluck('id')->all();
+
+ $currentCount = $this->unifiedPatientService
+ ->getLivePatientCountByStatus($department, $user, 'current', $dateRange, $branchId);
+
+ $misSurgicalCount = [
+ $this->patientQueryService->getSurgicalPatients(
+ 'emergency',
+ $branchId,
+ $dateRange,
+ true
+ ),
+ $this->patientQueryService->getSurgicalPatients(
+ 'plan',
+ $branchId,
+ $dateRange,
+ true
+ )
+ ];
+ $manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange);
+ $surgicalCount = [
+ ($misSurgicalCount[0] ?? 0) + ($manualSurgicalCount[0] ?? 0),
+ ($misSurgicalCount[1] ?? 0) + ($manualSurgicalCount[1] ?? 0),
+ ];
+
+ $misBranch = MisStationarBranch::where('StationarBranchID', $branchId)->first();
+ $beds = Department::where('rf_mis_department_id', $misBranch->rf_DepartmentID)
+ ->first()->metrikaDefault->where('rf_metrika_item_id', 1)->first();
+
+ if ($outcomeCount == 0) {
+ $percentDead = 0;
+ } else {
+ $percentDead = ($deadCount / $outcomeCount) * 100;
+ $percentDead = round($percentDead, 2);
+ }
+
+ return [
+ 'recipientCount' => $recipientCount,
+ 'extractCount' => $outcomeCount,
+ 'currentCount' => $currentCount,
+ 'deadCount' => $deadCount,
+ 'surgicalCount' => $surgicalCount,
+ 'recipientIds' => $recipientIds,
+ 'planCount' => $planCount,
+ 'emergencyCount' => $emergencyCount,
+ 'percentDead' => $percentDead,
+ 'beds' => $beds->value
+ ];
}
/**
@@ -58,6 +134,7 @@ class ReportService
$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);
return $report;
});
@@ -78,6 +155,149 @@ class ReportService
return $report;
}
+ public function buildAutoFillReportPayload(User $user, Department $department, DateRange $dateRange): array
+ {
+ $branchId = $this->getBranchId($department->rf_mis_department_id);
+
+ $metrics = $this->buildAutoFillMetrics($department, $user, $branchId, $dateRange);
+
+ return [
+ 'departmentId' => $department->department_id,
+ 'userId' => $user->rf_lpudoctor_id ?? $user->id,
+ 'dates' => [
+ $dateRange->startTimestamp(),
+ $dateRange->endTimestamp(),
+ ],
+ 'sent_at' => $dateRange->endSql(),
+ 'created_at' => $dateRange->endSql(),
+ 'metrics' => [
+ 'metrika_item_4' => $metrics['plan'],
+ 'metrika_item_12' => $metrics['emergency'],
+ 'metrika_item_3' => $metrics['recipient'],
+ 'metrika_item_7' => $metrics['discharged'] + $metrics['deceased'],
+ 'metrika_item_8' => $metrics['current'],
+ 'metrika_item_9' => $metrics['deceased'],
+ 'metrika_item_10' => $metrics['emergency_surgery'],
+ 'metrika_item_11' => $metrics['plan_surgery'],
+ 'metrika_item_13' => $metrics['transferred'],
+ 'metrika_item_14' => 0,
+ 'metrika_item_15' => $metrics['discharged'],
+ ],
+ 'observationPatients' => [],
+ 'unwantedEvents' => [],
+ ];
+ }
+
+ private function buildAutoFillMetrics(Department $department, User $user, int $branchId, DateRange $dateRange): array
+ {
+ $manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange);
+ $recipientQuery = $this->buildRecipientMedicalHistoryQuery($branchId, $dateRange);
+ $dischargeCodes = [1, 11, 2, 12, 7, 18, 48];
+ $deceasedCodes = [5, 6, 15, 16];
+ $transferCodes = [4, 14];
+
+ $planRecipient = (clone $recipientQuery)
+ ->where('rf_EmerSignID', 1)
+ ->distinct()
+ ->count('MedicalHistoryID');
+
+ $emergencyRecipient = (clone $recipientQuery)
+ ->whereIn('rf_EmerSignID', [2, 4])
+ ->distinct()
+ ->count('MedicalHistoryID');
+
+ $recipientTotal = (clone $recipientQuery)
+ ->distinct()
+ ->count('MedicalHistoryID');
+
+ $discharged = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $dischargeCodes);
+ $deceased = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $deceasedCodes);
+ $transferred = $this->countOutcomeByVisitResultIds($branchId, $dateRange, $transferCodes);
+
+ return [
+ 'plan' => $planRecipient,
+ 'emergency' => $emergencyRecipient,
+ 'recipient' => $recipientTotal,
+ 'discharged' => $discharged,
+ 'transferred' => $transferred,
+ 'deceased' => $deceased,
+ 'current' => $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'current', $dateRange, $branchId, null, true),
+ 'plan_surgery' => $this->patientQueryService->getSurgicalPatients(
+ 'plan',
+ $branchId,
+ $dateRange,
+ true
+ ) + ($manualSurgicalCount[1] ?? 0),
+ 'emergency_surgery' => $this->patientQueryService->getSurgicalPatients(
+ 'emergency',
+ $branchId,
+ $dateRange,
+ true
+ ) + ($manualSurgicalCount[0] ?? 0),
+ ];
+ }
+
+ private function buildRecipientMedicalHistoryQuery(int $branchId, DateRange $dateRange)
+ {
+ $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();
+ }
+
+ return MisMedicalHistory::query()
+ ->where('MedicalHistoryID', '<>', 0)
+ ->whereExists(function ($query) use ($branchId, $startAt, $endAt) {
+ $query->select(DB::raw(1))
+ ->from('stt_migrationpatient as mp')
+ ->whereColumn('mp.rf_MedicalHistoryID', 'stt_medicalhistory.MedicalHistoryID')
+ ->where('mp.rf_StationarBranchID', $branchId)
+ ->where('mp.DateIngoing', '>', $startAt)
+ ->where('mp.DateIngoing', '<=', $endAt);
+ });
+ }
+
+ private function buildTreatedMedicalHistoryQuery(int $branchId, DateRange $dateRange)
+ {
+ $query = MisMedicalHistory::query()
+ ->where('MedicalHistoryID', '<>', 0)
+ ->whereExists(function ($query) use ($branchId) {
+ $query->select(DB::raw(1))
+ ->from('stt_migrationpatient as mp')
+ ->whereColumn('mp.rf_MedicalHistoryID', 'stt_medicalhistory.MedicalHistoryID')
+ ->where('mp.rf_StationarBranchID', $branchId);
+ });
+
+ if ($dateRange->isOneDay) {
+ return $query
+ ->where('DateExtract', '>', $dateRange->startSql())
+ ->where('DateExtract', '<=', $dateRange->endSql());
+ }
+
+ $startAt = $dateRange->startSql();
+ $endDate = $dateRange->end()->toDateString();
+
+ return $query
+ ->where('DateExtract', '>', $startAt)
+ ->whereDate('DateExtract', '<=', $endDate);
+ }
+
+ private function countOutcomeByVisitResultIds(int $branchId, DateRange $dateRange, array $visitResultIds): int
+ {
+ return $this->buildTreatedMedicalHistoryQuery($branchId, $dateRange)
+ ->whereExists(function ($query) use ($branchId, $visitResultIds) {
+ $query->select(DB::raw(1))
+ ->from('stt_migrationpatient as mp')
+ ->whereColumn('mp.rf_MedicalHistoryID', 'stt_medicalhistory.MedicalHistoryID')
+ ->where('mp.rf_StationarBranchID', $branchId)
+ ->whereIn('mp.rf_kl_VisitResultID', $visitResultIds);
+ })
+ ->distinct()
+ ->count('MedicalHistoryID');
+ }
+
/**
* Сохранить метрику койко-дня из снапшотов отчета
*/
@@ -300,15 +520,37 @@ class ReportService
bool $beforeCreate = false,
?bool $includeCurrentPatients = null
) {
+ [$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
$branchId = $this->getBranchId($department->rf_mis_department_id);
- $useSnapshots = $this->shouldUseSnapshots($department, $user, $dateRange, $beforeCreate);
-
- if ($useSnapshots) {
- return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId);
+ if ($baseStatus === 'reanimation') {
+ return $this->getPatientsFromReplica(
+ $department,
+ $user,
+ $status,
+ $dateRange,
+ $branchId,
+ $onlyIds,
+ $includeCurrentPatients
+ );
}
- return $this->getPatientsFromReplica($department, $user, $status, $dateRange, $branchId, $onlyIds, $includeCurrentPatients);
+ $useSnapshots = !$this->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange)
+ && $this->shouldUseSnapshots($department, $user, $dateRange, $beforeCreate);
+
+ if ($useSnapshots) {
+ return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId, $onlyIds);
+ }
+
+ return $this->getPatientsFromReplica(
+ $department,
+ $user,
+ $status,
+ $dateRange,
+ $branchId,
+ $onlyIds,
+ $includeCurrentPatients
+ );
}
/**
@@ -320,9 +562,15 @@ class ReportService
string $status,
DateRange $dateRange
): int {
+ [$baseStatus] = $this->parseScopedStatus($status);
$branchId = $this->getBranchId($department->rf_mis_department_id);
- $useSnapshots = $this->shouldUseSnapshots($department, $user, $dateRange);
+ if ($baseStatus === 'reanimation') {
+ return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId);
+ }
+
+ $useSnapshots = !$this->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange)
+ && $this->shouldUseSnapshots($department, $user, $dateRange);
if ($useSnapshots) {
return $this->getPatientsCountFromSnapshots($department, $status, $dateRange);
@@ -331,6 +579,62 @@ class ReportService
return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId);
}
+ public function getPatientsCountsMap(Department $department, User $user, DateRange $dateRange): array
+ {
+ $baseStatuses = [
+ 'plan',
+ 'emergency',
+ 'observation',
+ 'reanimation',
+ 'outcome-discharged',
+ 'outcome-deceased',
+ 'outcome-transferred',
+ ];
+
+ $counts = [
+ 'mis-plan' => 0,
+ 'mis-emergency' => 0,
+ 'mis-observation' => 0,
+ 'mis-reanimation' => 0,
+ 'mis-outcome' => 0,
+ 'mis-outcome-discharged' => 0,
+ 'mis-outcome-deceased' => 0,
+ 'mis-outcome-transferred' => 0,
+ 'special-plan' => 0,
+ 'special-emergency' => 0,
+ 'special-observation' => 0,
+ 'special-reanimation' => 0,
+ 'special-outcome' => 0,
+ 'special-outcome-discharged' => 0,
+ 'special-outcome-deceased' => 0,
+ 'special-outcome-transferred' => 0,
+ ];
+
+ 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-outcome'] = ($counts['mis-outcome-discharged'] ?? 0) + ($counts['mis-outcome-deceased'] ?? 0);
+ $counts['special-outcome'] = ($counts['special-outcome-discharged'] ?? 0) + ($counts['special-outcome-deceased'] ?? 0);
+
+ return $counts;
+ }
+
/**
* Получить ID отделения из стационарного отделения
*/
@@ -358,17 +662,42 @@ class ReportService
return !$dateRange->isEndDateToday() || $reportToday;
}
+ private function shouldUseReplicaForLiveStatus(User $user, string $status, DateRange $dateRange): bool
+ {
+ if ($user->isHeadOfDepartment() || $user->isAdmin()) {
+ return false;
+ }
+
+ return in_array($status, ['plan', 'emergency', 'recipient', 'current'], true)
+ && $dateRange->isOneDay
+ && $dateRange->isEndDateToday();
+ }
+
/**
* Создать или обновить отчет
*/
private function createOrUpdateReport(array $data, User $user): Report
{
+ $rangeStartAt = isset($data['dates'][0])
+ ? $this->dateRangeService->toSqlFormat($data['dates'][0])
+ : null;
+ $rangeEndAt = isset($data['dates'][1])
+ ? $this->dateRangeService->toSqlFormat($data['dates'][1])
+ : null;
+
+ $dateRange = $this->dateRangeService->createDateRangeForDate($this->dateRangeService->toCarbon($data['dates'][1]), $user);
+
+ $sentAt = $data['sent_at'] ?? $rangeEndAt ?? $this->dateRangeService->toSqlFormat(\Illuminate\Support\Carbon::now());
+ $createdAt = $data['created_at'] ?? $rangeEndAt ?? $this->dateRangeService->toSqlFormat(\Illuminate\Support\Carbon::now());
+
$reportData = [
'rf_department_id' => $data['departmentId'],
'rf_user_id' => $user->id,
'rf_lpudoctor_id' => $data['userId'],
- 'sent_at' => $data['sent_at'] ?? $this->dateRangeService->toSqlFormat(\Illuminate\Support\Carbon::now()),
- 'created_at' => $data['created_at'] ?? $this->dateRangeService->toSqlFormat(\Illuminate\Support\Carbon::now()),
+ 'sent_at' => $sentAt,
+ 'period_start' => $dateRange->startSql(),
+ 'period_end' => $dateRange->endSql(),
+ 'created_at' => $createdAt,
];
if (isset($data['reportId']) && $data['reportId']) {
@@ -433,7 +762,7 @@ class ReportService
{
if (empty($unwantedEvents)) {
$report->unwantedEvents()->delete();
- $this->saveMetrics($report, [16 => 0]);
+ $this->saveMetric($report, 16, 0);
return;
}
@@ -459,7 +788,7 @@ class ReportService
}
// Обновить метрику
- $this->saveMetrics($report, [16 => count($unwantedEvents)]);
+ $this->saveMetric($report, 16, count($unwantedEvents));
}
/**
@@ -475,14 +804,15 @@ class ReportService
->where('rf_report_id', $report->report_id)
->delete();
// Обновить метрику
- $this->saveMetrics($report, [14 => 0]);
+ $this->saveMetric($report, 14, 0);
return;
}
foreach ($observationPatients as $patient) {
ObservationPatient::updateOrCreate(
[
- 'rf_medicalhistory_id' => $patient['id'],
+ 'rf_medicalhistory_id' => $patient['medical_history_id'] ?? null,
+ 'rf_department_patient_id' => $patient['department_patient_id'] ?? null,
'rf_department_id' => $departmentId,
],
[
@@ -494,7 +824,82 @@ class ReportService
}
// Обновить метрику
- $this->saveMetrics($report, [14 => count($observationPatients)]);
+ $this->saveMetric($report, 14, count($observationPatients));
+ }
+
+ private function syncCalculatedMetrics(Report $report, User $user, array $data): void
+ {
+ if (!isset($data['dates'][0], $data['dates'][1])) {
+ return;
+ }
+
+ $department = Department::query()->where('department_id', $report->rf_department_id)->first();
+ if (!$department) {
+ return;
+ }
+
+ $dateRange = $this->dateRangeService->getNormalizedDateRange(
+ $user,
+ (string) $data['dates'][0],
+ (string) $data['dates'][1]
+ );
+
+ $branchId = $this->getBranchId($department->rf_mis_department_id);
+
+ $planCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['plan']);
+ $emergencyCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['emergency']);
+ $recipientCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['recipient']);
+ $dischargedCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['discharged']);
+ $transferredCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['transferred']);
+ $deceasedCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['deceased']);
+ $currentCount = $this->countUniqueSnapshotsForTypes($report->report_id, ['current']);
+ $outcomeCount = $dischargedCount + $deceasedCount;
+
+ $manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange);
+ $misEmergencySurgery = $branchId
+ ? $this->patientQueryService->getSurgicalPatients('emergency', $branchId, $dateRange, true)
+ : 0;
+ $misPlanSurgery = $branchId
+ ? $this->patientQueryService->getSurgicalPatients('plan', $branchId, $dateRange, true)
+ : 0;
+
+ $observationCount = ObservationPatient::query()
+ ->where('rf_department_id', $department->department_id)
+ ->where('rf_report_id', $report->report_id)
+ ->count();
+
+ $unwantedEventsCount = UnwantedEvent::query()
+ ->where('rf_report_id', $report->report_id)
+ ->count();
+
+ $this->saveMetric($report, 3, $recipientCount);
+ $this->saveMetric($report, 4, $planCount);
+ $this->saveMetric($report, 7, $outcomeCount);
+ $this->saveMetric($report, 8, $currentCount);
+ $this->saveMetric($report, 9, $deceasedCount);
+ $this->saveMetric($report, 10, $misEmergencySurgery + ($manualSurgicalCount[0] ?? 0));
+ $this->saveMetric($report, 11, $misPlanSurgery + ($manualSurgicalCount[1] ?? 0));
+ $this->saveMetric($report, 12, $emergencyCount);
+ $this->saveMetric($report, 13, $transferredCount);
+ $this->saveMetric($report, 14, $observationCount);
+ $this->saveMetric($report, 15, $dischargedCount);
+ $this->saveMetric($report, 16, $unwantedEventsCount);
+ }
+
+ private function countUniqueSnapshotsForTypes(int $reportId, array $patientTypes): int
+ {
+ return MedicalHistorySnapshot::query()
+ ->where('rf_report_id', $reportId)
+ ->whereIn('patient_type', $patientTypes)
+ ->get(['medical_history_snapshot_id', 'patient_uid', 'rf_medicalhistory_id'])
+ ->map(function (MedicalHistorySnapshot $snapshot) {
+ return $snapshot->patient_uid
+ ?: ($snapshot->rf_medicalhistory_id
+ ? "mis:{$snapshot->rf_medicalhistory_id}"
+ : "snapshot:{$snapshot->medical_history_snapshot_id}");
+ })
+ ->unique()
+ ->count();
}
/**
@@ -564,9 +969,159 @@ class ReportService
/**
* Удалить пациента из наблюдения
*/
- public function removeObservationPatient(int $medicalHistoryId): void
+ public function removeObservationPatient(string $patientId): void
{
- ObservationPatient::where('rf_medicalhistory_id', $medicalHistoryId)->delete();
+ [$sourceType, $id] = explode(':', $patientId) + [null, null];
+
+ if ($sourceType === 'manual') {
+ ObservationPatient::where('rf_department_patient_id', $id)->delete();
+ return;
+ }
+
+ ObservationPatient::where('rf_medicalhistory_id', $id)->delete();
+ }
+
+ public function createManualPatient(Department $department, User $user, array $data)
+ {
+ return $this->unifiedPatientService->createManualPatient($department, $user, $data);
+ }
+
+ public function setManualPatientOutcome(int $departmentPatientId, array $data)
+ {
+ $patient = \App\Models\DepartmentPatient::where('department_patient_id', $departmentPatientId)->firstOrFail();
+
+ return $this->unifiedPatientService->recordManualOutcome($patient, $data);
+ }
+
+ public function updateManualPatient(User $user, int $departmentPatientId, array $data)
+ {
+ $patient = $this->resolveManageableManualPatient($user, $departmentPatientId);
+
+ $updatedPatient = $this->unifiedPatientService->updateManualPatient($patient, $data);
+ $this->syncManualPatientSnapshots($updatedPatient, $user, $data);
+
+ return $updatedPatient;
+ }
+
+ public function linkManualPatientToMis(int $departmentPatientId, int $medicalHistoryId)
+ {
+ $patient = \App\Models\DepartmentPatient::where('department_patient_id', $departmentPatientId)->firstOrFail();
+
+ return $this->unifiedPatientService->linkManualPatientToMis($patient, $medicalHistoryId);
+ }
+
+ public function getManualPatientOperations(User $user, int $departmentPatientId)
+ {
+ $patient = $this->resolveManageableManualPatient($user, $departmentPatientId);
+
+ return $patient->operations()
+ ->with('serviceMedical')
+ ->orderByDesc('started_at')
+ ->get();
+ }
+
+ public function createManualPatientOperation(User $user, int $departmentPatientId, array $data): DepartmentPatientOperation
+ {
+ $patient = $this->resolveManageableManualPatient($user, $departmentPatientId);
+ $service = MisServiceMedical::query()
+ ->where('ServiceMedicalID', $data['service_id'])
+ ->firstOrFail();
+
+ return $patient->operations()->create([
+ 'rf_kl_service_medical_id' => $service->ServiceMedicalID,
+ 'service_code' => $service->ServiceMedicalCode,
+ 'service_name' => $service->ServiceMedicalName,
+ 'urgency' => $data['urgency'],
+ 'started_at' => $data['started_at'],
+ 'ended_at' => $data['ended_at'],
+ 'created_by' => $user->id,
+ ])->load('serviceMedical');
+ }
+
+ public function updateManualPatientOperation(User $user, int $departmentPatientId, int $operationId, array $data): DepartmentPatientOperation
+ {
+ $patient = $this->resolveManageableManualPatient($user, $departmentPatientId);
+ $service = MisServiceMedical::query()
+ ->where('ServiceMedicalID', $data['service_id'])
+ ->firstOrFail();
+
+ $operation = $patient->operations()
+ ->where('department_patient_operation_id', $operationId)
+ ->firstOrFail();
+
+ $operation->update([
+ 'rf_kl_service_medical_id' => $service->ServiceMedicalID,
+ 'service_code' => $service->ServiceMedicalCode,
+ 'service_name' => $service->ServiceMedicalName,
+ 'urgency' => $data['urgency'],
+ 'started_at' => $data['started_at'],
+ 'ended_at' => $data['ended_at'],
+ ]);
+
+ return $operation->fresh()->load('serviceMedical');
+ }
+
+ public function deleteManualPatientOperation(User $user, int $departmentPatientId, int $operationId): void
+ {
+ $patient = $this->resolveManageableManualPatient($user, $departmentPatientId);
+
+ $patient->operations()
+ ->where('department_patient_operation_id', $operationId)
+ ->firstOrFail()
+ ->delete();
+ }
+
+ public function searchMisPatientsForDepartment(Department $department, string $query)
+ {
+ return $this->unifiedPatientService->searchMisPatients($department, $query);
+ }
+
+ private function resolveManageableManualPatient(User $user, int $departmentPatientId): DepartmentPatient
+ {
+ $query = DepartmentPatient::query()
+ ->where('department_patient_id', $departmentPatientId)
+ ->whereIn('source_type', ['manual', 'special']);
+
+ if (!$user->isAdmin() && !$user->isHeadOfDepartment()) {
+ $query->where('rf_department_id', $user->department->department_id);
+ }
+
+ return $query->firstOrFail();
+ }
+
+ 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();
+
+ if (empty($reportIds)) {
+ return;
+ }
+
+ MedicalHistorySnapshot::query()
+ ->whereIn('rf_report_id', $reportIds)
+ ->where('rf_department_patient_id', $patient->department_patient_id)
+ ->update([
+ 'patient_kind' => $patient->patient_kind,
+ 'full_name' => $patient->full_name,
+ 'birth_date' => $patient->birth_date,
+ 'diagnosis_code' => $patient->diagnosis_code,
+ 'diagnosis_name' => $patient->diagnosis_name,
+ 'admitted_at' => $patient->admitted_at,
+ 'updated_at' => now(),
+ ]);
}
/**
@@ -582,6 +1137,7 @@ class ReportService
$reportIds = $reports->pluck('report_id')->toArray();
$lastReport = array_first($reportIds);
+ $recipientReportIds = $this->getSnapshotRecipientReportIds($reportIds);
// Получаем статистику из снапшотов
$snapshotStats = [
@@ -598,11 +1154,10 @@ class ReportService
];
// Получаем ID поступивших пациентов
- $recipientIds = MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds)
- ->where('patient_type', 'recipient')
- ->pluck('rf_medicalhistory_id')
- ->unique()
- ->toArray();
+ $recipientIds = $this->snapshotService
+ ->getPatientsFromSnapshots('recipient', $recipientReportIds)
+ ->pluck('id')
+ ->all();
// Получаем количество операций из метрик
$surgicalCount = [
@@ -633,61 +1188,17 @@ class ReportService
/**
* Получить статистику из реплики БД
*/
- private function getStatisticsFromReplica(User $user, DateRange $dateRange, int $branchId): array
+ private function getStatisticsFromReplica(Department $department, User $user, DateRange $dateRange, int $branchId): array
{
- $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
-
- // Плановые: поступившие сегодня + уже лечащиеся
- $planCount = $this->patientQueryService->getPatientsCountWithCurrent(
- 'plan',
- $isHeadOrAdmin,
- $branchId,
- $dateRange
- );
-
- // Экстренные: поступившие сегодня + уже лечащиеся
- $emergencyCount = $this->patientQueryService->getPatientsCountWithCurrent(
- 'emergency',
- $isHeadOrAdmin,
- $branchId,
- $dateRange
- );
-
- // Все пациенты в отделении: поступившие + лечащиеся
- $currentCount = $this->patientQueryService->getAllPatientsInDepartment(
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- true
- );
-
- // Поступившие сегодня (только новые поступления)
- $recipientCount = $this->patientQueryService->getPlanOrEmergencyPatients(
- null, // все типы
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- true,
- false,
- false // не включаем уже лечащихся
- );
-
- // Выбывшие за период
- $outcomeCount = $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'without-transferred'
- )->count();
-
- // Умершие за период
- $deadCount = $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'deceased'
- )->count();
+ $planCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'plan', $dateRange, $branchId, true);
+ $emergencyCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'emergency', $dateRange, $branchId, true);
+ $currentCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'current', $dateRange, $branchId);
+ $recipientCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'recipient', $dateRange, $branchId);
+ $outcomeCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'outcome', $dateRange, $branchId);
+ $deadCount = $this->unifiedPatientService->getLivePatientCountByStatus($department, $user, 'outcome-deceased', $dateRange, $branchId);
// Операции
- $surgicalCount = [
+ $misSurgicalCount = [
$this->patientQueryService->getSurgicalPatients(
'emergency',
$branchId,
@@ -701,17 +1212,17 @@ class ReportService
true
)
];
+ $manualSurgicalCount = $this->getManualSurgicalCounts($department, $dateRange);
+ $surgicalCount = [
+ ($misSurgicalCount[0] ?? 0) + ($manualSurgicalCount[0] ?? 0),
+ ($misSurgicalCount[1] ?? 0) + ($manualSurgicalCount[1] ?? 0),
+ ];
// ID поступивших сегодня (для отметки в таблице)
- $recipientIds = $this->patientQueryService->getPlanOrEmergencyPatients(
- null,
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- false,
- true,
- false // только поступившие сегодня
- );
+ $recipientIds = $this->unifiedPatientService
+ ->getLivePatientsByStatus($department, $user, 'recipient', $dateRange, $branchId)
+ ->pluck('id')
+ ->all();
$misBranch = MisStationarBranch::where('StationarBranchID', $branchId)->first();
$beds = Department::where('rf_mis_department_id', $misBranch->rf_DepartmentID)
@@ -738,6 +1249,38 @@ class ReportService
];
}
+ private function getManualSurgicalCounts(Department $department, DateRange $dateRange): array
+ {
+ $baseQuery = DepartmentPatientOperation::query()
+ ->whereBetween('started_at', [$dateRange->startSql(), $dateRange->endSql()])
+ ->whereHas('patient', function ($query) use ($department) {
+ $query->where('rf_department_id', $department->department_id)
+ ->whereIn('source_type', ['manual', 'special']);
+ });
+
+ $emergencyCount = (clone $baseQuery)
+ ->where(function ($query) {
+ $query->where('urgency', 'emergency')
+ ->orWhere(function ($fallback) {
+ $fallback->whereNull('urgency')
+ ->whereHas('patient', fn ($patientQuery) => $patientQuery->where('patient_kind', 'emergency'));
+ });
+ })
+ ->count();
+
+ $planCount = (clone $baseQuery)
+ ->where(function ($query) {
+ $query->where('urgency', 'plan')
+ ->orWhere(function ($fallback) {
+ $fallback->whereNull('urgency')
+ ->whereHas('patient', fn ($patientQuery) => $patientQuery->where('patient_kind', 'plan'));
+ });
+ })
+ ->count();
+
+ return [$emergencyCount, $planCount];
+ }
+
/**
* Получить пациентов из снапшотов
*/
@@ -748,29 +1291,52 @@ class ReportService
int $branchId,
bool $onlyIds = false
) {
+ [$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
$reports = $this->getReportsForDateRange(
$department->department_id,
$dateRange
);
$reportIds = $reports->pluck('report_id')->toArray();
+ $recipientReportIds = $this->getSnapshotRecipientReportIds($reportIds);
$patientTypeMap = [
'plan' => 'plan',
'emergency' => 'emergency',
+ 'recipient' => 'recipient',
'outcome-discharged' => 'discharged',
'outcome-transferred' => 'transferred',
'outcome-deceased' => 'deceased',
'observation' => 'observation'
];
- $patientType = $patientTypeMap[$status] ?? null;
+ $patientType = $patientTypeMap[$baseStatus] ?? null;
if ($patientType === 'observation') {
- return $this->patientQueryService->getObservationPatients($department->department_id, $onlyIds); //$this->getObservationPatientsFromSnapshots($user->rf_department_id, $reportIds, $onlyIds);
+ return $this->unifiedPatientService->getObservationPatients($department, $onlyIds, $sourceScope);
}
- return $this->snapshotService->getPatientsFromSnapshots($patientType, $reportIds, $branchId, $onlyIds);
+ if ($dateRange->isOneDay && in_array($baseStatus, ['plan', 'emergency'], true)) {
+ $patients = $this->snapshotService->getPatientsFromOneDayCurrentSnapshots(
+ $patientType,
+ $reportIds,
+ false,
+ $recipientReportIds
+ );
+
+ return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds);
+ }
+
+ $patients = $this->snapshotService->getPatientsFromSnapshots(
+ $patientType,
+ $reportIds,
+ $branchId,
+ false,
+ in_array($baseStatus, ['plan', 'emergency'], true),
+ $recipientReportIds
+ );
+
+ return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds);
}
/**
@@ -785,63 +1351,31 @@ class ReportService
bool $onlyIds = false,
?bool $isIncludeCurrent = null
) {
- $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
+ [$baseStatus] = $this->parseScopedStatus($status);
// Для плановых и экстренных включаем уже лечащихся
- $includeCurrent = $isIncludeCurrent ?? in_array($status, ['plan', 'emergency']);
+ $includeCurrent = $isIncludeCurrent ?? in_array($baseStatus, ['plan', 'emergency'], true);
return match($status) {
- 'plan', 'emergency' => $this->patientQueryService->getPlanOrEmergencyPatients(
+ '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,
- $isHeadOrAdmin,
- $branchId,
$dateRange,
- false,
+ $branchId,
$onlyIds,
$includeCurrent
- ),
- 'observation' => $this->patientQueryService->getObservationPatients($department->department_id, $onlyIds),
- 'outcome' => $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'without-transferred',
- $onlyIds
- ), // Выписанные без перевода
- 'outcome-discharged' => $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'discharged',
- $onlyIds
- ),
- 'outcome-transferred' => $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'transferred',
- $onlyIds
- ),
- 'outcome-deceased' => $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'deceased',
- $onlyIds
- ),
- 'current' => $this->patientQueryService->getAllPatientsInDepartment(
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- false,
- $onlyIds
- ),
- 'recipient' => $this->patientQueryService->getPlanOrEmergencyPatients(
- null,
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- false,
- $onlyIds,
- false // только поступившие
- ),
- default => collect()
+ )
};
}
@@ -850,6 +1384,7 @@ class ReportService
*/
private function getPatientsCountFromSnapshots(Department $department, string $status, DateRange $dateRange): int
{
+ [$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
$reports = $this->getReportsForDateRange(
$department->department_id,
$dateRange
@@ -857,7 +1392,17 @@ class ReportService
$reportIds = $reports->pluck('report_id')->toArray();
- if ($status === 'outcome') {
+ if ($baseStatus === 'outcome') {
+ if ($sourceScope !== 'all') {
+ return $this->getPatientsFromSnapshots(
+ $department,
+ $status,
+ $dateRange,
+ $this->getBranchId($department->rf_mis_department_id),
+ false
+ )->count();
+ }
+
return MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds)
->whereIn('patient_type', ['discharged', 'deceased'])
->distinct('rf_medicalhistory_id')
@@ -873,16 +1418,24 @@ class ReportService
'outcome-deceased' => 'deceased'
];
- $patientType = $patientTypeMap[$status] ?? null;
+ $patientType = $patientTypeMap[$baseStatus] ?? null;
if (!$patientType) {
return 0;
}
if ($patientType === 'observation') {
- return ObservationPatient::whereIn('rf_report_id', $reportIds)
- ->distinct('rf_medicalhistory_id')
- ->count('rf_medicalhistory_id');
+ return $this->unifiedPatientService->getObservationPatients($department, false, $sourceScope)->count();
+ }
+
+ if ($sourceScope !== 'all') {
+ return $this->getPatientsFromSnapshots(
+ $department,
+ $status,
+ $dateRange,
+ $this->getBranchId($department->rf_mis_department_id),
+ false
+ )->count();
}
return MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds)
@@ -891,6 +1444,15 @@ class ReportService
->count('rf_medicalhistory_id');
}
+ private function getSnapshotRecipientReportIds(array $reportIds): array
+ {
+ if (empty($reportIds)) {
+ return [];
+ }
+
+ return [reset($reportIds)];
+ }
+
/**
* Получить количество пациентов из реплики БД
*/
@@ -902,40 +1464,70 @@ class ReportService
int $branchId
): int
{
- $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
+ [$baseStatus] = $this->parseScopedStatus($status);
return match($status) {
- 'plan', 'emergency' => $this->patientQueryService->getPatientsCountWithCurrent(
+ 'plan', 'emergency', 'observation', 'reanimation', 'outcome', 'outcome-discharged', 'outcome-transferred', 'outcome-deceased', 'current', 'recipient' =>
+ $this->unifiedPatientService->getLivePatientCountByStatus(
+ $department,
+ $user,
+ $status,
+ $dateRange,
+ $branchId,
+ in_array($status, ['plan', 'emergency'], true)
+ ),
+ default => $this->unifiedPatientService->getLivePatientCountByStatus(
+ $department,
+ $user,
$status,
- $isHeadOrAdmin,
- $branchId,
$dateRange,
- ),
- 'observation' => ObservationPatient::where('rf_department_id', $department->department_id)->count(),
- 'outcome' => $this->patientQueryService->getOutcomePatients(
$branchId,
- $dateRange,
- 'without-transferred'
- )->count(),
- 'outcome-discharged' => $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'discharged'
- )->count(),
- 'outcome-transferred' => $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'transferred'
- )->count(),
- 'outcome-deceased' => $this->patientQueryService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'deceased'
- )->count(),
- default => 0
+ in_array($baseStatus, ['plan', 'emergency'], true)
+ )
};
}
+ private function filterSnapshotPatientsByScope($patients, string $sourceScope, bool $onlyIds = false)
+ {
+ if ($sourceScope === 'all') {
+ return $onlyIds ? $patients->pluck('id') : $patients;
+ }
+
+ $filtered = $patients->filter(function ($patient) use ($sourceScope) {
+ return match ($sourceScope) {
+ 'mis' => $patient->sourceType === 'mis',
+ 'special' => in_array($patient->sourceType, ['manual', 'special'], true),
+ default => true,
+ };
+ })->values();
+
+ return $onlyIds ? $filtered->pluck('id') : $filtered;
+ }
+
+ private function parseScopedStatus(string $status): array
+ {
+ foreach (['mis', 'special'] as $scope) {
+ $prefix = "{$scope}-";
+
+ if (str_starts_with($status, $prefix)) {
+ return [substr($status, strlen($prefix)), $scope];
+ }
+ }
+
+ return [$status, 'all'];
+ }
+
+ private function isSpecialScopedPatient($patient): bool
+ {
+ $sourceType = $patient->sourceType ?? $patient->source_type ?? null;
+
+ if ($sourceType !== null) {
+ return in_array($sourceType, ['manual', 'special'], true);
+ }
+
+ return str_starts_with((string) ($patient->id ?? ''), 'manual:');
+ }
+
/**
* Получить нежелательные события за дату
*/
@@ -1025,11 +1617,23 @@ class ReportService
->orderBy('created_at', 'DESC')
->get();
+ if (!$sum) {
+ foreach ($reports as $report) {
+ $metric = $report->metrikaResults
+ ->firstWhere('rf_metrika_item_id', $metrikaItemId);
+
+ if ($metric) {
+ return intval($metric->value) ?? 0;
+ }
+ }
+
+ return 0;
+ }
+
foreach ($reports as $report) {
foreach ($report->metrikaResults as $metrikaResult) {
if ($metrikaResult->rf_metrika_item_id === $metrikaItemId) {
- if ($sum) $count += intval($metrikaResult->value) ?? 0;
- else $count = intval($metrikaResult->value) ?? 0;
+ $count += intval($metrikaResult->value) ?? 0;
}
}
}
diff --git a/app/Services/SnapshotService.php b/app/Services/SnapshotService.php
index ed44aaa..cc57209 100644
--- a/app/Services/SnapshotService.php
+++ b/app/Services/SnapshotService.php
@@ -2,20 +2,22 @@
namespace App\Services;
+use App\Data\UnifiedPatientData;
+use App\Models\Department;
+use App\Models\DepartmentPatientOperation;
use App\Models\MedicalHistorySnapshot;
use App\Models\MetrikaResult;
use App\Models\MisMedicalHistory;
use App\Models\MisStationarBranch;
-use App\Models\ObservationPatient;
use App\Models\Report;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\DB;
class SnapshotService
{
public function __construct(
+ protected UnifiedPatientService $unifiedPatientService,
protected PatientService $patientService,
protected DateRangeService $dateRangeService,
) {}
@@ -25,118 +27,124 @@ class SnapshotService
*/
public function createPatientSnapshots(Report $report, User $user, array $dates, $fillableAuto = false): void
{
- $branchId = $this->getBranchId($user->department->rf_mis_department_id);
+ $department = Department::query()->where('department_id', $report->rf_department_id)->first() ?? $user->department;
+ $branchId = $department
+ ? $this->getBranchId($department->rf_mis_department_id)
+ : null;
+
+ if (!$department || !$branchId) {
+ return;
+ }
+
+ MedicalHistorySnapshot::query()
+ ->where('rf_report_id', $report->report_id)
+ ->delete();
+
[$startDate, $endDate] = $this->parseDates($dates);
$dateRange = $this->dateRangeService->getNormalizedDateRange($user, $startDate, $endDate);
- $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
-
- // Массив для хранения подсчитанных метрик
$metrics = [];
- // 1. Плановые пациенты
- $planIds = $this->patientService->getPlanOrEmergencyPatients(
+ $planPatients = $this->unifiedPatientService->getLivePatientsByStatus(
+ $department,
+ $user,
'plan',
- $isHeadOrAdmin,
- $branchId,
$dateRange,
+ $branchId,
false,
- true,
- true,
+ !$fillableAuto,
$fillableAuto
);
- $this->createSnapshotsForType($report, 'plan', $planIds);
- $metrics[4] = $this->patientService->getPlanOrEmergencyPatients(
- 'plan',
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- true,
- $fillableAuto
- ); // metrika_item_3 - плановые
+ $this->createSnapshotsForType($report, 'plan', $planPatients);
+ $metrics[4] = $planPatients->count();
- // 2. Экстренные пациенты
- $emergencyIds = $this->patientService->getPlanOrEmergencyPatients(
+ $emergencyPatients = $this->unifiedPatientService->getLivePatientsByStatus(
+ $department,
+ $user,
'emergency',
- $isHeadOrAdmin,
- $branchId,
$dateRange,
+ $branchId,
false,
- true,
- true,
+ !$fillableAuto,
$fillableAuto
);
- $this->createSnapshotsForType($report, 'emergency', $emergencyIds);
- $metrics[12] = $this->patientService->getPlanOrEmergencyPatients(
- 'emergency',
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- true,
- $fillableAuto
- );; // metrika_item_12 - экстренные
+ $this->createSnapshotsForType($report, 'emergency', $emergencyPatients);
+ $metrics[12] = $emergencyPatients->count();
- // 3. Выписанные
- $dischargedIds = $this->patientService->getOutcomePatients(
- $branchId,
+ $dischargedPatients = $this->unifiedPatientService->getLivePatientsByStatus(
+ $department,
+ $user,
+ 'outcome-discharged',
$dateRange,
- 'discharged',
- true
- );
- $this->createSnapshotsForType($report, 'discharged', $dischargedIds);
- $metrics[15] = count($dischargedIds); // metrika_item_15 - выписанные
-
- // 4. Переведенные
- $transferredIds = $this->patientService->getOutcomePatients(
$branchId,
- $dateRange,
- 'transferred',
- true
- );
- $this->createSnapshotsForType($report, 'transferred', $transferredIds);
- $metrics[13] = count($transferredIds); // metrika_item_13 - переведенные
-
- // 5. Умершие
- $deceasedIds = $this->patientService->getOutcomePatients(
- $branchId,
- $dateRange,
- 'deceased',
- true
- );
- $this->createSnapshotsForType($report, 'deceased', $deceasedIds);
-// $metrics[9] = count($deceasedIds); // metrika_item_9 - умершие
-
- // 6. Поступившие (все новые поступления - плановые + экстренные)
- $recipientIds = $this->patientService->getPlanOrEmergencyPatients(
+ false,
null,
- $isHeadOrAdmin,
- $branchId,
- $dateRange,
- false,
- true,
- false // только поступившие сегодня
+ $fillableAuto
);
- $this->createSnapshotsForType($report, 'recipient', $recipientIds);
-// $metrics[3] = count($recipientIds); // metrika_item_3 - поступившие
+ $this->createSnapshotsForType($report, 'discharged', $dischargedPatients);
+ $metrics[15] = $dischargedPatients->count();
+
+ $transferredPatients = $this->unifiedPatientService->getLivePatientsByStatus(
+ $department,
+ $user,
+ 'outcome-transferred',
+ $dateRange,
+ $branchId,
+ false,
+ null,
+ $fillableAuto
+ );
+ $this->createSnapshotsForType($report, 'transferred', $transferredPatients);
+ $metrics[13] = $transferredPatients->count();
+
+ $deceasedPatients = $this->unifiedPatientService->getLivePatientsByStatus(
+ $department,
+ $user,
+ 'outcome-deceased',
+ $dateRange,
+ $branchId,
+ false,
+ null,
+ $fillableAuto
+ );
+ $this->createSnapshotsForType($report, 'deceased', $deceasedPatients);
+
+ $recipientPatients = $this->unifiedPatientService->getLivePatientsByStatus(
+ $department,
+ $user,
+ 'recipient',
+ $dateRange,
+ $branchId,
+ false,
+ null,
+ $fillableAuto
+ );
+ $this->createSnapshotsForType($report, 'recipient', $recipientPatients);
+
+ $currentPatients = $this->unifiedPatientService->getLivePatientsByStatus(
+ $department,
+ $user,
+ 'current',
+ $dateRange,
+ $branchId,
+ false,
+ null,
+ $fillableAuto
+ );
+ $this->createSnapshotsForType($report, 'current', $currentPatients);
- // 8. Плановые операции
$planSurgeryCount = $this->patientService->getSurgicalPatients(
'plan',
$branchId,
$dateRange,
true
);
-// $metrics[11] = $planSurgeryCount; // metrika_item_11 - плановые операции
-
- // 9. Экстренные операции
$emergencySurgeryCount = $this->patientService->getSurgicalPatients(
'emergency',
$branchId,
$dateRange,
true
);
-// $metrics[10] = $emergencySurgeryCount; // metrika_item_10 - экстренные операции
- // Сохраняем все метрики
$this->saveMetrics($report, $metrics);
}
@@ -181,43 +189,136 @@ class SnapshotService
string $type,
array $reportIds,
?int $branchId = null,
- bool $onlyIds = false
+ bool $onlyIds = false,
+ bool $markRecipients = false,
+ ?array $recipientReportIds = null
): Collection {
- // Получаем ID историй болезни напрямую через DB::table() — это быстрее
- $medicalHistoryIds = DB::table('medical_history_snapshots')
- ->select('rf_medicalhistory_id')
+ $snapshots = MedicalHistorySnapshot::query()
->whereIn('rf_report_id', $reportIds)
->where('patient_type', $type)
- ->distinct()
- ->pluck('rf_medicalhistory_id');
+ ->get()
+ ->unique(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}"))
+ ->values();
- if ($medicalHistoryIds->isEmpty()) {
+ if ($snapshots->isEmpty()) {
return collect();
}
- $query = MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds);
-
- if ($type === 'plan') {
- $query->plan();
- } elseif ($type === 'emergency') {
- $query->emergency();
+ $recipientIds = [];
+ if ($markRecipients) {
+ $recipientReportIds ??= $reportIds;
+ $recipientIds = MedicalHistorySnapshot::query()
+ ->whereIn('rf_report_id', $recipientReportIds)
+ ->where('patient_type', 'recipient')
+ ->get()
+ ->map(fn (MedicalHistorySnapshot $snapshot) => UnifiedPatientData::fromSnapshot($snapshot)->id)
+ ->unique()
+ ->values()
+ ->all();
}
- // Загрузка отношений, необходимых для FormattedPatientResource
- $query->with([
- 'outcomeMigration.mainDiagnosis.mkb', // mkb через диагноз
- 'surgicalOperations.serviceMedical', // операции с услугами
- ]);
+ $operationsByHistoryId = $this->getOperationsByMedicalHistoryId($snapshots);
+ $operationsByDepartmentPatientId = $this->getOperationsByDepartmentPatientId($snapshots);
- $query->orderBy('DateRecipient', 'DESC');
+ $patients = $snapshots->map(function (MedicalHistorySnapshot $snapshot) use ($recipientIds, $operationsByHistoryId, $operationsByDepartmentPatientId) {
+ $patientId = $snapshot->rf_department_patient_id
+ ? "manual:{$snapshot->rf_department_patient_id}"
+ : ($snapshot->patient_uid ?: "mis:{$snapshot->rf_medicalhistory_id}");
- $results = $query->get();
+ $misOperations = $snapshot->rf_medicalhistory_id
+ ? ($operationsByHistoryId[$snapshot->rf_medicalhistory_id] ?? [])
+ : [];
+ $manualOperations = $snapshot->rf_department_patient_id
+ ? ($operationsByDepartmentPatientId[$snapshot->rf_department_patient_id] ?? [])
+ : [];
+ $operations = collect($misOperations)
+ ->merge($manualOperations)
+ ->unique(fn (array $operation) => ($operation['code'] ?? '') . '|' . ($operation['name'] ?? ''))
+ ->values()
+ ->all();
+
+ return UnifiedPatientData::fromSnapshot(
+ $snapshot,
+ in_array($patientId, $recipientIds, true),
+ $operations
+ );
+ })->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')->values();
if ($onlyIds) {
- return $results->pluck('MedicalHistoryID');
+ return $patients->pluck('id');
}
- return $results;
+ return $patients;
+ }
+
+ public function getPatientsFromOneDayCurrentSnapshots(
+ string $type,
+ array $reportIds,
+ bool $onlyIds = false,
+ ?array $recipientReportIds = null
+ ): Collection {
+ $snapshots = MedicalHistorySnapshot::query()
+ ->whereIn('rf_report_id', $reportIds)
+ ->where('patient_type', 'current')
+ ->get();
+
+ if ($snapshots->isEmpty()) {
+ return $this->getPatientsFromSnapshots(
+ $type,
+ $reportIds,
+ null,
+ $onlyIds,
+ true,
+ $recipientReportIds
+ );
+ }
+
+ $recipientReportIds ??= $reportIds;
+ $recipientIds = MedicalHistorySnapshot::query()
+ ->whereIn('rf_report_id', $recipientReportIds)
+ ->where('patient_type', 'recipient')
+ ->get()
+ ->map(fn (MedicalHistorySnapshot $snapshot) => UnifiedPatientData::fromSnapshot($snapshot)->id)
+ ->unique()
+ ->values()
+ ->all();
+
+ $operationsByHistoryId = $this->getOperationsByMedicalHistoryId($snapshots);
+ $operationsByDepartmentPatientId = $this->getOperationsByDepartmentPatientId($snapshots);
+
+ $patients = $snapshots
+ ->filter(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_kind === $type)
+ ->unique(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}"))
+ ->map(function (MedicalHistorySnapshot $snapshot) use ($recipientIds, $operationsByHistoryId, $operationsByDepartmentPatientId) {
+ $misOperations = $snapshot->rf_medicalhistory_id
+ ? ($operationsByHistoryId[$snapshot->rf_medicalhistory_id] ?? [])
+ : [];
+ $manualOperations = $snapshot->rf_department_patient_id
+ ? ($operationsByDepartmentPatientId[$snapshot->rf_department_patient_id] ?? [])
+ : [];
+ $operations = collect($misOperations)
+ ->merge($manualOperations)
+ ->unique(fn (array $operation) => ($operation['code'] ?? '') . '|' . ($operation['name'] ?? ''))
+ ->values()
+ ->all();
+
+ $patient = UnifiedPatientData::fromSnapshot(
+ $snapshot,
+ false,
+ $operations
+ );
+ $patient->isRecipientToday = in_array($patient->id, $recipientIds, true);
+
+ return $patient;
+ })
+ ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')
+ ->values();
+
+ if ($onlyIds) {
+ return $patients->pluck('id');
+ }
+
+ return $patients;
}
/**
@@ -233,26 +334,32 @@ class SnapshotService
$query->where('patient_type', $type);
}
- return $query->distinct('rf_medicalhistory_id')
- ->count('rf_medicalhistory_id');
+ return $query->get()
+ ->map(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}"))
+ ->unique()
+ ->count();
}
/**
* Создать снапшоты для определенного типа пациентов
*/
- private function createSnapshotsForType(Report $report, string $type, Collection $medicalHistoryIds): void
+ private function createSnapshotsForType(Report $report, string $type, Collection $patients): void
{
- foreach ($medicalHistoryIds as $id) {
+ foreach ($patients as $patient) {
+ if (!$patient instanceof UnifiedPatientData) {
+ continue;
+ }
+
MedicalHistorySnapshot::updateOrCreate(
[
'rf_report_id' => $report->report_id,
- 'rf_medicalhistory_id' => $id,
- 'patient_type' => $type
+ 'patient_uid' => $patient->patientUid,
+ 'patient_type' => $type,
],
[
'rf_report_id' => $report->report_id,
- 'rf_medicalhistory_id' => $id,
'patient_type' => $type,
+ ...$patient->toSnapshotPayload($type),
]
);
}
@@ -277,4 +384,47 @@ class SnapshotService
Carbon::createFromTimestampMs($dates[1])->setTimezone('Asia/Yakutsk'),
];
}
+
+ private function getOperationsByMedicalHistoryId(Collection $snapshots): array
+ {
+ $historyIds = $snapshots->pluck('rf_medicalhistory_id')->filter()->unique()->values();
+
+ if ($historyIds->isEmpty()) {
+ return [];
+ }
+
+ return MisMedicalHistory::query()
+ ->whereIn('MedicalHistoryID', $historyIds)
+ ->with(['surgicalOperations.serviceMedical'])
+ ->get()
+ ->mapWithKeys(function (MisMedicalHistory $history) {
+ return [
+ $history->MedicalHistoryID => $history->surgicalOperations->map(fn ($operation) => [
+ 'code' => $operation->serviceMedical?->ServiceMedicalCode,
+ 'name' => $operation->serviceMedical?->ServiceMedicalName,
+ ])->values()->all()
+ ];
+ })
+ ->all();
+ }
+
+ private function getOperationsByDepartmentPatientId(Collection $snapshots): array
+ {
+ $departmentPatientIds = $snapshots->pluck('rf_department_patient_id')->filter()->unique()->values();
+
+ if ($departmentPatientIds->isEmpty()) {
+ return [];
+ }
+
+ return DepartmentPatientOperation::query()
+ ->whereIn('rf_department_patient_id', $departmentPatientIds)
+ ->with('serviceMedical')
+ ->get()
+ ->groupBy('rf_department_patient_id')
+ ->map(fn (Collection $operations) => $operations->map(fn (DepartmentPatientOperation $operation) => [
+ 'code' => $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code,
+ 'name' => $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name,
+ ])->values()->all())
+ ->all();
+ }
}
diff --git a/app/Services/StationarBranchService.php b/app/Services/StationarBranchService.php
index c77b1ae..410514e 100644
--- a/app/Services/StationarBranchService.php
+++ b/app/Services/StationarBranchService.php
@@ -2,7 +2,23 @@
namespace App\Services;
+use App\Models\MisStationarBranch;
+use Illuminate\Support\Facades\Cache;
+
class StationarBranchService
{
-
+ /**
+ * Получение идентификаторов приемных отделений. Кешируется на 24ч
+ * @return array
+ */
+ public function getWardIds(): array
+ {
+ return Cache::tags(['wards_ids'])
+ ->remember('branch_ward_ids', now()->addHours(24), function () {
+ return MisStationarBranch::query()
+ ->where('IsHospitalWard', 1)
+ ->pluck('StationarBranchID')
+ ->toArray();
+ });
+ }
}
diff --git a/app/Services/UnifiedPatientService.php b/app/Services/UnifiedPatientService.php
new file mode 100644
index 0000000..294ce63
--- /dev/null
+++ b/app/Services/UnifiedPatientService.php
@@ -0,0 +1,391 @@
+parseScopedStatus($status);
+
+ if ($baseStatus === 'observation') {
+ return $this->getObservationPatients($department, $onlyIds, $sourceScope);
+ }
+
+ $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),
+ };
+
+ if ($onlyIds) {
+ return $patients->pluck('id');
+ }
+
+ return $patients;
+ }
+
+ public function getLivePatientCountByStatus(
+ Department $department,
+ User $user,
+ string $status,
+ DateRange $dateRange,
+ int $branchId,
+ ?bool $includeCurrent = null,
+ bool $fillableAuto = false
+ ): int {
+ return $this->getLivePatientsByStatus($department, $user, $status, $dateRange, $branchId, false, $includeCurrent, $fillableAuto)->count();
+ }
+
+ public function createManualPatient(Department $department, User $user, array $data): DepartmentPatient
+ {
+ return DepartmentPatient::create([
+ 'rf_department_id' => $department->department_id,
+ 'source_type' => 'special',
+ 'full_name' => $data['full_name'],
+ 'birth_date' => $data['birth_date'],
+ 'patient_kind' => $data['patient_kind'],
+ 'diagnosis_code' => $data['diagnosis_code'] ?? null,
+ 'diagnosis_name' => $data['diagnosis_name'] ?? null,
+ 'admitted_at' => $data['admitted_at'] ?? now(),
+ 'is_current' => true,
+ 'created_by' => $user->id,
+ ]);
+ }
+
+ public function recordManualOutcome(DepartmentPatient $patient, array $data): DepartmentPatient
+ {
+ $patient->update([
+ 'is_current' => false,
+ 'outcome_type' => $data['outcome_type'],
+ 'outcome_at' => $data['outcome_at'] ?? now(),
+ ]);
+
+ return $patient->fresh();
+ }
+
+ public function updateManualPatient(DepartmentPatient $patient, array $data): DepartmentPatient
+ {
+ $patient->update([
+ 'full_name' => $data['full_name'],
+ 'birth_date' => $data['birth_date'],
+ 'patient_kind' => $data['patient_kind'],
+ 'diagnosis_code' => $data['diagnosis_code'] ?? null,
+ 'diagnosis_name' => $data['diagnosis_name'] ?? null,
+ 'admitted_at' => $data['admitted_at'] ?? $patient->admitted_at,
+ ]);
+
+ return $patient->fresh();
+ }
+
+ public function linkManualPatientToMis(DepartmentPatient $patient, int $medicalHistoryId): DepartmentPatient
+ {
+ $misPatient = MisMedicalHistory::where('MedicalHistoryID', $medicalHistoryId)->firstOrFail();
+
+ $patient->update([
+ 'rf_medicalhistory_id' => $misPatient->MedicalHistoryID,
+ 'linked_to_mis_at' => now(),
+ 'full_name' => $patient->full_name ?: trim("{$misPatient->FAMILY} {$misPatient->Name} {$misPatient->OT}"),
+ 'birth_date' => $patient->birth_date ?: $misPatient->BD,
+ ]);
+
+ return $patient->fresh();
+ }
+
+ public function searchMisPatients(Department $department, string $query): Collection
+ {
+ $branchId = \App\Models\MisStationarBranch::where('rf_DepartmentID', $department->rf_mis_department_id)
+ ->value('StationarBranchID');
+
+ return MisMedicalHistory::query()
+ ->whereHas('migrations', fn ($builder) => $builder->where('rf_StationarBranchID', $branchId))
+ ->where(function ($builder) use ($query) {
+ $builder->where('FAMILY', 'like', "%{$query}%")
+ ->orWhere('Name', 'like', "%{$query}%")
+ ->orWhere('OT', 'like', "%{$query}%");
+ })
+ ->with(['outcomeMigration.mainDiagnosis.mkb'])
+ ->limit(20)
+ ->get()
+ ->map(fn (MisMedicalHistory $patient) => UnifiedPatientData::fromMisMedicalHistory($patient));
+ }
+
+ public function getObservationPatients(
+ Department $department,
+ bool $onlyIds = false,
+ string $sourceScope = 'all'
+ ): Collection
+ {
+ $observationPatients = ObservationPatient::where('rf_department_id', $department->department_id)->get();
+
+ $misIds = $observationPatients->pluck('rf_medicalhistory_id')->filter()->unique()->values();
+ $manualIds = $observationPatients->pluck('rf_department_patient_id')->filter()->unique()->values();
+
+ $misPatients = MisMedicalHistory::whereIn('MedicalHistoryID', $misIds)
+ ->with(['outcomeMigration.mainDiagnosis.mkb'])
+ ->get()
+ ->keyBy('MedicalHistoryID');
+
+ $manualPatients = DepartmentPatient::whereIn('department_patient_id', $manualIds)->get()->keyBy('department_patient_id');
+
+ $patients = $observationPatients->map(function (ObservationPatient $observation) use ($misPatients, $manualPatients, $sourceScope) {
+ if ($observation->rf_department_patient_id && $manualPatients->has($observation->rf_department_patient_id)) {
+ if ($sourceScope === 'mis') {
+ return null;
+ }
+
+ return UnifiedPatientData::fromDepartmentPatient(
+ $manualPatients[$observation->rf_department_patient_id],
+ false,
+ [],
+ $observation->comment
+ );
+ }
+
+ if ($observation->rf_medicalhistory_id && $misPatients->has($observation->rf_medicalhistory_id)) {
+ if ($sourceScope === 'special') {
+ return null;
+ }
+
+ return UnifiedPatientData::fromMisMedicalHistory(
+ $misPatients[$observation->rf_medicalhistory_id],
+ false,
+ null,
+ $observation->comment
+ );
+ }
+
+ return null;
+ })->filter()->values();
+
+ if ($onlyIds) {
+ return $patients->pluck('id');
+ }
+
+ return $patients;
+ }
+
+ private function getAggregatedPatientDtos(
+ Department $department,
+ User $user,
+ string $status,
+ DateRange $dateRange,
+ int $branchId,
+ ?bool $includeCurrent = null,
+ bool $fillableAuto = 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)
+ ->whereIn('source_type', self::SPECIAL_SOURCE_TYPES)
+ ->whereNotNull('rf_medicalhistory_id')
+ ->get()
+ ->keyBy('rf_medicalhistory_id');
+
+ $mergedMisPatients = $misPatients->map(function ($patient) use ($linkedManualPatients) {
+ $linkedManual = $linkedManualPatients->get($patient->MedicalHistoryID);
+
+ return UnifiedPatientData::fromMisMedicalHistory(
+ $patient,
+ (bool) ($patient->is_recipient_today ?? false),
+ $linkedManual,
+ $this->resolveObservationComment($patient->MedicalHistoryID, null)
+ );
+ });
+
+ $manualDtos = $this->mapManualPatients($manualPatients, $dateRange);
+
+ return UnifiedPatientData::unique($mergedMisPatients->concat($manualDtos))
+ ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')
+ ->values();
+ }
+
+ private function getMisPatientDtos(
+ User $user,
+ string $status,
+ DateRange $dateRange,
+ int $branchId,
+ ?bool $includeCurrent = null,
+ bool $fillableAuto = false
+ ): Collection {
+ return $this->getMisPatients($user, $status, $dateRange, $branchId, $includeCurrent, $fillableAuto)
+ ->map(fn ($patient) => UnifiedPatientData::fromMisMedicalHistory(
+ $patient,
+ (bool) ($patient->is_recipient_today ?? false),
+ ))
+ ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')
+ ->values();
+ }
+
+ private function getSpecialPatientDtos(
+ Department $department,
+ string $status,
+ DateRange $dateRange
+ ): Collection {
+ return $this->mapManualPatients(
+ $this->getManualPatients($department, $status, $dateRange, self::SPECIAL_SOURCE_TYPES),
+ $dateRange
+ );
+ }
+
+ private function getMisPatients(
+ User $user,
+ string $status,
+ DateRange $dateRange,
+ int $branchId,
+ ?bool $includeCurrent = null,
+ bool $fillableAuto = false
+ ): Collection {
+ $isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
+ $includeCurrent = $includeCurrent ?? in_array($status, ['plan', 'emergency'], true);
+
+ return match ($status) {
+ 'plan', 'emergency' => $this->patientService->getPlanOrEmergencyPatients(
+ $status,
+ $isHeadOrAdmin,
+ $branchId,
+ $dateRange,
+ false,
+ false,
+ $includeCurrent,
+ $fillableAuto
+ ),
+ 'current' => $this->patientService->getAllPatientsInDepartment(
+ $isHeadOrAdmin,
+ $branchId,
+ $dateRange,
+ false,
+ false,
+ $fillableAuto
+ ),
+ 'recipient' => $this->patientService->getPlanOrEmergencyPatients(
+ null,
+ $isHeadOrAdmin,
+ $branchId,
+ $dateRange,
+ false,
+ false,
+ false,
+ $fillableAuto
+ ),
+ 'outcome' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'without-transferred'),
+ 'outcome-discharged' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'discharged'),
+ 'outcome-transferred' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'transferred'),
+ 'outcome-deceased' => $this->patientService->getOutcomePatients($branchId, $dateRange, 'deceased'),
+ 'reanimation' => $this->patientService->getReanimationPatients($branchId, $dateRange),
+ default => collect(),
+ };
+ }
+
+ private function getManualPatients(
+ Department $department,
+ string $status,
+ DateRange $dateRange,
+ ?array $sourceTypes = self::SPECIAL_SOURCE_TYPES
+ ): Collection
+ {
+ $query = DepartmentPatient::where('rf_department_id', $department->department_id)
+ ->with(['operations.serviceMedical']);
+
+ if ($sourceTypes !== null) {
+ $query->whereIn('source_type', $sourceTypes);
+ }
+
+ return match ($status) {
+ 'plan', 'emergency' => $query
+ ->current()
+ ->where('patient_kind', $status)
+ ->get(),
+ 'current' => $query
+ ->current()
+ ->get(),
+ 'recipient' => $query
+ ->whereBetween('admitted_at', [$dateRange->startSql(), $dateRange->endSql()])
+ ->get(),
+ 'outcome' => $query
+ ->whereNotNull('outcome_type')
+ ->whereIn('outcome_type', ['discharged', 'deceased'])
+ ->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()])
+ ->get(),
+ 'outcome-discharged', 'outcome-transferred', 'outcome-deceased' => $query
+ ->where('outcome_type', str_replace('outcome-', '', $status))
+ ->whereBetween('outcome_at', [$dateRange->startSql(), $dateRange->endSql()])
+ ->get(),
+ 'reanimation' => collect(),
+ default => collect(),
+ };
+ }
+
+ private function mapManualPatients(Collection $manualPatients, DateRange $dateRange): Collection
+ {
+ return $manualPatients
+ ->map(function (DepartmentPatient $patient) use ($dateRange) {
+ $operations = $patient->operations->map(fn ($operation) => [
+ 'id' => $operation->department_patient_operation_id,
+ 'code' => $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code,
+ 'name' => $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name,
+ 'startAt' => $operation->started_at?->toIso8601String(),
+ 'endAt' => $operation->ended_at?->toIso8601String(),
+ ])->filter(fn ($operation) => $operation['code'] || $operation['name'])->values()->all();
+
+ return UnifiedPatientData::fromDepartmentPatient(
+ $patient,
+ $patient->admitted_at?->betweenIncluded($dateRange->startDate, $dateRange->endDate) ?? false,
+ $operations,
+ $this->resolveObservationComment(null, $patient->department_patient_id)
+ );
+ })
+ ->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')
+ ->values();
+ }
+
+ private function parseScopedStatus(string $status): array
+ {
+ foreach (['mis', 'special'] as $scope) {
+ $prefix = "{$scope}-";
+
+ if (str_starts_with($status, $prefix)) {
+ return [substr($status, strlen($prefix)), $scope];
+ }
+ }
+
+ return [$status, 'all'];
+ }
+
+ private function resolveObservationComment(?int $medicalHistoryId, ?int $departmentPatientId): ?string
+ {
+ $query = ObservationPatient::query();
+
+ if ($departmentPatientId) {
+ $query->where('rf_department_patient_id', $departmentPatientId);
+ } elseif ($medicalHistoryId) {
+ $query->where('rf_medicalhistory_id', $medicalHistoryId);
+ } else {
+ return null;
+ }
+
+ return $query->pluck('comment')->filter()->implode('; ') ?: null;
+ }
+}
diff --git a/config/time.php b/config/time.php
new file mode 100644
index 0000000..211267d
--- /dev/null
+++ b/config/time.php
@@ -0,0 +1,5 @@
+ env('TIME_EVENT_SOURCE_URL', null),
+];
\ No newline at end of file
diff --git a/database/migrations/2026_04_09_120000_create_department_patients_table.php b/database/migrations/2026_04_09_120000_create_department_patients_table.php
new file mode 100644
index 0000000..3922046
--- /dev/null
+++ b/database/migrations/2026_04_09_120000_create_department_patients_table.php
@@ -0,0 +1,39 @@
+id('department_patient_id');
+ $table->unsignedBigInteger('rf_department_id');
+ $table->string('source_type')->default('manual');
+ $table->unsignedBigInteger('rf_medicalhistory_id')->nullable();
+ $table->string('full_name');
+ $table->date('birth_date');
+ $table->string('patient_kind');
+ $table->string('diagnosis_code')->nullable();
+ $table->text('diagnosis_name')->nullable();
+ $table->dateTime('admitted_at');
+ $table->boolean('is_current')->default(true);
+ $table->string('outcome_type')->nullable();
+ $table->dateTime('outcome_at')->nullable();
+ $table->unsignedBigInteger('created_by')->nullable();
+ $table->dateTime('linked_to_mis_at')->nullable();
+ $table->timestamps();
+
+ $table->index(['rf_department_id', 'patient_kind'], 'idx_department_patients_department_kind');
+ $table->index(['rf_department_id', 'is_current'], 'idx_department_patients_department_current');
+ $table->index(['rf_medicalhistory_id'], 'idx_department_patients_medical_history');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('department_patients');
+ }
+};
diff --git a/database/migrations/2026_04_09_120100_add_unified_columns_to_medical_history_snapshots_table.php b/database/migrations/2026_04_09_120100_add_unified_columns_to_medical_history_snapshots_table.php
new file mode 100644
index 0000000..120cabc
--- /dev/null
+++ b/database/migrations/2026_04_09_120100_add_unified_columns_to_medical_history_snapshots_table.php
@@ -0,0 +1,52 @@
+string('patient_uid')->nullable()->after('patient_type');
+ $table->string('patient_source_type')->nullable()->after('patient_uid');
+ $table->unsignedBigInteger('rf_department_patient_id')->nullable()->after('rf_medicalhistory_id');
+ $table->string('patient_kind')->nullable()->after('rf_department_patient_id');
+ $table->string('full_name')->nullable()->after('patient_kind');
+ $table->date('birth_date')->nullable()->after('full_name');
+ $table->string('diagnosis_code')->nullable()->after('birth_date');
+ $table->text('diagnosis_name')->nullable()->after('diagnosis_code');
+ $table->dateTime('admitted_at')->nullable()->after('diagnosis_name');
+ $table->string('outcome_type')->nullable()->after('admitted_at');
+ $table->dateTime('outcome_at')->nullable()->after('outcome_type');
+ $table->boolean('is_manual')->default(false)->after('outcome_at');
+
+ $table->index(['rf_report_id', 'patient_uid'], 'idx_snapshots_report_uid');
+ $table->index(['patient_uid'], 'idx_snapshots_patient_uid');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('medical_history_snapshots', function (Blueprint $table) {
+ $table->dropIndex('idx_snapshots_report_uid');
+ $table->dropIndex('idx_snapshots_patient_uid');
+
+ $table->dropColumn([
+ 'patient_uid',
+ 'patient_source_type',
+ 'rf_department_patient_id',
+ 'patient_kind',
+ 'full_name',
+ 'birth_date',
+ 'diagnosis_code',
+ 'diagnosis_name',
+ 'admitted_at',
+ 'outcome_type',
+ 'outcome_at',
+ 'is_manual',
+ ]);
+ });
+ }
+};
diff --git a/database/migrations/2026_04_09_120200_add_department_patient_reference_to_observation_patients_table.php b/database/migrations/2026_04_09_120200_add_department_patient_reference_to_observation_patients_table.php
new file mode 100644
index 0000000..7342916
--- /dev/null
+++ b/database/migrations/2026_04_09_120200_add_department_patient_reference_to_observation_patients_table.php
@@ -0,0 +1,24 @@
+unsignedBigInteger('rf_department_patient_id')->nullable()->after('rf_medicalhistory_id');
+ $table->index(['rf_department_patient_id'], 'idx_observation_patients_department_patient');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('observation_patients', function (Blueprint $table) {
+ $table->dropIndex('idx_observation_patients_department_patient');
+ $table->dropColumn('rf_department_patient_id');
+ });
+ }
+};
diff --git a/database/migrations/2026_04_15_090000_create_department_patient_operations_table.php b/database/migrations/2026_04_15_090000_create_department_patient_operations_table.php
new file mode 100644
index 0000000..3d2e9b3
--- /dev/null
+++ b/database/migrations/2026_04_15_090000_create_department_patient_operations_table.php
@@ -0,0 +1,31 @@
+id('department_patient_operation_id');
+ $table->unsignedBigInteger('rf_department_patient_id');
+ $table->unsignedBigInteger('rf_kl_service_medical_id')->nullable();
+ $table->string('service_code', 64)->nullable();
+ $table->string('service_name', 500)->nullable();
+ $table->dateTime('started_at');
+ $table->dateTime('ended_at');
+ $table->unsignedBigInteger('created_by')->nullable();
+ $table->timestamps();
+
+ $table->index(['rf_department_patient_id'], 'idx_department_patient_operations_patient');
+ $table->index(['started_at'], 'idx_department_patient_operations_started_at');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('department_patient_operations');
+ }
+};
diff --git a/database/migrations/2026_04_15_101000_make_rf_medicalhistory_id_nullable_in_medical_history_snapshots_table.php b/database/migrations/2026_04_15_101000_make_rf_medicalhistory_id_nullable_in_medical_history_snapshots_table.php
new file mode 100644
index 0000000..e62962c
--- /dev/null
+++ b/database/migrations/2026_04_15_101000_make_rf_medicalhistory_id_nullable_in_medical_history_snapshots_table.php
@@ -0,0 +1,17 @@
+string('urgency', 32)->nullable()->after('service_name');
+ $table->index(['urgency'], 'idx_department_patient_operations_urgency');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('department_patient_operations', function (Blueprint $table) {
+ $table->dropIndex('idx_department_patient_operations_urgency');
+ $table->dropColumn('urgency');
+ });
+ }
+};
diff --git a/database/migrations/2026_04_20_093409_add_period_columns_in_reports_table.php b/database/migrations/2026_04_20_093409_add_period_columns_in_reports_table.php
new file mode 100644
index 0000000..d727e33
--- /dev/null
+++ b/database/migrations/2026_04_20_093409_add_period_columns_in_reports_table.php
@@ -0,0 +1,41 @@
+string('period_type')->default('day') // day|week|month|year
+ ->comment('Тип отчетного периода');
+ $table->dateTime('period_start')->nullable()
+ ->comment('Начало отчетного периода');
+ $table->dateTime('period_end')->nullable()
+ ->comment('Окончание отчетного периода');
+
+ $table->integer('report_month')->storedAs('EXTRACT(MONTH FROM created_at)::integer')
+ ->comment('Отчетный месяц');
+ $table->integer('report_year')->storedAs('EXTRACT(YEAR FROM created_at)::integer')
+ ->comment('Отчетный год');
+
+ $table->string('status')->default('draft')
+ ->comment('Статус отчета');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('reports', function (Blueprint $table) {
+ $table->dropColumn(['period_type', 'period_start', 'period_end', 'status', 'report_year', 'report_month']);
+ });
+ }
+};
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
new file mode 100644
index 0000000..1c1ef7e
--- /dev/null
+++ b/database/migrations/2026_04_20_163507_add_code_column_in_metrika_items_table.php
@@ -0,0 +1,28 @@
+string('code')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('metrika_items', function (Blueprint $table) {
+ //
+ });
+ }
+};
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000..f3a822d
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,85 @@
+# Частые вопросы
+
+## Не получается войти в систему
+
+Проверьте:
+
+- правильность логина;
+- правильность пароля;
+- раскладку клавиатуры;
+- не включен ли `Caps Lock`.
+
+Если проблема сохраняется, обратитесь к администратору системы.
+
+## Почему я не вижу нужный раздел
+
+Доступность разделов зависит от вашей роли.
+
+Например:
+
+- врач работает со сводной;
+- заведующий видит статистику отделения;
+- администратор имеет доступ к административным функциям.
+
+Если у вас несколько ролей, попробуйте переключить роль в верхней части интерфейса.
+
+## Почему кнопка «Заполнить сводную» недоступна
+
+Возможные причины:
+
+- для вашей роли действует ограничение по времени;
+- у вас нет права на создание отчета;
+- текущая роль не предназначена для заполнения сводной.
+
+## Почему система сообщает, что сводная уже создана
+
+Это означает, что за выбранную дату и отделение уже есть отчет.
+
+В этом случае:
+
+1. Откройте существующую сводную.
+2. Проверьте, кто указан ответственным.
+3. При необходимости уточните дальнейшие действия у заведующего или администратора.
+
+## Почему данные за период выглядят слишком большими или слишком маленькими
+
+Сначала проверьте:
+
+- дату;
+- диапазон дат;
+- отделение;
+- выбранную роль.
+
+Чаще всего причина связана с тем, что выбран не тот период, например месяц вместо недели или текущий год вместо одного дня.
+
+## Почему я не могу сохранить отчет
+
+Проверьте:
+
+- заполнены ли обязательные поля;
+- нет ли предупреждения на странице;
+- доступна ли кнопка `Сохранить отчет`;
+- открыт ли отчет в режиме редактирования, а не только просмотра.
+
+## Как понять, за какой период сейчас показаны данные
+
+Ориентируйтесь на блок выбора даты в верхней части страницы.
+
+Именно выбранная там дата или диапазон определяют:
+
+- содержимое сводной;
+- содержимое статистики;
+- выгрузку в Excel.
+
+## Что делать, если отображаются неверные данные
+
+Подготовьте для обращения в поддержку следующую информацию:
+
+- ваше ФИО;
+- роль, под которой вы работаете;
+- отделение;
+- дата или период;
+- краткое описание проблемы;
+- что вы ожидали увидеть на экране.
+
+Это поможет быстрее найти причину и проверить данные.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..328f2b2
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,79 @@
+# Метрика
+
+Добро пожаловать в руководство пользователя системы **«Метрика»**.
+
+Система помогает:
+
+- формировать сводный отчет по отделению;
+- просматривать статистику по отделению или по нескольким отделениям;
+- контролировать нежелательные события;
+- отслеживать пациентов на контроле;
+- работать в пределах своей роли и доступных отделений.
+
+## Для кого предназначена система
+
+Система используется медицинским персоналом и сотрудниками, которые работают с оперативной отчетностью отделений.
+
+В зависимости от вашей роли в системе вам могут быть доступны разные разделы:
+
+- `Врач` — заполнение сводной;
+- `Заведующий отделением` — просмотр статистики и работа со сводными по отделению;
+- `Администратор` — полный доступ, включая административную панель.
+
+Если у вас несколько ролей, нужную роль можно переключить в верхней части интерфейса.
+
+## Как начать работу
+
+1. Откройте страницу входа в систему.
+2. Введите логин и пароль.
+3. После входа вы попадете на стартовую страницу.
+4. Выберите нужный раздел:
+ - `Заполнить сводную`;
+ - `Статистика моего отделения`;
+ - `Панель администратора` — только для администратора.
+
+## Что находится на стартовой странице
+
+На стартовой странице отображаются:
+
+- имя пользователя;
+- текущая дата и время сервера;
+- кнопки перехода к основным разделам;
+- кнопка выхода из системы.
+
+### Кнопка «Заполнить сводную»
+
+Назначение кнопки зависит от вашей роли:
+
+- для `врача` она открывает выбор ответственного и отделения перед переходом к отчету;
+- для `администратора` она открывает форму сводной напрямую;
+- для `заведующего` доступ к сводной определяется рабочим сценарием и выбранной ролью.
+
+Для врача кнопка доступна только в установленный интервал времени. Если кнопка заблокирована, поверх нее отображается подсказка с доступным временем.
+
+## Основные разделы документации
+
+- [Работа со сводной](./reporting.md)
+- [Статистика](./statistics.md)
+- [Частые вопросы](./faq.md)
+
+## Общая логика работы
+
+Система строится вокруг даты и отделения.
+
+Обычно работа выглядит так:
+
+1. Пользователь выбирает нужный раздел.
+2. При необходимости выбирает отделение и ответственного.
+3. Указывает дату или период.
+4. Просматривает данные.
+5. При наличии права сохраняет отчет.
+
+## Полезный совет
+
+Если данные на экране не совпадают с ожидаемыми, в первую очередь проверьте:
+
+- выбранную роль;
+- выбранное отделение;
+- дату или диапазон дат;
+- наличие уже созданной сводной за нужный день.
diff --git a/docs/reporting.md b/docs/reporting.md
new file mode 100644
index 0000000..eb7d6f1
--- /dev/null
+++ b/docs/reporting.md
@@ -0,0 +1,132 @@
+# Работа со сводной
+
+Эта страница описывает, как открыть, проверить и сохранить сводный отчет.
+
+## Что такое сводная
+
+Сводная — это отчет по отделению за выбранную дату или период.
+
+В сводной отображаются:
+
+- показатели отделения;
+- количество поступивших и выбывших пациентов;
+- число пациентов, состоящих в отделении;
+- данные по операциям;
+- нежелательные события;
+- пациенты на контроле.
+
+Состав и доступность полей могут зависеть от роли пользователя и от того, открыт ли отчет только для просмотра или для заполнения.
+
+## Как открыть сводную
+
+### Вариант 1. Со стартовой страницы
+
+1. Нажмите `Заполнить сводную`.
+2. Если откроется окно выбора, укажите:
+ - ответственного;
+ - отделение.
+3. Нажмите `Перейти к заполнению сводной`.
+
+Если сводная за текущую дату уже существует, система предупредит об этом и предложит перейти к уже созданному отчету.
+
+### Вариант 2. Из раздела статистики
+
+1. Откройте `Статистика моего отделения`.
+2. Выберите нужную дату или период.
+3. Нажмите на название отделения в таблице.
+4. Откроется сводная по выбранному отделению и выбранному периоду.
+
+## Что находится на странице сводной
+
+На странице сводной обычно доступны:
+
+- название отделения;
+- выбранная дата или диапазон дат;
+- сводные числовые показатели;
+- поля формы отчета;
+- разделы с пациентами;
+- кнопка `Сохранить отчет`, если редактирование разрешено.
+
+Также на странице может отображаться предупреждение, если отчет за этот день уже был создан другим пользователем.
+
+## Как выбрать дату
+
+В верхней части страницы находится выбор даты.
+
+В зависимости от роли можно выбрать:
+
+- одну дату;
+- диапазон дат.
+
+После выбора периода система обновляет данные на странице.
+
+### Кнопка «Сегодня» и «Текущий год»
+
+Рядом с выбором даты могут быть доступны быстрые действия:
+
+- `Сегодня` — переход к текущей дате;
+- `Текущий год` — переход к периоду с начала года до текущей даты.
+
+## Что проверять перед сохранением
+
+Перед тем как нажать `Сохранить отчет`, проверьте:
+
+- правильность отделения;
+- правильность даты или периода;
+- корректность числовых значений;
+- заполнение обязательных полей;
+- список нежелательных событий;
+- пациентов на контроле, если они должны быть указаны.
+
+## Как сохранить отчет
+
+1. Заполните доступные поля.
+2. Проверьте предупреждения на странице.
+3. Нажмите `Сохранить отчет`.
+
+Если форма заполнена корректно, система сохранит отчет и покажет сообщение об успешном сохранении.
+
+## Когда кнопка сохранения может быть недоступна
+
+Кнопка `Сохранить отчет` может отсутствовать или быть недоступной, если:
+
+- у пользователя нет права на сохранение;
+- отчет открыт только для просмотра;
+- выбран период, для которого редактирование не предусмотрено;
+- отчет уже создан и текущий пользователь не является ответственным за его заполнение.
+
+## Нежелательные события
+
+На странице сводной можно работать с нежелательными событиями.
+
+Обычно пользователь может:
+
+- просмотреть список событий;
+- добавить новое событие;
+- отредактировать существующее;
+- удалить событие, если это разрешено.
+
+Количество нежелательных событий отображается рядом с соответствующей кнопкой.
+
+## Пациенты на контроле
+
+В системе предусмотрен раздел для пациентов, требующих дополнительного внимания.
+
+Пользователь может:
+
+- просматривать список пациентов на контроле;
+- добавлять комментарии;
+- удалять пациента из контроля, если необходимость отпала.
+
+## Если данные выглядят неверно
+
+Если в сводной отображаются неожиданные данные:
+
+1. Проверьте выбранный диапазон дат.
+2. Убедитесь, что выбрано нужное отделение.
+3. Проверьте, не открыта ли уже существующая сводная за другой день.
+4. Обновите страницу и снова выберите дату.
+5. Если проблема сохраняется, сообщите администратору, указав:
+ - отделение;
+ - дату или период;
+ - что именно отображается неверно.
diff --git a/docs/statistics.md b/docs/statistics.md
new file mode 100644
index 0000000..8757e5c
--- /dev/null
+++ b/docs/statistics.md
@@ -0,0 +1,108 @@
+# Статистика
+
+Раздел статистики помогает просматривать показатели по отделениям за выбранную дату или период.
+
+## Для чего нужен раздел
+
+В разделе статистики можно:
+
+- оценить показатели работы отделений;
+- сравнить данные по нескольким отделениям;
+- открыть сводную конкретного отделения;
+- просмотреть нежелательные события;
+- посмотреть пациентов на контроле;
+- выгрузить таблицу в Excel.
+
+## Как открыть статистику
+
+1. На стартовой странице нажмите `Статистика моего отделения`.
+2. В верхней части страницы выберите дату или период.
+3. Дождитесь обновления таблицы.
+
+## Как работает выбор периода
+
+В верхней части страницы расположен календарь.
+
+С его помощью можно выбрать:
+
+- одну дату;
+- диапазон дат.
+
+После выбора периода система пересчитывает статистику по выбранному интервалу.
+
+### Быстрые действия
+
+Для быстрого перехода могут использоваться кнопки:
+
+- `Сегодня`;
+- `Текущий год`.
+
+Они помогают быстро открыть самые частые сценарии просмотра данных.
+
+## Что отображается в таблице
+
+В таблице статистики могут присутствовать следующие показатели:
+
+- отделение;
+- количество коек;
+- поступило;
+- выбыло;
+- состоит;
+- средний койко-день;
+- предоперационный койко-день;
+- процент загруженности;
+- процент летальности;
+- количество операций;
+- число умерших;
+- количество медицинского персонала.
+
+В отдельных строках система может показывать:
+
+- групповые заголовки;
+- итоговые строки;
+- индикатор наличия отчета по отделению.
+
+## Как открыть сводную из статистики
+
+1. Найдите нужное отделение в таблице.
+2. Нажмите на его название.
+3. Система откроет страницу сводной по выбранному отделению.
+
+При переходе сохраняется выбранный период, чтобы вы сразу видели данные за тот же интервал.
+
+## Нежелательные события и пациенты на контроле
+
+Напротив отделения могут отображаться дополнительные индикаторы:
+
+- значок нежелательных событий;
+- значок пациентов на контроле.
+
+Нажатие на них открывает отдельное окно с подробностями.
+
+## Выгрузка в Excel
+
+Чтобы скачать статистику:
+
+1. Выберите нужную дату или период.
+2. Нажмите `Сохранить в Excel`.
+3. Дождитесь скачивания файла.
+
+Файл будет сформирован по текущему периоду, который выбран на странице в момент выгрузки.
+
+## Как правильно интерпретировать данные
+
+Перед анализом статистики убедитесь, что:
+
+- выбран корректный период;
+- вы находитесь в нужной роли;
+- данные относятся к нужному отделению;
+- отчет за интересующую дату уже создан, если вы ожидаете увидеть итоговые значения по завершенному дню.
+
+## Когда данные могут отличаться от ожидаемых
+
+Чаще всего причина в одном из следующих факторов:
+
+- выбран не тот диапазон дат;
+- открыта статистика по другому отделению;
+- отчет за нужную дату еще не сформирован;
+- пользователь работает под другой ролью.
diff --git a/resources/css/app.css b/resources/css/app.css
index 3281ba9..7ac934d 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -37,6 +37,38 @@
font-weight: 800;
src: url("/fonts/Golos-Text_Black.woff2");
}
+
+ body {
+ background-image:
+ radial-gradient(
+ circle at top center,
+ color-mix(in oklch, var(--primary) 10%, transparent) 0,
+ transparent 34rem
+ ),
+ radial-gradient(
+ circle at bottom left,
+ color-mix(in oklch, var(--chart-2) 12%, transparent) 0,
+ transparent 26rem
+ );
+ min-height: 100vh;
+ }
+}
+
+@layer utilities {
+ .grid-overlay {
+ background-image:
+ linear-gradient(
+ to right,
+ color-mix(in oklch, var(--color-neutral-800) 50%, transparent) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ color-mix(in oklch, var(--color-neutral-800) 45%, transparent) 1px,
+ transparent 1px
+ );
+ background-size: 40px 40px;
+ }
}
@theme {
diff --git a/resources/js/Components/StartButton.vue b/resources/js/Components/StartButton.vue
index f6d0270..12fbb72 100644
--- a/resources/js/Components/StartButton.vue
+++ b/resources/js/Components/StartButton.vue
@@ -1,5 +1,6 @@
-
+ {{ d }}
+
+