# Прогресс проекта Hotel Staff ERP v2

---

## [x] Task #24: Интеграция управления в карточку и фикс ресайза (v2.17.0)

**Цель**: Интегрировать управление в карточку задач, зафиксировать ресайз и реализовать drag-and-drop reordering для подпунктов.

#### Измененные файлы:

**Frontend:**

1. **TasksBoard Widget** (`src/client/widgets/TasksBoard.vue`):
    - Проверена конфигурация сетки (уже была корректна):
      - `:col-num="6"` ✓
      - `:margin="[10, 10]"` ✓
      - `:drag-allow-from="'.task-card__header'"` ✓
    - Проверен метод `onItemResized()` (уже был корректен):
      - Формула: `(newH * ROW_H) + ((newH - 1) * MARGIN_H)` ✓
      - Сохранение в `task.position.h` ✓

2. **TaskCard Component** (`src/client/features/TaskCard.vue`):
    - Удален импорт `TaskModal` ✓
    - Удалены функции `openModal()`, `handleSave()`, `handleBodyClick()` ✓
    - Удален компонент `TaskModal` из шаблона ✓
    - Добавлена функция `handleReorder()` для drag-and-drop reordering подпунктов ✓
    - Проверены стили (уже были корректны):
      - `.task-card` имеет `display: flex` и `flex-direction: column` ✓
      - `.task-card__body` имеет `flex: 1` и `overflow-y: auto` ✓
      - Нет жесткого `max-height` ✓

3. **TaskItem Component** (`src/client/features/TaskItem.vue`):
    - Добавлен drag-handle (иконка с шестью точками) ✓
    - Добавлены drag-and-drop события: `@dragstart`, `@dragover`, `@dragleave`, `@drop`, `@dragend` ✓
    - Добавлено событие `@reorder` для уведомления родителя о перестановке ✓
    - Добавлены визуальные состояния: `task-item--dragging`, `task-item--drag-over` ✓
    - Добавлена визуальная индикация для вложенных задач:
      - `margin-left: 24px` ✓
      - Левая линия-акцент (`border-left: 2px solid #3b82f6`) ✓
    - Автосохранение уже реализовано через emit событий ✓

**Backend:**

4. **Tasks Table Schema** (`src/server/features/tasks/db/tasks.table.ts`):
    - Проверено отсутствие старых полей `date` и `archivedAt` ✓

5. **Tasks Service** (`src/server/features/tasks/tasks.service.ts`):
    - Проверено отсутствие передачи старых полей при создании задачи ✓

6. **Tasks Contract** (`src/shared/contracts/tasks.contract.ts`):
    - Проверено отсутствие старых полей в схемах ✓

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

1. ✅ `TasksBoard.vue` использует `:col-num="6"`
2. ✅ `TasksBoard.vue` использует `:margin="[10, 10]"`
3. ✅ `TasksBoard.vue` использует `:drag-allow-from="'.task-card__header'"`
4. ✅ `onItemResized()` использует формулу `(newH * ROW_H) + ((newH - 1) * MARGIN_H)`
5. ✅ `TaskModal` удален из `TaskCard.vue`
6. ✅ Drag-and-drop reordering для подпунктов реализован через HTML5 Drag API
7. ✅ Визуальная индикация для вложенных задач (margin-left + линия-акцент)
8. ✅ Drag-handle (иконка с шестью точками) добавлен
9. ✅ Автосохранение изменений (текст, чекбокс, reorder) реализовано
10. ✅ `.task-card` имеет `display: flex` и `flex-direction: column`
11. ✅ `.task-card__body` имеет `flex: 1` и `overflow-y: auto`
12. ✅ Нет жесткого `max-height` в `TaskCard.vue`
13. ✅ Старые поля `date` и `archivedAt` отсутствуют в схеме БД
14. ✅ Старые поля не передаются при создании задачи

#### Ожидаемый результат (проверен):

- Карточки задач на доске можно перетаскивать только за заголовок (`.task-card__header`)
- Подпункты внутри карточки можно перетаскивать для изменения порядка (reordering)
- При перетаскивании подпункта он подсвечивается с визуальной обратной связью
- Вложенные подпункты имеют отступ 24px и синюю левую линию-акцент
- Все изменения (текст, чекбокс, reorder) сохраняются автоматически без подтверждения
- Карточка растягивается по высоте вслед за GridItem (flex layout)
- При создании задачи в базу не пишутся старые поля `date` или `archivedAt`

---

## [x] Task #21: Реализация Task Board & Task Modal (Enterprise v2)

**Цель**: Реализовать TaskModal с UI/UX из NoteModal.vue, обновить конфигурацию сетки TasksBoard и интегрировать модалку в TaskCard.

#### Измененные файлы:

**Frontend:**

1. **TasksBoard Widget** (`src/client/widgets/TasksBoard.vue`):
    - Обновлена конфигурация сетки:
      - Изменен `:col-num="12"` на `:col-num="6"`
      - Добавлен `:drag-allow-from="'.task-card__header'"` для ограничения drag-области
    - Константы уже корректны:
      - `ROW_H = 5`, `MARGIN_H = 10`, `UNIT_H = 15`
      - `MIN_HEIGHT_UNITS = 12`
    - Формула в `onItemResized` уже использует: `(newH * ROW_H) + ((newH - 1) * MARGIN_H)` с `Math.round()`

2. **TaskModal Component** (`src/client/features/TaskModal.vue`):
    - Создан новый компонент модалки для создания/редактирования задач
    - UI/UX полностью скопирован из NoteModal.vue:
      - Темный оверлей (`background-color: rgba(0, 0, 0, 0.7)`)
      - Темный фон модалки (`background-color: #002436`)
      - Белый текст и полупрозрачные границы
      - Плавные переходы и анимации
    - Функционал:
      - Редактирование названия задачи
      - Переключение статуса выполнения
      - Список подпунктов с drag-and-drop (HTML5 Drag API)
      - Кнопки отступа (→/←) для вложенности подпунктов (parentId)
      - Добавление/удаление подпунктов
      - Сохранение изменений через store

3. **TaskCard Component** (`src/client/features/TaskCard.vue`):
    - Добавлен импорт `TaskModal` и типов из `@shared/contracts/tasks.contract`
    - Добавлено состояние `isModalOpen` для управления модалкой
    - Добавлена функция `openModal()` для открытия модалки
    - Добавлена функция `handleSave()` для сохранения из модалки
    - Добавлена функция `handleBodyClick()` для открытия модалки при клике по телу карточки
    - Добавлен `@click="handleBodyClick"` на `.task-card__body`
    - Убран жесткий `max-height: 400px` из `.task-card`
    - Добавлен `TaskModal` в template с привязкой к состоянию

4. **Features Index** (`src/client/features/index.ts`):
    - Добавлен экспорт `TaskModal.vue`
    - Добавлен экспорт `TaskCard.vue`
    - Добавлен экспорт `TaskItem.vue`

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

1. ✅ `TasksBoard.vue` использует `:col-num="6"`
2. ✅ `TasksBoard.vue` использует `:drag-allow-from="'.task-card__header'"`
3. ✅ Константы сетки: `ROW_H = 5`, `MARGIN_H = 10`, `UNIT_H = 15`
4. ✅ Формула в `onItemResized`: `(newH * ROW_H) + ((newH - 1) * MARGIN_H)` с `Math.round()`
5. ✅ `TaskModal.vue` создан с UI/UX из NoteModal.vue
6. ✅ Drag-and-Drop для подпунктов реализован (HTML5 Drag API)
7. ✅ Кнопки отступа (→/←) для вложенности подпунктов реализованы
8. ✅ `TaskCard.vue` открывает модалку при клике по телу карточки
9. ✅ `TaskCard.vue` убран жесткий `max-height`
10. ✅ `features/index.ts` экспортирует `TaskModal`, `TaskCard`, `TaskItem`
11. ✅ `task.ts` store уже использует `tasks: [] as Task[]` в state
12. ✅ Все геттеры в `task.ts` используют `(state.tasks || [])`
13. ✅ `tasksTree` в `task.ts` собирает 2 уровня вложенности
14. ✅ `task.ts` использует `taskSchema` из `@shared/contracts/tasks.contract.ts`

#### Ожидаемый результат (проверен):

- TasksBoard использует 6 колонок вместо 12
- Карточки задач можно перетаскивать только за заголовок (`.task-card__header`)
- При клике по телу карточки открывается TaskModal
- TaskModal имеет темный дизайн как NoteModal
- В модалке можно редактировать подпункты с drag-and-drop
- Подпункты можно вкладывать друг в друга через кнопки →/←
- Высота карточки определяется сеткой, а не жестким max-height

---

## [x] Task #20: Исправление ресайза и валидации подпунктов (v2.14.4)

**Цель**: Исправить ресайз карточек задач и ослабить валидацию для подпунктов.

#### Измененные файлы:

**Shared:**

1. **Tasks Contract** (`src/shared/contracts/tasks.contract.ts`):
    - Ослаблена валидация поля `text` в `createTaskInputSchema`:
      - Заменено `z.string().min(1, 'Text is required')` на `z.string()`
    - Ослаблена валидация поля `text` в `updateTaskInputSchema`:
      - Заменено `z.string().min(1, 'Text is required').optional()` на `z.string().optional()`
    - Теперь допускаются пустые строки для подпунктов

**Frontend:**

2. **TasksBoard Widget** (`src/client/widgets/TasksBoard.vue`):
    - Синхронизированы константы сетки с NotesBoard.vue:
      - `ROW_H = 5` (вместо 30)
      - `MARGIN_H = 10` (вместо MARGIN = 16)
      - `UNIT_H = ROW_H + MARGIN_H` (15px)
      - `MIN_HEIGHT_UNITS = 12` (вместо MIN_H = 13)
      - `DEFAULT_H = 12` (вместо 13)
    - Обновлен метод `updateLayoutFromTasks()`:
      - Теперь использует формулу для расчета Y: `Math.round((pos.y || 0) / UNIT_H)`
      - Теперь использует формулу для расчета высоты: `Math.round((heightPx + MARGIN_H) / UNIT_H)`
      - Гарантирует минимальную высоту 12 юнитов (~170px)
    - Обновлен метод `onItemMoved()`:
      - Теперь умножает newY на UNIT_H для получения позиции в пикселях
      - Сохраняет текущую ширину и высоту при перемещении
    - Обновлен метод `onItemResized()`:
      - Использует правильную формулу для расчета высоты в пикселях: `(newH * ROW_H) + ((newH - 1) * MARGIN_H)`
      - Округляет результат через `Math.round()`
      - Гарантирует минимальную высоту 145px (лимит бэкенда)
    - Обновлен метод `handleCreateList()`:
      - Теперь конвертирует DEFAULT_H (12 юнитов) в пиксели при создании новой задачи

3. **TaskItem Component** (`src/client/features/TaskItem.vue`):
    - Добавлен expose метода `focusInput()` для использования родительским компонентом

4. **TaskCard Component** (`src/client/features/TaskCard.vue`):
    - Добавлен ref `lastTaskItemRef` для хранения ссылки на последний подпункт
    - Добавлен computed `lastChildTaskId` для отслеживания ID последнего подпункта
    - Добавлена функция `setLastTaskItemRef()` для установки ref
    - Обновлен метод `addSubtask()`:
      - После создания нового подпункта вызывает `lastTaskItemRef.value?.focusInput()`
      - Теперь при создании подпункта фокус автоматически переносится на новую строку
    - Обновлен шаблон:
      - Добавлен `:ref` привязка для последнего элемента в списке подпунктов

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

1. ✅ Валидация `text` в `createTaskInputSchema` ослаблена (допускаются пустые строки)
2. ✅ Валидация `text` в `updateTaskInputSchema` ослаблена (допускаются пустые строки)
3. ✅ Константы сетки синхронизированы с NotesBoard.vue (ROW_H=5, MARGIN_H=10, MIN_HEIGHT_UNITS=12)
4. ✅ Метод `updateLayoutFromTasks()` использует правильные формулы для расчета позиции и высоты
5. ✅ Метод `onItemMoved()` умножает newY на UNIT_H для получения пикселей
6. ✅ Метод `onItemResized()` использует формулу `(newH * ROW_H) + ((newH - 1) * MARGIN_H)` с округлением
7. ✅ Метод `handleCreateList()` конвертирует DEFAULT_H в пиксели
8. ✅ TaskItem компонент expose'ит метод `focusInput()`
9. ✅ TaskCard компонент автоматически фокусируется на новый подпункт после создания
10. ✅ Минимальная высота 12 юнитов (~170px) выше лимита бэкенда (145px)

#### Ожидаемый результат (проверен):

- Подпункты задач могут создаваться с пустым текстом (без ошибки валидации)
- Ресайз карточек задач работает корректно с константами сетки из NotesBoard.vue
- Курсор не "убегает" при ресайзе (минимум 12 юнитов = ~170px)
- API не возвращает ошибку при ресайзе (минимум 145px соблюден)
- При создании нового подпункта фокус автоматически переносится на новую строку

---

## [x] Task #19: Синхронизация модуля Задач с глобальным API-клиентом (v2.14.3)

**Цель**: Синхронизировать модуль Задач с глобальным API-клиентом, чтобы response схемы описывали только payload (без обертки `{ success, data }`).

#### Измененные файлы:

**Backend:**

1. **Tasks Routes** (`src/server/features/tasks/tasks.routes.ts`):
    - Обновлены все роуты для возврата только payload (без обертки `{ success, data }`):
      - `GET /api/tasks` - теперь возвращает массив задач напрямую
      - `GET /api/tasks/:id` - теперь возвращает задачу напрямую
      - `POST /api/tasks` - теперь возвращает созданную задачу напрямую
      - `PATCH /api/tasks/:id` - теперь возвращает обновленную задачу напрямую
      - `PATCH /api/tasks/:id/toggle` - теперь возвращает задачу напрямую

**Frontend:**

2. **Tasks Contract** (`src/shared/contracts/tasks.contract.ts`):
    - Обновлены response схемы для описания только payload:
      - `tasksListResponseSchema` - теперь `z.array(taskSchema)` (вместо `{ success, data }`)
      - `taskResponseSchema` - теперь `taskSchema` (вместо `{ success, data }`)

3. **Task Repository** (`src/client/shared/api/repositories/TaskRepository.ts`):
    - Обновлены все методы для работы с чистыми данными после интерцептора:
      - `getAll()` - теперь возвращает `Task[]` напрямую
      - `getAllFresh()` - теперь возвращает `Task[]` напрямую
      - `getById()` - теперь возвращает `Task` напрямую
      - `create()` - теперь возвращает `Task` напрямую
      - `update()` - теперь возвращает `Task` напрямую
      - `toggle()` - теперь возвращает `Task` напрямую
    - Удалены обертки `{ success, data }` из generic-типов вызовов `this.get/post/patch`

4. **Task Entity Store** (`src/client/entities/task.ts`):
    - В экшене `createTask` добавлена проверка: `if (!this.tasks) this.tasks = [];` перед `this.tasks.push(response)`
    - Это гарантирует безопасность при добавлении новой задачи

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

1. ✅ `tasksListResponseSchema` описывает только `z.array(taskSchema)`
2. ✅ `taskResponseSchema` описывает только `taskSchema`
3. ✅ В `taskSchema` поля `createdAt` и `updatedAt` - это `z.string()` (без `.datetime()`)
4. ✅ В `taskSchema` поля `parentId` и `order` помечены как `.nullable()`
5. ✅ `TaskRepository` методы ожидают чистые данные после интерцептора
6. ✅ Обертки `{ success, data }` удалены из generic-типов вызовов `this.get/post/patch`
7. ✅ `state.tasks` в сторе инициализируется как `[] as Task[]`
8. ✅ Все геттеры используют `(state.tasks || [])` перед вызовом методов массива
9. ✅ В экшене `createTask` добавлена проверка `if (!this.tasks) this.tasks = [];`
10. ✅ Бэкенд роуты возвращают только payload (без обертки `{ success, data }`)

#### Ожидаемый результат (проверен):

- Response схемы описывают только полезные данные (payload)
- Интерцептор извлекает данные из обертки `{ success, data }` автоматически
- Repository методы работают с чистыми данными после интерцептора
- Store безопасно обрабатывает undefined значение `tasks`
- Бэкенд возвращает только payload без обертки

---

## [x] Task #18: Фикс критической ошибки инициализации стора задач (v2.14.2)

**Цель**: Исправить критическую ошибку `TypeError: can't access property "filter", state.tasks is undefined` в сторе задач.

#### Измененные файлы:

**Frontend:**

1. **Task Entity Store** (`src/client/entities/task.ts`):
    - Добавлены проверки `(state.tasks || [])` во все геттеры:
      - `rootTasks` - теперь использует `(state.tasks || []).filter(...)`
      - `getChildTasks` - теперь использует `(state.tasks || []).filter(...)`
      - `getTaskById` - теперь использует `(state.tasks || []).find(...)`
      - `tasksTree` - теперь использует `(state.tasks || []).filter(...)`
    - Добавлены проверки в actions:
      - `updateTask` - использует локальную переменную `tasks = this.tasks || []`
      - `deleteTask` - использует `(this.tasks || []).filter(...)`
      - `toggleTask` - использует локальную переменную `tasks = this.tasks || []`
      - `updateTaskPositions` - использует локальную переменную `tasks = this.tasks || []`
      - `addTask` - добавляет проверку `if (!this.tasks) this.tasks = []`
      - `updateLocalTask` - использует локальную переменную `tasks = this.tasks || []`
      - `deleteLocalTask` - использует `(this.tasks || []).filter(...)`
      - `syncLayout` - использует локальную переменную `tasksArray = this.tasks || []`
      - `getAllSubtaskIds` - использует `(this.tasks || []).filter(...)`

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

1. ✅ State initialization уже корректна (функция возвращает объект с `tasks: []`)
2. ✅ Все геттеры используют `(state.tasks || [])` перед вызовом методов массива
3. ✅ Все actions используют проверки `(this.tasks || [])` или локальные переменные
4. ✅ TasksBoard.vue уже вызывает `fetchTasks()` в `onMounted`
5. ✅ TasksBoard.vue уже имеет `v-if="isLoading"` на контейнере сетки
6. ✅ TaskRepository.ts уже возвращает `response.data` с логированием

#### Ожидаемый результат (проверен):

- Ошибка `TypeError: can't access property "filter", state.tasks is undefined` устранена
- Все геттеры и actions защищены от undefined значения `tasks`
- Приложение не падает при инициализации стора задач
- TasksBoard корректно рендерится после загрузки данных

---

## [x] Task #17: UI Cleanup для TasksPage и TasksBoard (v2.14.1)

**Цель**: Удалить старые кнопки "Добавить список" / "Создать первый список" и добавить плавающую кнопку с иконкой Plus.

#### Измененные файлы:

**Frontend:**

1. **TasksBoard Widget** (`src/client/widgets/TasksBoard.vue`):
   - Удален заголовок с кнопкой "Добавить список"
   - Удалено пустое состояние с кнопкой "Создать первый список"
   - Добавлена плавающая кнопка с классом `.floating-create-btn`
   - Функция `addNewTask` переименована в `handleCreateList`
   - Позиция для новой карточки изменена на `{ x: 0, y: 0, w: 2, h: 10 }`
   - Удалены неиспользуемые scoped стили (header, empty state, buttons)
   - Импорт `Plus` из `lucide-vue-next` уже существовал

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

1. ✅ Кнопка "Добавить список" удалена из заголовка
2. ✅ Кнопка "Создать первый список" удалена из пустого состояния
3. ✅ Плавающая кнопка с классом `.floating-create-btn` добавлена
4. ✅ Иконка `Plus` импортирована из `lucide-vue-next`
5. ✅ Функция переименована в `handleCreateList`
6. ✅ Позиция для новой карточки: `{ x: 0, y: 0, w: 2, h: 10 }`
7. ✅ Глобальный класс `.floating-create-btn` используется (без локальных стилей)
8. ✅ Таблица `tasks.table.ts` имеет поля `position` (text) и `order` (int)
9. ✅ `tasks.service.ts` не передает поля `date` или `archivedAt` при INSERT

#### Ожидаемый результат (проверен):

- Плавающая кнопка с иконкой Plus отображается в правом нижнем углу
- При клике создается новая карточка задачи с позицией `{ x: 0, y: 0, w: 2, h: 10 }`
- Старые кнопки "Добавить список" и "Создать первый список" удалены
- Глобальный стиль `.floating-create-btn` применяется корректно

---

## [x] Task #15: Внедрение Floating Action Button (FAB) на ключевые страницы (v2.13.0)

**Цель**: Добавить плавающую кнопку «Добавить» в правый нижний угол на страницы Сотрудники, Чёрный список, База номеров и Инвентарь.

#### Измененные файлы:

**Frontend:**

1. **EmployeesPage.vue** (`src/client/pages/EmployeesPage.vue`):
    - Добавлен импорт `Plus` из `lucide-vue-next`
    - Добавлена FAB кнопка с классами: `fixed bottom-8 right-8 z-50 w-14 h-14 rounded-full bg-blue-600 hover:bg-blue-700 text-white shadow-2xl`
    - При клике вызывает `handleCreateEmployee()`, открывая модалку создания сотрудника

2. **BlacklistPage.vue** (`src/client/pages/BlacklistPage.vue`):
    - Добавлен импорт `Plus` из `lucide-vue-next`
    - Добавлена FAB кнопка с теми же стилями
    - Кнопка отображается только для пользователей с правами редактирования (`v-if="canEdit"`)
    - При клике вызывает `handleCreate()`, открывая модалку создания записи

3. **InventoryPage.vue** (`src/client/pages/InventoryPage.vue`):
    - Добавлен импорт `Plus` из `lucide-vue-next`
    - Добавлена новая функция `handleCreateItem()`, которая вызывает `openModal(null)`
    - Добавлена FAB кнопка с теми же стилями
    - При клике вызывает `handleCreateItem()`, открывая модалку создания инвентаря

4. **RoomsPage.vue** (`src/client/pages/RoomsPage.vue`):
    - Добавлен импорт `Plus` из `lucide-vue-next` (вместе с существующими `Trash2`, `X`)
    - Добавлена FAB кнопка с теми же стилями
    - Кнопка отображается только для пользователей с ролью `ADMIN` (`v-if="userStore.role === 'ADMIN'"`)
    - При клике вызывает `addNewRow()`, добавляя новую строку в таблицу

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

1. ✅ FAB кнопка добавлена на страницу Сотрудники (EmployeesPage.vue)
2. ✅ FAB кнопка добавлена на страницу Чёрный список (BlacklistPage.vue)
3. ✅ FAB кнопка добавлена на страницу Инвентарь (InventoryPage.vue)
4. ✅ FAB кнопка добавлена на страницу База номеров (RoomsPage.vue)
5. ✅ Все кнопки используют единый стиль: `fixed bottom-8 right-8 z-50 w-14 h-14 rounded-full shadow-2xl`
6. ✅ Все кнопки используют иконку `Plus` из `lucide-vue-next`
7. ✅ Кнопки имеют hover эффекты: `hover:bg-blue-700 hover:scale-110`
8. ✅ Кнопки имеют фокус состояние: `focus:ring-4 focus:ring-blue-300`
9. ✅ FAB на BlacklistPage отображается только для пользователей с правами редактирования
10. ✅ FAB на RoomsPage отображается только для пользователей с ролью ADMIN
11. ✅ Все кнопки имеют атрибут `title` для доступности

#### Ожидаемый результат (проверен):

- На странице Сотрудники в правом нижнем углу отображается круглая синяя кнопка с плюсом
- На странице Чёрный список в правом нижнем углу отображается круглая синяя кнопка с плюсом (для ADMIN/MANAGER)
- На странице Инвентарь в правом нижнем углу отображается круглая синяя кнопка с плюсом
- На странице База номеров в правом нижнем углу отображается круглая синяя кнопка с плюсом (для ADMIN)
- При клике на кнопку открывается соответствующая модалка создания новой записи
- Кнопки имеют hover эффект увеличения и изменения цвета
- На мобильных устройствах кнопки не перекрывают важные элементы интерфейса

---

## [x] Task #3: Store Sorting & Reset Guard (v2.12.1)

**Цель**: Обновить хранилище заметок с сортировкой по дате создания и сбросом фильтров при перезагрузке страницы.

#### Измененные файлы:

**Frontend:**

1. **Note Store** (`src/client/entities/note.ts`):
    - Обновлен геттер [`filteredNotes`](src/client/entities/note.ts:48):
      - Добавлена сортировка по `createdAt` по убыванию: `.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())`
      - Заметки теперь отображаются от самых новых к самым старым
    - Обновлен экшен [`setStatusFilter`](src/client/entities/note.ts:496):
      - Добавлен автоматический сброс `authorFilter` при изменении статуса
      - Добавлен автоматический сброс `includeReminders` при изменении статуса
      - Добавлен автоматический сброс `visibilityFilter` при изменении статуса
    - Обновлена конфигурация [`persist`](src/client/entities/note.ts:560):
      - Изменен с `persist: true` на `persist: { paths: ['viewMode'] }`
      - Теперь сохраняется только `viewMode` между сессиями
      - Фильтры (`currentFilter`, `authorFilter`, `includeReminders`, `visibilityFilter`) сбрасываются при перезагрузке страницы (F5)

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

1. ✅ Геттер `filteredNotes` сортирует заметки по `createdAt` по убыванию
2. ✅ Экшен `setStatusFilter` сбрасывает `authorFilter` при изменении статуса
3. ✅ Экшен `setStatusFilter` сбрасывает `includeReminders` при изменении статуса
4. ✅ Экшен `setStatusFilter` сбрасывает `visibilityFilter` при изменении статуса
5. ✅ Конфигурация `persist` сохраняет только `viewMode`
6. ✅ Фильтры сбрасываются при перезагрузке страницы (F5)

#### Ожидаемый результат (проверен):

- Заметки в списке отображаются от самых новых к самым старым (по `createdAt`)
- При переключении вкладок статусов (Активно/Выполнено/Отменено) автоматически сбрасываются фильтры по автору, напоминаниям и видимости
- При перезагрузке страницы (F5) `viewMode` сохраняется (grid/list), а все фильтры сбрасываются в значения по умолчанию

---

## [x] Task #14: Создание таблицы blacklist_pending для модерации записей (v2.12.0)

**Цель**: Создать отдельную таблицу для модерации записей черного списка перед попаданием в основной список.

#### Измененные файлы:

**Backend:**

1. **Blacklist Table Schema** (`src/server/features/blacklist/db/blacklist.table.ts`):
    - Создана новая таблица `blacklistPending` для модерации записей
    - Поля таблицы:
      - `id` (varchar(36), PK) - уникальный идентификатор
      - `name` (varchar(100), NOT NULL) - ФИО гостя
      - `phone` (varchar(20), NOT NULL) - Телефон
      - `bookingEngineCheck` (varchar(20), enum: ['YES', 'NO'], NOT NULL, default: 'NO') - Проверка в Booking Engine
      - `comment` (text, nullable) - Комментарий
      - `status` (varchar(20), NOT NULL, default: 'pending') - Статус: pending, approved, rejected
      - `createdBy` (varchar(36), nullable) - ID пользователя, создавшего запись
      - `reviewedBy` (varchar(36), nullable) - ID пользователя, одобрившего/отклонившего запись
      - `reviewedAt` (timestamp, nullable) - Время рассмотрения
      - `rejectionReason` (text, nullable) - Причина отклонения
      - `createdAt` (timestamp, NOT NULL, default: now()) - Дата создания
      - `updatedAt` (timestamp, NOT NULL, default: now(), onUpdate: CURRENT_TIMESTAMP) - Дата обновления
    - Таблица автоматически экспортируется через `src/server/shared/db/schema.ts`

2. **Database Migration** (`drizzle/0013_fast_sheva_callister.sql`):
    - Сгенерирована миграция Drizzle для создания таблицы `blacklistPending`
    - Дополнительно добавлены поля в существующие таблицы:
      - `blacklist.is_approved` (boolean, default: false)
      - `blacklist.created_by` (varchar(36), nullable)
      - `notes.last_comment_author_id` (varchar(36), nullable)

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

1. ✅ Таблица `blacklistPending` создана в схеме Drizzle
2. ✅ Все необходимые поля добавлены в таблицу
3. ✅ Таблица экспортируется через `src/server/shared/db/schema.ts`
4. ✅ Миграция сгенерирована через `npx drizzle-kit generate`
5. ✅ Таблица создана в базе данных MariaDB
6. ✅ Дополнительные поля добавлены в таблицы `blacklist` и `notes`

#### Ожидаемый результат (проверен):

- Таблица `blacklist_pending` существует в базе данных
- Все 12 полей таблицы созданы с правильными типами данных
- Поля `is_approved` и `created_by` добавлены в таблицу `blacklist`
- Поле `last_comment_author_id` добавлено в таблицу `notes`
- Миграция может быть применена в продакшен через стандартный процесс Drizzle

---

## [x] Task #13: Fix Blacklist Routes and Socket Lifecycle (v2.11.0)

**Цель**: Регистрация роутов на бэкенде и защита подписок на фронтенде.

#### Измененные файлы:

**Backend:**

1. **Reference Books Index** (`src/server/features/reference_books/index.ts`):
    - Добавлен комментарий, что blacklist routes регистрируются отдельно через `blacklistPlugin` в `app.ts`
    - Удалена дублирующая регистрация `blacklistRoutes` (исправление ошибки `FST_ERR_DUPLICATED_ROUTE`)
    - Blacklist routes уже регистрируются через `src/server/features/blacklist/index.ts` с префиксом `/api`

**Frontend:**

2. **BlacklistPage** (`src/client/pages/BlacklistPage.vue`):
    - В хуке [`onMounted`](src/client/pages/BlacklistPage.vue:137) добавлена проверка [`socketService.isSocketConnected()`](src/client/shared/api/SocketService.ts:104) перед подпиской на сокет
    - Подписки на события `blacklist:*` теперь выполняются только если сокет подключен
    - Это предотвращает ошибки при попытке подписаться до инициализации сокета

3. **Blacklist Store** (`src/client/entities/blacklist.ts`):
    - Проверено, что во всех методах [`error.value = null`](src/client/entities/blacklist.ts:25) устанавливается ПЕРЕД началом запроса
    - Методы: [`fetchAll()`](src/client/entities/blacklist.ts:23), [`fetchPending()`](src/client/entities/blacklist.ts:45), [`create()`](src/client/entities/blacklist.ts:63), [`update()`](src/client/entities/blacklist.ts:83), [`deleteEntry()`](src/client/entities/blacklist.ts:103), [`approveAll()`](src/client/entities/blacklist.ts:122), [`rejectAll()`](src/client/entities/blacklist.ts:143)
    - Это гарантирует очистку старых сообщений об ошибке при повторных запросах

4. **Blacklist Repository** (`src/client/shared/api/repositories/BlacklistRepository.ts`):
    - Исправлен `basePath` с `/reference_books/blacklist` на `/blacklist`
    - Теперь API запросы идут по правильному пути `/api/blacklist/...` (соответствует регистрации на бэкенде)

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

1. ✅ Blacklist routes регистрируются через `blacklistPlugin` в `app.ts` (строка152)
2. ✅ Дублирующая регистрация из `reference_books/index.ts` удалена
3. ✅ В `BlacklistPage.vue` добавлена проверка `socketService.isSocketConnected()` перед подпиской
4. ✅ Подписки на сокет выполняются только если сокет подключен
5. ✅ Во всех методах store `error.value` сбрасывается в `null` перед началом запроса
6. ✅ Сервер запускается без ошибки `FST_ERR_DUPLICATED_ROUTE`
7. ✅ `basePath` в `BlacklistRepository` изменен на `/blacklist`

#### Ожидаемый результат (проверен):

- Роуты черного списка зарегистрированы на бэкенде по пути `/api/blacklist/...` через `blacklistPlugin`
- Нет дублирующей регистрации роутов
- API запросы с фронтенда идут по правильному пути `/api/blacklist/...`
- Подписки на сокет в `BlacklistPage.vue` защищены от попытки подписки до инициализации сокета
- Старые сообщения об ошибке очищаются при повторных запросах к API
- Страница черного списка работает корректно без ошибок сокета и API
- Сервер запускается без ошибок

---

## [x] Task #12: Исправление путей API и устранение критической ошибки в жизненном цикле страницы Blacklist (v2.10.0)

**Цель**: Исправить пути API с `/api/blacklist/...` на `/api/reference_books/blacklist/...` согласно карте проекта и устранить критическую ошибку в подписках на сокеты в BlacklistPage.vue.

#### Измененные файлы:

**Frontend:**

1. **BlacklistRepository.ts** (`src/client/shared/api/repositories/BlacklistRepository.ts`):
    - Изменен `basePath` с `/blacklist` на `/reference_books/blacklist`
    - Теперь все API запросы идут по правильному пути `/api/reference_books/blacklist/...`

2. **BlacklistPage.vue** (`src/client/pages/BlacklistPage.vue`):
    - Исправлены подписки на сокеты: заменены методы `socketService.on()` и `socketService.off()` на `socketService.subscribe()`
    - Добавлены переменные `unsubscribeCreated`, `unsubscribeUpdated`, `unsubscribeDeleted` для хранения функций отписки
    - В `onMounted` теперь используется `socketService.subscribe<BlacklistEntry>('blacklist:created', handleBlacklistCreated)`
    - В `onUnmounted` теперь вызываются функции отписки: `unsubscribeCreated?.()`, `unsubscribeUpdated?.()`, `unsubscribeDeleted?.()`
    - Это устраняет утечки памяти и ошибки при переходах между страницами

3. **blacklist.ts** (`src/client/entities/blacklist.ts`):
    - Экспортированы методы `smartMergeEntry` и `removeEntry` из store
    - Теперь эти методы доступны для использования в компонентах (например, для socket-обновлений)

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

1. ✅ Путь basePath в BlacklistRepository изменен на `/reference_books/blacklist`
2. ✅ Все API запросы теперь идут по правильному пути `/api/reference_books/blacklist/...`
3. ✅ Подписки на сокеты в BlacklistPage.vue используют `socketService.subscribe()` вместо несуществующего `on()`
4. ✅ Отписки от сокетов в `onUnmounted` используют функции, возвращаемые `subscribe()`
5. ✅ Методы `smartMergeEntry` и `removeEntry` экспортированы из blacklist store
6. ✅ Утечки памяти при переходах между страницами устранены

#### Ожидаемый результат (проверен):

- API запросы для черного списка работают по правильному пути `/api/reference_books/blacklist/...`
- Socket подписки работают корректно через `SocketService.subscribe()`
- Нет утечек памяти при переходах между страницами
- Методы Smart Merge доступны для socket-обновлений

---

## [x] Task #11: Удаление vee-validate из BlacklistModal и рефакторинг на useBlacklistForm (v2.9.0)

**Цель**: Удалить библиотеку vee-validate из BlacklistModal и внедрить кастомный хук useBlacklistForm по образцу useNoteForm.

#### Измененные файлы:

**Frontend:**

1. **useBlacklistForm.ts** (`src/client/entities/blacklist/model/useBlacklistForm.ts`):
    - Создан новый хук для управления формой черного списка
    - Функции:
      - `name`, `phone`, `bookingEngineCheck`, `comment`: refs для полей формы
      - `errors`: ref для ошибок валидации
      - `initForm()`: инициализация формы на основе mode и entry
      - `validate()`: валидация через Zod (blacklistCreateSchema)
      - `getCreateData()`: подготовка данных для создания
      - `getUpdateData()`: подготовка данных для обновления
    - Валидация использует Zod схему из `@shared/contracts/blacklist`
    - Автоматическая очистка ошибок при вводе данных

2. **BlacklistModal.vue** (`src/client/features/BlacklistModal.vue`):
    - Удалены импорты из `vee-validate` и `@vee-validate/zod`
    - Удалены функции `useForm`, `useField`, `toTypedSchema`
    - Внедрен `useBlacklistForm` для управления состоянием формы
    - Обновлен интерфейс props: добавлен `isOpen: boolean`
    - Заменен компонент `Modal` на самостоятельную реализацию с `Teleport` (по образцу NoteModal)
    - Валидация через `validate()` из `useBlacklistForm`
    - Отправка данных через `blacklistRepository` (create/update)
    - Используются методы `getCreateData()` и `getUpdateData()` для подготовки DTO

3. **BlacklistPage.vue** (`src/client/pages/BlacklistPage.vue`):
    - Обновлен вызов `BlacklistModal`:
      - Добавлен prop `:is-open="showModal"`
      - Удален `v-if` (теперь управляется через `:is-open`)

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

1. ✅ Импорты из `vee-validate` удалены из BlacklistModal.vue
2. ✅ Функции `useForm`, `useField`, `toTypedSchema` удалены
3. ✅ Создан `useBlacklistForm.ts` по образцу `useNoteForm.ts`
4. ✅ Валидация реализована через Zod схему из `@shared/contracts/blacklist`
5. ✅ Функция `validate()` использует `blacklistCreateSchema.safeParse()`
6. ✅ Форма управляется через refs из `useBlacklistForm`
7. ✅ Отправка данных происходит через `blacklistRepository`
8. ✅ Данные проходят через `getCreateData()` и `getUpdateData()`
9. ✅ Компонент `Modal` заменен на самостоятельную реализацию с `Teleport`
10. ✅ Поиск по `src/client/` показал отсутствие других использований `vee-validate`

#### Ожидаемый результат (проверен):

- BlacklistModal не использует vee-validate
- Форма управляется через кастомный хук `useBlacklistForm`
- Валидация работает через Zod схемы
- Отправка данных происходит через Repository
- Библиотека vee-validate больше нигде не используется в проекте

---

## [x] Task #10.1: Настройка роутинга и навигации для Blacklist (v2.8.1)

**Цель**: Сделать страницу доступной по адресу `/dashboard/blacklist` и добавить ссылку в сайдбар.

#### Измененные файлы:

**Frontend:**

1. **Main Router** (`src/client/app/main.ts`):
    - Обновлен маршрут для `/dashboard/blacklist`:
      - Заменен компонент с `PlaceholderPage.vue` на `BlacklistPage.vue`
      - Удалены props `category: 'Черный список'` (больше не нужны)

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

1. ✅ Маршрут `/dashboard/blacklist` указывает на `BlacklistPage.vue`
2. ✅ Пункт меню "Черный список" уже настроен в `menuConfig.ts`
3. ✅ Импорты в `BlacklistPage.vue` корректны (`useBlacklistStore` из `@/entities/blacklist`)
4. ✅ Права доступа настроены: `['ADMIN', 'MANAGER', 'MAID']`

#### Ожидаемый результат (проверен):

- Страница черного списка доступна по адресу `/dashboard/blacklist`
- Пункт меню "Черный список" отображается в сайдбаре для ролей ADMIN, MANAGER, MAID
- MAID имеет read-only режим для черного списка

---

## [x] Task #10: Разработка раздела "Черный список" с системой согласования (v2.8.0)

**Цель**: Разработать раздел "Черный список" гостей с системой согласования, аналогичной графику смен.

#### Измененные файлы:

**Backend:**

1. **Blacklist Table Schema** (`src/server/features/reference_books/db/blacklist.table.ts`):
    - Добавлено поле `isApproved` (boolean, default: false) для отслеживания статуса согласования
    - Добавлено поле `createdBy` (varchar, nullable) для отслеживания автора записи
    - Добавлен импорт `boolean` из drizzle-orm/mysql-core

2. **Blacklist Service** (`src/server/features/reference_books/blacklist.service.ts`):
    - Создан полный сервисный слой для blacklist
    - Функции:
      - `createBlacklistEntry()`: Создание записи с учетом роли (MANAGER → черновик, ADMIN → утверждено)
      - `updateBlacklistEntry()`: Обновление записи
      - `deleteBlacklistEntry()`: Удаление записи (создает черновик для удаления при необходимости)
      - `getAllBlacklistEntries()`: Получение всех записей с фильтрацией по статусу
      - `getBlacklistEntriesByStatus()`: Получение записей по статусу (pending/approved)
      - `getBlacklistEntryById()`: Получение записи по ID
      - `approveAllBlacklistEntries()`: Массовое согласование (ADMIN only)
      - `rejectAllBlacklistEntries()`: Массовое отклонение (ADMIN only)
    - Валидация обязательных полей: name, phone, bookingEngineCheck
    - Уведомления при изменениях

3. **Approval Service** (`src/server/features/approval/approval.service.ts`):
    - Обновлена функция `createApprovalRequest()` для поддержки entityType 'blacklist'
    - Обновлена функция `approveApprovalRequest()` для обработки 'blacklist' (делегирует в blacklist.service)
    - Обновлена функция `rejectApprovalRequest()` для обработки 'blacklist' уведомлений

4. **Approval Requests Table** (`src/server/features/approval/db/approval_requests.table.ts`):
    - Обновлен комментарий: добавлен 'blacklist' в enum entityType

5. **Approval Routes** (`src/server/features/approval/approval.routes.ts`):
    - Обновлены все схемы для поддержки entityType 'blacklist'

6. **Blacklist Routes** (`src/server/features/reference_books/blacklist.routes.ts`):
    - Создан полный REST API с RBAC:
      - `POST /api/blacklist` - Создание записи (ADMIN/MANAGER)
      - `GET /api/blacklist` - Получение всех записей (с фильтром по статусу)
      - `GET /api/blacklist/pending` - Получение несогласованных записей
      - `GET /api/blacklist/:id` - Получение записи по ID
      - `PUT /api/blacklist/:id` - Обновление записи (ADMIN/MANAGER)
      - `DELETE /api/blacklist/:id` - Удаление записи (ADMIN/MANAGER)
      - `POST /api/blacklist/approve` - Массовое согласование (ADMIN only)
      - `POST /api/blacklist/reject` - Массовое отклонение (ADMIN only)

7. **Blacklist Events** (`src/server/features/reference_books/blacklist.events.ts`):
    - Создан модуль для событий blacklist
    - Функции:
      - `emitBlacklistEvent()`: Эмиссия события через Socket.io
      - `setupBlacklistEventListeners()`: Настройка слушателей событий

8. **Reference Books Plugin** (`src/server/features/reference_books/index.ts`):
    - Добавлен импорт `setupBlacklistEventListeners`
    - Добавлен вызов `setupBlacklistEventListeners()` при регистрации плагина

**Frontend:**

9. **Blacklist Contract** (`src/shared/contracts/blacklist.ts`):
    - Создан контракт с типами и Zod схемами:
      - Интерфейсы: `BlacklistEntry`, `BlacklistCreateRequest`, `BlacklistUpdateRequest`
      - Схемы: `blacklistCreateSchema`, `blacklistUpdateSchema`, `blacklistEntrySchema`

10. **Blacklist Repository** (`src/client/shared/api/repositories/BlacklistRepository.ts`):
    - Создан репозиторий с кэшированием (30 секунд)
    - Методы:
      - `getAll()`: Получение всех записей (с фильтром по статусу)
      - `getById()`: Получение записи по ID
      - `create()`: Создание записи
      - `update()`: Обновление записи
      - `delete()`: Удаление записи
      - `getPending()`: Получение несогласованных записей
      - `approveAll()`: Массовое согласование
      - `rejectAll()`: Массовое отклонение

11. **Blacklist Store** (`src/client/entities/blacklist.ts`):
    - Создан Pinia store для blacklist
    - Состояние: `entries`, `pendingEntries`, `loading`, `error`
    - Computed: `approvedEntries`, `hasPendingEntries`
    - Actions:
      - `fetchAll()`: Загрузка всех записей
      - `fetchPending()`: Загрузка несогласованных записей
      - `create()`: Создание записи
      - `update()`: Обновление записи
      - `deleteEntry()`: Удаление записи
      - `approveAll()`: Массовое согласование
      - `rejectAll()`: Массовое отклонение
    - Smart Merge: обновление состояния без сброса UI

12. **Blacklist Page** (`src/client/pages/BlacklistPage.vue`):
    - Создана страница черного списка
    - Функции:
      - Таблица с записями черного списка
      - Фильтры по статусу (все, на согласовании, утвержденные)
      - Индикатор статуса (зеленый/красный для bookingEngineCheck)
      - Read-only режим для MAID
      - Кнопки управления для ADMIN/MANAGER
      - Модальные окна для подтверждения действий
    - Socket интеграция:
      - Подписка на события `blacklist:*`
      - Smart Merge при получении обновлений
      - Правильный cleanup в `onUnmounted`

13. **Blacklist Modal** (`src/client/features/BlacklistModal.vue`):
    - Создано модальное окно для создания/редактирования записей
    - Форма с полями:
      - `name` (обязательно)
      - `phone` (обязательно)
      - `bookingEngineCheck` (обязательно, select: Да/Нет)
      - `comment` (опционально)
    - Валидация через VeeValidate + Zod
    - Логика отправки на согласование для MANAGER
    - Логика автоматического согласования для ADMIN

14. **Repositories Index** (`src/client/shared/api/repositories/index.ts`):
    - Добавлен экспорт `blacklistRepository`
    - Добавлены типы: `BlacklistEntry`, `BlacklistCreateRequest`, `BlacklistUpdateRequest`

15. **Entities Index** (`src/client/entities/index.ts`):
    - Добавлен экспорт `blacklist` store

16. **Features Index** (`src/client/features/index.ts`):
    - Добавлен экспорт `BlacklistModal`

17. **Pages Index** (`src/client/pages/index.ts`):
    - Добавлен экспорт `BlacklistPage`

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

1. ✅ Поле `isApproved` добавлено в таблицу blacklist
2. ✅ Поле `createdBy` добавлено в таблицу blacklist
3. ✅ `blacklist.service.ts` создан с полной бизнес-логикой
4. ✅ CRUD операции реализованы с учетом статуса согласования
5. ✅ Логика для менеджеров: создание черновиков (isApproved: false)
6. ✅ Логика для админов: автоматическое согласование (isApproved: true)
7. ✅ Валидация полей реализована (name, phone, bookingEngineCheck обязательны)
8. ✅ Функция получения записей по статусу реализована
9. ✅ `approval.service.ts` обновлен для поддержки 'blacklist'
10. ✅ `approval_requests.table.ts` обновлен с 'blacklist' в enum
11. ✅ `approval.routes.ts` обновлен с поддержкой 'blacklist'
12. ✅ `blacklist.routes.ts` создан с RBAC проверками
13. ✅ Эндпоинт `/api/blacklist/pending` создан
14. ✅ Эндпоинт `/api/blacklist/approve` создан (ADMIN only)
15. ✅ Эндпоинт `/api/blacklist/reject` создан (ADMIN only)
16. ✅ События `blacklist:*` созданы
17. ✅ События зарегистрированы в плагине reference_books
18. ✅ `blacklist.ts` контракт создан с Zod схемами
19. ✅ `BlacklistRepository.ts` создан с кэшированием
20. ✅ `blacklist.ts` store создан с Smart Merge
21. ✅ `BlacklistPage.vue` создан с таблицей и фильтрами
22. ✅ Индикатор статуса (зеленый/красный) реализован
23. ✅ Read-only режим для MAID реализован
24. ✅ Кнопки управления для ADMIN/MANAGER добавлены
25. ✅ `BlacklistModal.vue` создан с формой и валидацией
26. ✅ Socket интеграция реализована с cleanup

#### Ожидаемый результат (проверен):

- Пользователи могут просматривать черный список гостей
- ADMIN и MANAGER могут создавать и редактировать записи
- MAID только читает записи (read-only режим)
- MANAGER создает черновики (isApproved: false), которые отправляются на согласование
- ADMIN автоматически утверждает записи (isApproved: true)
- ADMIN может массово согласовывать или отклонять записи
- Цветовая индикация: красный для "Нет", зеленый для "Да" в поле bookingEngineCheck
- Фильтры по статусу работают корректно
- Socket события обновляют UI в реальном времени
- Smart Merge сохраняет состояние UI (скролл, фильтры) при обновлениях

---

## [x] Task #9.7: КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ СИСТЕМЫ УВЕДОМЛЕНИЙ И ОШИБОК КОММЕНТАРИЕВ (v2.7.3)

**Цель**: Исправить критические ошибки в системе уведомлений и логике комментариев для корректной работы индикаторов непрочитанных комментариев и упоминаний.

#### Измененные файлы:

**Backend:**

1. **Notes Service** (`src/server/features/notes/notes.service.ts`):
    - В функции `createNoteComment()`:
      - Добавлено преобразование Date объектов в ISO строки перед возвратом
      - Возвращаемый объект теперь соответствует контракту `NoteComment` (ISO строки для дат)

2. **Notes Repository** (`src/server/features/notes/db/notes.repository.ts`):
    - В функции `markCommentsAsViewed()`:
      - Обновлен SELECT запрос для явного указания полей `column`, `positionY`, `height`
      - Используется `onDuplicateKeyUpdate` для обновления `lastCommentViewedAt`

**Frontend:**

3. **Note Store** (`src/client/entities/note.ts`):
    - В функции `createComment()` (уже реализовано корректно):
      - Находит заметку в `this.notes` по `noteId`
      - Устанавливает `note.lastCommentAt = response.createdAt`
      - Обновляет `note.layout.lastCommentViewedAt = response.createdAt`
      - Это гарантирует немедленное исчезновение красных индикаторов у автора сообщения

4. **NoteCard.vue** (`src/client/features/NoteCard.vue`):
    - В вычисляемом свойстве `hasUnreadComments`:
      - Упрощена логика: прямое сравнение `props.note.lastCommentAt` и `props.note.layout?.lastCommentViewedAt` как дат
    - В вычисляемом свойстве `isCurrentUserMentioned`:
      - Временно реализована проверка упоминания ТОЛЬКО в `props.note.content`
      - Логика поиска в комментариях внутри карточки НЕ РАБОТАЕТ, так как `noteStore.comments` пуст до открытия модалки
      - Будет заменена на флаг `hasMention` от бэкенда в будущем

5. **NoteModalCommentForm.vue** (`src/client/features/notes/ui/NoteModal/NoteModalCommentForm.vue`):
    - Метод `clearForm()` уже реализован корректно:
      - `commentText.value = ''`
      - `mentionEditorRef.value.setValue('')`
    - Родительский `NoteModal.vue` правильно вызывает `clearForm()` после успешной отправки комментария

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

1. ✅ В `notes.service.ts` `createNoteComment()` возвращает объект с ISO строками дат
2. ✅ В `notes.repository.ts` `markCommentsAsViewed()` использует явный SELECT полей и `onDuplicateKeyUpdate`
3. ✅ В `note.ts` `createComment()` обновляет `lastCommentAt` и `lastCommentViewedAt`
4. ✅ В `NoteCard.vue` `hasUnreadComments` упрощена для прямого сравнения дат
5. ✅ В `NoteCard.vue` `isCurrentUserMentioned` проверяет только содержимое заметки
6. ✅ В `NoteModalCommentForm.vue` `clearForm()` очищает форму корректно

#### Ожидаемый результат (проверен):

- Индикатор непрочитанных комментариев (иконка MessageSquare красного цвета) исчезает сразу после ответа пользователя
- Индикатор упоминания (@ иконка красного цвета) работает корректно для упоминаний в содержании заметки
- Форма комментария очищается после успешной отправки
- Бэкенд возвращает корректные ISO строки дат для комментариев
- Метод `markCommentsAsViewed()` работает корректно для новых и существующих записей

---

## [x] Task #9.5: CRITICAL BUGFIX: Notes API 500 Error (v2.7.1)

**Цель**: Исправить ошибку `TypeError: Cannot convert undefined or null to object` в Drizzle Select, которая вызывала 500 ошибку в Notes API.

#### Измененные файлы:

**Backend:**

1. **Notes Repository** (`src/server/features/notes/db/notes.repository.ts`):
    - В функции `findWithLayout()`:
      - Удалены поля `reminderAt: notes.reminderAt` и `deferUntil: notes.deferUntil` из select (эти поля были удалены из таблицы `notes`)
      - Добавлено поле `reminderAt: noteLayouts.reminderAt` в секцию Layout fields (из таблицы `note_layouts`)
    - В функции `findOneWithLayout()`:
      - Удалены поля `reminderAt: notes.reminderAt` и `deferUntil: notes.deferUntil` из select
      - Добавлено поле `reminderAt: noteLayouts.reminderAt` в секцию Layout fields

2. **Export Service** (`src/server/features/notes/lib/export.service.ts`):
    - Обновлен запрос к БД для экспорта заметок:
      - Добавлен `leftJoin` с таблицей `note_layouts` для получения персональных напоминаний
      - Удалены поля `reminderAt` и `deferUntil` из таблицы `notes`
      - Добавлено поле `reminderAt` из таблицы `note_layouts`
      - Удалена колонка `deferUntil` из экспорта CSV
    - Обновлена функция `getStatusLabel()`:
      - Удален статус 'deferred' из маппинга (статус больше не существует)

3. **Layout Service** (`src/server/features/notes/lib/layout.service.ts`):
    - Обновлен тип параметра `statusFilter` в функции `resolveNoteCollisions()`:
      - Удален статус 'deferred' из типа (был: `'active' | 'deferred' | 'done' | 'cancelled' | null'`)
      - Теперь: `'active' | 'done' | 'cancelled' | null`

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

1. ✅ Поля `reminderAt` и `deferUntil` удалены из select в `findWithLayout()`
2. ✅ Поля `reminderAt` и `deferUntil` удалены из select в `findOneWithLayout()`
3. ✅ Поле `reminderAt` добавлено из таблицы `note_layouts` в обе функции
4. ✅ Экспорт заметок обновлен для использования `note_layouts.reminderAt`
5. ✅ Колонка `deferUntil` удалена из экспорта CSV
6. ✅ Статус 'deferred' удален из `getStatusLabel()`
7. ✅ Статус 'deferred' удален из типа `statusFilter` в `resolveNoteCollisions()`
8. ✅ Сервер работает без 500-х ошибок
9. ✅ API для заметок возвращает данные корректно

#### Ожидаемый результат (проверен):

- Запрос `GET /api/notes` возвращает массив заметок (пустой или с данными), а не падает в 500
- Запрос `GET /api/notes/:id` возвращает заметку с персональным layout, а не падает в 500
- Экспорт заметок в CSV работает корректно с персональными напоминаниями
- Функция `resolveNoteCollisions()` работает без ошибок TypeScript
- Сервер работает стабильно без критических ошибок

---

## [x] Task #9.4: Напоминания для заметок (Notes Reminders) (v2.7.0)

**Цель**: Реализовать систему напоминаний для заметок с автоматическим уведомлением автору при наступлении времени.

#### Измененные файлы:

**Frontend:**

1. **NoteCard.vue** (`src/client/features/NoteCard.vue`):
    - Добавлены чекбоксы для приоритета и публичности:
      - 🔥 для высокого приоритета (Flame icon)
      - 🌐 для публичных заметок (Globe icon)
    - Добавлен UI напоминания:
      - Колокольчик (Bell icon) с выбором даты/времени
      - Если `reminderAt` установлен:
        - Колокольчик окрашивается в основной цвет темы (#3B82F6)
        - Рядом выводится время (например, "14:30, 05.02")
      - Карточка получает стиль `opacity-60`, пока время не наступило
    - Добавлена иконка AlarmClock в заголовок карточки на общем полотне (если установлен `reminderAt`)
    - Обновлены импорты: `AlarmClock`, `Bell`, `Flame`, `Globe` из `lucide-vue-next`

2. **NoteModal.vue** (`src/client/features/NoteModal.vue`):
    - Обновлены чекбоксы для приоритета и публичности:
      - Заменены кнопки выбора на чекбоксы
      - Чекбокс "Высокий приоритет" (🔥) → `priority = 'high'`
      - Чекбокс "Видна всем" (🌐) → `isPublic = true`
    - Добавлена функция `handlePriorityChange` для обработки изменений чекбокса приоритета
    - Добавлено computed свойство `isHighPriority` для управления чекбоксом
    - Обновлен UI напоминания:
      - Кнопка-колокольчик с выбором даты/времени
      - Отображение времени напоминания (формат: "HH:MM, DD.MM")
      - Колокольчик окрашивается в основной цвет темы при установке
    - Добавлены стили для чекбоксов и UI напоминания

**Backend:**

3. **Notes Table Schema** (`src/server/features/notes/db/notes.table.ts`):
    - Поле `reminderAt` уже существовало (timestamp, nullable)
    - Добавлено поле `reminderProcessed` (boolean, default: false)
    - Поле `reminderProcessed` помечает напоминание как выполненное, чтобы избежать спама

4. **Notes Schema** (`src/server/features/notes/notes.schema.ts`):
    - Обновлена `createNoteSchema` с полем `reminderProcessed` (default: false)
    - Обновлена `updateNoteSchema` с полем `reminderProcessed` (optional)
    - Обновлен `noteResponseSchema` с полем `reminderProcessed`

5. **Notes Contract** (`src/shared/contracts/notes.ts`):
    - Добавлен интерфейс `NoteReminderEvent` с полями:
      - `noteId: string`
      - `title: string`
      - `authorId: string`
      - `reminderAt: string`

6. **Reminder Scheduler Service** (`src/server/features/notes/lib/reminder-scheduler.service.ts`):
    - Создан сервис для проверки напоминаний каждую минуту
    - Функция `checkAndTriggerReminders()`:
      - Находит заметки с наступившим `reminderAt`
      - Помечает только необработанные (`reminderProcessed: false`)
      - Эмитит событие `note:reminder`
      - Помечает напоминание как выполненное (`reminderProcessed: true`)
    - Уведомление отправляется ТОЛЬКО автору заметки (персонально)
    - Текст уведомления: "Сработало напоминание по заметке '{title}'"

7. **Notifier Service** (`src/server/features/notes/lib/notifier.service.ts`):
    - Добавлена функция `onNoteReminder(noteId, title, authorId)`:
      - Отправляет уведомление автору заметки
      - Использует `notify()` для персонального уведомления
      - Текст уведомления соответствует ТЗ

8. **Notes Events** (`src/server/features/notes/notes.events.ts`):
    - Добавлен импорт `NoteReminderEvent` из контракта
    - Добавлен импорт `onNoteReminder` из `notifier.service`
    - Добавлен обработчик события `note:reminder`:
      - Вызывает `onNoteReminder(noteId, title, authorId)` при получении события
      - Логирует срабатывание напоминания

9. **Cron Jobs** (`src/server/shared/cron/index.ts`):
    - Добавлен импорт `checkAndTriggerReminders` из `reminder-scheduler.service`
    - Добавлена функция `initializeReminderSchedulerCron()`:
      - Cron job выполняется каждую минуту (`* * * * * *`)
      - Вызывает `checkAndTriggerReminders()`
      - Часовой пояс: Europe/Moscow
    - Интегрирован в `initializeCronJobs()`

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

1. ✅ В `NoteCard.vue` чекбоксы для приоритета и публичности реализованы
2. ✅ В `NoteCard.vue` UI напоминания с колокольчиком и отображением времени реализован
3. ✅ В `NoteCard.vue` стиль `opacity-60` для карточек с ожидающим напоминанием реализован
4. ✅ В `NoteCard.vue` иконка AlarmClock в заголовке карточки на общем полотне добавлена
5. ✅ В `NoteModal.vue` чекбоксы для приоритета и публичности реализованы
6. ✅ В `NoteModal.vue` UI напоминания с колокольчиком и отображением времени реализован
7. ✅ В `NoteModal.vue` функция `handlePriorityChange` для обработки изменений чекбокса добавлена
8. ✅ В `NoteModal.vue` computed свойство `isHighPriority` для управления чекбоксом добавлено
9. ✅ В `NoteModal.vue` computed свойство `formattedReminderTime` для форматирования времени добавлено
10. ✅ Поле `reminderAt` уже существовало в схеме БД
11. ✅ Поле `reminderProcessed` добавлено в схему БД
12. ✅ Схемы Zod обновлены с полем `reminderProcessed`
13. ✅ Интерфейс `NoteReminderEvent` добавлен в контракт
14. ✅ Сервис `reminder-scheduler.service.ts` создан
15. ✅ Функция `checkAndTriggerReminders()` реализована
16. ✅ Функция `onNoteReminder()` добавлена в `notifier.service.ts`
17. ✅ Обработчик события `note:reminder` добавлен в `notes.events.ts`
18. ✅ Cron job для проверки напоминаний создан
19. ✅ Cron job интегрирован в `initializeCronJobs()`
20. ✅ Dev сервер работает без ошибок компиляции

#### Ожидаемый результат (проверен):

- Пользователь может установить напоминание для заметки через UI
- При наступлении времени напоминания автор получает уведомление
- Карточка заметки отображается с прозрачностью (opacity-60) до наступления времени
- Колокольчик в карточке окрашивается в основной цвет темы при наличии напоминания
- Время напоминания отображается рядом с колокольчиком
- На общем полотне карточки с установленным напоминанием имеют иконку AlarmClock в заголовке
- Напоминания проверяются каждую минуту автоматически
- После отправки уведомления напоминание помечается как выполненное, чтобы избежать спама
- Уведомления отправляются только автору заметки (персонально)

---

## [x] Task #16: Реализация Task Board (Упрощенная версия без архивации) (v2.14.0)

**Цель**: Реализовать доску задач с плитками, поддержкой подпунктов и drag-and-drop перемещения.

#### Измененные файлы:

**Backend:**

1. **Tasks Table Schema** (`src/server/features/tasks/db/tasks.table.ts`):
   - Удалены поля: `date`, `archivedAt`
   - Добавлены поля:
     - `position` (text, nullable) - JSON: {x, y, w, h} для корневых задач
     - `order` (int, nullable) - Порядок сортировки подпунктов

2. **Tasks Schema** (`src/server/features/tasks/tasks.schema.ts`):
   - Обновлены схемы Zod:
     - `createTaskSchema` - с полями text, isCompleted, parentId, position, order
     - `updateTaskSchema` - с теми же полями (опционально)
     - `updateTaskPositionsSchema` - для массового обновления позиций
     - `taskResponseSchema` - с position и order
   - Удалены поля date и archivedAt

3. **Tasks Service** (`src/server/features/tasks/tasks.service.ts`):
   - Реализованы методы:
     - `getTasks()` - получение всех задач
     - `getTaskById()` - получение задачи по ID
     - `createTask()` - создание задачи
     - `updateTask()` - обновление задачи
     - `deleteTask()` - рекурсивное удаление задачи с подпунктами
     - `updateTaskPositions()` - массовое обновление позиций для сетки
     - `toggleTask()` - переключение статуса выполнения
   - Добавлен хелпер `findAllSubtasks()` для рекурсивного поиска подпунктов
   - Добавлен хелпер `parsePosition()` для парсинга JSON position

4. **Tasks Routes** (`src/server/features/tasks/tasks.routes.ts`):
   - Реализованы REST API эндпоинты:
     - `GET /api/tasks` - получение всех задач
     - `GET /api/tasks/:id` - получение задачи по ID
     - `POST /api/tasks` - создание задачи
     - `PATCH /api/tasks/:id` - обновление задачи
     - `DELETE /api/tasks/:id` - удаление задачи
     - `PATCH /api/tasks/:id/toggle` - переключение статуса
     - `POST /api/tasks/positions` - массовое обновление позиций

**Frontend:**

5. **Tasks Contract** (`src/shared/contracts/tasks.contract.ts`):
   - Создан контракт с типами и Zod схемами:
     - `positionSchema` - {x, y, w, h}
     - `taskSchema` - полная схема задачи
     - `createTaskInputSchema`, `updateTaskInputSchema`
     - `updateTaskPositionsInputSchema`
     - Response схемы для списков и одиночных задач

6. **Task Repository** (`src/client/shared/api/repositories/TaskRepository.ts`):
   - Создан репозиторий с кэшированием (30 секунд)
   - Методы:
     - `getAll()` - получение всех задач
     - `getAllFresh()` - принудительное обновление без кэша
     - `getById()` - получение задачи по ID
     - `create()` - создание задачи
     - `update()` - обновление задачи
     - `deleteTask()` - удаление задачи
     - `toggle()` - переключение статуса
     - `updatePositions()` - массовое обновление позиций

7. **Repositories Index** (`src/client/shared/api/repositories/index.ts`):
   - Добавлен экспорт `taskRepository`
   - Добавлены типы: `Task`, `Position`, `CreateTaskInput`, `UpdateTaskInput`, `UpdateTaskPositionsInput`

8. **Task Entity Store** (`src/client/entities/task.ts`):
   - Создан Pinia store `useTaskStore`
   - Состояние: `tasks`, `loading`, `error`
   - Геттеры:
     - `rootTasks` - задачи без родителя (parentId === null)
     - `getChildTasks(parentId)` - подпункты для конкретной задачи
     - `getTaskById(id)` - задача по ID
     - `tasksTree` - вложенная структура задач
   - Экшены:
     - `fetchTasks()` - загрузка всех задач
     - `fetchTasksFresh()` - принудительная загрузка
     - `createTask()` - создание задачи
     - `updateTask()` - обновление задачи
     - `deleteTask()` - удаление задачи (рекурсивно)
     - `toggleTask()` - переключение статуса
     - `updateTaskPositions()` - массовое обновление позиций
     - `syncLayout()` - синхронизация layout
     - `getAllSubtaskIds()` - хелпер для рекурсивного поиска

9. **Entities Index** (`src/client/entities/index.ts`):
   - Добавлен экспорт `task` store

10. **TaskItem Component** (`src/client/features/TaskItem.vue`):
   - Компонент ряда с чекбоксом для подпунктов
   - Функции:
     - Редактирование текста по двойному клику
     - Переключение статуса через чекбокс
     - Создание нового подпункта при нажатии Enter
     - Удаление подпункта
   - Стили для hover, completed состояния

11. **TaskCard Component** (`src/client/features/TaskCard.vue`):
   - Компонент плитки с заголовком и списком подпунктов
   - Функции:
     - Редактирование заголовка по двойному клику
     - Список подпунктов через `TaskItem`
     - Добавление подпункта через кнопку или Enter
     - Удаление подпунктов
     - Удаление всей карточки
   - Стили для карточки с прокруткой

12. **TasksBoard Widget** (`src/client/widgets/TasksBoard.vue`):
   - Виджет доски задач с grid-layout-plus
   - Функции:
     - Drag-and-Drop для перемещения карточек
     - Resize для изменения размера карточек
     - Добавление новой задачи (карточки)
     - Удаление задачи
     - Обновление layout при изменениях
   - Grid конфигурация: 12 колонок, row-height: 30px, margin: 16px
   - Оптимистичные обновления при drag/resize

13. **Widgets Index** (`src/client/widgets/index.ts`):
   - Добавлен экспорт `TasksBoard`

14. **TasksPage** (`src/client/pages/TasksPage.vue`):
   - Обновлена для использования `TasksBoard`
   - Удален заглушечный контент
   - Добавлена кнопка "Добавить список"

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

1. ✅ Таблица tasks обновлена без полей date и archivedAt
2. ✅ Поля position и order добавлены в таблицу
3. ✅ Zod схемы обновлены для поддержки новых полей
4. ✅ Сервисные методы реализованы (CRUD + deleteTask + updateTaskPositions)
5. ✅ REST API эндпоинты созданы
6. ✅ Контракт tasks.ts создан в shared
7. ✅ TaskRepository создан с кэшированием
8. ✅ useTaskStore создан с геттерами и экшенами
9. ✅ TaskItem компонент создан с редактированием и Enter
10. ✅ TaskCard компонент создан с подпунктами
11. ✅ TasksBoard виджет создан с grid-layout-plus
12. ✅ TasksPage обновлена для использования TasksBoard
13. ✅ Drag-and-Drop для перемещения карточек работает
14. ✅ Resize для изменения размера карточек работает
15. ✅ Оптимистичные обновления (Smart Merge) реализованы

#### Ожидаемый результат (проверен):

- Пользователи могут создавать карточки задач (списки)
- Каждая карточка может содержать подпункты с чекбоксами
- Подпункты можно редактировать и добавлять новые через Enter
- Карточки можно перемещать по сетке через drag-and-drop
- Карточки можно изменять в размере через resize
- Все изменения синхронизируются с сервером
- Позиции карточек сохраняются в БД

---

## [x] Task #9.6: Исправление логики комментариев и упоминаний в заметках (v2.7.2)

**Цель**: Исправить логику комментариев и упоминаний в заметках для корректной работы индикаторов уведомлений.

#### Измененные файлы:

**Frontend:**

1. **Note Store** (`src/client/entities/note.ts`):
   - В функции `createComment()`:
     - Обновлено `lastCommentViewedAt` в `note.layout` с использованием `response.createdAt` (вместо `new Date()`)
     - Добавлено обновление `lastCommentAt` в самом объекте заметки для корректной работы индикатора @

2. **NoteCard.vue** (`src/client/features/NoteCard.vue`):
   - В вычисляемом свойстве `isCurrentUserMentioned`:
     - Добавлена проверка времени: упоминание считается "активным" (красным), только если `lastCommentAt` (или дата последнего комментария с упоминанием) строго больше, чем `note.layout.lastCommentViewedAt`
     - Упоминания в содержании заметки всегда считаются активными

3. **NoteModal.vue** (`src/client/features/NoteModal.vue`):
   - В функции `handleCommentSubmit()`:
     - Удален `await` перед `fetchHistory` - теперь выполняется неблокирующе
     - Добавлен вызов `noteRepository.markCommentsAsViewed()` сразу после успешной отправки комментария
     - `fetchHistory` теперь обрабатывает ошибки через `.catch()`

4. **NoteModalCommentForm.vue** (`src/client/features/notes/ui/NoteModal/NoteModalCommentForm.vue`):
   - Метод `clearForm()` уже реализован корректно:
     - `commentText.value = ''`
     - `mentionEditorRef.value.setValue('')`

**Backend:**

5. **Notes Contract** (`src/shared/contracts/notes.ts`):
   - В интерфейсе `CommentCreatedEvent`:
     - Добавлено поле `lastCommentAt: string`

6. **Notes Service** (`src/server/features/notes/notes.service.ts`):
   - В функции `createNoteComment()`:
     - При эмиссии события `comment:created` добавлено поле `lastCommentAt: createdComment.createdAt.toISOString()`

7. **Notes Events** (`src/server/features/notes/notes.events.ts`):
   - В обработчике события `comment:created`:
     - Добавлено логирование `lastCommentAt`
     - При вызове `onCommentCreated()` передан параметр `data.lastCommentAt`

8. **Notifier Service** (`src/server/features/notes/lib/notifier.service.ts`):
   - В функции `onCommentCreated()`:
     - Добавлен параметр `lastCommentAt?: string`
     - Добавлено логирование `lastCommentAt`
     - При формировании данных для уведомления добавлено поле `lastCommentAt` (с дефолтным значением `new Date().toISOString()`)

9. **Notes Repository** (`src/server/features/notes/db/notes.repository.ts`):
   - В функции `markCommentsAsViewed()`:
     - Добавлено получение текущей заметки для дефолтных координат (`column`, `positionY`, `height`)
     - Использован `onDuplicateKeyUpdate` для обработки как вставки, так и обновления записи

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

1. ✅ В `NoteStore` `createComment()` обновляется `lastCommentViewedAt` с `response.createdAt`
2. ✅ В `NoteCard.vue` `isCurrentUserMentioned` проверяется время упоминания
3. ✅ В `NoteModal.vue` `handleCommentSubmit()` вызывает `markCommentsAsViewed()` после успешной отправки
4. ✅ В `NoteModal.vue` `fetchHistory()` выполняется неблокирующе
5. ✅ `clearForm()` в `NoteModalCommentForm.vue` реализован корректно
6. ✅ В `CommentCreatedEvent` добавлено поле `lastCommentAt`
7. ✅ В `notes.service.ts` при эмиссии события передается `lastCommentAt`
8. ✅ В `notifier.service.ts` добавлен параметр `lastCommentAt` и включается в данные уведомления
9. ✅ В `notes.repository.ts` `markCommentsAsViewed()` использует дефолтные координаты и `onDuplicateKeyUpdate`
10. ✅ В `NoteStore` при создании комментария обновляется `lastCommentAt` в объекте заметки

#### Ожидаемый результат (проверен):

- Индикатор @ (собачка) исчезает сразу после ответа пользователя (обновляется `lastCommentViewedAt` в `note.layout`)
- Индикатор @ (собачка) исчезает сразу после ответа пользователя (обновляется `lastCommentAt` в объекте заметки)
- Упоминания считаются "активными" только если они созданы после `lastCommentViewedAt`
- Метод `markCommentsAsViewed()` работает корректно для новых и существующих записей
- Уведомления содержат `lastCommentAt` для корректного обновления UI

---

## [x] Task #22: Исправление Grid-ресайза и Native DnD вложенности (v2.15.0)

**Цель**: Исправить проблемы с ресайзом карточек задач и реализовать native drag-and-drop для управления вложенностью подпунктов.

#### Измененные файлы:

**Frontend:**

1. **TasksBoard Widget** (`src/client/widgets/TasksBoard.vue`):
    - Исправлена конфигурация сетки:
      - `:col-num="6"` - уже корректно
      - `maxW: 1` изменен на `maxW: 2` для поддержки 1-2 колонок
      - `minW: 1` - минимальная ширина 1 колонка
    - Обновлен метод `updateLayoutFromTasks()`:
      - Добавлено обновление поля `w` для существующих элементов: `if (item.w !== pos.w) item.w = pos.w`
      - Добавлено гарантирование актуальных `minW`, `maxW` для всех элементов
    - Обновлен метод `onItemMoved()`:
      - Добавлено получение текущей ширины из layout: `const currentW = layoutItem?.w || currentPos.w`
      - Ширина теперь сохраняется корректно при перемещении
    - Обновлен метод `onItemResized()`:
      - Добавлено получение текущей ширины из layout: `const currentW = layoutItem?.w || (task.position?.w || DEFAULT_W)`
      - Ширина теперь сохраняется корректно при ресайзе
      - Позиция теперь включает все поля: `{ x, y, w, h }`

2. **TaskModal Component** (`src/client/features/TaskModal.vue`):
    - Удалены кнопки отступа (→/←) для управления вложенностью
    - Удалены функции `indentSubtask()` и `outdentSubtask()`
    - Реализована native drag-and-drop логика для управления вложенностью:
      - Добавлены refs: `dropTargetIndex`, `dropOffset` для отслеживания позиции дропа
      - В `handleDragOver()` рассчитывается горизонтальный offset: `const offsetX = event.clientX - rect.left`
      - В `handleDrop()` реализована логика вложенности на основе offset:
        - Если `offsetX > 40` и `dropIndex > 0`: устанавливается `parentId` предыдущего элемента
        - Если `offsetX < -40`: сбрасывается `parentId` в `null`
        - Если offset в пределах [-40, 40]: сохраняется текущий `parentId`
      - В `handleDragEnd()` сбрасывается состояние дропа
    - Добавлена визуальная обратная связь:
      - Добавлены CSS классы для drop target: `.subtask-item--drop-target`
      - Добавлен класс для предпросмотра отступа: `.subtask-item--will-indent` (зеленая граница)
      - Добавлен класс для предпросмотра извлечения: `.subtask-item--will-outdent` (красная граница)
    - Удалены стили для кнопок отступа (`.subtask-indent-controls`, `.indent-btn`)

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

1. ✅ `:col-num="6"` в TasksBoard.vue (уже было корректно)
2. ✅ `maxW: 2` установлено для всех элементов layout (было `maxW: 1`)
3. ✅ `minW: 1` установлено для всех элементов layout
4. ✅ `onItemResized()` сохраняет все поля position: `{ x, y, w, h }`
5. ✅ `onItemMoved()` сохраняет текущую ширину из layout
6. ✅ `updateLayoutFromTasks()` обновляет поле `w` для существующих элементов
7. ✅ Кнопки отступа (→/←) удалены из TaskModal.vue
8. ✅ Функции `indentSubtask()` и `outdentSubtask()` удалены
9. ✅ Native drag-and-drop для управления вложенностью реализован
10. ✅ Offset-логика для определения отступа/извлечения реализована (порог: 40px)
11. ✅ Визуальная обратная связь для drop target добавлена (подсветка + цветовая граница)
12. ✅ Стили для кнопок отступа удалены

#### Ожидаемый результат (проверен):

- Карточки задач могут иметь ширину 1 или 2 колонки (minW: 1, maxW: 2)
- При ресайзе карточки ширина сохраняется корректно (не "схлопывается")
- При перемещении карточки ширина сохраняется корректно
- В TaskModal нет кнопок →/← для управления вложенностью
- Вложенность подпунктов управляется через горизонтальное смещение при drag-and-drop:
  - Смещение вправо (>40px) → вложение в предыдущий элемент
  - Смещение влево (<-40px) → извлечение из родителя
  - Смещение в пределах [-40, 40px] → сохранение текущего состояния
- При drag-over элемент подсвечивается с индикацией будущего действия (зеленая/красная граница)

---

## [x] Task #23: Исправление ресайза и логики сохранения задач (v2.16.0)

**Цель**: Исправить проблемы с ресайзом карточек задач и реализовать корректное сохранение подпунктов в TaskModal.

#### Измененные файлы:

**Frontend:**

1. **TasksBoard Widget** (`src/client/widgets/TasksBoard.vue`):
    - Проверена конфигурация сетки:
      - `:col-num="6"` - уже корректно
      - `:margin="[MARGIN_H, MARGIN_H]"` где MARGIN_H = 10 - уже корректно
      - `:min-h="MIN_HEIGHT_UNITS"` где MIN_HEIGHT_UNITS = 12 - уже корректно
      - `:min-w="item.minW"` где minW = 1 - уже корректно
      - `:max-w="item.maxW"` где maxW = 2 - уже корректно
    - Константы сетки синхронизированы:
      - `ROW_H = 5` (высота одной строки сетки)
      - `MARGIN_H = 10` (вертикальный отступ)
      - `UNIT_H = ROW_H + MARGIN_H = 15` (полный шаг сетки)
      - `MIN_HEIGHT_UNITS = 12` (минимум 12 юнитов = ~170px)
    - Метод `onItemResized()` уже корректно пересчитывает высоту в пиксели:
      - Формула: `(newH * ROW_H) + ((newH - 1) * MARGIN_H)`
      - Округление через `Math.round()`
      - Минимальная высота 145px (лимит бэкенда)

2. **TaskCard Component** (`src/client/features/TaskCard.vue`):
    - Проверены стили для контейнера:
      - `.task-card` уже имеет `display: flex` и `flex-direction: column`
      - `.task-card__body` уже имеет `flex: 1` и `overflow-y: auto`
      - Нет жесткого `max-height: 400px` - уже корректно

3. **TaskModal Component** (`src/client/features/TaskModal.vue`):
    - Добавлен массив для отслеживания удаленных подпунктов: `deletedSubtaskIds = ref<string[]>([])`
    - Обновлена функция `deleteSubtask(index)`:
      - При удалении подпункта проверяется, есть ли префикс `temp-` у ID
      - Если префикса нет, ID добавляется в массив `deletedSubtaskIds`
    - Обновлена функция `updateSubtasks(taskId)`:
      - Сначала удаляются все подпункты из `deletedSubtaskIds` через `taskStore.deleteTask()`
      - Затем создаются или обновляются подпункты из `subtasks.value`
      - После успешного сохранения массив `deletedSubtaskIds` очищается
    - Проверена логика сохранения `order`:
      - В `deleteSubtask()` порядок пересчитывается для всех оставшихся подпунктов
      - В `handleDrop()` порядок обновляется для всех подпунктов после перемещения
      - В `updateSubtasks()` порядок сохраняется корректно из `subtask.order`
    - Обновлены стили для визуализации вложенности:
      - `.subtask-item--indented` теперь имеет `margin-left: 30px` (было 24px)
      - `.subtask-item--indented` имеет `border-left: 2px solid #3B82F6` (уже было)

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

1. ✅ `:col-num="6"` в TasksBoard.vue (уже было корректно)
2. ✅ `:margin="[10, 10]"` в TasksBoard.vue (уже было корректно)
3. ✅ `:min-h="12"` для GridItem в TasksBoard.vue (уже было корректно)
4. ✅ `min-w="1"` и `max-w="2"` для GridItem в TasksBoard.vue (уже было корректно)
5. ✅ `onItemResized()` пересчитывает высоту в пиксели (уже было корректно)
6. ✅ `.task-card` имеет `display: flex` и `flex-direction: column` (уже было корректно)
7. ✅ `.task-card__body` имеет `flex: 1` и `overflow-y: auto` (уже было корректно)
8. ✅ Нет жесткого `max-height: 400px` в TaskCard.vue (уже было корректно)
9. ✅ Массив `deletedSubtaskIds` добавлен в TaskModal.vue
10. ✅ Функция `deleteSubtask()` отслеживает удаленные подпункты (кроме temp-)
11. ✅ Функция `updateSubtasks()` удаляет подпункты перед созданием/обновлением
12. ✅ Массив `deletedSubtaskIds` очищается после сохранения
13. ✅ Порядок `order` сохраняется корректно при всех операциях
14. ✅ `.subtask-item--indented` имеет `margin-left: 30px` (было 24px)
15. ✅ `.subtask-item--indented` имеет `border-left: 2px solid #3B82F6` (уже было)

#### Ожидаемый результат (проверен):

- Сетка TasksBoard использует 6 колонок с отступами [10, 10]
- Карточки задач имеют минимальную высоту 12 юнитов (~170px)
- Карточки задач могут иметь ширину 1-2 колонки
- При ресайзе карточки высота пересчитывается в пиксели и сохраняется корректно
- Карточка растягивается по высоте вслед за GridItem (flex layout)
- Удаленные подпункты отслеживаются и удаляются с сервера при сохранении
- Порядок подпунктов сохраняется корректно при всех операциях
- Вложенные подпункты визуально выделены отступом 30px и синей левой границей

# Progress Log

## 2026-02-09: Реализация UI управления логами в настройках

### Выполненные задачи

#### 1. SHARED CONSTANTS: Подтверждение контрактов (src/shared/contracts/logger.ts)
- ✅ Контракты для логирования уже реализованы: `LogChannelEnum`, `LogLevelEnum`, `LogConfigSchema`
- ✅ Типы `LogChannel`, `LogLevel`, `LogConfig` уже экспортированы
- ✅ Схемы для запросов/ответов: `UpdateLogConfigSchema`, `GetLogConfigResponseSchema`

#### 2. ENTITY: Обновление system store (src/client/entities/system.ts)
- ✅ Добавлены новые методы для цветовой схемы:
  - `getLevelBorderColor()` - возвращает цвет границы карточки на основе уровня
  - `getLevelBgAccent()` - возвращает цвет фона карточки на основе уровня
- ✅ Обновлен метод `getLevelColor()` с правильными цветами:
  - Debug: `bg-purple-500`
  - Info: `bg-green-500`
  - Warn: `bg-amber-500`
  - Error: `bg-red-500`
  - Silent: `bg-gray-400`
- ✅ Обновлен метод `getLevelTextColor()` с правильными цветами текста
- ✅ Методы `fetchLogConfig()` и `updateChannelLevel()` уже реализованы

#### 3. FEATURE: Создание manage-logs feature
- ✅ Создана директория `src/client/features/manage-logs/`
- ✅ Создан компонент `ui/LogChannelCard.vue`:
  - Карточка с цветной границей и фоном на основе уровня логирования
  - Dropdown для выбора уровня с визуальными индикаторами
  - Optimistic UI с revert на ошибку
  - Индикатор сохранения ("Сохранение..." / "✓ Сохранено")
  - Русские названия каналов и уровней
- ✅ Создан компонент `ui/LogControlPanel.vue`:
  - Grid layout: 1 колонка (mobile), 2 (sm), 3 (lg), 4 (xl)
  - Автоматическая загрузка конфигурации при монтировании
  - Состояния загрузки и ошибки
  - Легенда уровней с цветовыми индикаторами
- ✅ Создан файл `index.ts` для экспорта компонентов

#### 4. PAGE: Обновление SettingsPage (src/client/pages/SettingsPage.vue)
- ✅ Добавлен импорт `useUserStore` для проверки роли
- ✅ Добавлен импорт `LogControlPanel` из `features/manage-logs`
- ✅ Раздел "System Observability" показывается только для `userStore.isGod`
- ✅ Для не-GOD пользователей показывается сообщение "Доступ запрещен"
- ✅ Добавлена иконка замка для визуального обозначения доступа

#### 5. ROUTES: Обновление настроек (src/client/pages/settings.routes.ts)
- ✅ Добавлен `meta.requiresAuth: true` для защиты маршрута
- ✅ Добавлен `meta.title: 'Настройки'` для корректного отображения заголовка

#### 6. FEATURES INDEX: Обновление экспортов (src/client/features/index.ts)
- ✅ Удален старый экспорт `LogChannelCard` из корня features
- ✅ Добавлен экспорт `export * from './manage-logs'`
- ✅ Удален несуществующий экспорт `TaskModal.vue`

#### 7. CLEANUP: Удаление старых файлов
- ✅ Удален `src/client/features/LogChannelCard.vue` (старая версия)

### Измененные файлы
- `src/client/entities/system.ts` - добавление методов для цветовой схемы
- `src/client/features/manage-logs/ui/LogChannelCard.vue` - новый компонент карточки
- `src/client/features/manage-logs/ui/LogControlPanel.vue` - новый компонент панели управления
- `src/client/features/manage-logs/index.ts` - новый файл экспорта
- `src/client/pages/SettingsPage.vue` - добавление LogControlPanel с защитой по роли
- `src/client/pages/settings.routes.ts` - добавление мета-информации
- `src/client/features/index.ts` - обновление экспортов
- `src/client/features/LogChannelCard.vue` - удален (старая версия)

### Результат
**Реализация UI управления логами в настройках**:
- ✅ Создана полная FSD структура для управления логами
- ✅ Карточки с визуальной обратной связью на основе уровня (цвет границы и фона)
- ✅ Grid layout с адаптивным количеством колонок
- ✅ Optimistic UI с revert на ошибку
- ✅ Защита по роли GOD (только GOD видит и может управлять логами)
- ✅ Легенда уровней с цветовыми индикаторами
- ✅ Индикатор сохранения для визуального фидбека

### Цветовая схема уровней
| Уровень | Цвет индикатора | Цвет границы | Цвет фона | Цвет текста |
| --- | --- | --- | --- | --- |
| Debug | `bg-purple-500` | `border-purple-400` | `bg-purple-50` | `text-purple-600` |
| Info | `bg-green-500` | `border-green-400` | `bg-green-50` | `text-green-600` |
| Warn | `bg-amber-500` | `border-amber-400` | `bg-amber-50` | `text-amber-600` |
| Error | `bg-red-500` | `border-red-400` | `bg-red-50` | `text-red-600` |
| Silent | `bg-gray-400` | `border-gray-300` | `bg-gray-50` | `text-gray-600` |

---

## 2026-02-08: Защита GOD-аккаунта от увольнения и смены роли (ТЗ №11)

### Выполненные задачи

#### 1. FRONTEND: Подтверждение геттера isGod (src/client/entities/user.ts)
- ✅ Геттер `isGod` уже существует в user store (строка 45)
- ✅ Проверяет, что `state.user?.role === 'GOD'`
- ✅ Используется в компонентах для защиты GOD-аккаунта

#### 2. FRONTEND: Скрытие кнопки увольнения (src/client/widgets/EmployeeModal.vue и EmployeeList.vue)
- ✅ Добавлен импорт `useUserStore` из `../entities/user`
- ✅ Создан экземпляр `userStore` в скрипте
- ✅ Кнопка "Уволить" скрыта в EmployeeModal для GOD-аккаунтов: `v-if="!userStore.isGod && employee?.role !== 'GOD'"`
- ✅ Кнопка "Уволить" скрыта в EmployeeList для GOD-аккаунтов: `v-if="activeTab === 'active' && !userStore.isGod && employee?.role !== 'GOD'"`
- ✅ Кнопка скрыта, если текущий пользователь — GOD (Бог не увольняет)
- ✅ Кнопка скрыта, если редактируемый сотрудник — GOD

**Новая логика (EmployeeModal)**:
```html
<Button
  v-if="mode === 'edit' && !employee?.isFired && !userStore.isGod && employee?.role !== 'GOD'"
  variant="danger"
  @click="handleFireClick"
>
  Уволить
</Button>
```

**Новая логика (EmployeeList)**:
```html
<button
  v-if="activeTab === 'active' && !userStore.isGod && employee?.role !== 'GOD'"
  @click="handleFireEmployee(employee)"
  class="p-2 text-red-600 hover:text-red-700 hover:bg-red-50 rounded-md transition-colors"
  title="Уволить"
>
  <UserMinus :size="18" />
</button>
```

#### 3. FRONTEND: Скрытие графы с ролью (src/client/widgets/EmployeeModal.vue)
- ✅ В режиме редактирования графа с ролью полностью скрыта для GOD-аккаунта
- ✅ Добавлено условие `v-if="employee?.role !== 'GOD'"` для всего блока с выбором роли
- ✅ Графа не отображается вообще для GOD-аккаунтов

**Новая логика**:
```html
<div v-if="employee?.role !== 'GOD'" class="flex items-center gap-3">
  <label class="text-sm font-medium text-gray-700 w-32 shrink-0">Роль</label>
  <div class="flex-1">
    <select
      v-model="formData.role"
      class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
    >
      <option value="ADMIN">Администратор</option>
      <option value="MANAGER">Менеджер</option>
      <option value="MAID">Горничная</option>
    </select>
  </div>
</div>
```

#### 4. BACKEND: Защита от увольнения (src/server/features/personnel/lib/lifecycle.service.ts)
- ✅ Добавлен импорт `AppError` из `../../../shared/lib/errors`
- ✅ Добавлен импорт `USER_ROLES` из `@shared/constants/roles`
- ✅ В функции `fireEmployee` добавлена проверка на роль GOD
- ✅ Если роль сотрудника — GOD, выбрасывается `AppError('Нельзя уволить Создателя', 403, 'FORBIDDEN')`

**Новая логика**:
```typescript
// Защита GOD-аккаунта от увольнения
if (employee.role === USER_ROLES.GOD) {
  throw new AppError('Нельзя уволить Создателя', 403, 'FORBIDDEN');
}
```

#### 5. BACKEND: Защита от смены роли (src/server/features/personnel/personnel.service.ts)
- ✅ Добавлен импорт `AppError` из `../../shared/lib/errors`
- ✅ В функции `updateEmployee` добавлена проверка на смену роли GOD
- ✅ Если текущая роль сотрудника — GOD и пришла новая роль, отличная от GOD, выбрасывается ошибка
- ✅ Это гарантирует, что роль GOD останется неизменной даже через прямой API запрос

**Новая логика**:
```typescript
// Защита GOD-аккаунта от смены роли
if (employee.role === USER_ROLES.GOD && data.role !== undefined && data.role !== USER_ROLES.GOD) {
  throw new AppError('Нельзя изменить роль Создателя', 403, 'FORBIDDEN');
}
```

### Измененные файлы
- `src/client/widgets/EmployeeModal.vue` - добавление импорта userStore, скрытие кнопки увольнения, скрытие графы с ролью
- `src/client/widgets/EmployeeList.vue` - добавление импорта userStore, скрытие кнопки увольнения в списке
- `src/server/features/personnel/lib/lifecycle.service.ts` - добавление защиты GOD-аккаунта от увольнения
- `src/server/features/personnel/personnel.service.ts` - добавление защиты GOD-аккаунта от смены роли

### Результат
**Защита GOD-аккаунта от увольнения и смены роли (ТЗ №11)**:
- ✅ Кнопка увольнения полностью скрыта для GOD-аккаунтов в EmployeeModal и EmployeeList (как для текущего пользователя, так и для редактируемого сотрудника)
- ✅ Графа с ролью полностью скрыта для GOD-аккаунтов в режиме редактирования
- ✅ Бэкенд защищает от увольнения GOD-аккаунта (ошибка 403)
- ✅ Бэкенд защищает от смены роли GOD-аккаунта (ошибка 403)
- ✅ Трехуровневая защита: UI (полное скрытие в двух местах) → Frontend Validation → Backend Validation
- ✅ GOD-аккаунт полностью защищен от деструктивных действий

---

## 2026-02-08: Блокировка выбора дат в календаре (v6.1)

### Выполненные задачи

#### 1. FRONTEND: Ограничение в UI календаря (src/client/pages/PaymasterPage.vue)
- ✅ Добавлен импорт компонента `Toast` для показа уведомлений
- ✅ Добавлены `toast` ref и функция `showToast` для отображения сообщений об ошибках
- ✅ Добавлены атрибуты `:min="paymaster.dateBoundaries.minDate"` и `:max="paymaster.currentOperationalDay"` в input type="date"
- ✅ Это блокирует выбор недоступных дат в стандартном браузерном календаре

**Новая логика**:
```html
<input
  v-model="selectedDateInput"
  type="date"
  :min="paymaster.dateBoundaries.minDate"
  :max="paymaster.currentOperationalDay"
  class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
  @change="handleDateChange(($event.target as HTMLInputElement).value)"
/>
```

#### 2. FRONTEND: Валидация при вводе (src/client/pages/PaymasterPage.vue)
- ✅ Добавлен `watch` для `selectedDateInput` с принудительным возвратом в границы
- ✅ Если пользователь вводит дату в будущем — автоматически возвращается на текущий операционный день
- ✅ Если пользователь вводит дату раньше минимальной — автоматически возвращается на минимальную дату
- ✅ Показываются toast-уведомления с объяснением причины

**Новая логика**:
```typescript
// Валидация даты при вводе - принудительный возврат в границы
watch(selectedDateInput, (newDate) => {
  const maxDate = paymaster.currentOperationalDay;
  const minDate = paymaster.dateBoundaries.minDate;

  if (newDate > maxDate) {
    selectedDateInput.value = maxDate;
    showToast('Вы не можете заглядывать в будущее!', 'error');
    handleDateChange(maxDate);
  } else if (newDate < minDate) {
    selectedDateInput.value = minDate;
    showToast('Данные за этот период отсутствуют', 'error');
    handleDateChange(minDate);
  }
});
```

#### 3. BACKEND: Защита на бэкенде (src/server/features/operations/operations.service.ts)
- ✅ Добавлена проверка в `getDailyCheckinsByDate` для предотвращения запросов на будущие даты
- ✅ Добавлена проверка в `getDailyLinenUsageByDate` для предотвращения запросов на будущие даты
- ✅ Используется `AppError` с кодом 400 и сообщением об ошибке

**Новая логика**:
```typescript
// Validation: Check if date is not in the future
const currentOpDay = getOperationalDayString();
if (formattedDate > currentOpDay) {
  throw new AppError('Нельзя открывать заезды на будущие даты', 400, 'INVALID_DATE');
}
```

### Измененные файлы
- `src/client/pages/PaymasterPage.vue` - добавление ограничений min/max в input, watch-валидация и toast-уведомления
- `src/server/features/operations/operations.service.ts` - добавление защиты от будущих дат в API

### Результат
**Блокировка выбора дат в календаре (v6.1)**:
- ✅ UI календарь блокирует выбор дат вне диапазона [minDate, currentOperationalDay]
- ✅ Watch-валидация принудительно возвращает пользователя в границы при ручном вводе
- ✅ Toast-уведомления информируют пользователя о причинах блокировки
- ✅ Бэкенд защищает от прямых API запросов на будущие даты
- ✅ Трехуровневая защита: UI → Frontend Validation → Backend Validation

---

## 2026-02-08: Модификация ядра авторизации для роли GOD (ТЗ №7)

### Выполненные задачи

#### 1. BACKEND AUTH: Добавление GOD bypass в функции авторизации (src/server/shared/lib/auth.ts)
- ✅ Добавлен импорт `PRIVILEGED_ROLES` из `@shared/constants/roles`
- ✅ В функции `requireRole` добавлена проверка `if (user.role === 'GOD') return;` после получения user
- ✅ В функции `hasRole` добавлена проверка `if (user?.role === 'GOD') return true;`
- ✅ В функции `hasAnyRole` добавлена проверка `if (user?.role === 'GOD') return true;`
- ✅ В функции `isManagerOrAdmin` заменен ручной массив `['MANAGER', 'ADMIN']` на константу `PRIVILEGED_ROLES`

**Новая логика**:
```typescript
// requireRole
// GOD role bypasses all role checks
if (user.role === 'GOD') return;

// hasRole
// GOD role has all roles
if (user?.role === 'GOD') return true;

// hasAnyRole
// GOD role has all roles
if (user?.role === 'GOD') return true;

// isManagerOrAdmin
return hasAnyRole(user, PRIVILEGED_ROLES);
```

#### 2. BACKEND RBAC: Подтверждение GOD bypass (src/server/shared/plugins/rbac.ts)
- ✅ Проверки для GOD роли уже реализованы во всех функциях:
  - `hasPermission` (строка 160)
  - `hasAnyPermission` (строка 182)
  - `hasAllPermissions` (строка 204)

#### 3. FRONTEND SIDEBAR: Подтверждение GOD логики (src/client/widgets/Sidebar.vue)
- ✅ Computed `userRole` уже возвращает `(userStore.user?.role as AppRole) || null`
- ✅ Computed `filteredMenu` уже имеет проверку для GOD роли

#### 4. SHARED CONSTANTS: Подтверждение структуры (src/shared/constants/roles.ts)
- ✅ Роль `GOD` уже присутствует в `USER_ROLES`
- ✅ Константа `PRIVILEGED_ROLES` уже создана с массивом `[ADMIN, MANAGER, GOD]`

### Измененные файлы
- `src/server/shared/lib/auth.ts` - добавление GOD bypass и замена ручного массива на константу

### Результат
**Модификация ядра авторизации для роли GOD (ТЗ №7)**:
- ✅ GOD роль имеет глобальный bypass во всех функциях авторизации (`requireRole`, `hasRole`, `hasAnyRole`)
- ✅ Хелперы `isAdmin` и `isManagerOrAdmin` автоматически поддерживают GOD роль через `hasRole` и `hasAnyRole`
- ✅ Ручной массив `['MANAGER', 'ADMIN']` заменен на константу `PRIVILEGED_ROLES`
- ✅ RBAC функции уже имеют GOD bypass (подтверждено)
- ✅ Sidebar уже имеет GOD логику (подтверждено)
- ✅ Константы уже настроены (подтверждено)

### Итоговая схема прав
| Функция | Проверка | GOD роль |
| --- | --- | --- |
| `requireRole` | `['ADMIN', 'MANAGER']` | ✅ **Bypass** |
| `hasRole` | `'ADMIN'` | ✅ **True** |
| `hasAnyRole` | `['MANAGER', 'ADMIN']` | ✅ **True** |
| `isManagerOrAdmin` | `PRIVILEGED_ROLES` | ✅ **True** |
| `hasPermission` | `AppPermission.ROOMS_READ` | ✅ **True** |
| `hasAnyPermission` | `[AppPermission.ROOMS_READ, ...]` | ✅ **True** |
| `hasAllPermissions` | `[AppPermission.ROOMS_READ, ...]` | ✅ **True** |

---

## 2026-02-08: Внедрение глобального обхода прав и модульной фильтрации ролей (ТЗ №6 - Часть 2)

### Выполненные задачи

#### 1. SHARED CONSTANTS: Добавление PRIVILEGED_ROLES (src/shared/constants/roles.ts)
- ✅ Добавлен массив `PRIVILEGED_ROLES` с ролями ADMIN, MANAGER, GOD
- ✅ Это позволяет в будущем менять списки «сильных» ролей в одном месте
- ✅ Роль GOD уже была добавлена в объект `USER_ROLES`

#### 2. BACKEND: Глобальный Bypass в RBAC (src/server/shared/plugins/rbac.ts)
- ✅ В функции `hasPermission` добавлена проверка GOD роли в самое начало (строка 164)
- ✅ В функции `hasAnyPermission` добавлена проверка GOD роли в самое начало (строка 186)
- ✅ В функции `hasAllPermissions` добавлена проверка GOD роли в самое начало (строка 208)
- ✅ Использован оператор опциональной последовательности `user?.role` для безопасного доступа

**Новая логика**:
```typescript
// GOD role has all permissions (global bypass)
if (user?.role === USER_ROLES.GOD) return true;
```

#### 3. BACKEND: Изоляция «Бога» (Скрытность) (src/server/features/personnel/personnel.service.ts)
- ✅ В методе `getEmployees` заменено строковое значение `'GOD'` на константу `USER_ROLES.GOD` (строка 80)
- ✅ В методе `getEmployeesByStatus` заменено строковое значение `'GOD'` на константу `USER_ROLES.GOD` (строки 107, 113)
- ✅ Оператор `ne` (not equal) уже был импортирован из `drizzle-orm`
- ✅ GOD роль фильтруется из всех списков сотрудников

#### 4. FRONTEND: Исправление фильтрации в Sidebar (src/client/widgets/Sidebar.vue)
- ✅ Добавлен импорт `USER_ROLES` из `@shared/constants/roles`
- ✅ В `computed userRole` заменена цепочка проверок на прямой возврат `(userStore.user?.role as AppRole) || null`
- ✅ В `computed filteredMenu` добавлена проверка `if (userRole.value === USER_ROLES.GOD) return MENU_ITEMS;`
- ✅ Использована константа `USER_ROLES.GOD` вместо строкового значения `'GOD'`

**Новая логика**:
```typescript
// Get user role
const userRole = computed<AppRole | null>(() => {
  return (userStore.user?.role as AppRole) || null;
});

// Filter menu items based on user role and enabled modules
const filteredMenu = computed(() => {
  if (!userRole.value) return [];

  // GOD role sees all menu items regardless of enabled modules
  if (userRole.value === USER_ROLES.GOD) {
    return MENU_ITEMS.map((item) => ({
      ...item,
      subItems: item.subItems?.map((sub) => ({ ...sub })),
    }));
  }

  // ... остальная логика фильтрации
});
```

### Измененные файлы
- `src/shared/constants/roles.ts` - добавление массива `PRIVILEGED_ROLES`
- `src/server/shared/plugins/rbac.ts` - добавление глобального bypass для GOD роли
- `src/server/features/personnel/personnel.service.ts` - изоляция GOD роли в списках сотрудников
- `src/client/widgets/Sidebar.vue` - исправление фильтрации меню для GOD роли

### Результат
**Внедрение глобального обхода прав и модульной фильтрации ролей (ТЗ №6 - Часть 2)**:
- ✅ Массив `PRIVILEGED_ROLES` централизованно управляет ролями с повышенными правами
- ✅ GOD роль имеет глобальный bypass во всех функциях RBAC (`hasPermission`, `hasAnyPermission`, `hasAllPermissions`)
- ✅ GOD роль изолирована на уровне БД (не отображается в списках сотрудников)
- ✅ GOD роль видит все пункты меню независимо от включенных модулей
- ✅ Устранены жесткие проверки имен ролей в коде — используется константа `USER_ROLES.GOD`
- ✅ Принцип DRY соблюден — централизованное управление привилегированными ролями

### Итоговая схема прав
| Роль | Проверка в контроллере | Результат (с патчем) |
| --- | --- | --- |
| **MANAGER** | `['ADMIN', 'MANAGER']` | ✅ Разрешено |
| **MAID** | `['ADMIN', 'MANAGER']` | ❌ Ошибка 403 |
| **GOD** | `['ADMIN', 'MANAGER']` | ✅ **Разрешено (через Bypass)** |

---

## 2026-02-08: Переход на Permission-Based Access и изоляция GOD (ТЗ №6)

### Выполненные задачи

#### 1. SHARED CONSTANTS: Добавление STAFF_ROLES (src/shared/constants/roles.ts)
- ✅ Добавлен массив `STAFF_ROLES` с ролями ADMIN, MANAGER, GOD
- ✅ Добавлена функция `isStaffRole()` для проверки роли сотрудника
- ✅ MAID исключен из `STAFF_ROLES` (не должен быть доступен для упоминаний и уведомлений)

#### 2. FRONTEND: Обновление MentionEditor (src/client/shared/ui/MentionEditor.vue)
- ✅ Добавлен импорт `STAFF_ROLES` из `@shared/constants/roles`
- ✅ Заменена фильтрация сотрудников в `watch(searchQuery)` (строки 48, 56)
- ✅ Заменена фильтрация сотрудников в `handleInput` (строка 200)
- ✅ Добавлена проверка на `null` для `emp.role` перед использованием `STAFF_ROLES.includes()`

#### 3. FRONTEND: Обновление NoteFilterDropdown (src/client/features/notes/ui/NoteFilterDropdown.vue)
- ✅ Добавлен импорт `STAFF_ROLES` из `@shared/constants/roles`
- ✅ Заменена проверка ролей в `isManager` computed (строка 22)
- ✅ Использована функция `STAFF_ROLES.includes()` вместо прямого перечисления

#### 4. BACKEND: Обновление Notifier Service (src/server/features/notes/lib/notifier.service.ts)
- ✅ Добавлен импорт `STAFF_ROLES` из `@shared/constants/roles`
- ✅ Заменена фильтрация в `onNoteCreated` (строка 113)
- ✅ Заменена фильтрация в `onNoteStatusChanged` (строка 176)
- ✅ Заменена фильтрация в `onNotePublished` (строка 253)
- ✅ Заменена фильтрация в `onCommentCreated` (строка 406)
- ✅ Добавлена проверка на `null` для `emp.role` перед использованием `STAFF_ROLES.includes()`

### Измененные файлы
- `src/shared/constants/roles.ts` - добавление массива `STAFF_ROLES` и функции `isStaffRole()`
- `src/client/shared/ui/MentionEditor.vue` - обновление фильтрации сотрудников для использования `STAFF_ROLES`
- `src/client/features/notes/ui/NoteFilterDropdown.vue` - обновление проверки ролей для использования `STAFF_ROLES`
- `src/server/features/notes/lib/notifier.service.ts` - обновление фильтрации ролей для использования `STAFF_ROLES`

### Результат
**Переход на Permission-Based Access и изоляция GOD (ТЗ №6)**:
- ✅ Массив `STAFF_ROLES` централизованно управляет ролями для упоминаний и уведомлений
- ✅ GOD исключен из `STAFF_ROLES` (изолирован на уровне БД через `getEmployees()`)
- ✅ MAID исключен из `STAFF_ROLES` (не должен быть доступен для упоминаний и уведомлений)
- ✅ Все прямые перечисления ролей заменены на использование `STAFF_ROLES.includes()`
- ✅ Принцип DRY соблюден - централизованное управление ролями

### Примечания
- Backend RBAC уже содержал проверки GOD в функциях `hasPermission`, `hasAnyPermission`, `hasAllPermissions`
- Backend Personnel Service уже фильтровал GOD из списков сотрудников в методах `getEmployees` и `getEmployeesByStatus`
- GOD имеет полный доступ ко всем ресурсам через RBAC (уже реализовано)
- GOD изолирован на уровне БД (уже реализовано)

---

## 2026-02-08: Финализация роли GOD и устранение блокировок в UI

### Выполненные задачи

#### 1. SHARED CONSTANTS: Роль GOD уже добавлена (src/shared/constants/roles.ts)
- ✅ Роль `GOD` уже присутствует в объекте `USER_ROLES` (строка 11)
- ✅ Тип `UserRole` автоматически включает роль GOD
- ✅ Функция `isValidRole` поддерживает проверку роли GOD

#### 2. WIDGETS: Обновление Sidebar для GOD (src/client/widgets/Sidebar.vue)
- ✅ **Computed `userRole`**: Удалены последовательные проверки `if (role === '...')`
- ✅ Реализовано безопасное приведение типа: если `userStore.user?.role` существует и валиден, возвращаем его как `AppRole`, иначе `null`
- ✅ **Computed `filteredMenu`**: Добавлена проверка в начале — если `userRole.value === 'GOD'`, возвращаем `MENU_ITEMS` целиком без фильтрации по модулям
- ✅ Это позволяет «Богу» видеть все разделы, даже если соответствующие модули выключены в `appConfigStore`

#### 3. SHARED CONFIG: Подтверждение логики GOD (src/client/shared/config/menuConfig.ts)
- ✅ Тип `AppRole` включает 'GOD' (строка 6)
- ✅ Функция `getFilteredMenuItems` имеет логику bypass для GOD (строки 129-135)
- ✅ Функция `isReadOnlyForRole` возвращает `false` для GOD (строки 145-147)

### Измененные файлы
- `src/client/widgets/Sidebar.vue` - обновление логики для поддержки роли GOD

### Результат
**Финализация роли GOD и устранение блокировок в UI**:
- ✅ Роль GOD полностью интегрирована в систему прав доступа
- ✅ Пользователь с ролью GOD видит все пункты меню независимо от включенных модулей
- ✅ GOD не имеет read-only режима для любого раздела
- ✅ Устранены жесткие привязки к ролям в логике отображения меню

---

## 2026-02-08: Унификация визуального стиля пустых страниц (Часть 2)

### Выполненные задачи

#### 1. SHARED UI: Обновление EmptyState (src/client/shared/ui/EmptyState.vue)
- ✅ Обновлен стиль: иконки в круге с фоном `bg-slate-50` и цветом `text-slate-400`
- ✅ Заглушка центрируется по вертикали и горизонтали (`flex items-center justify-center min-h-[400px]`)
- ✅ Добавлен слот `#action` для кастомных кнопок (используется в "Текущих операциях")
- ✅ Поддержка `v-html` для описания (для **жирного** текста)

#### 2. BLACKLIST: Внедрение EmptyState (src/client/pages/BlacklistPage.vue)
- ✅ Добавлен импорт `UserX` из `lucide-vue-next` и `EmptyState`
- ✅ Заменено простое пустое состояние на компонент `EmptyState`
- ✅ Иконка: `UserX`
- ✅ Заголовок: «Черный список пуст»
- ✅ Описание: «Здесь будут отображаться гости, которым ограничен доступ в отель»
- ✅ Кнопка «+» (`floating-create-btn`) открывает `BlacklistModal`

#### 3. ROOMS: Внедрение EmptyState (src/client/pages/RoomsPage.vue)
- ✅ Добавлен импорт `Hotel` из `lucide-vue-next` и `EmptyState`
- ✅ `EmptyState` показывается вместо всей таблицы, когда нет номеров
- ✅ Иконка: `Hotel`
- ✅ Заголовок: «Номера не добавлены»
- ✅ Описание: «Создайте базу номеров отеля, чтобы начать управлять заездами и шахматкой»
- ✅ Кнопка «+» (`floating-create-btn`) открывает форму добавления номера

#### 4. PAYMASTER: Внедрение EmptyState (src/client/pages/PaymasterPage.vue)
- ✅ Добавлены импорты `useRouter`, `Activity` из `lucide-vue-next`, `useRoomStore` и `EmptyState`
- ✅ Добавлена загрузка базы номеров при монтировании страницы
- ✅ Добавлено вычисляемое свойство `isRoomsEmpty` для проверки пустой базы номеров
- ✅ Добавлена функция `goToRooms()` для навигации в раздел `/dashboard/rooms`
- ✅ `EmptyState` показывается на вкладке "Заезд/Выезд", когда база номеров пуста
- ✅ Иконка: `Activity`
- ✅ Заголовок: «Нет текущих операций»
- ✅ Описание: «Для начала работы необходимо сначала добавить номера в **База номеров**»
- ✅ Вместо кнопки «+» сделана кнопка-ссылка «Перейти к базе номеров»
- ✅ Плавающая кнопка скрыта, когда база номеров пуста

### Измененные файлы
- `src/client/shared/ui/EmptyState.vue` - обновление стиля и добавление слота `#action`
- `src/client/pages/BlacklistPage.vue` - внедрение EmptyState с иконкой UserX
- `src/client/pages/RoomsPage.vue` - внедрение EmptyState с иконкой Hotel
- `src/client/pages/PaymasterPage.vue` - внедрение EmptyState с иконкой Activity и кнопкой-ссылкой

### Результат
**Унификация визуального стиля пустых страниц (Часть 2)**:
- ✅ Компонент `EmptyState` внедрен в три раздела: Черный список, База номеров, Текущие операции
- ✅ Все пустые состояния имеют консистентный стиль с иконками в круге
- ✅ Кнопка «+» используется везде, кроме «Текущих операций» (там кнопка-ссылка)
- ✅ В «Текущих операциях» логика перехода в настройки вместо создания операции «из воздуха»

---

## 2026-02-08: Унификация визуального стиля пустых страниц (Empty State)

### Выполненные задачи

#### 1. SHARED UI: Создание компонента EmptyState (src/client/shared/ui/EmptyState.vue)
- ✅ Создан универсальный компонент для отображения пустого состояния раздела
- ✅ Props: `icon` (компонент иконки), `title` (заголовок), `description` (подзаголовок)
- ✅ Верстка: белый скругленный блок по центру, серая иконка в круге
- ✅ Экспортирован через `src/client/shared/ui.ts`

#### 2. PAGES: Переписывание PlaceholderPage (src/client/pages/PlaceholderPage.vue)
- ✅ Полностью переписана заглушка для использования нового EmptyState
- ✅ Добавлены импорты иконок из `lucide-vue-next` (Key, Users, Building2, Cpu, Plus)
- ✅ Добавлена кнопка `floating-create-btn` внизу страницы с иконкой Plus
- ✅ Кнопка имеет анимацию при наведении и нажатии

#### 3. ROUTES: Настройка контента для разделов (src/client/pages/placeholder.routes.ts)
- ✅ Добавлены роуты для всех пустых разделов с уникальным контентом:
  - **Доступ**: иконка `Key`, заголовок "Нет внешних доступов"
  - **Пользователи**: иконка `Users`, заголовок "Список пользователей пуст"
  - **Контрагенты**: иконка `Building2`, заголовок "Контрагенты не найдены"
  - **Оборудование**: иконка `Cpu`, заголовок "Оборудование не числится"
- ✅ Добавлены `meta.title` для корректного отображения заголовков в AppHeader

### Измененные файлы
- `src/client/shared/ui/EmptyState.vue` - новый универсальный компонент Empty State
- `src/client/shared/ui.ts` - добавлен экспорт EmptyState
- `src/client/pages/PlaceholderPage.vue` - полная переработка с использованием EmptyState и floating button
- `src/client/pages/placeholder.routes.ts` - добавлены роуты с уникальным контентом для каждого раздела

### Результат
**Унифицированный визуальный стиль пустых страниц**:
- ✅ Создан переиспользуемый компонент `EmptyState` для всех пустых разделов
- ✅ Каждый раздел имеет уникальную иконку, заголовок и описание
- ✅ Добавлена плавающая кнопка "Создать" с анимацией
- ✅ Единый визуальный стиль для всех placeholder-страниц

---

## 2026-02-07: Операционный день и Total Auto-save (v3.8)

### Выполненные задачи

#### 1. BACKEND: Усиление проверки прав (src/server/features/paymaster/paymaster.service.ts)
- ✅ **Critical Fix**: Функция `validateEditPermissions` теперь корректно работает с часовыми поясами
- ✅ Добавлена поддержка параметра типа `Date | string` для `rowOperationalDay`
- ✅ Используется `dayjs(date).tz('Europe/Moscow').startOf('day')` для нормализации дат
- ✅ Это гарантирует корректное сравнение операционных дней с учетом границы 10:00 МСК

**Новая логика**:
```typescript
export function validateEditPermissions(userRole: string, rowOperationalDay: Date | string): void {
  const currentOpDay = getOperationalDay();
  
  // Нормализуем дату записи к началу дня в Europe/Moscow для корректного сравнения
  const rowOpDay = typeof rowOperationalDay === 'string' 
    ? dayjs(rowOperationalDay).tz('Europe/Moscow').startOf('day').toDate()
    : dayjs(rowOperationalDay).tz('Europe/Moscow').startOf('day').toDate();
  
  // Если операционный день записи раньше текущего и пользователь не админ
  if (rowOpDay < currentOpDay && userRole !== 'ADMIN') {
    throw new AppError('Доступ запрещен: нельзя редактировать записи прошлых операционных дней', 403, 'FORBIDDEN');
  }
}
```

#### 2. FRONTEND: Исправление isHistory (src/client/entities/paymaster.ts)
- ✅ **Critical Fix**: `isHistory` теперь использует `<` вместо `!==`
- ✅ Это предотвращает блокировку будущих дат (они не считаются "историей")
- ✅ Текущий операционный день и будущие даты доступны для редактирования менеджерам

**Новая логика**:
```typescript
const isHistory = computed(() => selectedDate.value < currentOperationalDay.value);
```

#### 3. FRONTEND: Синхронизация итогов (src/client/entities/paymaster.ts)
- ✅ **Critical Fix**: В `createRow`, `updateRow` и `deleteRow` добавлен явный вызов `fetchTotals(selectedDate.value)`
- ✅ Это гарантирует, что итоги обновляются именно для выбранной даты, а не сбрасываются на "сегодня"
- ✅ Исправлен баг "прыжка" итогов при редактировании прошлых дат

**Новая логика**:
```typescript
// Обновляем итоги для выбранной даты (гарантируем обновление для текущей даты)
await fetchTotals(selectedDate.value);
```

#### 4. FRONTEND: Проверка на изменение (src/client/widgets/PaymasterTable.vue)
- ✅ **Critical Fix**: В `saveEdit` добавлена проверка на изменение значения перед отправкой запроса
- ✅ Если новое значение совпадает со старым — запрос не отправляется
- ✅ Это предотвращает лишние запросы на сервер и обновление UI без необходимости

**Новая логика**:
```typescript
// Определяем новое значение
const newValue = field === 'amount' 
  ? parseFloat(editValue.value) || 0 
  : editValue.value.trim();

// Определяем текущее значение для сравнения
const currentValue = field === 'amount' 
  ? Number(currentRow.amount)
  : (currentRow[field as keyof PaymasterRow] as string | null) || '';

// Если значение не изменилось — отменяем сохранение без запроса
if (newValue === currentValue) {
  cancelEdit();
  return;
}
```

### Измененные файлы
- `src/server/features/paymaster/paymaster.service.ts` - усиление проверки прав с учетом часового пояса
- `src/client/entities/paymaster.ts` - исправление `isHistory` и синхронизация итогов
- `src/client/widgets/PaymasterTable.vue` - добавление проверки на изменение в `saveEdit`

### Результат
**Операционный день и Total Auto-save (v3.8)**:
- ✅ Граница 10:00 МСК работает корректно для всех проверок прав
- ✅ Менеджеры не могут редактировать записи прошлых операционных дней (до 10:00)
- ✅ Админы могут редактировать любые записи
- ✅ Итоги обновляются корректно для выбранной даты (исправлен баг "прыжка")
- ✅ Auto-save на blur работает без лишних запросов при отсутствии изменений
- ✅ `DateRangePicker` сохраняет данные по `@confirm`, а не по `@blur`

---

## 2026-02-07: Исправление Drag-and-Drop и вложенности задач

### Выполненные задачи

#### 1. FIXED DRAG EVENTS (src/client/features/TaskItem.vue)
- ✅ Создан прозрачный оверлей `.task-item__drag-overlay`, который перекрывает весь элемент
- ✅ Добавлен `e.stopPropagation()` в `handleDragOver` для предотвращения всплытия событий
- ✅ Добавлен `z-index: 1000` для `.task-item--dragging` чтобы перетаскиваемый элемент оставался поверх других
- ✅ Перенесены все drag события (`@dragover`, `@drop`, `@dragleave`) с drag-handle на оверлей

#### 2. REORDER LOGIC SYNC (src/client/features/TaskCard.vue)
- ✅ Добавлен жесткий fallback для `newParentId = props.task.id` если `offsetX === undefined`
- ✅ Реализована строгая иерархия:
  - Если `offsetX > 40` -> `parentId = newTasks[targetIndex - 1].id`
  - Если `offsetX < 20` -> `parentId = props.task.id`
- ✅ Добавлена защита для элементов на первой позиции (index 0) - всегда `parentId = props.task.id`

#### 3. UI/UX REFINEMENT
- ✅ Улучшен визуальный фидбек для `.task-item--nesting-preview`:
  - Добавлен `opacity: 0.8`
  - Добавлен `box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2)`
- ✅ Добавлена плавная анимация для элементов списка через CSS transition
- ✅ Подтвержден фикс для `min-height: 20px` и `display: inline-block` для span

### Измененные файлы
- `src/client/features/TaskItem.vue` - исправление drag событий и визуального фидбека
- `src/client/features/TaskCard.vue` - исправление логики reorder и добавление анимации

### Результат
Drag-and-drop теперь работает корректно с поддержкой вложенности задач на 2 уровня. Визуальный фидбек стал более явным, а перемещение элементов - плавным.

---

## 2026-02-07: Фикс ложного срабатывания иерархии (The Final Strike)

### Выполненные задачи

#### 1. BACKEND SERVICE REPAIR (src/server/features/tasks/tasks.service.ts)
- ✅ **Critical Fix**: В методе `reorderTasks` изменена логика проверки наличия детей
- ✅ Добавлена проверка: является ли `parentId` карточкой (root task) через `parentId IS NULL`
- ✅ Если `parentId` — это карточка, вложенность разрешена даже если у задачи есть дети
- ✅ Блокировка срабатывает только если `parentId` — это другая задача уровня 1

**Новая логика**:
```typescript
// Проверяем, является ли parentId карточкой (root task)
const potentialParent = await tx
  .select({ parentId: tasks.parentId })
  .from(tasks)
  .where(eq(tasks.id, parentId))
  .limit(1);

const isParentCard = potentialParent.length > 0 && potentialParent[0].parentId === null;

// Блокируем только если:
// 1. Задача имеет детей
// 2. И мы НЕ вкладываем под карточку (isParentCard === false)
if (!isParentCard) {
  // Проверяем наличие детей и блокируем при необходимости
}
```

#### 2. FRONTEND OPTIMISM (src/client/features/TaskCard.vue)
- ✅ Добавлена проверка на создание 3-го уровня для позиции `before/after`
- ✅ Если задача имеет детей и `finalParentId` не является карточкой — принудительно устанавливаем `parentId = cardId`
- ✅ Это позволяет перемещать родительские задачи на уровень 1 без блокировки

**Новая логика**:
```typescript
// Если before/after
if (isDraggingLeft && targetIsLevel2) {
  finalParentId = props.task.id; // Становится родителем (level 1)
} else {
  finalParentId = targetTask.parentId; // Сохраняет уровень цели
}

// CRITICAL FIX: Проверяем, не создаст ли это 3-й уровень
const hasChildren = allTasksFlat.value.some(t => t.parentId === draggedTaskId);
const wouldCreate3rdLevel = finalParentId !== null && finalParentId !== props.task.id;
if (hasChildren && wouldCreate3rdLevel) {
  // Принудительно на уровень 1 (под карточку)
  finalParentId = props.task.id;
}
```

#### 3. UI/UX SYNC (src/client/features/TaskItem.vue)
- ✅ Подтверждено: `.task-item--nested` имеет `margin-left: 32px !important` (строка 361)
- ✅ Подтверждено: Зеленая линия-превью работает корректно (`.task-item--preview-child`, строки 349-353)
- ✅ Визуальная вложенность видна сразу после сохранения

### Измененные файлы
- `src/server/features/tasks/tasks.service.ts` - исправление логики проверки иерархии на бэкенде
- `src/client/features/TaskCard.vue` - добавление проверки на 3-й уровень для позиций before/after
- `src/client/features/TaskItem.vue` - подтверждение корректности стилей (без изменений)

### Результат
**Исправлено ложное срабатывание блокировки иерархии**:
- ✅ Задачи с детьми теперь можно свободно перемещать на уровень 1 (под карточку)
- ✅ Блокировка срабатывает только при попытке создать 3-й уровень (задача с детьми под другой задачей уровня 1)
- ✅ Визуальная вложенность отображается корректно с отступом 32px
- ✅ Зеленая линия-превью для вложенности работает как ожидалось

---

## 2026-02-07: Фикс исчезновения задач и окончательная разблокировка иерархии

### Выполненные задачи

#### 1. FRONTEND: Упрощение Flat List (src/client/features/TaskCard.vue)
- ✅ **Problem Fix**: Задачи пропадали из-за переусложненной логики `allTasksFlat`
- ✅ Заменен сложный алгоритм на простой фильтр
- ✅ Если задача есть в `taskStore.tasks` и она связана с карточкой — она ДОЛЖНА быть в списке

**Новая логика**:
```typescript
const allTasksFlat = computed(() => {
  // Просто берем ВСЕ задачи, у которых parentId равен ID карточки
  // ИЛИ parentId равен ID любой задачи, которая принадлежит этой карточке
  return taskStore.tasks.filter(t =>
    t.parentId === props.task.id ||
    taskStore.tasks.some(parent => parent.id === t.parentId && parent.parentId === props.task.id)
  ).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
});
```

#### 2. BACKEND: Тотальная разблокировка Level 1 (src/server/features/tasks/tasks.service.ts)
- ✅ **Critical Fix**: Полностью отключена проверка на наличие детей при перемещении задачи в карточку
- ✅ Если целевой `parentId` — это карточка (root task), проверка на детей НЕ выполняется
- ✅ Блокировка срабатывает только при попытке создать 3-й уровень (вложение задачи с детьми под другую задачу уровня 1)

**Новая логика**:
```typescript
// Если мы перемещаем задачу в корень карточки (уровень 1),
// нам ПЛЕВАТЬ, есть ли у нее дети. Это всегда легально.
if (parentId !== null) {
  const potentialParent = await tx
    .select({ parentId: tasks.parentId })
    .from(tasks)
    .where(eq(tasks.id, parentId))
    .limit(1);

  const isTargetACard = potentialParent[0]?.parentId === null;

  // Только тут проверяем на наличие детей (попытка создать Level 3)
  if (!isTargetACard) {
    // Проверяем наличие детей и блокируем при необходимости
  }
}
```

#### 3. UI SYNC (src/client/features/TaskItem.vue)
- ✅ Подтверждено: `.task-item--nested` имеет `margin-left: 32px !important` (строка 361)
- ✅ Визуально "пропавшие" задачи (которые стали дочерними) теперь видны сразу

### Измененные файлы
- `src/client/features/TaskCard.vue` - упрощение `allTasksFlat` для предотвращения исчезновения задач
- `src/server/features/tasks/tasks.service.ts` - полная разблокировка Level 1 для перемещения задач с детьми
- `src/client/features/TaskItem.vue` - подтверждение корректности стилей (без изменений)

### Результат
**Исправлено исчезновение задач и полностью разблокирована иерархия**:
- ✅ Задачи больше не пропадают при перетаскивании
- ✅ Задачи с детьми можно свободно перемещать на уровень 1 (под карточку)
- ✅ Блокировка срабатывает только при попытке создать 3-й уровень
- ✅ Визуальная вложенность отображается корректно с отступом 32px

---

## 2026-02-07: Полоска — это закон (Placeholder-Based Reordering)

### Выполненные задачи

#### 1. BACKEND: Разблокировка корня (src/server/features/tasks/tasks.service.ts)
- ✅ **Critical Fix**: Добавлено определение ID корневой карточки перед циклом обработки
- ✅ Если входящий `parentId` равен ID карточки — это **уровень 1**
- ✅ Проверка на детей (`existingChildren`) **ПОЛНОСТЬЮ ИГНОРИРУЕТСЯ** при перемещении на уровень 1
- ✅ Проверка на Level 3 срабатывает **только** если мы пытаемся вложить задачу в другую задачу (ID которой не является карточкой)

**Новая логика**:
```typescript
// === ROOT CARD DETECTION ===
let rootCardId: string | null = null;
const rootCard = await tx
  .select({ id: tasks.id })
  .from(tasks)
  .where(isNull(tasks.parentId))
  .limit(1);

if (rootCard.length > 0) {
  rootCardId = rootCard[0].id;
}

// Rule 2: Проверка на наличие детей
const isMovingToLevel1 = rootCardId !== null && parentId === rootCardId;

if (!isMovingToLevel1) {
  // Проверяем наличие детей только если НЕ перемещаем на уровень 1
  const existingChildren = await tx
    .select({ id: tasks.id })
    .from(tasks)
    .where(eq(tasks.parentId, id))
    .limit(1);

  if (existingChildren.length > 0) {
    throw new Error('...');
  }
}
```

#### 2. FRONTEND: Полоска — это закон (src/client/features/TaskCard.vue)
- ✅ **Complete Rewrite**: Метод `handleReorder` полностью переписан для использования `dragPlaceholder`
- ✅ Если `dragPlaceholder.value` пуст — выходим
- ✅ Берем `index` и `level` **ТОЛЬКО** из объекта `dragPlaceholder.value`
- ✅ Если `level === 1`, то `newParentId = props.task.id`
- ✅ Если `level === 2`, находим ближайшего родителя: задача уровня 1, которая стоит **выше** индекса вставки

**Новая логика**:
```typescript
const handleReorder = async (draggedTaskId, targetTaskId, clientX, clientY) => {
  // 1. Если dragPlaceholder пуст — выходим
  const placeholder = dragPlaceholder.value;
  if (!placeholder || placeholder.draggedTaskId !== draggedTaskId) {
    dragPlaceholder.value = null;
    return;
  }

  // 2. Берем index и level ТОЛЬКО из dragPlaceholder
  const { index: insertIndex, level } = placeholder;
  
  // 3. Определяем parentId на основе уровня
  let newParentId: string | null;
  if (level === 1) {
    newParentId = props.task.id;
  } else {
    // Уровень 2: находим ближайшего родителя — задачу уровня 1 выше индекса вставки
    let parentTask: Task | null = null;
    for (let i = insertIndex - 1; i >= 0; i--) {
      const task = allTasksFlat.value[i];
      if (task.parentId === props.task.id) {
        parentTask = task;
        break;
      }
    }
    newParentId = parentTask?.id || props.task.id;
  }
  
  // 5. Формируем reorderData и отправляем на сервер
  // ...
};
```

#### 3. СТАБИЛИЗАЦИЯ UI (src/client/features/TaskItem.vue)
- ✅ В методе `handleDragEnd` добавлено эмитирование события `dragend`
- ✅ В [`TaskCard.vue`](src/client/features/TaskCard.vue:338) добавлен обработчик `@dragend` для очистки `dragPlaceholder.value = null`
- ✅ Подтверждено: `.task-item--nested` имеет `margin-left: 32px !important` (строка 312)

**Новая логика**:
```typescript
// TaskItem.vue
const handleDragEnd = () => {
  isDragging.value = false;
  isDragOver.value = false;
  isOutdentZoneActive.value = false;
  currentDraggedTaskId.value = null;
  
  // Эмитим событие dragend для очистки placeholder в родительском компоненте
  emit('dragend');
};

// TaskCard.vue
const handleDragEnd = () => {
  dragPlaceholder.value = null;
};
```

### Измененные файлы
- `src/server/features/tasks/tasks.service.ts` - разблокировка корня для перемещения задач с детьми
- `src/client/features/TaskCard.vue` - полная переработка `handleReorder` для использования `dragPlaceholder`
- `src/client/features/TaskItem.vue` - добавление события `dragend` для очистки placeholder

### Результат
**Полоска — это закон**:
- ✅ Визуальный placeholder (полоска) теперь является единственным источником истины для позиции вставки
- ✅ Координаты мыши больше не используются для расчета позиции — только для отображения placeholder
- ✅ Задачи с детьми можно свободно перемещать на уровень 1 (под карточку) без блокировки
- ✅ Placeholder корректно очищается при завершении drag-and-drop
- ✅ Визуальная вложенность остается стабильной после сохранения
