Обновлен стартовый экран

Переписаны запросы для статистики, отчетов
Добавлена интеграция отчета сестры
This commit is contained in:
brusnitsyn
2026-05-28 22:10:00 +09:00
parent 90e0d04dfd
commit 739168d427
96 changed files with 6663 additions and 1465 deletions

View File

@@ -0,0 +1,191 @@
<?php
namespace App\Console\Commands;
use App\Models\Department;
use App\Models\ReportDuty;
use App\Models\User;
use App\Services\DateRange;
use App\Services\DateRangeService;
use App\Services\DutyReportService;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Console\Command\Command as CommandAlias;
use Throwable;
class GenerateDutyReport extends Command
{
protected $signature = 'duty: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:0009:00)';
public function __construct(
protected DutyReportService $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('DutyReportShiftGeneration', [
'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,
DutyReportService $reportService,
DateRangeService $dateRangeService
): string {
$deptId = $dept->department_id;
$misDeptId = $dept->rf_mis_department_id;
// Пропуск, если отчёт уже существует за ЭТУ ЖЕ СМЕНУ
if ($this->option('skip-existing')) {
$exists = ReportDuty::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 = $user->rf_lpudoctor_id ?? 1;
$dateRange = $dateRangeService->createDateRangeForDate($shiftEnd, $user);
// Цепочка из вашего контроллера
$report = $reportService->saveReport($dateRange, $userId, $lpuDoctorId, $deptId);
$stats = $reportService->saveSnapshot($dateRange, $report, $misDeptId, $userId);
$reportService->saveMetrics($stats, $report);
return 'success';
}
}