192 lines
8.0 KiB
PHP
192 lines
8.0 KiB
PHP
<?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:00–09: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 = 0;
|
||
$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';
|
||
}
|
||
}
|