chore: очистка устаревшей документации
- Удалена устаревшая документация из backend/docs/ - Актуальные гайды сохранены в backend/docs/guides/ и других подпапках - Удален migrate_participants_to_memberships.py (уже был создан в communications/management/commands/)
This commit is contained in:
parent
91342d199d
commit
b94c6499d4
@ -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'
|
||||
)
|
||||
)
|
||||
@ -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. Обновить остальную документацию при необходимости
|
||||
@ -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 г.
|
||||
@ -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/)
|
||||
|
||||
---
|
||||
|
||||
**Легенда:**
|
||||
- ✨ Добавлено - новая функциональность
|
||||
- 🔄 Изменено - изменения в существующей функциональности
|
||||
- 🗑️ Удалено - удаленная функциональность
|
||||
- 🐛 Исправлено - исправления багов
|
||||
- 🔒 Безопасность - закрытие уязвимостей
|
||||
- ⚡ Производительность - улучшения производительности
|
||||
- 📚 Документация - только документация
|
||||
- 🧪 Тестирование - добавление/изменение тестов
|
||||
@ -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)
|
||||
- [ ] Предпросмотр аватара в списке чатов
|
||||
- [ ] Поиск чатов по названию в модале
|
||||
@ -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)
|
||||
@ -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** 🎉
|
||||
@ -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);
|
||||
// => '<script>alert("xss")</script>'
|
||||
```
|
||||
|
||||
#### `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 "quoted" 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
|
||||
@ -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 обращайтесь к системному администратору.
|
||||
@ -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. **Удаление** - если больше не актуально
|
||||
|
||||
---
|
||||
|
||||
**Поддерживайте документацию актуальной!** 📚
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user