diff --git a/AVATAR_CROPPER_IMPLEMENTATION.md b/AVATAR_CROPPER_IMPLEMENTATION.md
new file mode 100644
index 00000000..430f0480
--- /dev/null
+++ b/AVATAR_CROPPER_IMPLEMENTATION.md
@@ -0,0 +1,330 @@
+# Avatar Cropper Implementation for FaceID
+
+## 📋 Описание
+
+Реализован компонент обрезки аватара с требованиями для системы распознавания лиц (FaceID).
+
+## 🎯 Цели
+
+1. ✅ Обеспечить единообразие фотографий для FaceID
+2. ✅ Гарантировать, что лицо занимает 70-80% кадра
+3. ✅ Минимальный размер: 600x600px
+4. ✅ Формат: JPEG с оптимизацией качества (92%)
+5. ✅ Интеграция в 2 места: регистрация + редактор профиля
+
+## 🔧 Технические детали
+
+### Используемые технологии
+
+- **Cropper.js v1.6.1** - библиотека для обрезки изображений
+- **Bootstrap 5 Modal** - модальное окно
+- **Canvas API** - конвертация в нужный формат
+- **File API** - работа с файлами
+
+### Компоненты
+
+#### 1. **avatarCropperHandler.js**
+Основной модуль с классом `AvatarCropper`:
+
+```javascript
+import { initAvatarCropper } from '/static/js/components/avatarCropperHandler.js';
+
+// Инициализация
+initAvatarCropper({
+ fileInputSelector: 'input[type="file"][name="avatar"]',
+ previewImageId: 'avatarPreview',
+ placeholderId: 'avatarPlaceholder',
+ modalId: 'avatarCropperModal'
+});
+```
+
+**Возможности:**
+- ✅ Автоматическое открытие cropper при выборе файла
+- ✅ Валидация размера и формата
+- ✅ Квадратная область обрезки (1:1)
+- ✅ Минимальный размер обрезки: 200x200px
+- ✅ Выходной размер: 800x800px
+- ✅ Качество JPEG: 92%
+- ✅ Индикатор размера в реальном времени
+- ✅ Превью обрезанного изображения
+- ✅ Автоматическое обновление file input
+
+### Интеграция
+
+#### Место 1: Регистрация (`templates/auth/register.html`)
+
+```html
+
+
+
+
+
+```
+
+#### Место 2: Редактор профиля (`templates/employees/components/employee_form_scripts.html`)
+
+```html
+
+
+
+
+
+```
+
+## 📝 Требования FaceID
+
+### Обязательные требования:
+1. ✅ **Размер лица**: 70-80% кадра
+2. ✅ **Центрирование**: Лицо в центре рамки
+3. ✅ **Минимальный размер**: 600x600px
+4. ✅ **Соотношение сторон**: 1:1 (квадрат)
+5. ✅ **Формат**: JPEG
+6. ✅ **Качество**: 92%
+
+### Рекомендации для пользователей:
+- Избегайте теней и бликов
+- Смотрите прямо в камеру
+- Нейтральное выражение лица
+- Хорошее освещение
+
+## 🎨 UI/UX
+
+### Модальное окно cropper
+
+```
+┌─────────────────────────────────────────┐
+│ Обрезка фото для FaceID [X] │
+├─────────────────────────────────────────┤
+│ │
+│ ℹ️ Требования для системы распознавания:│
+│ • Лицо должно занимать 70-80% кадра │
+│ • Расположите лицо в центре рамки │
+│ • Избегайте теней и бликов │
+│ • Смотрите прямо в камеру │
+│ • Нейтральное выражение лица │
+│ │
+│ ┌───────────────────────────────────┐ │
+│ │ │ │
+│ │ [Cropper Canvas Area] │ │
+│ │ │ │
+│ └───────────────────────────────────┘ │
+│ │
+│ Размер обрезанного фото: 800x800px │
+│ │
+├─────────────────────────────────────────┤
+│ [Отмена] [✓ Применить] │
+└─────────────────────────────────────────┘
+```
+
+### Превью в форме
+
+- **До выбора**: Иконка placeholder (bi-person или bi-person-circle)
+- **После обрезки**: Обрезанное изображение 120x120px
+- **Подсказка**: Текст с требованиями FaceID
+
+## 🚀 Workflow
+
+### Шаг 1: Пользователь выбирает файл
+```
+Input[type=file] → change event
+```
+
+### Шаг 2: Валидация
+```javascript
+✓ Формат: JPG, PNG, GIF
+✓ Размер: < 10 MB
+✓ Минимальный размер: 600x600px
+```
+
+### Шаг 3: Открытие Cropper
+```
+FileReader → DataURL → Modal → Cropper.js init
+```
+
+### Шаг 4: Обрезка
+```
+User adjusts crop area → Real-time size indicator
+```
+
+### Шаг 5: Применение
+```
+getCroppedCanvas(800x800) → Blob → File → Update input
+```
+
+### Шаг 6: Превью
+```
+Canvas → DataURL → Update preview image
+```
+
+### Шаг 7: Отправка формы
+```
+FormData with cropped file → Backend API
+```
+
+## 📊 Конфигурация
+
+### Настройки по умолчанию:
+
+```javascript
+{
+ // Cropper.js options
+ aspectRatio: 1,
+ viewMode: 2,
+ dragMode: 'move',
+ autoCropArea: 0.8,
+ minCropBoxWidth: 200,
+ minCropBoxHeight: 200,
+
+ // Output settings
+ minOutputSize: 600,
+ outputSize: 800,
+ outputQuality: 0.92,
+ outputFormat: 'image/jpeg',
+
+ // Validation
+ maxFileSize: 10 * 1024 * 1024, // 10 MB
+ allowedFormats: ['image/jpeg', 'image/png', 'image/gif']
+}
+```
+
+### Кастомизация:
+
+```javascript
+initAvatarCropper({
+ // Селекторы
+ fileInputSelector: '#myAvatarInput',
+ previewImageId: 'myPreview',
+ modalId: 'myCropperModal',
+
+ // Cropper опции
+ cropperOptions: {
+ aspectRatio: 1,
+ autoCropArea: 0.9
+ },
+
+ // Выходные параметры
+ outputSize: 1024,
+ outputQuality: 0.95,
+
+ // Тексты
+ texts: {
+ modalTitle: 'Обрезать фото',
+ cropButton: 'Готово',
+ cancelButton: 'Отмена'
+ }
+});
+```
+
+## 🧪 Тестирование
+
+### Тестовые сценарии:
+
+1. **Регистрация нового пользователя**
+ - Открыть `/auth/register/`
+ - Выбрать фото
+ - Проверить открытие cropper
+ - Обрезать фото
+ - Отправить форму
+ - Проверить, что аватар сохранился
+
+2. **Редактирование профиля**
+ - Открыть `/employees/profile/`
+ - Выбрать новое фото
+ - Обрезать фото
+ - Сохранить изменения
+ - Проверить обновление аватара
+
+3. **Валидация**
+ - Попытаться загрузить файл < 600x600px → Ошибка
+ - Попытаться загрузить файл > 10MB → Ошибка
+ - Попытаться загрузить .txt файл → Ошибка
+
+4. **Отмена**
+ - Выбрать фото
+ - Открыть cropper
+ - Нажать "Отмена"
+ - Проверить, что input очищен
+
+## 📦 Зависимости
+
+### CDN (используется):
+```html
+
+
+
+
+
+```
+
+### Существующие зависимости:
+- ✅ Bootstrap 5 (для модального окна)
+- ✅ Bootstrap Icons (для иконок)
+
+## 🔍 Troubleshooting
+
+### Проблема: Cropper не открывается
+**Решение:** Проверьте, что Cropper.js загружен из CDN и Bootstrap модальные окна работают.
+
+### Проблема: File input не обновляется
+**Решение:** Проверьте console на ошибки. Убедитесь, что DataTransfer API поддерживается.
+
+### Проблема: Превью не обновляется
+**Решение:** Проверьте селекторы `previewImageId` и `placeholderId`.
+
+### Проблема: Ошибка "Image too small"
+**Решение:** Убедитесь, что исходное изображение >= 600x600px.
+
+## ✅ Checklist
+
+### Файлы созданы:
+- ✅ `backend/static/js/components/avatarCropperHandler.js` (542 строки)
+- ✅ `AVATAR_CROPPER_IMPLEMENTATION.md` (этот файл)
+
+### Файлы изменены:
+- ✅ `backend/templates/auth/register.html` - добавлен cropper
+- ✅ `backend/templates/employees/components/employee_form_scripts.html` - добавлен cropper
+- ✅ `backend/templates/employees/components/employee_form_personal.html` - обновлен текст подсказки
+
+### Функционал:
+- ✅ Валидация размера файла (10 MB)
+- ✅ Валидация формата (JPG, PNG, GIF)
+- ✅ Валидация минимального размера (600x600px)
+- ✅ Cropper с квадратной областью (1:1)
+- ✅ Индикатор размера в реальном времени
+- ✅ Превью обрезанного изображения
+- ✅ Обновление file input
+- ✅ Инструкции для FaceID
+- ✅ Модальное окно Bootstrap
+- ✅ Обработка ошибок
+- ✅ Cleanup при закрытии
+
+## 🎉 Готово к тестированию!
+
+Все изменения в ветке `feature/avatar-validation`.
+
+**Следующие шаги:**
+1. Тестирование на локальном сервере
+2. Проверка валидации
+3. Проверка сохранения аватара
+4. Коммит и push изменений
+5. Создание PR в master (после тестирования)
diff --git a/backend/static/js/components/avatarCropperHandler.js b/backend/static/js/components/avatarCropperHandler.js
new file mode 100644
index 00000000..4ada0b2c
--- /dev/null
+++ b/backend/static/js/components/avatarCropperHandler.js
@@ -0,0 +1,436 @@
+/**
+ * avatarCropperHandler.js
+ * Компонент для обрезки аватара с требованиями FaceID
+ *
+ * Требования для FaceID:
+ * - Лицо должно занимать 70-80% кадра
+ * - Центрирование лица в кадре
+ * - Минимальный размер: 600x600px
+ * - Соотношение сторон: 1:1 (квадрат)
+ * - Формат: JPEG (оптимизированный для распознавания)
+ *
+ * Зависимости: Cropper.js (https://github.com/fengyuanchen/cropperjs)
+ */
+
+const DEFAULT_CONFIG = {
+ // Селекторы
+ fileInputSelector: 'input[type="file"][name="avatar"]',
+ modalId: 'avatarCropperModal',
+ previewImageId: 'avatarPreview',
+ placeholderId: 'avatarPlaceholder',
+
+ // Настройки Cropper.js
+ cropperOptions: {
+ aspectRatio: 1, // Квадратное фото
+ viewMode: 2, // Ограничить canvas областью контейнера
+ dragMode: 'move', // Перемещение изображения
+ autoCropArea: 0.8, // Начальный размер области обрезки (80%)
+ restore: false,
+ guides: true,
+ center: true,
+ highlight: true,
+ cropBoxMovable: true,
+ cropBoxResizable: true,
+ toggleDragModeOnDblclick: false,
+ minCropBoxWidth: 200,
+ minCropBoxHeight: 200,
+ },
+
+ // Требования для FaceID
+ minOutputSize: 600, // Минимальный размер выходного изображения
+ outputSize: 800, // Рекомендуемый размер выходного изображения
+ outputQuality: 0.92, // Качество JPEG (0.0 - 1.0)
+ outputFormat: 'image/jpeg', // Формат выходного файла
+
+ // Тексты для UI
+ texts: {
+ modalTitle: 'Обрезка фото для FaceID',
+ instructions: `
+
+
Требования для системы распознавания лиц:
+
+ - Лицо должно занимать 70-80% кадра
+ - Расположите лицо в центре рамки
+ - Избегайте теней и бликов
+ - Смотрите прямо в камеру
+ - Нейтральное выражение лица
+
+
+ `,
+ cropButton: 'Применить обрезку',
+ cancelButton: 'Отмена',
+ errorTooSmall: 'Изображение слишком маленькое. Минимальный размер: ',
+ errorInvalidFormat: 'Неподдерживаемый формат. Используйте JPG, PNG или GIF.',
+ errorFileSize: 'Файл слишком большой. Максимум 10 МБ.',
+ }
+};
+
+/**
+ * Создает и управляет модальным окном для обрезки аватара
+ */
+export class AvatarCropper {
+ constructor(config = {}) {
+ this.config = { ...DEFAULT_CONFIG, ...config };
+ this.cropper = null;
+ this.currentFile = null;
+ this.croppedBlob = null;
+
+ this.init();
+ }
+
+ /**
+ * Инициализация компонента
+ */
+ init() {
+ this.createModal();
+ this.attachEventListeners();
+ }
+
+ /**
+ * Создает модальное окно для cropper
+ */
+ createModal() {
+ // Проверяем, не существует ли уже модальное окно
+ if (document.getElementById(this.config.modalId)) {
+ return;
+ }
+
+ const modalHtml = `
+
+
+
+
+
+ ${this.config.texts.instructions}
+
+
+
+
![]()
+
+
+
+
+
+ Размер обрезанного фото: -
+
+
+
+
+
+
+
+ `;
+
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
+ }
+
+ /**
+ * Навешивает обработчики событий
+ */
+ attachEventListeners() {
+ // Обработчик выбора файла
+ const fileInput = document.querySelector(this.config.fileInputSelector);
+ if (fileInput) {
+ fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
+ }
+
+ // Обработчик кнопки "Применить"
+ const applyBtn = document.getElementById('cropperApplyBtn');
+ if (applyBtn) {
+ applyBtn.addEventListener('click', () => this.applyCrop());
+ }
+
+ // Обработчик закрытия модального окна
+ const modal = document.getElementById(this.config.modalId);
+ if (modal) {
+ modal.addEventListener('hidden.bs.modal', () => this.destroyCropper());
+ }
+
+ // Обработчик изменения области обрезки (для обновления индикатора размера)
+ const cropperImage = document.getElementById('cropperImage');
+ if (cropperImage) {
+ cropperImage.addEventListener('crop', (e) => this.updateSizeIndicator(e.detail));
+ }
+ }
+
+ /**
+ * Обработка выбора файла
+ */
+ async handleFileSelect(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+
+ // Валидация формата
+ if (!file.type.match(/^image\/(jpeg|jpg|png|gif)$/i)) {
+ alert(this.config.texts.errorInvalidFormat);
+ event.target.value = '';
+ return;
+ }
+
+ // Валидация размера (10 МБ)
+ if (file.size > 10 * 1024 * 1024) {
+ alert(this.config.texts.errorFileSize);
+ event.target.value = '';
+ return;
+ }
+
+ this.currentFile = file;
+
+ // Проверяем размер изображения
+ const isValidSize = await this.validateImageSize(file);
+ if (!isValidSize) {
+ event.target.value = '';
+ return;
+ }
+
+ // Открываем cropper
+ this.openCropper(file);
+ }
+
+ /**
+ * Проверяет размер изображения
+ */
+ validateImageSize(file) {
+ return new Promise((resolve) => {
+ const img = new Image();
+ const url = URL.createObjectURL(file);
+
+ img.onload = () => {
+ URL.revokeObjectURL(url);
+
+ const minSize = this.config.minOutputSize;
+ if (img.width < minSize || img.height < minSize) {
+ alert(
+ `${this.config.texts.errorTooSmall}${minSize}x${minSize}px\n` +
+ `Ваше изображение: ${img.width}x${img.height}px`
+ );
+ resolve(false);
+ } else {
+ resolve(true);
+ }
+ };
+
+ img.onerror = () => {
+ URL.revokeObjectURL(url);
+ alert('Не удалось загрузить изображение');
+ resolve(false);
+ };
+
+ img.src = url;
+ });
+ }
+
+ /**
+ * Открывает модальное окно с cropper
+ */
+ openCropper(file) {
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ const cropperImage = document.getElementById('cropperImage');
+ cropperImage.src = e.target.result;
+
+ // Показываем модальное окно
+ const modal = new bootstrap.Modal(document.getElementById(this.config.modalId));
+ modal.show();
+
+ // Инициализируем Cropper.js после отображения модального окна
+ setTimeout(() => {
+ this.initCropper(cropperImage);
+ }, 200);
+ };
+
+ reader.readAsDataURL(file);
+ }
+
+ /**
+ * Инициализирует Cropper.js
+ */
+ initCropper(imageElement) {
+ // Уничтожаем предыдущий экземпляр, если есть
+ if (this.cropper) {
+ this.cropper.destroy();
+ }
+
+ // Создаем новый экземпляр Cropper
+ this.cropper = new Cropper(imageElement, {
+ ...this.config.cropperOptions,
+ ready: () => {
+ console.log('[AvatarCropper] Cropper ready');
+ this.updateSizeIndicator();
+ },
+ crop: (event) => {
+ this.updateSizeIndicator(event.detail);
+ }
+ });
+ }
+
+ /**
+ * Обновляет индикатор размера обрезанного изображения
+ */
+ updateSizeIndicator(detail) {
+ if (!this.cropper) return;
+
+ const data = detail || this.cropper.getData();
+ const width = Math.round(data.width);
+ const height = Math.round(data.height);
+
+ const indicator = document.getElementById('cropperSizeIndicator');
+ if (indicator) {
+ const sizeSpan = indicator.querySelector('.fw-bold');
+ if (sizeSpan) {
+ const minSize = this.config.minOutputSize;
+ const isValid = width >= minSize && height >= minSize;
+
+ sizeSpan.textContent = `${width}x${height}px`;
+ sizeSpan.className = isValid ? 'fw-bold text-success' : 'fw-bold text-danger';
+
+ if (!isValid) {
+ sizeSpan.textContent += ` (мин. ${minSize}x${minSize}px)`;
+ }
+ }
+ }
+ }
+
+ /**
+ * Применяет обрезку и закрывает модальное окно
+ */
+ async applyCrop() {
+ if (!this.cropper) return;
+
+ const applyBtn = document.getElementById('cropperApplyBtn');
+ const originalText = applyBtn.innerHTML;
+
+ try {
+ // Показываем индикатор загрузки
+ applyBtn.disabled = true;
+ applyBtn.innerHTML = 'Обработка...';
+
+ // Получаем обрезанное изображение
+ const canvas = this.cropper.getCroppedCanvas({
+ width: this.config.outputSize,
+ height: this.config.outputSize,
+ imageSmoothingEnabled: true,
+ imageSmoothingQuality: 'high',
+ });
+
+ // Конвертируем в Blob
+ this.croppedBlob = await new Promise((resolve) => {
+ canvas.toBlob(
+ (blob) => resolve(blob),
+ this.config.outputFormat,
+ this.config.outputQuality
+ );
+ });
+
+ // Создаем File объект из Blob
+ const croppedFile = new File(
+ [this.croppedBlob],
+ this.currentFile.name,
+ { type: this.config.outputFormat }
+ );
+
+ // Обновляем превью
+ this.updatePreview(canvas);
+
+ // Обновляем input файла
+ this.updateFileInput(croppedFile);
+
+ // Закрываем модальное окно
+ const modal = bootstrap.Modal.getInstance(document.getElementById(this.config.modalId));
+ modal.hide();
+
+ } catch (error) {
+ console.error('[AvatarCropper] Error applying crop:', error);
+ alert('Произошла ошибка при обрезке изображения');
+ } finally {
+ applyBtn.disabled = false;
+ applyBtn.innerHTML = originalText;
+ }
+ }
+
+ /**
+ * Обновляет превью аватара
+ */
+ updatePreview(canvas) {
+ const previewImg = document.getElementById(this.config.previewImageId);
+ const placeholder = document.getElementById(this.config.placeholderId);
+
+ const dataUrl = canvas.toDataURL(this.config.outputFormat, this.config.outputQuality);
+
+ if (previewImg) {
+ // Если превью - это img элемент
+ previewImg.src = dataUrl;
+ previewImg.classList.remove('d-none');
+
+ if (placeholder) {
+ placeholder.classList.add('d-none');
+ }
+ } else if (placeholder && placeholder.tagName !== 'IMG') {
+ // Если превью - это placeholder, заменяем его на img
+ const img = document.createElement('img');
+ img.id = this.config.previewImageId;
+ img.src = dataUrl;
+ img.alt = 'Фото';
+ img.className = 'rounded';
+ img.style.cssText = 'width: 120px; height: 120px; object-fit: cover; border: 2px solid #dee2e6;';
+ placeholder.replaceWith(img);
+ }
+ }
+
+ /**
+ * Обновляет input файла с обрезанным изображением
+ */
+ updateFileInput(croppedFile) {
+ const fileInput = document.querySelector(this.config.fileInputSelector);
+ if (!fileInput) return;
+
+ // Создаем новый DataTransfer объект
+ const dataTransfer = new DataTransfer();
+ dataTransfer.items.add(croppedFile);
+
+ // Обновляем files input
+ fileInput.files = dataTransfer.files;
+
+ console.log('[AvatarCropper] File input updated with cropped image:', {
+ name: croppedFile.name,
+ size: croppedFile.size,
+ type: croppedFile.type
+ });
+ }
+
+ /**
+ * Уничтожает экземпляр Cropper
+ */
+ destroyCropper() {
+ if (this.cropper) {
+ this.cropper.destroy();
+ this.cropper = null;
+ }
+
+ // Очищаем src изображения
+ const cropperImage = document.getElementById('cropperImage');
+ if (cropperImage) {
+ cropperImage.src = '';
+ }
+ }
+}
+
+/**
+ * Инициализирует Avatar Cropper
+ * @param {Object} config - Конфигурация
+ * @returns {AvatarCropper} Экземпляр AvatarCropper
+ */
+export function initAvatarCropper(config = {}) {
+ return new AvatarCropper(config);
+}
diff --git a/backend/templates/auth/register.html b/backend/templates/auth/register.html
index 958c110e..42294bdf 100644
--- a/backend/templates/auth/register.html
+++ b/backend/templates/auth/register.html
@@ -115,7 +115,13 @@
{% bootstrap_field form.avatar %}
- Рекомендуемый размер: 200×200 px
+
+
+ Требования для системы распознавания лиц (FaceID):
+ • Лицо должно занимать 70-80% кадра
+ • Минимальный размер: 600x600px
+ • Форматы: JPG, PNG, GIF. Максимум 10 МБ
+
@@ -133,20 +139,25 @@
{% endblock %}
{% block extra_js %}
+
+
+
+
+
{# Подключаем модули для управления формой сотрудника #}