- ApiClient теперь обрабатывает 401 и 403 ошибки для обновления токена
- Добавлен JWTRefreshMiddleware для проактивного обновления токенов за 5 минут до истечения
- Добавлена настройка JWT_REFRESH_THRESHOLD_MINUTES (по умолчанию 5)
- Исправлена проблема с периодическими 403 ошибками после истечения access token
Date: Mon, 22 Sep 2025 10:30:00 +0300
Subject: [requests] Drawer «Рассмотреть», корректный чекбокс «Черновик», дружелюбные даты,
robust-обработка ошибок reject, и глобальный бейдж непрочитанных чатов
- Список заявлений:
- вместо россыпи кнопок добавлен аккуратный выезжающий блок действий (collapse drawer)
по клику «Рассмотреть»; компактные нейтральные стили, мягкая анимация.
- в модале «Изменить» добавлена галочка «Сохранить как черновик» с правильной
семантикой: hidden `_save_as=submit` по умолчанию + checkbox `_save_as=draft`.
Теперь снятие галочки действительно снимает draft.
- улучшены стили и структура разметки.
- Даты:
- на фронт-обёртке нормализуем datetime-поля (created_at/updated_at/decided_at)
в человекочитаемый вид, чтобы не светить сырые ISO 8601.
- Обработка ошибок:
- фикс падения при попытке отклонить уже одобренную заявку: ответ 400 у DRF
может приходить списком ошибок; теперь сообщение извлекается безопасно.
- Чаты:
- синхронизировали название ключа контекста с проектом (`chat_unread_total`);
- бейдж «Чаты» обновляется глобальным событием `chats:unread-total`, даже если
список чатов не открыт (fallback — суммирование счётчиков на странице).
Примечание: пути к шаблонам могут отличаться в вашем проекте — ниже даны
типовые варианты (`backend/requests_app/templates/...`). Если у вас другой
layout, внесите изменения в соответствующие файлы шаблонов.
Большое обновление календаря. Объединили модель событий, добавили полноценную повторяемость (hourly/daily/weekly/monthly/annual), материализацию вхождений в списке, навели порядок с правами отделов и обновили фронтовой шаблон. Покрыли API pytest-тестами и починили мелкие баги.
### Ключевые изменения
**Модель**
* Заменили `CompanyEvent` и `DepartmentEvent` на единую **`CalendarEvent`**:
* `department` (nullable FK) — если задан, событие отдела; если `NULL`, событие компании.
* Даты/время: `start_date`, `end_date`, `start_time`, `end_time`, `all_day`.
* Повторяемость: `recurrence` ∈ {`one_time`, `hourly`, `daily`, `weekly`, `monthly`, `annual`}.
* Параметры повторов: `recurrence_interval` (int>0), `recurrence_until` (date), `recurrence_count` (int), `weekdays_mask` (bitmask для weekly).
* Индексы под выборки и сортировку; check-constraint `cal_event_date_time_range_valid` (валидность диапазонов даты/времени; для одного дня `start_time <= end_time` при наличии обоих).
* Поля оформления: `title`, `description`, `color`, `location`, `created_by`, `created_at`.
* Правка по умолчаниям: `weekdays_mask` теперь с безопасным default=0 (исправляет `NOT NULL` в SQLite).
**Сервисы**
* Добавлен сервис `expand_event_occurrences(event, range_start, range_end, max_instances=1000)`:
* Материализует все вхождения события в окне `[range_start, range_end)`.
* Корректно обрабатывает: leap day (29 Feb), месяцы с «дырками» (31-е), еженедельные маски, ежечасные интервалы.
* Выдаёт ровные границы дат/датавремени для FullCalendar.
**Сериализаторы**
* `CalendarEventWriteSerializer` — строгая валидация:
* Для hourly требуется `start_time` и `end_time`.
* Для weekly — пустой/непустой `weekdays` валидируется и мапится в `weekdays_mask`.
* `recurrence_interval` > 0; взаимоисключение `recurrence_until` и `recurrence_count`.
* `CalendarEventSerializer` — read-вариант с плоскими полями для фронта.
* Доп. сериализатор вьюхи для списка не нужен — материализуем в контроллере (см. ниже).
**ViewSet / API**
* Новый единый роут: **`/api/v1/calendar/events/`** (`ModelViewSet`):
* `list`: возвращает **материализованные** вхождения в окне `[start, end)` (query-параметры `YYYY-MM-DD`). Если передан `department_id` — фильтрует события отдела, иначе — компании.
* `retrieve/create/update/partial_update/destroy`: операции над самим `CalendarEvent`.
* `get_queryset`: для `list` фильтрует по department/company, для detail — полный QS.
* При `create`: подставляет `department_id` либо из контекста/параметров, либо из body, проставляет `created_by`, чистит кеш.
* **Права**:
* Используется комбинированный пермишен **`ManageCalendarPerm(AdminOrDeptAllowed)`**.
* Для компании делегирует на `AdminOrActionOrModelPerms`.
* Для отдела требует **строковый код**: `"manage_department_events"`.
* Обновляет инстанс пользователя из БД (сбрасывает кэши), чтобы в одном тесте «выдали право → сразу работает».
* `get_permissions()` → `[IsAuthenticated(), ManageCalendarPerm()]`.
* Починки:
* Подключены `parse_date`/`status` где нужно.
* Исправлен конфликт дат/датавремени в материализации.
* Возврат полей в `list`: `start`, `end`, `allDay`, `color`, `recurrence`, `department_id` (ID родителя можно оставить как `event.id`).
**Signals**
* Добавлен сигнал на изменение `date_of_birth` сотрудника:
* Создаёт/обновляет/удаляет соответствующее ежегодное событие в **глобальном** календаре.
* Идентификация через `source="employee_birthday"` + `source_ref=<employee_id>`.
**Админка**
* `CalendarEventAdmin`:
* Поиск по названию/описанию/локации.
* Фильтры по отделу, типу повторения, all\_day, наличию времени, «company vs department».
* Инлайнов нет; list-представление упрощено (исправлены ошибки `admin.E116` — фильтры теперь валидные `SimpleListFilter`/поля).
**Фронт (шаблон правой колонки)**
* Переписан `<script>` под единый эндпоинт:
* Список: GET `/api/v1/calendar/events/?[department_id=...]&start=YYYY-MM-DD&end=YYYY-MM-DD`.
* Создание:
* Компания: POST `/api/v1/calendar/events/` с `{title, start_date, [end_date], all_day}`.
* Отдел: POST туда же, но с `department_id` (в query или в body).
* Если указано время — **оба** (`start_time`, `end_time`) обязательны; иначе сохраняем как all-day.
* Нормализация дат в локали, аккуратные конвертации для FullCalendar.
* Заголовки авторизации: `Authorization: Bearer <token>` из `<meta name="api-access">`/`localStorage.api.access`. Добавлен fallback на CSRF-куку при отсутствии токена (для SessionAuth).
* Улучшен UI выбора отдела (`/api/v1/departments/my-departments/`), подписи, недельная лента.
**Тесты**
* Добавлены/актуализированы pytest-тесты API:
* Роуты и методы (CRUD).
* Позитив/негатив создания (валидация дат/времени/повторов).
* Изменение повторяемости (weekly → daily и пр.).
* Материализация (hourly alignment, weekly mask, monthly edge-cases, leap day).
* Права: отдел без/с `manage_department_events` (фикс за счёт строкового кода).
* Сигналы: апсерт/удаление дней рождения.
* Исправлены прежние падения: `NOT NULL weekdays_mask`, `NameError(parse_date/status)`, сравнения date/datetime, несоответствие `all_day` vs `allDay`, и др.
### Совместимость и заметки по деплою
* **BREAKING**: единый эндпоинт `/api/v1/calendar/events/` заменяет старые `/company-events/` и `/department-events/`. Если есть сторонние клиенты — переориентировать на новый.
* Миграции обязательны (новая таблица, индексы, дефолты).
* Если где-то ещё использовались константы прав — убедиться, что везде строка `"manage_department_events"` (тесты ориентируются на неё).
* Для фронта: убедиться, что реально прокидывается access-токен в `<meta name="api-access">` **или** есть валидная CSRF-кука при SessionAuth. Иначе POST вернёт 401.
### Почему так
* Единая модель/роут упрощают код и устраняют разъезд схем компании/отделов.
* Материализация в контроллере избавляет фронт от логики повторов и держит поведение единообразным.
* Явная строка кода права синхронизирована с тестовой выдачей пермишена и реальным хранилищем прав отделов.
* Валидация сериализатора закрывает большую часть негативных кейсов на уровне API.
- **EmployeeActionSerializer**
- Добавлен `history[]` со снапшотами simple-history и diff по полям: `action`, `date`, `comment`, `extra`, `employee_id`.
- Локализация названий событий через `employees.constants.ACTION_CHOICES`.
- Поля `action_display`, `date_display` для удобного фронтового рендера.
- **EmployeeSerializer**
- Возвращает `actions` (массив событий) с историей, **только** если в контексте выставлено `include_action_history=True`.
- Навыки: чтение — `skills` как список объектов `{id, name}`, запись — через `skills_ids`.
- Должность: чтение — объект `{id, name}`, запись — через `position_id`.
- Отделы: добавлен флаг `is_head` (сотрудник — руководитель отдела).
- **EmployeeListSerializer**
- Те же правки для навыков/должности и `is_head`, без `actions` (для производительности).
- **Views (EmployeeViewSet)**
- `get_queryset`: префетч `actions` добавляется **только** для `retrieve/me`, иначе не тянем историю.
- `get_serializer_context`: для `retrieve` и `me` выставляем `include_action_history=True`.
- Экшен `me`: выборка с `Prefetch("actions", order_by -date)` и передачей `include_action_history=True`.
- **Разное**
- Используем `ACTION_CHOICES` для «человеческих» названий событий в истории.
- `HistoryActionMixin` оставлен без изменений; история объектов доступна по `/{basename}/{pk}/history/`.
Почему так
- История кадровых событий бывает тяжёлой → грузим и отдаём её только там, где нужно (`retrieve/me`).
- Явное разделение read/write для навыков и должности упрощает фронту жизнь и убирает двусмысленность.
- `is_head` позволяет фронту не вычислять этот флаг отдельно.
Файлы
- `backend/api/v1/employees/serializers.py`
- `backend/api/v1/employees/views.py`
- (использованы константы из) `backend/employees/constants.py`
Тесты/проверка
1) GET `/api/v1/employees/1/` → поле `actions[]` заполнено, внутри каждого элемента `history[]` со снапшотами и переводами событий.
2) GET `/api/v1/employees/?page=1` → `actions` отсутствуют (по замыслу), навыки и должность приходят в «красивом» виде.
3) GET `/api/v1/employees/me/` → аналогично `retrieve`, с историей.
* UI (templates/employees/employee\_detail.html)
* Навыки: больше не итерируем по строке, выводим объекты `{id,name}` (`s.name`).
Защита от пустых списков.
* Отделы: убрал «аватарки» у элементов списка, оставил иконку у кнопки раздела.
Роль сотрудника показывается «Название отдела — Роль».
Для руководителя добавлен понятный бейдж с иконкой `bi-award-fill` + tooltip «Руководитель отдела».
* Кадровые события: добавлен раздел «Кадровые события» (для аутентифицированных).
Отрисовывает список всех событий (дата, комментарий, extra), а также историю каждого события (`action.history`) в `<details>`.
В истории показываем только название события и новые значения полей (без «создано/изменено» и без зачёркнутых старых значений).
Перевод кодов событий (`hired`, `dismissed`, …) в человекочитаемый вид согласно `employees/constants.py`.
* Аккордеон: кнопка и выпадающая панель объединены в один блок `.ios-acc` единой ширины.
Добавлена разделительная линия между заголовком и контентом (`.ios-acc-body { border-top: ... }`).
Исправлены дёрганья при анимации: убраны внешние отступы у `.collapse`, возврат стандартной скорости Bootstrap (`.35s ease`), `overflow:hidden` на фазе `collapsing`.
* Views (employees/views.py — веб-приложение)
* В `employee_detail` и `employee_me` дополнительно запрашиваем полную историю кадровых событий:
`GET v1/employee-actions/?employee=<id>&ordering=-date` и передаём в шаблон `employee_actions`.
Тем самым отображаем все события, а не только последнее.
* Шаблон умеет брать события из `employee_actions`, а при отсутствии — из `emp.actions`.
* API (backend/api/v1/employees/serializers.py)
* `EmployeeActionSerializer`: добавлен `history` (MethodField) с формированием массива изменений из `HistoricalRecords` (без старых значений; только `changes[field].new`, дата/пользователь).
Добален `action_display` и `date_display` для удобного рендера на фронте.
* `EmployeeSerializer`: возвращает `departments` через `get_departments()` (id, name, role\_id, role\_name, is\_head) и `skills` как массив объектов `{id,name}`.
* Прочее/фиксы
* Безопасные проверки на пустые коллекции (контакты, навыки, отделы, события).
* Устранена ошибка шаблона «Failed lookup…» при попытке обратиться к отсутствующим ключам.
* Не изменяем скорость анимации: возвращён дефолт Bootstrap; корректное закрытие панелей.
Refs: employees/constants.py (ACTION\_CHOICES), templates/employees/employee\_detail.html, employees/views.py, api/v1/employees/serializers.py
### Что сделано
**Position API**
* Добавлен `PositionViewSet` с CRUD и экшенами:
* `POST /positions/{id}/set_groups`
* `POST /positions/{id}/add_groups`
* `POST /positions/{id}/remove_groups`
* `GET /positions/{id}/permissions` (агрегирует permissions из связанных групп)
* Чтение: `IsAuthenticated`; запись: staff/superuser **или** наличие модельных прав (`employees.add|change|delete_position`), экшены групп — кастомное право `employees.assign_position_groups`.
* Выставление требуемого права через `view.required_perm_code` в `get_permissions()`.
* Поиск/сортировка: `?search=`, `?ordering=`.
* **Отключена пагинация** (возвращаем plain list, как ожидают тесты).
* Сериализатор включает `groups` и `groups_verbose`.
* Роутер: зарегистрирован `positions` в `api:api_v1`.
**Кастомный пермишен для Position**
* В `Position.Meta.permissions` добавлен `("assign_position_groups", "Can assign auth groups to positions")`.
* В `employees.apps.EmployeesConfig.ready()` добавлен `post_migrate`-хук для гарантированного создания этого пермишена (страхует тестовую БД).
**DepartmentRole API**
* Добавлен `DepartmentRoleViewSet` (CRUD + экшены управления правами роли):
* `GET /department-roles/{id}/perms`
* `POST /department-roles/{id}/set_perms`
* `POST /department-roles/{id}/add_perms`
* `POST /department-roles/{id}/remove_perms`
* Фильтр: `?department=<id>` (+ безопасная обработка нечислового значения → пустой список).
* Права:
* CRUD: требуется `manage_department` (через главу отдела, роль с правом, staff/superuser **или** глобальное модельное право `employees.manage_department`);
* управление правами роли: `assign_department_role` (аналогично).
* Сериализатор `DepartmentRoleSerializer` с полями `permissions` и `permissions_verbose`.
**Пермишены и проверки доступа**
* `IsDeptManagerForWrite`:
* Научили работать с объектами, у которых есть `obj.department` (например, `DepartmentRole`).
* Добавили поддержку **глобальных** модельных прав (`user.has_perm("employees.<code>")`), чтобы уполномоченные пользователи могли управлять **любым** отделом.
* `IsStaffOrHasModelPerm`:
* Перед `user.has_perm(...)` сбрасываем кэш прав (`_perm_cache`, `_user_perm_cache`, `_group_perm_cache`) — важно для тестов, где права выдают в ходе сценария.
**Department.save(): членство и права главы**
* Перевели создание/активацию линка `EmployeeDepartment` для **нового** главы на **синхронный** режим (без `on_commit`) при **смене** главы.
* **Не** создаём линк на первичном создании отдела с уже заданным `head` (чтобы не ловить `UNIQUE` в кейсах, где тесты добавляют линк вручную).
* При смене главы:
* для **нового** главы — гарантируем наличие активной ссылки (`is_active=True`, `date_from` при необходимости);
* для **старого** главы:
* если `head -> None` — гарантируем наличие ссылки и деактивируем её (`is_active=False`, проставляем `date_to`);
* если смена `A -> B` — гарантируем, что ссылка старого главы существует и остаётся активной (бизнес-логика и тесты).
* Таким образом, права «главы» корректно переходят и отражаются в ссылках членства.
**Маршрутизация**
* Зарегистрированы в `api/v1/urls.py`:
* `positions`
* `department-roles`
**Тесты**
* **positions:** полный набор на права, экшены групп, поиск/сортировку, валидацию payload’ов; фикс хелпера телефонов (уникальные номера).
* **department-roles:** основной набор + расширенный (`extra`) на:
* роли с правами внутри отдела (manage/assign),
* глобальные модельные права (доступ к любому отделу),
* кросс-доступ (роль в отделе A не даёт прав в отделе B),
* семантику `set_perms` (полная замена, дедуп), идемпотентность `add/remove`,
* валидацию payload’ов, 404 при удалении несуществующей роли.
* **department\_head\_rights:** сценарии смены/снятия главы:
* старый глава теряет права (если нет роли),
* новому создаётся активная ссылка и права работают сразу,
* снятие головы → деактивация ссылки + `date_to`,
* у старого главы права сохраняются, если назначена роль с `manage_department`,
* смена головы не деактивирует ссылку старого главы по умолчанию.
* Все тесты зелёные.
### Технические детали / файлы
* `employees/models.py` — правки `Position.Meta.permissions`, логика `Department.save()`.
* `employees/apps.py` — `post_migrate` для `assign_position_groups`.
* `employees/permissions.py` — обновлён `IsDeptManagerForWrite`, `IsStaffOrHasModelPerm`.
* `api/v1/employees/serializers.py` — `PositionSerializer`, `DepartmentRoleSerializer`.
* `api/v1/employees/views.py` — `PositionViewSet` (без пагинации), `DepartmentRoleViewSet` (CRUD + экшены), безопасный фильтр `department`.
* `api/v1/urls.py` — регистрация роутеров.
* `tests/api/v1/employees/` — новые файлы тестов и доработки существующих.
### Миграции/заметки
* Требуется миграция для добавления `assign_position_groups` в `Position.Meta.permissions`.
* `post_migrate` гарантирует наличие права в тестовой БД.
* После деплоя: `python manage.py migrate`.
---
Таким образом, добавлены полноценные API для **Position** и **DepartmentRole**, корректно выстроена модель прав (глава/роль/глобальные), а сценарии смены/снятия главы полностью покрыты тестами.
**Что сделано**
* **API**
* Добавлен `POST /api/v1/auth/register/` — создание пользователя, генерация кода и письмо (повторная отправка, если email есть, но не подтверждён).
* `POST /api/v1/auth/verify-email/` возвращает `user_id` при успехе.
* `POST /api/v1/auth/resend-email/` — без изменений по контракту.
* **Фронт (views\_front)**
* `RegisterView.form_valid` теперь вызывает API `auth/register` (без прямой работы с ORM).
* `EmailVerifyView.post` переводён на API `auth/verify-email` + автологин при успехе.
* Повторная отправка кода (`resend_email`) уже через API, добавлена защита от частых запросов по сессии.
* Исправлена сериализация телефона (E.164/строка) при отправке в API.
* **Клиент API**
* Новый модуль `employees/api_client.py`: безопасная сериализация payload, удобные ошибки (`api_bad_json status=… body=…`) и абсолютные URL через `request.build_absolute_uri`.
* **Middleware**
* `AuthRequiredMiddleware` теперь **пропускает**: `/api/v1/**`, статику/медиа, страницы логина/регистрации/сброса пароля, админ-логин и `OPTIONS`. Остальное для анонима — редирект на `login`.
* **Аутентификация/выход**
* Страница подтверждения выхода (в стиле iOS bottom sheet) + `ConfirmLogoutView`. Пункт «Выйти» ведёт на GET `/auth/logout/`, далее POST подтверждения.
* Шаблон выхода принимает `next` из контекста (без обращений к `request.GET.next` в шаблоне).
* **UX/мелочи**
* Устранены 500 при отсутствии `password_reset` — добавлены/учтены URL’ы потока сброса (или безопасные ссылки).
* Уточнены сообщения об ошибках при регистрации/верификации.
**Как проверить**
1. Аноним может вызвать:
* `POST /api/v1/auth/register/` → `201 {"ok":true}` / `200 {"ok":true,"resent":true}` / `400 {"error":"email_taken"}`.
* `POST /api/v1/auth/verify-email/` с корректным кодом → `200 {"ok":true,"user_id":...}`.
* `POST /api/v1/auth/resend-email/` → `200 {"ok":true}`.
2. UI:
* Регистрация в форме → обращение к API, письмо с кодом, переход на ввод кода.
* Ввод кода → API, автологин, редирект в ленту.
* «Выйти» в меню → страница подтверждения → POST → сообщение «Вы вышли…» и редирект на логин/`next`.
3. Проверить, что `/api/v1/**` не редиректится на логин (middleware whitelist работает).
4. Телефон в форме не ломает JSON (нет `TypeError: PhoneNumber is not JSON serializable`).
**Миграции**: нет.
## Что сделано
### Модели
- **employees**
- : теперь включает отделы, где сотрудник является **руководителем**.
- Уточнены :
- → у , у .
- : добавлены удобные геттеры , ; улучшён .
- Модели , , приведены к актуальным полям (без start_date/end_date/level и т.п., чтобы не конфликтовать с формами/админкой).
- **feed**
- Вынесены константы типов постов: , , + .
- Жёсткие **CheckConstraint**:
- для департаментских постов ,
- для остальных — .
- Индексы по , , .
- **communications**
- :
- для (чаты отдела удаляются вместе с отделом).
- Метод : активные сотрудники отдела + **руководитель**; для глобального — все активные.
- Запрет удаления **основных** чатов (global/department), но обычные удаляются нормально.
### Админка
- Приведена в соответствие моделям: убраны несуществующие поля, добавлены фильтры/поиск по реальным полям, inlines для отделов (связи сотрудников).
- Исправлены ошибки system check (E033/E108/E116).
### Формы
- , : валидация аватара, корректные required/optional, PhoneNumberField для WhatsApp/WeChat.
- , (динамический queryset сотрудников, не состоящих в отделе).
- , , : соответствуют актуальным моделям, с базовой валидацией диапазонов дат (если применимо).
### Вьюхи (employees)
- Переписаны аккуратно и проще:
- Списки/детали отделов на CBV (, ), в контексте — сотрудники, новости отдела, новые сотрудники.
- Профиль и чужой профиль использует общий шаблон с подключаемыми блоками.
- : доступ только **админу/руководителю**; добавление связи и автодобавление участника (и руководителя) в **чат отдела**.
- : подготовлено к фильтрации по .
### Вьюхи (feed)
- — **универсальная** форма:
- контекст определяется автоматически из GET: | ,
- скрываем поля , чтобы пользователь не ломал контекст,
- редирект — на **красивые** страницы: главная лента / страница отдела / лента сотрудника.
- : только админ — для компании, только **руководитель отдела** — для департамента.
- : комментарии + форма добавления.
- **Добавлены** CRUD для постов и комментариев:
- , — автор/админ/руководитель отдела.
- , — только автор комментария.
### URL-ы
- : добавлены маршруты , , , .
- : все нужные эндпоинты в рабочем состоянии, на месте.
### Шаблоны
- : опрятная шапка, линк на «Все сотрудники отдела», кнопка «Пригласить» (для админа/руководителя), внизу — include **новостей отдела**.
- : приведение к общему стилю, кнопка «Новая публикация» с автоподстановкой department.
- : отображает бейдж контекста (Компания/Отдел/Личное), **одна** кнопка «Отмена», приходит из вьюхи.
- : контекстная кнопка «Назад к ленте» (определяется автоматически), кнопки **Редактировать/Удалить** (по правам), комментарии с кнопками **ред./удал.** у автора.
- Исправлены include-пути (везде , без обратных слешей).
## Почему всё ок
- Нет циклических импортов, выровнены.
- Никакой магии в шаблонах: вся логика определения контекста — во вьюхах.
- Чаты отдела удаляются вместе с отделом; основные чаты защищены от удаления.
- Учитываем **руководителя отдела** везде, где нужно (новости, чат, права).
## Что проверить локально
1) Миграции:
- python manage.py makemigrations
- python manage.py migrate
2) Создать отдел, назначить руководителя.
3) Пригласить сотрудника → автоматически попадёт в чат отдела.
4) Создать новости:
- с главной → тип company,
- со страницы отдела → тип department, поле Department скрыто,
- из ленты сотрудника → тип employee.
5) Закрепление новостей:
- компания — только staff,
- отдел — только head.
6) Редактирование/удаление постов и комментариев — по правам.
## Примечания
- Если ранее в БД были посты с типом 'personal' — вручную мигрируйте на 'employee'.
- Add birth_date, created_at/updated_at to Employee; remove username field
- Introduce EmployeeAction for tracking HR events and is_actually_active/status properties
- Create BotSubscriber model (OneToOne relation) for TG/WA binding
- Implement Aiogram-based Telegram bot:
• request contact to bind by phone or @username
• department selection keyboard from DEPARTMENTS dict
• async ORM via sync_to_async, custom filters for bound & active employees
- Update profile view & template:
• show “Bind Telegram/WhatsApp” button only when not already bound
• preview avatar on upload
- Remove registration tokens in favor of contact-based auth
- Clean up templates, forms and add avatar validation mixin