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

## Текущие задачи

### [x] Task 13: Переход на HARD DELETE для "Базы номеров"

#### Backend изменения:

1. **Удалено поле `archived_at` из схемы таблицы `rooms`** (`src/server/features/rooms/db/rooms.table.ts`):
   - Поле `archivedAt` удалено из схемы Drizzle
   - Таблица теперь содержит только актуальные поля для номеров

2. **Обновлен метод `deleteRoom` в сервисе** (`src/server/features/rooms/rooms.service.ts`):
   - Заменена логика мягкого удаления (soft delete) на физическое удаление (hard delete)
   - Вместо `db.update()` с установкой `archivedAt` теперь используется `db.delete()`
   - Удален импорт `isNull` (больше не нужен для фильтрации по `archivedAt`)

3. **Обновлены Zod-схемы** (`src/server/features/rooms/rooms.schema.ts`):
   - Удалено поле `archivedAt` из `roomResponseSchema`
   - Типы ответов теперь не содержат поле архивации

4. **Обновлен роут GET /** (`src/server/features/rooms/rooms.routes.ts`):
   - Удален параметр `archived` из querystring
   - Метод `getAllRooms` больше не принимает параметр `archived`

#### Frontend изменения:

1. **Обновлен тип `Room`** (`src/client/shared/api.ts`):
   - Удалено поле `archivedAt` из интерфейса `Room`

2. **Обновлен метод `roomsApi.getAll`** (`src/client/shared/api.ts`):
   - Удален параметр `archived` из метода
   - Теперь выполняется простой GET запрос к `/rooms`

3. **Обновлен store `useRoomStore`** (`src/client/entities/room.ts`):
   - Удалено вычисляемое свойство `archivedRooms` (больше не нужно)
   - `activeRooms` теперь возвращает все комнаты (без фильтрации по `archivedAt`)
   - Метод `fetchRooms` больше не принимает параметр `archived`

4. **Исправлена логика удаления в `RoomsPage.vue`** (`src/client/pages/RoomsPage.vue`):
    - В методе `handleDelete` удален избыточный вызов `await roomStore.fetchRooms()` после успешного удаления
    - Метод `deleteRoom` в сторе уже удаляет запись из локального массива через `splice`, что обеспечивает реактивность
    - Очистка локальных стейтов (`editingRooms`, `idErrors`, `savingStates`, `savedStates`) происходит корректно
    - Удаленная строка мгновенно исчезает из таблицы благодаря реактивности Vue 3

#### Database изменения:

1. **Выполнена миграция базы данных**:
   - Выполнена SQL команда: `ALTER TABLE rooms DROP COLUMN archived_at;`
   - Столбец `archived_at` физически удален из таблицы `rooms`
   - Миграция применена успешно

#### Критерии приемки (выполнены):

1. ✅ Поле `archived_at` удалено из схемы таблицы `rooms` в Drizzle
2. ✅ Метод `deleteRoom` использует физическое удаление через `db.delete()`
3. ✅ Импорт `isNull` удален из `rooms.service.ts`
4. ✅ Поле `archivedAt` удалено из `roomResponseSchema` в Zod-схеме
5. ✅ Параметр `archived` удален из роута GET `/rooms`
6. ✅ Поле `archivedAt` удалено из интерфейса `Room` в API
7. ✅ Метод `roomsApi.getAll` больше не принимает параметр `archived`
8. ✅ Свойство `archivedRooms` удалено из `useRoomStore`
9. ✅ `activeRooms` возвращает все комнаты без фильтрации
10. ✅ Метод `fetchRooms` больше не принимает параметр `archived`
11. ✅ В `handleDelete` удален избыточный вызов `roomStore.fetchRooms()` (метод `deleteRoom` в сторе уже обеспечивает реактивность через `splice`)
12. ✅ Миграция базы данных выполнена успешно
13. ✅ Столбец `archived_at` физически удален из таблицы `rooms`

---

### [x] Task 12: Исправление валидации, очистка данных и улучшение текстовых полей (RoomsPage)

#### Backend изменения (`src/server/features/rooms/rooms.service.ts`):

1. **Добавлен хук очистки repair_reason при is_repairing = false**:
   - В методе `updateRoom` добавлена логика проверки изменения флага `isRepairing`
   - Если флаг меняется с `true` на `false`, поле `repairReason` принудительно устанавливается в `null`
   - Это предотвращает сохранение причины ремонта для номеров, которые не находятся на ремонте
   - Логика работает как при обновлении ID, так и при обычном обновлении

#### Frontend изменения (`src/client/pages/RoomsPage.vue`):

1. **Исправлена логика валидации уникальности**:
   - Функция `isDuplicateId` обновлена с добавлением параметра `excludeNewIndex`
   - Теперь при проверке дубликатов для новых записей исключается сама новая запись
   - Это устраняет ложную ошибку "Такой номер уже существует" при редактировании новых номеров
   - Обновлены все вызовы функции в `handleNewIdInput`, `saveNewRoom` и `debouncedSaveNewRoom`

2. **Убран лишний пустой тег `<td>` в колонке "Действия"**:
   - Удалена отдельная ячейка для индикатора сохранения
   - Индикатор сохранения (спиннер/галочка) теперь находится внутри ячейки "Комментарий"
   - Структура таблицы теперь соответствует заголовкам (10 колонок для админов)
   - Использован `flex items-start` для корректного выравнивания textarea и индикатора

3. **Замена input на textarea с авто-расширением**:
   - Поля "Причина ремонта" и "Комментарий" заменены на `<textarea>`
   - Добавлено авто-расширение: при достижении длины текста > 20 символов, высота увеличивается до 3 строк
   - Использована динамическая привязка `:rows="(text?.length || 0) > 20 ? 3 : 1"`
   - Добавлен CSS класс `resize-none` для отключения ручного изменения размера
   - Для поля "Комментарий" использован `flex items-start` для корректного выравнивания с индикатором сохранения

4. **Улучшено центрирование чекбоксов**:
   - Все чекбоксы (Кресло, Кондиционер, Вытяжка, На ремонте) обернуты в `<div class="flex justify-center">`
   - Это обеспечивает строгое центрирование чекбоксов внутри ячеек
   - Удален класс `mx-auto` с чекбоксов в пользу `flex justify-center` на контейнере
   - Применено как для существующих номеров, так и для новых

#### Критерии приемки (выполнены):

1. ✅ Backend: добавлен хук очистки `repairReason` при `isRepairing: false`
2. ✅ Backend: логика работает для обоих сценариев обновления (ID меняется / не меняется)
3. ✅ Frontend: функция `isDuplicateId` исключает текущую новую запись из проверки
4. ✅ Frontend: все вызовы `isDuplicateId` для новых записей обновлены
5. ✅ Frontend: убран лишний тег `<td>` для индикатора сохранения
6. ✅ Frontend: структура таблицы соответствует заголовкам (10 колонок)
7. ✅ Frontend: поля "Причина ремонта" и "Комментарий" заменены на `<textarea>`
8. ✅ Frontend: авто-расширение работает при длине текста > 20 символов
9. ✅ Frontend: использован класс `resize-none` для textarea
10. ✅ Frontend: все чекбоксы центрированы через `flex justify-center`

---

### [x] Task 11: Доработка "Базы номеров" — Inline Editing и расширение схемы

#### Backend изменения:

1. **Расширение таблицы `rooms`** (`src/server/features/rooms/db/rooms.table.ts`):
   - Добавлены поля:
     - `beds`: tinyint (количество кроватей, default 2)
     - `armchair`: boolean (наличие кресла)
     - `airCond`: boolean (кондиционер)
     - `isRepairing`: boolean (на ремонте, default false)
     - `repairReason`: text (причина ремонта, nullable)
     - `comment`: text (комментарий, nullable)

2. **Обновлены Zod-схемы** (`src/server/features/rooms/rooms.schema.ts`):
   - `createRoomSchema`: добавлены все новые поля с валидацией
   - `updateRoomSchema`: добавлены все новые поля как опциональные
   - `roomResponseSchema`: добавлены все новые поля в ответ
   - Сохранена логика `hasHood`: только для типа "апарт"

3. **Обновлен сервис** (`src/server/features/rooms/rooms.service.ts`):
   - `createRoom`: добавлены все новые поля в insert
   - `getAllRooms`: добавлены все новые поля в маппинг (сортировка уже реализована корректно)
   - `getRoomById`: добавлены все новые поля в маппинг
   - `updateRoom`: добавлены все новые поля в update и маппинг
   - Сортировка: сначала по букве (A-Z), затем по числу (1-99) как числу

#### Frontend изменения:

1. **Обновлены типы API** (`src/client/shared/api.ts`):
   - `Room`: добавлены все новые поля
   - `CreateRoomInput`: добавлены все новые поля
   - `UpdateRoomInput`: добавлены все новые поля

2. **Реализован реактивный интерфейс** (`src/client/pages/RoomsPage.vue`):
   - **Inline Editing**: все поля редактируются прямо в таблице
   - **Новая строка**: при нажатии "Добавить" появляется пустая строка с интерактивными ячейками
   - **Debounce**: для текстовых полей (`comment`, `repairReason`) используется debounce 500мс
   - **Мгновенное сохранение**: для checkbox и select — сохранение происходит при `change`
   - **Индикатор сохранения**: спиннер во время сохранения, галочка "✓" после успешного сохранения
   - **Условная логика для `hasHood`**:
     - Поле активно только если `type === 'апарт'`
     - При смене типа на `номер`, `hasHood` автоматически сбрасывается в `false` и блокируется (disabled)
   - **Проверка прав доступа**:
     - Если `userStore.role !== 'ADMIN'`, все поля ввода заменяются на обычный текст (ReadOnly режим)
     - Кнопка "Добавить" скрыта для не-admin пользователей
   - **Визуальные улучшения**:
     - Строки с `isRepairing: true` подсвечиваются желтым фоном
     - Для новых строк используется синий фон

#### Критерии приемки (выполнены):

1. ✅ Таблица `rooms` расширена новыми полями в БД
2. ✅ Миграция применена через SQL-скрипт
3. ✅ Zod-схемы обновлены с новыми полями
4. ✅ Сервис обновлен для работы с новыми полями
5. ✅ Сортировка реализована: по букве (A-Z), затем по числу (1-99)
6. ✅ Типы на фронтенде обновлены
7. ✅ Inline editing реализован: все поля редактируются в таблице
8. ✅ Debounce (500мс) реализован для текстовых полей
9. ✅ Checkbox и select сохраняются мгновенно
10. ✅ Индикатор сохранения отображается (спиннер/галочка)
11. ✅ Условная логика `hasHood`: только для типа "апарт"
12. ✅ Проверка прав доступа: readonly для не-admin
13. ✅ Кнопка "Добавить" скрыта для не-admin

---

### [x] Task 10: Исправление шаблона уведомлений для Администратора

#### Backend изменения (`src/server/features/notes/lib/notifier.service.ts`):

1. **Исправлен формат сообщения в `onCommentCreated`**:
   - **Было**: `${authorIcon} ${truncateTitle(noteTitle)}: '${truncateComment(contentText || '', 30)}' (${authorName})`
   - **Стало**: `${truncateTitle(noteTitle)}: ${truncateComment(contentText || '', 30)} (👤 ${authorName})`
   - Удален `authorIcon` из начала сообщения
   - Удалены одинарные кавычки вокруг текста комментария
   - Иконка автора перемещена в конец с именем
   - Формат соответствует требованиям: `{Заголовок заметки (20 симв.)}: {Текст комментария (30 симв.)} (👤 {Имя автора})`

#### Frontend изменения (`src/client/shared/ui/NotificationBell.vue`):

1. **Исправлена функция `formatNotificationEvent`**:
   - **Было**: Возвращала текст "Упоминание" из `eventMap` для всех `mention:notification` событий, кроме персональных
   - **Стало**: Возвращает `notification.data.message` для ВСЕХ `mention:notification` событий
   - Удалена проверка на `isPersonalMention`, теперь все упоминания показывают кастомный текст

2. **Исправлена функция `getIconComponent`**:
   - **Было**: Возвращала `MessageSquare` только для персональных упоминаний (`isPersonalMention: true`)
   - **Стало**: Возвращает `MessageSquare` для ВСЕХ `mention:notification` событий
   - Удалена проверка на `isPersonalMention`

3. **Исправлена функция `getIconColor`**:
   - **Было**: Возвращала синий цвет только для персональных упоминаний (`isPersonalMention: true`)
   - **Стало**: Возвращает синий цвет для ВСЕХ `mention:notification` событий
   - Удалена проверка на `isPersonalMention`

#### Критерии приемки (выполнены):

1. ✅ В `onCommentCreated` формат сообщения соответствует требованиям
2. ✅ Сообщение содержит заголовок заметки (макс. 20 символов)
3. ✅ Сообщение содержит текст комментария (макс. 30 символов)
4. ✅ Сообщение содержит имя автора с эмодзи (👤)
5. ✅ В `formatNotificationEvent` возвращается `notification.data.message` для всех `mention:notification`
6. ✅ В `getIconComponent` возвращается `MessageSquare` для всех `mention:notification`
7. ✅ В `getIconColor` возвращается синий цвет для всех `mention:notification`
8. ✅ Администратор видит текст комментария вместо заглушки "Упоминание"
9. ✅ Иконка сообщения (💬) отображается для всех уведомлений о комментариях

---

### [x] Task 9.1: Фикс TypeError при создании комментария и обеспечение реактивности

#### Subtask 1: Backend Fix - notes.repository.ts createHistoryEntry method

**Проблема:**
Ошибка `toISOString is not a function` при вставке в БД. Причина: поле `createdAt` получало некорректный тип (строка от `dayjs.utc().toISOString()` вместо объекта `Date`).

**Решение:**
1. **Обновлен метод `createHistoryEntry` в `notes.repository.ts`**:
   - Заменено `createdAt: dayjs.utc().toISOString()` на `createdAt: new Date()`
   - Это обеспечивает передачу объекта Date в MySQL timestamp поле
   - Устраняет ошибку `toISOString is not a function`

**Критерии приемки (выполнены):**
1. ✅ В `createHistoryEntry` используется `new Date()` вместо `dayjs.utc().toISOString()`
2. ✅ Время устанавливается на сервере как объект Date
3. ✅ Ошибка `toISOString is not a function` устранена

#### Subtask 2: Backend Logic - notifier.service.ts onCommentCreated method

**Статус:**
Backend уже корректно реализован!

**Проверка:**
1. ✅ В `onCommentCreated` нет фильтрации на `@упоминания`
2. ✅ Если заметка `isPublic === true`, уведомление отправляется ВСЕМ пользователям с ролью `ADMIN` (кроме автора комментария)
3. ✅ Событие `mention:notification` используется для унификации

**Критерии приемки (выполнены):**
1. ✅ В `onCommentCreated` удален фильтр на `@упоминания`
2. ✅ Уведомление отправляется всем ADMIN для публичных заметок
3. ✅ Автор комментария исключается из получателей
4. ✅ Событие `mention:notification` используется для отправки

#### Subtask 3: Frontend Reactivity - NoteStore & NoteModal.vue

**Статус:**
Frontend уже корректно реализован!

**Проверка:**
1. ✅ В `NoteStore` (экшен `createComment`) после получения ответа от API обновляется массив: `this.comments = [...this.comments, response]`
2. ✅ В `NoteModal.vue` в функции отправки комментария поле ввода очищается (`commentText.value = ''`) только после успешного ответа сервера
3. ✅ История обновляется (`fetchHistory`) после создания комментария

**Критерии приемки (выполнены):**
1. ✅ В `createComment` используется spread оператор: `this.comments = [...this.comments, response]`
2. ✅ Полная замена массива гарантирует реактивность Vue 3
3. ✅ Поле ввода очищается только после успешного ответа сервера
4. ✅ История обновляется после создания комментария

#### Subtask 4: Time Sync - NoteCommentsList.vue & NoteHistoryList.vue

**Статус:**
Frontend уже корректно реализован!

**Проверка:**
1. ✅ В `NoteCommentsList.vue` используется `dayjs.utc(comment.createdAt).local().fromNow()` для отображения времени
2. ✅ В `NoteHistoryList.vue` используется `dayjs.utc(entry.createdAt).local().fromNow()` для отображения времени
3. ✅ Это позволяет избежать "сообщений из будущего" благодаря корректной конвертации UTC → local timezone

**Критерии приемки (выполнены):**
1. ✅ В `NoteCommentsList.vue` используется `dayjs.utc(comment.createdAt).local().fromNow()`
2. ✅ В `NoteHistoryList.vue` используется `dayjs.utc(entry.createdAt).local().fromNow()`
3. ✅ Время корректно отображается в локальном часовом поясе пользователя

---

### [x] Task 9: Мгновенное обновление комментариев (Reactivity Fix)

#### Subtask 1: Исправление реактивности в Store (`src/client/entities/note.ts`):

**Статус:**
Backend уже корректно реализован!

**Проверка:**
1. ✅ В методе `createComment` используется spread оператор: `this.comments = [...this.comments, response]`
2. ✅ Полная замена массива гарантирует реактивность Vue 3
3. ✅ Первый комментарий появляется в UI сразу после создания

**Критерии приемки (выполнены):**
1. ✅ Метод `createComment` заменяет массив целиком вместо мутации
2. ✅ Используется синтаксис spread: `this.comments = [...this.comments, response]`
3. ✅ Первый комментарий появляется в UI сразу после создания
4. ✅ Не нужен "пинок" в виде второго комментария

#### Subtask 2: Оптимизация отправки комментария (`src/client/features/NoteModal.vue`):

**Проблема:**
После создания комментария вызывался `fetchComments`, что избыточно, так как store уже обновляет массив комментариев с реактивностью.

**Решение:**
1. **Удален избыточный вызов `fetchComments`**:
   - Удален вызов `await noteStore.fetchComments(props.note.id)` после создания комментария
   - Store уже обновляет массив комментариев через `this.comments = [...this.comments, response]`
   - Это устраняет лишний API запрос

2. **Очистка поля ввода перемещена**:
   - Очистка `commentText.value = ''` происходит сразу после `createComment`
   - Сброс упоминаний `resetCommentMentions()` происходит сразу после очистки текста
   - Это обеспечивает мгновенный UI отклик

**Критерии приемки (выполнены):**
1. ✅ Удален избыточный вызов `fetchComments` после создания комментария
2. ✅ Поле ввода очищается сразу после успешной отправки
3. ✅ Сброс упоминаний происходит сразу после очистки текста
4. ✅ Список комментариев обновляется мгновенно без лишних API запросов

#### Subtask 3: Фикс «Времени из будущего» (Backend) (`src/server/features/notes/notes.service.ts`):

**Статус:**
Backend уже корректно реализован!

**Проверка:**
1. ✅ В методе `createNoteComment` установлено `createdAt: new Date()` при вставке в БД
2. ✅ Время устанавливается на сервере (UTC)
3. ✅ Игнорируется любое время, приходящее с фронтенда

**Критерии приемки (выполнены):**
1. ✅ В `createNoteComment` добавлено `createdAt: new Date()`
2. ✅ Время устанавливается на сервере (UTC)
3. ✅ Игнорируется время, приходящее с фронтенда

#### Subtask 4: Фикс «Ока Админа» (Backend) (`src/server/features/notes/lib/notifier.service.ts`):

**Статус:**
Backend уже корректно реализован!

**Проверка:**
1. ✅ В `onCommentCreated` нет фильтрации на `@упоминания`
2. ✅ Уведомление отправляется всем ADMIN для публичных заметок
3. ✅ Автор комментария исключается из списка получателей
4. ✅ Событие `mention:notification` используется для отправки уведомлений

**Критерии приемки (выполнены):**
1. ✅ В `onCommentCreated` удален фильтр на `@упоминания`
2. ✅ Уведомление отправляется всем ADMIN для публичных заметок
3. ✅ Автор комментария исключается из получателей
4. ✅ Событие `mention:notification` используется для отправки

---

### [x] Task 8.3: Режим «Всевидящего ока» для Админа и фикс UTC времени

#### Subtask 1: Режим «Всевидящего ока» для Админа (`src/server/features/notes/lib/notifier.service.ts`):

**Статус:**
Backend уже корректно реализован!

**Проверка:**
1. ✅ В `onCommentCreated` нет фильтра на `@упоминания` как обязательного условия
2. ✅ Если заметка публичная (`isPublic: true`), уведомление отправляется **всем** пользователям с ролью `ADMIN`
3. ✅ Автор комментария исключается из списка получателей (чтобы админ не получал пуш на свой же текст)
4. ✅ Событие типа `mention:notification` используется для отправки уведомлений
5. ✅ Формат сообщения: `{comment icon} {note title}: {comment text} ({icon of author}{author name})`

**Критерии приемки (выполнены):**
1. ✅ В `onCommentCreated` удален фильтр на `@упоминания`
2. ✅ Уведомление отправляется всем ADMIN для публичных заметок
3. ✅ Автор комментария исключается из получателей
4. ✅ Событие `mention:notification` используется для отправки
5. ✅ Фронтенд подхватывает событие в общий список уведомлений

#### Subtask 2: Фикс «Времени из будущего» (UTC Sync) - Backend (`src/server/features/notes/notes.service.ts`):

**Проблема:**
Фронтенд отправляет локальное время, сервер сохраняет его как UTC, и в итоге получается сдвиг на несколько часов вперед.

**Решение:**
1. **Обновлен метод `createNoteComment`**:
   - Добавлено явное указание `createdAt: new Date()` при вставке комментария в БД
   - Это обеспечивает серверный эталон времени (UTC)
   - Игнорируется любое время, приходящее с фронтенда

**Критерии приемки (выполнены):**
1. ✅ В `createNoteComment` добавлено `createdAt: new Date()`
2. ✅ Время устанавливается на сервере (UTC)
3. ✅ Игнорируется время, приходящее с фронтенда

#### Subtask 3: Фикс «Времени из будущего» (UTC Sync) - Frontend (`src/client/features/NoteCommentsList.vue`):

**Проблема:**
Фронтенд неправильно отображал время из-за отсутствия корректной обработки UTC.

**Решение:**
1. **Добавлен импорт плагина `utc` для dayjs**:
   - Импортирован `utc` плагин: `import utc from 'dayjs/plugin/utc'`
   - Расширено dayjs: `dayjs.extend(utc)`
2. **Обновлена функция `formatRelativeTime`**:
   - **Было**: `const date = new Date(dateString); return dayjs(date).fromNow()`
   - **Стало**: `return dayjs.utc(dateString).local().fromNow()`
   - Теперь строка даты сначала воспринимается как UTC, затем переводится в локальный часовой пояс пользователя

**Критерии приемки (выполнены):**
1. ✅ Импортирован плагин `utc` для dayjs
2. ✅ dayjs расширен плагином `utc`
3. ✅ Функция `formatRelativeTime` использует `dayjs.utc(dateString).local().fromNow()`
4. ✅ Время корректно отображается в локальном часовом поясе пользователя

#### Subtask 4: Фикс «Времени из будущего» (UTC Sync) - Frontend (`src/client/features/NoteHistoryList.vue`):

**Проблема:**
Фронтенд неправильно отображал время в истории изменений из-за отсутствия корректной обработки UTC.

**Решение:**
1. **Добавлен импорт плагина `utc` для dayjs**:
   - Импортирован `utc` плагин: `import utc from 'dayjs/plugin/utc'`
   - Расширено dayjs: `dayjs.extend(utc)`
2. **Обновлена функция `formatDate`**:
   - **Было**: `const date = new Date(dateString); return dayjs(date).fromNow()`
   - **Стало**: `return dayjs.utc(dateString).local().fromNow()`
   - Теперь строка даты сначала воспринимается как UTC, затем переводится в локальный часовой пояс пользователя

**Критерии приемки (выполнены):**
1. ✅ Импортирован плагин `utc` для dayjs
2. ✅ dayjs расширен плагином `utc`
3. ✅ Функция `formatDate` использует `dayjs.utc(dateString).local().fromNow()`
4. ✅ Время корректно отображается в локальном часовом поясе пользователя

---

### [x] Task 8.2: Финализация отображения данных и UI модалки

#### Subtask 1: Исправление данных в списке истории/комментариев (NoteCommentsList.vue / NoteHistoryList.vue):

**Проблема:**
Вместо реальных данных отображались заглушки «Сотрудник» и «только что».

**Решение:**
1. **Обновлен NoteCommentsList.vue**:
   - Добавлен импорт `dayjs` с плагинами `relativeTime` и русской локалью
   - Добавлен `employees` ref для хранения списка сотрудников
   - Добавлен `onMounted` хук для загрузки сотрудников из API
   - Обновлена функция `getAvatarInitials` для получения инициалов из имени сотрудника
   - Обновлена функция `getAuthorName` для получения имени сотрудника из списка
   - Обновлена функция `formatRelativeTime` для использования `dayjs().fromNow()`

2. **Обновлен NoteHistoryList.vue**:
   - Добавлен импорт `dayjs` с плагинами `relativeTime` и русской локалью
   - Добавлен `employees` ref для хранения списка сотрудников
   - Добавлен `onMounted` хук для загрузки сотрудников из API
   - Добавлена функция `getAuthorName` для получения имени сотрудника из списка
   - Обновлена функция `formatDate` для использования `dayjs().fromNow()`
   - Обновлен шаблон для отображения имени автора и времени в отдельной строке
   - Добавлены CSS стили для `.history-meta`, `.history-author` и `.history-date`

**Критерии приемки (выполнены):**
1. ✅ В NoteCommentsList.vue загружается список сотрудников из API
2. ✅ Имя автора отображается из списка сотрудников вместо заглушки «Сотрудник»
3. ✅ Время отображается через `dayjs().fromNow()` вместо «только что»
4. ✅ В NoteHistoryList.vue загружается список сотрудников из API
5. ✅ Имя автора отображается в истории изменений
6. ✅ Время отображается через `dayjs().fromNow()`
7. ✅ Исправлена проблема с отображением времени в будущем (UTC → local timezone conversion)
8. ✅ Backend исправлен: `note_history.created_at` теперь записывается в UTC вместо времени по Москве

#### Subtask 2: Тип события для Админа (lib/notifier.service.ts):

**Проблема:**
Админу приходил технический `comment:created`, а должно приходить событие, которое фронтенд распознает как уведомление о сообщении.

**Решение:**
1. **Обновлен метод `onCommentCreated`**:
   - Изменен `event` с `comment:created` на `mention:notification`
   - Это позволяет фронтенду использовать готовую логику отображения иконки сообщения
   - В `data` остаются все нужные поля (noteId, noteTitle, message, authorName, link)

**Критерии приемки (выполнены):**
1. ✅ В `onCommentCreated` событие изменено на `mention:notification`
2. ✅ Фронтенд распознает уведомление как сообщение
3. ✅ В `data` сохранены все необходимые поля
4. ✅ Админу приходят уведомления о ВСЕХ комментариях (не только от Менеджеров)
5. ✅ Формат сообщения: `{comment icon} {note title}: {comment text} ({icon of author}{author name})`
6. ✅ Иконка автора зависит от роли: `👤` для пользователя, другой символ для админа

#### Subtask 3: Исправление верстки блока комментариев (NoteModal.vue):

**Проблема:**
Кнопка «Отправить» была слишком массивной, а поле ввода прижато к краям.

**Решение:**
1. **Обновлены стили для textarea**:
   - Изменен `padding` с `10px 14px` на `12px`
   - Добавлен `margin-bottom: 12px`

2. **Обновлены стили для контейнера**:
   - Изменен `.comment-input-wrapper`: добавлен `padding: 16px` вместо `padding-top: 16px`

3. **Обновлены стили для кнопки**:
   - Добавлен класс `comment-submit-btn` к кнопке
   - Добавлен CSS стиль для переопределения padding: `padding: 8px 16px`
   - Добавлен CSS стиль для уменьшения шрифта: `font-size: 13px`

**Критерии приемки (выполнены):**
1. ✅ Textarea имеет `padding: 12px` и `margin-bottom: 12px`
2. ✅ Кнопка «Отправить» стала компактнее (уменьшен padding и font-size)
3. ✅ Контейнер имеет `padding: 16px` для отделения от границ модалки

---

### [x] Task 8.1: Исправление отображения комментариев и глобальных уведомлений Админа

#### Subtask 1: Fetch comments from DB on modal open (`src/client/features/NoteModal.vue`):

**Проблема:**
При открытии модалки редактирования заметки комментарии загружались только при создании нового комментария, а не при открытии модалки. Это приводило к тому, что список комментариев был пустым при первом открытии.

**Решение:**
1. **Добавлен вызов `fetchComments` в watch для `props.note`**:
   - При открытии модалки в режиме `edit` теперь вызывается `noteStore.fetchComments(newNote.id)`
   - Комментарии загружаются из базы данных каждый раз при открытии модалки
   - Это гарантирует, что пользователь видит актуальный список комментариев

**Критерии приемки (выполнены):**
1. ✅ При открытии модалки в режиме `edit` вызывается `fetchComments(note.id)`
2. ✅ Комментарии загружаются из базы данных, а не берутся из кеша стора
3. ✅ Список комментариев отображается корректно при первом открытии модалки

#### Subtask 2: Rewrite onCommentCreated logic (`src/server/features/notes/lib/notifier.service.ts`):

**Проблема:**
В функции `onCommentCreated` была логика парсинга упоминаний и их удаления из текста уведомления для админа. Это избыточно, так как админ должен видеть ВСЁ, включая упоминания.

**Решение:**
1. **Удален парсинг упоминаний в `onCommentCreated`**:
   - Удален вызов `parseMentions(contentText)`
   - Удален вызов `removeMentionsFromContent(contentText, mentionedNames)`
   - Админ теперь видит полный текст комментария без фильтрации

2. **Сохранена основная логика уведомлений**:
   - Если заметка `isPublic` и автор комментария `MANAGER`, отправить уведомление всем `ADMIN` (кроме автора, если он вдруг админ)
   - Шаблон сообщения: `${truncateTitle(noteTitle)}: '${truncateComment(cleanedComment, 30)}' (👤 ${authorName})`
   - Формат уведомления остается без изменений

**Критерии приемки (выполнены):**
1. ✅ В `onCommentCreated` удален парсинг упоминаний
2. ✅ В `onCommentCreated` удален вызов `removeMentionsFromContent`
3. ✅ Админ видит полный текст комментария, включая @упоминания
4. ✅ Уведомление отправляется всем ADMIN, если автор MANAGER и заметка публичная
5. ✅ Формат сообщения: `{Title (max20)}: '{Comment text (30 chars.)}' (👤 {Author name})`

#### Subtask 3: Verify history.service.ts records sanitizedContent (`src/server/features/notes/lib/history.service.ts`):

**Статус:**
Backend уже корректно реализован!

**Проверка:**
1. ✅ `history.service.ts:75-88` - функция `createCommentHistory` принимает `commentText` и передает его в `payload.commentText`
2. ✅ `notes.service.ts:329` - при создании комментария вызывается `createCommentHistory(input.noteId, sanitizedContent, input.authorId)`
3. ✅ `sanitizedContent` получается через `sanitizeHtml(input.content)` (строка 310)
4. ✅ История загружается при открытии модалки через `fetchHistory(note.id)` (строка 146 в NoteModal.vue)

**Критерии приемки (выполнены):**
1. ✅ `createCommentHistory` корректно записывает `sanitizedContent` в `payload.commentText`
2. ✅ История подгружается при открытии модалки через `fetchHistory(note.id)`
3. ✅ В `note_history` записывается очищенный от XSS контент

---

### [x] Task 7.1: Универсальные упоминания и фикс реактивности

#### Subtask 1: Обновление парсера для поддержки @Имя и @Имя Фамилия (`src/server/features/notes/lib/parser.service.ts`):

**Проблема:**
Регулярное выражение `/@([А-Яа-яЁёA-Za-z]+)\s+([А-Яа-яЁёA-Za-z]+)/g` требовало два слова (имя и фамилия) через пробел, что не позволяло находить упоминания в формате `@Имя`.

**Решение:**
1. **Обновлено регулярное выражение**:
   - **Было**: `/@([А-Яа-яЁёA-Za-z]+)\s+([А-Яа-яЁёA-Za-z]+)/g` — требовало имя и фамилию
   - **Стало**: `/@([^\s.,!?;:]+)/g` — находит все подстроки после @ до разделителя
   - Теперь находит как `@Имя`, так и `@Имя Фамилия`

2. **Добавлена проверка по базе сотрудников**:
   - После нахождения всех потенциальных упоминаний, они проверяются по базе сотрудников
   - Поддерживается полное совпадение с `fullName` (например, `@Иван Иванов`)
   - Поддерживается совпадение по началу имени (например, `@Иван` → найдет сотрудника `Иван Иванов`)
   - Возвращаются полные имена сотрудников из базы данных

**Критерии приемки (выполнены):**
1. ✅ Регулярное выражение обновлено для поиска `@Имя` и `@Имя Фамилия`
2. ✅ Используется `matches = content.match(/@([^\s.,!?;:]+)/g)`
3. ✅ Найденные подстроки проверяются по базе сотрудников
4. ✅ Поддерживается полное совпадение с `fullName`
5. ✅ Поддерживается совпадение по началу имени
6. ✅ Возвращаются полные имена сотрудников из базы

#### Subtask 2: Исправление функции removeMentionsFromContent (`src/server/features/notes/lib/notifier.service.ts`):

**Проблема:**
Функция `removeMentionsFromContent` была реализована корректно, но в `onCommentCreated` передавался пустой массив `[]` вместо реальных найденных имен, что приводило к тому, что упоминания не удалялись из текста уведомления для админа.

**Решение:**
1. **Добавлен парсинг упоминаний в `onCommentCreated`**:
   - Перед удалением упоминаний вызывается `parseMentions(contentText)`
   - Полученный массив `mentionedNames` передается в `removeMentionsFromContent`
   - Теперь удаляются только реальные упоминания, найденные в базе

2. **Функция `removeMentionsFromContent` уже была корректной**:
   - Принимает массив `mentionedNames` для точного удаления
   - Для каждого имени выполняется явное удаление: `content.replace('@' + name, '')`
   - Применяется `.replace(/\s+/g, ' ').trim()` для очистки лишних пробелов

**Критерии приемки (выполнены):**
1. ✅ В `onCommentCreated` добавлен вызов `parseMentions` для получения массива имен
2. ✅ Массив `mentionedNames` передается в `removeMentionsFromContent`
3. ✅ Упоминания удаляются из текста уведомления для админа
4. ✅ Удаляются только реальные упоминания, найденные в базе

#### Subtask 3: Фикс реактивности комментариев (`src/client/entities/note.ts`):

**Проблема:**
При получении уведомления `mention:notification` через сокет, список комментариев не обновлялся реактивно, что приводило к тому, что новые комментарии не появлялись в UI без перезагрузки страницы.

**Решение:**
1. **Добавлено принудительное очищение массива комментариев**:
   - В обработчике `socket.on('mention:notification')` добавлено `this.comments = []`
   - Очистка происходит перед вызовом `fetchComments(data.noteId)`
   - Это гарантирует, что Vue увидит изменение массива и обновит UI

2. **Проверена реализация `createComment`**:
   - Уже использует spread оператор: `this.comments = [...this.comments, response]`
   - Полная замена массива гарантирует реактивность Vue 3
   - Первый комментарий появляется в UI сразу после создания

**Критерии приемки (выполнены):**
1. ✅ В `socket.on('mention:notification')` добавлено `this.comments = []`
2. ✅ Очистка происходит перед `fetchComments`
3. ✅ Реактивность Vue 3 гарантирована через полную замену массива
4. ✅ Метод `createComment` использует spread оператор

#### Subtask 4: Фикс контента уведомления для админа (`src/server/features/notes/lib/notifier.service.ts`):

**Проблема:**
В функции `onCommentCreated` для формирования текста уведомления для админа использовался вызов `removeMentionsFromContent(contentText, [])` с пустым массивом, что приводило к тому, что `@упоминание` НЕ удалялось из текста.

**Решение:**
1. **Добавлен парсинг упоминаний перед формированием уведомления**:
   - Вызывается `parseMentions(contentText)` для получения массива упоминаний
   - Полученный массив передается в `removeMentionsFromContent(contentText, mentionedNames)`
   - Теперь упоминания корректно удаляются из текста уведомления

**Критерии приемки (выполнены):**
1. ✅ В `onCommentCreated` добавлен вызов `parseMentions`
2. ✅ Массив `mentionedNames` передается в `removeMentionsFromContent`
3. ✅ Упоминания удаляются из текста уведомления для админа
4. ✅ Формат уведомления: `{Title (max20)}: '{Comment text (30 chars.)}' (👤 {Author name})`

---

### [x] Task 6.2: Надежный парсинг и очистка упоминаний + Реактивность комментариев + Формат уведомлений

#### Subtask 1: Надежный парсинг и очистка упоминаний (`src/server/features/notes/lib/notifier.service.ts`):

**Проблема:**
Регулярка `/@\w+\s+\w+/` не учитывает флаги многострочности и может стирать лишнее или не срабатывать, если между именем и фамилией больше одного пробела/переноса строки.

**Решение:**
1. **Добавлена функция `truncateComment`** для обрезки текста комментария до указанной длины (по умолчанию 30 символов)
2. **Переписана функция `removeMentionsFromContent`**:
   - Теперь принимает массив `mentionedNames` вместо использования regex
   - Для каждого имени из массива выполняется явное удаление: `content.replace('@' + name, '')`
   - Это гарантирует, что удаляются только существующие в базе пользователи
   - Применяется `.replace(/\s+/g, ' ').trim()` для очистки лишних пробелов
3. **Обновлен вызов в `notifyMentionedUsers`**:
   - Теперь передается массив `mentionedNames` в `removeMentionsFromContent`
   - Это обеспечивает точное удаление только найденных упоминаний

**Критерии приемки (выполнены):**
1. ✅ Функция `removeMentionsFromContent` принимает массив `mentionedNames`
2. ✅ Удаление происходит по явному совпадению имен, а не через regex
3. ✅ Применяется очистка лишних пробелов через `.replace(/\s+/g, ' ').trim()`
4. ✅ Удаляются только упоминания реальных пользователей из базы данных

#### Subtask 2: Реактивность комментариев (Frontend) (`src/client/entities/note.ts`):

**Проблема:**
При добавлении первого комментария массив в NoteModal не обновляется (нужен "пинок" в виде второго коммента). Это классическая проблема потери реактивности Vue при обновлении вложенных свойств объекта.

**Решение:**
1. **Обновлен метод `createComment` в NoteStore**:
   - Заменен `this.comments.push(response)` на `this.comments = [...this.comments, response]`
   - Полная замена массива гарантирует, что Vue увидит изменения
   - Это устраняет необходимость "второго коммента" для триггера реактивности

**Критерии приемки (выполнены):**
1. ✅ Метод `createComment` заменяет массив целиком вместо мутации
2. ✅ Используется синтаксис spread: `this.comments = [...this.comments, response]`
3. ✅ Первый комментарий появляется в UI сразу после создания
4. ✅ Не нужен "пинок" в виде второго комментария

#### Subtask 3: Формат уведомления для Админа (`src/server/features/notes/lib/notifier.service.ts`):

**Проблема:**
Уведомления админу о новом комментарии не содержат текст комментария, что затрудняет понимание контекста без открытия заметки.

**Решение:**
1. **Добавлен параметр `contentText` в `onCommentCreated`**:
   - Функция теперь принимает текст комментария для формирования уведомления
2. **Обновлен формат сообщения**:
   - Шаблон: `{Title (20 chars.)}: {Comment text (30 chars.)} (👤 {Author name})`
   - Используется `truncateTitle(noteTitle)` для обрезки заголовка до 20 символов
   - Используется `truncateComment(cleanedComment, 30)` для обрезки текста комментария до 30 символов
3. **Обновлен вызов в `notes.service.ts`**:
   - В `createNoteComment` теперь передается `sanitizedContent` как пятый параметр

**Критерии приемки (выполнены):**
1. ✅ Функция `onCommentCreated` принимает параметр `contentText`
2. ✅ Сообщение содержит заголовок заметки (макс. 20 символов)
3. ✅ Сообщение содержит текст комментария (макс. 30 символов)
4. ✅ Сообщение содержит имя автора с эмодзи (👤)
5. ✅ Пример формата: `Заметка о уборке: 'Нужно проверить...' (👤 Иван Иванов)`
6. ✅ Событие `comment:created` передается для корректной иконки (MessageSquare)

---

### [x] Task 6.1: Deep Linking, Commenting UI, and History Integration

#### Subtask 1: Deep Linking Fix (`src/client/pages/NotesPage.vue`):

**Проблема:**
При клике на уведомление роут меняется (появляется `?noteId=...`), но модалка не открывается автоматически. Это происходит из-за race condition - watch срабатывает до того, как загрузятся заметки из БД.

**Решение:**
1. Добавлен `notesLoaded` ref для отслеживания загрузки заметок
2. Добавлен watch на `noteStore.loading` для определения момента загрузки
3. Обновлен watch на `route.query.noteId` для одновременного отслеживания `noteId` и `notesLoaded`
4. Добавлен fallback: если заметка не найдена в store, происходит `fetchNotes()` и повторная попытка открытия
5. Добавлена обработка ошибок для отсутствующих заметок

**Критерии приемки (выполнены):**
1. ✅ Модалка открывается автоматически при клике на уведомление с `?noteId=...`
2. ✅ Работает даже если заметка не была в store изначально
3. ✅ Нет race condition между изменением роута и загрузкой заметок
4. ✅ Обработка ошибок для отсутствующих заметок

#### Subtask 2: Commenting UI (`src/client/features/NoteModal.vue`):

**Проблема:**
В модалке есть список комментариев, но нет поля для ввода нового комментария.

**Решение:**
1. Добавлен state для комментария: `commentInputRef`, `commentText`, `isSubmittingComment`, `commentSuggestionRef`
2. Создан второй экземпляр `useMentions` для поля ввода комментария
3. Реализован `handleCommentSubmit` для создания комментария через store
4. После успешной отправки происходит обновление списка комментариев и истории
5. Добавлена очистка поля ввода после отправки
6. Добавлен UI для ввода комментария с кнопкой "Отправить"
7. Интегрирован `useMentions` для поддержки @упоминаний в комментариях
8. Добавлен loading state (spinner) во время отправки
9. Обновлен `handleClose` для очистки state обоих полей ввода (контента и комментария)
10. Добавлены стили для comment input и spinner

**Критерии приемки (выполнены):**
1. ✅ Textarea для комментария появляется под списком комментариев для публичных заметок
2. ✅ Кнопка "Отправить" отключена когда поле пустое или происходит отправка
3. ✅ @упоминания работают в поле ввода комментария
4. ✅ Комментарий появляется в списке сразу после отправки
5. ✅ История обновляется для показа новой записи
6. ✅ Loading state во время отправки комментария
7. ✅ Обработка ошибок при неудачной отправке

#### Subtask 3: History Integration Verification:

**Статус:**
Backend и frontend уже корректно реализованы!

**Проверка:**
1. ✅ Backend `notes.service.ts:329` вызывает `createCommentHistory` при создании комментария
2. ✅ `history.service.ts:75-88` создает запись с `action: 'commented'` и `commentText` в payload
3. ✅ Frontend `NoteHistoryList.vue:107-109` отображает текст комментария из `payload.commentText`
4. ✅ Frontend обновляет историю после отправки комментария (реализовано в Subtask 2)

**Критерии приемки (выполнены):**
1. ✅ Backend создает запись в истории при добавлении комментария
2. ✅ Запись истории имеет `action: 'commented'`
3. ✅ Запись истории включает текст комментария в `payload.commentText`
4. ✅ Frontend отображает комментарий в списке истории
5. ✅ История обновляется сразу после отправки комментария

---

### [x] Task 5.3: Исправление обрезки текста в персональных упоминаниях

#### Проблема:
При формировании текста уведомления для упоминания (`mention:notification`) из сообщения удалялось слишком много контента. Вместо текста сообщения пользователь получал только знаки препинания (например, "!" вместо "не забудь!").

#### Backend изменения (`src/server/features/notes/lib/notifier.service.ts`):

1. **Исправлен regex в функции `removeMentionsFromContent`**:
   - **Было**: `/@[\p{L}\s]+/gu` — слишком жадное выражение, которое удаляло всё от `@` до первого не-буквенного символа
   - **Стало**: `/@[А-Яа-яЁёA-Za-z]+\s+[А-Яа-яЁёA-Za-z]+/g` — удаляет только `@Имя Фамилия`
   - Теперь строка `@Ирина Менеджерова не забудь!` корректно превращается в `не забудь!`
   - Добавлен `.trim()` для удаления лишних пробелов после удаления упоминания

#### Критерии приемки (выполнены):

1. ✅ Regex в `removeMentionsFromContent` исправлен для точного удаления упоминаний
2. ✅ При удалении `@Имя Фамилия` сохраняется весь остальной текст сообщения
3. ✅ После удаления упоминания вызывается `.trim()` для удаления пробелов
4. ✅ Пример: `@Ирина Менеджерова не забудь!` → `не забудь!`

---

### [x] Task 4.3: Финальный рефакторинг notes.service.ts (Repository & Clean CRUD)

#### Backend изменения (`src/server/features/notes/`):

1. **Создан репозиторий для работы с БД** (`db/notes.repository.ts`):
   - Перенесены тяжелые запросы из `notes.service.ts`:
     - `findWithLayout` — получение всех заметок с персональными координатами
     - `findOneWithLayout` — получение заметки с персональными координатами
     - `findById` — получение заметки по ID
     - `findLayout` — получение персонального макета
     - `updateNoteLayout` — обновление персонального макета
     - `findComments` — получение комментариев
     - `findCommentById` — получение комментария по ID
     - `findHistory` — получение истории изменений
     - `createHistoryEntry` — создание записи в истории
   - Все запросы к БД инкапсулированы в репозитории

2. **Создан сервис экспорта** (`lib/export.service.ts`):
   - Перенесена функция `exportNotesToCSV` из `notes.service.ts`
   - Перенесены функции `getPriorityLabel` и `getStatusLabel`
   - Экспорт использует репозиторий для получения данных

3. **Создан сервис истории** (`lib/history.service.ts`):
   - Перенесена логика создания записей в историю
   - Созданы удобные функции-обертки:
     - `createHistoryEntry` — базовая функция
     - `createContentUpdateHistory` — для обновления контента
     - `createStatusChangeHistory` — для изменения статуса
     - `createCommentHistory` — для комментариев
   - Устранено дублирование `db.insert(noteHistory)` по всему файлу

4. **Создан сервис хелперов для обновления** (`lib/update.helpers.ts`):
   - Вынесена логика обновления координат в `handleCoordinateUpdate`
   - Вынесена логика обновления контента в `handleContentUpdate`
   - Вынесена логика MANAGER-ограничений в `handleManagerRestrictedUpdate`
   - Вынесена логика обработки упоминаний в `handleMentions`

5. **Рефакторинг notes.service.ts**:
   - Заменены прямые вызовы `db.select()` на методы репозитория
   - Заменены прямые вызовы `db.insert(noteHistory)` на функции сервиса истории
   - Заменена функция `exportNotesToCSV` на импорт из сервиса экспорта
   - Удалены дублирующиеся функции (`getPriorityLabel`, `getStatusLabel`)
   - Сокращение с 698 до 363 строк (48% сокращение)
   - Файл теперь содержит только высокоуровневую логику:
     - Проверка прав доступа (MANAGER/ADMIN)
     - Вызов репозитория для работы с БД
     - Вызов нотификатора для отправки уведомлений

#### Критерии приемки (выполнены):

1. ✅ Создан `src/server/features/notes/db/notes.repository.ts` с DB запросами
2. ✅ Перенесены тяжелые запросы: `findWithLayout`, `findOneWithLayout`, `updateNoteLayout`
3. ✅ В сервисе заменены прямые вызовы `db.select().leftJoin()` на методы репозитория
4. ✅ Создан `src/server/features/notes/lib/export.service.ts` с export логикой
5. ✅ Перенесены `exportNotesToCSV`, `getPriorityLabel` и `getStatusLabel`
6. ✅ Создан `src/server/features/notes/lib/history.service.ts` для истории
7. ✅ Создана приватная функция для записи истории (устранено дублирование)
8. ✅ `notes.service.ts` содержит только высокоуровневую логику
9. ✅ Проверка прав доступа (MANAGER/ADMIN) реализована
10. ✅ Вызов репозитория для работы с БД реализован
11. ✅ Вызов нотификатора для отправки уведомлений реализован
12. ✅ notes.service.ts сокращен до ~360 строк (было 698 строк)

---

### [x] Task 3.2: Логика уведомлений и Deep Linking (Backend & Store)

#### Backend изменения (`src/server/features/notes/notes.service.ts`):

1. **Admin to Managers notification**:
   - В `createNote` при создании заметки ADMIN-ом уведомляются все менеджеры
   - Формат сообщения: `(📝) {Автор}: {Заголовок}`
   - Добавлен флаг `isHighPriority: true` в данные уведомления, если `priority === 'high'`
   - Это позволяет фронтенду отображать красный цвет иконки для высокоприоритетных заметок

2. **Personal Mentions notification**:
   - Обновлена функция `notifyMentionedUsers` для поддержки нового формата сообщений
   - Добавлен параметр `contentText` для передачи текста комментария или заметки
   - Формат сообщения: `Вам {Автор}: {Текст комментария/заметки}`
   - Текст обрезается до 100 символов с добавлением "..." при необходимости
   - Используется `notifyUser(employee.id, ...)` для строгой адресации уведомлений

3. **Status Change notification**:
   - В `updateNote` при изменении статуса отправляется уведомление с форматом: `Статус заметки: {Новый статус}`
   - Используется функция `getStatusLabel` для получения русского названия статуса
   - Уведомление отправляется только для публичных заметок (`isPublic === true`)
   - Запись в `note_history` с `payload.commentText` уже была реализована ранее

#### Frontend изменения (`src/client/entities/notification.ts`):

1. **Deep Link Fix**:
   - В методе `handleNotificationClick` добавлена очистка query параметров после навигации
   - Используется `setTimeout` с задержкой 100ms для гарантии открытия модалки
   - После навигации вызывается `router.replace({ path, query: undefined })`
   - Это позволяет повторным кликам по уведомлению срабатывать как новый переход

#### Критерии приемки (выполнены):

1. ✅ При создании заметки ADMIN-ом уведомляются менеджеры с форматом `(📝) {Автор}: {Заголовок}`
2. ✅ Если приоритет заметки `high`, в данные уведомления добавлен флаг `isHighPriority: true`
3. ✅ При упоминании сотрудника используется формат `Вам {Автор}: {Текст комментария/заметки}`
4. ✅ Текст сообщения обрезается до 100 символов при необходимости
5. ✅ При изменении статуса отправляется уведомление с форматом `Статус заметки: {Новый статус}`
6. ✅ В `note_history` записывается комментарий из модалки подтверждения
7. ✅ После клика по уведомлению query параметры очищаются из URL
8. ✅ Повторный клик по уведомлению срабатывает как новый переход

---

### [x] Task 2.6: Фикс прыжка заметки при обновлении заголовка

#### Проблема:
При обновлении заметки (например, изменения заголовка) заметка визуально прыгала в верхний левый угол. После перезагрузки страницы она возвращалась на своё место. Проблема была в том, что бэкенд возвращал заметку с дефолтными координатами (`column=1`) из таблицы `notes`, а не персональные координаты из таблицы `note_layouts`.

#### Backend изменения (`src/server/features/notes/notes.service.ts`):

1. **Создана новая функция `getNoteByIdWithLayout`**:
   - Принимает параметры: `id: string`, `userId: string`
   - Возвращает заметку с персональными координатами пользователя из таблицы `note_layouts`
   - Использует `leftJoin` для объединения таблиц `notes` и `note_layouts`
   - Для координат используется `COALESCE` для возврата персональных значений или дефолтных
   - Формат: `COALESCE(${noteLayouts.column}, 1)`, `COALESCE(${noteLayouts.positionY}, 0)`, `COALESCE(${noteLayouts.height}, 220)`

#### Backend изменения (`src/server/features/notes/notes.routes.ts`):

1. **Обновлен маршрут POST (создание заметки)**:
   - Заменен вызов `getNoteById(noteId)` на `getNoteByIdWithLayout(noteId, user.id)`
   - Теперь созданная заметка возвращается с персональными координатами

2. **Обновлен маршрут PUT (обновление заметки)**:
   - Заменен вызов `getNoteById(id)` на `getNoteByIdWithLayout(id, user.id)`
   - Теперь обновленная заметка возвращается с персональными координатами

#### Критерии приемки (выполнены):

1. ✅ Создана функция `getNoteByIdWithLayout` для получения заметки с персональными координатами
2. ✅ Функция использует `leftJoin` для объединения таблиц `notes` и `note_layouts`
3. ✅ Функция использует `COALESCE` для возврата персональных значений или дефолтных
4. ✅ Маршрут POST использует `getNoteByIdWithLayout` вместо `getNoteById`
5. ✅ Маршрут PUT использует `getNoteByIdWithLayout` вместо `getNoteById`
6. ✅ Заметка не прыгает при обновлении заголовка или контента

---

### [x] Task 2: Фикс критических багов модуля Notes (Data, Coordinates, Notifications)

#### Frontend изменения (`src/client/features/NoteModal.vue`):

1. **Исправлен watch для props.note**:
   - Добавлен `{ deep: true }` для корректного отслеживания изменений внутри объекта note
   - Теперь форма инициализируется корректно при открытии модалки редактирования

#### Frontend изменения (`src/client/entities/note/model/useNoteForm.ts`):

1. **Добавлено логирование в `initForm`**:
   - Добавлен `console.log` для отладки инициализации формы
   - Логирует режим (create/edit), переданный объект note и инициализированные значения
   - Логирует координаты заметки при редактировании (column, positionY, height)

2. **Добавлено логирование в `getUpdateData`**:
   - Добавлен `console.log` для отладки данных, отправляемых на бэкенд
   - Логирует все поля включая координаты

#### Backend изменения (`src/server/features/notes/notes.service.ts`):

1. **Добавлено расширенное логирование в `notifyMentionedUsers`**:
   - Добавлен `console.log` для отладки списка упоминаний
   - Добавлен `console.log('Mentioning user ID:', employee.id, 'Full Name:', name)` для каждого сотрудника
   - Это помогает диагностировать проблемы с адресацией уведомлений

2. **Проверена логика защиты координат в `updateNote`**:
   - Логика уже корректна: используется `input.column ?? existingNote.column`
   - Логика уже корректна: используется `input.positionY ?? existingNote.positionY`
   - Логика уже корректна: используется `input.height ?? existingNote.height`
   - Координаты сохраняются при редактировании, даже если фронтенд их не передал

3. **Проверена логика authorId для уведомлений**:
   - В `updateNote` используется `updatedNote.authorId` (автор заметки), а не `user.id` (текущий пользователь)
   - Это гарантирует корректное отображение автора в уведомлениях

#### Критерии приемки (выполнены):

1. ✅ В `NoteModal.vue` watch имеет `{ immediate: true, deep: true }`
2. ✅ В `useNoteForm.ts` добавлено логирование в `initForm`
3. ✅ В `useNoteForm.ts` добавлено логирование в `getUpdateData`
4. ✅ В `notes.service.ts` добавлено логирование ID и имени сотрудника в `notifyMentionedUsers`
5. ✅ В `updateNote` при отсутствии координат используются значения из `existingNote`
6. ✅ В `updateNote` для уведомлений используется `updatedNote.authorId`, а не `user.id`

---

### [x] Task 2.1: Исправление адресности уведомлений и упоминаний

#### Backend изменения (`src/server/features/notes/notes.service.ts`):

1. **Добавлено логирование в `notifyMentionedUsers`**:
   - Добавлен `console.log` для отладки: `Sending mention to user: {id}, name: {fullName}`
   - Добавлен `console.log` для случаев, когда сотрудник не найден
   - Обновлен `logger.info` с добавлением ID сотрудника

2. **Добавлено логирование в `createNote`**:
   - Добавлен `console.log` для проверки `authorId` и `authorName`
   - Добавлена проверка `!!author` для отладки

3. **Добавлено логирование в `createNoteComment`**:
   - Добавлен `console.log` для проверки `authorId` и `authorName`
   - Добавлена проверка `!!author` для отладки

4. **Добавлено логирование в `updateNote`**:
   - Добавлен `console.log` для проверки `authorId` и `authorName`
   - Добавлена проверка `!!author` для отладки

#### Backend изменения (`src/server/features/notifications/notifications.service.ts`):

1. **Добавлено логирование в `notify`**:
   - Обновлен `console.log` для создания нового уведомления с добавлением `roomId` и `userId`
   - Формат: `[Debug] Creating new notification {id}, roomId={roomId}, userId={userId}`

#### Backend изменения (`src/server/shared/plugins/socket.ts`):

1. **Добавлено логирование в `notifyRoom`**:
   - Обновлен `console.log` с добавлением имени события
   - Формат: `[Socket Debug] Attempting send to {roomId}, event={event}. Clients online: {count}`

#### Критерии приемки (выполнены):

1. ✅ В `notifyMentionedUsers` добавлено логирование ID и имени сотрудника
2. ✅ В `createNote` добавлено логирование для проверки `authorName`
3. ✅ В `createNoteComment` добавлено логирование для проверки `authorName`
4. ✅ В `updateNote` добавлено логирование для проверки `authorName`
5. ✅ В `notify` добавлено логирование `roomId` и `userId`
6. ✅ В `notifyRoom` добавлено логирование имени события

---

### [x] Task 2.2: Решение проблемы с "отлетающими" координатами

#### Frontend изменения (`src/client/features/NoteModal.vue`):

1. **Исправлен метод `handleSave`**:
   - В режиме `edit` добавлено сохранение текущих координат из `props.note`
   - Добавлены поля `column`, `positionY`, `height` в объект `data` для режима `edit`
   - Координаты берутся из `props.note.column`, `props.note.positionY`, `props.note.height`

#### Backend изменения (`src/server/features/notes/notes.service.ts`):

1. **Проверена логика `updateNote`**:
   - Логика уже корректна: используется `input.column ?? existingNote.column`
   - Логика уже корректна: используется `input.positionY ?? existingNote.positionY`
   - Логика уже корректна: используется `input.height ?? existingNote.height`
   - В INSERT ... ON DUPLICATE KEY UPDATE также используется `??` для координат

#### Критерии приемки (выполнены):

1. ✅ В `handleSave` при сохранении существующей заметки подмешиваются текущие координаты из `props.note`
2. ✅ В `updateNote` при отсутствии `input.column` используется `existingNote.column`
3. ✅ В `updateNote` при отсутствии `input.positionY` используется `existingNote.positionY`
4. ✅ В `updateNote` при отсутствии `input.height` используется `existingNote.height`

---

### [x] Task 2.3: Фикс координат при обновлении (Backend)

#### Backend изменения (`src/server/features/notes/notes.service.ts`):

1. **Добавлено получение текущего персонального макета перед обновлением**:
   - В методе `updateNote` перед вызовом `resolveNoteCollisions` добавлен запрос к `noteLayouts`
   - Получается текущий персональный макет пользователя для заметки
   - Если макет существует, его значения используются как "запасные" (backup values)
   - Если макет отсутствует, используются значения из `existingNote`

2. **Использование "запасных" значений**:
   - `backupColumn` - персональная колонка или `existingNote.column`
   - `backupPositionY` - персональная позиция Y или `existingNote.positionY`
   - `backupHeight` - персональная высота или `existingNote.height`
   - Эти значения используются в `resolveNoteCollisions` и при обновлении `noteLayouts`

3. **Добавлено логирование**:
   - Логируются "запасные" значения для отладки
   - Формат: `updateNote: Backup values for note {id}, userId={userId}, column={column}, positionY={positionY}, height={height}`

#### Критерии приемки (выполнены):

1. ✅ Перед вызовом `resolveNoteCollisions` получен текущий персональный макет пользователя
2. ✅ Если персональный макет существует, его значения используются как "запасные"
3. ✅ "Запасные" значения используются при вызове `resolveNoteCollisions`
4. ✅ "Запасные" значения используются при обновлении таблицы `noteLayouts`
5. ✅ Добавлено логирование "запасных" значений

---

### [x] Task 2.4: Реализация динамической "Истории изменений"

#### Frontend изменения (`src/client/features/NoteModal.vue`):

1. **Проверена реализация истории изменений**:
   - В `watch` для `props.note` уже вызывается `noteStore.fetchHistory(newNote.id)` при режиме `edit`
   - В шаблоне уже используется `v-for="entry in noteStore.history"` для отображения истории
   - Функция `getHistoryActionText` уже реализует маппинг текстов для разных действий
   - Маппинг действий:
     - `created` -> "Заметка создана"
     - `updated` -> "Заметка обновлена"
     - `status_changed` -> "Статус изменен: {oldStatus} → {newStatus}"
     - `commented` -> "Добавлен комментарий"

#### Backend изменения (`src/server/features/notes/notes.service.ts`):

1. **Добавлена запись в историю при изменении контента**:
   - В `updateNote` добавлена проверка изменения заголовка или содержания
   - Если `sanitizedTitle !== undefined` или `sanitizedContent !== undefined`, создается запись в `note_history`
   - Действие: `updated`
   - Payload содержит информацию о том, что изменилось (`titleChanged`, `contentChanged`)

#### Критерии приемки (выполнены):

1. ✅ В `onMounted` или при открытии модалки в режиме `edit` вызывается `noteStore.fetchHistory(props.note.id)`
2. ✅ В шаблоне используется `v-for="entry in noteStore.history"`
3. ✅ Реализован маппинг текстов для разных действий (`created`, `status_changed`, `commented`)
4. ✅ При изменении контента (title или content) добавляется запись в `note_history`

---

### [x] Task 2.5: Рефакторинг NoteModal.vue (Декомпозиция)

#### Frontend изменения (`src/client/features/NoteModal.vue`):

**Создан хук `useNoteForm.ts`** (`src/client/entities/note/model/useNoteForm.ts`):
   - Инкапсулирует управление формой заметки (поля, валидация, сохранение)
   - Предоставляет методы: `initForm`, `validate`, `getCreateData`, `getUpdateData`, `toggleReminder`

**Создан хук `useMentions.ts`** (`src/client/shared/lib/useMentions.ts`):
   - Инкапсулирует управление упоминаниями сотрудников в текстовых полях
   - Предоставляет методы: `loadEmployees`, `handleInput`, `handleBlur`, `selectEmployee`, `closeSuggestion`
   - Фильтрует сотрудников по `isFired` (уволенные сотрудники не отображаются)
   - Автоматически обрабатывает ввод `@` и показывает выпадающий список

**Создан компонент `NoteHistoryList.vue`**:
   - Вынесен логику отображения истории изменений из мода
   - Принимает проп `history` с массивом `NoteHistoryEntry[]`
   - Использует строгие типы из `NoteHistoryAction`
   - Предоставляет методы форматирования: `getHistoryActionText`, `formatDate`, `getStatusLabel`
   - Имеет отдельные иконки для разных действий (created, updated, status_changed, commented, returned)

**Создан компонент `NoteCommentsList.vue`**:
   - Вынесен логику отображения комментариев из мода
   - Принимает проп `comments` с массивом `NoteComment[]`
   - Предоставляет методы: `parseMentions`, `getAvatarInitials`, `getAuthorName`, `formatRelativeTime`
   - Поддерживает отображение аватарок и относительного времени

**Рефакторинг `NoteModal.vue`**:
   - Удалены локальные `ref` для полей формы (заменены на хук `useNoteForm`)
   - Удалены локальные `ref` для состояния упоминаний (заменены на хук `useMentions`)
   - Удалены дублирующиеся функции: `handleContentInput`, `handleTextareaBlur`, `selectEmployee`, `loadEmployees`, `formatDate`, `getStatusLabel`, `getHistoryActionText`, `parseMentions`
   - Удален блок истории изменений (заменен на компонент `NoteHistoryList`)
   - Удален блок комментариев (заменен на компонент `NoteCommentsList`)
   - Упрощен код компонента с ~300 строк (было ~1500 строк)
   - Добавлен импорт типа `Employee` для использования в функции-обертке
   - Сохранена вся функциональность мода
   - Улучшена читаемость и поддерживаемость кода

#### Backend изменения:

**Без изменений** - Бэкенд для заметок не требовал изменений

#### Критерии приемки:
1. ✅ Хук `useNoteForm` корректно инкапсулирует форму
2. ✅ Хук `useMentions` корректно управляет упоминаниями
3. ✅ Компонент `NoteHistoryList` корректно отображает историю
4. ✅ Компонент `NoteCommentsList` корректно отображает комментарии
5. ✅ Типизация использует строгие типы из `NoteHistoryAction`
6. ✅ Фильтрация сотрудников по `isFired` работает корректно
7. ✅ Удалены дублирующиеся функции и переменные
8. ✅ Код компонента упрощен и стал более читаемым

---

### [x] Исправление системы упоминаний в NoteModal.vue

#### Frontend изменения (`src/client/features/NoteModal.vue`):

1. **Обработка Escape для закрытия окон**:
   - Обновлена функция `handleKeyDown` для приоритетного закрытия окон
   - Сначала закрывается окно упоминаний (`showEmployeeSuggestion`)
   - Затем закрывается контекстное меню (`showContextMenu`)
   - В конце закрывается модальное окно (`handleClose`)

2. **Обработка клика вне области**:
   - Добавлены refs для DOM элементов: `contextMenuRef`, `employeeSuggestionRef`
   - Реализована функция `handleClickOutside` для обработки кликов вне окон
   - При клике вне окна упоминаний: закрывается окно и очищается поисковый запрос
   - При клике вне контекстного меню: закрывается меню
   - Слушатель события `click` добавлен в `onMounted` и удален в `onUnmounted`

3. **Добавление refs к элементам шаблона**:
   - Добавлен `ref="contextMenuRef"` к контекстному меню
   - Добавлен `ref="employeeSuggestionRef"` к окну упоминаний сотрудников

#### Критерии приемки (выполнены):

1. ✅ При нажатии Escape закрывается окно упоминаний (если открыто)
2. ✅ При нажатии Escape закрывается контекстное меню (если открыто)
3. ✅ При нажатии Escape закрывается модальное окно (если другие окна закрыты)
4. ✅ При клике вне окна упоминаний оно закрывается
5. ✅ При клике вне контекстного меню оно закрывается
6. ✅ При закрытии окна упоминаний очищается поисковый запрос

---

### [x] Динамический статус пользователя в UserModal (бывш. EmployeeModal)

#### Frontend изменения (`src/client/widgets/EmployeeModal.vue`):

1. **Обновлено вычисляемое свойство `accountStatus`**:
   - Возвращает объект с текстом статуса и цветом (green/red/orange)
   - Логика условий:
     - Если `employee.isFired === true` -> Статус: "Учетная запись отключена", цвет: red
     - Если `employee.isFired === false` И `employee.archivedAt` (в будущем) -> Статус: "Пользователь будет уволен с {дата}", цвет: orange
     - Если `employee.isFired === false` И `archivedAt` отсутствует -> Статус: "Учетная запись активна", цвет: green

2. **Обновлен шаблон индикатора статуса**:
   - Динамическое отображение статуса через `accountStatus.text`
   - Динамическая цветовая схема через `accountStatus.color`
   - Цветовые схемы (без border):
     - Отключена: `bg-red-100 text-red-800`
     - Запланировано: `bg-orange-100 text-orange-800`
     - Активна: `bg-green-100 text-green-800`
   - Иконки по статусам:
     - Активна: `Check` (галочка)
     - Отключена: X-крестик (SVG)
     - Запланировано: `Calendar` (календарь)

#### Критерии приемки (выполнены):

1. ✅ Статус "Учетная запись отключена" отображается с красной цветовой схемой для уволенных пользователей
2. ✅ Статус "Учетная запись активна" отображается с зеленой цветовой схемой для активных пользователей
3. ✅ Статус "Пользователь будет уволен с {дата}" отображается с оранжевой цветовой схемой для запланированного увольнения
4. ✅ Для статуса "Запланировано" используется иконка `Calendar` вместо предупреждения
5. ✅ Используется существующая функция `formatDate` для вывода даты
6. ✅ Используются только данные из объекта `props.employee` (сущность User)

---

### [x] Исправление дублей, «невидимок» и уведомлений

#### Backend изменения (`src/server/features/schedule/schedule.service.ts`):

1. **Гарантированное обновление черновика в `updateShift`**:
   - Упрощена логика для менеджеров (`!isAdmin`): всегда ищется существующий черновик (`draft = existing.find(s => !s.isApproved)`)
   - Если черновик найден — выполняется `update` статуса (включая статус `Blank`)
   - Если черновик не найден — выполняется `insert` новой записи с `isApproved: false`
   - Сохранен Smart Removal: если менеджер ставит `Blank` и нет согласованной записи, черновики удаляются физически

2. **Гарантированное обновление черновика в `bulkUpdateShifts`**:
   - Применена та же логика для пакетных обновлений
   - Убраны инкременты счетчика `actualDraftUpdatesCount` (счетчик больше не нужен)
   - Для синхронизации Manager -> Maid также применена логика гарантированного обновления черновика

3. **Исправление системы уведомлений (Approval Requests)**:
   - Убрана проверка `actualDraftUpdatesCount > 0` в методе `bulkUpdateShifts`
   - Теперь заявка на согласование создается **всегда**, если менеджер нажал кнопку «Сохранить»
   - В `payload.updatesCount` передается длина массива `updates` вместо счетчика
   - Удален счетчик `actualDraftUpdatesCount` из метода `bulkUpdateShifts`

#### Frontend изменения (`src/client/widgets/ScheduleGrid.vue`):

1. **Метод `getDisplayStatus` — приоритет черновика из БД**:
   - Добавлена проверка черновиков из БД (`!isApproved`) с приоритетом над согласованными записями
   - Если есть локальные изменения (`pendingChanges`) — возвращается их статус
   - Если нет локальных изменений, но есть черновик из БД — возвращается статус черновика
   - Если менеджер предложил `Blank`, метод возвращает `Blank` для корректного отображения в поповере

2. **Метод `getCellClass` — отображение `pending-change` для черновиков**:
   - Добавлена проверка черновиков из БД для отображения класса `pending-change`
   - Теперь черновики из БД (включая со статусом `Blank`) получают синюю рамку
   - Локальные изменения и черновики из БД обрабатываются одинаково при отображении

3. **Методы `hasUnapprovedShifts` и `hasAnyUnapprovedContent` — учет черновиков Blank**:
   - Убрана фильтрация `s.status !== 'Blank'`
   - Теперь черновики со статусом `Blank` учитываются при отображении кнопок «Согласовать/Отклонить» для админа
   - Админ видит все несогласованные изменения, включая предложения удалить смену

4. **Метод `isConflictCell` — уже был корректным**:
   - Не фильтрует по статусу, проверяет только наличие `isApproved: true` и `isApproved: false`
   - Конфликтом считается ситуация, когда в массиве смен для одной даты есть и согласованная, и черновик запись

#### Критерии приемки (выполнены):

1. ✅ Менеджер не создает более одной записи `isApproved: false` для одной ячейки (гарантированное обновление черновика)
2. ✅ При наличии черновика менеджер обновляет его вместо создания нового (update вместо insert)
3. ✅ Smart Removal работает: если менеджер ставит `Blank` и нет согласованной записи, черновики удаляются
4. ✅ Админ видит красную точку уведомления на все сохранения менеджера (без фильтрации `actualDraftUpdatesCount > 0`)
5. ✅ Админ видит восклицательный знак на пустых ячейках с черновиком `Blank`
6. ✅ Ячейки с черновиком `Blank` получают синюю рамку (`pending-change`)
7. ✅ Поповер показывает текст «предлагает заменить на Пусто» при наличии черновика со статусом `Blank`

---

### [x] Исправление визуальных индикаторов в ScheduleGrid.vue

#### Frontend изменения (`src/client/widgets/ScheduleGrid.vue`):

1. **Метод `getCellClass` — исправлена логика отображения рамок и фона**:
   - **Синяя рамка (`pending-change`)**: теперь применяется ТОЛЬКО для несохраненных локальных правок (`pendingShift`)
   - **Удалена проверка `|| draftShift`**: синяя рамка больше не отображается для черновиков из БД
   - **Красная рамка (`ring-2 ring-red-500 ring-inset`)**: применяется ТОЛЬКО для админа при наличии черновика в БД (`shift && !shift.isApproved`)
   - **Серый фон для менеджера**: если есть черновик из БД (`isApproved === false`) и роль пользователя НЕ `ADMIN`, ячейка имеет серый фон без синей рамки
   - **Локальные правки для админа**: если админ начал править ячейку с черновиком, отображается синяя рамка поверх красной

2. **Функция `getStatusColorClass` (`src/client/shared/lib/scheduleHelpers.ts`)**:
   - Уже корректно обрабатывает случай `isApproved === false` (строки 118-120)
   - Возвращает серый фон `bg-gray-300` для несогласованных смен

#### Критерии приемки (выполнены):

1. ✅ Синяя рамка отображается только для локальных несохраненных изменений (`pendingChanges`)
2. ✅ Синяя рамка НЕ отображается для черновиков из БД (`draftShift`)
3. ✅ Для менеджера: черновик из БД отображается с серым фоном и без синей рамки
4. ✅ Для админа: черновик из БД отображается с красной рамкой
5. ✅ Для админа: при локальной правке ячейки с черновиком отображается синяя рамка поверх красной

---

---

### [x] Исправление логики красной рамки для согласованных ячеек

#### Проблема:
При изменении менеджером статуса ячейки, которая уже была согласована (`isApproved: true`), в сетке у админа не отображалась красная рамка. Это происходило из-за того, что метод `getShift` возвращал только одну запись из индексированного словаря, а не проверял наличие черновика в массиве смен.

#### Frontend изменения (`src/client/widgets/ScheduleGrid.vue`):

1. **Метод `getCellClass` — исправлена логика отображения красной рамки**:
   - Добавлена переменная `dateStr` для форматирования даты в формате `YYYY-MM-DD`
   - Добавлена проверка `hasDraftInDb` через `gridData.value?.shifts.some()` для поиска черновиков в базе данных
   - **Синяя рамка (`pending-change`)**: отображается ТОЛЬКО для несохраненных локальных правок (`pendingShift`)
   - **Красная рамка (`ring-2 ring-red-500 ring-inset`)**: отображается для ADMIN при наличии черновика в БД (`hasDraftInDb === true`), включая предложения по удалению (status: Blank)
   - Логика проверки черновика теперь напрямую ищет в массиве смен, а не полагается на `getShift`, который может вернуть не ту запись

#### Критерии приемки (выполнены):

1. ✅ Красная рамка отображается для ADMIN при наличии черновика (`isApproved: false`) в базе данных
2. ✅ Красная рамка отображается для черновиков со статусом `Blank` (предложение удалить смену)
3. ✅ Красная рамка отображается для черновиков поверх согласованной записи (`isApproved: true`)
4. ✅ Синяя рамка отображается только для локальных несохраненных изменений (`pendingShift`)
5. ✅ Если в базе есть черновик, а админ начал править ячейку — отображается и синяя, и красная рамка

---

### [x] Автоматическое удаление согласованных смен со статусом Blank

#### Backend изменения (`src/server/features/schedule/schedule.service.ts`):

1. **Добавлено физическое удаление «мусорных» записей в методе `approveShiftsForMonth`**:
   - Сразу после массового обновления черновиков (строки 930-936) добавлена операция удаления
   - Удаляются записи, где `status = 'Blank'` И `isApproved = true` в указанном месяце
   - Используются фильтры: `gte(shifts.date, startDate)`, `lte(shifts.date, endDate)`, `eq(shifts.status, 'Blank')`, `eq(shifts.isApproved, true)`
   - Это предотвращает засорение базы данных избыточными записями («пустота, подтвержденная админом»)

#### Критерии приемки (выполнены):

1. ✅ После согласования смен физически удаляются записи со статусом `Blank` и `isApproved: true`
2. ✅ Удаление происходит в рамках указанного месяца (startDate — endDate)
3. ✅ Операция выполняется сразу после массового обновления черновиков

---

## Архив задач

(пока нет)

---

### [x] Реализация системы уведомлений и упоминаний в модуле Notes

#### Backend изменения (`src/server/features/notes/`):

---

### [x] Реализация раздела "База номеров"

#### Backend изменения (`src/server/features/rooms/`):

1. **Создана таблица `rooms`** (`db/rooms.table.ts`):
   - Поля: `id` (varchar(10), `type` (enum), `hasHood` (boolean), `createdAt`, `updatedAt`, `archivedAt`
   - ID формата: буква A-Z + число 1-99 (например, A1, B12)

2. **Созданы Zod-схемы** (`rooms.schema.ts`):
   - Валидация ID: `regex(/^[A-Z][1-9][0-9]?$/)`
   - Тип: enum `['апарт', 'номер']`
   - Логика `hasHood`: если `type !== 'апарт'`, то всегда `false`
   - Схемы ответа с обертками

3. **Создан сервис** (`rooms.service.ts`):
   - CRUD операции: `createRoom`, `getAllRooms`, `getRoomById`, `updateRoom`, `deleteRoom`
   - Специфическая сортировка: сначала по букве, затем по числу как числу (A2 перед A10)
   - Soft delete через `archivedAt`

4. **Созданы роуты** (`rooms.routes.ts`):
   - POST `/` - создание номера (ADMIN only)
   - GET `/` - получение всех номеров
   - GET `/:id` - получение номера по ID
   - PUT `/:id` - обновление номера (ADMIN only)
   - DELETE `/:id` - удаление номера (ADMIN only)
   - Использование `preHandler: requireRole('ADMIN')` для проверки ролей
   - Обертки ответов: `{ success: true, data: ... }`

5. **Создан плагин** (`index.ts`):
   - Регистрация роутов с префиксом `/api/rooms`

6. **Зарегистрирована таблица** (`src/server/shared/db/schema.ts`):
   - Добавлен экспорт таблицы `rooms`

7. **Зарегистрирован плагин** (`src/server/app.ts`):
   - Добавлен импорт и регистрация `roomsPlugin`

#### Frontend изменения (`src/client/`):

1. **Добавлены API методы** (`shared/api.ts`):
   - Интерфейсы: `Room`, `CreateRoomInput`, `UpdateRoomInput`
   - Методы: `getAll`, `getById`, `create`, `update`, `delete`

2. **Создан entity** (`entities/room.ts`):
   - Pinia store: `useRoomStore`
   - Состояние: `rooms`, `loading`, `error`
   - Вычисляемые свойства: `activeRooms`, `archivedRooms`
   - Actions: `fetchRooms`, `fetchRoomById`, `createRoom`, `updateRoom`, `deleteRoom`, `clearError`

3. **Создана страница** (`pages/RoomsPage.vue`):
   - Таблица с колонками: №, Тип, Вытяжка, Действия
   - Кнопка "Добавить номер" (только для ADMIN)
   - Модальное окно создания/редактирования
   - Логика: при смене типа с "апарт" на "номер", чекбокс "Вытяжка" автоматически выключается и блокируется
   - Модальное окно подтверждения удаления

4. **Добавлен маршрут** (`app/main.ts`):
   - Маршрут `/dashboard/rooms` для RoomsPage

5. **Добавлен пункт в меню** (`shared/config/menuConfig.ts`):
   - Пункт "База номеров" с иконкой `Home`
   - Доступ для ролей: ADMIN, MANAGER, MAID
   - Read-only mode для MANAGER и MAID

6. **Добавлен экспорт** (`pages/index.ts`):
   - Экспорт `RoomsPage`

#### Критерии приемки (выполнены):

1. ✅ Backend: создана таблица `rooms` с правильной структурой
2. ✅ Backend: Zod-валидация ID с regex `^[A-Z][1-9][0-9]?$`
3. ✅ Backend: логика `hasHood` - только для типа "апарт"
4. ✅ Backend: специфическая сортировка (буква, затем число)
5. ✅ Backend: проверка ролей через `requireRole('ADMIN')`
6. ✅ Frontend: API методы для работы с `/rooms`
7. ✅ Frontend: Pinia store для управления номерами
8. ✅ Frontend: страница RoomsPage с таблицей и модалками
9. ✅ Frontend: автоматическое выключение чекбокса "Вытяжка" при смене типа
10. ✅ Frontend: маршрут `/dashboard/rooms` зарегистрирован
11. ✅ Frontend: пункт "База номеров" добавлен в меню ПЕРЕД "Локальное оборудование"
12. ✅ Frontend: read-only mode для MANAGER и MAID

---

### [x] Реализация системы уведомлений и упоминаний в модуле Notes

#### Backend изменения (`src/server/features/notes/`):

1. **Уведомление менеджеров при создании заметки ADMIN-ом** (`notes.service.ts`):
   - В `createNote` добавлена проверка роли автора
   - Если автор — ADMIN, отправляется уведомление всем менеджерам через `notifyMultipleUsers`
   - Событие: `note:created`

2. **Логика статусов с комментариями в историю** (`notes.service.ts`):
   - В `updateNote` добавлено создание записей в `note_history` при изменении статуса
   - Если передано поле `comment`, создается запись с типом `status_changed` и текстом комментария в `payload.commentText`
   - Работает для MANAGER и ADMIN

3. **Реализация метода createNoteComment с поддержкой упоминаний** (`notes.service.ts`):
   - Добавлен интерфейс `CreateNoteCommentInput`
   - Реализована функция `createNoteComment` для создания комментариев
   - При создании комментария текст прогоняется через `parseMentions`
   - Упомянутым пользователям отправляются уведомления через `notifyMentionedUsers`
   - Создается запись в истории с типом `commented`

4. **Добавление роутов для комментариев и истории** (`notes.routes.ts`):
   - Добавлен роут `GET /:id/comments` для получения комментариев
   - Добавлен роут `POST /comments` для создания комментариев
   - Добавлен роут `GET /:id/history` для получения истории

5. **Обновление схемы** (`notes.schema.ts`):
   - Добавлено поле `comment` в `updateNoteSchema` для передачи комментария при изменении статуса

#### Frontend изменения (`src/client/features/NoteModal.vue`):

1. **Employee Suggestion**:
    - Добавлен импорт `employeeApi` и `AtSign` иконки
    - Добавлены состояния: `employees`, `showEmployeeSuggestion`, `employeeSuggestionPosition`, `mentionSearchQuery`, `filteredEmployees`
    - Реализована функция `handleContentInput` для детекции ввода `@`
    - Реализована функция `selectEmployee` для вставки выбранного сотрудника в текст
    - Добавлен выпадающий список с фильтрацией сотрудников
    - Стилизация выпадающего списка

2. **Улучшение UX окна упоминаний**:
    - Добавлена обработка Escape для закрытия окна упоминаний и контекстного меню
    - Добавлена обработка клика вне области для закрытия окон
    - При закрытии окна упоминаний очищается поисковый запрос
    - При выборе сотрудника окно закрывается и фокус возвращается на textarea

2. **Status Change UI**:
   - Добавлены состояния: `showStatusChangeModal`, `statusChangeComment`, `pendingStatus`
   - Реализована функция `handleStatusChange` для открытия модалки при изменении статуса
   - Реализованы функции `confirmStatusChange` и `cancelStatusChange`
   - Добавлена модалка с полем для ввода комментария
   - Обновлена функция `handleSave` для включения комментария

3. **Динамический History Rendering**:
   - Добавлена загрузка истории при открытии модалки в режиме edit
   - Заменен статичный список истории на динамический `noteStore.history`
   - Реализована функция `getHistoryActionText` для отображения текста действия
   - Добавлены иконки для разных типов действий
   - Добавлено отображение комментария из `payload.commentText`

#### Frontend изменения (`src/client/pages/NotesPage.vue`):

1. **Deep Linking**:
   - Добавлен импорт `useRoute` и `useNoteStore`
   - Реализован `watch` для отслеживания `route.query.noteId`
   - При наличии `noteId` в query, заметка открывается в режиме edit через `openEditModal`

#### Frontend изменения (`src/client/entities/note.ts`):

1. **Socket Integration для mention:notification**:
   - Обновлен обработчик `mention:notification` для обновления списка комментариев
   - Добавлен вызов `fetchNotes()` для обновления состояния заметок

#### Frontend изменения (`src/client/entities/note.ts`):

1. **Обновление интерфейса UpdateNoteInput**:
   - Добавлено поле `comment?: string` для передачи комментария при изменении статуса

#### Frontend изменения (`src/client/widgets/NotesBoard.vue`):

1. **Добавление функции openEditModal**:
   - Функция `openEditModal` добавлена в `defineExpose` для вызова из родительского компонента

#### Критерии приемки (выполнены):

1. ✅ При создании заметки ADMIN-ом уведомляются все менеджеры
2. ✅ При изменении статуса с комментарием создается запись в истории
3. ✅ При создании комментария поддерживаются упоминания (@Имя Фамилия)
4. ✅ Упомянутым пользователям отправляются уведомления
5. ✅ В NoteModal реализован выпадающий список сотрудников при вводе `@`
6. ✅ При изменении статуса запрашивается комментарий через модалку
7. ✅ История изменений отображается динамически из `noteStore.history`
8. ✅ Deep linking работает: заметка открывается по `?noteId=...`
9. ✅ Фронтенд слушает `mention:notification` и обновляет состояние
10. ✅ При нажатии Escape закрывается окно упоминаний (если открыто)
11. ✅ При нажатии Escape закрывается контекстное меню (если открыто)
12. ✅ При клике вне окна упоминаний оно закрывается
13. ✅ При клике вне контекстного меню оно закрывается
14. ✅ При закрытии окна упоминаний очищается поисковый запрос
