+
+{% else %}
+
+{% endif %}
+
+
{% block content %}{% endblock %}
-
-
-
+{% bootstrap_javascript %}
{% if user.is_authenticated %}
-{% endblock %}
diff --git a/backend/templates/requests_app/all_requests.html b/backend/templates/requests_app/all_requests.html
new file mode 100644
index 00000000..005f1646
--- /dev/null
+++ b/backend/templates/requests_app/all_requests.html
@@ -0,0 +1,55 @@
+{% extends 'base.html' %}
+{% load static django_bootstrap5 %}
+{% block title %}Все заявления сотрудников{% endblock %}
+{% block content %}
+
Заявления сотрудников
+
+{% if requests %}
+
+
+
+
+ | Сотрудник |
+ Тип |
+ Период |
+ Статус |
+ Дата подачи |
+ Действия |
+
+
+
+ {% for req in requests %}
+
+ | {{ req.employee.get_full_name }} |
+ {{ req.get_type_display }} |
+
+ {% if req.date_from or req.date_to %}
+ {{ req.date_from|date:'d.m.Y' }}{% if req.date_to %} — {{ req.date_to|date:'d.m.Y' }}{% endif %}
+ {% else %}
+ —
+ {% endif %}
+ |
+
+ {% if req.status == 'pending' %}
+ На рассмотрении
+ {% elif req.status == 'approved' %}
+ Одобрено
+ {% elif req.status == 'rejected' %}
+ Отклонено
+ {% endif %}
+ |
+ {{ req.created_at|date:'d.m.Y H:i' }} |
+
+
+ Рассмотреть
+
+ |
+
+ {% endfor %}
+
+
+
+{% else %}
+
Заявлений нет.
+{% endif %}
+{% endblock %}
diff --git a/backend/templates/requests_app/my_requests.html b/backend/templates/requests_app/my_requests.html
new file mode 100644
index 00000000..e05ac1d2
--- /dev/null
+++ b/backend/templates/requests_app/my_requests.html
@@ -0,0 +1,67 @@
+{% extends 'base.html' %}
+{% load static django_bootstrap5 %}
+{% block title %}Мои заявления{% endblock %}
+{% block content %}
+
Мои заявления
+
+
+ Подать новое заявление
+
+
+{% if requests %}
+
+
+
+
+ | Тип |
+ Период |
+ Статус |
+ Комментарий |
+ Дата подачи |
+ |
+
+
+
+ {% for req in requests %}
+
+ | {{ req.get_type_display }} |
+
+ {% if req.date_from or req.date_to %}
+ {{ req.date_from|date:'d.m.Y' }}{% if req.date_to %} — {{ req.date_to|date:'d.m.Y' }}{% endif %}
+ {% else %}
+ —
+ {% endif %}
+ |
+
+ {% if req.status == 'pending' %}
+ На рассмотрении
+ {% elif req.status == 'approved' %}
+ Одобрено
+ {% elif req.status == 'rejected' %}
+ Отклонено
+ {% endif %}
+ |
+
+ {{ req.comment|default:'—' }}
+ |
+ {{ req.created_at|date:'d.m.Y H:i' }} |
+
+
+ Подробнее
+
+ {% if req.status == 'pending' %}
+
+ Отозвать
+
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+
+{% else %}
+
У вас пока нет заявлений.
+{% endif %}
+
+{% endblock %}
diff --git a/backend/templates/requests_app/request_confirm_delete.html b/backend/templates/requests_app/request_confirm_delete.html
new file mode 100644
index 00000000..b6e9794b
--- /dev/null
+++ b/backend/templates/requests_app/request_confirm_delete.html
@@ -0,0 +1,27 @@
+{% extends 'base.html' %}
+{% load static django_bootstrap5 %}
+{% block title %}Отозвать заявление{% endblock %}
+{% block content %}
+
Отозвать заявление
+
+
+ Внимание! После отзыва заявление будет удалено без возможности восстановления.
+
+
+
+{% endblock %}
diff --git a/backend/templates/requests_app/request_detail.html b/backend/templates/requests_app/request_detail.html
new file mode 100644
index 00000000..e7f64033
--- /dev/null
+++ b/backend/templates/requests_app/request_detail.html
@@ -0,0 +1,42 @@
+{% extends 'base.html' %}
+{% load static django_bootstrap5 %}
+{% block title %}Заявление{% endblock %}
+{% block content %}
+
Детали заявления
+
+
+
+
+ - Тип:
+ - {{ request_obj.get_type_display }}
+
+ - Период:
+ -
+ {% if request_obj.date_from or request_obj.date_to %}
+ {{ request_obj.date_from|date:'d.m.Y' }}{% if request_obj.date_to %} — {{ request_obj.date_to|date:'d.m.Y' }}{% endif %}
+ {% else %}
+ —
+ {% endif %}
+
+
+ - Статус:
+ -
+ {% if request_obj.status == 'pending' %}
+ На рассмотрении
+ {% elif request_obj.status == 'approved' %}
+ Одобрено
+ {% elif request_obj.status == 'rejected' %}
+ Отклонено
+ {% endif %}
+
+
+ - Комментарий:
+ - {{ request_obj.comment|default:"—" }}
+
+ - Дата подачи:
+ - {{ request_obj.created_at|date:'d.m.Y H:i' }}
+
+
К списку заявлений
+
+
+{% endblock %}
diff --git a/backend/templates/requests_app/request_form.html b/backend/templates/requests_app/request_form.html
new file mode 100644
index 00000000..08b71130
--- /dev/null
+++ b/backend/templates/requests_app/request_form.html
@@ -0,0 +1,19 @@
+{% extends 'base.html' %}
+{% load static django_bootstrap5 %}
+{% block title %}Подать заявление{% endblock %}
+{% block content %}
+
+
Подать новое заявление
+
+
+
+{% endblock %}
diff --git a/backend/templates/requests_app/request_process.html b/backend/templates/requests_app/request_process.html
new file mode 100644
index 00000000..6b6af1b2
--- /dev/null
+++ b/backend/templates/requests_app/request_process.html
@@ -0,0 +1,31 @@
+{% extends 'base.html' %}
+{% load static django_bootstrap5 %}
+{% block title %}Рассмотрение заявления{% endblock %}
+{% block content %}
+
Рассмотрение заявления
+
+
+ Сотрудник: {{ request_obj.employee.get_full_name }}
+ Тип: {{ request_obj.get_type_display }}
+ Период:
+ {% if request_obj.date_from or request_obj.date_to %}
+ {{ request_obj.date_from|date:'d.m.Y' }}{% if request_obj.date_to %} — {{ request_obj.date_to|date:'d.m.Y' }}{% endif %}
+ {% else %}
+ —
+ {% endif %}
+
+ Комментарий сотрудника: {{ request_obj.comment|default:"—" }}
+ Дата подачи: {{ request_obj.created_at|date:'d.m.Y H:i' }}
+
+
+
+{% endblock %}
diff --git a/backend/users/admin.py b/backend/users/admin.py
deleted file mode 100644
index 0ef20c74..00000000
--- a/backend/users/admin.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from django.contrib import admin
-from django.utils.translation import gettext_lazy as _
-
-from .models import Employee, EmployeeAction
-
-
-@admin.register(Employee)
-class EmployeeAdmin(admin.ModelAdmin):
- list_display = (
- 'last_name', 'first_name', 'phone_number', 'position', 'employment_status', 'is_actually_active'
- )
- list_filter = ('position',)
- search_fields = ('last_name', 'first_name', 'phone_number', 'email')
- readonly_fields = ('created_at', 'updated_at')
- ordering = ('last_name', 'first_name')
- fieldsets = (
- (None, {
- 'fields': ('first_name', 'last_name', 'patronymic', 'gender', 'avatar', 'birth_date')
- }),
- (_('Contact Info'), {
- 'fields': ('phone_number', 'email', 'telegram', 'whatsapp')
- }),
- (_('Job'), {
- 'fields': ('position',)
- }),
- (_('Timestamps'), {
- 'fields': ('created_at', 'updated_at'),
- 'classes': ('collapse',),
- }),
- )
-
-
-@admin.register(EmployeeAction)
-class EmployeeActionAdmin(admin.ModelAdmin):
- list_display = ('employee', 'action', 'date', 'comment')
- list_filter = ('action', 'date')
- search_fields = ('employee__last_name', 'employee__first_name', 'comment')
- date_hierarchy = 'date'
- ordering = ('-date',)
-
- def save_model(self, request, obj, form, change):
- # При создании или обновлении записи автоматически обновляем статус сотрудника
- super().save_model(request, obj, form, change)
\ No newline at end of file
diff --git a/backend/users/apps.py b/backend/users/apps.py
deleted file mode 100644
index 9677be11..00000000
--- a/backend/users/apps.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# users/apps.py
-from django.apps import AppConfig
-
-
-class UsersConfig(AppConfig):
- name = 'users'
-
- def ready(self):
- import users.signals # noqa
diff --git a/backend/users/constants.py b/backend/users/constants.py
deleted file mode 100644
index cc63030a..00000000
--- a/backend/users/constants.py
+++ /dev/null
@@ -1,5 +0,0 @@
-GENDER_CHOICES = (
- (1, 'Мужской'),
- (2, 'Женский'),
- (0, 'Не указан'),
-)
diff --git a/backend/users/migrations/0002_employee_gender.py b/backend/users/migrations/0002_employee_gender.py
deleted file mode 100644
index c668c2db..00000000
--- a/backend/users/migrations/0002_employee_gender.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 5.2.4 on 2025-07-24 09:24
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('users', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='employee',
- name='gender',
- field=models.PositiveSmallIntegerField(choices=[(1, 'Мужской'), (2, 'Женский'), (0, 'Не указан')], default=0, verbose_name='Пол'),
- ),
- ]
diff --git a/backend/users/migrations/0003_employee_birth_date_employee_created_at_and_more.py b/backend/users/migrations/0003_employee_birth_date_employee_created_at_and_more.py
deleted file mode 100644
index 7cd716cf..00000000
--- a/backend/users/migrations/0003_employee_birth_date_employee_created_at_and_more.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Generated by Django 5.2.4 on 2025-07-25 06:35
-
-import django.db.models.deletion
-import django.utils.timezone
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('users', '0002_employee_gender'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='employee',
- name='birth_date',
- field=models.DateField(blank=True, null=True, verbose_name='Дата рождения'),
- ),
- migrations.AddField(
- model_name='employee',
- name='created_at',
- field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
- preserve_default=False,
- ),
- migrations.AddField(
- model_name='employee',
- name='updated_at',
- field=models.DateTimeField(auto_now=True),
- ),
- migrations.CreateModel(
- name='EmployeeAction',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('action', models.CharField(choices=[('hired', 'Принят'), ('dismissed', 'Уволен'), ('on_leave', 'В отпуске'), ('returned_from_leave', 'Вернулся из отпуска'), ('on_maternity', 'В декрете'), ('returned_from_maternity', 'Вернулся из декрета'), ('transferred', 'Переведен'), ('rehired', 'Восстановлен')], max_length=32, verbose_name='Кадровое событие')),
- ('date', models.DateTimeField(verbose_name='Дата действия')),
- ('comment', models.TextField(blank=True, verbose_name='Комментарий/причина')),
- ('extra', models.JSONField(blank=True, null=True, verbose_name='Дополнительно')),
- ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='actions', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'verbose_name': 'Кадровое событие',
- 'verbose_name_plural': 'Кадровые события',
- 'ordering': ['-date'],
- },
- ),
- ]
diff --git a/backend/users/migrations/0004_alter_employee_managers.py b/backend/users/migrations/0004_alter_employee_managers.py
deleted file mode 100644
index 5fffc9bb..00000000
--- a/backend/users/migrations/0004_alter_employee_managers.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 5.2.4 on 2025-07-29 05:31
-
-import users.models
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('users', '0003_employee_birth_date_employee_created_at_and_more'),
- ]
-
- operations = [
- migrations.AlterModelManagers(
- name='employee',
- managers=[
- ('objects', users.models.EmployeeManager()),
- ],
- ),
- ]
diff --git a/backend/users/models.py b/backend/users/models.py
deleted file mode 100644
index 8aa3ce65..00000000
--- a/backend/users/models.py
+++ /dev/null
@@ -1,142 +0,0 @@
-from django.db import models
-from django.contrib.auth.models import AbstractUser
-from django.contrib.auth.base_user import BaseUserManager
-from phonenumber_field.modelfields import PhoneNumberField
-from django.core.exceptions import ValidationError
-from django.utils.translation import gettext_lazy as _
-from django.utils import timezone
-from .constants import GENDER_CHOICES
-
-
-class EmployeeManager(BaseUserManager):
- use_in_migrations = True
-
- def create_user(self, phone_number, email, password=None, **extra_fields):
- """
- Создает и сохраняет обычного сотрудника с переданным номером телефона и email.
- """
- if not phone_number:
- raise ValueError(_('Пользователь должен иметь номер телефона'))
- if not email:
- raise ValueError(_('Пользователь должен иметь email'))
- email = self.normalize_email(email)
- user = self.model(phone_number=phone_number,
- email=email, **extra_fields)
- user.set_password(password)
- user.save(using=self._db)
- return user
-
- def create_superuser(self, phone_number, email, password=None, **extra_fields):
- """
- Создает и сохраняет суперпользователя.
- """
- extra_fields.setdefault('is_staff', True)
- extra_fields.setdefault('is_superuser', True)
- extra_fields.setdefault('is_active', True)
-
- if extra_fields.get('is_staff') is not True:
- raise ValueError(_('Суперпользователь должен иметь is_staff=True'))
- if extra_fields.get('is_superuser') is not True:
- raise ValueError(
- _('Суперпользователь должен иметь is_superuser=True'))
-
- return self.create_user(phone_number, email, password, **extra_fields)
-
-
-class Employee(AbstractUser):
- gender = models.PositiveSmallIntegerField(
- _('Пол'), choices=GENDER_CHOICES, default=0)
- avatar = models.ImageField(_('Фото'), upload_to='users/avatars')
- phone_number = PhoneNumberField(
- 'Номер телефона', max_length=100, unique=True, blank=False)
- patronymic = models.CharField(_('Отчество'), max_length=100, blank=True)
- birth_date = models.DateField(_('Дата рождения'), blank=True, null=True)
- position = models.CharField(_('Должность'), max_length=255)
- email = models.EmailField(_('email address'), unique=True, blank=False)
- telegram = models.CharField(
- 'Telegram', max_length=100, blank=True)
- whatsapp = PhoneNumberField('WhatsApp', blank=True)
- username = None
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- objects = EmployeeManager()
-
- USERNAME_FIELD = 'phone_number'
- REQUIRED_FIELDS = ['first_name', 'last_name', 'email']
-
- class Meta:
- verbose_name = 'Сотрудник'
- verbose_name_plural = 'Сотрудники'
- ordering = ('last_name', 'first_name')
-
- def clean(self):
- super().clean()
- if not self.whatsapp and not self.telegram:
- raise ValidationError(
- 'Заполните хотя бы одно из полей: WhatsApp или Telegram',
- code='contact_required'
- )
-
- def __str__(self):
- return f'{self.last_name} {self.first_name}'
-
- @property
- def employment_status(self):
- """Текущий статус сотрудника (последнее кадровое событие)"""
- last_action = self.actions.order_by('-date').first()
- if last_action:
- return last_action.get_action_display()
- return 'Нет данных'
-
- @property
- def is_actually_active(self):
- """Сотрудник считается работающим по последнему действию"""
- last_action = self.actions.order_by('-date').first()
- if not last_action:
- return False
- return last_action.action in [
- EmployeeAction.ACTION_HIRED,
- EmployeeAction.ACTION_REHIRED,
- EmployeeAction.ACTION_RETURNED_FROM_LEAVE,
- EmployeeAction.ACTION_RETURNED_FROM_MATERNITY,
- EmployeeAction.ACTION_TRANSFERRED,
- ]
-
-
-class EmployeeAction(models.Model):
- ACTION_HIRED = 'hired'
- ACTION_DISMISSED = 'dismissed'
- ACTION_ON_LEAVE = 'on_leave'
- ACTION_RETURNED_FROM_LEAVE = 'returned_from_leave'
- ACTION_ON_MATERNITY = 'on_maternity'
- ACTION_RETURNED_FROM_MATERNITY = 'returned_from_maternity'
- ACTION_TRANSFERRED = 'transferred'
- ACTION_REHIRED = 'rehired'
-
- ACTION_CHOICES = [
- (ACTION_HIRED, 'Принят'),
- (ACTION_DISMISSED, 'Уволен'),
- (ACTION_ON_LEAVE, 'В отпуске'),
- (ACTION_RETURNED_FROM_LEAVE, 'Вернулся из отпуска'),
- (ACTION_ON_MATERNITY, 'В декрете'),
- (ACTION_RETURNED_FROM_MATERNITY, 'Вернулся из декрета'),
- (ACTION_TRANSFERRED, 'Переведен'),
- (ACTION_REHIRED, 'Восстановлен'),
- ]
-
- employee = models.ForeignKey(
- Employee, related_name='actions', on_delete=models.CASCADE)
- action = models.CharField(_('Кадровое событие'),
- max_length=32, choices=ACTION_CHOICES)
- date = models.DateTimeField(_('Дата действия'))
- comment = models.TextField(_('Комментарий/причина'), blank=True)
- extra = models.JSONField(_('Дополнительно'), blank=True, null=True)
-
- class Meta:
- verbose_name = _('Кадровое событие')
- verbose_name_plural = _('Кадровые события')
- ordering = ['-date']
-
- def __str__(self):
- return f'{self.employee} — {self.get_action_display()} — {self.date:%d.%m.%Y}'
diff --git a/backend/users/views.py b/backend/users/views.py
deleted file mode 100644
index 8698729a..00000000
--- a/backend/users/views.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import os
-from django.shortcuts import render, redirect
-from django.contrib.auth.decorators import login_required
-from django.contrib import messages
-
-from .forms import ProfileUpdateForm
-from bots.models import BotSubscriber
-from dotenv import load_dotenv
-
-load_dotenv()
-
-USERNAME_TELEGRAM_BOT = os.getenv('USERNAME_TELEGRAM_BOT')
-
-@login_required
-def profile(request):
- bot_sub, _ = BotSubscriber.objects.get_or_create(user=request.user)
- if request.method == 'POST':
- form = ProfileUpdateForm(request.POST, request.FILES, instance=request.user)
- if form.is_valid():
- form.save()
- return redirect('profile')
- else:
- form = ProfileUpdateForm(instance=request.user)
-
- tg_link = f"https://t.me/{USERNAME_TELEGRAM_BOT}"
-
- return render(request, 'profile.html', {
- 'form': form,
- 'tg_link': tg_link,
- 'bot_sub': bot_sub,
- })
-
-@login_required
-def avatar_remove(request):
- if request.method == 'POST':
- user = request.user
- if user.avatar:
- user.avatar.delete(save=True)
- messages.success(request, 'Аватар успешно удалён')
- return redirect('profile')
- return redirect('profile')