# Прогресс разработки Hotel ERP

---

## Operations: корректная очистка комментария без type errors

### ✅ Выполнено

- В [`OperationsRepository.ts`](src/client/shared/api/repositories/OperationsRepository.ts:1) клиентская схема `patchDailyCheckinRequestSchema` синхронизирована с сервером: `comment` теперь допускает `null`.
- В [`operations.ts`](src/client/entities/operations.ts:1) обновлена нормализация `comment` при `updateCheckin()`:
  - пустая строка по-прежнему преобразуется в `null` для удаления комментария;
  - добавлена безопасная обработка `null`, чтобы не вызывать `.trim()` на nullable-значении.

### 🎯 Результат

- Очистка комментария в разделе заезда/выезда сохраняет удаление комментария и больше не ломает TypeScript typecheck.

---

## AppHeader: скрытие right zone для read-only разделов

### ✅ Выполнено

- В [`AppHeader.vue`](src/client/widgets/app-header/ui/AppHeader.vue:1) добавено определение текущего раздела через `route.meta.moduleName` с опорой на [`MENU_ITEMS`](src/client/shared/config/menuConfig.ts:1).
- Правая зона хедера (`actions`, системные инструменты и `NotificationBell`) теперь полностью скрывается, если раздел доступен роли только в режиме чтения по общей проверке [`isReadOnlyForPermissions()`](src/client/shared/config/menuConfig.ts:1).
- Для составного раздела `Текущие операции` переиспользуется существующая permission-логика по набору ресурсов, без дублирования условий в хедере.

### 🎯 Результат

- В разделах с доступом только `read` интерфейс хедера больше не показывает интерактивную правую зону и уведомления.

---

## Cleaning schedule print: разбиение по половинам месяца

### ✅ Выполнено

- Печатный вид [`CleaningScheduleTable.vue`](src/client/widgets/CleaningScheduleTable.vue:1) теперь разбит на две страницы: `1–15` и `16–конец месяца`.
- Заголовок печати приведен к формату как у графика смен, с отдельными подзаголовками для обеих половин месяца.

### 🎯 Результат

- Печатный график уборки стал визуально и логически совпадать с печатным графиком смен.

---

## Заезд/выезд: комментарий

### ✅ Выполнено

- Исправлено сохранение пустого комментария в [`DailyOperationsTable.vue`](src/client/widgets/DailyOperationsTable.vue:1).
- Ограничена высота textarea комментария диапазоном от 1 до 3 строк.
- Для длинных комментариев включен постоянный вертикальный скролл.
- Скроллбар комментария сделан постоянно видимым через локальные стили.
- Очистка комментария теперь корректно отправляет `null` и удаляет текст на сервере.
- Серверный `PATCH /operations/checkins/:id` теперь принимает `comment: null`.

---

## Cleaning schedule: прошедшие дни без серого выделения

### ✅ Выполнено

- Убрано серое выделение прошедших дней в [`CleaningScheduleTable.vue`](src/client/widgets/CleaningScheduleTable.vue:1).
- Прошедшие даты в графике уборки теперь визуально не отличаются от текущих, кроме существующих состояний `today`, `weekend` и заполненных ячеек.

### 🎯 Результат

- График уборки стал визуально единообразным по дням месяца.

---

## Sidebar permissions: скрытие раздела «Задачи» без `tasks:read`

### ✅ Выполнено

- Исправлена логика отображения пунктов меню в [`Sidebar.vue`](src/client/widgets/Sidebar.vue:1):
  - разделы теперь показываются только при наличии базового права `resource:read`;
  - роль `god` больше не может видеть «Задачи» по одному только не-read праву вроде `tasks:access`.

- Добавлен общий helper [`hasReadPermissionForItem()`](src/client/shared/config/menuConfig.ts:1) для переиспользования строгой проверки read-доступа.

### 🎯 Результат

- Модуль «Задачи» скрывается, если у пользователя нет `tasks:read`, независимо от роли.

---

## Permissions matrix UI: вертикальные вкладки по модулям

### ✅ Выполнено

- Переработана вкладка [`Матрица прав`](src/client/pages/SettingsPage.vue:1) в `SettingsPage.vue`:
  - плоская таблица с общим списком прав заменена на layout с вертикальной навигацией по секциям;
  - добавлен отдельный state `activePermissionSection` с сохранением в `localStorage`;
  - права теперь показываются только для выбранного раздела.

- Добавлена доменная группировка секций матрицы:
  - `Заметки`, `Сотрудники`, `График смен`, `Чёрный список`, `Доступ`, `Контрагенты`, `База номеров`, `Локальное оборудование`, `Инвентарь`, `Текущие операции`, `Задачи`, `Система`, `Согласования и уведомления`.

- Для секции `Текущие операции` реализованы внутренние подгруппы:
  - `Заезд/выезд` (`checkin:*`);
  - `Кассовый отчёт` (`paymaster:*`);
  - `График уборки` (`cleaning:*`).

- Уточнена визуальная принадлежность служебных прав без изменения backend-контракта:
  - `system:receive_mentions` теперь отображается в секции `Заметки` как `Получать упоминания`;
  - `approval:*` и `notifications:read` объединены в секцию `Согласования и уведомления`.

- Сохранена текущая логика работы матрицы:
  - загрузка и сохранение через существующие `/system/permissions` endpoints не менялись;
  - правила `read`-зависимых прав сохранены;
  - текущие взаимоисключения `edit` / `request_approval` не затронуты.

### ✅ Проверка

- Выполнен `npm run build` — успешно.

### 🎯 Результат

- Матрица прав стала читаемой при большом количестве настроек.
- Навигация по правам теперь соответствует реальным доменным разделам системы.
- Служебные права отображаются ближе к их фактическому назначению без миграции permission-ключей.

---

## План: минимальный рефакторинг матрицы прав (без смены структуры `resource:action`)

### 1) Таблица соответствия удаляемых прав и замен

| Удаляемое право | Статус использования | Замена | Комментарий |
|---|---|---|---|
| `tasks:read` | Не используется в guards/сервисах задач | `tasks:access` | Модуль задач уже живет на едином праве доступа |
| `paymaster:read` | Не участвует в runtime-проверках кассы | `paymaster:today_only` / `paymaster:any_date` | Доступ уже разделен по операционному дню и произвольной дате |
| `operations:delete` | Не используется в routes/service операций | `operations:manage` | Админский сценарий операций покрывается `manage` |
| `approval:read` | Не используется в approval-routes/service | — | Ключ избыточен, безопасно удалить |
| `approval:manage` | Не используется в approval-routes/service | — | Ключ избыточен, безопасно удалить |
| `notifications:read` | Не используется в runtime, встречается только в примере-комментарии | `approval:receive_notifications` | Для бизнес-уведомлений согласований уже используется точечное право |

### 2) Минимальный scope рефакторинга

- Контракт: удалить перечисленные выше ключи из `AppPermission`.
- Матрица UI: удалить эти же строки из `availablePermissions`.
- Сидер: синхронизировать `managerEnabledPermissions` и дефолтное распределение без удаленных ключей.
- UI-правила dual-edit: расширить взаимоисключение `edit` ↔ `request_approval` с `blacklist/access/contractors` на `inventory/equipment`.
- Backend-проверки: убедиться, что ссылки на удаленные ключи отсутствуют (включая комментарии-примеры).

### 3) Порядок безопасного внедрения

1. Обновить контракт и backend-ссылки на удаляемые ключи.
2. Обновить сидер прав и менеджерский default-набор.
3. Обновить матрицу прав в UI и dual-edit ограничения.
4. Прогнать проверку ролей `GOD/ADMIN/MANAGER/MAID/VIEWER` на чтение/редактирование/согласование.

### 4) Чек-лист валидации

- В `fastify.can/canAny/canAll` нет удаленных permission-key.
- В `Settings > Матрица прав` не отображаются удаленные права.
- Для `inventory` и `equipment` в UI корректно работает взаимоисключение `edit` и `request_approval`.
- `managerEnabledPermissions` не содержит «мертвых» ключей.
- `permissions/my` и sidebar/read-only не теряют доступы после пересидирования.

---

## Security hardening: XSS, cookie-policy, event payload standard, Shared UI isolation

### ✅ Выполнено

- Устранены XSS-векторы через HTML-рендер в UI:
  - удален [`v-html`](src/client/shared/ui/NotificationBell.vue:1) и внедрен безопасный рендер сегментов сообщения с бейджами без HTML-инъекции;
  - удален [`v-html`](src/client/widgets/app-header/ui/AppHeader.vue:1) для `action.icon`, рендер переведен в текстовый безопасный вывод.

- Усилена cookie-политика аутентификации:
  - в [`auth.routes.ts`](src/server/features/auth/auth.routes.ts:1) добавлен env-aware helper `getAuthCookieOptions()`;
  - для production теперь используется `secure: true` и `sameSite: 'strict'`, для dev сохранен `sameSite: 'lax'`.

- Приведен к стандарту emission событий (action/payload/metadata):
  - в [`events.ts`](src/server/shared/lib/events.ts:1) добавлен `StandardEventPayload`, обновлен [`emitEvent()`](src/server/shared/lib/events.ts:1);
  - обновлены точки эмита в [`system.service.ts`](src/server/features/system/system.service.ts:1) и [`notes.events.ts`](src/server/features/notes/notes.events.ts:1) на использование `emitEvent(...)`.

- Устранена store-зависимость в Shared UI и исключен HTML-инжект в отображении упоминаний:
  - [`MentionDisplay.vue`](src/client/shared/ui/MentionDisplay.vue:1) переведен на props-only API (`currentUserId`) без `useUserStore()`;
  - удален `v-html`, реализован безопасный парсинг в сегменты `text/mention` и рендер через шаблон.

- Обновлены места использования [`MentionDisplay`](src/client/shared/ui/MentionDisplay.vue:1):
  - [`NoteCard.vue`](src/client/features/NoteCard.vue:1), [`NoteActivityItem.vue`](src/client/features/NoteActivityItem.vue:1), [`NoteCommentsList.vue`](src/client/features/NoteCommentsList.vue:1).

### ✅ Проверка

- Поиск по проекту: `v-html=` отсутствует.
- Выполнен [`npm run -s typecheck`](package.json:1) — успешно (exit code 0).

### 🎯 Результат

- Закрыты критичные XSS-пути рендера HTML.
- Усилена политика cookie для сессии в production.
- События приведены к унифицированному payload-стандарту без поломки обратной совместимости обработчиков.
- Shared UI приведен к требованию изоляции (без store внутри shared-компонента).

---

## Contractors: множественные ответственные (форма, список, согласование)

### ✅ Выполнено

- Расширен контракт контрагентов поддержкой массива ответственных:
  - добавлены `ContractorResponsibleManager` и `contractorResponsibleManagerSchema` в `src/shared/contracts/contractors.ts`;
  - в DTO/схемы create/update/entry/pending добавлено поле `responsibleManagers`.

- Обновлена backend-модель и миграция:
  - в `src/server/features/reference_books/db/contractors.table.ts` в таблицы `contractors` и `contractors_pending` добавлено поле `responsible_managers` (JSON);
  - добавлена миграция `drizzle/0021_contractors_responsible_managers.sql`:
    - добавляет JSON-колонки;
    - переносит существующие `manager_*` в массив `responsible_managers` (с fallback на пустой массив).

- Обновлены backend-схемы/маппинг/approval-flow:
  - `src/server/features/reference_books/contractors.schema.ts` — добавлены схемы и типы для массива ответственных;
  - `src/server/features/reference_books/lib/contractors.mapper.ts` — добавлен парсинг JSON с обратной совместимостью по legacy-полям `manager_*`;
  - `src/server/features/reference_books/lib/contractors.core.ts`, `contractors.drafts.ts`, `contractors.service.ts`, `contractors.workflow.ts` — передача/сохранение `responsibleManagers` во всех сценариях:
    - прямое создание/обновление;
    - черновики на согласование;
    - approve/reject;
    - pending-запросы на удаление.

- Обновлен frontend API-слой и диффы:
  - `src/client/shared/api/repositories/ContractorsRepository.ts` — схемы response/create/update поддерживают `responsibleManagers`;
  - `src/client/shared/lib/diffHelpers.ts` — `getContractorChangedFields()` теперь учитывает изменения массива ответственных (структурное сравнение).

- Реализована форма «Добавить контрагента» с множественными ответственными (по аналогии с «Доп. сервисы» в оборудовании, без копирования):
  - `src/client/entities/contractor/model/useContractorForm.ts`:
    - динамический массив ответственных;
    - add/remove;
    - отдельная валидация `name/phone/email` для каждого блока;
    - нормализация в payload;
    - сохранена backward-совместимость через заполнение legacy `managerName/managerPhone/managerEmail` первым ответственным;
  - `src/client/features/ContractorModal.vue`:
    - UI-блок «Ответственные» с добавлением/удалением;
    - поле телефона с тем же паттерном ввода (`+` префикс), без кнопок копирования.

- Обновлен список контрагентов и подсветка согласования:
  - `src/client/pages/ContractorsPage.vue`:
    - вывод всех ответственных в колонке «Ответственный» с телефоном и email;
    - поиск по `responsibleManagers`;
    - tooltip-диффы и подсветка поля `responsibleManagers` для pending-изменений;
  - `src/client/entities/contractor/model/unifiedList.ts`:
    - `responsibleManagers` добавлен в unified-данные и `originalData` для корректного diff в approve-workflow.

### ✅ Проверка

- Выполнен `npm run typecheck` — успешно, без ошибок TypeScript.

### 🎯 Результат

- В разделе «Контрагенты» можно добавлять несколько ответственных в одной записи.
- В таблице отображаются все ответственные, включая email.
- В согласовании корректно подсвечиваются и объясняются изменения по ответственным (включая добавление/редактирование/удаление).

---

## Router hotfix: редирект авторизованного пользователя в ближайший доступный раздел вместо login/404

### ✅ Выполнено

- Усилен router-guard в [`main.ts`](src/client/app/main.ts):
  - добавлен список кандидатных dashboard-роутов с проверкой одновременно по активному модулю и permission (`resource:action`) через [`canAccessDashboardCandidate()`](src/client/app/main.ts:66);
  - переработан fallback-резолвер [`getFirstAccessibleDashboardRoute()`](src/client/app/main.ts:84):
    - теперь выбирает первый реально доступный раздел из фиксированного порядка,
    - учитывает исключаемый путь,
    - больше не возвращает `/404` для авторизованного пользователя (fallback внутри dashboard).

- Исправлен сценарий «урезали URL до `/dashboard/paymaster` / удалили часть пути»:
  - в guard добавлена ранняя обработка [`/login`](src/client/app/main.ts:130):
    - если пользователь уже авторизован, выполняется редирект в ближайший доступный раздел,
    - если не авторизован — страница логина остается доступной как раньше.

- Исправлен fallback при `404` и несопоставленных маршрутах (включая HMR/горячие изменения кода):
  - добавлена проверка [`to.matched.length === 0`](src/client/app/main.ts:145) и явного пути `/404`;
  - авторизованные пользователи перенаправляются в ближайший доступный dashboard-раздел;
  - неавторизованные пользователи перенаправляются на `/login?reason=session_expired`.

- Усилен fallback при отключенном модуле:
  - вместо `enabledModules[0]` теперь используется общий резолвер доступного раздела,
  - редирект также учитывает права и активность модуля.

### ✅ Проверка

- Выполнен [`npm run -s typecheck`](package.json:1) — успешно, без ошибок типизации.

### 🎯 Результат

- Авторизованный пользователь больше не выкидывается на страницу входа при некорректном/урезанном dashboard-URL.
- При попадании в `/404` или при route-mismatch во время hot-reload пользователь перенаправляется в ближайший доступный рабочий раздел.
- Навигация стала консистентной: fallback определяется единообразно через модуль+permission-проверку.

---

## Notes PBAC hotfix: убрать GOD-bypass и вернуть матричные ограничения на чтение/создание

### ✅ Выполнено

- Удален клиентский bypass прав для роли `GOD` в [`permissions` store](src/client/entities/permissions.ts):
  - в [`hasPermission`](src/client/entities/permissions.ts:44) удалена ветка `if (role === USER_ROLES.GOD) return true`;
  - проверки теперь идут строго по массиву прав, полученному из `/api/system/permissions/my`.

- Удалены client-side bypass-ветки в [`usePermissions`](src/client/shared/lib/usePermissions.ts):
  - в [`can`](src/client/shared/lib/usePermissions.ts:39), [`hasPermission`](src/client/shared/lib/usePermissions.ts:50), [`hasAnyPermission`](src/client/shared/lib/usePermissions.ts:61), [`hasAllPermissions`](src/client/shared/lib/usePermissions.ts:72) удален `userStore.isGod` shortcut;
  - UI для notes теперь полностью управляется матрицей прав (`notes:read`, `notes:create_private`, `notes:create_public` и т.д.).

- Исправлен read-only бейдж в меню для `GOD`-пользователей в [`menuConfig`](src/client/shared/config/menuConfig.ts):
  - в [`isReadOnlyForPermissions`](src/client/shared/config/menuConfig.ts:104) удален ранний выход `if (userStore.isGod) return false`;
  - бейдж «Чтение» теперь корректно отображается, если для роли оставлено только `*:read`.

- Удален серверный глобальный bypass роли `GOD` в RBAC-плагине [`rbac.ts`](src/server/shared/plugins/rbac.ts):
  - в [`hasPermission`](src/server/shared/plugins/rbac.ts:113), [`hasAnyPermission`](src/server/shared/plugins/rbac.ts:136), [`hasAllPermissions`](src/server/shared/plugins/rbac.ts:163) удалены проверки `user?.role === USER_ROLES.GOD`;
  - доступ к notes API теперь определяется только динамической матрицей `role_permissions`.

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:22) — успешно, ошибок типизации нет.

### 🎯 Результат

- Если у роли `GOD` сняты права на заметки, пользователь больше не может просматривать/создавать заметки через UI и API, так как bypass удален на обоих уровнях.
- Если для `GOD` оставлено только чтение (`notes:read`), в меню корректно показывается бейдж «Чтение».

---

## Notes UI hotfix: скрытие FAB «Создать заметку» в read-only и обработка 403 без unhandled promise

### ✅ Выполнено

- В [`NotesBoard.vue`](src/client/widgets/NotesBoard.vue) добавлена permission-проверка создания заметок через [`canCreateNotes`](src/client/widgets/NotesBoard.vue:208):
  - проверяются права `notes:create_private` или `notes:create_public` через [`permissionsStore.hasAnyPermission(...)`](src/client/widgets/NotesBoard.vue:209);
  - источник прав — матрица permissions, без role-hardcode.

- Кнопка создания скрыта при read-only доступе:
  - добавлен `v-if="canCreateNotes"` на FAB в grid-режиме [`NotesBoard.vue`](src/client/widgets/NotesBoard.vue:629);
  - добавлен `v-if="canCreateNotes"` на FAB в list-режиме [`NotesBoard.vue`](src/client/widgets/NotesBoard.vue:754).

- Добавлена защитная проверка в [`openCreateModal()`](src/client/widgets/NotesBoard.vue:486):
  - при отсутствии прав модалка создания не открывается.

- Устранен `Unhandled error during execution of component event handler` в create-flow:
  - в [`handleCreateNote()`](src/client/widgets/NotesBoard.vue:499) добавлен `try/catch`;
  - ошибки создания (включая 403) логируются контролируемо и не приводят к unhandled promise в Vue runtime.

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:22) — успешно, ошибок типизации нет.

### 🎯 Результат

- Если у пользователя только `notes:read`, кнопка «Создать заметку» не отображается.
- Попытка create без прав больше не приводит к unhandled promise в консоли приложения.

---

## Access: взаимоисключение прав и унификация action-иконок с Blacklist

### ✅ Выполнено

- В [`togglePermission()`](src/client/pages/SettingsPage.vue:359) исправлено взаимоисключение dual-edit прав для модуля «Внешние доступы»:
  - ранее при переключении `access:edit`/`access:request_approval` ошибочно выключалась противоположная ячейка только у `blacklist:*`;
  - теперь opposite-permission рассчитывается динамически от текущего ресурса (`blacklist`/`access`), поэтому для `access` гарантированно работает пара:
    - `access:edit` ↔ `access:request_approval`.

- В таблице внешних доступов [`AccessTable.vue`](src/client/widgets/AccessTable.vue:268) расширен показ действия «Редактировать» (карандаш):
  - кнопка редактирования теперь доступна не только при `canEdit`, но и при `canRequestApproval`;
  - пользователь с правом «Редактирование с согласованием» может открыть редактирование и отправить изменения на согласование.

- Приведено оформление action-иконок модуля «Внешние доступы» к точному эталону «Черного списка»:
  - в [`AccessTable.vue`](src/client/widgets/AccessTable.vue) удалены локальные scoped-стили `.action-btn/.edit-btn/.delete-btn/.approve-btn/.reject-btn`;
  - компонент теперь использует единые глобальные стили из [`index.css`](src/client/app/index.css:29), которые используются в Blacklist.

- По обратной связи исправлена подсветка изменений в таблице внешних доступов:
  - в [`AccessTable.vue`](src/client/widgets/AccessTable.vue) добавлена проверка [`isFieldChanged()`](src/client/widgets/AccessTable.vue:87), которая опирается на [`getAccessChangedFields()`](src/client/shared/lib/diffHelpers.ts:172);
  - желтая подсветка теперь применяется только к реально измененным полям pending-редактирования, а не ко всей строке записи.

- По обратной связи исправлены права на отмену своих запросов согласования (reject/cancel flow):
  - в [`external_access.routes.ts`](src/server/features/reference_books/external_access.routes.ts:191) для `POST /external-access/reject/:id` guard изменен с `can(access:edit)` на `canAny(access:edit, access:request_approval)`;
  - в [`blacklist.routes.ts`](src/server/features/blacklist/blacklist.routes.ts:159) для `POST /reject/:id` guard изменен с `can(blacklist:edit)` на `canAny(blacklist:edit, blacklist:request_approval)`;
  - внутренняя ABAC-проверка «без edit можно отклонять только свои заявки» сохранена без изменений.

- Аналогичная логика применена к модулю «Контрагенты»:
  - в матрице прав [`SettingsPage.vue`](src/client/pages/SettingsPage.vue:139) переименованы пункты в формат dual-edit:
    - «Контрагенты: Редактирование без согласования»;
    - «Контрагенты: Редактирование с согласованием»;
  - в [`togglePermission()`](src/client/pages/SettingsPage.vue:359) добавлено взаимоисключение для ресурса `contractors` (по аналогии с `blacklist` и `access`);
  - на странице [`ContractorsPage.vue`](src/client/pages/ContractorsPage.vue:31) синхронизирована модель прав `canEditWithoutApproval/canEditWithApproval`, `canApprove` привязан только к `contractors:edit`;
  - в таблице [`ContractorsPage.vue`](src/client/pages/ContractorsPage.vue:708) кнопка редактирования (карандаш) и удаления включены для `canEdit || canRequestApproval`;
  - унифицированы подсказки в [`ContractorsPage.vue`](src/client/pages/ContractorsPage.vue:93):
    - формат изменений как в Blacklist/Access (`Было/Стало/Автор изменений`);
    - для удаления: `Автор удаления: ...`;
    - для новой записи: `Автор новой записи: ...`;
  - в модальном окне [`ContractorModal.vue`](src/client/features/ContractorModal.vue:26) предупреждение о согласовании переведено на логику request_approval-only и текст приведен к единому стандарту:
    - «Примечание: Запись будет создана как черновик и отправлена на предварительное согласование.»;
  - в backend-роутах [`contractors.routes.ts`](src/server/features/reference_books/contractors.routes.ts:90) обновлены guards:
    - `POST /contractors/reject/:id` → `canAny(contractors:edit, contractors:request_approval)`;
    - `DELETE /contractors/:id` → `canAny(contractors:edit, contractors:request_approval)`;
    - при этом ABAC-ограничение «без edit — только свои заявки» в reject flow сохранено.

- Подпись в модальном окне внешних доступов уже соответствует требуемой формулировке и логике Blacklist:
  - [`AccessModal.vue`](src/client/features/AccessModal.vue:331)
  - «Примечание: Запись будет создана как черновик и отправлена на предварительное согласование.»
  - отображается только для сценария `request_approval` без `edit`.

---

## Access: доработка матрицы прав и плавающих подсказок (feedback)

### ✅ Выполнено

- В [`SettingsPage.vue`](src/client/pages/SettingsPage.vue) обновлена матрица прав модуля «Внешние доступы»:
  - `access:edit` переименовано в «Редактирование без согласования»;
  - `access:request_approval` переименовано в «Редактирование с согласованием».

- В [`togglePermission()`](src/client/pages/SettingsPage.vue) добавлено взаимоисключение для `access:edit` и `access:request_approval` (по аналогии с `blacklist`):
  - при включении одного права второе автоматически выключается.

- В [`AccessModal.vue`](src/client/features/AccessModal.vue) скорректирована заметка про согласование:
  - предупреждение о черновике показывается только для роли с `access:request_approval` без `access:edit`;
  - текст приведен к формулировке: «Запись будет создана как черновик и отправлена на предварительное согласование.»;
  - при наличии `access:edit` предупреждение про черновик не показывается.

- В [`AccessPage.vue`](src/client/pages/AccessPage.vue), [`AccessTable.vue`](src/client/widgets/AccessTable.vue) и [`diffHelpers.ts`](src/client/shared/lib/diffHelpers.ts) добавлена логика информативных hover-подсказок для таблицы:
  - для удаления: `Автор удаления: {ФИО}`;
  - для новой записи: `Автор новой записи: {ФИО}`;
  - для изменений: многострочный формат
    - `Было: ...`
    - `Стало: ...`
    - `Автор изменений: ...`;
  - убран ложный сценарий показа FAB-тултипа над строками таблицы.

- Дополнительно синхронизирован текст FAB-подсказки на странице [`AccessPage.vue`](src/client/pages/AccessPage.vue) к формулировке «Добавить запись».

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:22) — успешно, ошибок типизации нет.

---

## Access: матрица прав и workflow согласования как в Blacklist

### ✅ Выполнено

- Backend модуля внешних доступов переведен на тот же approval-flow, что и в черном списке:
  - добавлены pending-сущности и mapper/notifications/core/drafts/workflow:
    - [`external_access.table.ts`](src/server/features/reference_books/db/external_access.table.ts)
    - [`access.mapper.ts`](src/server/features/reference_books/lib/access.mapper.ts)
    - [`access.notifications.ts`](src/server/features/reference_books/lib/access.notifications.ts)
    - [`access.core.ts`](src/server/features/reference_books/lib/access.core.ts)
    - [`access.drafts.ts`](src/server/features/reference_books/lib/access.drafts.ts)
    - [`access.workflow.ts`](src/server/features/reference_books/lib/access.workflow.ts)
  - сервис [`access.service.ts`](src/server/features/reference_books/access.service.ts) переписан с прямого CRUD на логику:
    - direct edit для `access:edit`;
    - создание/изменение/удаление через pending-заявки для `access:request_approval`;
    - авто-approve черновиков при редактировании админом;
    - получение pending-записей для вкладки согласования.
  - роуты [`external_access.routes.ts`](src/server/features/reference_books/external_access.routes.ts) расширены до паритета с Blacklist:
    - `GET /external-access?status=all|pending|approved`
    - `GET /external-access/pending`
    - `POST /external-access/approve/:id`
    - `POST /external-access/reject/:id`
    - `DELETE` теперь `canAny(access:edit, access:request_approval)`.

- Shared contracts синхронизированы под approval-сценарий Access:
  - [`access.ts`](src/shared/contracts/access.ts):
    - `ExternalAccess` расширен полями `isApproved`, `createdBy`;
    - добавлены `ExternalAccessPendingEntry`, `ExternalAccessUnifiedEntry`;
    - добавлены `externalAccessPendingEntrySchema` и `externalAccessPendingEntriesSchema`.
  - [`events.ts`](src/shared/contracts/events.ts): добавлены события `access:created|updated|deleted`.

- Approval subsystem обновлен для новой сущности `access`:
  - [`createApprovalRequest()`](src/server/features/approval/approval.service.ts:43) теперь принимает `entityType: 'access'`.
  - В approve/reject flow добавлены вызовы [`approveAccessEntry()`](src/server/features/reference_books/lib/access.workflow.ts:14) и [`rejectAccessEntry()`](src/server/features/reference_books/lib/access.workflow.ts:102).

- Frontend Access приведен к той же UX-логике согласования:
  - стор [`access.ts`](src/client/entities/access.ts) переведен на `entries + pendingEntries + unifiedList`, approve/reject, socket-refetch.
  - репозиторий [`AccessRepository.ts`](src/client/shared/api/repositories/AccessRepository.ts) дополнен:
    - `getAllByStatus()`
    - `getPending()`
    - `approveEntry()`
    - `rejectEntry()`.
  - страница [`AccessPage.vue`](src/client/pages/AccessPage.vue):
    - добавлены вкладки `Все/На согласовании`;
    - добавлены действия approve/reject;
    - подключены socket bind/unbind;
    - сохранена permission-first матрица (`access:edit`, `access:request_approval`).
  - таблица [`AccessTable.vue`](src/client/widgets/AccessTable.vue) обновлена:
    - статусные бейджи (`Новая/Изменено/К удалению/Утверждено`);
    - разные кнопки действий для approved/pending записей;
    - manager может «Отменить запрос» только для своих pending-записей.

- Добавлена SQL-миграция [`0020_access_approval_workflow.sql`](drizzle/0020_access_approval_workflow.sql):
  - `external_access`: `is_approved`, `created_by`;
  - новая таблица `external_access_pending`.

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:22) — успешно, ошибок типизации нет.

---

## Hotfix: Sidebar — ФИО и должность для роли VIEWER без role-hardcode

### ✅ Выполнено

- Расширен auth-контракт пользователя полем `fullName`:
  - backend-схема [`userResponseSchema`](src/server/features/auth/auth.schema.ts:27);
  - frontend-схема [`userResponseSchema`](src/client/shared/api/repositories/AuthRepository.ts:13).

- Обновлен backend auth-service для возврата `fullName` в auth-flow:
  - в [`login()`](src/server/features/auth/auth.service.ts:67) добавлен select/return `employees.fullName`;
  - в [`guestLogin()`](src/server/features/auth/auth.service.ts:123) добавлен возврат `fullName` (включая системный guest);
  - в [`register()`](src/server/features/auth/auth.service.ts:184) добавлен возврат `fullName`.

- Обновлен user-store на фронтенде:
  - в [`User`](src/client/entities/user.ts:8) добавлено поле `fullName?: string | null`;
  - в [`userResponseToUser()`](src/client/entities/user.ts:29) добавлен маппинг `fullName`.

- Обновлен Sidebar fallback без хардкода ролей:
  - в [`userDisplayName`](src/client/widgets/Sidebar.vue:168) приоритет отображения изменен на `user.fullName -> employee.fullName -> username -> "Пользователь"`.

### 🎯 Результат

- Для учеток с ролью `VIEWER` (включая системный guest) ФИО и должность в нижнем блоке Sidebar отображаются так же, как для остальных ролей.
- Исправление выполнено через единый auth/user контракт и data-flow, без role-based хардкода в UI.

---

## Черный список: feedback-фикс по удалению и плавающим подсказкам

### ✅ Выполнено

- В [`blacklist.routes.ts`](src/server/features/blacklist/blacklist.routes.ts) обновлен guard удаления:
  - `DELETE /api/blacklist/:id` теперь разрешен по `canAny(blacklist:edit, blacklist:request_approval)`;
  - роль с правом «Редактирование с согласованием» теперь отправляет удаление в pending-flow (статус `to_delete`) вместо прямого запрета.

- В [`BlacklistPage.vue`](src/client/pages/BlacklistPage.vue) обновлены тексты и формат плавающих подсказок:
  - для удаления: `Автор удаления: {ФИО}`;
  - для новой записи: `Автор новой записи: {ФИО}`;
  - для изменений: многострочный формат
    - `Было: ...`
    - `Стало: ...`
    - `Автор изменений: ...`

- В [`BlacklistPage.vue`](src/client/pages/BlacklistPage.vue) исправлена логика отображения глобального курсорного тултипа:
  - добавлен `activeEntryTooltip` для показа подсказок только по реально наведенной строке/ячейке;
  - убран ложный fallback-показ «Добавить запись» над строками таблицы.

### 🎯 Результат

- Пользователь с правом «Редактирование с согласованием» может отправлять удаление записи на согласование.
- Подсказка удаления показывает корректного автора удаления.
- Подсказки изменений стали информативными и структурированными по строкам.
- Бессмысленная подсказка «Добавить запись» больше не перехватывает hover по записям таблицы.

---

## Черный список: доработка матрицы прав (взаимоисключение + корректный UX согласования)

### ✅ Выполнено

- В [`SettingsPage.vue`](src/client/pages/SettingsPage.vue) обновлены названия прав в матрице для модуля «Черный список»:
  - `blacklist:edit` → «Редактирование без согласования»;
  - `blacklist:request_approval` → «Редактирование с согласованием».

- В [`togglePermission()`](src/client/pages/SettingsPage.vue) добавлено правило взаимоисключения:
  - при включении `blacklist:edit` автоматически выключается `blacklist:request_approval`;
  - при включении `blacklist:request_approval` автоматически выключается `blacklist:edit`.

- В [`BlacklistModal.vue`](src/client/features/BlacklistModal.vue) обновлена логика заметок по правам:
  - для роли с правом `blacklist:edit` показывается только сценарий «автоматически утверждена»;
  - для роли только с `blacklist:request_approval` показывается сценарий черновика;
  - текст предупреждения изменен на: «Запись будет создана как черновик и отправлена на предварительное согласование.»
  - предупреждение про черновик не отображается при наличии права «без согласования».

- В [`BlacklistPage.vue`](src/client/pages/BlacklistPage.vue) синхронизированы вычисления прав:
  - введены разделенные флаги `canEditWithoutApproval` и `canEditWithApproval`;
  - `canApprove` теперь привязан только к `blacklist:edit`;
  - сохранен доступ к create/edit flow при наличии любого из двух прав.

### 🎯 Результат

- Права «Редактирование без согласования» и «Редактирование с согласованием» в матрице работают строго взаимоисключающе.
- UI и тексты модалки черного списка соответствуют новой бизнес-логике.
- Согласование и автосогласование на странице черного списка теперь корректно маршрутизируются по целевым правам.

---

## Текущие операции / Кассовый отчет: удаление «Прочее» и цветные типы в дропдауне

### ✅ Выполнено

- В итоговых информационных карточках на странице кассы удалена карточка «Прочее»:
  - сетка карточек переведена с `md:grid-cols-5` на `md:grid-cols-4` в [`PaymasterPage.vue`](src/client/pages/PaymasterPage.vue).

- Убран хардкод печати, скрывающий «предпоследний элемент» (бывшую карточку «Прочее»):
  - удалены print-правила с `nth-child(4)` и перестройкой `.grid.md\:grid-cols-5` в [`PaymasterTable.vue`](src/client/widgets/PaymasterTable.vue).

- Реализована цветовая подсветка значений в выпадающем списке типа операции:
  - в [`SmartDropdown.vue`](src/client/shared/ui/SmartDropdown.vue) добавлен новый проп `getOptionClass?: (option: string) => string`;
  - класс опции теперь может передаваться извне, с fallback на `text-gray-900`;
  - в [`PaymasterTable.vue`](src/client/widgets/PaymasterTable.vue) передан `:get-option-class="getTypeClass"` для поля «Тип операции»;
  - для неизвестных/кастомных типов (бывшее «прочее») установлен цвет `text-black`, дефолтные типы окрашены по существующей карте цветов карточек.

### 🎯 Результат

- Блок итогов в кассовом отчете теперь содержит только 4 карточки: Наличные, Безнал, Аванс, Расход.
- Печатная версия больше не зависит от хардкода по позиции карточки «Прочее».
- В дропдауне «Тип операции» значения визуально дифференцируются по цветам, а кастомные типы отображаются черным.

---

## Локальное оборудование: MAC рядом с IP, порт сервиса и усиленная фронтенд-валидация сети

### ✅ Выполнено

- В модалке оборудования [`EquipmentModal.vue`](src/client/features/EquipmentModal.vue) для категории IT добавлено поле `MAC-адрес` рядом с полем сетевого адреса:
  - UI теперь отображает оба поля в одной строке (2 колонки).
  - MAC сделан необязательным.

- В блоке дополнительных доступов (`accessPorts`) добавлено поле `Порт`:
  - расширен интерфейс порта в [`accessPortSchema`](src/client/shared/api/repositories/EquipmentRepository.ts) и в backend-схеме [`accessPortSchema`](src/server/features/reference_books/local_it.schema.ts);
  - добавлена проверка диапазона `1..65535` на фронте в [`validateAccessPorts()`](src/client/features/EquipmentModal.vue).

- Усилена фронтенд-валидация сетевого адреса в [`EquipmentModal.vue`](src/client/features/EquipmentModal.vue):
  - добавлена явная проверка формата `IPv4 | CIDR | DHCP` перед отправкой;
  - невалидный сетевой адрес блокирует submit и показывает понятную ошибку в форме (без ухода в «внутреннюю ошибку сервера»).

- Добавлена валидация MAC с поддержкой маскированного хвоста:
  - поддерживаются форматы `AA:BB:CC:DD:EE:FF`, `AA-BB-CC-DD-EE-FF`;
  - поддерживаются маски в последних сегментах `XX` и кириллический вариант `ХХ` (нормализуется до `XX`);
  - правило: wildcard-сегменты разрешены только в конце адреса.

- Обновлены DTO/схемы Local IT под новое поле `macAddress`:
  - frontend-репозиторий: [`localITResponseSchema`](src/client/shared/api/repositories/EquipmentRepository.ts), create/update request schemas;
  - backend-схемы: [`createLocalITSchema`](src/server/features/reference_books/local_it.schema.ts), [`updateLocalITSchema`](src/server/features/reference_books/local_it.schema.ts), [`localITResponseSchema`](src/server/features/reference_books/local_it.schema.ts);
  - backend-service mapping: [`mapToLocalITDTO()`](src/server/features/reference_books/local_it.service.ts).

- Обновлен вывод в таблице оборудования на странице [`EquipmentPage.vue`](src/client/pages/EquipmentPage.vue):
  - в колонке «Сетевой адрес» теперь показывается связка `IP • MAC: ...` (если MAC заполнен);
  - поиск по таблице расширен на `networkAddress` и `macAddress`.

- Обновлена схема БД Local IT:
  - добавлено поле `macAddress` в таблицу [`localIT`](src/server/features/reference_books/db/local_it.table.ts).

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:22) — успешно, ошибок типизации нет.

### 🎯 Результат

- В карточке/таблице локального оборудования сеть теперь ведется в более полном виде: IP + MAC.
- Для сервисных доступов появился отдельный порт с валидацией.
- Невалидные сетевые значения отсекаются на фронте до отправки на сервер.
- MAC поддерживает частично неизвестные последние сегменты через `XX/ХХ` при сохранении строгой маски формата.

---

## Sidebar: отображение ФИО и должности вместо username/email

### ✅ Выполнено

- В [`user` store](src/client/entities/user.ts) расширен тип пользователя:
  - в интерфейс `User` добавлено поле `employeeId?: string | null`;
  - в маппере [`userResponseToUser()`](src/client/entities/user.ts) добавлено пробрасывание `employeeId` из auth-контракта.

- В [`Sidebar.vue`](src/client/widgets/Sidebar.vue) реализовано отображение данных сотрудника:
  - подключен [`useEmployeeStore`](src/client/entities/employee.ts);
  - добавлена вычисляемая связь текущего пользователя с сотрудником по `employeeId`;
  - добавлены вычисления `userDisplayName` (ФИО) и `userDisplayPosition` (должность) с fallback на `username/role`;
  - добавлена безопасная подзагрузка сотрудника через `employeeStore.fetchById()` при `onMounted` и при изменении `employeeId`.

- В шаблоне нижнего user-блока сайдбара:
  - верхняя строка заменена с `userStore.user?.username` на `userDisplayName`;
  - нижняя строка заменена с `userStore.user?.email` на `userDisplayPosition`.

- В стилях [`Sidebar.vue`](src/client/widgets/Sidebar.vue) класс подписи переименован:
  - `.user-email` → `.user-position`.

### 🎯 Результат

- В нижнем блоке сайдбара отображается:
  - основная строка: ФИО сотрудника;
  - вторичная строка: должность сотрудника.
- При отсутствии данных сотрудника UI не ломается и использует fallback значения (`username` и `role`).

### 🔧 Дополнительный фикс

- По обратной связи уточнен источник должности: используется поле `users.position` из auth-пользователя.
- Для этого расширены auth-контракты и маппинг:
  - backend: [`auth.service.ts`](src/server/features/auth/auth.service.ts) и [`auth.schema.ts`](src/server/features/auth/auth.schema.ts) возвращают `position` в `user`;
  - frontend: [`AuthRepository.ts`](src/client/shared/api/repositories/AuthRepository.ts) и [`user.ts`](src/client/entities/user.ts) принимают и сохраняют `position` в user-store;
  - sidebar: [`Sidebar.vue`](src/client/widgets/Sidebar.vue) показывает под ФИО сначала `user.position` (из users), затем fallback на employee position.

---

## Sticky Header: фиксация заголовков матрицы прав в Settings

### ✅ Выполнено

- В [`SettingsPage.vue`](src/client/pages/SettingsPage.vue:545) обновлен контейнер таблицы матрицы прав:
  - было: `overflow-x-auto`;
  - стало: `overflow-auto max-h-[75vh] border-b border-gray-200`.
  - Это добавило внутренний вертикальный скролл с ограничением высоты и сохранило горизонтальный скролл.

- В [`SettingsPage.vue`](src/client/pages/SettingsPage.vue:547) усилена фиксация шапки таблицы (`thead`):
  - обновлены классы до `bg-gray-50 sticky top-0 z-20 shadow-md outline outline-1 outline-gray-200`.
  - Повышен слой (`z-20`) и визуальное отделение (`shadow-md` + `outline`) для корректного отображения при прокрутке.

### 🎯 Результат

- Заголовки ролей в матрице прав остаются видимыми при вертикальном скролле.
- Контент таблицы прокручивается под фиксированной шапкой.
- Горизонтальная прокрутка на узких экранах продолжает работать вместе с вертикальной.

---

## PBAC hotfix: SettingsPage на `SYSTEM_MANAGE` + удаление мусорных прав approval/notifications

### ✅ Выполнено

- Страница настроек переведена с role-hardcode `userStore.isGod` на permission-first проверку `SYSTEM_MANAGE` в [`SettingsPage.vue`](src/client/pages/SettingsPage.vue):
  - добавлен импорт [`usePermissions`](src/client/shared/lib/usePermissions.ts) и инициализация `const { hasPermission } = usePermissions();`;
  - в [`onMounted`](src/client/pages/SettingsPage.vue) загрузка данных (`loadPermissionsMatrix/loadSystemModules/loadRoleColors`) теперь выполняется только при `hasPermission.value(AppPermission.SYSTEM_MANAGE)`;
  - в шаблоне access-denied ветка переведена на `v-if="!hasPermission(AppPermission.SYSTEM_MANAGE)"`;
  - текст заглушки обновлен: «доступно только администраторам системы».

- Из shared-контракта прав удалены избыточные/неиспользуемые CRUD-права для модулей «Согласования» и «Уведомления» в [`AppPermission`](src/shared/contracts/permissions.ts):
  - удалены `APPROVAL_WRITE`, `APPROVAL_DELETE`;
  - удалены `NOTIFICATIONS_WRITE`, `NOTIFICATIONS_DELETE`, `NOTIFICATIONS_MANAGE`.

- Из UI-матрицы прав удалены соответствующие строки в [`availablePermissions`](src/client/pages/SettingsPage.vue):
  - убраны объекты для `AppPermission.APPROVAL_WRITE`, `AppPermission.APPROVAL_DELETE`;
  - убраны объекты для `AppPermission.NOTIFICATIONS_WRITE`, `AppPermission.NOTIFICATIONS_DELETE`, `AppPermission.NOTIFICATIONS_MANAGE`.

### ✅ Проверка

- Выполнен поиск по коду на остаточные использования удаленных enum-ключей (`APPROVAL_WRITE`, `APPROVAL_DELETE`, `NOTIFICATIONS_WRITE`, `NOTIFICATIONS_DELETE`, `NOTIFICATIONS_MANAGE`) — совпадений не найдено.
- Выполнен [`npm run typecheck`](package.json:22) — успешно (exit code 0), ошибок TypeScript нет.

### 🎯 Результат

- Доступ к странице конфигурации теперь определяется PBAC-правом `system:manage`, а не жесткой ролью `GOD`.
- Контракт прав и UI-матрица очищены от мусорных CRUD-прав в модулях «Согласования» и «Уведомления».

---

## RBAC Phase 2: глубокая зачистка role-hardcode (`isManagerOrAdmin`, `STAFF_ROLES`)

### ✅ Выполнено

- В shared-контракт прав добавлены системные права в [`AppPermission`](src/shared/contracts/permissions.ts):
  - `SYSTEM_MANAGE = 'system:manage'`;
  - `SYSTEM_RECEIVE_MENTIONS = 'system:receive_mentions'`.

- Из backend auth-утилит удален legacy helper [`isManagerOrAdmin`](src/server/shared/lib/auth.ts), основанный на жестко зашитом массиве ролей.

- Контракт сотрудника расширен полем `isMentionable?: boolean`:
  - в shared DTO [`Employee`](src/shared/contracts/personnel.ts);
  - в backend response-схеме [`employeeResponseSchema`](src/server/features/personnel/personnel.schema.ts);
  - в frontend response-схеме [`employeeResponseSchema`](src/client/shared/api/repositories/EmployeeRepository.ts).

- Реализована backend PBAC-логика mention-доступности в [`getEmployees()`](src/server/features/personnel/personnel.service.ts):
  - роли получаются динамически через [`getRolesWithPermission()`](src/server/shared/lib/rbac.ts) по праву `AppPermission.SYSTEM_RECEIVE_MENTIONS`;
  - в DTO выставляется `isMentionable` при условии `notifyRoles.includes(emp.role)`;
  - сохранен обязательный GOD-бэкдор (`emp.role === USER_ROLES.GOD`).

- Компонент упоминаний переведен на contract-first фильтрацию в [`MentionEditor.vue`](src/client/shared/ui/MentionEditor.vue):
  - удалены импорт `USER_ROLES` и константа `STAFF_ROLES`;
  - фильтрация кандидатов теперь основана на `employee.isMentionable === true`.

- Удален role-hardcode в фильтрах заметок в [`NoteFilterDropdown.vue`](src/client/features/notes/ui/NoteFilterDropdown.vue):
  - удалены `USER_ROLES`/`STAFF_ROLES` и вычисление «менеджер/админ» по роли;
  - доступ к расширенному блоку фильтра «Видимость» переведен на permission-check через [`usePermissions()`](src/client/shared/lib/usePermissions.ts) + `hasAnyPermission([AppPermission.NOTES_MANAGE, AppPermission.NOTES_EDIT_ANY])`.

### ✅ Проверка

- Выполнен поиск по проекту на запрещенные паттерны `isManagerOrAdmin` и `STAFF_ROLES` — в целевых модулях упоминаний/фильтров/auth следов не осталось.
- Выполнен [`npm run -s typecheck`](package.json) — успешно (exit code 0).

### 🎯 Результат

- Phase 2 завершен: backend/frontend очищены от ключевых role-hardcode точек (`isManagerOrAdmin`, `STAFF_ROLES`) и переведены на гранулярные permission-first проверки.
- Доступность пользователей для упоминаний теперь вычисляется динамически по PBAC-матрице и передается в контрактный DTO сотрудника.

---

## Backend RBAC: динамические получатели approval-уведомлений (без ADMIN-hardcode)

### ✅ Выполнено

- В shared-контракт прав добавлено новое атомарное право [`APPROVAL_RECEIVE_NOTIFICATIONS`](src/shared/contracts/permissions.ts):
  - `approval:receive_notifications`.

- Добавлена shared RBAC-утилита [`getRolesWithPermission()`](src/server/shared/lib/rbac.ts) с in-memory TTL-кэшем 60 секунд:
  - выборка выполняется через `SELECT DISTINCT role` по таблице `role_permissions` (`resource`, `action`, `is_enabled = true`);
  - добавлен сброс кэша через [`clearRolesWithPermissionCache()`](src/server/shared/lib/rbac.ts).

- Выполнен рефакторинг approval-сервиса [`approval.service.ts`](src/server/features/approval/approval.service.ts):
  - удалена фильтрация получателей по `emp.role === USER_ROLES.ADMIN`;
  - добавлен helper [`getApprovalNotificationRecipientIds()`](src/server/features/approval/approval.service.ts), который:
    - получает роли из динамической матрицы через `getRolesWithPermission(AppPermission.APPROVAL_RECEIVE_NOTIFICATIONS)`;
    - сохраняет GOD-бэкдор (`emp.role === USER_ROLES.GOD`);
    - исключает уволенных (`!emp.isFired`).
  - новая логика применена в `createApprovalRequest`, `approveApprovalRequest`, `rejectApprovalRequest`.

- Проведена очистка shared-констант ролей в [`roles.ts`](src/shared/constants/roles.ts):
  - удалены `STAFF_ROLES`, `PRIVILEGED_ROLES`, `isStaffRole`;
  - оставлены `USER_ROLES`, `UserRole`, `ALL_ROLES`, `isValidRole`.

- Сохранена совместимость мест, использовавших удаленные константы:
  - [`auth.ts`](src/server/shared/lib/auth.ts): `isManagerOrAdmin()` переведен на явный массив `[ADMIN, MANAGER, GOD]` через `USER_ROLES`;
  - [`notifier.service.ts`](src/server/features/notes/lib/notifier.service.ts): локально введен `STAFF_NOTIFICATION_ROLES` вместо импорта `STAFF_ROLES`.

### 🎯 Результат

- Approval-уведомления теперь маршрутизируются по динамической матрице прав из БД, а не по хардкоду роли `ADMIN`.
- Изоляция соблюдена: `approval.service.ts` не обращается напрямую к `role_permissions`, доступ идет через shared-утилиту.
- Требование по `GOD`-бэкдору сохранено.

---

## Зачистка role-hardcode в List View заметок (NotesBoard)

### ✅ Выполнено

- В [`src/client/widgets/NotesBoard.vue`](src/client/widgets/NotesBoard.vue) удален импорт `USER_ROLES` и добавлен импорт [`useAppConfigStore`](src/client/entities/appConfig.ts).
- В [`src/client/widgets/NotesBoard.vue`](src/client/widgets/NotesBoard.vue) инициализирован `appConfigStore` и удалена legacy-функция `getPriorityIndicatorClasses` с классами `dot-admin/dot-manager-*`.
- В [`src/client/widgets/NotesBoard.vue`](src/client/widgets/NotesBoard.vue) добавлена новая функция `getNoteDynamicColor(note)` для вычисления цвета индикатора на основе `appConfigStore.roleColors` с fallback-цветами.
- В шаблоне List View обновлен рендер индикатора приоритета:
  - заменен вызов `:class="getPriorityIndicatorClasses(note)"`;
  - добавлены базовые классы `priority-dot`, `dot-high|dot-normal` и inline CSS-переменная `--dynamic-note-color` через `:style`.
- В [`src/client/widgets/styles/NotesBoard.css`](src/client/widgets/styles/NotesBoard.css) удалены устаревшие классы `.dot-admin`, `.dot-manager-private`, `.dot-manager-public`.
- В [`src/client/widgets/styles/NotesBoard.css`](src/client/widgets/styles/NotesBoard.css) базовые стили индикаторов переведены на `var(--dynamic-note-color)`:
  - `.priority-dot.dot-normal` — полый круг с динамической рамкой;
  - `.priority-dot.dot-high` — заполненный круг с динамическим цветом.

### 🎯 Результат

- List View заметок полностью переведен на динамические цвета ролей из `appConfigStore` без role-hardcode по `ADMIN/GOD`.
- Индикаторы приоритета теперь синхронизированы с новой системой цветов и не зависят от legacy CSS-классов.

---

## Динамические цветовые схемы ролей для заметок (role colors)

### ✅ Выполнено

- Добавлена новая таблица цветов ролей [`roleColorsTable`](src/server/features/system/db/role_colors.table.ts:8) с полями `privateNormal/privateHigh/publicNormal/publicHigh` и уникальностью по `role`.
- Таблица подключена в экспорт system-db [`index.ts`](src/server/features/system/db/index.ts:4) и в общий drizzle schema-export [`schema.ts`](src/server/shared/db/schema.ts:29).
- Добавлена SQL-миграция [`0018_create_role_colors.sql`](drizzle/0018_create_role_colors.sql).

- Расширен backend system-модуль:
  - добавлены схемы [`roleColorsSchema`](src/server/features/system/system.schema.ts:72), [`updateRoleColorsBodySchema`](src/server/features/system/system.schema.ts:83), [`roleParamSchema`](src/server/features/system/system.schema.ts:90), [`getRoleColorsResponseSchema`](src/server/features/system/system.schema.ts:113);
  - в сервисе добавлены методы [`getAllRoleColors()`](src/server/features/system/system.service.ts:564) и [`upsertRoleColors()`](src/server/features/system/system.service.ts:584) с `onDuplicateKeyUpdate`;
  - добавлены endpoint'ы [`GET /role-colors`](src/server/features/system/system.routes.ts:180) и [`PUT /role-colors/:role`](src/server/features/system/system.routes.ts:209) с проверкой прав `ADMIN/GOD`.

- Добавлен shared-контракт системных цветов [`system.ts`](src/shared/contracts/system.ts:1) и экспорт в [`contracts/index.ts`](src/shared/contracts/index.ts:13).

- Расширен глобальный конфиг-стор [`useAppConfigStore`](src/client/entities/appConfig.ts:54):
  - добавлен state `roleColors` с дефолтами;
  - добавлены экшены [`fetchRoleColors()`](src/client/entities/appConfig.ts:151), [`updateRoleColors()`](src/client/entities/appConfig.ts:185), [`getRoleColors()`](src/client/entities/appConfig.ts:194);
  - загрузка role-colors встроена в [`fetchConfig()`](src/client/entities/appConfig.ts:98) вместе с модулями.

- Обновлена страница настроек [`SettingsPage.vue`](src/client/pages/SettingsPage.vue:24):
  - добавлена вкладка `colors`;
  - добавлен UI выбора роли + 4 color picker;
  - реализованы загрузка и сохранение схем через новый API.

- Обновлена карточка заметки [`NoteCard.vue`](src/client/features/NoteCard.vue:125):
  - добавлен вычисляемый цвет [`computedNoteColor`](src/client/features/NoteCard.vue:125) на базе роли автора, публичности и приоритета;
  - цвет пробрасывается через CSS-переменную `--dynamic-note-color` в корневой элемент;
  - удалены role-hardcoded классы, цвет рамки переведен на `var(--dynamic-note-color)` в [`NoteCard.css`](src/client/widgets/styles/NoteCard.css:84).

### ✅ Проверка

- Выполнен [`npm run -s typecheck`](package.json:22) — успешно, без ошибок.

---

## PBAC: полная зачистка role-hardcode в меню и цветах заметок (`NOTES_AUTHORITY_HIGHLIGHT`)

### ✅ Выполнено

- В контракт прав добавлено новое право [`NOTES_AUTHORITY_HIGHLIGHT`](src/shared/contracts/permissions.ts) со значением `notes:authority_highlight`.

- В UI-матрицу прав на странице настроек добавлена строка:
  - «Заметки: Цветовое выделение руководства» в [`availablePermissions`](src/client/pages/SettingsPage.vue).

- Удален GOD-бэкдор из фильтрации пунктов меню:
  - в [`Sidebar.vue`](src/client/widgets/Sidebar.vue) удалена проверка `if (currentUserRole === 'GOD') return true;`;
  - видимость меню теперь определяется только динамической PBAC-проверкой по `permissionsStore.permissions` + включенности модуля.

- На backend добавлен расчет флага `isAuthority` для заметок:
  - в [`notes.service.ts`](src/server/features/notes/notes.service.ts) добавлены хелперы для определения ролей с правом `notes:authority_highlight` через таблицу [`rolePermissionsTable`](src/server/features/system/db/role_permissions.table.ts) и для маппинга ролей авторов;
  - флаг `isAuthority` теперь рассчитывается и возвращается в `getNotes`, `getNoteById`, `getNoteByIdWithLayout`;
  - для роли `GOD` сохранена страховка через принудительное включение в authority-список.

- Контракты/схемы заметок расширены полем `isAuthority`:
  - shared-контракт [`notes.ts`](src/shared/contracts/notes.ts);
  - клиентская схема репозитория [`NoteRepository.ts`](src/client/shared/api/repositories/NoteRepository.ts).

- Фронтенд карточки заметки переведен на новый PBAC-флаг:
  - в [`NoteCard.vue`](src/client/features/NoteCard.vue) удалена логика цвета «руководства» по `ADMIN/GOD`;
  - цвет/класс теперь определяются через `note.isAuthority` (данные от backend), без role-hardcode.

### ✅ Проверка

- Выполнен [`npm run -s typecheck`](package.json) — успешно, без ошибок.

### 🎯 Результат

- Клиентская навигация больше не содержит встроенных привилегий роли `GOD` для показа меню.
- Цветовое выделение заметок руководства переведено на permission-first модель через право `notes:authority_highlight` и backend-флаг `isAuthority`.
- Role-hardcode `ADMIN/GOD` в логике цветового выделения заметок удален.

---

## NOTES PBAC: право `NOTES_VIEW_PUBLIC_BOARD` + lifecycle лэйаутов

### ✅ Выполнено

- Добавлено новое право [`AppPermission.NOTES_VIEW_PUBLIC_BOARD`](src/shared/contracts/permissions.ts) со значением `notes:view_public_board`.

- На странице настроек в список [`availablePermissions`](src/client/pages/SettingsPage.vue) добавлен пункт:
  - `Заметки: Отображение на дашборде` (`resource: notes`, `action: view_public_board`).

- В сервисе лэйаутов заметок [`layout.service.ts`](src/server/features/notes/lib/layout.service.ts):
  - добавлены функции:
    - [`initializeLayoutsForPublicNote`](src/server/features/notes/lib/layout.service.ts) — создание лэйаутов только для автора и пользователей с PBAC-правом `notes:view_public_board`;
    - [`initializeLayoutsForPrivateNote`](src/server/features/notes/lib/layout.service.ts) — создание лэйаута только для автора;
    - [`deleteLayoutsForNote`](src/server/features/notes/lib/layout.service.ts) — удаление всех лэйаутов заметки;
  - логика публичной инициализации теперь использует таблицу ролей [`rolePermissionsTable`](src/server/features/system/db/role_permissions.table.ts) и сотрудников через [`getEmployees('SYSTEM')`](src/server/features/personnel/personnel.service.ts), с обязательным включением роли `GOD`.
  - [`distributeNoteToAllUsers`](src/server/features/notes/lib/layout.service.ts) сохранена как совместимая обертка, делегирующая в PBAC-инициализацию.

- В сервисе заметок [`updateNote`](src/server/features/notes/notes.service.ts) внедрен жёсткий lifecycle-контроль лэйаутов:
  - при смене статуса на `done`/`cancelled` выполняется удаление всех лэйаутов через [`deleteLayoutsForNote`](src/server/features/notes/lib/layout.service.ts);
  - при возврате статуса в `active` из `done`/`cancelled`:
    - для публичной заметки — [`initializeLayoutsForPublicNote`](src/server/features/notes/lib/layout.service.ts);
    - для личной заметки — [`initializeLayoutsForPrivateNote`](src/server/features/notes/lib/layout.service.ts);
  - при изменении `isPublic`:
    - сначала всегда удаляются старые лэйауты;
    - затем лэйауты создаются заново **только если** текущий статус заметки `active`;
    - для `isPublic=true` применяется PBAC-инициализация, для `isPublic=false` — личная.

### 🧪 Проверка

- Выполнен [`npm run typecheck`](package.json) — успешно, без ошибок.


## PBAC hotfix: удаление хардкода ролей из логики трудовых периодов (SCHEDULE_TRACK_MANAGER)

### ✅ Выполнено

- В shared-контракт прав добавлено новое атомарное право [`SCHEDULE_TRACK_MANAGER`](src/shared/contracts/permissions.ts):
  - `schedule:track_manager`.

- В матрицу прав на странице настроек добавлена новая строка в блоке прав графика:
  - [`AppPermission.SCHEDULE_TRACK_MANAGER`](src/client/pages/SettingsPage.vue) с названием «График: Участие в сетке администраторов».

- В [`personnel.service.ts`](src/server/features/personnel/personnel.service.ts) внедрен PBAC-хелпер [`checkRoleHasManagerTrack()`](src/server/features/personnel/personnel.service.ts):
  - добавлен импорт [`rolePermissionsTable`](src/server/features/system/db/role_permissions.table.ts);
  - добавлен импорт [`AppPermission`](src/shared/contracts/permissions.ts);
  - проверка доступа к manager-сетке теперь вычисляется по матрице прав (`resource:action === schedule:track_manager`) с учетом `isEnabled`;
  - для роли `GOD` сохранен глобальный bypass.

- Рефакторинг [`createEmployee()`](src/server/features/personnel/personnel.service.ts):
  - удален role-hardcode `USER_ROLES.MAID` из условия создания базового manager-периода;
  - период `isMaid: false` теперь создается только при наличии права `schedule:track_manager` у роли создаваемого сотрудника.

- Рефакторинг [`updateEmployee()`](src/server/features/personnel/personnel.service.ts):
  - логика переключения периодов при смене роли переведена на diff прав (`oldRoleHasTrack` / `newRoleHasTrack`) вместо проверок `MAID <-> non-MAID`;
  - при потере права manager-сетки открытый период закрывается/удаляется по прежним правилам истории;
  - при получении права manager-сетки открывается manager-период при отсутствии активного;
  - в блоке обработки `hireDate` удален хардкод роли: создание manager-периода теперь делается только если для `targetRole` есть `schedule:track_manager`.

### 🎯 Результат

- Бизнес-логика трудовых периодов больше не зависит от жестко зашитых ролей `MAID`/`MANAGER`/`ADMIN` в ключевых ветках создания и смены роли.
- Управление базовыми manager-периодами (`isMaid: false`) полностью переведено на permission-first модель через `schedule:track_manager`.

---

## Hotfix: корректная синхронизация manager-периодов при роли MAID (personnel)

### ✅ Выполнено

- Исправлено создание сотрудника в [`createEmployee()`](src/server/features/personnel/personnel.service.ts):
  - базовый manager-период (`isMaid: false`) теперь создается только если роль **не** `MAID`;
  - для «чистой» горничной лишний manager-период больше не вставляется.

- Добавлена логика переходов ролей в [`updateEmployee()`](src/server/features/personnel/personnel.service.ts):
  - при переходе **в `MAID`**:
    - ищется последний manager-период;
    - если он открыт и начат сегодня — период удаляется (исправление ошибочно созданного периода);
    - если он открыт и начат ранее — закрывается текущей датой (сохранение истории).
  - при переходе **из `MAID` в не-`MAID`**:
    - при отсутствии активного manager-периода создается новый manager-период с сегодняшней датой.

- Усилена защита обновления `hireDate` в [`updateEmployee()`](src/server/features/personnel/personnel.service.ts):
  - `createManagerPeriod(...)` больше не вызывается для целевой роли `MAID`;
  - обновление даты найма не создает manager-период у горничной.

### 🎯 Результат

- Устранен баг с появлением `isMaid: false` периода у сотрудников с ролью `MAID` при создании.
- Переходы `MAID <-> MANAGER/ADMIN` теперь корректно открывают/закрывают manager-периоды и не ломают данные для графика смен.
- Изменение `hireDate` больше не создает ошибочный manager-период для горничных.

---

## Этап 4.10: Финальная полировка UX заметок (notifications, teleport, pulse, status)

### ✅ Выполнено

- Устранены фантомные уведомления при удалении заметок:
  - в `src/server/features/notifications/notifications.service.ts` функция `deleteAllNotificationsByNoteId()` больше не фильтрует только непрочитанные уведомления;
  - удаляются все уведомления, связанные с noteId, включая уже прочитанные.

- Добавлена блокировка текущего статуса в модалке заметки:
  - в `src/client/features/NoteModal.vue` для кнопок статусов (`active`, `done`, `cancelled`) добавлены `:disabled="status === '...'")`;
  - исключено повторное подтверждение уже установленного статуса.

- Улучшены тексты и телепорт ссылок в уведомлениях:
  - в `src/shared/utils/formatters.ts` текст создания заметки изменен на инклюзивный: `создал(а)`;
  - добавлен новый текст для события `note:content_updated`;
  - в `src/server/features/notes/lib/notifier.service.ts` для `onNoteCreated()` и `onNotePublished()` добавлено поле `link` (`/dashboard/notes?noteId=...`) для корректной навигации из колокольчика.

- Реализовано уведомление об изменении контента заметки:
  - в `src/server/features/notes/lib/notifier.service.ts` добавлена новая функция `onNoteContentUpdated()`;
  - в `src/server/features/notes/notes.events.ts` внутри обработчика `note:updated` добавлен вызов `onNoteContentUpdated(...)` в блоке `if (data.contentUpdated)`.

- Усилена логика пульсации карточек заметок и suppress для автора:
  - в `src/client/entities/note.ts` геттер `hasUpdates` теперь учитывает `updatedAt` (`hasNewContent`);
  - в `updateNote()` после успешного обновления добавлен вызов `markAsRead(id)`, чтобы автор не получал пульсацию от собственного изменения.

### 🎯 Результат

- Уведомления по удаленным заметкам очищаются полностью (без «залипших» read-уведомлений).
- Статусные действия в модалке защищены от повторного клика по текущему значению.
- Уведомления о создании/публикации/обновлении содержания ведут пользователя напрямую в нужную заметку.
- Карточки заметок корректно пульсируют при изменении контента и не пульсируют у автора собственных правок.

### 🔧 Дополнительный фикс по обратной связи

- Уточнена зачистка уведомлений при hard-delete заметки в `deleteAllNotificationsByNoteId()`:
  - в фильтре теперь учитываются все варианты привязки noteId: `data.noteId`, `data.id`, `data.targetId`, `data.link.query.noteId`;
  - это покрывает уведомления типов `note:created`, `note:commented`, `note:content_updated` и устраняет остаточные «фантомы» у всех пользователей.

- Исправлен разбор поля `data` в `pending_notifications`, когда JSON приходит строкой:
  - добавлен хелпер `extractNotificationNoteId(data)` в `src/server/features/notifications/notifications.service.ts`;
  - удаление по noteId теперь работает и для `data` как object, и для `data` как stringified JSON;
  - этот же хелпер применен в `deleteNotificationsByNoteId()` и `deleteAllNotificationsByNoteId()` для консистентного поведения.

- Уточнена логика фиолетовой пульсации для автора заметки:
  - в `hasUpdates` (`src/client/entities/note.ts`) флаг `hasNewContent` теперь срабатывает только если `note.authorId !== currentUserId`;
  - в `createNote()` сразу после локального добавления вызван `markAsRead(response.id)`, чтобы автор не видел пульсацию на собственной только что созданной заметке.

---

## Этап 4.9: NOTES_EDIT_ANY — защита редактирования чужих заметок

### ✅ Выполнено

- В общий контракт прав добавлено новое атомарное право `notes:edit_any`:
  - в [`AppPermission`](src/shared/contracts/permissions.ts) добавлен `NOTES_EDIT_ANY` рядом с `NOTES_EDIT`.

- Обновлена матрица прав на странице настроек:
  - в [`availablePermissions`](src/client/pages/SettingsPage.vue) добавлена строка «Заметки: Редактирование чужих» с `id: AppPermission.NOTES_EDIT_ANY`.

- Усилена серверная бизнес-валидация редактирования заметок в [`updateNote()`](src/server/features/notes/notes.service.ts):
  - в блоке проверки изменений `title/content` добавлена проверка `canEditAny` через [`hasPermission`](src/server/shared/plugins/rbac.ts);
  - если пользователь не является автором заметки и у него нет `NOTES_EDIT_ANY`, выбрасывается ошибка:
    - «У вас нет прав на редактирование текста чужих заметок».

- Обновлен route-guard редактирования заметок:
  - в [`PUT /:id`](src/server/features/notes/notes.routes.ts) расширен `fastify.canAny(...)` правом `AppPermission.NOTES_EDIT_ANY`.

- Обновлена клиентская блокировка полей в модалке заметки:
  - в [`isLocked`](src/client/features/NoteModal.vue) добавлен учет `NOTES_EDIT_ANY` и авторства (`isAuthor`);
  - теперь `title/content` блокируются, если заметка чужая и у пользователя отсутствует право редактирования чужих заметок.

### 🎯 Результат

- Введено отдельное право `NOTES_EDIT_ANY` для точечного управления редактированием чужих заметок.
- Без этого права пользователь может редактировать только свои заметки (при соблюдении остальных ограничений: базовое право на редактирование и правило комментариев).
- Защита реализована консистентно на трех уровнях: контракт, backend-guard/сервис и frontend-lock.

---

## Этап 4.8: Notes — фикс автора в футере и новое право редактирования после комментария

### ✅ Выполнено

- Исправлено отображение автора в карточке заметки для ролей, отличных от `ADMIN`:
  - в [`findWithLayout()`](src/server/features/notes/db/repository/queries.ts:114) и [`findOneWithLayout()`](src/server/features/notes/db/repository/queries.ts:217) добавлен `leftJoin` на таблицу сотрудников и поле `authorName` в ответе API;
  - контракт заметки расширен полем `authorName` в [`noteResponseSchema`](src/shared/contracts/notes.ts:67);
  - клиентская схема репозитория заметок синхронизирована с новым полем в [`noteResponseSchema`](src/client/shared/api/repositories/NoteRepository.ts:18);
  - в [`NoteCard.vue`](src/client/features/NoteCard.vue:54) приоритет источника автора изменен на `note.authorName -> employeeStore -> "Неизвестный автор"`.

- Добавлено новое атомарное право `notes:edit_after_comment`:
  - в shared-контракт прав добавлен `AppPermission.NOTES_EDIT_AFTER_COMMENT` в [`permissions.ts`](src/shared/contracts/permissions.ts:22);
  - в матрицу прав на странице настроек добавлена строка «Заметки: Редактирование после комментария» в [`SettingsPage.vue`](src/client/pages/SettingsPage.vue:64).

- Обновлена backend-логика редактирования заметок после появления комментариев:
  - route-guard обновления заметки переведен на `canAny(NOTES_EDIT, NOTES_EDIT_AFTER_COMMENT)` в [`notes.routes.ts`](src/server/features/notes/notes.routes.ts:136);
  - в [`updateNote()`](src/server/features/notes/notes.service.ts:278) добавлена отдельная проверка `canEditAfterComment`:
    - если есть комментарии, редактирование `title/content` допускается при наличии `NOTES_EDIT_AFTER_COMMENT`;
    - запрет без нового права сохранен;
    - проверка авторства публичной заметки сохранена (редактировать текст может только автор).

- Обновлена frontend-блокировка модалки заметки:
  - в [`isLocked`](src/client/features/NoteModal.vue:132) добавлен учет `NOTES_EDIT_AFTER_COMMENT`, чтобы роль с этим правом могла редактировать заголовок и тело даже после первого комментария.

- Усилен Smart Seeder матрицы прав для обратной совместимости:
  - [`getAllRolePermissions()`](src/server/features/system/system.service.ts:100) теперь не только инициализирует пустую таблицу, но и досеивает недостающие права в уже существующую матрицу;
  - это гарантирует появление новой ячейки `notes:edit_after_comment` без ручного пересоздания `role_permissions`.

### 🎯 Результат

- Для не-админских ролей в футере заметки корректно отображается ФИО автора (при наличии в справочнике сотрудников), вместо ложного «Неизвестный автор».
- В матрице прав появилось отдельное право `notes:edit_after_comment`.
- Роль с этим правом может редактировать заголовок и содержание заметки после первого комментария, при сохранении существующих ограничений авторства/доступа.

---

## Этап 4.7: NOTES_DELETE_ANY в runtime и удаление GOD-hardcode в notes delete/edit flow

### ✅ Выполнено

- Обновлена логика удаления в [`NoteModal.vue`](src/client/features/NoteModal.vue):
  - [`canDeleteNote`](src/client/features/NoteModal.vue:93) полностью переведен на атомарные права `NOTES_DELETE_ANY` / `NOTES_DELETE_OWN` без bypass через `userStore.isGod`;
  - при `NOTES_DELETE_ANY` удаление разрешается безусловно;
  - при `NOTES_DELETE_OWN` удаление разрешается только автору и только при отсутствии комментариев.
  - кнопка удаления в футере больше не зависит от `isLocked`: в шаблоне [`NoteModal.vue`](src/client/features/NoteModal.vue) условие показа изменено на `v-if="canDeleteNote"`, чтобы `NOTES_DELETE_ANY` работало безусловно даже при наличии обсуждения/lock-состояния.

- Удален role-hardcode в блокировке редактирования в [`NoteModal.vue`](src/client/features/NoteModal.vue:129):
  - [`isLocked`](src/client/features/NoteModal.vue:129) больше не использует `userStore.isGod`;
  - блокировка теперь определяется строго по наличию `NOTES_EDIT` и факту наличия обсуждения (`lastCommentAt`).

- Обновлена backend ABAC-логика удаления заметки в [`deleteNote()`](src/server/features/notes/notes.service.ts:464):
  - удалены role-проверки и любые GOD-hardcode ветки;
  - `NOTES_DELETE_ANY` дает безусловное физическое удаление;
  - `NOTES_DELETE_OWN` работает только для автора и только при отсутствии комментариев;
  - отказ в доступе унифицирован сообщением «У вас нет прав для удаления этой заметки».

- Выполнена точечная верификация отсутствия legacy-hack в целевых файлах:
  - по [`src/client/features/NoteModal.vue`](src/client/features/NoteModal.vue) не осталось `userStore.isGod` / role-check bypass;
  - по [`src/server/features/notes/notes.service.ts`](src/server/features/notes/notes.service.ts) не осталось `role === 'GOD'`, `isGod`, `NOTES_MANAGE_ALL`.

### 🎯 Результат

- `NOTES_DELETE_ANY` теперь используется фактически в runtime и на фронте, и на бэке.
- Notes-flow удаления и блокировки редактирования очищен от role-hardcode для `GOD` в рамках затронутых сценариев и приведен к permission-first модели.

---

## Этап 4.6: Техническая гигиена — полный рефакторинг `availablePermissions` на `AppPermission`

### ✅ Выполнено

- В `src/client/pages/SettingsPage.vue` завершен полный перевод массива `availablePermissions` с магических строк на enum `AppPermission`.
- Все элементы массива по модулям `rooms`, `notes`, `tasks`, `schedule`, `personnel`, `paymaster`, `operations`, `refbooks`, `inventory`, `contractors`, `blacklist`, `equipment`, `access` теперь используют `AppPermission.*` в поле `id`.
- Поля `name`, `resource`, `action` оставлены строковыми без изменений (для UI/фильтрации).

- В `src/shared/contracts/permissions.ts` добавлены отсутствующие enum-ключи для уже используемых прав:
  - `TASKS_READ = 'tasks:read'`
  - `PAYMASTER_READ = 'paymaster:read'`

### 🎯 Результат

- Массив `availablePermissions` приведен к единому contract-first подходу без хардкода строковых идентификаторов прав.
- Исключены магические строки в `id`, улучшена типобезопасность и устойчивость к опечаткам.

---

## Этап 4: Финальная зачистка PBAC в справочниках (inventory / blacklist / contractors / equipment / access)

### ✅ Выполнено

- Полностью переведены роуты справочников на `AppPermission` + `fastify.can/fastify.canAny`:
  - [`inventory.routes.ts`](src/server/features/reference_books/inventory.routes.ts)
  - [`contractors.routes.ts`](src/server/features/reference_books/contractors.routes.ts)
  - [`blacklist.routes.ts`](src/server/features/blacklist/blacklist.routes.ts)
  - [`local_it.routes.ts`](src/server/features/reference_books/local_it.routes.ts)
  - [`external_access.routes.ts`](src/server/features/reference_books/external_access.routes.ts)

- По операциям чтения используется `*_READ`, по create/update — `canAny(*_EDIT, *_REQUEST_APPROVAL)`, по delete/approve/reject — `*_EDIT`.

- В роутах обновлены вызовы сервисов: для create/update/approve/reject/delete передается пользователь целиком (`request.user as UserInfo`), вместо передачи role/id и флагов `isAdmin`.

- Сервисный слой рефакторен под ABAC-проверки через `hasPermission/hasAnyPermission` и `user: UserInfo`:
  - [`inventory.service.ts`](src/server/features/reference_books/inventory.service.ts)
  - [`contractors.service.ts`](src/server/features/reference_books/contractors.service.ts)
  - [`blacklist.service.ts`](src/server/features/blacklist/lib/blacklist.service.ts)
  - [`local_it.service.ts`](src/server/features/reference_books/local_it.service.ts)
  - [`access.service.ts`](src/server/features/reference_books/access.service.ts)

- Удалены role-hardcode проверки и флаги `isAdmin`/`role`/`reviewerId` в затронутых сервисах справочников. Логика «можно отклонять/удалять только свои заявки» оставлена как ABAC-условие для пользователей без `*_EDIT`.

- Выполнена зачистка вспомогательных типов/импортов, привязанных к legacy `isAdmin`:
  - [`inventory.schema.ts`](src/server/features/reference_books/inventory.schema.ts)
  - [`contractors.schema.ts`](src/server/features/reference_books/contractors.schema.ts)
  - [`blacklist.schema.ts`](src/server/features/blacklist/blacklist.schema.ts)
  - [`contractors.core.ts`](src/server/features/reference_books/lib/contractors.core.ts)
  - [`contractors.drafts.ts`](src/server/features/reference_books/lib/contractors.drafts.ts)

### ✅ Проверка

- Выполнен [`npm run -s typecheck`](package.json:22) — ошибок типизации нет.

### 🎯 Результат

- Backend-модули справочников завершили миграцию на granular PBAC/ABAC: проверки ролей `ADMIN/MANAGER` и флаги `isAdmin` в целевых модулях заменены на permission-first подход через `AppPermission` и RBAC-утилиты.

---

## Этап 4.5: Удаление `NOTES_MANAGE_ALL` и строгий PBAC для заметок

### ✅ Выполнено

- Обновлен shared-контракт прав в `src/shared/contracts/permissions.ts`:
  - удален `NOTES_MANAGE_ALL = 'notes:manage_all'`;
  - добавлен `NOTES_DELETE_ANY = 'notes:delete_any'`.

- Обновлена матрица прав в `src/client/pages/SettingsPage.vue`:
  - в `availablePermissions` заменена строка `notes:manage_all` на
    `AppPermission.NOTES_DELETE_ANY` с названием «Заметки: Удаление любых».

- Исправлена логика создания заметок (строгий PBAC):
  - `src/client/entities/note/model/useNoteForm.ts`: в `initForm()` удален `canManageAll`, дефолт public теперь только при `!canCreatePrivate && canCreatePublic`;
  - `src/client/features/notes/ui/NoteModal/NoteModalToggles.vue`: `isPublicDisabled` больше не учитывает `manage_all` и опирается только на `create_private/create_public`;
  - `src/server/features/notes/notes.routes.ts` (`POST /`):
    - `preHandler` переведен на `fastify.canAny(AppPermission.NOTES_CREATE_PRIVATE, AppPermission.NOTES_CREATE_PUBLIC)`;
    - удален `hasManageAll` из handler-валидации;
    - проверки создания разделены строго по `isPublic` через `NOTES_CREATE_PUBLIC` / `NOTES_CREATE_PRIVATE`.

- Исправлена логика удаления заметок (`DELETE_ANY`):
  - `src/client/features/NoteModal.vue`: `canDeleteNote` переведен с `manage_all` на `delete_any` (и `GOD` bypass);
  - `src/server/features/notes/notes.service.ts` (`deleteNote`):
    - `NOTES_MANAGE_ALL` заменен на `NOTES_DELETE_ANY`;
    - при `DELETE_ANY` (или `GOD`) удаление выполняется безусловно;
    - без `DELETE_ANY` доступ только `DELETE_OWN + author`, для публичной заметки сохранена проверка на наличие комментариев.

- Зачищены проверки «отмычки» в редактировании и статусах:
  - `src/server/features/notes/notes.routes.ts`:
    - `PUT /:id` теперь только `fastify.can(AppPermission.NOTES_EDIT)`;
    - `POST /:id/status` теперь только `fastify.can(AppPermission.NOTES_CHANGE_STATUS)`;
  - `src/server/features/notes/notes.service.ts` (`updateNote`):
    - удален `hasManageAll` из блока `isPublicChanged`;
    - смена видимости требует строго `NOTES_CREATE_PUBLIC` / `NOTES_CREATE_PRIVATE`;
  - `src/client/features/NoteModal.vue`:
    - `isLocked` больше не использует `manage_all`; bypass оставлен только для `userStore.isGod`;
    - `handleStatusChange` оставлен только на `notes:change_status`.

### 🎯 Результат

- Полностью удалена «отмычка» `notes:manage_all` из notes-flow.
- Создание, изменение видимости, смена статуса и удаление заметок теперь работают по строго гранулярным правам.
- Для удаления любых заметок введено отдельное целевое право `notes:delete_any`.

---

## Этап 3.7: Упрощение прав графика и разблокировка UI ячеек

### ✅ Выполнено

- Удалено избыточное право согласования графика из контрактов и UI-матрицы прав:
  - удален `SCHEDULE_APPROVE` из `AppPermission` в `src/shared/contracts/permissions.ts`;
  - удалена строка `schedule:approve` из `availablePermissions` в `src/client/pages/SettingsPage.vue`.

- Подчищен backend графика:
  - в `src/server/features/schedule/schedule.routes.ts` для `POST /shifts/approve` заменен guard с `fastify.canAny(...)` на `fastify.can(AppPermission.SCHEDULE_MANAGE)`;
  - в `src/server/features/schedule/schedule.service.ts` в `updateShift` и `bulkUpdateShifts` проверка `hasApprove` переведена на `hasPermission(user, AppPermission.SCHEDULE_MANAGE)`;
  - удален неиспользуемый импорт `hasAnyPermission`.

- Разблокировано открытие/редактирование ячеек графика для пользователей с правом запроса согласования:
  - в `src/client/widgets/lib/useScheduleData.ts` добавлен `canEditCell`, который возвращает `true` для `SCHEDULE_MANAGE` или `SCHEDULE_REQUEST_APPROVAL`;
  - `canEditCell` возвращается из composable вместе с `canManageSchedule`;
  - в `src/client/widgets/ScheduleGrid.vue` использование блокировки клика и редактируемости переведено на `canEditCell` (вместо жесткой завязки на `canManageSchedule`).

- Проверено поведение поповера:
  - в `src/client/features/ScheduleCellPopover.vue` выбор базовых статусов не ограничен `canManageSchedule`; этот проп используется только для админского warning-блока при конфликте согласованного/чернового статуса.

### 🎯 Результат

- Право `schedule:approve` полностью удалено из контракта, матрицы и серверной проверки.
- Пользователь с `schedule:request_approval` теперь может открывать поповер ячейки и выбирать базовые статусы.
- Админские действия согласования остаются только у пользователей с `schedule:manage`.

---

## Hotfix: кнопка «Отправить на согласование» для графика не появлялась у менеджера

### ✅ Выполнено

- Найдена причина: на странице [`SchedulePage.vue`](src/client/pages/SchedulePage.vue:30) право для сценария отправки бралось по устаревшему действию `schedule:write`, которого нет в матрице графика.
- Исправлено вычисление [`canWriteSchedule`](src/client/pages/SchedulePage.vue:30):
  - было: `can.value('schedule', 'write')`
  - стало: `can.value('schedule', 'request_approval')`
- Условие показа кнопки [`Отправить на согласование`](src/client/pages/SchedulePage.vue:187) оставлено прежним по логике UX: показывать только при `request_approval`, без `manage`, и при наличии pending-изменений.

### 🎯 Результат

- Пользователь с правом `schedule:request_approval` после изменения ячеек графика получает кнопку отправки на согласование и может зафиксировать свои изменения через штатный flow.

---

## Аудит действий `schedule:*` после удаления `schedule:approve`

### ✅ Выполнено

- Проведен поиск всех упоминаний прав графика по клиенту и серверу.
- Сверка выполнена с актуальным контрактом [`AppPermission`](src/shared/contracts/permissions.ts), где для графика допустимы только:
  - `schedule:read`
  - `schedule:request_approval`
  - `schedule:manage`

### Результат проверки

- Невалидных действий (включая `schedule:write`, `schedule:delete`, `schedule:approve`) в runtime-коде графика не обнаружено.
- Все найденные проверки используют корректные действия:
  - [`SchedulePage.vue`](src/client/pages/SchedulePage.vue) → `manage` и `request_approval`;
  - [`schedule.routes.ts`](src/server/features/schedule/schedule.routes.ts) → `SCHEDULE_READ`, `SCHEDULE_REQUEST_APPROVAL`, `SCHEDULE_MANAGE`;
  - [`schedule.service.ts`](src/server/features/schedule/schedule.service.ts) → `SCHEDULE_MANAGE` и `SCHEDULE_REQUEST_APPROVAL`.

### 🎯 Итог

- Дополнительных замен по правам графика не требуется: матрица, frontend и backend согласованы с текущим контрактом прав.

## PBAC/ABAC Этап 3.6: массовая зачистка роутов и сервисов (Schedule / Operations / Personnel)

### ✅ Выполнено

- Модуль графика смен обновлен на `AppPermission` в [`schedule.routes.ts`](src/server/features/schedule/schedule.routes.ts):
  - добавлены route-guards через `fastify.can/fastify.canAny` для `SCHEDULE_READ`, `SCHEDULE_REQUEST_APPROVAL|SCHEDULE_MANAGE`, `SCHEDULE_APPROVE|SCHEDULE_MANAGE`;
  - удалены role-check ветки с `USER_ROLES.ADMIN`;
  - в обновление смен передается целиком `request.user as UserInfo`.

- Сервис графика смен переведен на permission-first в [`schedule.service.ts`](src/server/features/schedule/schedule.service.ts):
  - сигнатуры [`updateShift`](src/server/features/schedule/schedule.service.ts:236) и [`bulkUpdateShifts`](src/server/features/schedule/schedule.service.ts:544) обновлены до `user: UserInfo`;
  - добавлены проверки через [`hasAnyPermission`](src/server/shared/plugins/rbac.ts:141) и [`hasPermission`](src/server/shared/plugins/rbac.ts:113) с правами `SCHEDULE_APPROVE`, `SCHEDULE_MANAGE`, `SCHEDULE_REQUEST_APPROVAL`;
  - авто-согласование переведено на permission-проверку (`isApproved = hasApprove`), без хардкода ролей.

- Модуль операций переведен на атомарные права в [`operations.routes.ts`](src/server/features/operations/operations.routes.ts):
  - добавлены guards на чтение/изменение/управление через `OPERATIONS_READ`, `OPERATIONS_WRITE`, `OPERATIONS_MANAGE`;
  - для обновления белья в сервис передается `request.user as UserInfo`.

- Сервис операций обновлен для ABAC-проверки в [`operations.service.ts`](src/server/features/operations/operations.service.ts):
  - сигнатура [`upsertDailyLinenUsage`](src/server/features/operations/operations.service.ts:488) переведена на `user: UserInfo`;
  - проверка доступа к прошлым операционным дням переведена с `userRole === 'ADMIN'` на [`hasPermission`](src/server/shared/plugins/rbac.ts:113) c `AppPermission.OPERATIONS_MANAGE`.

- Модуль персонала переведен на `AppPermission` в [`personnel.routes.ts`](src/server/features/personnel/personnel.routes.ts):
  - чтение — `PERSONNEL_READ`;
  - создание/редактирование — `PERSONNEL_WRITE`;
  - удаление — `PERSONNEL_DELETE`;
  - управленческие операции (`fire/rehire/restore`) — `PERSONNEL_MANAGE`.

- Сервис персонала проверен: явных проверок `user.role === USER_ROLES.ADMIN` в [`personnel.service.ts`](src/server/features/personnel/personnel.service.ts) не обнаружено, дополнительных замен не потребовалось.

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:22) — ошибок типизации нет.

### 🎯 Результат

- Роуты и сервисы `schedule/operations/personnel` переведены с role/string-check подхода на granular PBAC/ABAC через `AppPermission` и `hasPermission/hasAnyPermission`.
- Хардкод ролей в затронутых точках доступа устранен, логика согласования и доступа теперь опирается на матрицу прав.

## PBAC/ABAC Этап 3.5: фикс граничных состояний `isPublic` (notes)

### ✅ Выполнено

- Усилена серверная валидация создания заметки в [`notes.routes.ts`](src/server/features/notes/notes.routes.ts):
  - в обработчике [`POST /`](src/server/features/notes/notes.routes.ts) сразу после чтения `body` добавлена строгая ABAC-проверка через динамический импорт [`hasPermission`](src/server/shared/plugins/rbac.ts);
  - при `isPublic=true` требуется `notes:create_public` или `notes:manage_all`;
  - при `isPublic=false` требуется `notes:create_private` или `notes:manage_all`;
  - удалена прежняя частичная проверка только на публичное создание через `hasAnyPermission`.

- Усилена серверная валидация изменения видимости заметки в [`updateNote()`](src/server/features/notes/notes.service.ts:278):
  - после вычисления `isPublicChanged` добавлена ABAC-проверка через динамический импорт [`hasPermission`](src/server/shared/plugins/rbac.ts);
  - при переходе в публичную заметку требуется `notes:create_public` или `notes:manage_all`;
  - при переходе в личную заметку требуется `notes:create_private` или `notes:manage_all`.

- Обновлен дефолт формы заметки в [`useNoteForm.ts`](src/client/entities/note/model/useNoteForm.ts):
  - `initForm()` переведен на permission-first логику с `AppPermission.NOTES_CREATE_PRIVATE`, `AppPermission.NOTES_CREATE_PUBLIC`, `AppPermission.NOTES_MANAGE_ALL`;
  - в режиме `create` дефолт `isPublic=true`, только если нет права на private, но есть public/manage_all;
  - в остальных случаях дефолт `isPublic=false`;
  - в режиме `edit` значение берется из `note.isPublic`.

- Обновлена блокировка тумблера видимости в [`NoteModalToggles.vue`](src/client/features/notes/ui/NoteModal/NoteModalToggles.vue):
  - добавлено вычисление `isPublicDisabled` с учетом режима (`create|edit`) и атомарных прав `NOTES_CREATE_PRIVATE|NOTES_CREATE_PUBLIC|NOTES_MANAGE_ALL`;
  - `:disabled` привязан к новому флагу;
  - добавлена визуальная индикация блокировки через классы `opacity-50 cursor-not-allowed`;
  - добавлен обязательный проп `mode` и обновлен вызов компонента в [`NoteModal.vue`](src/client/features/NoteModal.vue).

### 🎯 Результат

- Поле `isPublic` теперь согласовано с атомарными правами на обоих уровнях:
  - сервер принудительно валидирует право на создание/переключение типа заметки;
  - фронтенд выставляет корректный дефолт и блокирует недопустимые переключения в UI.

---

## PBAC/ABAC Этап 3: защита роутов и ABAC в сервисах (backend)

### ✅ Выполнено

- Заметки: роуты переведены на атомарные права `AppPermission` в [`notes.routes.ts`](src/server/features/notes/notes.routes.ts).
  - `POST /` теперь защищен через `fastify.canAny(notes:create_private | notes:create_public | notes:manage_all)`;
  - в обработчик добавлена ABAC-проверка публичного создания через [`hasAnyPermission`](src/server/shared/plugins/rbac.ts:141) для `NOTES_CREATE_PUBLIC|NOTES_MANAGE_ALL`;
  - `PUT /:id` переведен на `NOTES_EDIT|NOTES_MANAGE_ALL`;
  - `POST /comments` переведен на `requireAuth + NOTES_READ`;
  - `POST /:id/status` переведен на `NOTES_CHANGE_STATUS|NOTES_MANAGE_ALL`;
  - из `DELETE /:id` удалена route-level ABAC логика (`isGod/hasGlobalDeletePerm`), вызов делегирован в сервис.

- Заметки: сервисная ABAC-логика обновлена в [`notes.service.ts`](src/server/features/notes/notes.service.ts).
  - в [`deleteNote`](src/server/features/notes/notes.service.ts:451) удален role-hardcode `ADMIN/MANAGER`;
  - доступ теперь через [`hasPermission`](src/server/shared/plugins/rbac.ts:113):
    - `NOTES_MANAGE_ALL` — удаление любой заметки;
    - `NOTES_DELETE_OWN` + `authorId === user.id` — удаление своей;
    - для всех без `MANAGE_ALL` сохранено ограничение: публичную с комментариями удалять нельзя.
  - в [`updateNote`](src/server/features/notes/notes.service.ts:278) manager-role ветка заменена на проверку `hasManageAll` через `NOTES_MANAGE_ALL`.

- Касса: рефакторинг permission-check в [`paymaster.service.ts`](src/server/features/paymaster/paymaster.service.ts).
  - `validateEditPermissions` сделана асинхронной с сигнатурой `(user: UserInfo, rowOperationalDay: Date | string)`;
  - проверки ролей заменены на права `PAYMASTER_ANY_DATE` и `PAYMASTER_TODAY_ONLY`;
  - добавлены `await` для всех вызовов `validateEditPermissions` в `createPaymasterRow`, `updatePaymasterRow` (оба вызова), `deletePaymasterRow`;
  - сервисные сигнатуры переведены с `userRole: string` на `user: UserInfo`.

- Касса: роуты обновлены в [`paymaster.routes.ts`](src/server/features/paymaster/paymaster.routes.ts).
  - для `POST /rows`, `PUT /rows/:id`, `DELETE /rows/:id` добавлены `preHandler: [requireAuth(), fastify.canAny(PAYMASTER_TODAY_ONLY, PAYMASTER_ANY_DATE)]`;
  - из `POST /rows` удален legacy preHandler с хардкодом `user.role !== 'ADMIN'`;
  - сервисы вызываются с передачей `request.user` целиком.

- Задачи: во все роуты добавлена единая защита в [`tasks.routes.ts`](src/server/features/tasks/tasks.routes.ts):
  - `preHandler: [requireAuth(), fastify.can(AppPermission.TASKS_ACCESS)]` для `GET /`, `GET /:id`, `POST /`, `PATCH /:id`, `DELETE /:id`, `PATCH /:id/toggle`, `POST /positions`, `POST /reorder`.

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:22) — ошибок типизации нет.

### 🎯 Результат

- Backend-контуры `notes/paymaster/tasks` переведены с role-hardcode и legacy `notes:write` на атомарные права `AppPermission`.
- ABAC на удаление заметок централизован в сервисе и основан на `hasPermission/hasAnyPermission` из RBAC-плагина.

---

## PBAC/ABAC Этап 2: очистка UI от role-hardcode и переход на атомарные права

### ✅ Выполнено

- Обновлена матрица прав в [`availablePermissions`](src/client/pages/SettingsPage.vue:56):
  - `notes`: legacy `write/delete/manage` заменены на атомарные `create_private`, `create_public`, `edit`, `change_priority`, `change_status`, `delete_own`, `manage_all` (с понятными названиями в UI);
  - `tasks`: `write/delete/manage` заменены на единое `tasks:access`;
  - `schedule`: `write/delete` заменены на `request_approval` и `approve`, сохранены `read` и `manage`;
  - `paymaster`: `write/delete/manage` заменены на `today_only` и `any_date`;
  - `operations`: добавлено право `cleaning_manage`.

- Проведен ABAC-рефакторинг модалки заметки [`NoteModal.vue`](src/client/features/NoteModal.vue:92):
  - переписан [`canDeleteNote`](src/client/features/NoteModal.vue:92):
    - `true` для `GOD` или `notes:manage_all`;
    - для `notes:delete_own` удаление разрешено только автору при отсутствии комментариев (`noteStore.comments.length === 0`);
  - переписан [`isLocked`](src/client/features/NoteModal.vue:127):
    - `notes:manage_all` снимает блокировку;
    - при наличии комментариев (`lastCommentAt !== null`) заметка блокируется для не-админского полного доступа;
    - при отсутствии `notes:edit` заметка блокируется;
  - ограничен запуск [`handleStatusChange`](src/client/features/NoteModal.vue:223): только при `notes:manage_all` или `notes:change_status`.

- Обновлена проверка управления заметками в [`NoteModalToggles.vue`](src/client/features/notes/ui/NoteModal/NoteModalToggles.vue:29):
  - `notes:manage` заменен на `notes:manage_all`.

- Обновлена логика [`canEdit`](src/client/entities/paymaster.ts:95) в сторе кассы:
  - доступ на любую дату при `paymaster:any_date`;
  - доступ только на текущий операционный день при `paymaster:today_only` и совпадении `selectedDate === currentOperationalDay`;
  - сохранен bypass для `GOD`.

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:22) — ошибок типизации нет.

### 🎯 Результат

- UI-контуры `Settings / Notes / Paymaster` переведены с legacy CRUD/role-логики на атомарные права контракта `AppPermission`.
- Хардкод проверок по ролям в указанных компонентах устранен в пользу permission-first/ABAC проверок через `can(...)` и permission store.

---

## PBAC/ABAC Этап 1: фундамент контрактов, smart seeder и dev-вход горничной

### ✅ Выполнено

- Обновлен shared-контракт прав [`AppPermission`](src/shared/contracts/permissions.ts):
  - для `notes` удалены legacy CRUD-права и добавлены атомарные права:
    - `notes:read`, `notes:create_private`, `notes:create_public`, `notes:edit`, `notes:change_priority`, `notes:change_status`, `notes:delete_own`, `notes:manage_all`;
  - для `tasks` удалены legacy CRUD-права и добавлено единое `tasks:access`;
  - для `schedule` удалены `write/delete`, добавлены `schedule:request_approval` и `schedule:approve`, сохранены `schedule:read` и `schedule:manage`;
  - для `paymaster` удалены legacy CRUD-права и добавлены `paymaster:today_only`, `paymaster:any_date`;
  - для `operations` добавлено право `operations:cleaning_manage`.

- Переписан smart seeder в [`getAllRolePermissions()`](src/server/features/system/system.service.ts:99):
  - удален старый генератор `defaultResources/defaultActions`;
  - заполнение теперь строится по всем значениям `Object.values(AppPermission)`;
  - каждое право разбирается на `resource/action` через `split(':')`;
  - реализована новая матрица `isEnabled`:
    - `GOD`: полный доступ ко всем правам;
    - `ADMIN`: полный доступ, кроме `personnel:manage` и `personnel:delete`;
    - `MAID`: только `notes:read` и `tasks:access`;
    - `MANAGER`: включен заданный базовый набор прав по notes/tasks/personnel/schedule/blacklist/access/contractors/rooms/inventory/operations/paymaster.

- Добавлен dev-шорткат входа для горничной на странице логина [`LoginPage.vue`](src/client/pages/LoginPage.vue):
  - реализована функция [`handleDevLogin()`](src/client/pages/LoginPage.vue:46), которая выставляет `default_user/default_user` и выполняет `userStore.login(...)`;
  - в форму добавлена кнопка «Войти как горничная» после основной кнопки входа.

### 🎯 Результат

- Контракт прав готов к следующему этапу миграции Granular PBAC/ABAC.
- Smart seeding теперь формируется строго из единого источника прав и раздает роли по новой бизнес-матрице.
- Для тестирования доступов MAID добавлен быстрый dev-вход с формы авторизации.

---

## RBAC Matrix Refactor (Enterprise): Shared AppPermission, Dirty Save, Socket Invalidation, RBAC cache fix

### ✅ Выполнено

- Shared Kernel:
  - создан общий контракт [`AppPermission`](src/shared/contracts/permissions.ts) и подключен в общий экспорт [`contracts/index.ts`](src/shared/contracts/index.ts:11);
  - `AppPermission` перенесен из клиентского стора в shared-контракт, дубликат enum удален из [`rbac.ts`](src/server/shared/plugins/rbac.ts);
  - в enum добавлены недостающие read-права справочников:
    - `inventory:read`, `contractors:read`, `blacklist:read`, `equipment:read`, `access:read`.

- Импорты:
  - обновлены импорты `AppPermission` в затронутых client/server файлах на shared-контракт.

- Frontend (Smart Merge для матрицы прав):
  - в [`SettingsPage.vue`](src/client/pages/SettingsPage.vue) добавлен трекер изменений `dirtyPermissions`;
  - в [`togglePermission()`](src/client/pages/SettingsPage.vue:224) фиксируются только измененные ячейки (включая каскадное отключение при снятии `read`);
  - в [`savePermissionsMatrix()`](src/client/pages/SettingsPage.vue:156) payload формируется только из `dirtyPermissions`;
  - после успешного сохранения и после загрузки матрицы `dirtyPermissions` очищается.

- Cross-Stack (Event + Socket):
  - в [`bulkUpdateRolePermissions()`](src/server/features/system/system.service.ts:260) добавлен emit события `system:permissions_updated` с уникальными ролями;
  - в [`system` feature plugin](src/server/features/system/index.ts:8) добавлена проксирующая подписка на EventEmitter и broadcast в Socket.io;
  - в socket-контракты добавлено событие [`system:permissions_updated`](src/server/shared/plugins/socket.ts:27).

- Frontend Permissions Store (cache invalidation):
  - в [`permissions.ts`](src/client/entities/permissions.ts) добавлена socket-подписка на `system:permissions_updated`;
  - при событии стор принудительно сбрасывает `lastFetchTime` и вызывает `fetchMyPermissions()`;
  - добавлены bind/unbind socket events с безопасной очисткой.

- Backend RBAC cache fix:
  - в [`rbac.ts`](src/server/shared/plugins/rbac.ts) `cacheTimestamp` заменен на `cacheTimestamps: Record<string, number>`;
  - `getCachedPermissions()` проверяет TTL пер-роль через `cacheTimestamps[role]`;
  - [`clearPermissionsCache()`](src/server/shared/plugins/rbac.ts:24) очищает timestamp точечно по роли или полностью при глобальном сбросе.

---

## PBAC pipeline sync fix: сквозная синхронизация прав (Backend + Frontend)

### ✅ Выполнено

- Backend cache invalidation:
  - в [`rbac.ts`](src/server/shared/plugins/rbac.ts) добавлена экспортируемая функция [`clearPermissionsCache()`](src/server/shared/plugins/rbac.ts:23) с очисткой по роли или полностью;
  - после `PUT /api/system/permissions` в [`system.routes.ts`](src/server/features/system/system.routes.ts:133) добавлен обязательный вызов [`clearPermissionsCache()`](src/server/shared/plugins/rbac.ts:23).

- Endpoint прав текущего пользователя:
  - в [`GET /permissions/my`](src/server/features/system/system.routes.ts:92) логика приведена к «транслятору» из БД:
    - роль берется из `request.user.role`;
    - читаются записи через [`getRolePermissions()`](src/server/features/system/system.service.ts:160);
    - отдаются только `isEnabled = true`;
    - формат ответа — массив строк `resource:action`.
  - для этого добавлена схема [`getMyPermissionsResponseSchema`](src/server/features/system/system.schema.ts:89).

- Frontend hydration и store-инициализация:
  - в [`permissions.ts`](src/client/entities/permissions.ts) добавлен экшен [`fetchMyPermissions()`](src/client/entities/permissions.ts:185), использующий `/system/permissions/my`;
  - в [`main.ts`](src/client/app/main.ts:198) добавлен pre-hydration прав при старте приложения (для persisted-сессии);
  - в router guard заменен вызов на [`fetchMyPermissions()`](src/client/app/main.ts:83).

- Логика `hasPermission`:
  - упрощена до требуемой модели в [`permissions.ts`](src/client/entities/permissions.ts:119):
    - `GOD -> true`;
    - иначе `state.permissions.includes(permission)`;
    - удалены дополнительные role-based ветки.

- Реактивность UI + logout reset:
  - `read-only` в [`menuConfig.ts`](src/client/shared/config/menuConfig.ts:105) продолжает вычисляться через реактивный [`permissionsStore.hasPermission(...)`](src/client/shared/config/menuConfig.ts:119);
  - при logout в [`user.ts`](src/client/entities/user.ts:84) добавлен [`permissionsStore.$reset()`](src/client/entities/user.ts:84);
  - в [`clearUser()`](src/client/entities/user.ts:103) дополнительно очищается persisted-ключ `permissions` из localStorage.

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:22) — ошибок типизации нет.

### 🎯 Результат

- После изменения матрицы прав GOD-ом бэкенд больше не использует устаревший in-memory кэш.
- Текущий пользователь получает права строго из БД в формате массива разрешенных действий.
- Интерфейс и access-check’и реактивно опираются на актуальные права; при read-only-конфигурации (`rooms:read` без `write/manage/request_approval`) раздел отображается с плашкой `Чтение`, а действия редактирования/создания скрываются.

---

## PBAC Migration: зачистка role-hardcode в backend роутинге

### ✅ Выполнено

- Введен auth-only guard [`requireAuth()`](src/server/shared/lib/auth.ts:68) для проверки JWT без привязки к ролям.
- Во всех backend-роутах фич удалены вызовы `requireRole(...)` и заменены на [`requireAuth()`](src/server/shared/lib/auth.ts:68).
- Сохранены permission-checks через [`fastify.can(...)`](src/server/shared/plugins/rbac.ts:119) там, где они уже были настроены.
- Обновлены импорты в роут-файлах: вместо `requireRole` используется `requireAuth`.

### 🎯 Результат

- В `src/server/features/**/*.routes.ts` больше нет хардкодных role-based guard'ов `requireRole(...)` для доступа к бизнес-эндпоинтам.
- Базовый доступ проверяется через JWT (`requireAuth`), а контроль действий остается на PBAC-правах (`fastify.can`).

---

## ТЗ №8: Динамические права в Sidebar и EmployeeList (Frontend)

### ✅ Выполнено

- [x] **`src/client/entities/permissions.ts`**: удален хардкод `ROLE_PERMISSIONS` и обновлен геттер `hasPermission`
  - `hasPermission` теперь использует динамический `state.permissions`
  - добавлен bypass для роли `GOD`
  - поддержаны проверки как для одного права, так и для массива прав
  - `fetchPermissions()` переведен на загрузку прав с бэкенда через `GET /api/system/permissions/my` с маппингом в формат `resource:action`

- [x] **`src/client/widgets/Sidebar.vue`**: обновлена фильтрация меню на динамическую проверку прав
  - меню теперь фильтруется через `permissionsStore.hasPermission()`
  - проверка выполняется по правилу `<item.id>:read`
  - дополнительно сохранена проверка включенности модулей через `appConfigStore.isModuleEnabled(item.id)`
  - для `GOD` оставлен полный доступ ко всем активным модулям

- [x] **`src/client/widgets/EmployeeList.vue`**: разделены права `write` и `manage`
  - добавлены вычисляемые флаги `canEditEmployee` (`PERSONNEL_WRITE`) и `canManageEmployee` (`PERSONNEL_MANAGE`)
  - кнопка редактирования (карандаш) оставлена на `canEditEmployee`
  - кнопки «Уволить» и «Восстановить» переведены на `canManageEmployee`

### 🎯 Результат

Фронтенд переведен на динамическую модель прав без зависимости от статического ролевого хардкода в критичных точках: проверка прав в сторе, фильтрация бокового меню и разделение управленческих действий сотрудников.

---

## ТЗ №3: Умная плашка "Read-Only" и Панель управления GOD

### ✅ Выполнено

#### 1. SHARED KERNEL (Permissions & Contracts)

- **`src/client/entities/permissions.ts`**: Добавлены индивидуальные права для справочников
  - `INVENTORY_EDIT = 'inventory:edit'` — редактирование инвентаря
  - `INVENTORY_REQUEST_APPROVAL = 'inventory:request_approval'` — запрос согласования для инвентаря
  - `CONTRACTORS_EDIT = 'contractors:edit'` — редактирование контрагентов
  - `CONTRACTORS_REQUEST_APPROVAL = 'contractors:request_approval'` — запрос согласования для контрагентов
  - `BLACKLIST_EDIT = 'blacklist:edit'` — редактирование черного списка
  - `BLACKLIST_REQUEST_APPROVAL = 'blacklist:request_approval'` — запрос согласования для черного списка
  - `EQUIPMENT_EDIT = 'equipment:edit'` — редактирование оборудования
  - `EQUIPMENT_REQUEST_APPROVAL = 'equipment:request_approval'` — запрос согласования для оборудования
  - `ACCESS_EDIT = 'access:edit'` — редактирование внешних доступов
  - `ACCESS_REQUEST_APPROVAL = 'access:request_approval'` — запрос согласования для внешних доступов
  - Обновлен `ROLE_PERMISSIONS` для роли `MANAGER` — добавлены права `REQUEST_APPROVAL` для справочников
  - Обновлен `ROLE_PERMISSIONS` для роли `MAID` — добавлено право `REFBOOKS_READ` (read-only)

#### 2. FRONTEND LAYER (FSD)

##### A. Pages (Reference Books)

- **`src/client/pages/InventoryPage.vue`**: Обновлена для использования прав вместо ролей
  - Импортирован `usePermissions` из `@/shared/lib/usePermissions`
  - Заменена проверка `canEdit = computed(() => userStore.isManagerOrAdmin)` на `canEdit = computed(() => can.value('inventory', 'edit'))`
  - Добавлена проверка `canRequestApproval = computed(() => can.value('inventory', 'request_approval'))`
  - Обновлены хедеры и FAB кнопка для использования новых проверок

- **`src/client/pages/ContractorsPage.vue`**: Обновлена для использования прав вместо ролей
  - Импортирован `usePermissions` из `@/shared/lib/usePermissions`
  - Заменена проверка `canEdit = computed(() => userStore.isManagerOrAdmin)` на `canEdit = computed(() => can.value('contractors', 'edit'))`
  - Добавлена проверка `canRequestApproval = computed(() => can.value('contractors', 'request_approval'))`
  - Обновлены хедеры и FAB кнопка для использования новых проверок

- **`src/client/pages/BlacklistPage.vue`**: Обновлена для использования прав вместо ролей
  - Импортирован `usePermissions` из `@/shared/lib/usePermissions`
  - Заменена проверка `canEdit = computed(() => ['ADMIN', 'MANAGER'].includes(currentRole.value))` на `canEdit = computed(() => can.value('blacklist', 'edit'))`
  - Добавлена проверка `canRequestApproval = computed(() => can.value('blacklist', 'request_approval'))`
  - Обновлены хедеры и FAB кнопка для использования новых проверок

- **`src/client/pages/EquipmentPage.vue`**: Обновлена для использования прав вместо ролей
  - Импортирован `usePermissions` из `@/shared/lib/usePermissions`
  - Добавлена проверка `canEdit = computed(() => can.value('equipment', 'edit'))`
  - Добавлена проверка `canRequestApproval = computed(() => can.value('equipment', 'request_approval'))`
  - Обновлены хедеры и FAB кнопка для использования новых проверок

- **`src/client/pages/AccessPage.vue`**: Обновлена для использования прав вместо ролей
  - Импортирован `usePermissions` из `@/shared/lib/usePermissions`
  - Добавлена проверка `canEdit = computed(() => can.value('access', 'edit'))`
  - Добавлена проверка `canRequestApproval = computed(() => can.value('access', 'request_approval'))`
  - Обновлены хедеры и FAB кнопка для использования новых проверок

##### B. Shared (Menu Config)

- **`src/client/shared/config/menuConfig.ts`**: Обновлена функция `isReadOnlyForRole` для использования прав
  - Импортирован `usePermissionsStore` и `AppPermission` из `@/entities/permissions`
  - Добавлена логика проверки прав для справочников:
    - Для `inventory`: проверка `INVENTORY_EDIT` и `INVENTORY_REQUEST_APPROVAL`
    - Для `contractors`: проверка `CONTRACTORS_EDIT` и `CONTRACTORS_REQUEST_APPROVAL`
    - Для `blacklist`: проверка `BLACKLIST_EDIT` и `BLACKLIST_REQUEST_APPROVAL`
    - Для `access`: проверка `ACCESS_EDIT` и `ACCESS_REQUEST_APPROVAL`
    - Для `equipment`: проверка `EQUIPMENT_EDIT` и `EQUIPMENT_REQUEST_APPROVAL`
  - Плашка "Read-only" показывается только если нет прав ни на редактирование, ни на запрос согласования

##### C. Pages (Settings - GOD Control Panel)

- **`src/client/pages/SettingsPage.vue`**: Создана панель управления GOD с тремя вкладками
  - **Вкладка "Логи"**: Существующая панель управления логами (`LogControlPanel`)
  - **Вкладка "Матрица прав"**: Интерфейс для управления правами доступа
    - Таблица с правами (строки) и ролями (Столбцы: ADMIN, MANAGER, MAID)
    - Чекбоксы для включения/отключения прав для каждой роли
    - Кнопка "Сохранить" для сохранения изменений через `PUT /api/system/permissions`
    - Загрузка матрицы прав через `GET /api/system/permissions`
  - **Вкладка "Модули системы"**: Интерфейс для управления включенными модулями
    - Список всех доступных модулей с описаниями
    - Тумблеры (Switch) для включения/отключения модулей
    - Кнопка "Сохранить" для сохранения изменений через `PUT /api/system/modules`
    - Автоматическое обновление `appConfigStore` после сохранения
    - Динамическое скрытие пунктов меню в Sidebar при изменении модулей

#### 3. UI INTEGRATION (Sidebar)

- **`src/client/widgets/Sidebar.vue`**: Уже реагирует на изменения в `appConfigStore.enabledModules`
  - Использует `appConfigStore.isModuleEnabled(item.moduleName)` для фильтрации пунктов меню
  - При отключении модуля пункт меню автоматически скрывается без перезагрузки страницы
  - При включении модуля пункт меню автоматически появляется без перезагрузки страницы

### 📋 Технические детали

#### Permission-Based Read-Only Badge
- Плашка "Read-only" показывается только если у пользователя нет прав ни на редактирование, ни на запрос согласования
- Логика: `!canEdit && !canRequestApproval`
- Это позволяет гибко настраивать доступ без привязки к жестким ролям

#### GOD Control Panel
- Доступно только для роли `GOD`
- Три вкладки: Логи, Матрица прав, Модули системы
- Сохранение изменений через REST API
- Автоматическое обновление конфигурации при сохранении модулей

#### Dynamic Sidebar
- Sidebar использует `computed` свойство `filteredMenu` для реактивного обновления
- При изменении `appConfigStore.enabledModules` меню автоматически обновляется
- GOD роль видит все пункты меню независимо от включенных модулей

### 🎯 Результат

Реализована умная плашка "Read-Only" и панель управления GOD:
1. ✅ Добавлены индивидуальные права для справочников (edit, request_approval)
2. ✅ Обновлен маппинг прав для ролей MANAGER и MAID
3. ✅ Рефакторинг плашки "Read-Only" для использования прав вместо ролей
4. ✅ Обновлены все страницы справочников для использования новых проверок
5. ✅ Создана панель управления GOD с тремя вкладками
6. ✅ Реализована вкладка "Матрица прав" с управлением правами для ролей
7. ✅ Реализована вкладка "Модули системы" с тумблерами для включения/отключения модулей
8. ✅ Sidebar уже реагирует на изменения в `appConfigStore.enabledModules` (без перезагрузки страницы)

Функционал готов к тестированию и использованию.

---

## ТЗ: Глобальный аудит и зачистка хардкода ролей (Frontend PBAC Migration)

### ✅ Выполнено

- [x] Проведен глобальный аудит по `src/client/**/*.vue` и `src/client/**/*.ts` на role-based проверки (`ADMIN|MANAGER|MAID`).
- [x] Полностью переработан [`menuConfig`](src/client/shared/config/menuConfig.ts) в permission-first модель:
  - удалены `AppRole`, `roles`, `readOnlyModeFor`, `getFilteredMenuItems`, `isReadOnlyForRole`;
  - добавлен [`isReadOnlyForPermissions()`](src/client/shared/config/menuConfig.ts:92) с проверкой прав по модулям;
  - сохранен bypass для `GOD`.
- [x] Обновлен [`Sidebar`](src/client/widgets/Sidebar.vue):
  - удалены role-based импорты/типы;
  - read-only логика переведена на [`isReadOnlyForPermissions()`](src/client/shared/config/menuConfig.ts:92).
- [x] Рефакторинг страниц справочников и связанных модалок:
  - [`EquipmentPage`](src/client/pages/EquipmentPage.vue): доступ и UI-гейты переведены на `equipment:read|edit|request_approval`.
  - [`InventoryPage`](src/client/pages/InventoryPage.vue): убраны role-checks (`isManager/isAdmin`), сообщения/ветвления переведены на `canApprove`.
  - [`ContractorsPage`](src/client/pages/ContractorsPage.vue): убраны role-checks, логика approve/reject и сообщений переведена на permission-based.
  - [`BlacklistPage`](src/client/pages/BlacklistPage.vue): удален `currentRole`, исправлена типобезопасность tooltip-ховера через `hoveredEntry`.
  - [`RoomsPage`](src/client/pages/RoomsPage.vue): все `userStore.isAdmin` заменены на `rooms:write|manage|delete`.
  - [`ContractorModal`](src/client/features/ContractorModal.vue) и [`BlacklistModal`](src/client/features/BlacklistModal.vue): предупреждения о согласовании переведены на `*:approve` permission.
- [x] Дополнительно устранены устаревшие role-derived API в сторах/композаблах:
  - [`userStore`](src/client/entities/user.ts): удалены `isAdmin` и `isManagerOrAdmin`.
  - [`usePermissions`](src/client/shared/lib/usePermissions.ts): удален `isPrivileged` (role-based).
  - [`operationsStore`](src/client/entities/operations.ts) и [`paymasterStore`](src/client/entities/paymaster.ts): historical-edit доступ теперь permission-based (`*:write|manage`) + bypass `GOD`.
- [x] Переведена role-based логика расписания на permission-модель:
  - [`SchedulePage`](src/client/pages/SchedulePage.vue): action-панель строится по `schedule:write|manage`.
  - [`ScheduleGrid`](src/client/widgets/ScheduleGrid.vue): поведение редактирования/согласования больше не зависит от `ADMIN|MANAGER|MAID`, используется `canManageSchedule/canWriteSchedule`.
  - [`ScheduleCellPopover`](src/client/features/ScheduleCellPopover.vue): конфликтный warning по `canManageSchedule`.
  - [`useScheduleData`](src/client/widgets/lib/useScheduleData.ts): убраны role-фильтры в выдаче managers/maids.
  - [`shiftStore`](src/client/entities/shift.ts): `setCellStatus()` переведен с `currentUserRole` на `canManageSchedule`.
- [x] Зачищены role-based значения в заметках, влияющие на права:
  - [`useNoteForm`](src/client/entities/note/model/useNoteForm.ts): дефолт `isPublic` для create переведен с `ADMIN` на `notes:manage` (+ `GOD`).
  - [`NoteModalToggles`](src/client/features/notes/ui/NoteModal/NoteModalToggles.vue): блокировка visibility-тоггла переведена на `notes:manage`.
  - [`NoteModal`](src/client/features/NoteModal.vue): manager-lock ветки переведены на `notes:write && !notes:manage`.

### 🔍 Проверки

- Поиск по `src/client` не находит role-based access-check паттерны:
  - `userStore.isAdmin`
  - `isManagerOrAdmin`
  - `isReadOnlyForRole`
  - `getFilteredMenuItems`
  - role comparisons `=== 'ADMIN'|'MANAGER'|'MAID'` в проверках доступа
- Сохранен единственный прямой bypass по роли: `GOD`.

### 🎯 Итог

Frontend слой переведен на PBAC-подход: проверки доступа и read-only состояния больше не завязаны на hardcoded роли `ADMIN|MANAGER|MAID`, а работают через динамическую матрицу прав (`permissionsStore`) с сохранением глобального bypass для `GOD`.

---

## ТЗ №2: Фронтенд ядро (Стор прав, Конфиг модулей и Public-роутинг)

### ✅ Выполнено

#### 1. SHARED KERNEL (Constants & Contracts)
- **`src/shared/constants/roles.ts`**: Проверены константы ролей (`USER_ROLES`)
  - `ADMIN`, `MANAGER`, `MAID`, `GOD` — роли для RBAC
  - `STAFF_ROLES` — роли, которые могут быть упомянуты и получать уведомления
  - `PRIVILEGED_ROLES` — роли с повышенными правами

#### 2. BACKEND LAYER (VSA Slice)
- **`src/server/features/system/system.service.ts`**: Проверен сервис для работы с правами и модулями
  - `getRolePermissions(role)` — получение прав для роли
  - `getAllSystemModules()` — получение всех системных модулей
  - `checkRolePermission(role, resource, action)` — проверка конкретного права

- **`src/server/features/system/system.routes.ts`**: Проверены роуты для работы с правами и модулями
  - `GET /api/system/modules` — публичный роут для получения списка модулей
  - `GET /api/system/permissions` — GOD-only роут для получения всех прав

- **`src/server/shared/plugins/rbac.ts`**: Проверен RBAC плагин
  - `AppPermission` enum — все права системы
  - `ROLE_PERMISSIONS` — маппинг ролей к правам (fallback)
  - `hasPermission(user, permission)` — проверка права пользователя

#### 3. FRONTEND LAYER (FSD)

##### A. Entities (App Config & Permissions)

- **`src/client/entities/appConfig.ts`**: Обновлен стор для работы с модулями
  - Изменен эндпоинт с `/config/modules` на `/system/modules`
  - Обновлена схема валидации для парсинга ответа от бэкенда
  - `fetchConfig()` — получение списка активных модулей
  - `isModuleEnabled(moduleName)` — проверка включенности модуля
  - `isLoaded` — проверка актуальности конфигурации (5 минут кэш)

- **`src/client/entities/permissions.ts`**: Создан новый стор для управления правами
  - `AppPermission` enum — все права системы (скопирован из бэкенда)
  - `ROLE_PERMISSIONS` — маппинг ролей к правам (fallback)
  - `fetchPermissions(role)` — получение прав для роли
  - `hasPermission(permission)` — проверка конкретного права
  - `hasAnyPermission(permissions)` — проверка наличия любого из прав
  - `hasAllPermissions(permissions)` — проверка наличия всех прав
  - `can(resource, action)` — проверка права по ресурсу и действию
  - Поддержка кэширования (5 минут)

- **`src/client/entities/index.ts`**: Добавлен экспорт `permissions` стор

##### B. Shared (Composables)

- **`src/client/shared/lib/usePermissions.ts`**: Создан хук для проверки прав в UI компонентах
  - `can(resource, action)` — проверка права по ресурсу и действию
  - `hasPermission(permission)` — проверка конкретного права
  - `hasAnyPermission(permissions)` — проверка наличия любого из прав
  - `hasAllPermissions(permissions)` — проверка наличия всех прав
  - `isGuest` — проверка, что пользователь не авторизован
  - `isPrivileged` — проверка, что пользователь имеет привилегированную роль

- **`src/client/shared/lib.ts`**: Добавлен экспорт `usePermissions`

##### C. Router Guards (main.ts)

- **`src/client/app/main.ts`**: Обновлены навигационные гарды
  - Импортированы `usePermissionsStore` и `usePermissions`
  - **Гард модулей**: Проверка включенности модуля перед навигацией
    - Если модуль отключен — редирект на `/dashboard/notes` с уведомлением
  - **Гард прав**: Проверка прав доступа к роуту
    - Если `meta.publicAllowed === true` — пускаем гостей
    - Если пользователь авторизован — проверяем права через `can(resource, action)`
    - Если прав нет — редирект на `/dashboard/notes`
  - Автоматическая загрузка конфигурации и прав при старте приложения

### 📋 Технические детали

#### Module Guard
- Проверка `to.meta.moduleName` для определения модуля роута
- Использование `appConfigStore.isModuleEnabled(moduleName)`
- Редирект на `/dashboard/notes` если модуль отключен
- Отображение alert с названием отключенного модуля

#### Permission Guard
- Проверка `to.meta.publicAllowed` для определения публичного роута
- Проверка `to.meta.resource` и `to.meta.action` для определения прав
- Использование `can.value(resource, action)` для проверки прав
- GOD роль обходит все проверки прав

#### Static Permissions (Fallback)
- `ROLE_PERMISSIONS` используется как fallback для прав
- Может быть заменен на динамическую загрузку из бэкенда
- Поддержка всех ролей: GUEST, ADMIN, MANAGER, MAID, GOD

### 🎯 Результат

Реализовано фронтенд ядро для работы с правами и модулями:
1. ✅ Обновлен `appConfigStore` для работы с `/api/system/modules`
2. ✅ Создан `permissionsStore` для управления правами пользователей
3. ✅ Создан хук `usePermissions` для проверки прав в UI компонентах
4. ✅ Обновлены навигационные гарды с проверкой модулей и прав
5. ✅ Поддержка публичных роутов для гостей

Функционал готов к тестированию и использованию.

---

## ТЗ №4.1: Агрегированный отчет по Операционным Дням (09:00-09:00)

### ✅ Выполнено

#### 1. SHARED KERNEL (Logic & Contracts)
- **`src/shared/utils/formatters.ts`**: Реализована функция [`getOperationalDay()`](src/shared/utils/formatters.ts:227) для расчета операционного дня
  - Логика: Если время < 09:00, возвращаем `date - 1 день`. Если ≥ 09:00, возвращаем `date`
  - Формат: Строго `YYYY-MM-DD`
  - Использование `dayjs.tz(..., 'Europe/Moscow')` для корректной работы с часовыми поясами

- **`src/shared/contracts/paymaster.ts`**: Проверена схема [`paymasterRowSchema`](src/shared/contracts/paymaster.ts:30)
  - `operationalDay` определен как `z.string()` (строка в формате YYYY-MM-DD)
  - Добавлена схема [`paymasterPeriodReportSchema`](src/shared/contracts/paymaster.ts:150) для агрегированного отчета за период

#### 2. BACKEND LAYER (VSA Slice)
- **`src/server/features/paymaster/paymaster.service.ts`**: Реализован метод [`getPeriodReport()`](src/server/features/paymaster/paymaster.service.ts:320)
  - Фильтрация записей по колонке `operationalDay` (тип DATE в MySQL)
  - SQL запрос: `WHERE operational_day BETWEEN :startDate AND :endDate`
  - Использование `dayjs.tz(..., 'Europe/Moscow')` для всех расчетов времени
  - Возврат агрегированных данных: записи, итоги, границы периода

- **`src/server/features/paymaster/paymaster.routes.ts`**: Добавлен роут [`/period-report`](src/server/features/paymaster/paymaster.routes.ts:285)
  - Query параметры: `startDate` и `endDate` (формат YYYY-MM-DD)
  - Валидация через Zod
  - Возврат [`paymasterPeriodReportSchema`](src/server/features/paymaster/paymaster.schema.ts:22)

#### 3. FRONTEND LAYER (FSD)

##### A. Entities (Store & Author Guard)
- **`src/client/entities/paymaster.ts`**: Обновлен [`createRow()`](src/client/entities/paymaster.ts:142)
  - Автоматическое определение `operationalDay` через [`getOperationalDay()`](src/shared/utils/formatters.ts:227) если не передан явно
  - Добавлен метод [`fetchPeriodReport()`](src/client/entities/paymaster.ts:240) для получения отчета за период
  - Реэкспорт типа `PaymasterPeriodReport`

##### B. Widgets & Print (Enterprise Style)
- **`src/client/widgets/PaymasterPeriodResult.vue`**: Создан виджет для отображения агрегированного отчета
  - Печатная форма: Шапка с названием периода и датой формирования
  - Таблица с `page-break-inside: auto` для корректной печати
  - Скрытие колонки «Действия» в режиме печати (отсутствует в таблице)
  - Итоги по периоду с цветовой кодировкой
  - Форматирование валюты и дат

##### C. UI Integration
- **`src/client/pages/PaymasterPage.vue`**: Обновлена страница для поддержки отчета за период
  - Кнопка «Сформировать за период» (только для вкладки «Кассовый отчет»)
  - Модальное окно с выбором диапазона дат (DateRangePicker)
  - По умолчанию: начало текущего месяца до «вчерашнего» операционного дня
  - Отображение отчета в полном экране с возможностью печати
  - Интеграция с [`PaymasterPeriodResult`](src/client/widgets/PaymasterPeriodResult.vue:1)

### 📋 Технические детали

#### Timezone Lock
- Запрещено использовать `new Date()` без обертки в `dayjs.tz`
- Операционный день в 08:59 и в 09:01 — это разные финансовые периоды
- Все расчеты времени используют `dayjs.tz(..., 'Europe/Moscow')`

#### Snake/Camel Mapping
- `operational_day` из базы (snake_case) преобразуется в `operationalDay` (camelCase) перед попаданием в стор
- Маппинг выполняется в бэкенде сервисе

#### Idempotency Check
- При получении обновлений через сокеты для отчета, сверять `lastUpdateAuthorId !== currentUserId` (Rule 3.I.6)
- Это предотвращает перерисовку таблицы, если действие совершил сам пользователь

### 🎯 Результат

Реализован полный функционал агрегированного отчета по операционным дням (09:00-09:00):
1. ✅ Функция расчета операционного дня с учетом часового пояса
2. ✅ Backend API для получения отчета за период
3. ✅ Frontend store и repository для работы с отчетом
4. ✅ Виджет с печатной формой и итогами
5. ✅ Интеграция в страницу кассового отчета

Функционал готов к тестированию и использованию.

---

## ТЗ №5: Иконографика и UI-полировка модуля «Текущие операции»

### ✅ Выполнено

#### 1. SIDEBAR NAVIGATION (Sidebar & Menu)
- **`src/client/shared/config/menuConfig.ts`**: Пункт «Текущие операции» уже имеет иконку [`ClipboardList`](src/client/shared/config/menuConfig.ts:113)
- **`src/client/widgets/Sidebar.vue`**: Добавлен импорт иконки [`ClipboardList`](src/client/widgets/Sidebar.vue:45) из lucide-vue-next
- **`src/client/widgets/Sidebar.vue`**: Иконка добавлена в [`iconMap`](src/client/widgets/Sidebar.vue:89) для корректного рендеринга через динамический компонент `<component :is="getIconComponent(item.icon)" />`

#### 2. PAGE TABS (Paymaster Hub)
- **`src/client/pages/PaymasterPage.vue`**: Добавлен импорт иконки [`Users`](src/client/pages/PaymasterPage.vue:7) из lucide-vue-next
- **`src/client/pages/PaymasterPage.vue`**: Вкладка «Заезд/Выезд» теперь содержит иконку [`Users`](src/client/pages/PaymasterPage.vue:366) слева от текста
- **`src/client/pages/PaymasterPage.vue`**: Вкладка «Кассовый отчет» теперь содержит иконку [`Banknote`](src/client/pages/PaymasterPage.vue:376) слева от текста

#### 3. СТИЛИЗАЦИЯ (Tailwind & Transitions)
- **Размер иконок**: Использован размер `:size="18"` для всех иконок во вкладках
- **Плавные переходы**: Добавлен класс `transition-colors` для плавного изменения цвета при наведении и переключении вкладок
- **Цвет активной иконки**: Цвет активной иконки совпадает с основным акцентным цветом проекта (`text-blue-600`)
- **Цвет неактивной иконки**: Использован `text-gray-500` для неактивных вкладок

### 🎯 Результат

Реализована полная иконографика для модуля «Текущие операции»:
1. ✅ Иконка `ClipboardList` для пункта меню в Sidebar
2. ✅ Иконка `Users` для вкладки «Заезд/Выезд»
3. ✅ Иконка `Banknote` для вкладки «Кассовый отчет»
4. ✅ Плавные переходы цветов при переключении вкладок
5. ✅ Акцентный цвет для активной вкладки

Визуальный интерфейс модуля «Текущие операции» полностью полирован и готов к использованию.

---

## ТЗ №4.2: Промежуточные и финальные итоги (Strict 5-Field Aggregation)

### ✅ Выполнено

#### 1. SHARED CONTRACT (Data Structure)
- **`src/shared/contracts/paymaster.ts`**: Обновлена схема [`paymasterTotalsSchema`](src/shared/contracts/paymaster.ts:89)
  - Строгий формат из 5 полей: `cash`, `card`, `advance`, `other`, `expense`
  - Удалены поля: `income`, `expenseTotal`, `netTotal`
  - Добавлена схема [`paymasterDayReportSchema`](src/shared/contracts/paymaster.ts:136) для отчета за один день
  - Обновлена схема [`paymasterPeriodReportSchema`](src/shared/contracts/paymaster.ts:143) для нового формата ответа
    - Формат: `{ days: [...], grandTotals: {...} }`
    - `days`: массив отчетов по дням с промежуточными итогами
    - `grandTotals`: финальные итоги за весь период

#### 2. BACKEND LAYER (VSA Slice)
- **`src/server/features/paymaster/paymaster.service.ts`**: Обновлен метод [`calculateTotalsByDate()`](src/server/features/paymaster/paymaster.service.ts:243)
  - Возврат строгого формата из 5 полей (удалены вычисляемые поля)

- **`src/server/features/paymaster/paymaster.service.ts`**: Переписан метод [`getPeriodReport()`](src/server/features/paymaster/paymaster.service.ts:337)
  - Группировка записей по `operationalDay`
  - Расчет промежуточных итогов для каждого операционного дня
  - Расчет финальных итогов (`grandTotals`) за весь период
  - Формат ответа: `{ days: [{ date, rows, totals }], grandTotals }`

#### 3. FRONTEND LAYER (FSD)

##### A. Entities (Store Update)
- **`src/client/entities/paymaster.ts`**: Обновлен стор [`usePaymasterStore`](src/client/entities/paymaster.ts:42)
  - Инициализация `totals` с5 полями (соответствует новому контракту)
  - Вычисляемые свойства `totalIncome`, `totalExpense`, `netTotal` сохранены для совместимости

##### B. Widget (Table Layout)
- **`src/client/widgets/PaymasterPeriodResult.vue`**: Обновлен виджет для отображения промежуточных и финальных итогов
  - **Промежуточные итоги**: Строка-разделитель после записей каждого дня
    - Стилизация: серый фон, жирный шрифт
    - Формат: «Итого за [дата]: Нал: X | Б/н: X | Аванс: X | Прочее: X | Расход: X»
  - **Финальные итоги**: 5 карточек внизу страницы
    - Наличные, Безнал, Аванс, Прочее, Расход
    - Цветовая кодировка для каждого типа
  - **Print Mode**: Промежуточные итоги включены в печатную версию
    - Скрытие элементов управления (кнопки удаления, фильтры)
    - Чистый лог операций с промежуточными суммами после каждого блока даты

### 📋 Технические детали

#### Strict 5-Field Aggregation
- Контракт `paymasterTotalsSchema` содержит только «первичку»: `cash`, `card`, `advance`, `other`, `expense`
- Вычисляемые поля (`income`, `expenseTotal`, `netTotal`) удалены из контракта
- Frontend вычисляет эти поля локально (в сторе) для совместимости с существующим кодом

#### Intermediate Totals (Day Level)
- Группировка записей по `operationalDay` на бэкенде
- Промежуточные итоги рассчитываются для каждого дня отдельно
- Отображаются в виде строки-разделителя в таблице

#### Grand Totals (Period Level)
- Финальные итоги агрегируются из всех дней периода
- Отображаются в виде 5 карточек внизу страницы
- Используются для общей оценки финансового состояния за период

### 🎯 Результат

Реализована система промежуточных и финальных итогов для отчета за период:
1. ✅ Обновлен контракт `paymasterTotalsSchema` до строгих 5 полей
2. ✅ Backend группирует данные по дням и рассчитывает промежуточные итоги
3. ✅ Backend рассчитывает финальные итоги за весь период
4. ✅ Frontend отображает промежуточные итоги после каждого дня
  5. ✅ Frontend отображает финальные итоги в виде 5 карточек
  6. ✅ Print mode включает промежуточные итоги в печатную версию

Функционал готов к тестированию и использованию.

---

## ТЗ №6: Day.js Protocol для системы Заметок (Notes)

### ✅ Выполнено

#### 1. BACKEND LAYER (VSA Slice)
- **`src/server/features/notes/db/notes.repository.ts`**: Применен Day.js Protocol для всех операций с датами
  - Добавлен комментарий `// Day.js Protocol: Backend uses dayjs.utc() for all date operations`
  - **`findWithLayout()`**: Заменен `.toISOString()` на `dayjs.utc(date).toISOString()` для всех полей дат
  - **`findOneWithLayout()`**: Заменен `.toISOString()` на `dayjs.utc(date).toISOString()` для всех полей дат
  - **`findById()`**: Заменен `.toISOString()` на `dayjs.utc(date).toISOString()` для всех полей дат
  - **`findComments()`**: Заменен `.toISOString()` на `dayjs.utc(date).toISOString()` для всех полей дат
  - **`findCommentById()`**: Заменен `.toISOString()` на `dayjs.utc(date).toISOString()` для всех полей дат
  - **`findHistory()`**: Заменен `.toISOString()` на `dayjs.utc(date).toISOString()` для всех полей дат
  - **`createHistoryEntry()`**: Заменен `new Date()` на `dayjs.utc().toDate()`
  - **`markNoteAsViewed()`**: Заменен `new Date()` на `dayjs.utc().toDate()`
  - Удалены проверки `instanceof Date` — все даты теперь обрабатываются через dayjs.utc()

#### 2. SHARED CONTRACT (Zod Alignment)
- **`src/shared/contracts/notes.ts`**: Добавлена строгая валидация Zod v4
  - Импортирован `z` из `zod`
  - **`notePrioritySchema`**: `z.enum(['low', 'normal', 'high'])`
  - **`noteStatusSchema`**: `z.enum(['active', 'done', 'cancelled'])`
  - **`noteHistoryActionSchema`**: `z.enum(['created', 'updated', 'status_changed', 'commented', 'returned', 'priority_changed'])`
  - **`noteLayoutSchema`**: Все поля дат — `z.string().datetime().nullable()`
  - **`noteCommentSchema`**: Все поля дат — `z.string().datetime()`
  - **`noteHistoryEntrySchema`**: Все поля дат — `z.string().datetime()`
  - **`noteResponseSchema`**: Все поля дат — `z.string().datetime().nullable()` (СТРОГО ISO 8601)
  - **`noteCreatedEventSchema`**: Заменен `any` на `noteResponseSchema`
  - **`noteUpdatedEventSchema`**: Заменен `any` на `noteResponseSchema`
  - **`commentCreatedEventSchema`**: Все поля дат — `z.string().datetime()`
  - **`personalNoteReminderEventSchema`**: Все поля дат — `z.string().datetime()`
  - Удалены все `any` — строгая типизация для всех схем

#### 3. FRONTEND LAYER (FSD)

##### A. Entities (Store & Author Guard)
- **`src/client/entities/note.ts`**: Внедрены dayjs плагины и Rule 3.I.6 (Author Guard)
  - Импортированы плагины `utc` и `timezone` из dayjs
  - Инициализированы плагины: `dayjs.extend(utc)` и `dayjs.extend(timezone)`
  - Добавлен комментарий `// Day.js Protocol: Frontend uses dayjs.utc() for comparison, local display for UI`

##### B. Getters (Smart Merge & Day.js UTC)
- **`filteredNotes`**: Заменен `new Date().getTime()` на `dayjs.utc(date).valueOf()` при сортировке
- **`hasUnreadComments`**:
  - Добавлен комментарий `// Rule 3.I.6 (Author Guard)`
  - Заменен `dayjs(date)` на `dayjs.utc(date)` для корректного сравнения UTC дат
- **`hasUserMention`**:
  - Добавлен комментарий `// Rule 3.I.6 (Author Guard)`
  - Добавлена проверка `if (note.lastCommentAuthorId === currentUserId) return false`
  - Заменен `dayjs(date)` на `dayjs.utc(date)` для корректного сравнения UTC дат
- **`hasUpdates`**:
  - Добавлен комментарий `// Rule 3.I.6 (Author Guard)`
  - Заменен `dayjs(date)` на `dayjs.utc(date)` для корректного сравнения UTC дат
- **`combinedActivities`**: Заменен `new Date().getTime()` на `dayjs.utc(date).valueOf()` при сортировке

##### C. Actions (Optimistic UI)
- **`markAsRead()`**: Заменен `new Date().toISOString()` на `dayjs.utc().toISOString()`
- **`patchNoteDates()`**: Заменен `new Date().toISOString()` на `dayjs.utc().toISOString()`

### 📋 Технические детали

#### Day.js Protocol (Backend)
- Бэкенд использует `dayjs.utc()` для всех операций с датами
- Все даты из базы данных преобразуются через `dayjs.utc(date).toISOString()`
- Запрещено использовать `new Date()` без обертки в `dayjs.utc()`
- Хранение в базе: UTC Timestamp
- API Response: ISO 8601 String

#### Day.js Protocol (Frontend)
- Фронтенд использует `dayjs.utc()` для сравнения дат
- Локальное отображение в UI через `dayjs().tz('Europe/Moscow')`
- Все таймстемпы генерируются через `dayjs.utc().toISOString()`
- Инициализированы плагины `utc` и `timezone`

#### Contract-First (Zod v4)
- Все поля дат в контрактах — `z.string().datetime().nullable()`
- Удалены все `any` — строгая типизация
- Типы на фронте — `z.infer` от схем в `@shared/contracts/`

#### Author Guard (Rule 3.I.6)
- `hasUserMention`: Не показывает "новое", если упоминание от текущего пользователя
- `hasUnreadComments`: Не показывает "новое", если комментарий от текущего пользователя
- `hasUpdates`: Не показывает "новое", если приоритет изменен текущим пользователем

### 🎯 Результат

Применен Day.js Protocol для системы заметок:
1. ✅ Backend использует `dayjs.utc()` для всех операций с датами
2. ✅ Контракт имеет строгую валидацию Zod v4 с `z.string().datetime().nullable()`
3. ✅ Frontend использует `dayjs.utc()` для сравнения дат
4. ✅ Author Guard внедрен в геттеры `hasUserMention`, `hasUnreadComments`, `hasUpdates`
5. ✅ Удалены все `new Date()` и `any` — строгая типизация и единый протокол работы с датами

Функционал готов к тестированию и использованию.

---

## ТЗ №7: Исправление отзыва заявок менеджером и видимости одобренных записей (Contractors)

### ✅ Выполнено

#### 1. BACKEND LAYER (VSA Slice)

##### A. Права на отзыв (contractors.routes.ts)
- **`src/server/features/reference_books/contractors.routes.ts`**: Роут [`POST /contractors/reject/:id`](src/server/features/reference_books/contractors.routes.ts:92) уже имеет правильную логику
  - Разрешен доступ для ролей `ADMIN` и `MANAGER`
  - Для `MANAGER`: проверка `entry.createdBy === userId` перед удалением
  - Возврат 403 если MANAGER пытается отклонить чужую запись

##### B. События (contractors.workflow.ts)
- **`src/server/features/reference_books/lib/contractors.workflow.ts`**: События уже генерируются
  - [`approveContractorEntry()`](src/server/features/reference_books/lib/contractors.workflow.ts:19) вызывает `notifyContractorsUpdate('contractors:updated', ...)` после успешного одобрения
  - [`rejectContractorEntry()`](src/server/features/reference_books/lib/contractors.workflow.ts:130) вызывает `notifyContractorsUpdate('contractors:updated', ...)` после успешного отклонения
  - События отправляются в сокеты через SocketService

#### 2. FRONTEND LAYER (FSD)

##### A. Socket Events (contractor.ts store)
- **`src/client/entities/contractor.ts`**: Store уже имеет методы для работы с сокетами
  - [`bindSocketEvents()`](src/client/entities/contractor.ts:299): подписка на события `CONTRACTOR_UPDATED`, `CONTRACTOR_CREATED`, `CONTRACTOR_DELETED`
  - [`unbindSocketEvents()`](src/client/entities/contractor.ts:311): отписка от всех событий
  - [`handleExternalChange()`](src/client/entities/contractor.ts:290): универсальный обработчик с Refetch Strategy

##### B. Page Integration (ContractorsPage.vue)
- **`src/client/pages/ContractorsPage.vue`**: Добавлена интеграция с сокетами и кнопка отзыва для менеджера
  - Добавлен [`currentUserId`](src/client/pages/ContractorsPage.vue:31) для проверки авторства
  - Добавлена функция [`isEntryAuthor()`](src/client/pages/ContractorsPage.vue:34) для проверки авторства записи
  - Добавлена функция [`canRejectEntry()`](src/client/pages/ContractorsPage.vue:41) для проверки прав на отклонение
    - `ADMIN`: может отклонить любую pending запись
    - `MANAGER`: может отклонить только свои pending записи
  - Добавлена функция [`getRejectButtonTitle()`](src/client/pages/ContractorsPage.vue:53) для динамического заголовка кнопки
  - В [`onMounted()`](src/client/pages/ContractorsPage.vue:276) добавлен вызов `contractorStore.bindSocketEvents()`
  - В [`onUnmounted()`](src/client/pages/ContractorsPage.vue:284) добавлен вызов `contractorStore.unbindSocketEvents()`
  - Обновлен шаблон для отображения кнопки отзыва:
    - Кнопка "Одобрить" (✓) показывается только для ADMIN
    - Кнопка "Отклонить"/"Отменить запрос" (✗) показывается для ADMIN и MANAGER (только свои записи)

### 📋 Технические детали

#### Permission Logic (Backend)
- Роут `POST /contractors/reject/:id` использует `requireRole('ADMIN', 'MANAGER')`
- Для MANAGER: дополнительная проверка `entry.createdBy === userId` через `getContractorPendingEntryById()`
- При несоответствии: `throw new AppError('Вы можете отклонить только свои запросы', 403, 'FORBIDDEN')`

#### Event-Driven Architecture
- После `approveContractorEntry()` и `rejectContractorEntry()` генерируется событие `contractors:updated`
- Событие отправляется через `notifyContractorsUpdate()` в SocketService
- Frontend store подписывается на `CONTRACTOR_UPDATED`, `CONTRACTOR_CREATED`, `CONTRACTOR_DELETED`
- При получении события вызывается `handleExternalChange()` → `fetchAll()` + `fetchPending()`

#### UI/UX (Frontend)
- Для pending записей показываются кнопки действий:
  - ADMIN: "Одобрить" + "Отклонить"
  - MANAGER (только свои записи): "Отменить запрос"
- Для одобренных записей показываются кнопки: "Редактировать" + "Удалить"
- Использование флагов `isNew`, `isModified`, `isPendingDelete` вместо `status` для определения pending состояния

### 🎯 Результат

Исправлен функционал отзыва заявок менеджером и видимости одобренных записей:
1. ✅ Backend проверяет права MANAGER на отклонение только своих записей
2. ✅ Backend генерирует события `contractors:updated` после approve/reject
3. ✅ Frontend store подписывается на события через сокеты
4. ✅ Frontend страница вызывает `bindSocketEvents()`/`unbindSocketEvents()` в lifecycle hooks
5. ✅ MANAGER видит кнопку "Отменить запрос" для своих pending записей
6. ✅ ADMIN видит кнопки "Одобрить" и "Отклонить" для всех pending записей

Функционал готов к тестированию и использованию.

---

## PBAC hotfix: восстановление загрузки прав для не-GOD пользователей

### ✅ Выполнено

- Добавлен отсутствующий endpoint [`GET /api/system/permissions/my`](src/server/features/system/system.routes.ts:91), который возвращает права текущей роли из таблицы `role_permissions`.
- Endpoint защищен через [`requireRole(...)`](src/server/features/system/system.routes.ts:92) с допуском для `ADMIN`, `MANAGER`, `MAID`, `GOD` (при этом `GOD` сохраняет bypass).
- Логика получает роль из [`request.user`](src/server/features/system/system.routes.ts:102) и возвращает список через [`getRolePermissions(...)`](src/server/features/system/system.routes.ts:110).
- Устранена причина 404 на запросе фронта к `/api/system/permissions/my`, из-за которой у обычных пользователей не загружалась матрица прав и скрывалось меню.

### 🎯 Результат

- Меню и кнопки теперь могут вычисляться из динамической матрицы прав для всех ролей, а не только для `GOD`.
- Хардкод ролей в клиентских access-check остается удаленным; источник прав — только backend-матрица.

---

## Hotfix: восстановление управления ролью горничной и создания учетки при редактировании сотрудника

### ✅ Выполнено

- Исправлена логика отображения секции «Управление ролью горничной» в [`useEmployeeForm`](src/client/entities/employee/model/useEmployeeForm.ts):
  - секция снова доступна для роли `MANAGER` даже при пустом списке периодов;
  - это возвращает возможность вручную добавлять первый период горничной.

- Исправлен сценарий «создать учетную запись» в режиме редактирования сотрудника без логина:
  - в [`useEmployeeForm`](src/client/entities/employee/model/useEmployeeForm.ts) добавлено формирование `payload.createUser` для `edit`-режима, если у сотрудника нет учетной записи;
  - для `MAID` логин генерируется автоматически (как и при создании нового сотрудника);
  - для `ADMIN/MANAGER` отправляются введенные логин и пароль.

- Добавлена backend-поддержка создания учетки из `PUT /employees/:id` в [`updateEmployee()`](src/server/features/personnel/personnel.service.ts):
  - учтен флаг `createUser` и состояние «сотрудник без логина»;
  - добавлена валидация обязательного логина и пароля (пароль обязателен для не-`MAID`);
  - скорректирован апдейт `username` для промоута в пользователя;
  - для `MAID` при промоуте применяется дефолтный password hash.

- Обновлен shared-контракт [`EmployeeUpdate`](src/shared/contracts/personnel.ts):
  - добавлено поле `createUser?: boolean` для типобезопасной передачи флага из фронта в backend.

### 🎯 Результат

- В карточке сотрудника снова работает блок «Управление ролью горничной».
- При редактировании сотрудника без учетной записи чекбокс «Создать учетную запись» корректно создает учетку, а не только на этапе первичного создания сотрудника.

---

## PBAC hotfix: read-first логика доступа в разделы и матрицу прав

### ✅ Выполнено

- Обновлены backend-guards для «Локального оборудования» в [`local_it.routes.ts`](src/server/features/reference_books/local_it.routes.ts):
  - вместо role-only ограничений `ADMIN` добавлены проверки через [`fastify.can()`](src/server/features/reference_books/local_it.routes.ts:30);
  - чтение всех списков/карточек переведено на `equipment:read`;
  - create/update/delete переведены на `equipment:edit`;
  - сохранен допуск по ролям [`requireRole()`](src/server/shared/lib/auth.ts:69), но финальный доступ теперь определяется матрицей прав.

- Исправлено кэширование прав на фронтенде в [`permissions.ts`](src/client/entities/permissions.ts):
  - в [`isLoaded`](src/client/entities/permissions.ts:170) добавлена проверка соответствия `currentRole` текущей роли пользователя;
  - устранен сценарий, когда после смены пользователя/роли использовался устаревший persisted-кэш и в меню показывались недоступные разделы.

- Реализована read-first зависимость чекбоксов в матрице прав в [`SettingsPage.vue`](src/client/pages/SettingsPage.vue):
  - при снятии `*:read` автоматически снимаются все остальные права этого ресурса;
  - если `*:read` выключено, остальные права ресурса становятся неактивными (disabled);
  - добавлена функция [`isPermissionDisabled()`](src/client/pages/SettingsPage.vue:254) и обновлена логика [`togglePermission()`](src/client/pages/SettingsPage.vue:219).

### 🎯 Результат

- Если у роли нет права `read` на ресурс, раздел больше не должен отображаться в сайдбаре (в связке с существующей фильтрацией в [`Sidebar.vue`](src/client/widgets/Sidebar.vue:96)).
- Для «Локального оборудования» backend теперь синхронизирован с PBAC-матрицей и не блокирует менеджера ролью при наличии нужных прав.
- В матрице прав обеспечено консистентное поведение: без `read` нельзя включить/оставить `write|delete|manage|edit|request_approval`.

---

## PBAC hotfix v2: обязательное "Чтение" в матрице и единый бейдж доступа

### ✅ Выполнено

- В матрицу прав добавлены отсутствующие `*:read` для разделов-справочников в [`availablePermissions`](src/client/pages/SettingsPage.vue:53):
  - `inventory:read`
  - `contractors:read`
  - `blacklist:read`
  - `equipment:read`
  - `access:read`

- Бейдж режима только чтения в сайдбаре локализован:
  - заменен текст `Read-only` на `Чтение` в [`Sidebar.vue`](src/client/widgets/Sidebar.vue:216).

- Расширена read-only логика для всех основных модулей меню в [`isReadOnlyForPermissions()`](src/client/shared/config/menuConfig.ts:106):
  - добавлены ветки для `notes`, `tasks`, `schedule`, `paymaster`;
  - если у роли есть только `read`, раздел остается доступным для просмотра с плашкой `Чтение`.

### 🎯 Результат

- Для разделов, где у роли отсутствует `read`, пункт не отображается в меню (через текущую фильтрацию в [`filteredMenu`](src/client/widgets/Sidebar.vue:97)).
- Если включено только `read`, раздел отображается в меню и помечается плашкой `Чтение`.
- Матрица ролей теперь содержит явные права `Чтение` для всех пользовательских разделов/справочников.

---

## PBAC рефакторинг: скрытие меню по read, read-only UI и защита роутов

### ✅ Выполнено

- Усилена навигационная защита в [`router.beforeEach()`](src/client/app/main.ts:27):
  - добавлен fallback-маршрут на первый доступный раздел через [`getFirstAccessibleDashboardRoute()`](src/client/app/main.ts:42);
  - проверка прав выполняется не только по `meta.resource`, но и по `meta.moduleName` (как `read` по умолчанию), что закрывает обход для роутов без явного `resource/action`.

- Добавлены явные `meta.resource/meta.action` для всех основных dashboard-роутов:
  - [`rooms.routes.ts`](src/client/pages/rooms.routes.ts), [`notes.routes.ts`](src/client/pages/notes.routes.ts), [`tasks.routes.ts`](src/client/pages/tasks.routes.ts), [`employees.routes.ts`](src/client/pages/employees.routes.ts), [`schedule.routes.ts`](src/client/pages/schedule.routes.ts), [`blacklist.routes.ts`](src/client/pages/blacklist.routes.ts), [`access.routes.ts`](src/client/pages/access.routes.ts), [`contractors.routes.ts`](src/client/pages/contractors.routes.ts), [`inventory.routes.ts`](src/client/pages/inventory.routes.ts), [`equipment.routes.ts`](src/client/pages/equipment.routes.ts), [`paymaster.routes.ts`](src/client/pages/paymaster.routes.ts).

- Для раздела «База номеров» добавлена ранняя защита на странице:
  - в [`RoomsPage.vue`](src/client/pages/RoomsPage.vue:573) добавлена проверка `rooms:read` до загрузки данных;
  - при отсутствии права выполняется `router.replace('/dashboard')`, что предотвращает 403-вызов [`roomStore.fetchRooms()`](src/client/entities/room.ts:28) и падение в `mounted`.

- Приведена логика «только чтение» для операций редактирования:
  - в [`operations.ts`](src/client/entities/operations.ts:65) удален bypass «можно редактировать текущий день без прав»;
  - в [`paymaster.ts`](src/client/entities/paymaster.ts:94) удален аналогичный bypass;
  - теперь `operations.canEdit` и `paymaster.canEdit` зависят только от матрицы прав (`write/manage`) и роли `GOD`.

- Техническая чистка:
  - удален неиспользуемый импорт `Component` из [`menuConfig.ts`](src/client/shared/config/menuConfig.ts:4).

### 🎯 Результат

- Если снять `read` для раздела в матрице прав — пункт исчезает из меню и прямой переход по URL блокируется guard’ом.
- Если у роли оставлен только `read` — UI создания/редактирования (включая `+`) неактивен за счет строгой проверки `write/manage`.
- Для «Базы номеров» устранен сценарий с 403 и `Unhandled error during mounted hook` при отсутствии `rooms:read`.

---

## Hotfix: MANAGER с rooms:write/rooms:delete не мог изменять «Базу номеров»

### ✅ Выполнено

- Найдена причина: в [`rooms.routes.ts`](src/server/features/rooms/rooms.routes.ts:30) для `POST/PUT/DELETE` стоял жесткий role-guard `requireRole('ADMIN')`, который срабатывал раньше PBAC-проверки и блокировал менеджера даже при включенных правах в матрице.

- Исправлен role-guard для операций изменения номеров:
  - [`POST /api/rooms`](src/server/features/rooms/rooms.routes.ts:30) → `requireRole('ADMIN', 'MANAGER')`
  - [`PUT /api/rooms/:id`](src/server/features/rooms/rooms.routes.ts:90) → `requireRole('ADMIN', 'MANAGER')`
  - [`DELETE /api/rooms/:id`](src/server/features/rooms/rooms.routes.ts:114) → `requireRole('ADMIN', 'MANAGER')`

- При этом сохранены PBAC-проверки через [`fastify.can()`](src/server/features/rooms/rooms.routes.ts:32):
  - создание/изменение по `rooms:write`
  - удаление по `rooms:delete`

### 🎯 Результат

- Менеджер с включенными правами `rooms:write` и `rooms:delete` в матрице теперь может добавлять/редактировать/удалять номера без ошибки `Insufficient permissions. Required roles: ADMIN`.

---

## Hotfix: read-only режим в «База номеров» (бейдж + скрытие «Добавить номер»)

### ✅ Выполнено

- Исправлена логика read-only бейджа для пункта «База номеров» в [`isReadOnlyForPermissions()`](src/client/shared/config/menuConfig.ts:105):
  - модуль теперь определяется как `item.moduleName ?? item.id`, что покрывает элементы меню без явного `moduleName`;
  - для `rooms` read-only определяется строго как «есть только `rooms:read`, но нет `rooms:write|rooms:delete|rooms:manage`».

- Усилены UI-гейты на странице [`RoomsPage`](src/client/pages/RoomsPage.vue:409):
  - добавлен вычисляемый флаг `isRoomsReadOnly`;
  - FAB `+` («Добавить номер») скрывается при read-only (`v-if="canEditRooms && !isRoomsReadOnly"`);
  - в [`headerStore.setHandlers(...)`](src/client/pages/RoomsPage.vue:597) кнопка создания в шапке также отключается в read-only;
  - добавлен `watch` прав для реактивного обновления хедера при изменении permission state;
  - в [`addNewRow()`](src/client/pages/RoomsPage.vue:637) добавлена защитная проверка от создания строк без прав.

### 🎯 Результат

- Если у менеджера по модулю `rooms` только `read`, в сайдбаре отображается бейдж `Чтение`.
- В режиме только чтения кнопка «Добавить номер» (`+`) не отображается ни как FAB, ни через action в хедере.

---

## PBAC fix: RoomsPage на permissionsStore + универсальный Read-Only в menuConfig

### ✅ Выполнено

- В [`RoomsPage.vue`](src/client/pages/RoomsPage.vue) удален устаревший хук `usePermissions` и переход выполнен на централизованный PBAC через [`usePermissionsStore`](src/client/entities/permissions.ts):
  - удален импорт `usePermissions`;
  - удалена инициализация `const { can } = usePermissions();`;
  - права `canReadRooms/canEditRooms/canDeleteRooms/isRoomsReadOnly` теперь считаются через [`permissionsStore.hasPermission(...)`](src/client/pages/RoomsPage.vue).

- В [`menuConfig.ts`](src/client/shared/config/menuConfig.ts) полностью упрощена функция [`isReadOnlyForPermissions`](src/client/shared/config/menuConfig.ts):
  - удалены хардкод-ветки `if` по каждому модулю;
  - добавлено универсальное PBAC-правило через динамические права:
    - `${moduleName}:write`
    - `${moduleName}:manage`
    - `${moduleName}:request_approval`
  - сохранено исключение для `GOD` и системных разделов `dashboard/settings`.

### 🎯 Результат

- Права в «Базе номеров» вычисляются строго из единого PBAC-стора без legacy-оберток.
- Меню больше не содержит дублирующих модульных `if` для read-only бейджа — правило единообразно применяется ко всем разделам.

---

## Hotfix: отвязка read-first зависимости для ресурсов без `read` в матрице прав

### ✅ Выполнено

- В [`togglePermission()`](src/client/pages/SettingsPage.vue:331) обновлена логика включения не-`read` прав:
  - добавлена проверка существования базового права через `hasReadAction`;
  - блокировка включения дополнительных прав теперь применяется только если для ресурса реально существует `resource:read` в [`availablePermissions`](src/client/pages/SettingsPage.vue:68).

- В [`isPermissionDisabled()`](src/client/pages/SettingsPage.vue:375) добавлена аналогичная защита:
  - если у ресурса отсутствует действие `read`, чекбоксы больше не становятся `disabled`;
  - прежнее read-first поведение сохранено для ресурсов, где `read` присутствует.

### 🎯 Результат

- Для модуля `system` (права `system:manage`, `system:receive_mentions`) чекбоксы в матрице прав теперь активны и кликабельны.
- Для модулей с базовым правом чтения (`rooms`, `notes` и др.) логика осталась прежней: включение дополнительных прав без `read` запрещено.

---

## ТЗ: Гостевой вход через системный аккаунт (`is_system`) и роль `VIEWER`

### ✅ Выполнено

- Добавлена новая роль `VIEWER` в shared-роль-константы [`USER_ROLES`](src/shared/constants/roles.ts).
- Роль «Наблюдатель» добавлена в список ролей на странице настроек [`SettingsPage.vue`](src/client/pages/SettingsPage.vue).

- Расширена схема пользователей в Drizzle:
  - enum `role` дополнен значением `VIEWER` в [`employees.table.ts`](src/server/features/personnel/db/employees.table.ts);
  - добавлено поле `isSystem: boolean('is_system').notNull().default(false)` в [`employees.table.ts`](src/server/features/personnel/db/employees.table.ts).

- Сгенерирована миграция для БД:
  - создан файл [`0003_lucky_enchantress.sql`](drizzle/0003_lucky_enchantress.sql) с изменениями:
    - `ALTER TABLE users MODIFY COLUMN role ... 'VIEWER' ...`;
    - `ALTER TABLE users ADD is_system boolean DEFAULT false NOT NULL`.

- На backend добавлена фильтрация системных аккаунтов в бизнес-списках:
  - в [`getEmployees()`](src/server/features/personnel/personnel.service.ts) и [`getEmployeesByStatus()`](src/server/features/personnel/personnel.service.ts) добавлено условие `eq(employees.isSystem, false)`;
  - в [`getEmployeeById()`](src/server/features/personnel/personnel.service.ts) и [`getEmployeeByIdIncludingArchived()`](src/server/features/personnel/personnel.service.ts) исключены `is_system=true`;
  - в [`getUsersForScheduleGrid()`](src/server/features/personnel/lib/employment.service.ts) добавлена фильтрация `eq(employees.isSystem, false)`;
  - в [`getActiveUsers()`](src/server/features/schedule/schedule.service.ts) и в выборке имен для экспорта смен в [`exportShiftsToCSV()`](src/server/features/schedule/schedule.service.ts) исключены системные аккаунты.

- Реализован гостевой auth-flow на backend:
  - в [`auth.service.ts`](src/server/features/auth/auth.service.ts) добавлен метод `guestLogin()`:
    - ищет системного пользователя `isSystem = true` и `role = VIEWER`;
    - если не найден — создает seed-аккаунт `guest_system` с ФИО `Гость (Система)`;
    - возвращает стандартный `AuthResult` как в обычном логине;
  - в [`auth.routes.ts`](src/server/features/auth/auth.routes.ts) добавлен роут `POST /api/auth/guest`:
    - без body-валидации пароля;
    - выставляет стандартную cookie `auth_token`;
    - возвращает `user` в том же формате, что `POST /login`.

- Обновлен frontend под новый endpoint:
  - в [`AuthRepository`](src/client/shared/api/repositories/AuthRepository.ts) добавлен метод `guestLogin()` (`POST /auth/guest`);
  - в [`user` store](src/client/entities/user.ts) добавлен action `guestLogin()`;
  - кнопка «Войти без пароля» в [`LoginPage.vue`](src/client/pages/LoginPage.vue) переключена с `default_user/default_user` на новый flow `userStore.guestLogin()`;
  - в [`AuthLogin.vue`](src/client/features/AuthLogin.vue) добавлен обработчик `handleGuestLogin()` и кнопка «Войти без пароля», использующая новый action.

- Синхронизированы типы/схемы под роль `VIEWER`:
  - расширены role union в [`src/shared/contracts/personnel.ts`](src/shared/contracts/personnel.ts), [`schedule.schema.ts`](src/server/features/schedule/schedule.schema.ts), [`ScheduleRepository.ts`](src/client/shared/api/repositories/ScheduleRepository.ts), [`personnel.schema.ts`](src/server/features/personnel/personnel.schema.ts);
  - smart-seeding матрицы прав расширен ролью `VIEWER` в [`system.service.ts`](src/server/features/system/system.service.ts) (по умолчанию включаются только `*:read` права).

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:22) — успешно, без ошибок типизации.

---

## Hotfix: падение [`EquipmentModal`](src/client/features/EquipmentModal.vue) при редактировании записи IT

### ✅ Выполнено

- В [`EquipmentModal`](src/client/features/EquipmentModal.vue:191) добавлена безопасная нормализация `accessPorts` через функцию [`normalizeAccessPorts()`](src/client/features/EquipmentModal.vue:191).
- Устранена причина ошибки `TypeError: ...map is not a function` на строке заполнения формы редактирования:
  - ранее использовалось прямое `(itItem.accessPorts || []).map(...)`, что падало при не-массивном значении;
  - теперь перед маппингом выполняется приведение входа к корректному массиву портов.
- Добавлена поддержка обоих форматов источника `accessPorts`:
  - массив объектов;
  - JSON-строка с массивом.
- Для некорректного формата включен fail-safe: возвращается пустой массив без падения UI.

### 🎯 Результат

- При открытии редактирования IT-оборудования модалка больше не падает на watcher.
- Поле «Дополнительные доступы» корректно инициализируется даже при «грязных» данных из API/БД.
- Поведение сохранения остаётся совместимым с текущим контрактом [`AccessPort`](src/client/shared/api/repositories/EquipmentRepository.ts:18).

---

## Hotfix: не блокировать кнопку «Сохранить» из-за формата `networkAddress`

### ✅ Выполнено

- В вычислении [`isFormValid`](src/client/features/EquipmentModal.vue:102) для IT-категории убрана зависимость от [`validationErrors.networkAddress`](src/client/features/EquipmentModal.vue:114).
- Формат `networkAddress` продолжает валидироваться в [`saveItem()`](src/client/features/EquipmentModal.vue:310) и в live-обработчике [`handleInput()`](src/client/features/EquipmentModal.vue:384), но больше не отключает кнопку submit.

### 🎯 Результат

- Кнопка «Сохранить» остаётся активной при невалидном сетевом адресе.
- Пользователь может нажать «Сохранить» и получить явную ошибку валидации поля без блокировки UI.

---

## ТЗ: удаление избыточных глобальных прав модуля справочников (`refbooks`)

### ✅ Выполнено

- В [`AppPermission`](src/shared/contracts/permissions.ts) удалены глобальные права справочников:
  - `REFBOOKS_READ = 'refbooks:read'`
  - `REFBOOKS_WRITE = 'refbooks:write'`
  - `REFBOOKS_DELETE = 'refbooks:delete'`
  - `REFBOOKS_MANAGE = 'refbooks:manage'`
- Секция индивидуальных прав справочников (`INVENTORY_*`, `CONTRACTORS_*`, `BLACKLIST_*`, `EQUIPMENT_*`, `ACCESS_*`) сохранена без изменений.

- В [`availablePermissions`](src/client/pages/SettingsPage.vue) удалены элементы матрицы прав для `AppPermission.REFBOOKS_*`:
  - `Справочники: Чтение`
  - `Справочники: Запись`
  - `Справочники: Удаление`
  - `Справочники: Управление`

- Проведена глобальная проверка по исходникам `src/**`:
  - упоминаний `REFBOOKS_` и `refbooks:` не найдено;
  - проверок вида `can('refbooks', '...')` не найдено.

### ✅ Проверка

- Выполнен [`npm run typecheck`](package.json:1) — успешно, ошибок TypeScript нет.

### 🎯 Результат

- Из контрактов и UI-матрицы полностью удалены избыточные глобальные права `refbooks`.
- В «Матрице прав» больше не формируется строка/группа `Модуль: refbooks`.

---

## Security/Pattern Hardening: доп. этап проверки и усилений

### ✅ Выполнено

- Завершена унификация эмитов событий к стандартному payload через `emitEvent(...)`:
  - `src/server/features/rooms/rooms.events.ts`
  - `src/server/features/personnel/personnel.events.ts`
  - `src/server/features/blacklist/lib/blacklist.events.ts`
  - `src/server/shared/db/base.service.ts`

- Проверка прямых вызовов `emitter.emit(` по `src/server/**` показала отсутствие несанкционированных эмитов (кроме внутреннего вызова внутри `events.ts`).

- Выполнен build-check:
  - `npm run -s build` — успешно.

- Проверена cookie-политика в runtime через HTTP-запрос к guest login:
  - `Set-Cookie` содержит `HttpOnly`, `SameSite=Lax`, `Path=/`, `Max-Age` (dev-режим).

- Выполнен browser smoke:
  - guest login успешен;
  - сокет подключается и пользователь входит в персональную room;
  - выявлен отдельный runtime-дефект: `GET /api/employees/{id}` возвращает `500 Employee not found` для guest-пользователя.

### ⚠️ Замечания по quality gate

- `npm run lint` в текущем состоянии репозитория не может быть полноценно использован из-за отсутствия flat-конфига ESLint v9 (`eslint.config.*`).
- Рекомендация: добавить `eslint.config.js`/`eslint.config.mjs` и повторно запустить lint.

### 🎯 Результат

- Дополнительный этап hardening завершен: события стандартизированы, сборка проходит, cookie/socket поведение подтверждено.
- Обнаруженный backend runtime-дефект с guest employee lookup зафиксирован отдельно как follow-up bugfix.
