Corp/backend/templates/auth/login.html
Игорь Надеин 6b7ca64ce4 feat(ui+calendar+auth+depts): большой апдейт календаря, форм авторизации и списка отделов
Календарь (rightbar) :

• Цвет события по умолчанию (#0d6efd) + удобная палитра (input[type=color] + быстрые свотчи).

• Детали события в модале (центрирование, перенос модала в <body> при необходимости), кнопки Редактировать/Удалить.

• Редактирование: заполнение формы из деталки, PATCH по API, возврат к календарю, безопасные обработчики (навешиваются один раз).

• Еженедельная лента теперь собирает события со всех доступных календарей (компания + отделы пользователя), дедупликация по id.

• В ленте недели: учитывается цвет события, показ источника (Компания / Отдел: N), location и краткое описание (≤20 символов).

• Устранены дубли отделов в селекторе календарей; нормализованы id; аккуратные ошибки/403; безопасная работа без токена.

Список отделов:

• Поиск по названию и руководителю (data-haystack); счётчик сотрудников обновляется только по видимым строкам.

• Кнопка «Создать отдел» в стиле списка (вся строка — кликабельна), видна только при наличии прав/статуса.

• Чёткое центрирование текста и иконки без кастомных стилей (Bootstrap-утилиты), стрелка фиксирована справа.

• Прокрутка только у списка (#deptList): шапка, поиск и кнопка остаются на месте; высота как в employees_list.html.

Auth / Регистрация / Логин / Верификация email / Сброс пароля:

• Шаблоны приведены к единой теме: уважаем переключатель тем из base.html, устранён «мигание» темы.

• RegistrationForm: валидация контактов (хотя бы один из Telegram/WhatsApp/WeChat), пароли совпадают.

• Поля телефона (регистрация/логин): inputmode='numeric', pattern для E.164, подсказки; на бэке по-прежнему нормализуется.

• Регистрация: превью аватара с заглушкой, radio для пола; убраны поля навыков/должности из формы (они не нужны здесь).

• API регистрации: принимаем avatar через Base64ImageField и сохраняем user.avatar при создании; остальная логика без изменений.

• Сообщение об ошибке «нужно хотя бы одно контактное поле» возвращается в ключе detail (совместимо с тестом).

• Логин/верификация/сброс: единые заголовки, floating labels, иконки, соответствие теме.

Техническое:

• Аккуратные утилиты JS: извлечение токена, формирование URL с диапазонами дат, нормализация событий, один раз навешиваемые обработчики.

• Безопасные fallbacks для отсутствующих узлов/данных; кросс-браузерные мелочи (overflow-scrolling, aria-атрибуты).

Примечания:

• Миграций БД нет. Изменены шаблоны, фронтовый JS и вью/сериализатор регистрации.

• Если пути файлов в проекте отличаются, коммит всё равно включает все изменённые файлы (git add -A).
2025-09-10 17:03:45 +03:00

101 lines
4.3 KiB
HTML

{% extends 'base.html' %}
{% load static django_bootstrap5 %}
{% block title %}Авторизация{% endblock %}
{# ВАЖНО: применяем тему ещё в <head>, чтобы не было «мигания» #}
{% block extra_css %}
<script>
(function(){
const root = document.documentElement;
const media = window.matchMedia('(prefers-color-scheme: dark)');
const getSaved = () => localStorage.getItem('theme') || 'auto';
const effective = (m) => m === 'auto' ? (media.matches ? 'dark' : 'light') : m;
const apply = () => root.setAttribute('data-bs-theme', effective(getSaved()));
apply();
media.addEventListener('change', () => { if (getSaved()==='auto') apply(); });
})();
</script>
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-6">
<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" action="{% url 'auth_front:login' %}">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger alert-dismissible fade show mb-4">
{% for error in form.non_field_errors %}
<i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
{% endfor %}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
<div class="mb-4 form-floating">
<input type="tel"
name="username"
class="form-control {% if form.username.errors %}is-invalid{% endif %}"
id="id_username"
value="{{ form.username.value|default:'' }}"
required autofocus>
<label for="id_username">Номер телефона или email</label>
{% if form.username.errors %}
<div class="invalid-feedback">
{{ form.username.errors|striptags }}
</div>
{% endif %}
</div>
<div class="mb-4 form-floating position-relative">
<input type="password"
name="password"
class="form-control {% if form.password.errors %}is-invalid{% endif %}"
id="id_password"
placeholder="Пароль"
required>
<label for="id_password">Пароль</label>
<i class="bi bi-eye-slash password-toggle position-absolute top-50 end-0 translate-middle-y me-3"
style="cursor: pointer; z-index: 5"></i>
{% if form.password.errors %}
<div class="invalid-feedback">
{{ form.password.errors|striptags }}
</div>
{% endif %}
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-box-arrow-in-right me-2"></i>Войти
</button>
<a href="{% url 'auth_front:password_reset' %}" class="btn btn-link mt-2">Забыли пароль?</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.password-toggle').forEach(icon => {
icon.addEventListener('click', function() {
const input = this.closest('.form-floating').querySelector('input');
input.type = input.type === 'password' ? 'text' : 'password';
this.classList.toggle('bi-eye-slash');
this.classList.toggle('bi-eye');
});
});
document.getElementById('id_username')?.focus();
});
</script>
{% endblock %}