# UPSKILLS

Платформа корпоративного обучения (Laravel + React).

## Требования

- PHP 8.2+
- Composer, Node.js
- MySQL / MariaDB

## Artisan-команды

Все команды запускаются из корня проекта:

```bash
php artisan <команда> [опции]
```

### Дедлайны и уведомления

#### `deadlines:send-notifications`

Отправляет email-напоминания о дедлайнах (за 10, 5 и 1 день до срока). Одно письмо на пользователя со списком всех подходящих курсов. Сбрасывает флаги уведомлений, если курс завершён или дедлайн просрочен (без удаления журнала).

| Опция | Описание |
|-------|----------|
| `--dry-run` | Прогноз: сколько писем уйдёт, без отправки и без изменений в БД |

```bash
php artisan deadlines:send-notifications
php artisan deadlines:send-notifications --dry-run
```

**Cron:** каждые 10 минут (`routes/console.php`).

---

#### `deadlines:check`

Диагностика дедлайнов в консоли: список пользователей, дат дедлайна и предупреждений, статусы (просрочен / предупреждение активно).

| Опция | Описание |
|-------|----------|
| `--course-id=` | Только указанный курс |
| `--overdue` | Только просроченные |
| `--warning` | Только с активным окном предупреждения |

```bash
php artisan deadlines:check
php artisan deadlines:check --course-id=20 --overdue
```

---

#### `deadlines:recalculate`

Пересчитывает `deadline_at` и `warning_at` в `course_user_progress` по настройкам курса.

| Опция | Описание |
|-------|----------|
| `--course-id=` | Только указанный курс; без опции — все курсы с включённым дедлайном |

```bash
php artisan deadlines:recalculate
php artisan deadlines:recalculate --course-id=20
```

---

### Прогресс и завершение курсов

#### `progress:sync-completed-at`

Проставляет `completed_at`, если курс **фактически** пройден (все опубликованные уроки и сданные тесты). Не доверяет слепо полю `percent` — пересчитывает по данным. По умолчанию только просмотр; для записи в БД нужен `--execute`.

| Опция | Описание |
|-------|----------|
| `--execute` | Записать изменения (без флага — только таблица и статистика) |
| `--user-id=` | Только указанный пользователь |
| `--company-id=` | Только пользователи компании |
| `--min-percent=100` | Кандидаты: `percent` в БД не ниже значения |
| `--limit=500` | Максимум записей за запуск |
| `--include-without-assignment` | Включить прогресс без назначения в `course_user` |

```bash
# Сначала просмотр
php artisan progress:sync-completed-at --user-id=71

# Затем применение
php artisan progress:sync-completed-at --user-id=71 --execute

# По всей базе порциями
php artisan progress:sync-completed-at --limit=500
php artisan progress:sync-completed-at --limit=500 --execute
```

---

### Тесты

#### `tests:close-stale-attempts`

Завершает «зависшие» попытки тестов: истёк таймер теста или с начала прошло более 10 часов. Использует общую логику `finish` (ответы, балл, пересчёт прогресса).

| Опция | Описание |
|-------|----------|
| `--dry-run` | Показать, что будет закрыто, без изменений |
| `--user-id=` | Только указанный пользователь |
| `--limit=500` | Максимум попыток за запуск |

```bash
php artisan tests:close-stale-attempts --dry-run
php artisan tests:close-stale-attempts
php artisan tests:close-stale-attempts --user-id=2
```

**Cron:** каждый час (`routes/console.php`).

---

### Пользователи

#### `users:fix-last-activity`

Исправляет `last_activity` у пользователей: если дата в будущем или `null`, ставит текущее время. Разовая служебная команда.

```bash
php artisan users:fix-last-activity
```

---

### Прочее

#### `app:cleanup-orphaned-files`

Заготовка для очистки файлов без привязки в БД. **Логика не реализована** (пустой `handle`).

```bash
php artisan app:cleanup-orphaned-files
```

---

### Назначения курсов (отделы / группы)

#### `courses:snapshot`

**Только чтение.** Счётчики `course_user`, прогресса, дедлайнов и «осиротевшего» прогресса. Запускайте **до и после** деплоя на прод, чтобы убедиться, что данные не пропали.

```bash
php artisan courses:snapshot
php artisan courses:snapshot --output=/tmp/upskills-before.json
```

#### `courses:sync-member-assignments`

Синхронизирует `course_user` с каталогами `course_department` и `course_group` (идемпотентно, дубликаты не создаёт).

| Опция | Описание |
|-------|----------|
| `--company-id=` | Только одна компания |
| `--restore-deadlines` | Проставить дедлайн только там, где есть назначение, но `deadline_at` пустой (существующие даты **не затирает**) |

```bash
php artisan courses:sync-member-assignments
php artisan courses:sync-member-assignments --restore-deadlines
```

**Не пересчитывает** уже установленные дедлайны. Не удаляет прогресс.

---

## Бэкапы (Duplicati)

Прод: UI на **https://bk.upskills.net**, регламент daily/monthly и безопасная установка — см. [`ops/duplicati/SETUP.md`](ops/duplicati/SETUP.md).

Перед деплоем по-прежнему нужен явный дамп БД (ниже) или свежий daily из Duplicati.

## Деплой на прод: не потерять прогресс и дедлайны

### 1. Резервная копия БД (обязательно)

```bash
mysqldump -u USER -p DATABASE > upskills_backup_$(date +%Y%m%d_%H%M).sql
```

Храните дамп до проверки после деплоя.

### 2. Снимок до деплоя

```bash
php artisan courses:snapshot --output=/tmp/upskills-before.json
```

Запомните числа: `course_user`, `progress_with_deadline`, `orphan_progress`.

### 3. Миграции

```bash
php artisan migrate
```

Миграции `course_department` / `course_group` **только копируют** уникальные пары курс↔отдел/группа из `course_user` в новые таблицы. Строки `course_user` и `course_user_progress` **не удаляются**.

### 4. Деплой кода

Обычный выклад (git pull, composer, build фронта и т.д.).

### 5. Сразу после деплоя (до массового редактирования отделов в UI)

```bash
php artisan courses:sync-member-assignments
php artisan courses:snapshot --output=/tmp/upskills-after.json
```

Сравните `before` и `after`: `course_user` и `progress_with_deadline` не должны **сильно** уменьшиться.

Если у части учеников дедлайн пропал в UI, но назначение есть — осторожно:

```bash
php artisan courses:sync-member-assignments --restore-deadlines
```

(восстанавливает только **пустые** `deadline_at` при наличии `course_user`).

### 6. Чего избегать на проде сразу после выкладки

- Массово пересохранять отделы/группы в админке **до** `courses:sync-member-assignments` — старый код мог снять `course_user`; новый код при снятии **сохраняет прогресс**, но может обнулить дедлайн, если назначений не осталось.
- Не запускать `deadlines:recalculate` без необходимости — пересчитает даты по старым правилам курса, а не «как было вручную».
- Снятие курса у ученика вручную с удалением прогресса — по-прежнему удаляет запись прогресса (это отдельное действие в UI).

### 7. Если что-то пошло не так

Восстановление из дампа:

```bash
mysql -u USER -p DATABASE < upskills_backup_YYYYMMDD_HHMM.sql
```

---

## Планировщик (cron)

На сервере должен выполняться планировщик Laravel:

```cron
* * * * * cd /var/www/html/upskills.net && php artisan schedule:run >> /dev/null 2>&1
```

Зарегистрированные задачи (`routes/console.php`):

| Команда | Расписание |
|---------|------------|
| `tests:close-stale-attempts` | Каждый час |
| `deadlines:send-notifications` | Каждые 10 минут |

Просмотр расписания:

```bash
php artisan schedule:list
```

---

### Администрирование пользователей

#### Вход под пользователем (из `/settings`)

В форме редактирования ученика кнопка **«Войти»** вызывает:

```bash
POST /api/admin/users/{id}/impersonate
```

Доступно при праве `edit_users`, только внутри своей компании, нельзя войти под собой. В журнал Laravel пишется `Admin impersonation`.

Чтобы снова работать как администратор — выйти из системы и войти под своей учётной записью.

---

## Полезное

```bash
# Список всех команд проекта
php artisan list

# Справка по команде
php artisan help deadlines:send-notifications
```
