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
273 lines
12 KiB
HTML
273 lines
12 KiB
HTML
|
||
{% 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 %}
|
||
|