Добавлен генератор отчетов мед. сестры
This commit is contained in:
192
app/Console/Commands/GenerateNurseReport.php
Normal file
192
app/Console/Commands/GenerateNurseReport.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\ReportDuty;
|
||||
use App\Models\ReportNurse;
|
||||
use App\Models\User;
|
||||
use App\Services\DateRange;
|
||||
use App\Services\DateRangeService;
|
||||
use App\Services\DutyReportService;
|
||||
use App\Services\NurseReportService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Console\Command\Command as CommandAlias;
|
||||
use Throwable;
|
||||
|
||||
class GenerateNurseReport extends Command
|
||||
{
|
||||
protected $signature = 'nurse:generate
|
||||
{--start= : Начальная дата смены (YYYY-MM-DD). По умолчанию сегодня}
|
||||
{--end= : Конечная дата смены (YYYY-MM-DD). По умолчанию равно --start}
|
||||
{--departments= : ID отделений через запятую или "all"}
|
||||
{--user= : ID пользователя для аудита (обязательно для CLI)}
|
||||
{--shift-start=09:00 : Время начала смены (HH:MM)}
|
||||
{--timezone= : Часовой пояс смены (по умолчанию config(\'app.timezone\'))}
|
||||
{--dry-run : Тестовый режим без записи в БД}
|
||||
{--skip-existing : Пропускать смены, где отчёт уже существует}';
|
||||
|
||||
protected $description = 'Пакетная генерация суточных отчётов за период по сменам (09:00–09:00)';
|
||||
|
||||
public function __construct(
|
||||
protected NurseReportService $reportService,
|
||||
protected DateRangeService $dateRangeService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$tz = $this->option('timezone') ?: config('app.timezone', 'Europe/Moscow');
|
||||
$shiftStartTime = $this->option('shift-start') ?: '09:00';
|
||||
|
||||
// 1. Валидация и парсинг дат
|
||||
$startDate = Carbon::parse($this->option('start') ?: now($tz)->format('Y-m-d'), $tz)->setTimeFromTimeString($shiftStartTime);
|
||||
$endDate = Carbon::parse($this->option('end') ?: $this->option('start') ?: now($tz)->format('Y-m-d'), $tz)->setTimeFromTimeString($shiftStartTime);
|
||||
|
||||
if ($endDate->lt($startDate)) {
|
||||
$this->error('Конечная дата не может быть раньше начальной.');
|
||||
return CommandAlias::FAILURE;
|
||||
}
|
||||
|
||||
// 2. Разрешение списка отделений
|
||||
$departments = $this->resolveDepartments($this->option('departments'));
|
||||
if ($departments->isEmpty()) {
|
||||
$this->error('Отделения не найдены.');
|
||||
return CommandAlias::FAILURE;
|
||||
}
|
||||
|
||||
// 3. Генерация массива смен
|
||||
$shifts = [];
|
||||
$current = $startDate->copy();
|
||||
while ($current->lte($endDate)) {
|
||||
$shifts[] = [
|
||||
'start' => $current->copy(),
|
||||
'end' => $current->copy()->addDay(), // 09:00 → 09:00 следующего дня
|
||||
];
|
||||
$current->addDay();
|
||||
}
|
||||
|
||||
// 4. Пользователь CLI
|
||||
$userId = $this->option('user') ? (int) $this->option('user') : null;
|
||||
if (!$userId && !$this->option('dry-run')) {
|
||||
$this->error('Для записи в БД в CLI режиме укажите параметр --user=<ID>');
|
||||
return CommandAlias::FAILURE;
|
||||
}
|
||||
|
||||
// 5. Вывод информации
|
||||
$totalTasks = count($shifts) * $departments->count();
|
||||
if ($totalTasks === 0) {
|
||||
$this->warn('Нет задач для выполнения.');
|
||||
return CommandAlias::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Период: {$shifts[0]['start']->format('Y-m-d H:i')} → {$shifts[array_key_last($shifts)]['end']->format('Y-m-d H:i')} ({$tz})");
|
||||
$this->info("Отделений: {$departments->count()}");
|
||||
$this->info("Всего смен: {$totalTasks}");
|
||||
if ($this->option('dry-run')) $this->warn("Режим DRY RUN");
|
||||
if ($this->option('skip-existing')) $this->warn("Пропуск существующих отчётов включён");
|
||||
|
||||
$progressBar = $this->output->createProgressBar($totalTasks);
|
||||
$progressBar->start();
|
||||
|
||||
$success = 0; $skipped = 0; $errors = 0;
|
||||
|
||||
// 6. Основной цикл
|
||||
foreach ($departments as $dept) {
|
||||
foreach ($shifts as $shift) {
|
||||
try {
|
||||
$status = $this->processShift(
|
||||
$shift['start'], $shift['end'], $dept, $userId,
|
||||
$this->reportService, $this->dateRangeService
|
||||
);
|
||||
|
||||
if ($status === 'skip' || $status === 'dry_run') {
|
||||
$skipped++;
|
||||
} else {
|
||||
$success++;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$errors++;
|
||||
// Безопасное получение имени отделения
|
||||
$deptName = $dept->name ?? $dept->department_name ?? "Отдел #{$dept->department_id}";
|
||||
$this->error("\n[{$deptName}] {$shift['start']->format('Y-m-d H:i')}: {$e->getMessage()}");
|
||||
Log::error('NurseReportShiftGeneration', [
|
||||
'department_id' => $dept->department_id,
|
||||
'shift_start' => $shift['start']->format('Y-m-d H:i:s'),
|
||||
'shift_end' => $shift['end']->format('Y-m-d H:i:s'),
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
} finally {
|
||||
// ✅ ГАРАНТИРУЕМ ровно 1 шаг прогресса на каждую итерацию
|
||||
$progressBar->advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine(2);
|
||||
$this->info("Завершено. Успешно: {$success} | Пропущено: {$skipped} | Ошибок: {$errors}");
|
||||
|
||||
return $errors > 0 ? CommandAlias::FAILURE : CommandAlias::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение списка отделений
|
||||
*/
|
||||
private function resolveDepartments($input)
|
||||
{
|
||||
if (!$input || strtolower($input) === 'all') {
|
||||
return Department::orderBy('department_id')->get();
|
||||
}
|
||||
|
||||
$ids = array_map('trim', explode(',', $input));
|
||||
return Department::whereIn('department_id', $ids)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка одной смены
|
||||
* @return string 'success' | 'skip' | 'dry_run'
|
||||
*/
|
||||
private function processShift(
|
||||
Carbon $shiftStart,
|
||||
Carbon $shiftEnd,
|
||||
$dept,
|
||||
int $userId,
|
||||
NurseReportService $reportService,
|
||||
DateRangeService $dateRangeService
|
||||
): string {
|
||||
$deptId = $dept->department_id;
|
||||
$misDeptId = $dept->rf_mis_department_id;
|
||||
|
||||
// Пропуск, если отчёт уже существует за ЭТУ ЖЕ СМЕНУ
|
||||
if ($this->option('skip-existing')) {
|
||||
$exists = ReportNurse::where('rf_department_id', $deptId)
|
||||
->where('period_start', $shiftStart->format('Y-m-d H:i:s'))
|
||||
->where('period_end', $shiftEnd->format('Y-m-d H:i:s'))
|
||||
->exists();
|
||||
if ($exists) {
|
||||
return 'skip';
|
||||
}
|
||||
}
|
||||
|
||||
// Тестовый режим
|
||||
if ($this->option('dry-run')) {
|
||||
return 'dry_run';
|
||||
}
|
||||
|
||||
// Формируем DateRange через ваш сервис (учёт смен, часовых поясов)
|
||||
$user = User::find($userId);
|
||||
$lpuDoctorId = 0;
|
||||
$dateRange = $dateRangeService->createDateRangeForDate($shiftEnd, $user);
|
||||
|
||||
// Цепочка из вашего контроллера
|
||||
$report = $reportService->saveReport($dateRange, $userId, $lpuDoctorId, $deptId);
|
||||
$stats = $reportService->saveSnapshot($dateRange, $report, $misDeptId, $userId);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ class NurseReportController extends Controller
|
||||
$reportsNurse = ReportNurse::where('rf_department_id', $departmentId)
|
||||
->where('period_start', '>=', $dateRange->startSql())
|
||||
->where('period_end', '<=', $dateRange->endSql())
|
||||
->where('rf_lpudoctor_id', $selectedUserId)
|
||||
->orderBy('period_end', 'desc')
|
||||
->with(['doctor'])
|
||||
->get();
|
||||
|
||||
@@ -12,17 +12,23 @@ use Illuminate\Support\Collection;
|
||||
|
||||
class NurseMedicalHistoryService
|
||||
{
|
||||
public function getGroupedHistories(DateRange $dateRange, int $departmentId, ?array $reportIds = null): array
|
||||
public function getGroupedHistories(
|
||||
DateRange $dateRange,
|
||||
int $departmentId,
|
||||
?array $reportIds = null,
|
||||
?string $search = null,
|
||||
): array
|
||||
{
|
||||
$startYear = $dateRange->startDate->copy()->startOfYear()->format('Y-m-d');
|
||||
|
||||
$periodMigrationFilter = function ($q) use ($departmentId, $dateRange, $startYear) {
|
||||
$q->where('department_id', $departmentId)
|
||||
->where('ingoing_date', '<=', $dateRange->endSql())
|
||||
->where(function ($sub) use ($dateRange, $startYear) {
|
||||
// Миграции без out_date (еще лежат)
|
||||
$sub->whereNull('out_date')
|
||||
->where('ingoing_date', '>', $startYear);
|
||||
|
||||
// Миграции с out_date (закрытые)
|
||||
$sub->orWhere(function ($sub2) use ($dateRange, $startYear) {
|
||||
$sub2->whereNotNull('out_date')
|
||||
->where('out_date', '>', $dateRange->startSql())
|
||||
@@ -30,7 +36,6 @@ class NurseMedicalHistoryService
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$departmentMigrationFilter = function ($q) use ($departmentId) {
|
||||
$q->where('department_id', $departmentId)
|
||||
->orderByDesc('ingoing_date');
|
||||
@@ -38,16 +43,26 @@ class NurseMedicalHistoryService
|
||||
|
||||
// 1. Один запрос: получаем "сырые" данные (без вычисляемых статусов)
|
||||
$all = ReportNursePatient::query()
|
||||
->when($reportIds, function ($query, $reportIds) {
|
||||
->when($reportIds !== null, function ($query) use ($reportIds) {
|
||||
return $query->whereIn('report_nurse_id', $reportIds);
|
||||
})
|
||||
->whereHas('migrations', $periodMigrationFilter)
|
||||
->when($search, function ($query, $search) {
|
||||
// Поиск по ФИО (точное совпадение или LIKE)
|
||||
return $query->where(function ($q) use ($search) {
|
||||
$q->where('full_name', 'ilike', "%{$search}%"); // PostgreSQL
|
||||
});
|
||||
})
|
||||
->with([
|
||||
'latestMigration' => $periodMigrationFilter,
|
||||
'migrations' => $departmentMigrationFilter,
|
||||
'latestMigration.operations' => function ($q) use ($dateRange) {
|
||||
$q->where('start_date', '>=', $dateRange->startSql())
|
||||
->where('start_date', '<', $dateRange->endSql()); // по start_date
|
||||
},
|
||||
'operations' => function ($q) use ($departmentId, $dateRange) {
|
||||
$q->where('department_id', $departmentId)
|
||||
->where('start_date', '>=', $dateRange->startSql())
|
||||
// ->where('start_date', '>=', $dateRange->startSql())
|
||||
->where('start_date', '<', $dateRange->endSql())
|
||||
// Только операции пока пациент реально лежал в отделении
|
||||
->whereExists(function ($sub) use ($departmentId) {
|
||||
@@ -62,15 +77,20 @@ class NurseMedicalHistoryService
|
||||
});
|
||||
});
|
||||
}
|
||||
])
|
||||
]);
|
||||
|
||||
$all = $all
|
||||
->selectRaw('DISTINCT ON (original_id) report_nurse_patients.*')
|
||||
->orderBy('original_id')
|
||||
->orderBy('report_nurse_id', 'desc')
|
||||
->get();
|
||||
|
||||
// 2. Добавляем вычисляемые поля и превращаем в плоский массив
|
||||
//dd($all->where('original_id', 334564)->first());
|
||||
|
||||
// Добавляем вычисляемые поля и превращаем в плоский массив
|
||||
$prepared = $all->map(function (ReportNursePatient $h) use ($dateRange) {
|
||||
$patientStatus = PatientStatusClassifier::classify($h, $dateRange);
|
||||
$periodFlags = PatientStatusClassifier::classifyPeriodFlags($h, $dateRange);
|
||||
$patientUrgency = null;
|
||||
$patientReanimation = null;
|
||||
if (!in_array($patientStatus, [
|
||||
@@ -79,16 +99,21 @@ class NurseMedicalHistoryService
|
||||
PatientStatusClassifier::STATUS_TRANSFERRED
|
||||
])) {
|
||||
$patientUrgency = PatientStatusClassifier::classifyUrgency($h->urgency_id);
|
||||
$patientReanimation = PatientStatusClassifier::classifyReanimation($h->latestMigration?->reanimations, $dateRange);
|
||||
}
|
||||
|
||||
return [
|
||||
// Все исходные поля модели (автоматически через toArray)
|
||||
...$h->toArray(),
|
||||
|
||||
'operations' => $h->operations,
|
||||
// + вычисляемые мета-поля для фронтенда
|
||||
'patient_status' => $patientStatus,
|
||||
'patient_urgency' => $patientUrgency,
|
||||
'period_flags' => $periodFlags,
|
||||
'in_reanimation' => $patientReanimation,
|
||||
'admitted_today' => PatientStatusClassifier::classifyAdmitted($h->latestMigration?->ingoing_date, $dateRange),
|
||||
'in_observable' => PatientStatusClassifier::classifyObservable($h->observable, $dateRange),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -97,14 +122,27 @@ class NurseMedicalHistoryService
|
||||
$sortOrder = 'desc';
|
||||
$sorted = $prepared->sortBy($sortBy, SORT_REGULAR, $sortOrder === 'desc')->values();
|
||||
|
||||
// Операции
|
||||
$operations = $sorted->map(function ($h) {
|
||||
return $h['latest_migration']['operations'];
|
||||
})->flatten(1);
|
||||
|
||||
// 4. Возвращаем плоский массив + метаданные для фронтенда
|
||||
$countInDepartment = $sorted->where('patient_status', 'in_department')->count();
|
||||
$countRecipient = $sorted->where('patient_status', 'recipient')->count();
|
||||
$countDischarged = $sorted->where('patient_status', 'discharged')->count();
|
||||
$countDeceased = $sorted->where('patient_status', 'deceased')->count();
|
||||
$countUrgent = $sorted->where('patient_urgency', 'urgent')->count();
|
||||
$countPlanned = $sorted->where('patient_urgency', 'planned')->count();
|
||||
$countInDepartment = $sorted->where('period_flags.current_at_end', true)->count();
|
||||
$countRecipient = $sorted->where('period_flags.recipient', true)->count();
|
||||
$countDischarged = $sorted->where('period_flags.discharged', true)->count();
|
||||
$countDeceased = $sorted->where('period_flags.deceased', true)->count();
|
||||
$countUrgent = $sorted
|
||||
->where('period_flags.current_at_end', true)
|
||||
->where('period_flags.urgent', true)
|
||||
->count();
|
||||
$countPlanned = $sorted
|
||||
->where('period_flags.current_at_end', true)
|
||||
->where('period_flags.planned', true)
|
||||
->count();
|
||||
$countReanimations = $sorted->where('in_reanimation', true)->count();
|
||||
$countSurgPlanned = $operations->where('urgent_status', 6)->count();
|
||||
$countSurgUrgent = $operations->whereIn('urgent_status', [4, 5])->count();
|
||||
|
||||
// 4. Возвращаем плоский массив + метаданные для фронтенда
|
||||
return [
|
||||
@@ -114,13 +152,15 @@ class NurseMedicalHistoryService
|
||||
'sortBy' => $sortBy,
|
||||
'sortOrder' => $sortOrder,
|
||||
'counts' => [
|
||||
'in_department' => $countInDepartment + $countRecipient,
|
||||
'in_department' => $countInDepartment,
|
||||
'recipient' => $countRecipient,
|
||||
'discharged' => $countDischarged + $countDeceased,
|
||||
'discharged' => $countDischarged,
|
||||
'deceased' => $countDeceased,
|
||||
'urgent' => $countUrgent,
|
||||
'planned' => $countPlanned,
|
||||
'reanimations' => $countReanimations,
|
||||
'surgical_planned' => $countSurgPlanned,
|
||||
'surgical_urgent' => $countSurgUrgent,
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user