Corp/backend/templates/auth/register.html
Игорь Надеин d6e974746e feat(avatar): implement avatar cropper with FaceID requirements
Implemented avatar cropping component for FaceID system integration.

Features:
- Image cropper using Cropper.js library
- FaceID requirements validation (70-80% face coverage, 600x600px min)
- Square crop area (1:1 aspect ratio)
- Output: 800x800px JPEG at 92% quality
- Real-time size indicator
- Integrated in 2 places: registration + profile editor

Components:
- avatarCropperHandler.js: Main cropper module with AvatarCropper class
- Modal UI with Bootstrap 5
- FaceID instructions and guidelines
- File validation (size, format, dimensions)

Integration:
- auth/register.html: Registration form with cropper
- employees/employee_form_personal.html: Profile editor with cropper
- employees/employee_form_scripts.html: Scripts updated

Technical details:
- CDN: Cropper.js v1.6.1
- Min input size: 600x600px
- Max file size: 10 MB
- Formats: JPG, PNG, GIF
- Output format: JPEG (optimized for face recognition)

Validation:
✓ File format validation
✓ File size validation (10 MB max)
✓ Image dimensions validation (600x600px min)
✓ Real-time crop size indicator
✓ Error handling with user-friendly messages

UI/UX:
- Modal window with instructions
- Live preview of cropped image
- Size indicator (green/red based on requirements)
- Clear FaceID guidelines for users
- Responsive layout
2025-12-18 09:56:50 +03:00

273 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html' %}
{% load static django_bootstrap5 %}
{% block title %}Регистрация сотрудника{% endblock %}
{# ВАЖНО: применяем тему ещё в <head>, чтобы не было «мигания» темы #}
{% block extra_css %}
<script src="{% static 'js/utils/themeInitializer.js' %}"></script>
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow">
{# Заголовок без жёсткой покраски — следуем теме #}
<div class="card-header bg-primary text-white">
<h3 class="mb-0">Регистрация нового сотрудника</h3>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data" novalidate>
{% csrf_token %}
<!-- Контакты -->
<div class="row g-3 mb-4">
<div class="col-md-6">
{% bootstrap_field form.email show_label=False placeholder="E-mail" %}
</div>
<div class="col-md-6">
{% bootstrap_field form.phone_number show_label=False placeholder="Телефон" %}
</div>
</div>
<!-- Мессенджеры -->
<div class="row g-3 mb-4">
<div class="col-md-4 position-relative">
{% bootstrap_field form.whatsapp show_label=False placeholder="WhatsApp" %}
<span class="position-absolute top-50 end-0 translate-middle-y me-3">
<i class="bi bi-whatsapp text-success"></i>
</span>
</div>
<div class="col-md-4 position-relative">
{% bootstrap_field form.telegram show_label=False placeholder="Telegram" %}
<span class="position-absolute top-50 end-0 translate-middle-y me-3">
<i class="bi bi-telegram"></i>
</span>
</div>
<div class="col-md-4 position-relative">
{% bootstrap_field form.wechat show_label=False placeholder="WeChat" %}
<span class="position-absolute top-50 end-0 translate-middle-y me-3">
<i class="bi bi-wechat" style="color:#07C160;"></i>
</span>
</div>
{% if form.non_field_errors %}
<div class="alert alert-danger alert-dismissible fade show mb-0 mt-3" style="border-radius:8px;">
{% for error in form.non_field_errors %}
<i class="bi bi-exclamation-circle me-2"></i>{{ error }}
{% endfor %}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
</div>
<!-- ФИО -->
<div class="row g-3 mb-4">
<div class="col-md-4">{% bootstrap_field form.last_name show_label=False placeholder="Фамилия" %}</div>
<div class="col-md-4">{% bootstrap_field form.first_name show_label=False placeholder="Имя" %}</div>
<div class="col-md-4">{% bootstrap_field form.patronymic show_label=False placeholder="Отчество" %}</div>
</div>
<!-- Дата рождения + Пол -->
<div class="row g-3 mb-4">
<div class="col-md-4">
{% bootstrap_field form.birth_date show_label=False placeholder="Дата рождения" %}
</div>
<div class="col-md-8">
<label class="form-label d-block mb-2">Пол <span class="text-danger">*</span></label>
{% bootstrap_field form.gender show_label=False %}
</div>
</div>
<!-- Пароли -->
<div class="row g-3 mb-4">
<div class="col-md-6 position-relative">
{% bootstrap_field form.password1 show_label=False placeholder="Пароль" %}
<span class="password-toggle position-absolute top-50 end-0 translate-middle-y me-3 pe-5"
data-target="#{{ form.password1.auto_id }}"
style="cursor:pointer; z-index:10;">
<i class="bi bi-eye-slash"></i>
</span>
</div>
<div class="col-md-6 position-relative">
{% bootstrap_field form.password2 show_label=False placeholder="Повторите пароль" %}
<span class="password-toggle position-absolute top-50 end-0 translate-middle-y me-3 pe-5"
data-target="#{{ form.password2.auto_id }}"
style="cursor:pointer; z-index:10;">
<i class="bi bi-eye-slash"></i>
</span>
</div>
</div>
<!-- Аватар с превью -->
<div class="row g-3 align-items-center mb-4">
<div class="col-auto">
<div class="rounded-circle border bg-body-secondary d-flex align-items-center justify-content-center"
style="width:96px;height:96px;overflow:hidden;">
<img id="avatarPreview" src=""
alt="Превью аватара"
class="d-none"
style="width:100%;height:100%;object-fit:cover;">
<i id="avatarPlaceholder" class="bi bi-person-circle fs-1 text-secondary"></i>
</div>
</div>
<div class="col">
{% bootstrap_field form.avatar %}
<small class="text-muted d-block">
<i class="bi-camera-fill me-1"></i>
<strong>Требования для системы распознавания лиц (FaceID):</strong><br>
• Лицо должно занимать 70-80% кадра<br>
• Минимальный размер: 600x600px<br>
• Форматы: JPG, PNG, GIF. Максимум 10 МБ
</small>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-person-plus me-2"></i>Зарегистрироваться
</button>
<a href="{% url 'auth_front:login' %}" class="btn btn-link">Уже есть аккаунт? Войти</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- Cropper.js для обрезки аватара -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.css" integrity="sha512-hvNR0F/e2J7zPPfLC9auFe3/SE0yG4aJCOd/qxew74NN7eyiSKjr7xJJMu1Jy2wf7FXITpWS1E/RY8yzuXN7VA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.js" integrity="sha512-9KkIqdfN7ipEW6B6k+Aq20PV31bjODg4AA52W+tYtAE0jE0kMx49bjJ3FgvS56wzmyfMUHbQ4Km2b7l9+Y/+Eg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="module">
import { initPasswordToggle } from "{% static 'js/components/passwordToggleHandler.js' %}";
import { initAvatarCropper } from "{% static 'js/components/avatarCropperHandler.js' %}";
import { initLiveValidation } from "{% static 'js/components/liveValidationHandler.js' %}";
document.addEventListener('DOMContentLoaded', function () {
// Переключение видимости пароля
initPasswordToggle();
// Avatar Cropper для FaceID (заменяет простое превью)
initAvatarCropper({
fileInputSelector: 'input[type="file"][name="{{ form.avatar.html_name }}"]',
previewImageId: 'avatarPreview',
placeholderId: 'avatarPlaceholder',
modalId: 'avatarCropperModal'
});
// Live validation - валидация в реальном времени
const validator = initLiveValidation({
formSelector: 'form',
debounce: 500, // задержка 500мс после ввода
rules: {
email: [
{ type: 'required', message: 'Email обязателен для заполнения' },
{ type: 'email', message: 'Введите корректный email (example@company.com)' }
],
phone_number: [
{ type: 'required', message: 'Номер телефона обязателен' },
{
type: 'pattern',
pattern: /^\+\d{11,14}$/,
message: 'Введите номер в формате +79991234567 (11-14 цифр с плюсом)'
}
],
first_name: [
{ type: 'required', message: 'Имя обязательно для заполнения' },
{ type: 'minLength', value: 2, message: 'Минимум 2 символа' },
{
type: 'pattern',
pattern: /^[^\d]+$/,
message: 'Имя не должно содержать цифры'
}
],
last_name: [
{ type: 'required', message: 'Фамилия обязательна для заполнения' },
{ type: 'minLength', value: 2, message: 'Минимум 2 символа' },
{
type: 'pattern',
pattern: /^[^\d]+$/,
message: 'Фамилия не должна содержать цифры'
}
],
patronymic: [
{
type: 'pattern',
pattern: /^[^\d]*$/,
message: 'Отчество не должно содержать цифры',
required: false // Поле опциональное
}
],
birth_date: [
{ type: 'required', message: 'Дата рождения обязательна' },
{
type: 'custom',
validate: (value) => {
const date = new Date(value);
const today = new Date();
const age = today.getFullYear() - date.getFullYear();
return age >= 18 && age <= 100;
},
message: 'Возраст должен быть от 18 до 100 лет'
}
],
gender: [
{ type: 'required', message: 'Необходимо указать пол' }
],
avatar: [
{ type: 'required', message: 'Необходимо загрузить фотографию' }
],
password1: [
{ type: 'required', message: 'Пароль обязателен' },
{ type: 'minLength', value: 6, message: 'Пароль должен содержать минимум 6 символов' }
],
password2: [
{ type: 'required', message: 'Подтвердите пароль' },
{
type: 'match',
field: 'password1',
message: 'Пароли не совпадают'
}
],
// Специальная проверка контактов (хотя бы один)
telegram: [
{
type: 'anyContact',
fields: ['telegram', 'whatsapp', 'wechat'],
message: 'Заполните хотя бы один мессенджер (Telegram, WhatsApp или WeChat)'
}
],
whatsapp: [
{
type: 'anyContact',
fields: ['telegram', 'whatsapp', 'wechat'],
message: 'Заполните хотя бы один мессенджер (Telegram, WhatsApp или WeChat)'
}
],
wechat: [
{
type: 'anyContact',
fields: ['telegram', 'whatsapp', 'wechat'],
message: 'Заполните хотя бы один мессенджер (Telegram, WhatsApp или WeChat)'
}
]
}
});
// При изменении password1 - ревалидируем password2
document.querySelector('[name="password1"]')?.addEventListener('input', () => {
const password2 = document.querySelector('[name="password2"]');
if (password2 && password2.value) {
validator.validateField('password2');
}
});
});
</script>
{% endblock %}