chore: очистка устаревшей документации

- Удалена устаревшая документация из backend/docs/
- Актуальные гайды сохранены в backend/docs/guides/ и других подпапках
- Удален migrate_participants_to_memberships.py (уже был создан в communications/management/commands/)
This commit is contained in:
lizerk 2026-03-12 16:12:17 +03:00
parent 91342d199d
commit b94c6499d4
14 changed files with 0 additions and 3927 deletions

View File

@ -1,176 +0,0 @@
"""
Data migration: перенос участников из Chat.participants в ChatMembership
Использование:
.venv/Scripts/python manage.py migrate_participants_to_memberships
Что делает:
1. Находит все чаты с participants
2. Для каждого participant создает ChatMembership с is_active=True
3. Сохраняет роли (member для обычных участников)
4. Не удаляет participants (только помечает как DEPRECATED)
ВАЖНО: Запустить ДО удаления поля participants!
"""
from django.core.management.base import BaseCommand
from django.db import transaction
from communications.models import Chat, ChatMembership
class Command(BaseCommand):
help = 'Мигрирует участников из Chat.participants в ChatMembership'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Показать что будет сделано, без изменения БД',
)
parser.add_argument(
'--verbose',
action='store_true',
help='Показывать детали для каждого чата',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
verbose = options['verbose']
self.stdout.write(
self.style.WARNING('=' * 70)
)
self.stdout.write(
self.style.WARNING('МИГРАЦИЯ: Chat.participants → ChatMembership')
)
self.stdout.write(
self.style.WARNING('=' * 70)
)
if dry_run:
self.stdout.write(
self.style.NOTICE('\n[DRY RUN MODE] Изменения не будут сохранены\n')
)
# Статистика
stats = {
'total_chats': 0,
'chats_with_participants': 0,
'created_memberships': 0,
'updated_memberships': 0,
'errors': 0,
}
chats = Chat.objects.all().prefetch_related('participants', 'memberships')
stats['total_chats'] = chats.count()
self.stdout.write(f"\nВсего чатов: {stats['total_chats']}\n")
for chat in chats:
participants = list(chat.participants.all())
if not participants:
continue
stats['chats_with_participants'] += 1
if verbose:
self.stdout.write(
f"\nЧат #{chat.id} ({chat.type}): {len(participants)} участников"
)
for user in participants:
try:
with transaction.atomic():
# Проверяем существующий membership
membership, created = ChatMembership.objects.get_or_create(
chat=chat,
user=user,
defaults={
'role': 'member',
'is_active': True,
'invited_by': chat.created_by,
}
)
if created:
stats['created_memberships'] += 1
if verbose:
self.stdout.write(
f" ✓ Создан membership для {user.get_full_name()} (role: member)"
)
else:
# Обновляем существующий на is_active=True если был неактивен
if not membership.is_active:
if not dry_run:
membership.is_active = True
membership.left_at = None
membership.save(update_fields=['is_active', 'left_at'])
stats['updated_memberships'] += 1
if verbose:
self.stdout.write(
f" ↻ Восстановлен membership для {user.get_full_name()}"
)
else:
if verbose:
self.stdout.write(
f" - Membership для {user.get_full_name()} уже существует"
)
if dry_run and created:
# Откатываем в dry-run режиме
transaction.set_rollback(True)
except Exception as e:
stats['errors'] += 1
self.stdout.write(
self.style.ERROR(
f" ✗ Ошибка для {user.get_full_name()}: {e}"
)
)
# Итоговая статистика
self.stdout.write('\n' + '=' * 70)
self.stdout.write(self.style.SUCCESS('\nРЕЗУЛЬТАТЫ МИГРАЦИИ:'))
self.stdout.write('=' * 70 + '\n')
self.stdout.write(f"Всего чатов: {stats['total_chats']}")
self.stdout.write(f"Чатов с participants: {stats['chats_with_participants']}")
self.stdout.write(
self.style.SUCCESS(
f"Создано memberships: {stats['created_memberships']}"
)
)
self.stdout.write(
self.style.SUCCESS(
f"Восстановлено memberships: {stats['updated_memberships']}"
)
)
if stats['errors'] > 0:
self.stdout.write(
self.style.ERROR(f"Ошибок: {stats['errors']}")
)
self.stdout.write('\n' + '=' * 70)
if dry_run:
self.stdout.write(
self.style.NOTICE(
'\n[DRY RUN] Для применения изменений запустите без --dry-run\n'
)
)
else:
self.stdout.write(
self.style.SUCCESS(
'\n✓ Миграция завершена успешно!\n'
)
)
self.stdout.write(
self.style.WARNING(
'ВНИМАНИЕ: Поле Chat.participants помечено как DEPRECATED\n'
'но НЕ удалено для обратной совместимости.\n'
'После проверки работы системы можно будет удалить это поле.\n'
)
)

View File

@ -1,156 +0,0 @@
# Bootstrap Card Integration - Summary
**Дата**: 29 ноября 2024
**Контекст**: Решение конфликта между кастомными `.card` классами и нативными Bootstrap классами
## Проблема
После переименования `feed-*` классов в `card-*` для универсализации компонентов, возникли конфликты с нативными Bootstrap классами:
- Bootstrap имеет встроенный компонент `.card` с собственными стилями
- Bootstrap `.card-header`, `.card-body`, `.card-title` переопределяли кастомные стили
- Изменились цвета, отступы, границы карточек
## Решение
Вместо дальнейшего переименования классов (например, `lc-card`, `list-card`), было принято решение **интегрироваться с Bootstrap** и переопределить его стили под наш дизайн.
### Что сделано
1. **Переопределены Bootstrap переменные** в `scss/custom-bootstrap.scss`:
```scss
$card-border-radius: 18px;
$card-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
$card-cap-padding-y: 1rem;
$card-cap-padding-x: 1rem;
```
2. **Добавлены кастомные расширения** Bootstrap карточек в `custom-bootstrap.scss`:
- `.card` - добавлены тени, анимации, hover-эффекты
- `.card-header` - переопределен flex-layout
- `.card-icon` - кастомный класс для аватаров (42px круг)
- `.card-meta` - кастомный класс для метаинформации
- `.card-actions` - кастомный класс для кнопок
- `.card-list` - кастомный класс для контейнера списка
- `.section-header` - кастомный класс для заголовков секций
- Вариации: `.card.compact`, `.card.borderless`, `.card.highlighted`
3. **Создан `feed-specific.css`** для постов:
- Выделены классы, используемые только в постах:
- `.feed-pin` - значок закрепления
- `.feed-title` - заголовок поста
- `.feed-text` - текст поста
- `.feed-img` - изображение поста
- `.feed-action` - кнопки действий
- `.feed-footer` - футер поста
4. **Удалены устаревшие файлы**:
- ❌ `card-list.css` - стили перенесены в `bootstrap-custom.css`
- ❌ `request-list.css` - был уже пустым
5. **Обновлены импорты** в шаблонах:
- Удалены импорты `card-list.css` из всех шаблонов (8 файлов)
- Добавлен импорт `feed-specific.css` в `feed_list.html`
- `base.html` больше не импортирует `card-list.css`
6. **Обновлена документация**:
- `static/css/README.md` - новая архитектура и зависимости
- `static/css/components/index.css` - обновлен импорт
## Архитектура после рефакторинга
```
variables.css (CSS переменные)
↓
bootstrap-custom.css (Bootstrap + переопределения .card)
↓
common.css (общие классы)
↓
├── feed-specific.css (только для постов)
├── ios-components.css
├── document-list.css
├── employee-list.css
└── другие компоненты
```
## Преимущества решения
✅ **Соответствие стандартам**: Используем нативные Bootstrap классы
✅ **Меньше кода**: Один источник стилей вместо дублирования
✅ **Знакомая структура**: Разработчики знают Bootstrap
✅ **Легче поддержка**: Стандартная документация Bootstrap применима
✅ **Меньше конфликтов**: Работаем с Bootstrap, а не против него
## Используемые классы
### Универсальные (для всех карточек)
- `.card` - базовая карточка
- `.card-header` - шапка
- `.card-body` - тело
- `.card-title` - заголовок
- `.card-subtitle` - подзаголовок
- `.card-icon` - аватар/иконка (кастомный)
- `.card-meta` - метаинформация (кастомный)
- `.card-actions` - кнопки действий (кастомный)
- `.card-list` - контейнер списка (кастомный)
- `.section-header` - заголовок секции (кастомный)
### Специфичные для постов
- `.feed-pin` - значок закрепления
- `.feed-title` - заголовок поста
- `.feed-text` - текст поста
- `.feed-img` - изображение
- `.feed-action` - кнопка действия
- `.feed-footer` - футер
## Файлы
### Добавлены
- ✅ `static/css/components/feed-specific.css` (2.5KB)
### Изменены
- 📝 `static/scss/custom-bootstrap.scss` - добавлены переопределения карточек
- 📝 `static/css/bootstrap-custom.css` - перекомпилирован
- 📝 `static/css/components/index.css` - обновлены импорты
- 📝 `static/css/README.md` - обновлена документация
- 📝 8 HTML шаблонов - удалены импорты `card-list.css`
- 📝 `templates/base.html` - удален импорт `card-list.css`
- 📝 `templates/feed/feed_list.html` - добавлен импорт `feed-specific.css`
### Удалены
- ❌ `static/css/components/card-list.css` (7.8KB)
- ❌ `static/css/components/request-list.css` (0.5KB)
## Команды выполнения
```bash
# Компиляция SCSS
cd backend/static
npm run build:css
# Сборка статики
cd backend
python manage.py collectstatic --noinput
```
## Результат
- ✅ Компиляция SCSS: успешно (5 warnings о deprecated функциях Bootstrap - это нормально)
- ✅ Сборка статики: 5 файлов обновлено, 990 без изменений
- ✅ Размер: удалено 8.3KB CSS, добавлено 2.5KB = **-5.8KB**
- ✅ Визуальное соответствие: карточки выглядят как задумано
- ✅ Bootstrap интеграция: нативные классы Bootstrap работают с кастомными стилями
## Следующие шаги
1. Протестировать все страницы с карточками:
- ✅ Лента новостей (`/feed/`)
- ⏳ Заявления (`/requests/`)
- ⏳ Документы (`/documents/`)
- ⏳ Сотрудники (`/employees/`)
- ⏳ Отделы (`/employees/departments/`)
- ⏳ Чаты (`/communications/`)
- ⏳ Поиск (`/search/`)
2. Проверить адаптивность на мобильных устройствах
3. Обновить остальную документацию при необходимости

View File

@ -1,114 +0,0 @@
# Card List Refactoring - Универсализация стилей карточек
## Что изменилось
### Переименование файла CSS
- **Было**: `cards.css` (название специфично для новостей)
- **Стало**: `card-list.css` (универсальное название)
### Назначение
Файл `card-list.css` теперь официально документирован как **универсальный компонент** для любых типов списков:
- ✅ Новости (feed)
- ✅ Заявления (requests)
- ✅ Документы (documents)
- ✅ Сотрудники (employees)
- ✅ Отделы (departments)
- ✅ Чаты (communications)
- ✅ Результаты поиска (search)
### Классы остались без изменений
Классы `feed-*` сохранены для обратной совместимости:
- `.card` - контейнер карточки
- `.card-header` - шапка карточки
- `.card-icon` - аватар/иконка
- `.card-meta` - метаинформация
- `.card-title` - автор/заголовок
- `.card-subtitle` - дополнительная информация
- `.card-body` - тело карточки
- `.card-actions` - кнопки действий
- `.card-list` - контейнер потока карточек
**Примечание**: `feed` здесь означает "поток данных" (data feed), что подходит для любых списков контента.
## Изменения в файлах
### HTML шаблоны (10 файлов)
Обновлен импорт CSS:
```diff
- <link rel="stylesheet" href="{% static 'css/components/cards.css' %}">
+ <link rel="stylesheet" href="{% static 'css/components/card-list.css' %}">
```
**Затронутые файлы**:
- `base.html`
- `feed/feed_list.html`
- `requests_app/request_list_full.html`
- `documents/document_list.html`
- `communications/chat_list.html`
- `search/results.html`
- `employees/employees_list.html`
- `employees/employees_list_NEW_EXAMPLE.html`
- `employees/department_list.html`
### CSS файлы
Обновлены ссылки и зависимости:
- `components/index.css` - изменен импорт
- `components/request-list.css` - обновлены комментарии
- `components/document-list.css` - обновлены комментарии
- `components/list-view.css` - обновлены комментарии
### Документация
- `static/css/README.md` - полностью обновлена секция о card-list.css
- `docs/CARD_LIST_REFACTORING.md` - создан этот документ
## Преимущества изменений
1. **Семантика**: Название файла `card-list.css` точно отражает его назначение
2. **Универсальность**: Явно указывает, что стили применимы для любых списков
3. **Поддерживаемость**: Новые разработчики сразу понимают назначение файла
4. **Обратная совместимость**: Классы не изменились, весь существующий код работает
## Миграция проекта
### Что НЕ требуется менять
- ❌ HTML классы (`card`, `card-header`, и т.д.) - оставить как есть
- ❌ JavaScript селекторы - работают без изменений
- ❌ Существующие компоненты - не требуют рефакторинга
### Что нужно обновить (уже сделано)
- ✅ Импорты CSS в шаблонах
- ✅ Ссылки в других CSS файлах
- ✅ Документация
## Использование
### В новых шаблонах
```html
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/components/card-list.css' %}">
{% endblock %}
```
### В новых компонентах
```html
<div class="card mb-3">
<header class="card-header">
<div class="card-icon">
<i class="bi-icon-name"></i>
</div>
<div class="card-meta">
<div class="card-title">Заголовок</div>
<div class="card-subtitle">Дополнительная информация</div>
</div>
</header>
<div class="card-body">
Контент карточки
</div>
<footer class="card-actions">
<button class="btn btn-ghost">Действие</button>
</footer>
</div>
```
## Дата изменений
29 ноября 2025 г.

View File

@ -1,164 +0,0 @@
# Changelog - EUSRR Chat Module
Все значимые изменения в чат-модуле документируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
и этот проект придерживается [Semantic Versioning](https://semver.org/lang/ru/).
---
## [2.0.0] - 2026-01-13
### ✨ Добавлено
#### Умный автоскролл
- **Индикатор новых сообщений** - кнопка "N новых сообщений" при чтении истории
- **Telegram-like поведение** - автоскролл только если пользователь внизу
- **Счетчик непрочитанных** - отслеживание количества новых сообщений
- **Автоскрытие при скролле** - индикатор исчезает когда прокручиваем вниз вручную
- 18 автотестов для умного автоскролла (100% pass rate)
#### Производительность (MessageStoreV2)
- **Batch операции** - отложенные уведомления для массовых операций (3x быстрее)
- **Бинарный поиск** - O(log n) вставка в отсортированный список
- **Оптимизация ререндеров** - меньше перерисовок при загрузке истории
#### Устойчивость (MessageLoaderV2)
- **Retry логика** - автоматические повторы при сбоях сети
- **Exponential backoff** - умное увеличение задержки между попытками
- **AbortController** - отмена дублирующих запросов
- **Timeout handling** - обработка зависших запросов
#### Оптимизация (ScrollManagerV2)
- **Debounce для scroll** - 16x меньше обработок событий
- **Throttle для загрузки** - контроль частоты запросов истории
- **Улучшенный cleanup** - правильное удаление слушателей
#### Архитектура
- **Централизованная конфигурация** - `chatConfig.js` со всеми настройками
- `LOADER_CONFIG` - настройки загрузчика
- `SCROLL_CONFIG` - настройки скролла
- `AUTOSCROLL_CONFIG` - ⭐ НОВОЕ: настройки автоскролла
- `STORE_EVENTS`, `CHAT_EVENTS`, `WS_EVENTS` - константы событий
- **JSDoc типизация** - полное покрытие TypeScript-like типами
- **Модульность** - каждый компонент самостоятелен
#### Миграция
- **chatMigration.js** - утилиты для миграции со старой версии
- `migrateToV2()` - автоматическая миграция контроллера
- `ChatControllerCompat` - compatibility layer для плавной миграции
- `isV2Controller()`, `getControllerVersion()` - проверка версии
- **V2_MIGRATION_GUIDE.md** - подробное руководство по миграции
#### Тестирование
- **chatTestsV2.js** - портированные тесты для V2
- **chatTestsV2.html** - удобный UI для запуска тестов
- Покрытие: 8 сценариев умного автоскролла
### 🔄 Изменено
#### Breaking Changes
- **chat/index.js** - V2 теперь экспортируется как основная версия
```javascript
// БЫЛО:
export { ChatController } from '../controllers/chatController.js';
// СТАЛО:
export { ChatControllerV2 as ChatController } from '../controllers/chatControllerV2.js';
export { ChatController as ChatControllerLegacy } from '../controllers/chatController.js';
```
- **Приватные свойства** - префикс `_` для внутренних свойств в V2
- `newMessagesCount` → `_newMessagesCount`
- `initialized` → `_initialized`
- Решение: используйте `ChatControllerCompat` для обратной совместимости
#### API Changes
- **Опции конструктора**:
- Удалено: `lastReadTimestamp`
- Добавлено: `lastReadMessageId` (ID вместо timestamp)
- **Методы Store**:
- `addMessages()` теперь поддерживает batch режим
- Оптимизированы уведомления (одно событие вместо N)
### 🐛 Исправлено
- **Scroll watcher timing** - удален debounce для мгновенной реакции в тестах
- **Race conditions** - AbortController решает проблему дублирующих запросов
- **Memory leaks** - правильный cleanup слушателей в destroy()
- **Бесконечный скролл** - улучшена логика определения "внизу чата"
### ⚡ Производительность
- **3.3x faster** - batch загрузка 100 сообщений (50ms → 15ms)
- **16x меньше** - обработок scroll events (100/сек → 6/сек)
- **O(log n)** - вставка сообщений (вместо O(n))
- **-5.7% кода** - V2 более компактен (2145 строк vs 2274)
### 📚 Документация
- **V2_REFACTORING_EVALUATION.md** - полная оценка рефакторинга V2
- **V2_MIGRATION_GUIDE.md** - пошаговое руководство миграции
- **CHANGELOG.md** - этот файл
- Обновлены комментарии в коде - JSDoc типизация
### 🧪 Тестирование
- Портировано 18 тестов умного автоскролла
- Pass rate: 100% (18/18)
- Добавлен удобный HTML runner (chatTestsV2.html)
---
## [1.0.0] - 2025-12-XX
### Добавлено
- Базовая версия ChatController
- MessageStore для хранения сообщений
- MessageLoader для загрузки через API
- ScrollManager для управления скроллом
- MessageRenderer для рендеринга сообщений
### Функциональность
- Загрузка начальных сообщений
- Загрузка истории при скролле вверх
- WebSocket для real-time обновлений
- Оптимистичные обновления при отправке
- Базовый автоскролл
---
## Roadmap
### [2.1.0] - Планируется
- [ ] Виртуализация для очень длинных чатов (10000+ сообщений)
- [ ] Оффлайн режим с кешированием в IndexedDB
- [ ] Поиск по сообщениям чата
- [ ] Реакции на сообщения (улучшенный UX)
- [ ] Редактирование отправленных сообщений
### [3.0.0] - Будущее
- [ ] TypeScript миграция
- [ ] Web Workers для обработки больших объемов данных
- [ ] Service Worker для push-уведомлений
- [ ] E2E шифрование сообщений
---
## Ссылки
- [V2 Migration Guide](guides/V2_MIGRATION_GUIDE.md)
- [V2 Evaluation Report](../docs/reports/V2_REFACTORING_EVALUATION.md)
- [Tests](../static/js/tests/chatTestsV2.html)
- [Source Code](../static/js/)
---
**Легенда:**
- ✨ Добавлено - новая функциональность
- 🔄 Изменено - изменения в существующей функциональности
- 🗑️ Удалено - удаленная функциональность
- 🐛 Исправлено - исправления багов
- 🔒 Безопасность - закрытие уязвимостей
- ⚡ Производительность - улучшения производительности
- 📚 Документация - только документация
- 🧪 Тестирование - добавление/изменение тестов

View File

@ -1,275 +0,0 @@
# Улучшения модала создания чатов
## Что добавлено
### 1. Новые типы чатов
- **Чат отдела** (`department`) - автоматически включает всех сотрудников отдела
- **Глобальный чат** (`global`) - для всей компании
### 2. Новые поля
#### Аватар чата
- Загрузка изображения (JPG/PNG/GIF, до 5 МБ)
- Предпросмотр перед загрузкой
- **Автоматическое сжатие** до 512x512px (до 500KB)
- Удаление старых файлов при замене
#### Выбор отдела
- Для типа "Чат отдела"
- Dropdown со всеми отделами
- Автодобавление всех сотрудников отдела
#### Включить всех сотрудников
- Для типов: `announcement`, `global`, `channel`
- Автодобавление всех активных сотрудников компании
- Ограниченная отправка для `channel` и `announcement`
#### Основной чат
- Для типов: `global`, `department`
- Только 1 основной глобальный чат
- Только 1 основной чат на отдел
- Валидация на уровне API
### 3. Улучшенный UI
#### Типы чатов с подсказками
```
Личный: приватный чат с 1 сотрудником
Группа: чат с несколькими участниками
Отдел: автоматически включает всех сотрудников отдела
Канал: для объявлений с ограниченной отправкой
Объявления: односторонняя рассылка
Глобальный: общий чат компании (может быть только 1 основной)
```
#### Динамическое переключение полей
- **Private**: только выбор участника
- **Group**: название + описание + аватар + участники
- **Department**: название + описание + аватар + отдел + "основной"
- **Global**: название + описание + аватар + "включить всех" + "основной"
- **Channel/Announcement**: название + описание + аватар + "включить всех"
### 4. API изменения
#### FormData support
```javascript
// Теперь поддерживает загрузку файлов
const formData = new FormData();
formData.append('type', 'group');
formData.append('name', 'Команда разработки');
formData.append('avatar', file);
formData.append('participant_ids', [1, 2, 3]);
```
#### Новые параметры API
- `avatar` (file) - файл аватара
- `department_id` (int) - ID отдела для `type=department`
- `include_all_employees` (bool) - включить всех сотрудников
- `is_main` (bool) - сделать основным чатом
#### Валидация
- ✅ Проверка типа чата
- ✅ Обязательное название (кроме private)
- ✅ Обязательный отдел для `department`
- ✅ Уникальность основного глобального чата
- ✅ Уникальность основного чата отдела
### 5. Автоматическое сжатие аватаров
#### Signal `compress_and_cleanup_chat_avatar`
```python
@receiver(pre_save, sender='communications.Chat')
def compress_and_cleanup_chat_avatar(sender, instance, **kwargs):
# 1. Сжимает новый аватар до 512x512px (макс 500KB)
# 2. Удаляет старый файл при замене
# 3. Использует LANCZOS для высокого качества
```
#### Параметры сжатия
- Размер: 512x512px (оптимально для отображения)
- Формат: JPEG (универсальный)
- Качество: 50-85 (бинарный поиск оптимального)
- Максимум: 500KB
### 6. Автодобавление участников
#### Для `include_all_employees=True`
```python
# Добавляются все активные сотрудники
all_employees = Employee.objects.filter(is_active=True)
# Права зависят от типа чата:
# - channel/announcement: can_send_messages=False
# - остальные: can_send_messages=True
```
#### Для `type=department`
```python
# Добавляются сотрудники отдела
dept_employees = EmployeeDepartment.objects.filter(
department=department,
is_active=True
)
# Все получают can_send_messages=True
```
## Примеры использования
### Создать групповой чат с аватаром
```javascript
const formData = new FormData();
formData.append('type', 'group');
formData.append('name', 'DevOps Team');
formData.append('description', 'Обсуждение инфраструктуры');
formData.append('avatar', avatarFile);
formData.append('participant_ids', 5);
formData.append('participant_ids', 12);
formData.append('participant_ids', 23);
fetch('/communications/api/chat/create/', {
method: 'POST',
headers: { 'X-CSRFToken': csrftoken },
body: formData
});
```
### Создать основной чат отдела
```javascript
const formData = new FormData();
formData.append('type', 'department');
formData.append('name', 'Отдел разработки');
formData.append('department_id', 3);
formData.append('is_main', 'true');
fetch('/communications/api/chat/create/', {
method: 'POST',
headers: { 'X-CSRFToken': csrftoken },
body: formData
});
```
### Создать канал с аватаром для всех
```javascript
const formData = new FormData();
formData.append('type', 'channel');
formData.append('name', 'Новости компании');
formData.append('description', 'Официальные объявления');
formData.append('avatar', avatarFile);
formData.append('include_all_employees', 'true');
fetch('/communications/api/chat/create/', {
method: 'POST',
headers: { 'X-CSRFToken': csrftoken },
body: formData
});
```
## Технические детали
### Структура модели Chat
```python
class Chat(models.Model):
type = CharField(choices=[...])
name = CharField(max_length=255)
description = TextField()
avatar = ImageField(upload_to='chat_avatars/%Y/%m/') # NEW
created_by = ForeignKey(Employee)
department = ForeignKey(Department) # NEW
include_all_employees = BooleanField(default=False) # NEW
is_main = BooleanField(default=False) # NEW
participants = ManyToManyField(Employee)
created_at = DateTimeField(auto_now_add=True)
```
### Constraints
```python
# Только 1 основной глобальный чат
UniqueConstraint(
fields=["type"],
condition=Q(is_main=True, type="global"),
name="unique_main_global_chat"
)
# Только 1 основной чат на отдел
UniqueConstraint(
fields=["type", "department"],
condition=Q(is_main=True, type="department"),
name="unique_main_department_chat"
)
```
### Путь хранения аватаров
```
media/
chat_avatars/
2024/
11/
chat_avatar_abc123.jpg (сжато до 512x512, <500KB)
```
## Совместимость
### Backward compatibility
- ✅ Старые чаты без аватаров работают как прежде
- ✅ API поддерживает JSON (legacy) и FormData (новый)
- ✅ Все новые поля опциональные (кроме валидируемых случаев)
### Frontend
- Модал: Bootstrap 5
- Preview: FileReader API
- Validation: Client-side + Server-side
- CSRF: Из cookie или hidden input
## Производительность
### Оптимизации
- ✅ Сжатие аватаров на лету (JPEG quality 50-85)
- ✅ Удаление старых файлов (нет дубликатов)
- ✅ Batch добавление участников (без N+1 queries)
- ✅ `select_related('employee')` для отделов
### Размеры файлов
- **До**: 8.3 MB PNG, 800+ KB JPEG
- **После**: <500 KB JPEG, 512x512px
- **Экономия**: ~90-95% места на диске
## Тестирование
### Проверить создание чатов
1. Личный чат с 1 участником ✅
2. Групповой чат с аватаром ✅
3. Чат отдела с автодобавлением ✅
4. Глобальный основной чат (только 1) ✅
5. Канал для всех с ограниченной отправкой ✅
### Проверить сжатие
1. Загрузить PNG 5MB → сжато до <500KB JPEG ✅
2. Заменить аватар → старый файл удален ✅
3. Проверить preview в модале ✅
### Проверить валидацию
1. Попытка создать 2 основных глобальных чата → ошибка ✅
2. Чат отдела без выбора отдела → ошибка ✅
3. Файл >5MB → отклонен на клиенте ✅
## Changelog
### v1.0 (30 ноября 2024)
- ✅ Добавлены поля: avatar, department, include_all_employees, is_main
- ✅ Реализовано автосжатие аватаров (512x512, <500KB)
- ✅ Добавлены типы: department, global
- ✅ Динамические поля в модале
- ✅ API поддержка FormData + файлов
- ✅ Валидация основных чатов
- ✅ Автодобавление участников по правилам
- ✅ Signal для сжатия и cleanup
## TODO
- [ ] Crop аватара перед загрузкой (client-side)
- [ ] Batch upload нескольких аватаров
- [ ] История изменений аватара чата
- [ ] Права на изменение аватара (только owner/admin)
- [ ] Предпросмотр аватара в списке чатов
- [ ] Поиск чатов по названию в модале

View File

@ -1,92 +0,0 @@
# Chat Handler Refactoring - Quick Start
> **Статус**: 🔴 Требуется отдельная сессия (2-3 часа)
> **Приоритет**: Средний (после завершения можно переходить к Фазе 5)
> **Детальный план**: [CHAT_REFACTORING_SESSION.md](./CHAT_REFACTORING_SESSION.md)
## Что нужно сделать
Извлечь **521 строку** встроенного JavaScript из `templates/base.html` в **6 переиспользуемых модулей**.
## Структура
### Текущее состояние (base.html)
```
📄 base.html
├── Avatar Map (6 строк) - инициализация карты аватаров
├── Chat Detail Handler (186 строк) - автоскролл, textarea, отметка прочитанного
├── WebSocket Chat (161 строка) - реалтайм сообщения
├── Chat List Filter (42 строки) - поиск и фильтрация
├── Chat List Realtime (62 строки) - обновление списка через WS
└── Global Badge Updater (60 строк) - счетчик непрочитанных
```
### Целевая структура
```
📁 static/js/components/chat/
├── chatAvatars.js (простой, 5 минут)
├── chatListFilter.js (простой, 15 минут)
├── chatBadgeUpdater.js (средний, 30 минут)
├── chatListRealtime.js (средний, 30 минут)
├── chatDetailHandler.js (сложный, 60 минут) ⚠️
└── chatWebSocket.js (сложный, 60 минут) ⚠️
```
## Порядок работы
1. **chatAvatars.js** - быстрая победа ✅
2. **chatListFilter.js** - независимый модуль
3. **chatBadgeUpdater.js** - обновление бейджа
4. **chatListRealtime.js** - WebSocket для списка
5. **chatDetailHandler.js** - логика детального чата
6. **chatWebSocket.js** - WebSocket для сообщений
## Зависимости
```
chatAvatars.js
↓
chatWebSocket.js ← chatDetailHandler.js
↓ ↓
chatListRealtime.js → chatBadgeUpdater.js
↓
chatListFilter.js
```
## Результат
**До**: 521 строка встроенного JS
**После**: ~40 строк инициализации + 6 модулей
**Сокращение**: 92% 🎉
## Начало работы
1. Прочитать [CHAT_REFACTORING_SESSION.md](./CHAT_REFACTORING_SESSION.md)
2. Создать директорию `static/js/components/chat/`
3. Начать с простых модулей (chatAvatars, chatListFilter)
4. Тестировать каждый модуль после создания
5. Обновить base.html в конце
## Чеклист тестирования
- [ ] Открытие детального чата → автоскролл работает
- [ ] Отправка сообщения → появляется в реальном времени
- [ ] Получение сообщения → обновляется счетчик
- [ ] Поиск в списке чатов → фильтрация работает
- [ ] Новое сообщение → список пересортируется
- [ ] Прочтение чата → бейдж обновляется
## Потенциальные проблемы
⚠️ **Порядок инициализации**: chatWebSocket зависит от chatDetailHandler
✅ **Решение**: передавать chatDetail как параметр
⚠️ **Django context**: chatId, meId нужны из шаблона
✅ **Решение**: передавать через параметры инициализации
⚠️ **Обратная совместимость**: `window.__CHAT_MARK_READ__`
✅ **Решение**: экспортировать в window
---
**См. также**: [Полный план рефакторинга](./CHAT_REFACTORING_SESSION.md)

View File

@ -1,471 +0,0 @@
# Chat Handler Refactoring Session
## Цель
Извлечь все встроенные скрипты чата из `templates/base.html` в отдельные модули.
## Анализ текущего состояния
### Структура chat скриптов в base.html
**Всего: 521 строка JavaScript (строки 95-624)**
Разделяется на 6 логических блоков:
#### 1. Avatar Map (6 строк, 96-101)
```javascript
window.__CHAT_AVATARS__ = {
"{{ user.id }}": "{{ user.avatar.url }}"
};
```
**Зависимости**: Django template context (user)
**Использование**: WebSocket chat для отображения аватаров
---
#### 2. Chat Detail Handler (186 строк, 104-289)
**Функциональность**:
- Автоскролл к последнему сообщению
- Авто-рост textarea при вводе
- Кнопка "вниз" для скролла
- Разделитель "Новые сообщения"
- Отметка прочитанных (localStorage + API)
- IntersectionObserver для автоматической отметки
**Ключевые элементы**:
```javascript
const box = document.getElementById('chatScroll');
const ta = document.getElementById('id_content');
const form = document.getElementById('chatForm');
const btn = document.getElementById('scrollBtn');
```
**API endpoints**:
- `POST /communications/chat/{id}/mark-read/` - отметка прочитанных
**События**:
- `chat:read` - CustomEvent для обновления глобального счетчика
**Экспортирует**:
```javascript
window.__CHAT_MARK_READ__ = {
markRead,
observeLastForeign,
autoscroll,
atBottom
};
```
---
#### 3. WebSocket Chat (161 строк, 291-451)
**Функциональность**:
- WebSocket соединение `/ws/chat/{id}/`
- Отправка сообщений
- Получение новых сообщений в реальном времени
- Индикация "печатает..."
- Рендеринг сообщений в DOM
**Зависимости**:
- `window.__CHAT_AVATARS__` - карта аватаров
- `window.__CHAT_MARK_READ__` - методы из блока #2
- `window.esc()` - из stringUtils.js
- Django context: `{{ chat.id }}`, `{{ user.id }}`
**HTML шаблоны**:
- Создает `.msg` элементы динамически
- Создает `.day-divider` разделители
---
#### 4. Chat List Filter (42 строки, 454-495)
**Функциональность**:
- Поиск по названию чата
- Фильтр по типу (all/global/department/private)
- Скрытие пустых секций
**Ключевые элементы**:
```javascript
const search = document.getElementById('chatSearch');
const labels = document.querySelectorAll('[data-filter]');
const sections = {
global: '.chat-section[data-sec="global"]',
department: '.chat-section[data-sec="department"]',
private: '.chat-section[data-sec="private"]'
};
```
**Зависимости**:
- `window.norm()` - из stringUtils.js
- HTML структура списка чатов
---
#### 5. Chat List Realtime (62 строки, 498-559)
**Функциональность**:
- WebSocket соединение `/ws/chats/`
- Обновление preview/времени/счетчика при новом сообщении
- Пересортировка списка по времени последнего сообщения
**Ключевые элементы**:
```javascript
const rows = document.querySelectorAll('.chat-row');
// Атрибуты: data-chat-id, data-last-ts, data-type
```
**Зависимости**:
- Django context: `{{ user.id }}`
- HTML структура `.chat-row`
---
#### 6. Global Badge Updater (60 строк, 560-619)
**Функциональность**:
- Обновление счетчика непрочитанных в сайдбаре
- Карта непрочитанных по всем чатам
- Подписка на WebSocket `/ws/chats/`
- Обработка события `chat:read`
**Ключевые элементы**:
```javascript
const badge = document.getElementById('sidebarChatBadge');
const chatUnread = {}; // карта непрочитанных
```
**События**:
- Слушает: `chat:read` (из блока #2)
- Подписка на WebSocket `/ws/chats/`
---
## План рефакторинга
### Шаг 1: Подготовка
- [x] Анализ зависимостей между блоками
- [ ] Определение публичного API каждого модуля
- [ ] Создание схемы взаимодействия
### Шаг 2: Создание модулей
#### 2.1. `chatAvatars.js` (простой)
```javascript
export function initChatAvatars(userId, avatarUrl) {
window.__CHAT_AVATARS__ = window.__CHAT_AVATARS__ || {};
if (userId && avatarUrl) {
window.__CHAT_AVATARS__[userId] = avatarUrl;
}
return window.__CHAT_AVATARS__;
}
```
#### 2.2. `chatDetailHandler.js` (сложный, 186 строк)
**Экспорты**:
```javascript
export function initChatDetailHandler(options = {}) {
// ... инициализация
return {
markRead,
observeLastForeign,
autoscroll,
atBottom,
destroy
};
}
```
**Опции**:
```javascript
{
scrollContainerId: 'chatScroll',
textareaId: 'id_content',
formId: 'chatForm',
scrollBtnId: 'scrollBtn',
chatId: null,
meId: null,
markReadUrl: null // или авто-определение
}
```
#### 2.3. `chatWebSocket.js` (сложный, 161 строка)
**Экспорты**:
```javascript
export function initChatWebSocket(options = {}) {
// ... WebSocket логика
return {
send,
close,
reconnect,
ws // прямой доступ к WebSocket
};
}
```
**Опции**:
```javascript
{
chatId: null, // REQUIRED
meId: null, // REQUIRED
scrollContainerId: 'chatScroll',
formId: 'chatForm',
textareaId: 'id_content',
typingIndicatorId: 'typing',
avatars: window.__CHAT_AVATARS__,
markReadHandler: window.__CHAT_MARK_READ__ // или передать явно
}
```
#### 2.4. `chatListFilter.js` (средний, 42 строки)
**Экспорты**:
```javascript
export function initChatListFilter(options = {}) {
return {
applyFilter,
setActiveType,
destroy
};
}
```
#### 2.5. `chatListRealtime.js` (средний, 62 строки)
**Экспорты**:
```javascript
export function initChatListRealtime(options = {}) {
return {
ws,
disconnect,
reconnect
};
}
```
#### 2.6. `chatBadgeUpdater.js` (средний, 60 строк)
**Экспорты**:
```javascript
export function initChatBadgeUpdater(options = {}) {
return {
setBadge,
recompute,
getChatUnread,
destroy
};
}
```
### Шаг 3: Интеграция в base.html
Заменить все 6 блоков на:
```django
{% load static %}
{# Chat functionality - только для авторизованных #}
{% if user.is_authenticated %}
<script type="module">
import { initChatAvatars } from '{% static "js/components/chat/chatAvatars.js" %}';
import { initChatDetailHandler } from '{% static "js/components/chat/chatDetailHandler.js" %}';
import { initChatWebSocket } from '{% static "js/components/chat/chatWebSocket.js" %}';
import { initChatListFilter } from '{% static "js/components/chat/chatListFilter.js" %}';
import { initChatListRealtime } from '{% static "js/components/chat/chatListRealtime.js" %}';
import { initChatBadgeUpdater } from '{% static "js/components/chat/chatBadgeUpdater.js" %}';
document.addEventListener('DOMContentLoaded', () => {
// 1. Инициализация карты аватаров
const avatars = initChatAvatars(
"{{ user.id }}",
{% if user.avatar %}"{{ user.avatar.url }}"{% else %}""{% endif %}
);
// 2. Chat detail (если мы на странице детального чата)
{% if chat %}
const chatDetail = initChatDetailHandler({
chatId: {{ chat.id }},
meId: {{ user.id }},
markReadUrl: "{% url 'communications:chat_mark_read' chat.pk %}"
});
// 3. WebSocket для сообщений
const chatWS = initChatWebSocket({
chatId: {{ chat.id }},
meId: {{ user.id }},
avatars: avatars,
markReadHandler: chatDetail
});
{% endif %}
// 4. Фильтр списка чатов (если есть список)
const listFilter = initChatListFilter();
// 5. Realtime обновление списка
const listRealtime = initChatListRealtime({
meId: {{ user.id }}
});
// 6. Глобальный счетчик непрочитанных
const badgeUpdater = initChatBadgeUpdater({
meId: {{ user.id }}
});
// Экспорт в window для отладки
if (chatDetail) window.chatDetail = chatDetail;
if (chatWS) window.chatWS = chatWS;
if (listFilter) window.chatListFilter = listFilter;
if (listRealtime) window.chatListRealtime = listRealtime;
if (badgeUpdater) window.chatBadgeUpdater = badgeUpdater;
});
</script>
{% endif %}
```
### Шаг 4: Тестирование
Проверить каждый сценарий:
- [ ] Открытие страницы детального чата
- [ ] Автоскролл к последнему сообщению
- [ ] Разделитель "Новые сообщения" появляется
- [ ] Textarea автоматически растет при вводе
- [ ] Ctrl+Enter отправляет сообщение
- [ ] Кнопка "вниз" появляется при скролле вверх
- [ ] Отправка сообщений
- [ ] Сообщение отправляется через WebSocket
- [ ] Сообщение появляется в чате
- [ ] Аватар и имя отображаются правильно
- [ ] Время форматируется корректно
- [ ] Получение сообщений
- [ ] Новые сообщения приходят в реальном времени
- [ ] Автоскролл работает (если в конце списка)
- [ ] Индикация "печатает..." работает
- [ ] Отметка прочитанных срабатывает
- [ ] Список чатов
- [ ] Поиск работает
- [ ] Фильтр по типу работает
- [ ] Пустые секции скрываются
- [ ] Preview/время обновляются при новом сообщении
- [ ] Счетчик непрочитанных обновляется
- [ ] Сортировка по времени работает
- [ ] Глобальный счетчик
- [ ] Бейдж обновляется при новых сообщениях
- [ ] Бейдж обнуляется при прочтении
- [ ] События `chat:read` обрабатываются
### Шаг 5: Документация
- [ ] JSDoc для всех публичных функций
- [ ] README.md в `static/js/components/chat/`
- [ ] Примеры использования
- [ ] Описание событий и API
---
## Технические детали
### Зависимости между модулями
```
chatAvatars.js
↓
chatWebSocket.js ← chatDetailHandler.js
↓ ↓
chatListRealtime.js → chatBadgeUpdater.js
↓
chatListFilter.js
```
### Глобальные объекты (для обратной совместимости)
```javascript
window.__CHAT_AVATARS__ = {}; // карта аватаров
window.__CHAT_MARK_READ__ = {}; // методы chatDetailHandler
```
### События
**Генерируются**:
- `chat:read` - CustomEvent при отметке прочитанного
```javascript
window.dispatchEvent(new CustomEvent('chat:read', {
detail: { chatId: '123' }
}));
```
**Обрабатываются**:
- `chat:read` - в chatBadgeUpdater для обнуления счетчика
### WebSocket endpoints
1. `/ws/chat/{id}/` - детальный чат (сообщения)
- Отправка: `{ content: "текст" }` или `{ type: 'typing' }`
- Получение: `{ type: 'message', payload: {...} }`
2. `/ws/chats/` - список чатов (обновления)
- Получение: `{ type: 'list_update', chat_id: 123, message: {...} }`
### API endpoints
- `POST /communications/chat/{id}/mark-read/`
- Body: `upto_ts=1234567890`
- Response: 200 OK
---
## Оценка сложности
**Время**: ~2-3 часа чистого кодинга + тестирование
**Приоритет модулей** (по сложности):
1. ✅ `chatAvatars.js` - тривиальный (5 минут)
2. ⚠️ `chatListFilter.js` - простой (15 минут)
3. ⚠️ `chatBadgeUpdater.js` - средний (30 минут)
4. ⚠️ `chatListRealtime.js` - средний (30 минут)
5. 🔴 `chatDetailHandler.js` - сложный (60 минут)
6. 🔴 `chatWebSocket.js` - сложный (60 минут)
**Рекомендуемый порядок**:
1. chatAvatars (быстрая победа)
2. chatListFilter (независимый)
3. chatBadgeUpdater (зависит от chatListRealtime событий)
4. chatListRealtime (нужен для badgeUpdater)
5. chatDetailHandler (нужен для WebSocket)
6. chatWebSocket (последний, самый сложный)
---
## Потенциальные проблемы
1. **Порядок инициализации**: chatWebSocket зависит от chatDetailHandler
- Решение: передавать chatDetail как параметр в chatWebSocket
2. **Django template context**: chatId, meId нужны из контекста
- Решение: передавать через параметры при инициализации
3. **HTML структура**: модули зависят от конкретных селекторов
- Решение: делать проверки `if (!element) return null;`
4. **WebSocket reconnect**: при потере соединения нужно перезагрузить страницу
- Решение: оставить `setTimeout(() => location.reload(), 2000)`
5. **Обратная совместимость**: другие скрипты могут зависеть от `window.__CHAT_MARK_READ__`
- Решение: экспортировать в window для совместимости
---
## Результат
**До рефакторинга**:
- base.html: 521 строка встроенного JS
- 0 переиспользуемых модулей
- Нет документации
- Невозможно тестировать изолированно
**После рефакторинга**:
- base.html: ~40 строк инициализации
- 6 переиспользуемых модулей (~600 строк с документацией)
- JSDoc для всех публичных функций
- Возможность unit-тестирования
- Четкое разделение ответственности
**Сокращение**: 521 → 40 строк = **92% reduction** 🎉

View File

@ -1,792 +0,0 @@
# 📘 Руководство по JavaScript модулям EUSRR
**Версия**: 1.0
**Дата**: 4 ноября 2025 г.
---
## 📚 Содержание
1. [Обзор архитектуры](#обзор-архитектуры)
2. [Утилиты (utils)](#утилиты)
3. [Компоненты (components)](#компоненты)
4. [Примеры использования](#примеры-использования)
5. [Best Practices](#best-practices)
---
## 🏗️ Обзор архитектуры
### Структура файлов
```
static/js/
├── utils/ # Утилитарные функции
│ ├── stringUtils.js # Работа со строками
│ ├── timing.js # Debounce, throttle, delay
│ └── scroll.js # Управление скроллом
└── components/ # Переиспользуемые компоненты
├── calendarWidget.js # Виджет календаря
├── deptRoleEditor.js # Редактор ролей отделов
├── employeeFormHandler.js # Форма сотрудника
├── employeeGroupsManager.js # Группы сотрудника
├── groupPickers.js # Выбор групп должностей
├── headPicker.js # Autocomplete руководителя
├── likeHandler.js # Обработчик лайков
├── listFilter.js # Фильтрация списков
├── navbarHeight.js # CSS переменная navbar
├── positionManager.js # Управление должностями
├── roleManager.js # Управление ролями
├── skillsManager.js # Управление навыками
├── teamWheel.js # Колесо команды
├── textareaAutogrow.js # Автоувеличение textarea
└── index.js # Экспорт всех компонентов
```
---
## 🛠️ Утилиты
### `stringUtils.js`
Утилиты для работы со строками и HTML.
#### `esc(s: string): string`
Экранирует HTML-символы для безопасного вывода.
```javascript
import { esc } from '{% static "js/utils/stringUtils.js" %}';
const userInput = '<script>alert("xss")</script>';
const safe = esc(userInput);
// => '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
```
#### `norm(s: string): string`
Нормализует строку для поиска (lowercase, trim).
```javascript
import { norm } from '{% static "js/utils/stringUtils.js" %}';
const search = norm(' Hello World ');
// => 'hello world'
```
#### `escAttr(s: string): string`
Экранирует строку для использования в атрибутах.
```javascript
const attr = escAttr('some "quoted" text');
// => 'some &quot;quoted&quot; text'
```
#### `truncate(s: string, maxLen: number): string`
Обрезает строку до указанной длины с добавлением "...".
```javascript
const short = truncate('Very long text here', 10);
// => 'Very lon...'
```
#### `getInitials(name: string): string`
Получает инициалы из имени.
```javascript
const initials = getInitials('Иван Петрович Сидоров');
// => 'ИПС'
```
---
### `timing.js`
Утилиты для управления временем выполнения.
#### `debounce(fn: Function, ms: number): Function`
Откладывает выполнение функции до прекращения вызовов.
```javascript
import { debounce } from '{% static "js/utils/timing.js" %}';
const search = debounce((query) => {
console.log('Searching for:', query);
}, 300);
input.addEventListener('input', (e) => search(e.target.value));
```
#### `throttle(fn: Function, ms: number): Function`
Ограничивает частоту вызова функции.
```javascript
import { throttle } from '{% static "js/utils/timing.js" %}';
const onScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.addEventListener('scroll', onScroll);
```
#### `delay(ms: number): Promise<void>`
Промисифицированная задержка.
```javascript
import { delay } from '{% static "js/utils/timing.js" %}';
async function animateSequence() {
element.classList.add('step1');
await delay(300);
element.classList.add('step2');
await delay(300);
element.classList.add('complete');
}
```
---
### `scroll.js`
Утилиты для управления скроллом.
#### `smoothScrollTo(element: HTMLElement, options?: Object): void`
Плавно прокручивает к элементу.
```javascript
import { smoothScrollTo } from '{% static "js/utils/scroll.js" %}';
const target = document.getElementById('section');
smoothScrollTo(target, {
offset: 80, // Отступ сверху
duration: 500 // Длительность анимации
});
```
#### `scrollToTop(duration: number = 300): void`
Плавно прокручивает страницу наверх.
```javascript
button.addEventListener('click', () => scrollToTop(500));
```
#### `isElementInViewport(el: HTMLElement): boolean`
Проверяет, виден ли элемент в окне просмотра.
```javascript
if (isElementInViewport(element)) {
element.classList.add('animate');
}
```
---
## 🎨 Компоненты
### `navbarHeight.js`
Устанавливает CSS-переменную с высотой навбара.
#### API
```javascript
import { initNavbarHeight } from '{% static "js/components/navbarHeight.js" %}';
const navbarHeight = initNavbarHeight();
// Автоматически создаёт CSS переменную --navbar-height
```
**Использование в CSS:**
```css
.content {
margin-top: var(--navbar-height);
}
```
---
### `likeHandler.js`
Глобальный обработчик лайков для постов и комментариев.
#### API
```javascript
import { initLikeHandler } from '{% static "js/components/likeHandler.js" %}';
const likeHandler = initLikeHandler({
postUrl: '/api/v1/feed/posts/{id}/like/',
commentUrl: '/api/v1/feed/comments/{id}/like/'
});
```
#### HTML разметка
```django
<button class="like-btn"
data-post-id="{{ post.id }}"
data-liked="{% if post.i_liked %}1{% endif %}">
<i class="bi-heart{% if post.i_liked %}-fill{% endif %}"></i>
<span class="like-count">{{ post.likes_count }}</span>
</button>
```
#### Возвращаемый API
- `refresh()` - переинициализация обработчиков
---
### `textareaAutogrow.js`
Автоматически увеличивает высоту textarea при вводе текста.
#### API
```javascript
import { initTextareaAutogrow } from '{% static "js/components/textareaAutogrow.js" %}';
const autogrow = initTextareaAutogrow({
selector: '[data-autogrow]',
minHeight: 40,
maxHeight: 300
});
```
#### HTML разметка
```html
<textarea data-autogrow
data-max-height="200"
placeholder="Введите текст..."></textarea>
```
#### Возвращаемый API
- `refresh()` - переинициализация для новых элементов
- `destroy()` - отключение обработчиков
---
### `employeeFormHandler.js`
Управляет формой сотрудника (валидация, изменения, AJAX).
#### API
```javascript
import { initEmployeeForm } from '{% static "js/components/employeeFormHandler.js" %}';
const employeeForm = initEmployeeForm();
```
#### Возможности
- ✅ Отслеживание изменений полей
- ✅ Валидация перед отправкой
- ✅ Предупреждение о несохранённых изменениях
- ✅ AJAX отправка формы
- ✅ Обработка ошибок
#### Возвращаемый API
- `hasChanges()` - проверка наличия изменений
- `resetTracking()` - сброс отслеживания
- `validateForm()` - валидация формы
- `isDirty` - флаг изменений
---
### `positionManager.js`
Управляет CRUD операциями с должностями через модальные окна.
#### API
```javascript
import { initPositionManager } from '{% static "js/components/positionManager.js" %}';
const positionMgr = initPositionManager();
```
#### HTML требования
- `#positionBlock[data-update-url-tpl]` - шаблон URL обновления
- `#positionBlock[data-detail-url-tpl]` - шаблон URL получения
- `#positionBlock[data-create-url]` - URL создания
- Модалы: `#posEditModal`, `#posCreateModal`
#### Возвращаемый API
- `refresh()` - переинициализация
- `openCreateModal()` - открыть модал создания
- `openEditModal(positionId)` - открыть модал редактирования
---
### `groupPickers.js`
Пикеры для выбора групп должностей (создание и редактирование).
#### API
```javascript
import {
initPositionGroupPicker,
initPositionEditGroupPicker
} from '{% static "js/components/groupPickers.js" %}';
// Для формы создания должности
const createPicker = initPositionGroupPicker();
// Для формы редактирования должности
const editPicker = initPositionEditGroupPicker();
```
#### Возможности
- ✅ Выбор нескольких групп
- ✅ Отображение чипов выбранных групп
- ✅ Поиск по группам
- ✅ Синхронизация с hidden input'ами
---
### `employeeGroupsManager.js`
Управляет персональными группами прав сотрудника.
#### API
```javascript
import { initEmployeeGroupsManager } from '{% static "js/components/employeeGroupsManager.js" %}';
const groupsMgr = initEmployeeGroupsManager();
```
#### HTML требования
- `#empGroupsAssignBtn` - кнопка назначения групп
- `#empGroupsList` - список текущих групп
- `#empGroupsModal` - модал выбора групп
#### Возвращаемый API
- `refresh()` - обновление списка
- `assignGroups(groupIds)` - назначить группы
- `removeGroup(groupId)` - удалить группу
---
### `skillsManager.js`
Управляет навыками сотрудника (добавление/удаление через AJAX).
#### API
```javascript
import { initSkillsManager } from '{% static "js/components/skillsManager.js" %}';
const skillsManager = initSkillsManager({
blockId: 'skillsBlock',
formId: 'skillAddForm',
collapseId: 'skillsForm',
removeUrl: '/employees/1/skills/remove/'
});
```
#### Возможности
- ✅ AJAX добавление навыков
- ✅ AJAX удаление по клику на чип
- ✅ Анимация добавления/удаления
- ✅ Валидация дубликатов
#### Возвращаемый API
- `addSkill(name)` - добавить навык (возвращает Promise)
- `refresh()` - переинициализация
---
### `deptRoleEditor.js`
Редактор роли сотрудника в отделе с валидацией изменений.
#### API
```javascript
import { initDeptRoleEditor } from '{% static "js/components/deptRoleEditor.js" %}';
const deptRoleEditor = initDeptRoleEditor();
```
#### HTML разметка
```html
<div data-dept-role-editor>
<select name="role">...</select>
<button data-role-ok disabled>OK</button>
</div>
```
#### Возможности
- ✅ Активация кнопки OK только при изменении
- ✅ Инициализация tooltip'ов
- ✅ Множественные экземпляры на странице
#### Возвращаемый API
- `instances` - массив всех редакторов
- `refresh()` - переинициализация
---
### `headPicker.js`
Autocomplete для выбора руководителя отдела по имени или email.
#### API
```javascript
import { initHeadPicker } from '{% static "js/components/headPicker.js" %}';
const headPicker = initHeadPicker({
selector: '[data-head-picker]'
});
```
#### HTML требования
```html
<div data-head-picker data-choices='[{"id":1,"name":"Иван","email":"ivan@mail.ru"}]'>
<input type="text" class="form-control" placeholder="Имя или email">
<input type="hidden" name="head_id">
<div class="dropdown-menu"></div>
</div>
```
#### Возможности
- ✅ Поиск по имени и email
- ✅ Валидация выбора при submit
- ✅ Автозаполнение при загрузке
- ✅ Клавиатурная навигация (Enter, Escape)
---
### `roleManager.js`
Управление ролями и правами отделов (создание, редактирование, удаление).
#### API
```javascript
import { initRoleManager } from '{% static "js/components/roleManager.js" %}';
const roleManager = initRoleManager();
```
#### Возможности
- ✅ Autocomplete выбора роли
- ✅ Управление правами через чипы
- ✅ Создание новой роли
- ✅ Редактирование существующей роли
- ✅ Удаление роли с подтверждением
- ✅ Модалы с выбором прав (permissions)
#### Возвращаемый API
- `refresh()` - переинициализация
- `getSelectedRole()` - получить выбранную роль
- `getEditPermissions()` - получить выбранные права редактирования
- `getCreatePermissions()` - получить выбранные права создания
---
### `calendarWidget.js`
Полнофункциональный виджет календаря с FullCalendar.js.
#### API
```javascript
import { initCalendarWidget } from '{% static "js/components/calendarWidget.js" %}';
const widget = initCalendarWidget();
```
#### Возможности
- ✅ Отображение событий
- ✅ Создание новых событий
- ✅ Редактирование событий
- ✅ Drag & drop событий
- ✅ Интеграция с модалами
- ✅ AJAX сохранение
---
### `teamWheel.js`
Анимированное колесо команды отдела с автоскроллом.
#### API
```javascript
import { initTeamWheel } from '{% static "js/components/teamWheel.js" %}';
const wheel = initTeamWheel({
wheelId: 'teamWheel-1',
dataSourceId: 'teamData-1',
fallback: {
avatar: '/static/img/default-avatar.png',
name: 'Руководитель',
role: 'Руководитель отдела'
}
});
```
#### Возвращаемый API
- `start()` - запустить автоскролл
- `stop()` - остановить автоскролл
- `next()` - следующий участник
- `prev()` - предыдущий участник
---
### `listFilter.js`
Универсальный компонент фильтрации списков.
#### API
```javascript
import { ListFilter, createDataAttrMatcher } from '{% static "js/components/listFilter.js" %}';
new ListFilter({
listSelector: '#empList',
itemSelector: '.emp-row',
searchInputSelector: '#empFilter',
clearButtonSelector: '.ios-search-clear',
matchFn: createDataAttrMatcher(['name', 'depts']),
debounceMs: 300
});
```
#### Возможности
- ✅ Debounced поиск
- ✅ Кастомная функция сопоставления
- ✅ Подсчёт результатов
- ✅ Кнопка очистки
- ✅ Клавиатурные события
---
## 💡 Примеры использования
### Пример 1: Базовая инициализация в шаблоне
```django
{% load static %}
<script type="module">
import { initEmployeeForm } from '{% static "js/components/employeeFormHandler.js" %}';
document.addEventListener('DOMContentLoaded', () => {
const form = initEmployeeForm();
if (form) {
console.log('Форма инициализирована');
window.employeeForm = form; // Для отладки
}
});
</script>
```
### Пример 2: Множественная инициализация
```django
<script type="module">
import { initSkillsManager } from '{% static "js/components/skillsManager.js" %}';
import { initDeptRoleEditor } from '{% static "js/components/deptRoleEditor.js" %}';
import { smoothScrollTo } from '{% static "js/utils/scroll.js" %}';
document.addEventListener('DOMContentLoaded', () => {
const skills = initSkillsManager({ blockId: 'skillsBlock' });
const roles = initDeptRoleEditor();
// Скролл к форме на мобильных
const editWrap = document.getElementById('editFormWrap');
if (editWrap) {
editWrap.addEventListener('shown.bs.collapse', () => {
if (window.innerWidth < 768) {
smoothScrollTo(editWrap, { offset: 80 });
}
});
}
});
</script>
```
### Пример 3: Работа с API модуля
```javascript
import { initEmployeeForm } from '/static/js/components/employeeFormHandler.js';
const form = initEmployeeForm();
// Проверка изменений перед уходом со страницы
window.addEventListener('beforeunload', (e) => {
if (form && form.hasChanges()) {
e.preventDefault();
e.returnValue = 'У вас есть несохранённые изменения';
}
});
// Программная отправка формы
document.getElementById('customSaveBtn').addEventListener('click', async () => {
if (form && form.validateForm()) {
try {
await form.submit();
alert('Сохранено!');
} catch (error) {
alert('Ошибка: ' + error.message);
}
}
});
```
---
## ✅ Best Practices
### 1. Всегда проверяйте возвращаемое значение
```javascript
const component = initSomeComponent();
if (component) {
// Компонент успешно инициализирован
component.doSomething();
} else {
// Необходимые элементы не найдены
console.warn('Component not initialized');
}
```
### 2. Используйте DOMContentLoaded
```javascript
document.addEventListener('DOMContentLoaded', () => {
// Инициализация после загрузки DOM
initAllComponents();
});
```
### 3. Экспортируйте в window для отладки
```javascript
const component = initComponent();
if (component) {
window.debugComponent = component; // Доступ из консоли
}
```
### 4. Группируйте импорты
```javascript
// Utils
import { esc, norm } from '{% static "js/utils/stringUtils.js" %}';
import { debounce } from '{% static "js/utils/timing.js" %}';
// Components
import { initEmployeeForm } from '{% static "js/components/employeeFormHandler.js" %}';
import { initPositionManager } from '{% static "js/components/positionManager.js" %}';
```
### 5. Используйте деструктуризацию
```javascript
// ✅ Хорошо
import { initComponent } from './component.js';
// ❌ Плохо
import * as Component from './component.js';
const init = Component.initComponent;
```
### 6. Обрабатывайте ошибки
```javascript
try {
const result = await component.someAsyncOperation();
handleSuccess(result);
} catch (error) {
console.error('Operation failed:', error);
handleError(error);
}
```
### 7. Очищайте ресурсы
```javascript
// Если модуль возвращает destroy()
const component = initComponent();
// При удалении со страницы
function cleanup() {
if (component && component.destroy) {
component.destroy();
}
}
```
---
## 🔧 Отладка
### Доступ к компонентам из консоли
Все инициализированные компоненты доступны через `window`:
```javascript
// В консоли браузера
window.employeeForm.hasChanges() // true/false
window.positionManager.refresh() // переинициализация
window.likeHandler // объект API
```
### Логирование
Модули используют `console.warn()` для предупреждений:
```javascript
// Если элементы не найдены
console.warn('EmployeeForm: required elements not found');
```
### Проверка инициализации
```javascript
// Проверить, был ли модуль инициализирован
if (window.__employeeFormInitialized) {
console.log('EmployeeForm already initialized');
}
```
---
## 📞 Поддержка
При возникновении вопросов:
1. Проверьте JSDoc в исходном коде модуля
2. Изучите примеры использования в шаблонах
3. Проверьте консоль браузера на предупреждения
4. Используйте `window.componentName` для отладки
---
**Дата актуализации**: 4 ноября 2025 г.
**Версия**: 1.0

View File

@ -1,238 +0,0 @@
# Конфигурация LDAP в системе EUSRR
## Обзор
Система EUSRR поддерживает два режима работы:
1. **С интеграцией LDAP/Active Directory** (по умолчанию)
2. **Без LDAP** (автономный режим с аутентификацией через БД)
Переключение между режимами осуществляется через переменную окружения `LDAP_ENABLED`.
## Режимы работы
### Режим с LDAP (LDAP_ENABLED=true)
**Аутентификация:**
- Пользователи аутентифицируются через Active Directory/LDAP
- Пароли проверяются в LDAP, не хранятся в БД
- Автоматическая синхронизация профилей при входе
**Создание пользователей:**
- Пользователи создаются сначала в LDAP, затем в БД
- Устанавливается флаг `is_ldap_managed=True`
- DN и GUID пользователя сохраняются в `LdapSyncState`
**Обновление пользователей:**
- Изменения применяются сначала к LDAP, затем к БД
- Поддерживается двусторонняя синхронизация (LWW - Last Writer Wins)
**Бэкенды аутентификации (в порядке приоритета):**
1. `LDAP3Backend` - основной LDAP-бэкенд
2. `EmailOrPhoneBackend` - пропускается (так как LDAP активен)
3. `SuperuserOnlyBackend` - экстренный доступ для суперюзера
4. `PositionRoleBackend` - расчёт прав
5. `ModelBackend` - стандартный Django (как фоллбэк)
**Требуемые настройки в .env:**
```env
LDAP_ENABLED=true
LDAP_URI=ldaps://dcii.robotail.local:636
LDAP_BIND_DN=corp@robotail.local
LDAP_BIND_PASSWORD=your_password
LDAP_USER_BASE="OU=company,DC=robotail,DC=local"
LDAP_USERS_BASE="OU=Users,OU=company,DC=robotail,DC=local"
LDAP_USER_UPN_SUFFIX=@robotail.local
LDAP_WRITE_ENABLED=true
```
### Режим без LDAP (LDAP_ENABLED=false)
**Аутентификация:**
- Пользователи аутентифицируются через БД Django
- Пароли хешируются и хранятся в таблице `employees_employee`
- Используется стандартная система паролей Django
**Создание пользователей:**
- Пользователи создаются напрямую в БД
- Флаг `is_ldap_managed=False`
- Пароль устанавливается через `user.set_password()`
**Обновление пользователей:**
- Все изменения применяются только к БД
- Синхронизация с LDAP не выполняется
**Бэкенды аутентификации (в порядке приоритета):**
1. `LDAP3Backend` - пропускает запрос (LDAP_ENABLED=false)
2. `EmailOrPhoneBackend` - **основной бэкенд** в этом режиме
3. `SuperuserOnlyBackend` - экстренный доступ для суперюзера
4. `PositionRoleBackend` - расчёт прав
5. `ModelBackend` - стандартный Django
**Требуемые настройки в .env:**
```env
LDAP_ENABLED=false
```
## Переключение между режимами
### Включение LDAP
1. В `.env` установите:
```env
LDAP_ENABLED=true
```
2. Настройте параметры подключения к LDAP (см. выше)
3. Перезапустите Django:
```bash
python manage.py runserver
```
4. **Важно:** Существующие пользователи с `is_ldap_managed=False` продолжат работать через БД до тех пор, пока не будут мигрированы в LDAP
### Отключение LDAP
1. В `.env` установите:
```env
LDAP_ENABLED=false
```
2. Перезапустите Django
3. **Важно:** Пользователи с `is_ldap_managed=True` НЕ СМОГУТ войти в систему, так как их пароли хранятся в LDAP. Для восстановления доступа необходимо:
- Установить пароль вручную: `user.set_password('new_password')`
- Изменить флаг: `user.is_ldap_managed = False`
- Сохранить: `user.save()`
## Миграция пользователей
### Из БД в LDAP
```python
from employees.ldap.directory_service import DirectoryService
from employees.ldap.domain.dtos import DirectoryUserDTO
# Для каждого пользователя
dto = DirectoryUserDTO(
first_name=user.first_name,
last_name=user.last_name,
email=user.email,
phone_e164=user.phone_number,
initial_password="temporary_password",
is_active=user.is_active,
)
service = DirectoryService()
service.create_user(dto)
# Пользователь будет создан в LDAP и помечен как is_ldap_managed=True
```
### Из LDAP в БД
```python
# Для каждого LDAP-пользователя
user.set_password('new_password') # Устанавливаем пароль в БД
user.is_ldap_managed = False
user.save()
```
## Проверка текущего режима
В коде Django:
```python
from django.conf import settings
if settings.LDAP_ENABLED:
print("Режим: с LDAP")
else:
print("Режим: без LDAP")
```
В Django shell:
```bash
python manage.py shell
>>> from django.conf import settings
>>> settings.LDAP_ENABLED
True # или False
```
## Рекомендации по безопасности
1. **Для продакшена с LDAP:**
- Используйте LDAPS (порт 636)
- Проверяйте сертификат CA
- Используйте отдельную сервисную учётку с минимальными правами
- Включите `LDAP_WRITE_ENABLED` только если нужна запись в LDAP
2. **Для продакшена без LDAP:**
- Настройте строгие требования к паролям в Django
- Включите двухфакторную аутентификацию
- Регулярно аудитируйте доступы
- Используйте `PASSWORD_HASHERS` с современными алгоритмами
3. **Для разработки:**
- Можно отключить LDAP для упрощения разработки
- Создавайте тестовых пользователей напрямую в БД
- Не храните реальные LDAP-креды в репозитории
## Поддерживаемые операции
| Операция | С LDAP | Без LDAP |
|----------|--------|----------|
| Создание пользователя | LDAP → БД | БД |
| Аутентификация | LDAP | БД (Django) |
| Обновление профиля | LDAP → БД | БД |
| Смена пароля | LDAP | БД |
| Удаление пользователя | LDAP → БД | БД |
| Синхронизация групп | Да | Нет |
| Синхронизация отделов | Да | Нет |
## Устранение неполадок
### Ошибка: "LDAP_BASE_DN must be set"
**Причина:** Не настроены переменные LDAP в settings.py или .env
**Решение:**
1. Проверьте наличие в `.env`:
```env
LDAP_USERS_BASE="OU=Users,OU=company,DC=robotail,DC=local"
```
2. Или отключите LDAP:
```env
LDAP_ENABLED=false
```
### Ошибка: "LDAP create failed: ..."
**Причина:** Проблема подключения или прав в LDAP
**Решение:**
1. Проверьте сетевое подключение к LDAP-серверу
2. Проверьте права сервисной учётки
3. Временно отключите LDAP для продолжения работы
### Пользователи не могут войти после отключения LDAP
**Причина:** У пользователей установлен `is_ldap_managed=True`, но LDAP недоступен
**Решение:**
```python
# В Django shell
from employees.models import Employee
# Для конкретного пользователя
user = Employee.objects.get(email='user@example.com')
user.set_password('new_password')
user.is_ldap_managed = False
user.save()
# Или массово
Employee.objects.filter(is_ldap_managed=True).update(is_ldap_managed=False)
# Затем установить пароли вручную каждому
```
## Контакты
При возникновении проблем с интеграцией LDAP обращайтесь к системному администратору.

View File

@ -1,92 +0,0 @@
# Backend Documentation
Техническая документация по backend части проекта EUSRR.
## 📁 Структура
### [guides/](guides/)
**Руководства разработчика** - инструкции по работе с различными модулями
- CONTEXT_MENU_GUIDE.md - работа с контекстными меню
- REACTIONS_GUIDE.md - система реакций
### [troubleshooting/](troubleshooting/)
**Отладка и исправления** - диагностика проблем
- CONTEXT_MENU_DEBUG.md - отладка контекстного меню
- MESSAGE_DELETE_DEBUG.md - отладка удаления сообщений
- MESSAGE_DELETE_FIX.md - исправление проблем удаления
### [architecture/](architecture/)
**Архитектурная документация** - устройство систем
- API_ANALYSIS.md - анализ API
- API_REFACTORING_SUMMARY.md - результаты рефакторинга API
- VIEWSET_ANALYSIS.md - анализ ViewSet
### [testing/](testing/)
**Тестирование** - планы и результаты тестирования
- POSITION_PERMISSIONS_TESTING.md - тестирование прав должностей
- REQUESTS_API_TESTING_PLAN.md - план тестирования API заявок
- TEST_IMAGE_SCRIPTS.md - скрипты тестирования изображений
### [reports/](reports/)
**Отчеты и аудиты** - результаты проверок и реорганизаций
- BACKEND_CLEANUP_AUDIT.md - аудит очистки backend/
- REQUESTS_API_TESTING_PLAN.md - план тестирования API заявок
- TEST_IMAGE_SCRIPTS.md - скрипты тестирования изображений
## 🔍 Навигация
### По типу задачи:
- **Как работает X?** → [architecture/](architecture/)
- **Как настроить Y?** → [guides/](guides/)
- **Проблема с Z** → [troubleshooting/](troubleshooting/)
- **Как тестировать?** → [testing/](testing/)
### Связанная документация:
- [/docs/](../../docs/) - общая документация проекта
- [backend/scripts/](../scripts/) - вспомогательные скрипты
## 📝 Правила
### Добавление новой документации:
1. **Guides** - постоянные инструкции, reference материалы
2. **Troubleshooting** - временные документы, после решения → удалить
3. **Architecture** - долгосрочная документация, обновлять при изменениях
4. **Testing** - планы тестирования, после завершения → архивировать
### Формат файлов:
```markdown
# Заголовок
**Дата:** DD.MM.YYYY
**Автор:** Имя
**Статус:** Актуально / Устарело / В процессе
## Описание
...
## Использование
...
## Примеры
...
```
## 🔄 Жизненный цикл
1. **Создание** - документируйте новую фичу/проблему
2. **Актуализация** - обновляйте при изменениях
3. **Архивация** - после завершения/решения
4. **Удаление** - если больше не актуально
---
**Поддерживайте документацию актуальной!** 📚

View File

@ -1,319 +0,0 @@
# 📊 Итоги рефакторинга EUSRR Frontend
**Дата завершения**: 4 ноября 2025 г.
**Ветка**: `merge/master-and-ldap-writeback`
---
## 🎯 Цели рефакторинга
1. ✅ Извлечь встроенный JavaScript в переиспользуемые ES6 модули
2. ✅ Декомпозировать большие HTML-шаблоны на логические компоненты
3. ✅ Улучшить читаемость и поддерживаемость кода
4. ✅ Создать документацию для разработчиков
---
## 📈 Статистика изменений
### JavaScript модули (Фаза 4)
**Создано 11 новых модулей** (всего 15 в `static/js/components/`):
| Модуль | Строки | Назначение |
|--------|--------|------------|
| `employeeFormHandler.js` | 280 | Управление формой сотрудника |
| `positionManager.js` | 380 | CRUD должностей с модалами |
| `groupPickers.js` | 238 | Выбор групп должностей |
| `employeeGroupsManager.js` | 341 | Управление группами сотрудника |
| `navbarHeight.js` | 35 | CSS переменная высоты navbar |
| `likeHandler.js` | 145 | Глобальный обработчик лайков |
| `skillsManager.js` | 245 | Управление навыками сотрудника |
| `deptRoleEditor.js` | 65 | Редактор ролей в отделах |
| `textareaAutogrow.js` | 140 | Автоувеличение textarea |
| `headPicker.js` | 203 | Autocomplete выбора руководителя |
| `roleManager.js` | 393 | Управление ролями отделов |
**Итого**: 2465 строк модульного кода
**Уже существующие модули**:
- `calendarWidget.js` (38 КБ)
- `teamWheel.js` (11 КБ)
- `listFilter.js` (8.6 КБ)
- `index.js` (1.3 КБ)
---
### Сокращение шаблонов
#### Основные файлы
| Файл | Было | Стало | Изменение | % |
|------|------|-------|-----------|---|
| `employee_form_scripts.html` | 907 | 22 | **-885** | **-98%** |
| `employee_detail.html` | 600 | 432 | -168 | -28% |
| `_department_controls.html` | 621 | 275 | -346 | -56% |
| `base.html` | ~710 | 631 | -79 | -11% |
**Итого удалено из шаблонов**: ~1478 строк встроенного JavaScript
#### Декомпозированные шаблоны (Фаза 5)
| Шаблон | Размер | Статус | Компоненты |
|--------|--------|--------|------------|
| `employee_edit.html` | **24 строки** | ✅ Готов | employee_form_* (7 компонентов) |
| `department_detail.html` | **46 строк** | ✅ Готов | department_* (4 компонента) |
| `rightbar_calendar.html` | **17 строк** | ✅ Готов | calendar_* (5 компонентов) |
---
### Структура компонентов
#### Компоненты сотрудников
```
templates/employees/components/
├── employee_form_personal.html # Личные данные
├── employee_form_contacts.html # Контакты
├── employee_form_position.html # Должность
├── employee_form_employment.html # Трудоустройство
├── employee_form_photo.html # Фотография
├── employee_form_actions.html # Кнопки действий
├── employee_form_scripts.html # JS инициализация (22 строки)
├── employee_detail_scripts.html # JS для деталей (47 строк)
├── department_controls_scripts.html # JS управления отделом (19 строк)
└── department_scripts.html # JS отдела (304 строки)
```
#### Компоненты календаря
```
templates/includes/components/
├── calendar_desktop.html # 40 строк - десктопная карточка
├── calendar_mobile.html # 23 строки - мобильная offcanvas
├── calendar_modal_create.html # 132 строки - создание события
├── calendar_modal_details.html # 33 строки - просмотр события
└── calendar_scripts.html # 11 строк - инициализация
```
---
## 🏗️ Архитектура модулей
### Принципы проектирования
1. **ES6 Modules**: Все новые модули используют `export/import`
2. **Graceful Degradation**: Возврат `null` если элементы отсутствуют
3. **Protection**: Защита от повторной инициализации
4. **JSDoc**: Полная документация API
5. **Window Export**: Публикация в `window` для обратной совместимости
### Пример структуры модуля
```javascript
/**
* @fileoverview Краткое описание
* @module components/moduleName
*/
export function initModuleName(options = {}) {
// Защита от повторной инициализации
if (window.__moduleNameInitialized) {
return null;
}
// Проверка наличия элементов
const elements = {
target: document.getElementById('targetId')
};
if (!elements.target) {
return null;
}
// Логика модуля
// ...
// API
return {
refresh: () => { /* переинициализация */ },
getState: () => { /* получение состояния */ }
};
}
// Публикация для совместимости
if (typeof window !== 'undefined') {
window.initModuleName = initModuleName;
}
```
---
## 📦 Утилиты (utils/)
### Строковые утилиты (`stringUtils.js`)
- `esc(s)` - экранирование HTML
- `norm(s)` - нормализация для поиска
- `escAttr(s)` - экранирование атрибутов
- `truncate(s, len)` - обрезка текста
- `getInitials(name)` - получение инициалов
### Утилиты времени (`timing.js`)
- `debounce(fn, ms)` - антидребезг
- `throttle(fn, ms)` - троттлинг
- `delay(ms)` - промисификация таймаута
### Утилиты скролла (`scroll.js`)
- `smoothScrollTo(el, options)` - плавный скролл
- `scrollToTop(duration)` - скролл наверх
- `isElementInViewport(el)` - проверка видимости
---
## 🎨 CSS архитектура
### Структура стилей
```
static/css/
├── variables.css # CSS переменные (цвета, отступы)
├── components/
│ ├── common.css # Общие стили
│ ├── base-app.css # Базовая структура приложения
│ ├── ios-components.css # iOS-стайлинг
│ ├── cards.css # Карточки ленты
│ ├── ios-search.css # Поиск в iOS стиле
│ └── rightbar-calendar.css # Стили календаря
```
### CSS переменные
```css
:root {
--navbar-height: 56px;
--primary-color: #007aff;
--background-color: #f5f5f7;
/* ... */
}
```
---
## 🔄 Паттерны использования
### Инициализация в base.html
```django
<script type="module">
import { initNavbarHeight } from '{% static "js/components/navbarHeight.js" %}';
import { initLikeHandler } from '{% static "js/components/likeHandler.js" %}';
import { initTextareaAutogrow } from '{% static "js/components/textareaAutogrow.js" %}';
document.addEventListener('DOMContentLoaded', () => {
initNavbarHeight();
initLikeHandler();
initTextareaAutogrow();
});
</script>
```
### Инициализация страничных компонентов
```django
{% include "employees/components/employee_form_scripts.html" %}
```
---
## 📋 Оставшиеся задачи
### 🔴 Отложено на отдельные сессии
1. **Chat Handler** (521 строка)
- 6 блоков WebSocket логики
- План: `docs/CHAT_REFACTORING_SESSION.md`
- Оценка: 2-3 часа
2. **document_list.html** (705 строк, 419 строк JS)
- RecipientPicker класс (сложная логика)
- CRUD операции с документами
- Оценка: 3-4 часа
3. **employee_form_position.html** (360 строк)
- Большая форма с бизнес-логикой
- Уже декомпозирована как компонент
- Дальнейшее разделение не требуется
---
## ✅ Достигнутые результаты
### Улучшение качества кода
- ✅ Модульная архитектура JavaScript
- ✅ Переиспользуемые компоненты
- ✅ Документированный API (JSDoc)
- ✅ Защита от повторной инициализации
- ✅ Graceful degradation
### Улучшение структуры шаблонов
- ✅ Декомпозиция больших файлов (>1000 строк → <50 строк)
- ✅ Логическое разделение на компоненты
- ✅ Переиспользуемые include-блоки
- ✅ Чистое разделение HTML/JS
### Улучшение поддерживаемости
- ✅ Локализация изменений (изменение модуля не затрагивает другие)
- ✅ Упрощение тестирования (модули можно тестировать отдельно)
- ✅ Упрощение отладки (понятная структура, явные зависимости)
- ✅ Документация для новых разработчиков
---
## 📚 Документация
Создана полная документация:
- ✅ `REFACTORING_SUMMARY.md` - итоговый отчёт (этот файл)
- ⏳ `TEMPLATES_GUIDE.md` - руководство по шаблонам
- ⏳ `JS_MODULES_GUIDE.md` - руководство по JavaScript модулям
- ✅ `CHAT_REFACTORING_SESSION.md` - план рефакторинга чата
- ✅ `CHAT_REFACTORING_QUICKSTART.md` - быстрый старт для чата
---
## 🚀 Следующие шаги
### Немедленные
1. Создать `TEMPLATES_GUIDE.md` с описанием структуры шаблонов
2. Создать `JS_MODULES_GUIDE.md` с API всех модулей
3. Протестировать все страницы после изменений
### Среднесрочные
1. Рефакторинг Chat handler (отдельная сессия)
2. Настройка минификации JavaScript
3. Оптимизация CSS переменных
### Долгосрочные
1. Рефакторинг document_list.html
2. Внедрение TypeScript (опционально)
3. Настройка автоматического тестирования компонентов
---
## 📊 Метрики
### До рефакторинга
- Встроенного JavaScript: ~2000+ строк
- Средний размер шаблона: 600-1400 строк
- Переиспользуемых модулей: 4
### После рефакторинга
- Модульного JavaScript: 4035 строк (15 модулей)
- Средний размер шаблона: 17-46 строк (основные)
- Переиспользуемых модулей: 15
- Создано компонентов: 18+
### Улучшения
- **Читаемость**: +300% (оценочно)
- **Поддерживаемость**: +400% (оценочно)
- **Скорость разработки**: +250% (для новых функций)
- **Количество багов**: -50% (за счёт изоляции кода)
---
**Автор**: GitHub Copilot
**Проект**: EUSRR (Employee Unified System for Resource Routing)
**Репозиторий**: IgorNadein/EUSRR

View File

@ -1,735 +0,0 @@
# 📄 Руководство по шаблонам EUSRR
**Версия**: 1.0
**Дата**: 4 ноября 2025 г.
---
## 📚 Содержание
1. [Структура шаблонов](#структура-шаблонов)
2. [Базовый шаблон](#базовый-шаблон)
3. [Компоненты сотрудников](#компоненты-сотрудников)
4. [Компоненты календаря](#компоненты-календаря)
5. [Компоненты отделов](#компоненты-отделов)
6. [Best Practices](#best-practices)
---
## 🏗️ Структура шаблонов
### Общая организация
```
templates/
├── base.html # Базовый шаблон (631 строка)
├── includes/ # Общие компоненты
│ ├── navbar.html # Навигационная панель
│ ├── sidebar.html # Левая боковая панель
│ ├── rightbar_calendar.html # Правая панель календаря (17 строк)
│ └── components/ # Компоненты для includes
│ ├── calendar_desktop.html # Десктопная версия календаря
│ ├── calendar_mobile.html # Мобильная offcanvas версия
│ ├── calendar_modal_create.html # Модал создания события
│ ├── calendar_modal_details.html # Модал деталей события
│ └── calendar_scripts.html # JS инициализация календаря
├── employees/ # Шаблоны сотрудников
│ ├── employees_list.html # Список сотрудников
│ ├── employee_detail.html # Детали сотрудника (432 строки)
│ ├── _employee_edit.html # Форма редактирования (24 строки!)
│ ├── _department_controls.html # Управление отделом (275 строк)
│ └── components/ # Компоненты сотрудников
│ ├── employee_form_personal.html
│ ├── employee_form_contacts.html
│ ├── employee_form_position.html
│ ├── employee_form_employment.html
│ ├── employee_form_photo.html
│ ├── employee_form_actions.html
│ ├── employee_form_scripts.html
│ ├── employee_detail_scripts.html
│ ├── department_controls_scripts.html
│ └── department_scripts.html
├── departments/
│ ├── department_detail.html # Детали отдела (46 строк!)
│ └── components/
│ ├── department_header.html
│ ├── department_wheel.html
│ ├── department_members.html
│ └── department_sidebar.html
├── documents/
│ └── document_list.html # Список документов (705 строк)
├── search/
│ └── results.html # Результаты поиска (335 строк)
└── requests_app/
└── request_list_full.html # Список заявок (673 строки)
```
---
## 🎨 Базовый шаблон
### `base.html`
Основной шаблон приложения с трёхколоночной структурой.
#### Структура
```django
<!doctype html>
<html lang="ru">
<head>
<!-- Meta-теги -->
<title>{% block title %}HiRo{% endblock %}</title>
<!-- Bootstrap, Icons, FullCalendar -->
<link href="bootstrap@5.3.3" rel="stylesheet">
<link href="bootstrap-icons@1.11.3" rel="stylesheet">
<!-- CSS Components -->
<link rel="stylesheet" href="{% static 'css/variables.css' %}">
<link rel="stylesheet" href="{% static 'css/components/common.css' %}">
<link rel="stylesheet" href="{% static 'css/components/cards.css' %}">
<link rel="stylesheet" href="{% static 'css/components/ios-search.css' %}">
<link rel="stylesheet" href="{% static 'css/components/base-app.css' %}">
<link rel="stylesheet" href="{% static 'css/components/ios-components.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
{% include "includes/navbar.html" %}
<div class="container-xxl">
<div class="app-layout with-sidebar with-rightbar">
{# Левая боковая панель #}
{% if user.is_authenticated %}
{% include "includes/sidebar.html" %}
{% endif %}
{# Основной контент #}
<main class="main">
{% block content %}{% endblock %}
</main>
{# Правая панель с календарём #}
{% if user.is_authenticated %}
{% include "includes/rightbar_calendar.html" %}
{% endif %}
</div>
</div>
<!-- JS Scripts -->
<script src="bootstrap@5.3.3/bootstrap.bundle.min.js" defer></script>
<script src="fullcalendar@6.1.8" defer></script>
<!-- Global JS Modules -->
<script type="module">
// Утилиты
import { esc, norm, escAttr, truncate, getInitials } from '...stringUtils.js';
import { debounce, throttle, delay } from '...timing.js';
import { smoothScrollTo, scrollToTop, isElementInViewport } from '...scroll.js';
// Глобальные компоненты
import { initNavbarHeight } from '...navbarHeight.js';
import { initLikeHandler } from '...likeHandler.js';
import { initTextareaAutogrow } from '...textareaAutogrow.js';
document.addEventListener('DOMContentLoaded', () => {
// Инициализация глобальных компонентов
const navbarHeight = initNavbarHeight();
const likeHandler = initLikeHandler();
const textareaAutogrow = initTextareaAutogrow();
// Экспорт в window для совместимости
if (navbarHeight) window.navbarHeight = navbarHeight;
if (likeHandler) window.likeHandler = likeHandler;
if (textareaAutogrow) window.textareaAutogrow = textareaAutogrow;
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>
```
#### Блоки для переопределения
- `{% block title %}` - заголовок страницы
- `{% block extra_css %}` - дополнительные стили
- `{% block content %}` - основной контент
- `{% block extra_js %}` - дополнительные скрипты
---
## 👥 Компоненты сотрудников
### Форма редактирования сотрудника
#### `_employee_edit.html` (24 строки)
Главный шаблон формы, объединяющий все компоненты.
```django
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="container ios-page">
<form method="post" enctype="multipart/form-data" id="employeeForm">
{% csrf_token %}
<div class="row g-3">
{# Компоненты формы #}
{% include "employees/components/employee_form_personal.html" %}
{% include "employees/components/employee_form_contacts.html" %}
{% include "employees/components/employee_form_position.html" %}
{% include "employees/components/employee_form_employment.html" %}
{% include "employees/components/employee_form_photo.html" %}
{% include "employees/components/employee_form_actions.html" %}
</div>
</form>
</div>
{# JavaScript инициализация #}
{% include "employees/components/employee_form_scripts.html" %}
{% endblock %}
```
---
### Компоненты формы
#### `employee_form_personal.html`
Личные данные сотрудника.
**Поля:**
- Фамилия, Имя, Отчество
- Пол
- Дата рождения
**Особенности:**
- iOS-стайлинг аккордеона
- Валидация обязательных полей
- Автофокус на первое поле
---
#### `employee_form_contacts.html`
Контактная информация.
**Поля:**
- Email (корпоративная почта)
- Телефон (рабочий)
- Внутренний номер
**Особенности:**
- Форматирование телефона
- Валидация email
- Опциональные поля
---
#### `employee_form_position.html` (360 строк)
Управление должностью сотрудника.
**Разделы:**
1. Текущая должность (select/input)
2. Создание новой должности (collapse)
3. Редактирование должности (модал)
4. Назначение групп должностей
**JavaScript:**
- `positionManager.js` - CRUD должностей
- `groupPickers.js` - выбор групп
**Особенности:**
- Динамические модалы
- AJAX операции
- Валидация прав доступа
---
#### `employee_form_employment.html`
Информация о трудоустройстве.
**Поля:**
- Дата приёма на работу
- Статус (работает/уволен)
- Дата увольнения
**Особенности:**
- Datepicker интеграция
- Условное отображение (статус)
- Автозаполнение текущей даты
---
#### `employee_form_photo.html`
Загрузка фотографии.
**Поля:**
- Превью текущей фотографии
- File input для загрузки
- Кнопка удаления фото
**Особенности:**
- Предпросмотр перед загрузкой
- Валидация размера/формата
- Crop функционал (опционально)
---
#### `employee_form_actions.html`
Кнопки действий формы.
**Кнопки:**
- Сохранить изменения
- Отмена (возврат назад)
- Удалить сотрудника (для админов)
**Особенности:**
- Sticky позиционирование на мобильных
- Подтверждение удаления
- Отслеживание изменений
---
#### `employee_form_scripts.html` (22 строки)
JavaScript инициализация для формы.
```django
{% load static %}
<script type="module">
import { initEmployeeForm } from '{% static "js/components/employeeFormHandler.js" %}';
import { initPositionManager } from '{% static "js/components/positionManager.js" %}';
import { initPositionGroupPicker, initPositionEditGroupPicker } from '{% static "js/components/groupPickers.js" %}';
import { initEmployeeGroupsManager } from '{% static "js/components/employeeGroupsManager.js" %}';
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll);
} else {
initAll();
}
function initAll() {
const employeeForm = initEmployeeForm();
const positionMgr = initPositionManager();
const groupPicker = initPositionGroupPicker();
const editGroupPicker = initPositionEditGroupPicker();
const groupsMgr = initEmployeeGroupsManager();
// Экспорт для отладки
if (employeeForm) window.employeeForm = employeeForm;
if (positionMgr) window.positionManager = positionMgr;
if (groupPicker) window.groupPicker = groupPicker;
if (editGroupPicker) window.editGroupPicker = editGroupPicker;
if (groupsMgr) window.employeeGroupsManager = groupsMgr;
}
</script>
```
---
### Детали сотрудника
#### `employee_detail.html` (432 строки)
Страница просмотра профиля сотрудника.
**Разделы:**
1. Шапка с фото и основной информацией
2. Контактная информация
3. Должность и отделы
4. Навыки (skills)
5. История изменений
6. Форма редактирования (collapse)
**JavaScript:**
```django
{% include "employees/components/employee_detail_scripts.html" %}
```
#### `employee_detail_scripts.html` (47 строк)
```django
{% load static %}
<script type="module">
import { initSkillsManager } from '{% static "js/components/skillsManager.js" %}';
import { initDeptRoleEditor } from '{% static "js/components/deptRoleEditor.js" %}';
import { smoothScrollTo } from '{% static "js/utils/scroll.js" %}';
document.addEventListener('DOMContentLoaded', () => {
// 1. Менеджер навыков
const skillsManager = initSkillsManager({
blockId: 'skillsBlock',
formId: 'skillAddForm',
collapseId: 'skillsForm',
removeUrl: "{% url 'employees:skill_remove' emp.id %}"
});
// 2. Редакторы ролей в отделах
const deptRoleEditor = initDeptRoleEditor();
// 3. Smooth scroll к форме редактирования
const editWrap = document.getElementById('editFormWrap');
if (editWrap) {
const isMobile = () => window.matchMedia('(max-width: 767.98px)').matches;
editWrap.addEventListener('shown.bs.collapse', () => {
if (isMobile()) {
smoothScrollTo(editWrap, { offset: getHeaderOffset() });
}
});
}
// Экспорт в window
if (skillsManager) window.skillsManager = skillsManager;
if (deptRoleEditor) window.deptRoleEditor = deptRoleEditor;
});
</script>
```
---
## 📅 Компоненты календаря
### `rightbar_calendar.html` (17 строк)
Основной файл правой панели с календарём.
```django
{% load static %}
{# Правая колонка с календарём - рефакторированная версия #}
<link rel="stylesheet" href="{% static 'css/components/rightbar-calendar.css' %}">
{# Десктопная версия #}
{% include "includes/components/calendar_desktop.html" %}
{# Мобильная версия #}
{% include "includes/components/calendar_mobile.html" %}
{# Модальные окна #}
{% include "includes/components/calendar_modal_create.html" %}
{% include "includes/components/calendar_modal_details.html" %}
{# JavaScript инициализация #}
{% include "includes/components/calendar_scripts.html" %}
```
---
### `calendar_desktop.html` (40 строк)
Десктопная версия календаря в правой панели.
**Элементы:**
- Заголовок "События"
- Контейнер для FullCalendar
- Кнопка создания события
- Список предстоящих событий
**Классы:**
- `.rightbar-calendar` - основной контейнер
- `.calendar-widget` - виджет календаря
- `.event-list` - список событий
---
### `calendar_mobile.html` (23 строки)
Мобильная offcanvas версия календаря.
**Структура:**
```html
<div class="offcanvas offcanvas-end" id="calendarOffcanvas">
<div class="offcanvas-header">
<h5>События</h5>
<button type="button" class="btn-close"></button>
</div>
<div class="offcanvas-body">
<!-- Содержимое календаря -->
</div>
</div>
```
---
### `calendar_modal_create.html` (132 строки)
Модал для создания нового события.
**Поля формы:**
- Название события
- Описание
- Дата начала
- Дата окончания
- Цвет события
- Тип (личное/рабочее)
**Особенности:**
- Валидация дат
- Color picker
- AJAX отправка
---
### `calendar_modal_details.html` (33 строки)
Модал для просмотра деталей события.
**Отображаемая информация:**
- Название
- Описание
- Дата и время
- Участники
- Кнопки: Редактировать, Удалить
---
### `calendar_scripts.html` (11 строк)
Инициализация виджета календаря.
```django
{% load static %}
<script type="module">
import { initCalendarWidget } from '{% static "js/components/calendarWidget.js" %}';
const widget = initCalendarWidget();
if (widget) {
window.calendarWidget = widget;
}
</script>
```
---
## 🏢 Компоненты отделов
### `department_detail.html` (46 строк)
Страница деталей отдела.
```django
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="container ios-page">
<div class="row g-3">
{# Заголовок отдела #}
{% include "departments/components/department_header.html" %}
{# Колесо команды #}
{% include "departments/components/department_wheel.html" %}
{# Список участников #}
{% include "departments/components/department_members.html" %}
{# Боковая панель с настройками #}
{% include "departments/components/department_sidebar.html" %}
</div>
</div>
{# JavaScript инициализация #}
{% include "employees/components/department_scripts.html" %}
{% endblock %}
```
---
### `_department_controls.html` (275 строк)
Управление отделом (настройки, роли, права).
**Разделы:**
1. Выбор руководителя отдела (headPicker)
2. Управление ролями отдела (roleManager)
3. Создание/редактирование ролей
4. Назначение прав (permissions)
**JavaScript:**
```django
{% include "employees/components/department_controls_scripts.html" %}
```
#### `department_controls_scripts.html` (19 строк)
```django
{% load static %}
<script type="module">
import { initHeadPicker } from '{% static "js/components/headPicker.js" %}';
import { initRoleManager } from '{% static "js/components/roleManager.js" %}';
document.addEventListener('DOMContentLoaded', () => {
const headPicker = initHeadPicker();
const roleManager = initRoleManager();
if (headPicker) window.headPicker = headPicker;
if (roleManager) window.roleManager = roleManager;
});
</script>
```
---
## ✅ Best Practices
### 1. Структура компонентов
```
✅ ХОРОШО: Малый размер, одна ответственность
employees/components/
├── employee_form_personal.html # ~80 строк
├── employee_form_contacts.html # ~60 строк
└── employee_form_scripts.html # ~22 строки
❌ ПЛОХО: Монолитный файл
employees/
└── employee_edit.html # 1391 строка!
```
### 2. Именование файлов
```
✅ ХОРОШО: Префиксы, понятные имена
calendar_modal_create.html
employee_form_personal.html
department_controls_scripts.html
❌ ПЛОХО: Неясные имена
modal.html
form.html
scripts.html
```
### 3. Подключение компонентов
```django
✅ ХОРОШО: Явное включение с комментариями
{# Личные данные #}
{% include "employees/components/employee_form_personal.html" %}
{# Контакты #}
{% include "employees/components/employee_form_contacts.html" %}
❌ ПЛОХО: Неясный порядок без комментариев
{% include "comp1.html" %}
{% include "comp2.html" %}
```
### 4. Разделение JavaScript
```django
✅ ХОРОШО: Отдельный файл инициализации
{% include "employees/components/employee_form_scripts.html" %}
❌ ПЛОХО: Встроенный <script> в HTML
<script>
// 500 строк JavaScript прямо в шаблоне
</script>
```
### 5. Передача контекста
```django
✅ ХОРОШО: Явная передача параметров
{% include "component.html" with employee=emp can_edit=True %}
❌ ПЛОХО: Неявный контекст
{% include "component.html" %}
{# Компонент зависит от глобальных переменных #}
```
### 6. Блоки и наследование
```django
✅ ХОРОШО: Расширение базового шаблона
{% extends "base.html" %}
{% block content %}
<!-- Ваш контент -->
{% endblock %}
❌ ПЛОХО: Дублирование структуры
<!DOCTYPE html>
<html>
<!-- Полная структура заново -->
</html>
```
### 7. CSS подключение
```django
✅ ХОРОШО: Модульные стили в extra_css
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/components/custom.css' %}">
{% endblock %}
❌ ПЛОХО: Inline стили
<div style="color: red; font-size: 14px;">...</div>
```
### 8. Условное включение
```django
✅ ХОРОШО: Проверка прав перед включением
{% if user.is_authenticated %}
{% include "includes/rightbar_calendar.html" %}
{% endif %}
❌ ПЛОХО: Проверка внутри компонента
{# В компоненте: #}
{% if user.is_authenticated %}
<!-- Весь компонент -->
{% endif %}
```
---
## 🔧 Отладка шаблонов
### Вывод отладочной информации
```django
{# Проверка контекста #}
{% if debug %}
<pre>{{ employee|pprint }}</pre>
{% endif %}
{# Отладка переменных #}
{{ variable|default:"NOT SET" }}
```
### Комментарии для разработчиков
```django
{# === СЕКЦИЯ: Личные данные === #}
{# TODO: Добавить валидацию email #}
{# FIXME: Баг с датой рождения #}
{# NOTE: Этот компонент используется в 3 местах #}
```
---
## 📞 Поддержка
При создании новых шаблонов:
1. Следуйте структуре существующих компонентов
2. Используйте префиксы в именах файлов
3. Документируйте сложную логику
4. Разделяйте HTML и JavaScript
5. Тестируйте на разных экранах
---
**Дата актуализации**: 4 ноября 2025 г.
**Версия**: 1.0

View File

@ -1,182 +0,0 @@
# Унификация автоматического расширения textarea
## Описание изменений
Создан универсальный модуль `textareaAutoExpand.js`, который автоматически обрабатывает расширение всех textarea с `rows="1"` в формах комментариев и сообщений по всему приложению.
## Реализация
### Новый модуль: textareaAutoExpand.js
**Путь:** `backend/static/js/components/textareaAutoExpand.js`
**Функциональность:**
- Автоматически находит все textarea с атрибутом `rows="1"` или классом `composer-textarea`
- Расширяет textarea при вводе текста до максимум 6 строк (144px)
- Использует MutationObserver для отслеживания динамически добавляемых textarea
- Работает со всеми формами комментариев: лента, заявки, модальные окна, чат
**Принцип работы:**
```javascript
// Сбрасываем высоту для корректного вычисления scrollHeight
textarea.style.height = 'auto';
// Устанавливаем новую высоту, но не больше максимальной (6 строк)
const newHeight = Math.min(textarea.scrollHeight, MAX_HEIGHT);
textarea.style.height = newHeight + 'px';
```
### Поддерживаемые селекторы
Модуль автоматически работает с:
- `.comment-form-quick textarea` - быстрые комментарии в ленте
- `.comment-form-modal textarea` - комментарии в модальном окне
- `.request-comment-form textarea` - комментарии к заявкам
- `.comment-new textarea` - новые комментарии в списке заявок
- `.composer-textarea` - поле ввода в чате
- `.message-input` - общий класс для всех полей ввода
### Условия применения
Авторасширение применяется **только** к textarea, которые удовлетворяют условию:
- Имеют атрибут `rows="1"` **ИЛИ**
- Имеют класс `composer-textarea`
Это означает, что textarea в модальных окнах редактирования (с `rows="3"` или `rows="4"`) **не будут** автоматически расширяться.
## Удалённые файлы
### 1. textareaAutogrowHandler.js
- **Причина удаления:** Использовался только в `post_form.html`, функциональность заменена универсальным модулем
- **Зависимости:** Удалён импорт из `backend/templates/feed/post_form.html`
### 2. textareaAutogrow.js
- **Причина удаления:** Требовал явного атрибута `data-autogrow`, менее универсален
- **Зависимости:** Удалён импорт из `backend/templates/base.html`
## Изменённые файлы
### 1. backend/templates/base.html
```diff
- import { initTextareaAutogrow } from '{% static "js/components/textareaAutogrow.js" %}';
+ {# Универсальный модуль для автоматического расширения textarea #}
+ <script src="{% static 'js/components/textareaAutoExpand.js' %}" defer></script>
```
Удалена инициализация:
```diff
- const textareaAutogrow = initTextareaAutogrow();
- if (textareaAutogrow) window.textareaAutogrow = textareaAutogrow;
```
### 2. backend/templates/feed/post_form.html
```diff
- import { initTextareaAutogrow } from "{% static 'js/components/textareaAutogrowHandler.js' %}";
-
- // Автоматическое изменение размера textarea
- initTextareaAutogrow({ selector: 'form.card textarea' });
```
### 3. backend/static/js/components/chatMarkRead.js
```diff
function autosize() {
if (!ta) return;
- ta.style.height = 'auto';
- ta.style.height = Math.min(ta.scrollHeight, 6 * 24) + 'px';
+ // Авторасширение теперь обрабатывается textareaAutoExpand.js
+ // Оставляем только autoscroll логику
if (atBottom()) autoscroll();
}
```
### 4. backend/static/js/components/requestCommentsHandler.js
Удалена функция `initTextareaAutosize()` и её вызов:
```diff
- function initTextareaAutosize() {
- // ... закомментированный код
- }
-
- document.addEventListener('DOMContentLoaded', () => {
- autoloadAllCounts();
- initTextareaAutosize();
- });
+ document.addEventListener('DOMContentLoaded', autoloadAllCounts);
```
### 5. backend/static/scss/components/_message-inputs.scss
Удалены конфликтующие `min-height` и `max-height`, которые мешали автоматическому расширению:
```diff
.message-input,
.composer-textarea {
flex: 1 1 auto;
- min-height: 44px;
- max-height: 260px;
border: none !important;
...
}
.message-field--compact .message-input {
- min-height: 32px;
font-size: 0.95rem;
+ // min-height удален - управляется textareaAutoExpand.js
}
```
### 6. backend/static/css/components/request-list.css
Удалены специфичные стили для `.comment-new textarea`, которые перебивали универсальное поведение:
```diff
- /* ─────────────────────────────────────────────────────────────
- COMMENT TEXTAREA
- ────────────────────────────────────────────────────────────── */
- .comment-new textarea {
- resize: vertical;
- max-height: 220px;
- min-height: 44px;
- overflow-y: auto;
- }
```
## Преимущества новой реализации
1. **Единообразие:** Все textarea с `rows="1"` работают одинаково по всему приложению
2. **Автоматичность:** Не требует явной инициализации в каждом модуле
3. **Динамичность:** Автоматически обрабатывает textarea, добавленные через AJAX/модальные окна
4. **Простота поддержки:** Один файл вместо трёх разных реализаций
5. **Производительность:** Единая инициализация вместо множественных обработчиков
## Тестирование
Необходимо протестировать автоматическое расширение в:
- ✅ Быстрые комментарии в ленте (feed)
- ✅ Комментарии в модальном окне поста
- ✅ Комментарии к заявкам (request_detail.html)
- ✅ Комментарии в обработке заявок (request_process.html)
- ✅ Комментарии в списке заявок (request_list_full.html)
- ✅ Поле ввода в чате (composer-textarea)
**Ожидаемое поведение:**
- Начальная высота: 1 строка (~38px)
- Максимальная высота: 6 строк (144px)
- После 6 строк появляется скроллбар
- При удалении текста высота уменьшается
## Обратная совместимость
Модуль экспортирует API в `window.textareaAutoExpand`:
```javascript
window.textareaAutoExpand = {
init: initAllTextareas, // Пересканировать все textarea
initTextarea: initAutoExpand, // Инициализировать конкретный textarea
autoExpand: autoExpand // Применить расширение к textarea
};
```
Это позволяет вручную инициализировать textarea, если требуется:
```javascript
// Пример использования
const textarea = document.querySelector('#myTextarea');
window.textareaAutoExpand.initTextarea(textarea);
```
## Дата изменения
2025-01-XX

View File

@ -1,121 +0,0 @@
# 🎯 Тестирование унификации textarea
## Что было исправлено
**Проблема:** Поля ввода комментариев в заявлениях вели себя иначе, чем в новостях.
**Причина:**
1. В `request-list.css` были специфичные стили для `.comment-new textarea`:
- `min-height: 44px`
- `max-height: 220px`
- `resize: vertical`
2. В `_message-inputs.scss` были конфликтующие стили:
- `.message-input { min-height: 44px; max-height: 260px; }`
- `.message-field--compact .message-input { min-height: 32px; }`
**Решение:**
- ✅ Удалены все фиксированные `min-height` и `max-height` из CSS
- ✅ Удален `resize: vertical` (теперь только `resize: none`)
- ✅ Все textarea с `rows="1"` автоматически управляются через `textareaAutoExpand.js`
## Как тестировать
### 1. Комментарии в ленте новостей
1. Откройте главную страницу `/feed/`
2. Найдите любую новость
3. Начните вводить комментарий в поле "Оставить комментарий…"
4. ✅ **Ожидается:** Поле плавно расширяется от 1 до 6 строк (144px)
### 2. Комментарии в заявлениях
1. Откройте список заявлений `/requests/`
2. Нажмите на кнопку "Комментарии" под любой заявкой
3. Начните вводить комментарий в поле "Написать…"
4. ✅ **Ожидается:** Поле ведёт себя **ИДЕНТИЧНО** полю в новостях
### 3. Комментарии в детальном просмотре заявки
1. Откройте заявку `/requests/<id>/`
2. Прокрутите до раздела комментариев
3. Начните вводить комментарий
4. ✅ **Ожидается:** Точно такое же поведение, как в новостях
### 4. Комментарии при обработке заявки
1. Откройте заявку для обработки `/requests/<id>/process/`
2. Прокрутите до комментариев
3. Начните вводить
4. ✅ **Ожидается:** Единообразное поведение
### 5. Модальные окна
1. Откройте модальное окно новости
2. Попробуйте добавить комментарий
3. ✅ **Ожидается:** Расширение от 1 до 6 строк
## Что проверять
| Критерий | Ожидаемое поведение |
|----------|-------------------|
| **Начальная высота** | 1 строка (~38-40px) |
| **Максимальная высота** | 6 строк (144px) |
| **Поведение при вводе** | Плавное автоматическое расширение |
| **Скроллбар** | Появляется только после 6 строк |
| **Уменьшение высоты** | При удалении текста высота уменьшается |
| **Единообразие** | Все поля с `rows="1"` работают одинаково |
| **Визуальный стиль** | Нет отличий между новостями и заявлениями |
## Известные исключения
Следующие textarea **НЕ должны** автоматически расширяться:
- Модальные окна редактирования с `rows="3"` или `rows="4"`
- Поле создания новости с большим textarea
- Любые textarea без `rows="1"` и без класса `.composer-textarea`
## Проверка в DevTools
Откройте DevTools → Elements и проверьте textarea:
### ✅ Правильно (новости и заявления должны быть одинаковыми):
```html
<textarea class="form-control message-input"
rows="1"
data-auto-expand-init="true"
style="height: auto; height: 72px;">
```
**Проверки:**
- `rows="1"` ✅
- `data-auto-expand-init="true"` ✅ (добавлен модулем)
- `style="height: XXpx"` ✅ (динамически изменяется)
- Нет `min-height`, `max-height` в computed styles ❌ (должны отсутствовать!)
### ❌ Неправильно (если видите):
```css
/* В Computed Styles не должно быть: */
min-height: 44px; /* ❌ ПЛОХО */
max-height: 260px; /* ❌ ПЛОХО */
resize: vertical; /* ❌ ПЛОХО */
```
## Быстрый визуальный тест
1. Откройте новость → введите 5 строк текста → запомните высоту
2. Откройте заявку → введите 5 строк текста → сравните высоту
3. ✅ **Должны быть ИДЕНТИЧНЫ!**
## Если что-то пошло не так
1. Очистите кеш браузера (Ctrl+Shift+Delete)
2. Проверьте, что собрана статика: `python manage.py collectstatic`
3. Проверьте, что скомпилирован CSS: `cd backend/static && npm run build:css`
4. Перезагрузите страницу с Ctrl+F5
## Файлы для проверки
- ✅ `backend/static/js/components/textareaAutoExpand.js` - основной модуль
- ✅ `backend/static/scss/components/_message-inputs.scss` - стили без min/max height
- ✅ `backend/static/css/components/request-list.css` - удалены специфичные стили
- ✅ `backend/static/css/bootstrap-custom.css` - скомпилированный CSS
---
**Дата создания:** 29.11.2025
**Версия:** 1.0