Merge pull request 'mobile responsiveness' (#43) from frontend into main
Reviewed-on: #43
This commit is contained in:
commit
c4115efb52
6
.env
6
.env
@ -1,3 +1,3 @@
|
||||
DB_NAME=norahuniversity
|
||||
DB_USER=norahuniversity
|
||||
DB_PASSWORD=norahuniversity
|
||||
DB_NAME=haikal_db
|
||||
DB_USER=faheed
|
||||
DB_PASSWORD=Faheed@215
|
||||
@ -206,7 +206,9 @@ ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||
ACCOUNT_FORMS = {"signup": "recruitment.forms.StaffSignupForm"}
|
||||
|
||||
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
EMAIL_HOST = "10.10.1.110"
|
||||
EMAIL_PORT = 2225
|
||||
|
||||
# Crispy Forms Configuration
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
|
||||
@ -27,7 +27,8 @@ from .models import (
|
||||
Participants,
|
||||
Message,
|
||||
Person,
|
||||
Document
|
||||
Document,
|
||||
CustomUser
|
||||
)
|
||||
|
||||
# from django_summernote.widgets import SummernoteWidget
|
||||
@ -1022,13 +1023,15 @@ class HiringAgencyForm(forms.ModelForm):
|
||||
def clean_email(self):
|
||||
"""Validate email format and uniqueness"""
|
||||
email = self.cleaned_data.get("email")
|
||||
instance=self.instance
|
||||
if email:
|
||||
# Check email format
|
||||
if not "@" in email or "." not in email.split("@")[1]:
|
||||
raise ValidationError("Please enter a valid email address.")
|
||||
|
||||
# Check uniqueness (optional - remove if multiple agencies can have same email)
|
||||
instance = self.instance
|
||||
# instance = self.instance
|
||||
email = email.lower().strip()
|
||||
if not instance.pk: # Creating new instance
|
||||
if HiringAgency.objects.filter(email=email).exists():
|
||||
raise ValidationError("An agency with this email already exists.")
|
||||
@ -1039,6 +1042,16 @@ class HiringAgencyForm(forms.ModelForm):
|
||||
.exists()
|
||||
):
|
||||
raise ValidationError("An agency with this email already exists.")
|
||||
# if not instance.pk: # Creating new instance
|
||||
# if HiringAgency.objects.filter(email=email).exists():
|
||||
# raise ValidationError("An agency with this email already exists.")
|
||||
# else: # Editing existing instance
|
||||
# if (
|
||||
# HiringAgency.objects.filter(email=email)
|
||||
# .exclude(pk=instance.pk)
|
||||
# .exists()
|
||||
# ):
|
||||
# raise ValidationError("An agency with this email already exists.")
|
||||
return email.lower().strip() if email else email
|
||||
|
||||
def clean_phone(self):
|
||||
@ -2108,6 +2121,7 @@ class MessageForm(forms.ModelForm):
|
||||
"rows": 6,
|
||||
"placeholder": "Enter your message here...",
|
||||
"required": True,
|
||||
'spellcheck': 'true',
|
||||
}
|
||||
),
|
||||
"message_type": forms.Select(attrs={"class": "form-select"}),
|
||||
@ -2152,11 +2166,22 @@ class MessageForm(forms.ModelForm):
|
||||
"""Filter job options based on user type"""
|
||||
|
||||
if self.user.user_type == "agency":
|
||||
# Agency users can only see jobs assigned to their agency
|
||||
# # Agency users can only see jobs assigned to their agency
|
||||
# self.fields["job"].queryset = JobPosting.objects.filter(
|
||||
# hiring_agency__user=self.user,
|
||||
# status="ACTIVE"
|
||||
# ).order_by("-created_at")
|
||||
#
|
||||
job_assignments =AgencyJobAssignment.objects.filter(
|
||||
agency__user=self.user,
|
||||
job__status="ACTIVE"
|
||||
)
|
||||
job_ids = job_assignments.values_list('job__id', flat=True)
|
||||
self.fields["job"].queryset = JobPosting.objects.filter(
|
||||
hiring_agency__user=self.user,
|
||||
status="ACTIVE"
|
||||
id__in=job_ids
|
||||
).order_by("-created_at")
|
||||
|
||||
print("Agency user job queryset:", self.fields["job"].queryset)
|
||||
elif self.user.user_type == "candidate":
|
||||
# Candidates can only see jobs they applied for
|
||||
self.fields["job"].queryset = JobPosting.objects.filter(
|
||||
@ -2179,6 +2204,7 @@ class MessageForm(forms.ModelForm):
|
||||
self.fields["recipient"].queryset = User.objects.filter(
|
||||
user_type="staff"
|
||||
).distinct().order_by("username")
|
||||
|
||||
elif self.user.user_type == "candidate":
|
||||
# Candidates can only message staff
|
||||
self.fields["recipient"].queryset = User.objects.filter(
|
||||
@ -2276,6 +2302,11 @@ class ApplicantSignupForm(forms.ModelForm):
|
||||
raise forms.ValidationError("Passwords do not match.")
|
||||
|
||||
return cleaned_data
|
||||
def email_clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
if CustomUser.objects.filter(email=email).exists():
|
||||
raise forms.ValidationError("Email is already in use.")
|
||||
return email
|
||||
|
||||
|
||||
class DocumentUploadForm(forms.ModelForm):
|
||||
|
||||
740
recruitment/migrations/0001_initial.py
Normal file
740
recruitment/migrations/0001_initial.py
Normal file
@ -0,0 +1,740 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-27 15:36
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import django_ckeditor_5.fields
|
||||
import django_countries.fields
|
||||
import django_extensions.db.fields
|
||||
import recruitment.validators
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BreakTime',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('start_time', models.TimeField(verbose_name='Start Time')),
|
||||
('end_time', models.TimeField(verbose_name='End Time')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormStage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(help_text='Name of the stage', max_length=200)),
|
||||
('order', models.PositiveIntegerField(default=0, help_text='Order of the stage in the form')),
|
||||
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default resume stage')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Stage',
|
||||
'verbose_name_plural': 'Form Stages',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Interview',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('location_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], db_index=True, max_length=10, verbose_name='Location Type')),
|
||||
('topic', models.CharField(blank=True, help_text="e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'", max_length=255, verbose_name='Meeting/Location Topic')),
|
||||
('details_url', models.URLField(blank=True, max_length=2048, null=True, verbose_name='Meeting/Location URL')),
|
||||
('timezone', models.CharField(default='UTC', max_length=50, verbose_name='Timezone')),
|
||||
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
|
||||
('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')),
|
||||
('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended'), ('cancelled', 'Cancelled')], db_index=True, default='waiting', max_length=20)),
|
||||
('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')),
|
||||
('password', models.CharField(blank=True, max_length=20, null=True)),
|
||||
('zoom_gateway_response', models.JSONField(blank=True, null=True)),
|
||||
('participant_video', models.BooleanField(default=True)),
|
||||
('join_before_host', models.BooleanField(default=False)),
|
||||
('host_email', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('mute_upon_entry', models.BooleanField(default=False)),
|
||||
('waiting_room', models.BooleanField(default=False)),
|
||||
('physical_address', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('room_number', models.CharField(blank=True, max_length=50, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Interview Location',
|
||||
'verbose_name_plural': 'Interview Locations',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Participants',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Participant Name')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||
('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')),
|
||||
('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Source',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name')),
|
||||
('source_type', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, verbose_name='Source Type')),
|
||||
('description', models.TextField(blank=True, help_text='A description of the source', verbose_name='Description')),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, help_text='The IP address of the source', null=True, verbose_name='IP Address')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('api_key', models.CharField(blank=True, help_text='API key for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Key')),
|
||||
('api_secret', models.CharField(blank=True, help_text='API secret for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Secret')),
|
||||
('trusted_ips', models.TextField(blank=True, help_text='Comma-separated list of trusted IP addresses', null=True, verbose_name='Trusted IP Addresses')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Whether this source is active for integration', verbose_name='Active')),
|
||||
('integration_version', models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version')),
|
||||
('last_sync_at', models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At')),
|
||||
('sync_status', models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')),
|
||||
('sync_endpoint', models.URLField(blank=True, help_text='Endpoint URL for sending candidate data (for outbound sync)', null=True, verbose_name='Sync Endpoint')),
|
||||
('sync_method', models.CharField(blank=True, choices=[('POST', 'POST'), ('PUT', 'PUT')], default='POST', help_text='HTTP method for outbound sync requests', max_length=10, verbose_name='Sync Method')),
|
||||
('test_method', models.CharField(blank=True, choices=[('GET', 'GET'), ('POST', 'POST')], default='GET', help_text='HTTP method for connection testing', max_length=10, verbose_name='Test Method')),
|
||||
('custom_headers', models.TextField(blank=True, help_text='JSON object with custom HTTP headers for sync requests', null=True, verbose_name='Custom Headers')),
|
||||
('supports_outbound_sync', models.BooleanField(default=False, help_text='Whether this source supports receiving candidate data from ATS', verbose_name='Supports Outbound Sync')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Source',
|
||||
'verbose_name_plural': 'Sources',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('user_type', models.CharField(choices=[('staff', 'Staff'), ('agency', 'Agency'), ('candidate', 'Candidate')], default='staff', max_length=20, verbose_name='User Type')),
|
||||
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')),
|
||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
|
||||
('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')),
|
||||
('email', models.EmailField(error_messages={'unique': 'A user with this email already exists.'}, max_length=254, unique=True)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User',
|
||||
'verbose_name_plural': 'Users',
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormField',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('label', models.CharField(help_text='Label for the field', max_length=200)),
|
||||
('field_type', models.CharField(choices=[('text', 'Text Input'), ('email', 'Email'), ('phone', 'Phone'), ('textarea', 'Text Area'), ('file', 'File Upload'), ('date', 'Date Picker'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkboxes')], help_text='Type of the field', max_length=20)),
|
||||
('placeholder', models.CharField(blank=True, help_text='Placeholder text', max_length=200)),
|
||||
('required', models.BooleanField(default=False, help_text='Whether the field is required')),
|
||||
('order', models.PositiveIntegerField(default=0, help_text='Order of the field in the stage')),
|
||||
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default field')),
|
||||
('options', models.JSONField(blank=True, default=list, help_text='Options for selection fields (stored as JSON array)')),
|
||||
('file_types', models.CharField(blank=True, help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')", max_length=200)),
|
||||
('max_file_size', models.PositiveIntegerField(default=5, help_text='Maximum file size in MB (default: 5MB)')),
|
||||
('multiple_files', models.BooleanField(default=False, help_text='Allow multiple files to be uploaded')),
|
||||
('max_files', models.PositiveIntegerField(default=1, help_text='Maximum number of files allowed (when multiple_files is True)')),
|
||||
('stage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='recruitment.formstage')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Field',
|
||||
'verbose_name_plural': 'Form Fields',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(help_text='Name of the form template', max_length=200)),
|
||||
('description', models.TextField(blank=True, help_text='Description of the form template')),
|
||||
('is_active', models.BooleanField(default=False, help_text='Whether this template is active')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Template',
|
||||
'verbose_name_plural': 'Form Templates',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormSubmission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('submitted_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)),
|
||||
('applicant_email', models.EmailField(blank=True, db_index=True, help_text='Email of the applicant', max_length=254)),
|
||||
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)),
|
||||
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Submission',
|
||||
'verbose_name_plural': 'Form Submissions',
|
||||
'ordering': ['-submitted_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formstage',
|
||||
name='template',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HiringAgency',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')),
|
||||
('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')),
|
||||
('email', models.EmailField(blank=True, max_length=254)),
|
||||
('phone', models.CharField(blank=True, max_length=20)),
|
||||
('website', models.URLField(blank=True)),
|
||||
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
|
||||
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
|
||||
('address', models.TextField(blank=True, null=True)),
|
||||
('generated_password', models.CharField(blank=True, help_text='Generated password for agency user account', max_length=255, null=True)),
|
||||
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='agency_profile', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Hiring Agency',
|
||||
'verbose_name_plural': 'Hiring Agencies',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
|
||||
('cover_letter', models.FileField(blank=True, null=True, upload_to='cover_letters/', verbose_name='Cover Letter')),
|
||||
('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')),
|
||||
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
||||
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
||||
('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Document Review', 'Document Review'), ('Offer', 'Offer'), ('Hired', 'Hired'), ('Rejected', 'Rejected')], db_index=True, default='Applied', max_length=20, verbose_name='Stage')),
|
||||
('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=20, null=True, verbose_name='Applicant Status')),
|
||||
('exam_date', models.DateTimeField(blank=True, null=True, verbose_name='Exam Date')),
|
||||
('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=20, null=True, verbose_name='Exam Status')),
|
||||
('exam_score', models.FloatField(blank=True, null=True, verbose_name='Exam Score')),
|
||||
('interview_date', models.DateTimeField(blank=True, null=True, verbose_name='Interview Date')),
|
||||
('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=20, null=True, verbose_name='Interview Status')),
|
||||
('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')),
|
||||
('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected'), ('Pending', 'Pending')], max_length=20, null=True, verbose_name='Offer Status')),
|
||||
('hired_date', models.DateField(blank=True, null=True, verbose_name='Hired Date')),
|
||||
('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')),
|
||||
('ai_analysis_data', models.JSONField(blank=True, default=dict, help_text='Full JSON output from the resume scoring model.', null=True, verbose_name='AI Analysis Data')),
|
||||
('retry', models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry')),
|
||||
('hiring_source', models.CharField(blank=True, choices=[('Public', 'Public'), ('Internal', 'Internal'), ('Agency', 'Agency')], default='Public', max_length=255, null=True, verbose_name='Hiring Source')),
|
||||
('hiring_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='recruitment.hiringagency', verbose_name='Hiring Agency')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Application',
|
||||
'verbose_name_plural': 'Applications',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InterviewNote',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')),
|
||||
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content/Feedback')),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_notes', to=settings.AUTH_USER_MODEL, verbose_name='Author')),
|
||||
('interview', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.interview', verbose_name='Scheduled Interview')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Interview Note',
|
||||
'verbose_name_plural': 'Interview Notes',
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='JobPosting',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('department', models.CharField(blank=True, max_length=100)),
|
||||
('job_type', models.CharField(choices=[('Full-time', 'Full-time'), ('Part-time', 'Part-time'), ('Contract', 'Contract'), ('Internship', 'Internship'), ('Faculty', 'Faculty'), ('Temporary', 'Temporary')], default='Full-time', max_length=20)),
|
||||
('workplace_type', models.CharField(choices=[('On-site', 'On-site'), ('Remote', 'Remote'), ('Hybrid', 'Hybrid')], default='On-site', max_length=20)),
|
||||
('location_city', models.CharField(blank=True, max_length=100)),
|
||||
('location_state', models.CharField(blank=True, max_length=100)),
|
||||
('location_country', models.CharField(default='Saudia Arabia', max_length=100)),
|
||||
('description', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Description')),
|
||||
('qualifications', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
|
||||
('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)),
|
||||
('benefits', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
|
||||
('application_url', models.URLField(blank=True, help_text='URL where applicants apply', null=True, validators=[django.core.validators.URLValidator()])),
|
||||
('application_deadline', models.DateField(db_index=True)),
|
||||
('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
|
||||
('internal_job_id', models.CharField(editable=False, max_length=50)),
|
||||
('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)),
|
||||
('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20)),
|
||||
('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])),
|
||||
('linkedin_post_id', models.CharField(blank=True, help_text='LinkedIn post ID after posting', max_length=200)),
|
||||
('linkedin_post_url', models.URLField(blank=True, help_text='Direct URL to LinkedIn post')),
|
||||
('posted_to_linkedin', models.BooleanField(default=False)),
|
||||
('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)),
|
||||
('linkedin_posted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('linkedin_post_formated_data', models.TextField(blank=True, null=True)),
|
||||
('published_at', models.DateTimeField(blank=True, db_index=True, null=True)),
|
||||
('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)),
|
||||
('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)),
|
||||
('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')),
|
||||
('max_applications', models.PositiveIntegerField(blank=True, default=1000, help_text='Maximum number of applications allowed', null=True)),
|
||||
('cancel_reason', models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason')),
|
||||
('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')),
|
||||
('cancelled_at', models.DateTimeField(blank=True, null=True)),
|
||||
('ai_parsed', models.BooleanField(default=False, help_text='Whether the job posting has been parsed by AI', verbose_name='AI Parsed')),
|
||||
('cv_zip_file', models.FileField(blank=True, null=True, upload_to='job_zips/')),
|
||||
('zip_created', models.BooleanField(default=False)),
|
||||
('assigned_to', models.ForeignKey(blank=True, help_text='The user who has been assigned to this job', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_jobs', to=settings.AUTH_USER_MODEL, verbose_name='Assigned To')),
|
||||
('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing applicants for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')),
|
||||
('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Job Posting',
|
||||
'verbose_name_plural': 'Job Postings',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formtemplate',
|
||||
name='job',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BulkInterviewTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('start_date', models.DateField(db_index=True, verbose_name='Start Date')),
|
||||
('end_date', models.DateField(db_index=True, verbose_name='End Date')),
|
||||
('working_days', models.JSONField(verbose_name='Working Days')),
|
||||
('start_time', models.TimeField(verbose_name='Start Time')),
|
||||
('end_time', models.TimeField(verbose_name='End Time')),
|
||||
('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')),
|
||||
('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')),
|
||||
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')),
|
||||
('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')),
|
||||
('applications', models.ManyToManyField(blank=True, related_name='interview_schedules', to='recruitment.application')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interview', verbose_name='Location Template (Zoom/Onsite)')),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='job',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='recruitment.jobposting', verbose_name='Job'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AgencyJobAssignment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('max_candidates', models.PositiveIntegerField(help_text='Maximum candidates agency can submit for this job', verbose_name='Maximum Candidates')),
|
||||
('candidates_submitted', models.PositiveIntegerField(default=0, help_text='Number of candidates submitted so far', verbose_name='Candidates Submitted')),
|
||||
('assigned_date', models.DateTimeField(auto_now_add=True, verbose_name='Assigned Date')),
|
||||
('deadline_date', models.DateTimeField(help_text='Deadline for agency to submit candidates', verbose_name='Deadline Date')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
|
||||
('status', models.CharField(choices=[('ACTIVE', 'Active'), ('COMPLETED', 'Completed'), ('EXPIRED', 'Expired'), ('CANCELLED', 'Cancelled')], default='ACTIVE', max_length=20, verbose_name='Status')),
|
||||
('deadline_extended', models.BooleanField(default=False, verbose_name='Deadline Extended')),
|
||||
('original_deadline', models.DateTimeField(blank=True, help_text='Original deadline before extensions', null=True, verbose_name='Original Deadline')),
|
||||
('admin_notes', models.TextField(blank=True, help_text='Internal notes about this assignment', verbose_name='Admin Notes')),
|
||||
('agency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='job_assignments', to='recruitment.hiringagency', verbose_name='Agency')),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agency_assignments', to='recruitment.jobposting', verbose_name='Job')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Agency Job Assignment',
|
||||
'verbose_name_plural': 'Agency Job Assignments',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='JobPostingImage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('post_image', models.ImageField(upload_to='post/', validators=[recruitment.validators.validate_image_size])),
|
||||
('job', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='post_images', to='recruitment.jobposting')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('subject', models.CharField(max_length=200, verbose_name='Subject')),
|
||||
('content', models.TextField(verbose_name='Message Content')),
|
||||
('message_type', models.CharField(choices=[('direct', 'Direct Message'), ('job_related', 'Job Related'), ('system', 'System Notification')], default='direct', max_length=20, verbose_name='Message Type')),
|
||||
('is_read', models.BooleanField(default=False, verbose_name='Is Read')),
|
||||
('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Read At')),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='recruitment.jobposting', verbose_name='Related Job')),
|
||||
('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')),
|
||||
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL, verbose_name='Sender')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Message',
|
||||
'verbose_name_plural': 'Messages',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message', models.TextField(verbose_name='Notification Message')),
|
||||
('notification_type', models.CharField(choices=[('email', 'Email'), ('in_app', 'In-App')], default='email', max_length=20, verbose_name='Notification Type')),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('sent', 'Sent'), ('read', 'Read'), ('failed', 'Failed'), ('retrying', 'Retrying')], default='pending', max_length=20, verbose_name='Status')),
|
||||
('scheduled_for', models.DateTimeField(help_text='The date and time this notification is scheduled to be sent.', verbose_name='Scheduled Send Time')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('attempts', models.PositiveIntegerField(default=0, verbose_name='Send Attempts')),
|
||||
('last_error', models.TextField(blank=True, verbose_name='Last Error Message')),
|
||||
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Notification',
|
||||
'verbose_name_plural': 'Notifications',
|
||||
'ordering': ['-scheduled_for', '-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Person',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
|
||||
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||
('middle_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Middle Name')),
|
||||
('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')),
|
||||
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')),
|
||||
('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
|
||||
('gender', models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female')], max_length=1, null=True, verbose_name='Gender')),
|
||||
('gpa', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True, verbose_name='GPA')),
|
||||
('nationality', django_countries.fields.CountryField(blank=True, max_length=2, null=True, verbose_name='Nationality')),
|
||||
('address', models.TextField(blank=True, null=True, verbose_name='Address')),
|
||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
|
||||
('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')),
|
||||
('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')),
|
||||
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Person',
|
||||
'verbose_name_plural': 'People',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='person',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='recruitment.person', verbose_name='Person'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScheduledInterview',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('interview_date', models.DateField(db_index=True, verbose_name='Interview Date')),
|
||||
('interview_time', models.TimeField(verbose_name='Interview Time')),
|
||||
('interview_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], default='Remote', max_length=20)),
|
||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], db_index=True, default='scheduled', max_length=20)),
|
||||
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.application')),
|
||||
('interview', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interview', to='recruitment.interview', verbose_name='Interview/Meeting')),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')),
|
||||
('participants', models.ManyToManyField(blank=True, to='recruitment.participants')),
|
||||
('schedule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interviews', to='recruitment.bulkinterviewtemplate')),
|
||||
('system_users', models.ManyToManyField(blank=True, related_name='attended_interviews', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SharedFormTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')),
|
||||
('shared_with', models.ManyToManyField(blank=True, related_name='shared_templates', to=settings.AUTH_USER_MODEL)),
|
||||
('template', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='recruitment.formtemplate')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Shared Form Template',
|
||||
'verbose_name_plural': 'Shared Form Templates',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IntegrationLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')),
|
||||
('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')),
|
||||
('method', models.CharField(blank=True, max_length=50, verbose_name='HTTP Method')),
|
||||
('request_data', models.JSONField(blank=True, null=True, verbose_name='Request Data')),
|
||||
('response_data', models.JSONField(blank=True, null=True, verbose_name='Response Data')),
|
||||
('status_code', models.CharField(blank=True, max_length=10, verbose_name='Status Code')),
|
||||
('error_message', models.TextField(blank=True, verbose_name='Error Message')),
|
||||
('ip_address', models.GenericIPAddressField(verbose_name='IP Address')),
|
||||
('user_agent', models.CharField(blank=True, max_length=255, verbose_name='User Agent')),
|
||||
('processing_time', models.FloatField(blank=True, null=True, verbose_name='Processing Time (seconds)')),
|
||||
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_logs', to='recruitment.source', verbose_name='Source')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Integration Log',
|
||||
'verbose_name_plural': 'Integration Logs',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TrainingMaterial',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('title', models.CharField(max_length=255, verbose_name='Title')),
|
||||
('content', django_ckeditor_5.fields.CKEditor5Field(blank=True, verbose_name='Content')),
|
||||
('video_link', models.URLField(blank=True, verbose_name='Video Link')),
|
||||
('file', models.FileField(blank=True, upload_to='training_materials/', verbose_name='File')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Created by')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Training Material',
|
||||
'verbose_name_plural': 'Training Materials',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AgencyAccessLink',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('unique_token', models.CharField(editable=False, max_length=64, unique=True, verbose_name='Unique Token')),
|
||||
('access_password', models.CharField(help_text='Password for agency access', max_length=32, verbose_name='Access Password')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('expires_at', models.DateTimeField(help_text='When this access link expires', verbose_name='Expires At')),
|
||||
('last_accessed', models.DateTimeField(blank=True, null=True, verbose_name='Last Accessed')),
|
||||
('access_count', models.PositiveIntegerField(default=0, verbose_name='Access Count')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
|
||||
('assignment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='access_link', to='recruitment.agencyjobassignment', verbose_name='Assignment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Agency Access Link',
|
||||
'verbose_name_plural': 'Agency Access Links',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['unique_token'], name='recruitment_unique__f91e76_idx'), models.Index(fields=['expires_at'], name='recruitment_expires_954ed9_idx'), models.Index(fields=['is_active'], name='recruitment_is_acti_4b0804_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Document',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('object_id', models.PositiveIntegerField(verbose_name='Object ID')),
|
||||
('file', models.FileField(upload_to='documents/%Y/%m/', validators=[recruitment.validators.validate_image_size], verbose_name='Document File')),
|
||||
('document_type', models.CharField(choices=[('resume', 'Resume'), ('cover_letter', 'Cover Letter'), ('certificate', 'Certificate'), ('id_document', 'ID Document'), ('passport', 'Passport'), ('education', 'Education Document'), ('experience', 'Experience Letter'), ('other', 'Other')], default='other', max_length=20, verbose_name='Document Type')),
|
||||
('description', models.CharField(blank=True, max_length=200, verbose_name='Description')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Content Type')),
|
||||
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Uploaded By')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Document',
|
||||
'verbose_name_plural': 'Documents',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['content_type', 'object_id', 'document_type', 'created_at'], name='recruitment_content_547650_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FieldResponse',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)),
|
||||
('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')),
|
||||
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Field Response',
|
||||
'verbose_name_plural': 'Field Responses',
|
||||
'indexes': [models.Index(fields=['submission'], name='recruitment_submiss_474130_idx'), models.Index(fields=['field'], name='recruitment_field_i_097e5b_idx')],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='formsubmission',
|
||||
index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='formtemplate',
|
||||
index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='formtemplate',
|
||||
index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='agencyjobassignment',
|
||||
index=models.Index(fields=['agency', 'status'], name='recruitment_agency__491a54_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='agencyjobassignment',
|
||||
index=models.Index(fields=['job', 'status'], name='recruitment_job_id_d798a8_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='agencyjobassignment',
|
||||
index=models.Index(fields=['deadline_date'], name='recruitment_deadlin_57d3b4_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='agencyjobassignment',
|
||||
index=models.Index(fields=['is_active'], name='recruitment_is_acti_93b919_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='agencyjobassignment',
|
||||
unique_together={('agency', 'job')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='message',
|
||||
index=models.Index(fields=['sender', 'created_at'], name='recruitment_sender__49d984_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='message',
|
||||
index=models.Index(fields=['recipient', 'is_read', 'created_at'], name='recruitment_recipie_af0e6d_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='message',
|
||||
index=models.Index(fields=['job', 'created_at'], name='recruitment_job_id_18f813_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='message',
|
||||
index=models.Index(fields=['message_type', 'created_at'], name='recruitment_message_f25659_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['status', 'scheduled_for'], name='recruitment_status_0ebbe4_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['recipient'], name='recruitment_recipie_eadf4c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='person',
|
||||
index=models.Index(fields=['email'], name='recruitment_email_0b1ab1_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='person',
|
||||
index=models.Index(fields=['first_name', 'last_name'], name='recruitment_first_n_739de5_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='person',
|
||||
index=models.Index(fields=['created_at'], name='recruitment_created_33495a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='application',
|
||||
index=models.Index(fields=['person', 'job'], name='recruitment_person__34355c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='application',
|
||||
index=models.Index(fields=['stage'], name='recruitment_stage_52c2d1_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='application',
|
||||
index=models.Index(fields=['created_at'], name='recruitment_created_80633f_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='application',
|
||||
index=models.Index(fields=['person', 'stage', 'created_at'], name='recruitment_person__8715ec_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='application',
|
||||
unique_together={('person', 'job')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scheduledinterview',
|
||||
index=models.Index(fields=['job', 'status'], name='recruitment_job_id_f09e22_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scheduledinterview',
|
||||
index=models.Index(fields=['interview_date', 'interview_time'], name='recruitment_intervi_7f5877_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scheduledinterview',
|
||||
index=models.Index(fields=['application', 'job'], name='recruitment_applica_927561_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='jobposting',
|
||||
index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='jobposting',
|
||||
index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'),
|
||||
),
|
||||
]
|
||||
20
recruitment/migrations/0002_alter_person_user.py
Normal file
20
recruitment/migrations/0002_alter_person_user.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-28 10:24
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='user',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account'),
|
||||
),
|
||||
]
|
||||
0
recruitment/migrations/__init__.py
Normal file
0
recruitment/migrations/__init__.py
Normal file
@ -22,6 +22,18 @@ from django.db.models import F, Value, IntegerField, CharField
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.db.models.fields.json import KeyTransform, KeyTextTransform
|
||||
|
||||
class EmailContent(models.Model):
|
||||
subject = models.CharField(max_length=255, verbose_name=_("Subject"))
|
||||
message = CKEditor5Field(verbose_name=_("Message Body"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Email Content")
|
||||
verbose_name_plural = _("Email Contents")
|
||||
|
||||
def __str__(self):
|
||||
return self.subject
|
||||
|
||||
|
||||
class CustomUser(AbstractUser):
|
||||
"""Custom user model extending AbstractUser"""
|
||||
|
||||
@ -469,6 +481,10 @@ class JobPosting(Base):
|
||||
|
||||
return vacancy_fill_rate
|
||||
|
||||
def has_already_applied_to_this_job(self, person):
|
||||
"""Check if a given person has already applied to this job."""
|
||||
return self.applications.filter(person=person).exists()
|
||||
|
||||
|
||||
class JobPostingImage(models.Model):
|
||||
job = models.OneToOneField(
|
||||
@ -518,7 +534,7 @@ class Person(Base):
|
||||
# Optional linking to user account
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="person_profile",
|
||||
verbose_name=_("User Account"),
|
||||
null=True,
|
||||
@ -544,6 +560,17 @@ class Person(Base):
|
||||
verbose_name=_("Hiring Agency"),
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""
|
||||
Custom delete method to ensure the associated User account is also deleted.
|
||||
"""
|
||||
# 1. Delete the associated User account first, if it exists
|
||||
if self.user:
|
||||
self.user.delete()
|
||||
|
||||
# 2. Call the original delete method for the Person instance
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Person")
|
||||
verbose_name_plural = _("People")
|
||||
@ -584,6 +611,8 @@ class Person(Base):
|
||||
return Document.objects.filter(content_type=content_type, object_id=self.id)
|
||||
|
||||
|
||||
|
||||
|
||||
class Application(Base):
|
||||
"""Model to store job-specific application data"""
|
||||
|
||||
@ -2049,6 +2078,17 @@ class HiringAgency(Base):
|
||||
verbose_name_plural = _("Hiring Agencies")
|
||||
ordering = ["name"]
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""
|
||||
Custom delete method to ensure the associated User account is also deleted.
|
||||
"""
|
||||
# 1. Delete the associated User account first, if it exists
|
||||
if self.user:
|
||||
self.user.delete()
|
||||
|
||||
# 2. Call the original delete method for the Agency instance
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
class AgencyJobAssignment(Base):
|
||||
"""Assigns specific jobs to agencies with limits and deadlines"""
|
||||
|
||||
@ -1069,9 +1069,11 @@ def send_bulk_email_task(subject, message, recipient_list,attachments=None,sende
|
||||
# Since the async caller sends one task per recipient, total_recipients should be 1.
|
||||
for recipient in recipient_list:
|
||||
# The 'message' is the custom message specific to this recipient.
|
||||
if _task_send_individual_email(subject, message, recipient, attachments,sender,job):
|
||||
r=_task_send_individual_email(subject, message, recipient, attachments,sender,job)
|
||||
print(f"Email send result for {recipient}: {r}")
|
||||
if r:
|
||||
successful_sends += 1
|
||||
|
||||
print(f"successful_sends: {successful_sends} out of {total_recipients}")
|
||||
if successful_sends > 0:
|
||||
logger.info(f"Bulk email task completed successfully. Sent to {successful_sends}/{total_recipients} recipients.")
|
||||
return {
|
||||
|
||||
@ -438,7 +438,7 @@ urlpatterns = [
|
||||
name="applicant_portal_dashboard",
|
||||
),
|
||||
path(
|
||||
"applications/applications/<slug:slug>/",
|
||||
"applications/application/<slug:slug>/",
|
||||
views.applicant_application_detail,
|
||||
name="applicant_application_detail",
|
||||
),
|
||||
|
||||
@ -160,6 +160,13 @@ class PersonListView(StaffRequiredMixin, ListView):
|
||||
context_object_name = "people_list"
|
||||
def get_queryset(self):
|
||||
queryset=super().get_queryset()
|
||||
search_query=self.request.GET.get('search','')
|
||||
if search_query:
|
||||
queryset=queryset.filter(
|
||||
Q(first_name__icontains=search_query) |
|
||||
Q(last_name__icontains=search_query) |
|
||||
Q(email__icontains=search_query)
|
||||
)
|
||||
gender=self.request.GET.get('gender')
|
||||
if gender:
|
||||
queryset=queryset.filter(gender=gender)
|
||||
@ -179,6 +186,7 @@ class PersonListView(StaffRequiredMixin, ListView):
|
||||
nationality=self.request.GET.get('nationality')
|
||||
context['nationality']=nationality
|
||||
context['nationalities']=nationalities
|
||||
context['search_query'] = self.request.GET.get('search', '')
|
||||
return context
|
||||
|
||||
|
||||
@ -630,7 +638,7 @@ def job_detail(request, slug):
|
||||
# New statistics
|
||||
"avg_match_score": avg_match_score,
|
||||
"high_potential_count": high_potential_count,
|
||||
"high_potential_ratio": high_potential_ratio,
|
||||
# "high_potential_ratio": high_potential_ratio,
|
||||
"avg_t2i_days": avg_t2i_days,
|
||||
"avg_t_in_exam_days": avg_t_in_exam_days,
|
||||
"linkedin_content_form": linkedin_content_form,
|
||||
@ -700,6 +708,10 @@ def request_cvs_download(request, slug):
|
||||
job.save(update_fields=["zip_created"])
|
||||
# Use async_task to run the function in the background
|
||||
# Pass only simple arguments (like the job ID)
|
||||
if not job.applications.exists():
|
||||
messages.warning(request, _("No applications found for this job. ZIP file generation skipped."))
|
||||
return redirect('job_detail', slug=slug)
|
||||
|
||||
async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
|
||||
|
||||
# Provide user feedback and redirect
|
||||
@ -711,6 +723,9 @@ def download_ready_cvs(request, slug):
|
||||
View to serve the file once it is ready.
|
||||
"""
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
if not job.applications.exists():
|
||||
messages.warning(request, _("No applications found for this job. ZIP file download unavailable."))
|
||||
return redirect('job_detail', slug=slug)
|
||||
|
||||
if job.cv_zip_file and job.zip_created:
|
||||
# Django FileField handles the HttpResponse and file serving easily
|
||||
@ -3324,27 +3339,27 @@ def agency_detail(request, slug):
|
||||
"""View details of a specific hiring agency"""
|
||||
agency = get_object_or_404(HiringAgency, slug=slug)
|
||||
|
||||
# Get candidates associated with this agency
|
||||
candidates = Application.objects.filter(hiring_agency=agency).order_by(
|
||||
# Get applications associated with this agency
|
||||
applications = Application.objects.filter(hiring_agency=agency).order_by(
|
||||
"-created_at"
|
||||
)
|
||||
|
||||
# Statistics
|
||||
total_candidates = candidates.count()
|
||||
active_candidates = candidates.filter(
|
||||
total_applications = applications.count()
|
||||
active_applications = applications.filter(
|
||||
stage__in=["Applied", "Screening", "Exam", "Interview", "Offer"]
|
||||
).count()
|
||||
hired_candidates = candidates.filter(stage="Hired").count()
|
||||
rejected_candidates = candidates.filter(stage="Rejected").count()
|
||||
hired_applications = applications.filter(stage="Hired").count()
|
||||
rejected_applications = applications.filter(stage="Rejected").count()
|
||||
job_assignments=AgencyJobAssignment.objects.filter(agency=agency)
|
||||
print(job_assignments)
|
||||
context = {
|
||||
"agency": agency,
|
||||
"candidates": candidates[:10], # Show recent 10 candidates
|
||||
"total_candidates": total_candidates,
|
||||
"active_candidates": active_candidates,
|
||||
"hired_candidates": hired_candidates,
|
||||
"rejected_candidates": rejected_candidates,
|
||||
"applications": applications[:10], # Show recent 10 applications
|
||||
"total_applications": total_applications,
|
||||
"active_applications": active_applications,
|
||||
"hired_applications": hired_applications,
|
||||
"rejected_applications": rejected_applications,
|
||||
"generated_password": agency.generated_password
|
||||
if agency.generated_password
|
||||
else None,
|
||||
@ -4343,7 +4358,7 @@ def agency_portal_submit_application_page(request, slug):
|
||||
"total_submitted": total_submitted,
|
||||
"job": assignment.job,
|
||||
}
|
||||
return render(request, "recruitment/agency_portal_submit_candidate.html", context)
|
||||
return render(request, "recruitment/agency_portal_submit_application.html", context)
|
||||
|
||||
|
||||
@agency_user_required
|
||||
@ -4407,7 +4422,7 @@ def agency_portal_submit_application(request):
|
||||
"title": f"Submit Candidate for {assignment.job.title}",
|
||||
"button_text": "Submit Candidate",
|
||||
}
|
||||
return render(request, "recruitment/agency_portal_submit_candidate.html", context)
|
||||
return render(request, "recruitment/agency_portal_submit_application.html", context)
|
||||
|
||||
|
||||
def agency_portal_assignment_detail(request, slug):
|
||||
@ -4450,7 +4465,7 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
|
||||
return redirect("agency_portal_dashboard")
|
||||
|
||||
# Get candidates submitted by this agency for this job
|
||||
candidates = Application.objects.filter(
|
||||
applications = Application.objects.filter(
|
||||
hiring_agency=assignment.agency, job=assignment.job
|
||||
).order_by("-created_at")
|
||||
|
||||
@ -4461,7 +4476,7 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
|
||||
# No messages to mark as read
|
||||
|
||||
# Pagination for candidates
|
||||
paginator = Paginator(candidates, 20) # Show 20 candidates per page
|
||||
paginator = Paginator(applications, 20) # Show 20 candidates per page
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
@ -4471,12 +4486,12 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
|
||||
message_page_obj = message_paginator.get_page(message_page_number)
|
||||
|
||||
# Calculate progress ring offset for circular progress indicator
|
||||
total_candidates = candidates.count()
|
||||
max_candidates = assignment.max_candidates
|
||||
total_applications = applications.count()
|
||||
max_applications = assignment.max_candidates
|
||||
circumference = 326.73 # 2 * π * r where r=52
|
||||
|
||||
if max_candidates > 0:
|
||||
progress_percentage = total_candidates / max_candidates
|
||||
if max_applications > 0:
|
||||
progress_percentage = total_applications / max_applications
|
||||
stroke_dashoffset = circumference - (circumference * progress_percentage)
|
||||
else:
|
||||
stroke_dashoffset = circumference
|
||||
@ -4485,8 +4500,9 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
|
||||
"assignment": assignment,
|
||||
"page_obj": page_obj,
|
||||
"message_page_obj": message_page_obj,
|
||||
"total_candidates": total_candidates,
|
||||
"total_applications": total_applications,
|
||||
"stroke_dashoffset": stroke_dashoffset,
|
||||
"max_applications": max_applications,
|
||||
}
|
||||
return render(request, "recruitment/agency_portal_assignment_detail.html", context)
|
||||
|
||||
@ -4667,7 +4683,7 @@ def message_list(request):
|
||||
"search_query": search_query,
|
||||
}
|
||||
if request.user.user_type != "staff":
|
||||
return render(request, "messages/candidate_message_list.html", context)
|
||||
return render(request, "messages/application_message_list.html", context)
|
||||
return render(request, "messages/message_list.html", context)
|
||||
|
||||
|
||||
@ -4749,7 +4765,7 @@ def message_create(request):
|
||||
"form": form,
|
||||
}
|
||||
if request.user.user_type != "staff":
|
||||
return render(request, "messages/candidate_message_form.html", context)
|
||||
return render(request, "messages/application_message_form.html", context)
|
||||
return render(request, "messages/message_form.html", context)
|
||||
|
||||
|
||||
@ -4817,7 +4833,7 @@ def message_reply(request, message_id):
|
||||
"parent_message": parent_message,
|
||||
}
|
||||
if request.user.user_type != "staff":
|
||||
return render(request, "messages/candidate_message_form.html", context)
|
||||
return render(request, "messages/application_message_form.html", context)
|
||||
return render(request, "messages/message_form.html", context)
|
||||
|
||||
|
||||
@ -4875,7 +4891,6 @@ def message_mark_unread(request, message_id):
|
||||
|
||||
@login_required
|
||||
def message_delete(request, message_id):
|
||||
"""Delete a message"""
|
||||
"""
|
||||
Deletes a message using a POST request, primarily designed for HTMX.
|
||||
Redirects to the message list on success (either via standard redirect
|
||||
@ -4888,7 +4903,8 @@ def message_delete(request, message_id):
|
||||
Message.objects.select_related("sender", "recipient"), id=message_id
|
||||
)
|
||||
|
||||
# Check if user has permission to delete this message
|
||||
# 2. Permission Check
|
||||
# Only the sender or recipient can delete the message
|
||||
if message.sender != request.user and message.recipient != request.user:
|
||||
messages.error(request, "You don't have permission to delete this message.")
|
||||
|
||||
@ -4901,24 +4917,67 @@ def message_delete(request, message_id):
|
||||
# Standard navigation redirect
|
||||
return redirect("message_list")
|
||||
|
||||
# 3. Handle POST Request (Deletion)
|
||||
if request.method == "POST":
|
||||
message.delete()
|
||||
messages.success(request, "Message deleted successfully.")
|
||||
|
||||
# Handle HTMX requests
|
||||
if "HX-Request" in request.headers:
|
||||
return HttpResponse(status=200) # HTMX success response
|
||||
# 1. Set the HTMX response header for redirection
|
||||
response = HttpResponse(status=200)
|
||||
response["HX-Redirect"] = reverse("message_list") # <--- EXPLICIT HEADER
|
||||
return response
|
||||
|
||||
# Standard navigation fallback
|
||||
return redirect("message_list")
|
||||
|
||||
# For GET requests, show confirmation page
|
||||
context = {
|
||||
"message": message,
|
||||
"title": "Delete Message",
|
||||
"message": f"Are you sure you want to delete this message from {message.sender.get_full_name() or message.sender.username}?",
|
||||
"cancel_url": reverse("message_detail", kwargs={"message_id": message_id}),
|
||||
}
|
||||
return render(request, "messages/message_confirm_delete.html", context)
|
||||
# @login_required
|
||||
# def message_delete(request, message_id):
|
||||
# """Delete a message"""
|
||||
# """
|
||||
# Deletes a message using a POST request, primarily designed for HTMX.
|
||||
# Redirects to the message list on success (either via standard redirect
|
||||
# or HTMX's hx-redirect header).
|
||||
# """
|
||||
|
||||
# # 1. Retrieve the message
|
||||
# # Use select_related to fetch linked objects efficiently for checks/logging
|
||||
# message = get_object_or_404(
|
||||
# Message.objects.select_related("sender", "recipient"), id=message_id
|
||||
# )
|
||||
|
||||
# # Check if user has permission to delete this message
|
||||
# if message.sender != request.user and message.recipient != request.user:
|
||||
# messages.error(request, "You don't have permission to delete this message.")
|
||||
|
||||
# # HTMX requests should handle redirection via client-side logic (hx-redirect)
|
||||
# if "HX-Request" in request.headers:
|
||||
# # Returning 403 or 400 is ideal, but 200 with an empty body is often accepted
|
||||
# # by HTMX and the message is shown on the next page/refresh.
|
||||
# return HttpResponse(status=403)
|
||||
|
||||
# # Standard navigation redirect
|
||||
# return redirect("message_list")
|
||||
|
||||
# if request.method == "POST":
|
||||
# message.delete()
|
||||
# messages.success(request, "Message deleted successfully.")
|
||||
|
||||
# # Handle HTMX requests
|
||||
# if "HX-Request" in request.headers:
|
||||
# return HttpResponse(status=200) # HTMX success response
|
||||
|
||||
# return redirect("message_list")
|
||||
|
||||
# # For GET requests, show confirmation page
|
||||
# context = {
|
||||
# "message": message,
|
||||
# "title": "Delete Message",
|
||||
# "message": f"Are you sure you want to delete this message from {message.sender.get_full_name() or message.sender.username}?",
|
||||
# "cancel_url": reverse("message_detail", kwargs={"message_id": message_id}),
|
||||
# }
|
||||
# return render(request, "messages/message_confirm_delete.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@ -5038,7 +5097,7 @@ def document_upload(request, slug):
|
||||
if upload_target == 'person':
|
||||
return redirect("applicant_portal_dashboard")
|
||||
else:
|
||||
return redirect("applicant_application_detail", application_slug=application.slug)
|
||||
return redirect("applicant_application_detail", slug=application.slug)
|
||||
|
||||
# Handle GET request for AJAX
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
@ -5050,6 +5109,7 @@ def document_upload(request, slug):
|
||||
def document_delete(request, document_id):
|
||||
"""Delete a document"""
|
||||
document = get_object_or_404(Document, id=document_id)
|
||||
print(document)
|
||||
|
||||
# Initialize variables for redirection outside of the complex logic
|
||||
is_htmx = "HX-Request" in request.headers
|
||||
@ -5655,8 +5715,8 @@ def source_update(request, slug):
|
||||
context = {
|
||||
"form": form,
|
||||
"source": source,
|
||||
"title": f"Edit Source: {source.name}",
|
||||
"button_text": "Update Source",
|
||||
"title": _("Edit Source: %(name)s") % {'name': source.name},
|
||||
"button_text": _("Update Source"),
|
||||
}
|
||||
return render(request, "recruitment/source_form.html", context)
|
||||
|
||||
@ -5674,8 +5734,8 @@ def source_delete(request, slug):
|
||||
|
||||
context = {
|
||||
"source": source,
|
||||
"title": "Delete Source",
|
||||
"message": f'Are you sure you want to delete the source "{source.name}"?',
|
||||
"title": _("Delete Source: %(name)s") % {'name': source.name},
|
||||
"message": _('Are you sure you want to delete the source "%(name)s"?') % {'name': source.name},
|
||||
"cancel_url": reverse("source_detail", kwargs={"slug": source.slug}),
|
||||
}
|
||||
return render(request, "recruitment/source_confirm_delete.html", context)
|
||||
@ -5775,6 +5835,12 @@ def application_signup(request, slug):
|
||||
"recruitment/applicant_signup.html",
|
||||
{"form": form, "job": job},
|
||||
)
|
||||
else:
|
||||
# messages.error(request, "Please correct the errors below.")
|
||||
form = ApplicantSignupForm(request.POST)
|
||||
return render(
|
||||
request, "recruitment/applicant_signup.html", {"form": form, "job": job}
|
||||
)
|
||||
|
||||
form = ApplicantSignupForm()
|
||||
return render(
|
||||
|
||||
@ -279,7 +279,7 @@ def application_detail(request, slug):
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def application_resume_template_view(request, slug):
|
||||
"""Display formatted resume template for a candidate"""
|
||||
"""Display formatted resume template for a application"""
|
||||
application = get_object_or_404(models.Application, slug=slug)
|
||||
|
||||
if not request.user.is_staff:
|
||||
@ -398,7 +398,7 @@ def dashboard_view(request):
|
||||
|
||||
# --- 2. TIME SERIES: GLOBAL DAILY APPLICANTS ---
|
||||
|
||||
# Group ALL candidates by creation date
|
||||
# Group ALL applications by creation date
|
||||
global_daily_applications_qs = all_applications_queryset.annotate(
|
||||
date=TruncDate('created_at')
|
||||
).values('date').annotate(
|
||||
@ -482,7 +482,7 @@ def dashboard_view(request):
|
||||
# )
|
||||
|
||||
|
||||
# candidates_with_score_query= candidate_queryset.filter(is_resume_parsed=True).annotate(
|
||||
# applications_with_score_query= application_queryset.filter(is_resume_parsed=True).annotate(
|
||||
# # The Coalesce handles NULL values (from missing data, non-numeric data, or NullIf) and sets them to 0.
|
||||
# annotated_match_score=Coalesce(safe_match_score_cast, Value(0))
|
||||
# )
|
||||
@ -637,10 +637,10 @@ def dashboard_view(request):
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def applications_offer_view(request, slug):
|
||||
"""View for candidates in the Offer stage"""
|
||||
"""View for applications in the Offer stage"""
|
||||
job = get_object_or_404(models.JobPosting, slug=slug)
|
||||
|
||||
# Filter candidates for this specific job and stage
|
||||
# Filter applications for this specific job and stage
|
||||
applications = job.offer_applications
|
||||
|
||||
# Handle search
|
||||
|
||||
@ -732,3 +732,6 @@ html[dir="rtl"] .me-auto { margin-right: 0 !important; margin-left: auto !import
|
||||
content: ">";
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -403,13 +403,13 @@
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background: var(--success); /* Green for submit */
|
||||
background: var( --kaauh-teal-dark); /* Green for submit */
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(25, 135, 84, 0.3);
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background: #157347;
|
||||
background: var(--kaauh-teal);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
|
||||
@ -201,10 +201,10 @@
|
||||
</h4>
|
||||
|
||||
{# Tag Badge (Prominent) #}
|
||||
<span class="badge rounded-pill bg-kaauh-teal job-tag px-3 py-2 fs-6">
|
||||
|
||||
<span class="badge rounded-pill bg-kaauh-teal job-tag px-3 py-2 fs-6 d-none d-lg-inline-block">
|
||||
<i class="fas fa-tag me-1"></i>{% trans "Apply Before: " %}{{job.application_deadline}}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
{# Department/Context (Sub-text) #}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans "Careers" %} - {% block title %}{% translate "Application Form" %}{% endblock %}</title>
|
||||
<title>{% trans "Careers" %} - {% block title %}{% trans "Application Form" %}{% endblock %}</title>
|
||||
|
||||
{% comment %} Load the correct Bootstrap CSS file for RTL/LTR {% endcomment %}
|
||||
{% if LANGUAGE_CODE == 'ar' %}
|
||||
@ -309,7 +309,7 @@
|
||||
<nav id="topNavbar" class="navbar navbar-expand-lg sticky-top bg-white border-bottom" style="z-index: 1040;">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand text-dark fw-bold" href="{% url 'kaauh_career' %}">
|
||||
<img src="{% static 'image/kaauh.jpeg' %}" alt="{% translate 'KAAUH IMAGE' %}" style="height: 50px; margin-right: 10px;">
|
||||
<img src="{% static 'image/kaauh.jpeg' %}" alt="{% trans 'KAAUH IMAGE' %}" style="height: 50px; margin-right: 10px;">
|
||||
<span style="color:#00636e;">KAAUH Careers</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
@ -320,15 +320,27 @@
|
||||
<ul class="navbar-nav ms-auto">
|
||||
|
||||
{% comment %} <li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="{% url 'applicant_profile' %}">{% translate "Applications" %}</a>
|
||||
<a class="nav-link text-secondary" href="{% url 'applicant_profile' %}">{% trans "Applications" %}</a>
|
||||
</li> {% endcomment %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="{% url 'applicant_portal_dashboard' %}">{% translate "Profile" %}</a>
|
||||
|
||||
|
||||
|
||||
<li class="nav-item mx-2 mb-1">
|
||||
{% if request.user.user_type == 'candidate' and request.user.is_authenticated and request.user.profile_image.url %}
|
||||
<a href="{% url 'applicant_portal_dashboard' %}" class="mx-2">
|
||||
<img src="{{ request.user.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar"
|
||||
style="width: 50px; height: 50px; object-fit: cover; background-color: var(--kaauh-teal); display: inline-block; vertical-align: middle; border-radius: 50%;"
|
||||
title="{% trans 'Your account' %}">
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="nav-link text-primary-theme" href="{% url 'applicant_portal_dashboard' %}">{% trans "Profile" %}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="{% url 'kaauh_career' %}">{% translate "Careers" %}</a>
|
||||
<li class="nav-item mx-2 mb-1">
|
||||
<a class="nav-link text-secondary text-primary-theme" href="{% url 'kaauh_career' %}">{% trans "Careers" %}</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<button class="language-toggle-btn dropdown-toggle" type="button"
|
||||
data-bs-toggle="dropdown" data-bs-offset="0, 8" aria-expanded="false"
|
||||
|
||||
@ -50,10 +50,7 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
|
||||
<div class="container-fluid max-width-1600">
|
||||
|
||||
{# --- MOBILE BRAND LOGIC: Show small logo on mobile, large on desktop (lg) --- #}
|
||||
<a class="navbar-brand text-white d-block d-lg-none" href="{% url 'dashboard' %}" aria-label="Home">
|
||||
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" class="navbar-brand-mobile">
|
||||
</a>
|
||||
|
||||
|
||||
<a class="navbar-brand text-white d-none d-lg-block me-4 pe-4" href="{% url 'dashboard' %}" aria-label="Home">
|
||||
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 60px; height: 60px;">
|
||||
@ -118,7 +115,7 @@
|
||||
<i class="fas fa-envelope"></i>
|
||||
</a>
|
||||
</li> {% endcomment %}
|
||||
<li class="nav-item me-2">
|
||||
<li class="nav-item me-2 d-none d-lg-block">
|
||||
{% if LANGUAGE_CODE == 'en' %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
@ -137,7 +134,7 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="nav-item me-2">
|
||||
<li class="nav-item me-2 d-none d-lg-block">
|
||||
<a class="nav-link text-white" href="{% url 'message_list' %}">
|
||||
<i class="fas fa-envelope"></i> <span>{% trans "Messages" %}</span>
|
||||
</a>
|
||||
@ -189,6 +186,28 @@
|
||||
</div>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider my-1"></li>
|
||||
<div>
|
||||
<li class="nav-item me-3 dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal d-lg-none">
|
||||
{% if LANGUAGE_CODE == 'en' %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="ar" class="btn bg-primary-theme text-white" type="submit">
|
||||
<span class="me-2">🇸🇦</span> العربية
|
||||
</button>
|
||||
</form>
|
||||
{% elif LANGUAGE_CODE == 'ar' %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="en" class="btn bg-primary-theme text-white" type="submit">
|
||||
<span class="me-2">🇺🇸</span> English
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
<li class="d-lg-none"><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'message_list' %}"> <i class="fas fa-envelope fs-5 me-3"></i> <span>{% trans "Messages" %}</span></a></li>
|
||||
{% if request.user.is_authenticated %}
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'user_detail' request.user.pk %}"><i class="fas fa-user-circle me-3 fs-5"></i> <span>{% trans "My Profile" %}</span></a></li>
|
||||
|
||||
|
||||
35
templates/emails/rejection_screening_draft.html
Normal file
35
templates/emails/rejection_screening_draft.html
Normal file
@ -0,0 +1,35 @@
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px; box-sizing: border-box; background-color: #ffffff;">
|
||||
|
||||
<p style="font-family: Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #333333; margin-bottom: 20px;">
|
||||
Dear {{ applicant_name }},
|
||||
</p>
|
||||
|
||||
<p style="font-family: Arial, sans-serif; font-size: 16px; line-height: 1.6; color: #333333; margin-bottom: 15px;">
|
||||
Thank you very much for your interest in the **{{ job_title }}** position at **{{ company_name }}** and for taking the time to complete the initial screening process.
|
||||
</p>
|
||||
|
||||
<p style="font-family: Arial, sans-serif; font-size: 16px; line-height: 1.6; color: #333333; margin-bottom: 20px;">
|
||||
We carefully reviewed your qualifications and application materials. While your background is certainly impressive, we have decided to move forward with other candidates whose experience was a closer match for the specific requirements of this role at this time.
|
||||
</p>
|
||||
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="border: 1px solid #e0f2f1; background-color: #f0fdfa; border-radius: 8px; margin-bottom: 20px;">
|
||||
<tr>
|
||||
<td style="padding: 15px; font-family: Arial, sans-serif;">
|
||||
<p style="font-size: 16px; line-height: 1.5; color: #008080; margin: 0;">
|
||||
We truly appreciate you sharing your professional journey with us and encourage you to keep an eye on our <a href="{{ careers_page_link }}" style="color: #008080; text-decoration: underline;">careers page</a> for future opportunities that may be a better fit.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p style="font-family: Arial, sans-serif; font-size: 16px; line-height: 1.6; color: #333333; margin-top: 20px; margin-bottom: 5px;">
|
||||
We wish you the very best of luck in your job search and professional endeavors.
|
||||
</p>
|
||||
|
||||
<p style="font-family: Arial, sans-serif; font-size: 16px; line-height: 1.6; color: #333333; margin: 0;">
|
||||
Sincerely,<br>
|
||||
**{{ sender_name }}**<br>
|
||||
**{{ sender_title }}** | **{{ company_name }}**
|
||||
</p>
|
||||
|
||||
</div>
|
||||
@ -26,7 +26,7 @@
|
||||
<form
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
hx-post="{% url 'document_upload' application.id %}"
|
||||
hx-post="{% url 'application_document_upload' application.slug %}"
|
||||
hx-target="#documents-pane"
|
||||
hx-select="#documents-pane"
|
||||
hx-swap="outerHTML"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
{{ form.media }}
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
<div class="card-body">
|
||||
<form hx-boost="true" method="post" id="email-compose-form" action="{% url 'compose_application_email' job.slug %}"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
hx-target="#messageContent"
|
||||
hx-select="#messageContent"
|
||||
hx-push-url="false"
|
||||
@ -126,6 +126,7 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{{ form.media.css }}
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
@ -173,9 +174,12 @@
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('email-compose-form1');
|
||||
const sendBtn = document.getElementById('send-email-btn1');
|
||||
@ -388,7 +392,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
subject: subject.value,
|
||||
message: message.value,
|
||||
recipients: Array.from(form.querySelectorAll('input[name="{{ form.recipients.name }}"]:checked')).map(cb => cb.value),
|
||||
include_candidate_info: form.querySelector('#{{ form.include_candidate_info.id_for_label }}').checked,
|
||||
include_application_info: form.querySelector('#{{ form.include_application_info.id_for_label }}').checked,
|
||||
include_meeting_details: form.querySelector('#{{ form.include_meeting_details.id_for_label }}').checked
|
||||
};
|
||||
|
||||
@ -428,8 +432,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// Restore checkboxes
|
||||
if (draft.include_candidate_info) {
|
||||
form.querySelector('#{{ form.include_candidate_info.id_for_label }}').checked = draft.include_candidate_info;
|
||||
if (draft.include_application_info) {
|
||||
form.querySelector('#{{ form.include_application_info.id_for_label }}').checked = draft.include_application_info;
|
||||
}
|
||||
if (draft.include_meeting_details) {
|
||||
form.querySelector('#{{ form.include_meeting_details.id_for_label }}').checked = draft.include_meeting_details;
|
||||
@ -491,3 +495,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
console.log('Email compose form initialized');
|
||||
});
|
||||
</script>
|
||||
@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Create Onsite Interview{% endblock %}
|
||||
{% block title %}{% trans "Create Onsite Interview" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
@ -15,7 +16,7 @@
|
||||
<a href="{% url 'interview_create_type_selection' application.slug %}"
|
||||
class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
Back to Candidate List
|
||||
{% trans "Back to application List" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@ -41,7 +42,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.interview_date.id_for_label }}" class="form-label">
|
||||
<i class="fas fa-calendar me-1"></i>
|
||||
Topic
|
||||
{% trans "Topic" %}
|
||||
</label>
|
||||
{{ form.topic }}
|
||||
{% if form.topic.errors %}
|
||||
@ -57,7 +58,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.interview_date.id_for_label }}" class="form-label">
|
||||
<i class="fas fa-calendar me-1"></i>
|
||||
Interview Date
|
||||
{% trans "Interview Date" %}
|
||||
</label>
|
||||
{{ form.interview_date }}
|
||||
{% if form.interview_date.errors %}
|
||||
@ -72,7 +73,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.interview_time.id_for_label }}" class="form-label">
|
||||
<i class="fas fa-clock me-1"></i>
|
||||
Interview Time
|
||||
{% trans "Interview Time" %}
|
||||
</label>
|
||||
{{ form.interview_time }}
|
||||
{% if form.interview_time.errors %}
|
||||
@ -89,7 +90,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.duration.id_for_label }}" class="form-label">
|
||||
<i class="fas fa-hourglass-half me-1"></i>
|
||||
Duration (minutes)
|
||||
{% trans "Duration (minutes)" %}
|
||||
</label>
|
||||
{{ form.duration }}
|
||||
{% if form.duration.errors %}
|
||||
@ -104,7 +105,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.physical_address.id_for_label }}" class="form-label">
|
||||
<i class="fas fa-map-marker-alt me-1"></i>
|
||||
Physical Address
|
||||
{% trans "Physical Address" %}
|
||||
</label>
|
||||
{{ form.physical_address }}
|
||||
{% if form.physical_address.errors %}
|
||||
@ -119,7 +120,7 @@
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.room_number.id_for_label }}" class="form-label">
|
||||
<i class="fas fa-door-open me-1"></i>
|
||||
Room Number
|
||||
{% trans "Room Number" %}
|
||||
</label>
|
||||
{{ form.room_number }}
|
||||
{% if form.room_number.errors %}
|
||||
@ -133,7 +134,7 @@
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-2"></i>
|
||||
Schedule Onsite Interview
|
||||
{% trans "Schedule Onsite Interview" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -156,7 +157,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
dateInput.addEventListener('change', function() {
|
||||
if (this.value < today) {
|
||||
this.setCustomValidity('Interview date must be in the future');
|
||||
this.setCustomValidity('{% trans "Interview date must be in the future" %}');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n crispy_forms_tags %}
|
||||
|
||||
{% block title %}Create Remote Interview{% endblock %}
|
||||
{% block title %}{% trans "Create Remote Interview" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
@ -16,7 +16,7 @@
|
||||
<a href="{% url 'interview_create_type_selection' application.slug %}"
|
||||
class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
Back to Candidate List
|
||||
{% trans "Back to application List" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@ -40,7 +40,7 @@
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-2"></i>
|
||||
Schedule Remote Interview
|
||||
{% trans "Schedule Remote Interview" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -63,7 +63,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
dateInput.addEventListener('change', function() {
|
||||
if (this.value < today) {
|
||||
this.setCustomValidity('Interview date must be in the future');
|
||||
this.setCustomValidity('{% trans "Interview date must be in the future" %}');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Create Interview - Select Type{% endblock %}
|
||||
{% block title %}{% trans "Create Interview - Select Type" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
@ -24,8 +25,8 @@
|
||||
class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-video me-2"></i>
|
||||
<div class="mt-2">Remote Interview</div>
|
||||
<small class="d-block">Via Zoom/Video Conference</small>
|
||||
<div class="mt-2">{% trans "Remote Interview" %}</div>
|
||||
<small class="d-block">{% trans "Via Zoom/Video Conference" %}</small>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@ -33,8 +34,8 @@
|
||||
class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-building me-2"></i>
|
||||
<div class="mt-2">Onsite Interview</div>
|
||||
<small class="d-block">In-person at our facility</small>
|
||||
<div class="mt-2">{% trans "Onsite Interview" %}</div>
|
||||
<small class="d-block">{% trans "In-person at our facility" %}</small>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@ -44,7 +45,7 @@
|
||||
<a href="#"
|
||||
class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
Back to Candidate List
|
||||
{% trans "Back to application List" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
/* Filter Controls */
|
||||
.filter-controls {
|
||||
background-color: #f8f9fa;
|
||||
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
@ -258,8 +258,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- START OF JOB LIST CONTAINER --- #}
|
||||
|
||||
<div id="job-list">
|
||||
{% include "includes/_list_view_switcher.html" with list_id="job-list" %}
|
||||
|
||||
@ -267,7 +269,7 @@
|
||||
<div class="table-view d-none d-lg-block active">
|
||||
<div class="card shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<table class="table table-hover align-middle mb-0 d-none d-lg-block">
|
||||
|
||||
{# --- Corrected Multi-Row Header Structure --- #}
|
||||
<thead>
|
||||
@ -287,7 +289,7 @@
|
||||
|
||||
<tr class="nested-metrics-row">
|
||||
<th scope="col">{% trans "All" %}</th>
|
||||
<th scope="col">{% trans "Screened" %}</th>
|
||||
<th scope="col">{% trans "Screening" %}</th>
|
||||
<th scope="col">{% trans "Exam" %}</th>
|
||||
<th scope="col">{% trans "Interview" %}</th>
|
||||
<th scope="col">{% trans "DOC Review" %}</th>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "portal_base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% if form.instance.pk %}{% trans "Reply to Message"%}{% else %}{% trans"Compose Message"%}{% endif %}{% endblock %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% if form.instance.pk %}{% trans "Reply to Message" %}{% else %}{% trans "Compose Message" %}{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
@ -23,16 +23,20 @@
|
||||
{% if message.recipient == request.user %}
|
||||
<a href="{% url 'message_mark_unread' message.id %}"
|
||||
class="btn btn-outline-warning"
|
||||
hx-swap="outerHTML"
|
||||
hx-post="{% url 'message_mark_unread' message.id %}">
|
||||
<i class="fas fa-envelope"></i> {% trans "Mark Unread" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'message_delete' message.id %}"
|
||||
class="btn btn-outline-danger"
|
||||
hx-get="{% url 'message_delete' message.id %}"
|
||||
hx-confirm="{% trans 'Are you sure you want to delete this message?' %}">
|
||||
<i class="fas fa-trash"></i> {% trans "Delete" %}
|
||||
</a>
|
||||
<button type="button"
|
||||
class="btn btn-danger btn-lg"
|
||||
hx-post="{% url 'message_delete' message.id %}"
|
||||
hx-confirm="{% trans 'Are you sure you want to permanently delete this message? This action cannot be undone.' %}"
|
||||
hx-redirect="{% url 'message_list' %}"
|
||||
onclick="this.disabled=true;"
|
||||
title="{% trans 'Delete Message' %}">
|
||||
<i class="fas fa-trash me-1"></i> {% trans "Delete Message" %}
|
||||
</button>
|
||||
<a href="{% url 'message_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to Messages" %}
|
||||
</a>
|
||||
|
||||
@ -8,14 +8,13 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0">{% trans "Messages" %}</h4>
|
||||
<h4 class="mb-0 text-primary-theme">{% trans "Messages" %}</h4>
|
||||
<a href="{% url 'message_create' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus"></i> {% trans "Compose Message" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card mb-4">
|
||||
<div class="card mb-4 border-primary-theme-subtle">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3">
|
||||
<div class="col-md-3">
|
||||
@ -41,41 +40,41 @@
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" id="q" class="form-control"
|
||||
value="{{ search_query }}" placeholder="{% trans 'Search messages...' %}">
|
||||
<button class="btn btn-outline-secondary" type="submit">
|
||||
<button class="btn btn-outline-primary-theme" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label"> </label>
|
||||
<button type="submit" class="btn btn-secondary w-100">{% trans "Filter" %}</button>
|
||||
|
||||
<button type="submit" class="btn btn-main-action w-100">
|
||||
<i class="fa-solid fa-filter me-1"></i>{% trans "Filter" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card bg-primary-theme-subtle border-primary-theme-subtle">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{% trans "Total Messages" %}</h6>
|
||||
<h3 class="text-primary">{{ total_messages }}</h3>
|
||||
<h6 class="card-title text-primary-theme">{% trans "Total Messages" %}</h6>
|
||||
<h3 class="text-primary-theme">{{ total_messages }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card bg-warning-subtle border-warning-subtle">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{% trans "Unread Messages" %}</h6>
|
||||
<h6 class="card-title text-warning">{% trans "Unread Messages" %}</h6>
|
||||
<h3 class="text-warning">{{ unread_messages }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages List -->
|
||||
<div class="card">
|
||||
<div class="card border-primary-theme-subtle">
|
||||
<div class="card-body">
|
||||
{% if page_obj %}
|
||||
<div class="table-responsive">
|
||||
@ -93,14 +92,14 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for message in page_obj %}
|
||||
<tr class="{% if not message.is_read %}table-secondary{% endif %}">
|
||||
<tr class="{% if not message.is_read %}table-secondary-theme-light{% endif %}">
|
||||
<td>
|
||||
<a href="{% url 'message_detail' message.id %}"
|
||||
class="{% if not message.is_read %}fw-bold {% endif %}">
|
||||
{{ message.subject }}
|
||||
class="fw-bold text-primary-theme text-decoration-none">
|
||||
{{ message.subject|truncatechars:50 }}
|
||||
</a>
|
||||
{% if message.parent_message %}
|
||||
<span class="badge bg-secondary ms-2">{% trans "Reply" %}</span>
|
||||
<span class="badge bg-secondary-theme ms-2 text-decoration-none">{% trans "Reply" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ message.sender.get_full_name|default:message.sender.username }}</td>
|
||||
@ -126,7 +125,7 @@
|
||||
</a>
|
||||
{% if not message.is_read and message.recipient == request.user %}
|
||||
<a href="{% url 'message_mark_read' message.id %}"
|
||||
class="btn btn-sm btn-outline-success"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
hx-post="{% url 'message_mark_read' message.id %}"
|
||||
title="{% trans 'Mark as Read' %}">
|
||||
<i class="fas fa-check"></i>
|
||||
@ -138,7 +137,7 @@
|
||||
</a>
|
||||
<a href="{% url 'message_delete' message.id %}"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
hx-get="{% url 'message_delete' message.id %}"
|
||||
hx-post="{% url 'message_delete' message.id %}"
|
||||
hx-confirm="{% trans 'Are you sure you want to delete this message?' %}"
|
||||
title="{% trans 'Delete' %}">
|
||||
<i class="fas fa-trash"></i>
|
||||
@ -149,7 +148,7 @@
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted">
|
||||
<i class="fas fa-inbox fa-3x mb-3"></i>
|
||||
<i class="fas fa-inbox fa-3x mb-3 text-primary-theme"></i>
|
||||
<p class="mb-0">{% trans "No messages found." %}</p>
|
||||
<p class="small">{% trans "Try adjusting your filters or compose a new message." %}</p>
|
||||
</td>
|
||||
@ -159,13 +158,12 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if page_obj.has_other_pages %}
|
||||
<nav aria-label="Message pagination">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
|
||||
<a class="page-link text-primary-theme" href="?page={{ page_obj.previous_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
@ -174,18 +172,18 @@
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
<span class="page-link bg-primary-theme border-primary-theme">{{ num }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">{{ num }}</a>
|
||||
<a class="page-link text-primary-theme" href="?page={{ num }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
|
||||
<a class="page-link text-primary-theme" href="?page={{ page_obj.next_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
@ -195,7 +193,7 @@
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-inbox fa-3x mb-3"></i>
|
||||
<i class="fas fa-inbox fa-3x mb-3 text-primary-theme"></i>
|
||||
<p class="mb-0">{% trans "No messages found." %}</p>
|
||||
<p class="small">{% trans "Try adjusting your filters or compose a new message." %}</p>
|
||||
<a href="{% url 'message_create' %}" class="btn btn-main-action">
|
||||
@ -210,7 +208,7 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Auto-refresh unread count every 30 seconds
|
||||
setInterval(() => {
|
||||
@ -228,4 +226,3 @@ setInterval(() => {
|
||||
}, 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "Delete Person" %} - {{ block.super }}{% endblock %}
|
||||
{% block title %}{% trans "Delete Applicant" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
@ -50,7 +50,7 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Person Info Card */
|
||||
/* Applicant Info Card */
|
||||
.person-info {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.75rem;
|
||||
@ -176,14 +176,14 @@
|
||||
<div>
|
||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{% trans "Delete Person" %}
|
||||
{% trans "Delete Applicant" %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
{% trans "You are about to delete a person record. This action cannot be undone." %}
|
||||
{% trans "You are about to delete a Applicant record. This action cannot be undone." %}
|
||||
</p>
|
||||
</div>
|
||||
<a href="{% url 'person_detail' object.slug %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Person" %}
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Applicant" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -196,16 +196,16 @@
|
||||
</div>
|
||||
<h3 class="warning-title">{% trans "Warning: This action cannot be undone!" %}</h3>
|
||||
<p class="warning-text">
|
||||
{% trans "Deleting this person will permanently remove all associated data. Please review the information below carefully before proceeding." %}
|
||||
{% trans "Deleting this Applicant will permanently remove all associated data. Please review the information below carefully before proceeding." %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Person Information -->
|
||||
<!-- Applicant Information -->
|
||||
<div class="card kaauh-card mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-user me-2"></i>
|
||||
{% trans "Person to be Deleted" %}
|
||||
{% trans "Applicant to be Deleted" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@ -280,14 +280,14 @@
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-list me-2"></i>
|
||||
{% trans "What will happen when you delete this person?" %}
|
||||
{% trans "What will happen when you delete this Applicant?" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="consequence-list">
|
||||
<li>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
{% trans "The person profile and all personal information will be permanently deleted" %}
|
||||
{% trans "The Applicant profile and all Applicantal information will be permanently deleted" %}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
@ -334,7 +334,7 @@
|
||||
id="deleteButton"
|
||||
disabled>
|
||||
<i class="fas fa-trash me-2"></i>
|
||||
{% trans "Delete Person Permanently" %}
|
||||
{% trans "Delete Applicant Permanently" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -255,14 +255,11 @@
|
||||
{% if user.is_staff %}
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'person_update' person.slug %}" class="btn btn-light">
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Edit Person" %}
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Edit Applicant" %}
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'person_delete' person.slug %}"
|
||||
data-item-name="{{ person.get_full_name }}">
|
||||
<a href="{% url 'person_delete' person.slug %}" class="btn btn-light">
|
||||
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -270,7 +267,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Personal Information Column -->
|
||||
<!-- Applicantal Information Column -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
@ -535,19 +532,18 @@
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to People" %}
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Applicants" %}
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'person_update' person.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Edit Person" %}
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Edit Applicant" %}
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'person_delete' person.slug %}"
|
||||
data-item-name="{{ person.get_full_name }}">
|
||||
<a href="{% url 'person_delete' person.slug %}" class="btn btn-danger">
|
||||
|
||||
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -222,8 +222,8 @@
|
||||
|
||||
<!-- Table View (Default) -->
|
||||
<div class="table-view">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<div class="table-responsive d-none d-lg-block">
|
||||
<table class="table table-hover align-middle mb-0 ">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Photo" %}</th>
|
||||
@ -370,13 +370,13 @@
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-edit"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
{% comment %} <button type="button" class="btn btn-outline-danger btn-sm"
|
||||
title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'person_delete' person.slug %}"
|
||||
data-item-name="{{ person.get_full_name }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</button> {% endcomment %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -112,7 +112,7 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="{% url 'kaauh_career' %}">
|
||||
<i class="fas fa-globe me-1"></i> {% trans "KAAUH Careers" %}
|
||||
<i class="fas fa-globe me-1"></i> {% trans "Careers" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
@ -109,6 +109,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<!-- Assignment Overview -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Assignment Details Card -->
|
||||
@ -224,7 +225,7 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-users me-2"></i>
|
||||
{% trans "Submitted Applications" %} ({{ total_candidates }})
|
||||
{% trans "Submitted Applications" %} ({{ total_applications }})
|
||||
</h5>
|
||||
{% if access_link %}
|
||||
<a href="{% url 'agency_portal_login' %}" target="_blank" class="btn btn-outline-info btn-sm">
|
||||
@ -327,19 +328,19 @@
|
||||
style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/>
|
||||
</svg>
|
||||
<div class="position-absolute top-50 start-50 translate-middle text-center">
|
||||
<div class="h3 fw-bold mb-0 text-dark">{{ total_candidates }}</div>
|
||||
<div class="h3 fw-bold mb-0 text-dark">{{ total_applications }}</div>
|
||||
<div class="small text-muted text-uppercase">{% trans "of" %} {{ assignment.max_candidates}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="h4 mb-1">{{ total_candidates }}</div>
|
||||
<div class="h4 mb-1">{{ total_applications }}</div>
|
||||
<div class="text-muted">/ {{ assignment.max_candidates }} {% trans "applications" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="progress mt-3" style="height: 8px;">
|
||||
{% widthratio total_candidates assignment.max_candidates 100 as progress %}
|
||||
{% widthratio total_applications assignment.max_candidates 100 as progress %}
|
||||
<div class="progress-bar" style="width: {{ progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -353,7 +354,7 @@
|
||||
</h5>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a href=""
|
||||
<a href="{}"
|
||||
class="btn btn-outline-primary">
|
||||
<i class="fas fa-envelope me-1"></i> {% trans "Send Message" %}
|
||||
</a>
|
||||
|
||||
@ -232,8 +232,8 @@
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@ -313,7 +313,7 @@
|
||||
{{ agency.name }}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
{% trans "Hiring Agency Details and Candidate Management" %}
|
||||
{% trans "Hiring Agency Details and Application Management" %}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@ -531,7 +531,7 @@
|
||||
aria-selected="true"
|
||||
>
|
||||
<i class="fas fa-users me-1"></i>
|
||||
{% trans "Recent Candidates" %}
|
||||
{% trans "Recent Applications" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
@ -560,25 +560,25 @@
|
||||
role="tabpanel"
|
||||
aria-labelledby="candidates-tab"
|
||||
>
|
||||
{% if candidates %}
|
||||
{% for candidate in candidates %}
|
||||
{% if applications %}
|
||||
{% for application in applications %}
|
||||
<div class="candidate-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="candidate-name">{{ candidate.name }}</div>
|
||||
<div class="candidate-name">{{ application.name }}</div>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}
|
||||
{% if candidate.phone %}
|
||||
<span class="ms-3"><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</span>
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}
|
||||
{% if application.phone %}
|
||||
<span class="ms-3"><i class="fas fa-phone me-1"></i> {{ application.phone }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="stage-badge stage-{{ candidate.stage }}">
|
||||
{{ candidate.get_stage_display }}
|
||||
<span class="stage-badge stage-{{ application.stage }}">
|
||||
{{ application.get_stage_display }}
|
||||
</span>
|
||||
<div class="small text-muted mt-1">
|
||||
{{ candidate.created_at|date:"M d, Y" }}
|
||||
{{ application.created_at|date:"M d, Y" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -587,8 +587,8 @@
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-user-slash"></i>
|
||||
<h6>{% trans "No candidates yet" %}</h6>
|
||||
<p class="mb-0">{% trans "This agency hasn't submitted any candidates yet." %}</p>
|
||||
<h6>{% trans "No applications yet" %}</h6>
|
||||
<p class="mb-0">{% trans "This agency hasn't submitted any applications yet." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -654,32 +654,32 @@
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-chart-bar me-2"></i>
|
||||
{% trans "Candidate Statistics" %}
|
||||
{% trans "Application Statistics" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ total_candidates }}</div>
|
||||
<div class="stat-number">{{ total_applications }}</div>
|
||||
<div class="stat-label">{% trans "Total" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ active_candidates }}</div>
|
||||
<div class="stat-number">{{ active_applications }}</div>
|
||||
<div class="stat-label">{% trans "Active" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ hired_candidates }}</div>
|
||||
<div class="stat-number">{{ hired_applications }}</div>
|
||||
<div class="stat-label">{% trans "Hired" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ rejected_candidates }}</div>
|
||||
<div class="stat-number">{{ rejected_applications }}</div>
|
||||
<div class="stat-label">{% trans "Rejected" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -161,9 +161,9 @@
|
||||
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<h6 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-building me-2"></i> {{ title }}
|
||||
</h1>
|
||||
</h6>
|
||||
<div class="d-flex gap-2">
|
||||
{% if agency %}
|
||||
<a href="{% url 'agency_detail' agency.slug %}" class="btn btn-outline-secondary">
|
||||
@ -186,9 +186,7 @@
|
||||
<div class="current-profile">
|
||||
<h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="current-image d-flex align-items-center justify-content-center bg-light">
|
||||
<i class="fas fa-building text-muted"></i>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="mb-1">{{ agency.name }}</h5>
|
||||
{% if agency.contact_person %}
|
||||
|
||||
@ -172,8 +172,8 @@
|
||||
|
||||
<!-- Table View -->
|
||||
<div class="table-view">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<div class="table-responsive d-none d-lg-block">
|
||||
<table class="table table-hover align-middle mb-0 ">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Agency Name" %}</th>
|
||||
|
||||
@ -43,10 +43,10 @@
|
||||
border-radius: 0.35rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.status-ACTIVE { background-color: var(--kaauh-success); color: white; }
|
||||
.status-EXPIRED { background-color: var(--kaauh-danger); color: white; }
|
||||
.status-COMPLETED { background-color: var(--kaauh-info); color: white; }
|
||||
.status-CANCELLED { background-color: var(--kaauh-warning); color: #856404; }
|
||||
.status-ACTIVE { background-color: var(--kaauh-teal-dark); color: white; }
|
||||
.status-EXPIRED { background-color: var(--kaauh-teal-dark); color: white; }
|
||||
.status-COMPLETED { background-color: var(--kaauh-teal-dark); color: white; }
|
||||
.status-CANCELLED { background-color: var(--kaauh-teal-dark); color: white; }
|
||||
|
||||
.progress-ring {
|
||||
width: 120px;
|
||||
@ -115,7 +115,7 @@
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Dashboard" %}
|
||||
</a>
|
||||
<a href="{% url 'agency_portal_submit_application_page' assignment.slug %}" class="btn btn-sm btn-main-action {% if assignment.is_full %}disabled{% endif %}" >
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Submit New Candidate" %}
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Submit New application" %}
|
||||
</a>
|
||||
{% comment %} <a href="#" class="btn btn-outline-info">
|
||||
<i class="fas fa-envelope me-1"></i> {% trans "Messages" %}
|
||||
@ -164,14 +164,14 @@
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>{% trans "Expired" %}
|
||||
</small>
|
||||
{% else %}
|
||||
<small class="text-success">
|
||||
<small class="text-primary-theme">
|
||||
<i class="fas fa-clock me-1"></i>{{ assignment.days_remaining }} {% trans "days remaining" %}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small">{% trans "Maximum Candidates" %}</label>
|
||||
<div class="fw-bold">{{ assignment.max_candidates }} {% trans "candidates" %}</div>
|
||||
<label class="text-muted small">{% trans "Maximum applications" %}</label>
|
||||
<div class="fw-bold">{{max_applications }} {% trans "applications" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -195,18 +195,18 @@
|
||||
<div class="d-grid gap-2">
|
||||
{% if assignment.can_submit %}
|
||||
<a href="{% url 'agency_portal_submit_application_page' assignment.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Submit New Candidate" %}
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Submit New application" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<button class="btn btn-outline-secondary" disabled>
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Cannot Submit Candidates" %}
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Cannot Submit applications" %}
|
||||
</button>
|
||||
<div class="alert alert-warning mt-2">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{% if assignment.is_expired %}
|
||||
{% trans "This assignment has expired. Submissions are no longer accepted." %}
|
||||
{% elif assignment.is_full %}
|
||||
{% trans "Maximum candidate limit reached for this assignment." %}
|
||||
{% trans "Maximum application limit reached for this assignment." %}
|
||||
{% else %}
|
||||
{% trans "This assignment is not currently active." %}
|
||||
{% endif %}
|
||||
@ -221,14 +221,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submitted Candidates --> {% endcomment %}
|
||||
<!-- Submitted applications --> {% endcomment %}
|
||||
<div class="kaauh-card p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-users me-2"></i>
|
||||
{% trans "Submitted Candidates" %} ({{ total_candidates }})
|
||||
{% trans "Submitted applications" %} ({{ total_applications }})
|
||||
</h5>
|
||||
<span class="badge bg-info">{{ total_candidates }}/{{ assignment.max_candidates }}</span>
|
||||
<span class="badge bg-primary-theme">{{ total_applications }}/{{ max_applications }}</span>
|
||||
</div>
|
||||
|
||||
{% if page_obj %}
|
||||
@ -244,27 +244,27 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in page_obj %}
|
||||
{% for application in page_obj %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold">{{ candidate.name }}</div>
|
||||
<div class="fw-bold">{{ application.name }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="small">
|
||||
<div><i class="fas fa-envelope me-1"></i> {{ candidate.email }}</div>
|
||||
<div><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</div>
|
||||
<div><i class="fas fa-envelope me-1"></i> {{ application.email }}</div>
|
||||
<div><i class="fas fa-phone me-1"></i> {{ application.phone }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ candidate.get_stage_display }}</span>
|
||||
<span class="badge bg-primary-theme">{{ application.get_stage_display }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="small text-muted">
|
||||
{{ candidate.created_at|date:"Y-m-d H:i" }}
|
||||
{{ application.created_at|date:"Y-m-d H:i" }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'applicant_application_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Profile' %}">
|
||||
<a href="{% url 'applicant_application_detail' application.slug %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Profile' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
@ -311,9 +311,9 @@
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-users fa-2x text-muted mb-3"></i>
|
||||
<h6 class="text-muted">{% trans "No candidates submitted yet" %}</h6>
|
||||
<h6 class="text-muted">{% trans "No applications submitted yet" %}</h6>
|
||||
<p class="text-muted small">
|
||||
{% trans "Submit candidates using the form above to get started." %}
|
||||
{% trans "Submit applications using the form above to get started." %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -348,25 +348,25 @@
|
||||
style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/>
|
||||
</svg>
|
||||
<div class="progress-ring-text">
|
||||
{% widthratio total_candidates assignment.max_candidates 100 as progress %}
|
||||
{% widthratio total_applications assignment.max_candidates 100 as progress %}
|
||||
{{ progress|floatformat:0 }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="h4 mb-1">{{ total_candidates }}</div>
|
||||
<div class="text-muted">/ {{ assignment.max_candidates }} {% trans "candidates" %}</div>
|
||||
<div class="h4 mb-1">{{ total_applications }}</div>
|
||||
<div class="text-muted">/ {{ assignment.max_candidates }} {% trans "applications" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="progress mt-3" style="height: 8px;">
|
||||
{% widthratio total_candidates assignment.max_candidates 100 as progress %}
|
||||
<div class="progress-bar" style="width: {{ progress }}%"></div>
|
||||
{% widthratio total_applications assignment.max_candidates 100 as progress %}
|
||||
<div class="progress-bar bg-primary-theme" style="width: {{ progress }}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
{% if assignment.can_submit %}
|
||||
<span class="badge bg-success">{% trans "Can Submit" %}</span>
|
||||
<span class="badge bg-primary-theme">{% trans "Can Submit" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Cannot Submit" %}</span>
|
||||
{% endif %}
|
||||
@ -415,7 +415,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small">{% trans "Submission Rate" %}</label>
|
||||
<div class="fw-bold">
|
||||
{% widthratio total_candidates assignment.max_candidates 100 as progress %}
|
||||
{% widthratio total_applications assignment.max_candidates 100 as progress %}
|
||||
{{ progress|floatformat:1 }}%
|
||||
</div>
|
||||
</div>
|
||||
@ -500,7 +500,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<button type="button" class="btn btn-outline-primary" data-bs-dismiss="modal">
|
||||
{% trans "Cancel" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
@ -512,14 +512,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Candidate Modal -->
|
||||
<!-- Edit application Modal -->
|
||||
<div class="modal fade" id="editCandidateModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-edit me-2"></i>
|
||||
{% trans "Edit Candidate" %}
|
||||
{% trans "Edit application" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
@ -584,7 +584,7 @@
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-trash me-2"></i>
|
||||
{% trans "Remove Candidate" %}
|
||||
{% trans "Remove application" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
@ -594,16 +594,16 @@
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{% trans "Are you sure you want to remove this candidate? This action cannot be undone." %}
|
||||
{% trans "Are you sure you want to remove this application? This action cannot be undone." %}
|
||||
</div>
|
||||
<p><strong>{% trans "Candidate:" %}</strong> <span id="delete_candidate_name"></span></p>
|
||||
<p><strong>{% trans "Application:" %}</strong> <span id="delete_candidate_name"></span></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{% trans "Cancel" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash me-1"></i> {% trans "Remove Candidate" %}
|
||||
<i class="fas fa-trash me-1"></i> {% trans "Remove Application" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -634,12 +634,12 @@ function editCandidate(candidateId) {
|
||||
new bootstrap.Modal(document.getElementById('editCandidateModal')).show();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching candidate:', error);
|
||||
alert('{% trans "Error loading candidate data. Please try again." %}');
|
||||
console.error('Error fetching Application:', error);
|
||||
alert('{% trans "Error loading Application data. Please try again." %}');
|
||||
});
|
||||
}
|
||||
|
||||
// Delete Candidate
|
||||
// Delete Application
|
||||
function deleteCandidate(candidateId, candidateName) {
|
||||
// Update form action URL with candidate ID
|
||||
const deleteForm = document.getElementById('deleteCandidateForm');
|
||||
@ -670,12 +670,12 @@ document.getElementById('editCandidateForm').addEventListener('submit', function
|
||||
bootstrap.Modal.getInstance(document.getElementById('editCandidateModal')).hide();
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '{% trans "Error updating candidate. Please try again." %}');
|
||||
alert(data.message || '{% trans "Error updating Application. Please try again." %}');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('{% trans "Error updating candidate. Please try again." %}');
|
||||
alert('{% trans "Error updating Application. Please try again." %}');
|
||||
});
|
||||
});
|
||||
|
||||
@ -697,12 +697,12 @@ document.getElementById('deleteCandidateForm').addEventListener('submit', functi
|
||||
bootstrap.Modal.getInstance(document.getElementById('deleteCandidateModal')).hide();
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '{% trans "Error removing candidate. Please try again." %}');
|
||||
alert(data.message || '{% trans "Error removing Application. Please try again." %}');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('{% trans "Error removing candidate. Please try again." %}');
|
||||
alert('{% trans "Error removing Application. Please try again." %}');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -560,7 +560,9 @@
|
||||
{# Document List #}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for document in documents %}
|
||||
<li class="list-group-item d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center bg-white p-3">
|
||||
{# HTMX FIX: Added id to list item for hx-target #}
|
||||
<li class="list-group-item d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center bg-white p-3"
|
||||
id="document-{{ document.id }}">
|
||||
<div class="mb-2 mb-sm-0 fw-medium">
|
||||
<i class="fas fa-file-pdf me-2 text-primary-theme"></i> <strong>{{ document.document_type|title }}</strong>
|
||||
<span class="text-muted small">({{ document.file.name|split:"/"|last }})</span>
|
||||
@ -568,7 +570,15 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-muted small me-3">{% trans "Uploaded:" %} {{ document.uploaded_at|date:"d M Y" }}</span>
|
||||
<a href="{{ document.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary me-2"><i class="fas fa-eye"></i></a>
|
||||
<a href="{% url 'application_document_delete' document.id %}" class="btn btn-sm btn-outline-danger" onclick="return confirm('{% trans "Are you sure you want to delete this document?" %}')"><i class="fas fa-trash-alt"></i></a>
|
||||
|
||||
{# HTMX DELETE BUTTON #}
|
||||
<button hx-post="{% url 'application_document_delete' document.id %}"
|
||||
hx-target="#document-{{ document.id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="{% trans 'Are you sure you want to delete this file?' %}"
|
||||
class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{% empty %}
|
||||
|
||||
@ -1,35 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "Delete Application" %} - {{ block.super }}{% endblock %}
|
||||
{% block title %}{% trans "Confirm Delete" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1>
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8 col-xl-6">
|
||||
<div class="card border-danger shadow-lg">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h2 class="h4 mb-0 d-flex align-items-center">
|
||||
<svg class="heroicon me-2" viewBox="0 0 24 24" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10 11V17"></path>
|
||||
<path d="M14 11V17"></path>
|
||||
<path d="M4 7H20"></path>
|
||||
<path d="M6 7H18V19C18 19.5304 17.7893 20.0391 17.4142 20.4142C17.0391 20.7893 16.5304 21 16 21H8C7.46957 21 6.96086 20.7893 6.58579 20.4142C6.21071 20.0391 6 19.5304 6 19V7Z"></path>
|
||||
<path d="M9 7V4C9 3.46957 9.21071 2.96086 9.58579 2.58579C9.96086 2.21071 10.4696 2 11 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V7"></path>
|
||||
</svg>
|
||||
{% trans "Delete Application" %}: {{ object.candidate.full_name }}
|
||||
</h1>
|
||||
<a href="{% url 'applications_list' %}" class="btn btn-secondary">{% trans "Back to List" %}</a>
|
||||
{% trans "Confirm Deletion" %}
|
||||
</h2>
|
||||
</div>
|
||||
<p>{% trans "Are you sure you want to delete this application for" %} "{{ object.candidate.full_name }}" {% trans "for the job" %} "{{ object.job.title }}"? {% trans "This action cannot be undone." %}</p>
|
||||
{% if object.job %}
|
||||
<p><strong>{% trans "Job:" %}</strong> {{ object.job.title }}</p>
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<p class="lead text-danger fw-bold">
|
||||
{% trans "You are about to permanently delete the following Application." %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktrans with candidate_name=object.candidate.full_name job_title=object.job.title %}
|
||||
**Are you absolutely sure** you want to delete the application submitted by **{{ object}}** ?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<blockquote class="blockquote border-start border-danger border-4 ps-3 py-2 bg-light rounded-end">
|
||||
<h5 class="mb-1 text-dark">{% trans "Application Details" %}</h5>
|
||||
{% if object.candidate %}
|
||||
<p><strong>{% trans "Candidate:" %}</strong> {{ object.candidate.full_name }}</p>
|
||||
<p class="mb-0"><strong>{% trans "Candidate:" %}</strong> {{ object.candidate.full_name }}</p>
|
||||
{% endif %}
|
||||
<p><strong>{% trans "Application Date:" %}</strong> {{ object.created_at|date:"M d, Y" }}</p>
|
||||
{% if object.job %}
|
||||
<p class="mb-0"><strong>{% trans "Job Title:" %}</strong> {{ object.job.title }}</p>
|
||||
{% endif %}
|
||||
<p class="mb-0"><strong>{% trans "Applied On:" %}</strong> {{ object.created_at|date:"M d, Y \a\t P" }}</p>
|
||||
</blockquote>
|
||||
|
||||
<p class="mt-4 text-muted">
|
||||
{% trans "This action is **irreversible** and all associated data will be lost." %}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<a href="{% url 'application_list' %}" class="btn btn-secondary">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<button type="submit" class="btn btn-danger d-flex align-items-center">
|
||||
<svg class="heroicon me-1" viewBox="0 0 24 24" width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
{% trans "Yes, Delete Application" %}
|
||||
{% trans "Yes, Permanently Delete" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -706,7 +706,7 @@
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
||||
<div class="ai-loading-container">
|
||||
<i class="fas fa-robot ai-robot-icon"></i>
|
||||
<span>{% trans "Resume is been Scoring..." %}</span>
|
||||
<span>{% trans "Resume Analysis In Progress..." %}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@ -187,9 +187,9 @@
|
||||
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<h3 style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-user-edit me-2"></i> {% trans "Update Application" %}
|
||||
</h1>
|
||||
</h3>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'application_detail' object.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||
@ -255,7 +255,7 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url 'candidate_update' object.slug %}" enctype="multipart/form-data" id="candidate-form">
|
||||
<form method="post" action="{% url 'application_update' object.slug %}" enctype="multipart/form-data" id="candidate-form">
|
||||
{% csrf_token %}
|
||||
{{form|crispy}}
|
||||
</form>
|
||||
|
||||
@ -204,7 +204,7 @@
|
||||
<div class="col-md-6">
|
||||
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
|
||||
<div class="input-group input-group-lg">
|
||||
<form method="get" action="" class="w-100">
|
||||
<form method="get" action="." class="w-100">
|
||||
{% include 'includes/search_form.html' %}
|
||||
</form>
|
||||
</div>
|
||||
@ -269,8 +269,8 @@
|
||||
|
||||
{# Table View (Default for Desktop) #}
|
||||
<div class="table-view active d-none d-lg-block">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<div class="table-responsive d-none d-lg-block">
|
||||
<table class="table table-hover align-middle mb-0 ">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" >{% trans "Name" %}</th>
|
||||
@ -332,12 +332,12 @@
|
||||
<a href="{% url 'application_update' candidate.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{% comment %} <button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'application_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.name }}">
|
||||
data-item-name="{{ candidate.name }} ({{ candidate.job.title }})">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button> {% endcomment %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@ -351,7 +351,7 @@
|
||||
{# Card View (Default for Mobile) #}
|
||||
<div class="card-view row g-4 d-lg-none">
|
||||
{% for candidate in applications %}
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<div class="card candidate-card h-100 shadow-sm">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
@ -389,7 +389,7 @@
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'application_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.name }}">
|
||||
data-item-name="{{ candidate.name }} ({{ candidate.job.title }})">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
@ -419,4 +419,65 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{# ------------------------------------------------------------------------------------------ #}
|
||||
{# DELETE CONFIRMATION MODAL (Bootstrap 5) #}
|
||||
{# ------------------------------------------------------------------------------------------ #}
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content shadow-lg">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title text-primary-theme" id="deleteModalLabel"><i class="fas fa-exclamation-triangle me-2"></i> {% trans "Confirm Deletion" %}</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{% trans "Are you sure you want to delete the application for" %}:</p>
|
||||
<p class="text-danger lead fw-bold" id="modal-item-name"></p>
|
||||
<p class="text-muted small">{% trans "This action is irreversible and the application data will be permanently removed." %}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<form id="deleteForm" method="post" action="">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger d-flex align-items-center">
|
||||
<i class="fas fa-trash-alt me-2"></i> {% trans "Yes, Delete Permanently" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Get the modal element
|
||||
const deleteModal = document.getElementById('deleteModal');
|
||||
|
||||
// Check if the modal exists before proceeding
|
||||
if (deleteModal) {
|
||||
// Add an event listener for when the modal is about to be shown (Bootstrap event)
|
||||
deleteModal.addEventListener('show.bs.modal', function (event) {
|
||||
// Button that triggered the modal
|
||||
const button = event.relatedTarget;
|
||||
|
||||
// Extract info from data-* attributes
|
||||
const deleteUrl = button.getAttribute('data-delete-url');
|
||||
const itemName = button.getAttribute('data-item-name');
|
||||
|
||||
// Update the modal's content.
|
||||
const modalItemName = deleteModal.querySelector('#modal-item-name');
|
||||
const deleteForm = deleteModal.querySelector('#deleteForm');
|
||||
|
||||
// Update the text to show the item name
|
||||
modalItemName.textContent = itemName;
|
||||
|
||||
// Update the form action URL
|
||||
deleteForm.setAttribute('action', deleteUrl);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -8,8 +8,8 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||
<a href="{% url 'source_detail' source.slug %}" class="btn btn-outline-secondary">
|
||||
<h4 class="h3 mb-0">{{ title }}</h4>
|
||||
<a href="{% url 'source_detail' source.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Source
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -171,9 +171,9 @@
|
||||
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<h4 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-plug me-2"></i> {{ title }}
|
||||
</h1>
|
||||
</h4>
|
||||
<div class="d-flex gap-2">
|
||||
{% if source %}
|
||||
<a href="{% url 'source_detail' source.pk %}" class="btn btn-outline-secondary">
|
||||
|
||||
@ -68,23 +68,23 @@
|
||||
{% for source in page_obj %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'source_detail' source.pk %}" class="text-decoration-none">
|
||||
<a href="{% url 'source_detail' source.pk %}" class="text-decoration-none text-primary-theme">
|
||||
<strong>{{ source.name }}</strong>
|
||||
</a>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ source.source_type }}</span>
|
||||
<span class="badge bg-primary-theme">{{ source.source_type }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if source.is_active %}
|
||||
<span class="badge bg-success">{% trans "Active" %}</span>
|
||||
<span class="badge bg-primary-theme">{% trans "Active" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{% trans "Inactive" %}</span>
|
||||
<span class="badge bg-primary-theme">{% trans "Inactive" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<code class="small">{{ source.api_key|truncatechars:20 }}</code>
|
||||
<code class="small text-primary-theme">{{ source.api_key|truncatechars:20 }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">{{ source.created_at|date:"M d, Y" }}</small>
|
||||
|
||||
@ -221,7 +221,7 @@
|
||||
</a> {% endcomment %}
|
||||
|
||||
{# 2. Change Password Button (Key Icon) #}
|
||||
<a href="{% url 'set_staff_password' user.pk %}" class="btn btn-sm btn-outline-info" title="{% trans 'Change Password' %}">
|
||||
<a href="{% url 'set_staff_password' user.pk %}" class="btn btn-sm btn-main-action" title="{% trans 'Change Password' %}">
|
||||
<i class="fas fa-key"></i>
|
||||
</a>
|
||||
|
||||
|
||||
@ -112,7 +112,13 @@
|
||||
<p class="text-muted mb-0">{% trans "Manage your personal details and security." %}</p>
|
||||
</div>
|
||||
<div class="rounded-circle bg-primary-subtle text-accent d-flex align-items-center justify-content-center" style="width: 50px; height: 50px; font-size: 1.5rem;">
|
||||
{% if user.profile_image %}
|
||||
<img src="{{ user.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar"
|
||||
style="width: 100px; height: 100px; object-fit: cover; background-color: var(--kaauh-teal); display: inline-block; vertical-align: middle;"
|
||||
title="{% trans 'Your account' %}">
|
||||
{% else %}
|
||||
{% if user.first_name %}{{ user.first_name.0 }}{% else %}<i class="fas fa-user"></i>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user