diff --git a/smartsoltech/web/admin.py b/smartsoltech/web/admin.py index 436d38f..b023730 100644 --- a/smartsoltech/web/admin.py +++ b/smartsoltech/web/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Service, Project, Client, Order, Review, BlogPost, Category, ServiceRequest, HeroBanner, ContactInfo +from .models import Service, Project, Client, Order, Review, BlogPost, Category, ServiceRequest, HeroBanner, ContactInfo, Team, Career from .forms import ProjectForm @admin.register(ContactInfo) @@ -87,4 +87,96 @@ class CategoryAdmin(admin.ModelAdmin): class ServiceRequestAdmin(admin.ModelAdmin): list_display = ('service','token', 'client', 'created_at') search_fields = ('service','token', 'client') - list_filter = ('service','token','client') \ No newline at end of file + list_filter = ('service','token','client') + + +@admin.register(Team) +class TeamAdmin(admin.ModelAdmin): + list_display = ('first_name', 'last_name', 'position', 'department', 'is_active', 'display_order') + list_filter = ('department', 'is_active', 'show_on_about') + search_fields = ('first_name', 'last_name', 'position', 'skills') + list_editable = ('display_order', 'is_active') + + fieldsets = ( + ('Основная информация', { + 'fields': ('first_name', 'last_name', 'position', 'department') + }), + ('Контактные данные', { + 'fields': ('email', 'phone', 'photo'), + 'classes': ('collapse',) + }), + ('Профессиональная информация', { + 'fields': ('bio', 'skills', 'experience_years') + }), + ('Социальные сети', { + 'fields': ('linkedin', 'github', 'telegram'), + 'classes': ('collapse',) + }), + ('Настройки отображения', { + 'fields': ('is_active', 'show_on_about', 'display_order') + }), + ) + + def get_queryset(self, request): + return super().get_queryset(request).order_by('display_order', 'last_name') + + +@admin.register(Career) +class CareerAdmin(admin.ModelAdmin): + list_display = ('title', 'department', 'experience_level', 'employment_type', 'status', 'is_featured', 'created_at') + list_filter = ('status', 'employment_type', 'experience_level', 'department', 'is_featured') + search_fields = ('title', 'department', 'description', 'required_skills') + list_editable = ('status', 'is_featured') + + fieldsets = ( + ('Основная информация', { + 'fields': ('title', 'department', 'location', 'employment_type', 'experience_level') + }), + ('Описание вакансии', { + 'fields': ('description', 'responsibilities', 'requirements', 'benefits') + }), + ('Зарплата', { + 'fields': ('salary_min', 'salary_max', 'salary_currency'), + 'classes': ('collapse',) + }), + ('Навыки', { + 'fields': ('required_skills', 'preferred_skills'), + }), + ('Контактная информация', { + 'fields': ('contact_email', 'contact_person'), + 'classes': ('collapse',) + }), + ('Статус и метаданные', { + 'fields': ('status', 'is_featured', 'application_deadline', 'published_at') + }), + ) + + readonly_fields = ('created_at', 'updated_at') + + def get_queryset(self, request): + return super().get_queryset(request).order_by('-is_featured', '-created_at') + + def save_model(self, request, obj, form, change): + if obj.status == 'active' and not obj.published_at: + from django.utils import timezone + obj.published_at = timezone.now() + super().save_model(request, obj, form, change) + + actions = ['mark_as_active', 'mark_as_paused', 'mark_as_closed'] + + def mark_as_active(self, request, queryset): + from django.utils import timezone + updated = queryset.update(status='active') + queryset.filter(published_at__isnull=True).update(published_at=timezone.now()) + self.message_user(request, f'{updated} вакансий отмечены как активные.') + mark_as_active.short_description = "Отметить как активные" + + def mark_as_paused(self, request, queryset): + updated = queryset.update(status='paused') + self.message_user(request, f'{updated} вакансий приостановлены.') + mark_as_paused.short_description = "Приостановить" + + def mark_as_closed(self, request, queryset): + updated = queryset.update(status='closed') + self.message_user(request, f'{updated} вакансий закрыты.') + mark_as_closed.short_description = "Закрыть" \ No newline at end of file diff --git a/smartsoltech/web/migrations/0013_career_team.py b/smartsoltech/web/migrations/0013_career_team.py new file mode 100644 index 0000000..360fae6 --- /dev/null +++ b/smartsoltech/web/migrations/0013_career_team.py @@ -0,0 +1,75 @@ +# Generated by Django 5.1.1 on 2025-11-25 06:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0012_contactinfo'), + ] + + operations = [ + migrations.CreateModel( + name='Career', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200, verbose_name='Название вакансии')), + ('department', models.CharField(max_length=100, verbose_name='Отдел')), + ('location', models.CharField(default='Кванджу, Южная Корея', max_length=200, verbose_name='Местоположение')), + ('employment_type', models.CharField(choices=[('full_time', 'Полная занятость'), ('part_time', 'Частичная занятость'), ('contract', 'Контракт'), ('internship', 'Стажировка'), ('remote', 'Удаленная работа'), ('freelance', 'Фриланс')], default='full_time', max_length=20, verbose_name='Тип занятости')), + ('experience_level', models.CharField(choices=[('junior', 'Junior (0-1 год)'), ('middle', 'Middle (2-4 года)'), ('senior', 'Senior (5+ лет)'), ('lead', 'Team Lead'), ('intern', 'Стажер')], default='middle', max_length=20, verbose_name='Уровень опыта')), + ('description', models.TextField(verbose_name='Описание вакансии')), + ('responsibilities', models.TextField(verbose_name='Обязанности')), + ('requirements', models.TextField(verbose_name='Требования')), + ('benefits', models.TextField(blank=True, verbose_name='Преимущества и условия')), + ('salary_min', models.PositiveIntegerField(blank=True, null=True, verbose_name='Зарплата от (₩)')), + ('salary_max', models.PositiveIntegerField(blank=True, null=True, verbose_name='Зарплата до (₩)')), + ('salary_currency', models.CharField(default='KRW', max_length=10, verbose_name='Валюта')), + ('required_skills', models.TextField(help_text='Разделите навыки запятыми', verbose_name='Обязательные навыки')), + ('preferred_skills', models.TextField(blank=True, help_text='Разделите навыки запятыми', verbose_name='Желательные навыки')), + ('status', models.CharField(choices=[('active', 'Активная'), ('paused', 'Приостановлена'), ('closed', 'Закрыта'), ('draft', 'Черновик')], default='active', max_length=20, verbose_name='Статус')), + ('is_featured', models.BooleanField(default=False, verbose_name='Рекомендуемая вакансия')), + ('application_deadline', models.DateField(blank=True, null=True, verbose_name='Дедлайн подачи заявок')), + ('contact_email', models.EmailField(default='hr@smartsoltech.kr', max_length=254, verbose_name='Email для связи')), + ('contact_person', models.CharField(blank=True, max_length=200, verbose_name='Контактное лицо')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('published_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата публикации')), + ], + options={ + 'verbose_name': 'Вакансия', + 'verbose_name_plural': 'Карьера', + 'ordering': ['-is_featured', '-published_at', '-created_at'], + }, + ), + migrations.CreateModel( + name='Team', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=100, verbose_name='Имя')), + ('last_name', models.CharField(max_length=100, verbose_name='Фамилия')), + ('position', models.CharField(max_length=200, verbose_name='Должность')), + ('department', models.CharField(blank=True, max_length=100, verbose_name='Отдел')), + ('bio', models.TextField(blank=True, verbose_name='Биография/Описание')), + ('photo', models.ImageField(blank=True, null=True, upload_to='static/img/team/', verbose_name='Фотография')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')), + ('phone', models.CharField(blank=True, max_length=20, verbose_name='Телефон')), + ('linkedin', models.URLField(blank=True, verbose_name='LinkedIn')), + ('github', models.URLField(blank=True, verbose_name='GitHub')), + ('telegram', models.CharField(blank=True, max_length=100, verbose_name='Telegram')), + ('skills', models.TextField(blank=True, help_text='Разделите навыки запятыми', verbose_name='Навыки')), + ('experience_years', models.PositiveIntegerField(default=0, verbose_name='Лет опыта')), + ('is_active', models.BooleanField(default=True, verbose_name='Активен')), + ('display_order', models.PositiveIntegerField(default=0, verbose_name='Порядок отображения')), + ('show_on_about', models.BooleanField(default=True, verbose_name='Показывать на странице О нас')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Сотрудник', + 'verbose_name_plural': 'Команда', + 'ordering': ['display_order', 'last_name', 'first_name'], + }, + ), + ] diff --git a/smartsoltech/web/models.py b/smartsoltech/web/models.py index 2799c06..e014d6c 100644 --- a/smartsoltech/web/models.py +++ b/smartsoltech/web/models.py @@ -212,3 +212,174 @@ class Review(models.Model): def __str__(self): return f"Отзыв от {self.client.first_name} {self.client.last_name} for {self.service.name}" + +class Team(models.Model): + """Модель для управления персоналом компании""" + first_name = models.CharField(max_length=100, verbose_name="Имя") + last_name = models.CharField(max_length=100, verbose_name="Фамилия") + position = models.CharField(max_length=200, verbose_name="Должность") + department = models.CharField(max_length=100, verbose_name="Отдел", blank=True) + bio = models.TextField(verbose_name="Биография/Описание", blank=True) + photo = models.ImageField(upload_to='static/img/team/', blank=True, null=True, verbose_name="Фотография") + email = models.EmailField(blank=True, verbose_name="Email") + phone = models.CharField(max_length=20, blank=True, verbose_name="Телефон") + + # Социальные сети + linkedin = models.URLField(blank=True, verbose_name="LinkedIn") + github = models.URLField(blank=True, verbose_name="GitHub") + telegram = models.CharField(max_length=100, blank=True, verbose_name="Telegram") + + # Навыки и технологии + skills = models.TextField(blank=True, verbose_name="Навыки", help_text="Разделите навыки запятыми") + experience_years = models.PositiveIntegerField(default=0, verbose_name="Лет опыта") + + # Настройки отображения + is_active = models.BooleanField(default=True, verbose_name="Активен") + display_order = models.PositiveIntegerField(default=0, verbose_name="Порядок отображения") + show_on_about = models.BooleanField(default=True, verbose_name="Показывать на странице О нас") + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = 'Сотрудник' + verbose_name_plural = 'Команда' + ordering = ['display_order', 'last_name', 'first_name'] + + def __str__(self): + return f"{self.first_name} {self.last_name} - {self.position}" + + @property + def full_name(self): + return f"{self.first_name} {self.last_name}" + + @property + def skills_list(self): + """Возвращает список навыков""" + if self.skills: + return [skill.strip() for skill in self.skills.split(',') if skill.strip()] + return [] + + +class Career(models.Model): + """Модель для управления вакансиями и карьерными возможностями""" + + EMPLOYMENT_TYPE_CHOICES = [ + ('full_time', 'Полная занятость'), + ('part_time', 'Частичная занятость'), + ('contract', 'Контракт'), + ('internship', 'Стажировка'), + ('remote', 'Удаленная работа'), + ('freelance', 'Фриланс'), + ] + + EXPERIENCE_LEVEL_CHOICES = [ + ('junior', 'Junior (0-1 год)'), + ('middle', 'Middle (2-4 года)'), + ('senior', 'Senior (5+ лет)'), + ('lead', 'Team Lead'), + ('intern', 'Стажер'), + ] + + STATUS_CHOICES = [ + ('active', 'Активная'), + ('paused', 'Приостановлена'), + ('closed', 'Закрыта'), + ('draft', 'Черновик'), + ] + + title = models.CharField(max_length=200, verbose_name="Название вакансии") + department = models.CharField(max_length=100, verbose_name="Отдел") + location = models.CharField(max_length=200, default="Кванджу, Южная Корея", verbose_name="Местоположение") + + employment_type = models.CharField( + max_length=20, + choices=EMPLOYMENT_TYPE_CHOICES, + default='full_time', + verbose_name="Тип занятости" + ) + + experience_level = models.CharField( + max_length=20, + choices=EXPERIENCE_LEVEL_CHOICES, + default='middle', + verbose_name="Уровень опыта" + ) + + # Описание вакансии + description = models.TextField(verbose_name="Описание вакансии") + responsibilities = models.TextField(verbose_name="Обязанности") + requirements = models.TextField(verbose_name="Требования") + benefits = models.TextField(blank=True, verbose_name="Преимущества и условия") + + # Зарплатная вилка + salary_min = models.PositiveIntegerField(blank=True, null=True, verbose_name="Зарплата от (₩)") + salary_max = models.PositiveIntegerField(blank=True, null=True, verbose_name="Зарплата до (₩)") + salary_currency = models.CharField(max_length=10, default="KRW", verbose_name="Валюта") + + # Необходимые навыки + required_skills = models.TextField(verbose_name="Обязательные навыки", help_text="Разделите навыки запятыми") + preferred_skills = models.TextField(blank=True, verbose_name="Желательные навыки", help_text="Разделите навыки запятыми") + + # Статус и метаданные + status = models.CharField( + max_length=20, + choices=STATUS_CHOICES, + default='active', + verbose_name="Статус" + ) + + is_featured = models.BooleanField(default=False, verbose_name="Рекомендуемая вакансия") + application_deadline = models.DateField(blank=True, null=True, verbose_name="Дедлайн подачи заявок") + + # Контактная информация + contact_email = models.EmailField(default="hr@smartsoltech.kr", verbose_name="Email для связи") + contact_person = models.CharField(max_length=200, blank=True, verbose_name="Контактное лицо") + + # Timestamps + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + published_at = models.DateTimeField(blank=True, null=True, verbose_name="Дата публикации") + + class Meta: + verbose_name = 'Вакансия' + verbose_name_plural = 'Карьера' + ordering = ['-is_featured', '-published_at', '-created_at'] + + def __str__(self): + return f"{self.title} ({self.get_experience_level_display()})" + + @property + def required_skills_list(self): + """Возвращает список обязательных навыков""" + if self.required_skills: + return [skill.strip() for skill in self.required_skills.split(',') if skill.strip()] + return [] + + @property + def preferred_skills_list(self): + """Возвращает список желательных навыков""" + if self.preferred_skills: + return [skill.strip() for skill in self.preferred_skills.split(',') if skill.strip()] + return [] + + @property + def salary_range(self): + """Возвращает строку с зарплатной вилкой""" + if self.salary_min and self.salary_max: + return f"₩{self.salary_min:,} - ₩{self.salary_max:,}" + elif self.salary_min: + return f"от ₩{self.salary_min:,}" + elif self.salary_max: + return f"до ₩{self.salary_max:,}" + return "По договоренности" + + def is_active_position(self): + """Проверяет, активна ли вакансия""" + from django.utils import timezone + if self.status != 'active': + return False + if self.application_deadline and self.application_deadline < timezone.now().date(): + return False + return True +