Добавлен генератор отчетов мед. сестры
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)
|
$reportsNurse = ReportNurse::where('rf_department_id', $departmentId)
|
||||||
->where('period_start', '>=', $dateRange->startSql())
|
->where('period_start', '>=', $dateRange->startSql())
|
||||||
->where('period_end', '<=', $dateRange->endSql())
|
->where('period_end', '<=', $dateRange->endSql())
|
||||||
->where('rf_lpudoctor_id', $selectedUserId)
|
|
||||||
->orderBy('period_end', 'desc')
|
->orderBy('period_end', 'desc')
|
||||||
->with(['doctor'])
|
->with(['doctor'])
|
||||||
->get();
|
->get();
|
||||||
|
|||||||
@@ -12,17 +12,23 @@ use Illuminate\Support\Collection;
|
|||||||
|
|
||||||
class NurseMedicalHistoryService
|
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');
|
$startYear = $dateRange->startDate->copy()->startOfYear()->format('Y-m-d');
|
||||||
|
|
||||||
$periodMigrationFilter = function ($q) use ($departmentId, $dateRange, $startYear) {
|
$periodMigrationFilter = function ($q) use ($departmentId, $dateRange, $startYear) {
|
||||||
$q->where('department_id', $departmentId)
|
$q->where('department_id', $departmentId)
|
||||||
->where('ingoing_date', '<=', $dateRange->endSql())
|
->where('ingoing_date', '<=', $dateRange->endSql())
|
||||||
->where(function ($sub) use ($dateRange, $startYear) {
|
->where(function ($sub) use ($dateRange, $startYear) {
|
||||||
|
// Миграции без out_date (еще лежат)
|
||||||
$sub->whereNull('out_date')
|
$sub->whereNull('out_date')
|
||||||
->where('ingoing_date', '>', $startYear);
|
->where('ingoing_date', '>', $startYear);
|
||||||
|
|
||||||
|
// Миграции с out_date (закрытые)
|
||||||
$sub->orWhere(function ($sub2) use ($dateRange, $startYear) {
|
$sub->orWhere(function ($sub2) use ($dateRange, $startYear) {
|
||||||
$sub2->whereNotNull('out_date')
|
$sub2->whereNotNull('out_date')
|
||||||
->where('out_date', '>', $dateRange->startSql())
|
->where('out_date', '>', $dateRange->startSql())
|
||||||
@@ -30,7 +36,6 @@ class NurseMedicalHistoryService
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$departmentMigrationFilter = function ($q) use ($departmentId) {
|
$departmentMigrationFilter = function ($q) use ($departmentId) {
|
||||||
$q->where('department_id', $departmentId)
|
$q->where('department_id', $departmentId)
|
||||||
->orderByDesc('ingoing_date');
|
->orderByDesc('ingoing_date');
|
||||||
@@ -38,16 +43,26 @@ class NurseMedicalHistoryService
|
|||||||
|
|
||||||
// 1. Один запрос: получаем "сырые" данные (без вычисляемых статусов)
|
// 1. Один запрос: получаем "сырые" данные (без вычисляемых статусов)
|
||||||
$all = ReportNursePatient::query()
|
$all = ReportNursePatient::query()
|
||||||
->when($reportIds, function ($query, $reportIds) {
|
->when($reportIds !== null, function ($query) use ($reportIds) {
|
||||||
return $query->whereIn('report_nurse_id', $reportIds);
|
return $query->whereIn('report_nurse_id', $reportIds);
|
||||||
})
|
})
|
||||||
->whereHas('migrations', $periodMigrationFilter)
|
->whereHas('migrations', $periodMigrationFilter)
|
||||||
|
->when($search, function ($query, $search) {
|
||||||
|
// Поиск по ФИО (точное совпадение или LIKE)
|
||||||
|
return $query->where(function ($q) use ($search) {
|
||||||
|
$q->where('full_name', 'ilike', "%{$search}%"); // PostgreSQL
|
||||||
|
});
|
||||||
|
})
|
||||||
->with([
|
->with([
|
||||||
'latestMigration' => $periodMigrationFilter,
|
'latestMigration' => $periodMigrationFilter,
|
||||||
'migrations' => $departmentMigrationFilter,
|
'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) {
|
'operations' => function ($q) use ($departmentId, $dateRange) {
|
||||||
$q->where('department_id', $departmentId)
|
$q->where('department_id', $departmentId)
|
||||||
->where('start_date', '>=', $dateRange->startSql())
|
// ->where('start_date', '>=', $dateRange->startSql())
|
||||||
->where('start_date', '<', $dateRange->endSql())
|
->where('start_date', '<', $dateRange->endSql())
|
||||||
// Только операции пока пациент реально лежал в отделении
|
// Только операции пока пациент реально лежал в отделении
|
||||||
->whereExists(function ($sub) use ($departmentId) {
|
->whereExists(function ($sub) use ($departmentId) {
|
||||||
@@ -62,15 +77,20 @@ class NurseMedicalHistoryService
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
])
|
]);
|
||||||
|
|
||||||
|
$all = $all
|
||||||
->selectRaw('DISTINCT ON (original_id) report_nurse_patients.*')
|
->selectRaw('DISTINCT ON (original_id) report_nurse_patients.*')
|
||||||
->orderBy('original_id')
|
->orderBy('original_id')
|
||||||
->orderBy('report_nurse_id', 'desc')
|
->orderBy('report_nurse_id', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// 2. Добавляем вычисляемые поля и превращаем в плоский массив
|
//dd($all->where('original_id', 334564)->first());
|
||||||
|
|
||||||
|
// Добавляем вычисляемые поля и превращаем в плоский массив
|
||||||
$prepared = $all->map(function (ReportNursePatient $h) use ($dateRange) {
|
$prepared = $all->map(function (ReportNursePatient $h) use ($dateRange) {
|
||||||
$patientStatus = PatientStatusClassifier::classify($h, $dateRange);
|
$patientStatus = PatientStatusClassifier::classify($h, $dateRange);
|
||||||
|
$periodFlags = PatientStatusClassifier::classifyPeriodFlags($h, $dateRange);
|
||||||
$patientUrgency = null;
|
$patientUrgency = null;
|
||||||
$patientReanimation = null;
|
$patientReanimation = null;
|
||||||
if (!in_array($patientStatus, [
|
if (!in_array($patientStatus, [
|
||||||
@@ -79,16 +99,21 @@ class NurseMedicalHistoryService
|
|||||||
PatientStatusClassifier::STATUS_TRANSFERRED
|
PatientStatusClassifier::STATUS_TRANSFERRED
|
||||||
])) {
|
])) {
|
||||||
$patientUrgency = PatientStatusClassifier::classifyUrgency($h->urgency_id);
|
$patientUrgency = PatientStatusClassifier::classifyUrgency($h->urgency_id);
|
||||||
|
$patientReanimation = PatientStatusClassifier::classifyReanimation($h->latestMigration?->reanimations, $dateRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
// Все исходные поля модели (автоматически через toArray)
|
// Все исходные поля модели (автоматически через toArray)
|
||||||
...$h->toArray(),
|
...$h->toArray(),
|
||||||
|
|
||||||
|
'operations' => $h->operations,
|
||||||
// + вычисляемые мета-поля для фронтенда
|
// + вычисляемые мета-поля для фронтенда
|
||||||
'patient_status' => $patientStatus,
|
'patient_status' => $patientStatus,
|
||||||
'patient_urgency' => $patientUrgency,
|
'patient_urgency' => $patientUrgency,
|
||||||
|
'period_flags' => $periodFlags,
|
||||||
'in_reanimation' => $patientReanimation,
|
'in_reanimation' => $patientReanimation,
|
||||||
'admitted_today' => PatientStatusClassifier::classifyAdmitted($h->latestMigration?->ingoing_date, $dateRange),
|
'admitted_today' => PatientStatusClassifier::classifyAdmitted($h->latestMigration?->ingoing_date, $dateRange),
|
||||||
|
'in_observable' => PatientStatusClassifier::classifyObservable($h->observable, $dateRange),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,14 +122,27 @@ class NurseMedicalHistoryService
|
|||||||
$sortOrder = 'desc';
|
$sortOrder = 'desc';
|
||||||
$sorted = $prepared->sortBy($sortBy, SORT_REGULAR, $sortOrder === 'desc')->values();
|
$sorted = $prepared->sortBy($sortBy, SORT_REGULAR, $sortOrder === 'desc')->values();
|
||||||
|
|
||||||
|
// Операции
|
||||||
|
$operations = $sorted->map(function ($h) {
|
||||||
|
return $h['latest_migration']['operations'];
|
||||||
|
})->flatten(1);
|
||||||
|
|
||||||
// 4. Возвращаем плоский массив + метаданные для фронтенда
|
// 4. Возвращаем плоский массив + метаданные для фронтенда
|
||||||
$countInDepartment = $sorted->where('patient_status', 'in_department')->count();
|
$countInDepartment = $sorted->where('period_flags.current_at_end', true)->count();
|
||||||
$countRecipient = $sorted->where('patient_status', 'recipient')->count();
|
$countRecipient = $sorted->where('period_flags.recipient', true)->count();
|
||||||
$countDischarged = $sorted->where('patient_status', 'discharged')->count();
|
$countDischarged = $sorted->where('period_flags.discharged', true)->count();
|
||||||
$countDeceased = $sorted->where('patient_status', 'deceased')->count();
|
$countDeceased = $sorted->where('period_flags.deceased', true)->count();
|
||||||
$countUrgent = $sorted->where('patient_urgency', 'urgent')->count();
|
$countUrgent = $sorted
|
||||||
$countPlanned = $sorted->where('patient_urgency', 'planned')->count();
|
->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();
|
$countReanimations = $sorted->where('in_reanimation', true)->count();
|
||||||
|
$countSurgPlanned = $operations->where('urgent_status', 6)->count();
|
||||||
|
$countSurgUrgent = $operations->whereIn('urgent_status', [4, 5])->count();
|
||||||
|
|
||||||
// 4. Возвращаем плоский массив + метаданные для фронтенда
|
// 4. Возвращаем плоский массив + метаданные для фронтенда
|
||||||
return [
|
return [
|
||||||
@@ -114,13 +152,15 @@ class NurseMedicalHistoryService
|
|||||||
'sortBy' => $sortBy,
|
'sortBy' => $sortBy,
|
||||||
'sortOrder' => $sortOrder,
|
'sortOrder' => $sortOrder,
|
||||||
'counts' => [
|
'counts' => [
|
||||||
'in_department' => $countInDepartment + $countRecipient,
|
'in_department' => $countInDepartment,
|
||||||
'recipient' => $countRecipient,
|
'recipient' => $countRecipient,
|
||||||
'discharged' => $countDischarged + $countDeceased,
|
'discharged' => $countDischarged,
|
||||||
'deceased' => $countDeceased,
|
'deceased' => $countDeceased,
|
||||||
'urgent' => $countUrgent,
|
'urgent' => $countUrgent,
|
||||||
'planned' => $countPlanned,
|
'planned' => $countPlanned,
|
||||||
'reanimations' => $countReanimations,
|
'reanimations' => $countReanimations,
|
||||||
|
'surgical_planned' => $countSurgPlanned,
|
||||||
|
'surgical_urgent' => $countSurgUrgent,
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user