Merge pull request 'mobile responsiveness' (#43) from frontend into main

Reviewed-on: #43
This commit is contained in:
ismail 2025-12-02 13:06:03 +03:00
commit c4115efb52
49 changed files with 1402 additions and 317 deletions

6
.env
View File

@ -1,3 +1,3 @@
DB_NAME=norahuniversity DB_NAME=haikal_db
DB_USER=norahuniversity DB_USER=faheed
DB_PASSWORD=norahuniversity DB_PASSWORD=Faheed@215

View File

@ -206,7 +206,9 @@ ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
ACCOUNT_FORMS = {"signup": "recruitment.forms.StaffSignupForm"} 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 Forms Configuration
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"

View File

@ -27,7 +27,8 @@ from .models import (
Participants, Participants,
Message, Message,
Person, Person,
Document Document,
CustomUser
) )
# from django_summernote.widgets import SummernoteWidget # from django_summernote.widgets import SummernoteWidget
@ -1022,13 +1023,15 @@ class HiringAgencyForm(forms.ModelForm):
def clean_email(self): def clean_email(self):
"""Validate email format and uniqueness""" """Validate email format and uniqueness"""
email = self.cleaned_data.get("email") email = self.cleaned_data.get("email")
instance=self.instance
if email: if email:
# Check email format # Check email format
if not "@" in email or "." not in email.split("@")[1]: if not "@" in email or "." not in email.split("@")[1]:
raise ValidationError("Please enter a valid email address.") raise ValidationError("Please enter a valid email address.")
# Check uniqueness (optional - remove if multiple agencies can have same email) # 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 not instance.pk: # Creating new instance
if HiringAgency.objects.filter(email=email).exists(): if HiringAgency.objects.filter(email=email).exists():
raise ValidationError("An agency with this email already exists.") raise ValidationError("An agency with this email already exists.")
@ -1039,6 +1042,16 @@ class HiringAgencyForm(forms.ModelForm):
.exists() .exists()
): ):
raise ValidationError("An agency with this email already 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 return email.lower().strip() if email else email
def clean_phone(self): def clean_phone(self):
@ -2108,6 +2121,7 @@ class MessageForm(forms.ModelForm):
"rows": 6, "rows": 6,
"placeholder": "Enter your message here...", "placeholder": "Enter your message here...",
"required": True, "required": True,
'spellcheck': 'true',
} }
), ),
"message_type": forms.Select(attrs={"class": "form-select"}), "message_type": forms.Select(attrs={"class": "form-select"}),
@ -2152,11 +2166,22 @@ class MessageForm(forms.ModelForm):
"""Filter job options based on user type""" """Filter job options based on user type"""
if self.user.user_type == "agency": 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( self.fields["job"].queryset = JobPosting.objects.filter(
hiring_agency__user=self.user, id__in=job_ids
status="ACTIVE"
).order_by("-created_at") ).order_by("-created_at")
print("Agency user job queryset:", self.fields["job"].queryset)
elif self.user.user_type == "candidate": elif self.user.user_type == "candidate":
# Candidates can only see jobs they applied for # Candidates can only see jobs they applied for
self.fields["job"].queryset = JobPosting.objects.filter( self.fields["job"].queryset = JobPosting.objects.filter(
@ -2179,6 +2204,7 @@ class MessageForm(forms.ModelForm):
self.fields["recipient"].queryset = User.objects.filter( self.fields["recipient"].queryset = User.objects.filter(
user_type="staff" user_type="staff"
).distinct().order_by("username") ).distinct().order_by("username")
elif self.user.user_type == "candidate": elif self.user.user_type == "candidate":
# Candidates can only message staff # Candidates can only message staff
self.fields["recipient"].queryset = User.objects.filter( self.fields["recipient"].queryset = User.objects.filter(
@ -2276,6 +2302,11 @@ class ApplicantSignupForm(forms.ModelForm):
raise forms.ValidationError("Passwords do not match.") raise forms.ValidationError("Passwords do not match.")
return cleaned_data 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): class DocumentUploadForm(forms.ModelForm):

View 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'),
),
]

View 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'),
),
]

View File

View 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.functions import Coalesce, Cast
from django.db.models.fields.json import KeyTransform, KeyTextTransform 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): class CustomUser(AbstractUser):
"""Custom user model extending AbstractUser""" """Custom user model extending AbstractUser"""
@ -468,6 +480,10 @@ class JobPosting(Base):
vacancy_fill_rate = 0.0 vacancy_fill_rate = 0.0
return vacancy_fill_rate 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): class JobPostingImage(models.Model):
@ -518,7 +534,7 @@ class Person(Base):
# Optional linking to user account # Optional linking to user account
user = models.OneToOneField( user = models.OneToOneField(
User, User,
on_delete=models.SET_NULL, on_delete=models.CASCADE,
related_name="person_profile", related_name="person_profile",
verbose_name=_("User Account"), verbose_name=_("User Account"),
null=True, null=True,
@ -544,6 +560,17 @@ class Person(Base):
verbose_name=_("Hiring Agency"), 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: class Meta:
verbose_name = _("Person") verbose_name = _("Person")
verbose_name_plural = _("People") verbose_name_plural = _("People")
@ -582,6 +609,8 @@ class Person(Base):
content_type = ContentType.objects.get_for_model(self.__class__) content_type = ContentType.objects.get_for_model(self.__class__)
return Document.objects.filter(content_type=content_type, object_id=self.id) return Document.objects.filter(content_type=content_type, object_id=self.id)
class Application(Base): class Application(Base):
@ -2049,6 +2078,17 @@ class HiringAgency(Base):
verbose_name_plural = _("Hiring Agencies") verbose_name_plural = _("Hiring Agencies")
ordering = ["name"] 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): class AgencyJobAssignment(Base):
"""Assigns specific jobs to agencies with limits and deadlines""" """Assigns specific jobs to agencies with limits and deadlines"""

View File

@ -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. # Since the async caller sends one task per recipient, total_recipients should be 1.
for recipient in recipient_list: for recipient in recipient_list:
# The 'message' is the custom message specific to this recipient. # 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 successful_sends += 1
print(f"successful_sends: {successful_sends} out of {total_recipients}")
if successful_sends > 0: if successful_sends > 0:
logger.info(f"Bulk email task completed successfully. Sent to {successful_sends}/{total_recipients} recipients.") logger.info(f"Bulk email task completed successfully. Sent to {successful_sends}/{total_recipients} recipients.")
return { return {

View File

@ -438,7 +438,7 @@ urlpatterns = [
name="applicant_portal_dashboard", name="applicant_portal_dashboard",
), ),
path( path(
"applications/applications/<slug:slug>/", "applications/application/<slug:slug>/",
views.applicant_application_detail, views.applicant_application_detail,
name="applicant_application_detail", name="applicant_application_detail",
), ),

View File

@ -160,6 +160,13 @@ class PersonListView(StaffRequiredMixin, ListView):
context_object_name = "people_list" context_object_name = "people_list"
def get_queryset(self): def get_queryset(self):
queryset=super().get_queryset() 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') gender=self.request.GET.get('gender')
if gender: if gender:
queryset=queryset.filter(gender=gender) queryset=queryset.filter(gender=gender)
@ -179,6 +186,7 @@ class PersonListView(StaffRequiredMixin, ListView):
nationality=self.request.GET.get('nationality') nationality=self.request.GET.get('nationality')
context['nationality']=nationality context['nationality']=nationality
context['nationalities']=nationalities context['nationalities']=nationalities
context['search_query'] = self.request.GET.get('search', '')
return context return context
@ -630,7 +638,7 @@ def job_detail(request, slug):
# New statistics # New statistics
"avg_match_score": avg_match_score, "avg_match_score": avg_match_score,
"high_potential_count": high_potential_count, "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_t2i_days": avg_t2i_days,
"avg_t_in_exam_days": avg_t_in_exam_days, "avg_t_in_exam_days": avg_t_in_exam_days,
"linkedin_content_form": linkedin_content_form, "linkedin_content_form": linkedin_content_form,
@ -700,6 +708,10 @@ def request_cvs_download(request, slug):
job.save(update_fields=["zip_created"]) job.save(update_fields=["zip_created"])
# Use async_task to run the function in the background # Use async_task to run the function in the background
# Pass only simple arguments (like the job ID) # 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) async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
# Provide user feedback and redirect # Provide user feedback and redirect
@ -711,6 +723,9 @@ def download_ready_cvs(request, slug):
View to serve the file once it is ready. View to serve the file once it is ready.
""" """
job = get_object_or_404(JobPosting, slug=slug) 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: if job.cv_zip_file and job.zip_created:
# Django FileField handles the HttpResponse and file serving easily # 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""" """View details of a specific hiring agency"""
agency = get_object_or_404(HiringAgency, slug=slug) agency = get_object_or_404(HiringAgency, slug=slug)
# Get candidates associated with this agency # Get applications associated with this agency
candidates = Application.objects.filter(hiring_agency=agency).order_by( applications = Application.objects.filter(hiring_agency=agency).order_by(
"-created_at" "-created_at"
) )
# Statistics # Statistics
total_candidates = candidates.count() total_applications = applications.count()
active_candidates = candidates.filter( active_applications = applications.filter(
stage__in=["Applied", "Screening", "Exam", "Interview", "Offer"] stage__in=["Applied", "Screening", "Exam", "Interview", "Offer"]
).count() ).count()
hired_candidates = candidates.filter(stage="Hired").count() hired_applications = applications.filter(stage="Hired").count()
rejected_candidates = candidates.filter(stage="Rejected").count() rejected_applications = applications.filter(stage="Rejected").count()
job_assignments=AgencyJobAssignment.objects.filter(agency=agency) job_assignments=AgencyJobAssignment.objects.filter(agency=agency)
print(job_assignments) print(job_assignments)
context = { context = {
"agency": agency, "agency": agency,
"candidates": candidates[:10], # Show recent 10 candidates "applications": applications[:10], # Show recent 10 applications
"total_candidates": total_candidates, "total_applications": total_applications,
"active_candidates": active_candidates, "active_applications": active_applications,
"hired_candidates": hired_candidates, "hired_applications": hired_applications,
"rejected_candidates": rejected_candidates, "rejected_applications": rejected_applications,
"generated_password": agency.generated_password "generated_password": agency.generated_password
if agency.generated_password if agency.generated_password
else None, else None,
@ -4343,7 +4358,7 @@ def agency_portal_submit_application_page(request, slug):
"total_submitted": total_submitted, "total_submitted": total_submitted,
"job": assignment.job, "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 @agency_user_required
@ -4407,7 +4422,7 @@ def agency_portal_submit_application(request):
"title": f"Submit Candidate for {assignment.job.title}", "title": f"Submit Candidate for {assignment.job.title}",
"button_text": "Submit Candidate", "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): 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") return redirect("agency_portal_dashboard")
# Get candidates submitted by this agency for this job # Get candidates submitted by this agency for this job
candidates = Application.objects.filter( applications = Application.objects.filter(
hiring_agency=assignment.agency, job=assignment.job hiring_agency=assignment.agency, job=assignment.job
).order_by("-created_at") ).order_by("-created_at")
@ -4461,7 +4476,7 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
# No messages to mark as read # No messages to mark as read
# Pagination for candidates # 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_number = request.GET.get("page")
page_obj = paginator.get_page(page_number) 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) message_page_obj = message_paginator.get_page(message_page_number)
# Calculate progress ring offset for circular progress indicator # Calculate progress ring offset for circular progress indicator
total_candidates = candidates.count() total_applications = applications.count()
max_candidates = assignment.max_candidates max_applications = assignment.max_candidates
circumference = 326.73 # 2 * π * r where r=52 circumference = 326.73 # 2 * π * r where r=52
if max_candidates > 0: if max_applications > 0:
progress_percentage = total_candidates / max_candidates progress_percentage = total_applications / max_applications
stroke_dashoffset = circumference - (circumference * progress_percentage) stroke_dashoffset = circumference - (circumference * progress_percentage)
else: else:
stroke_dashoffset = circumference stroke_dashoffset = circumference
@ -4485,8 +4500,9 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
"assignment": assignment, "assignment": assignment,
"page_obj": page_obj, "page_obj": page_obj,
"message_page_obj": message_page_obj, "message_page_obj": message_page_obj,
"total_candidates": total_candidates, "total_applications": total_applications,
"stroke_dashoffset": stroke_dashoffset, "stroke_dashoffset": stroke_dashoffset,
"max_applications": max_applications,
} }
return render(request, "recruitment/agency_portal_assignment_detail.html", context) return render(request, "recruitment/agency_portal_assignment_detail.html", context)
@ -4667,7 +4683,7 @@ def message_list(request):
"search_query": search_query, "search_query": search_query,
} }
if request.user.user_type != "staff": 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) return render(request, "messages/message_list.html", context)
@ -4749,7 +4765,7 @@ def message_create(request):
"form": form, "form": form,
} }
if request.user.user_type != "staff": 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) return render(request, "messages/message_form.html", context)
@ -4817,7 +4833,7 @@ def message_reply(request, message_id):
"parent_message": parent_message, "parent_message": parent_message,
} }
if request.user.user_type != "staff": 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) return render(request, "messages/message_form.html", context)
@ -4875,50 +4891,93 @@ def message_mark_unread(request, message_id):
@login_required @login_required
def message_delete(request, message_id): def message_delete(request, message_id):
"""Delete a message"""
""" """
Deletes a message using a POST request, primarily designed for HTMX. Deletes a message using a POST request, primarily designed for HTMX.
Redirects to the message list on success (either via standard redirect Redirects to the message list on success (either via standard redirect
or HTMX's hx-redirect header). or HTMX's hx-redirect header).
""" """
# 1. Retrieve the message # 1. Retrieve the message
# Use select_related to fetch linked objects efficiently for checks/logging # Use select_related to fetch linked objects efficiently for checks/logging
message = get_object_or_404( message = get_object_or_404(
Message.objects.select_related("sender", "recipient"), id=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: if message.sender != request.user and message.recipient != request.user:
messages.error(request, "You don't have permission to delete this message.") messages.error(request, "You don't have permission to delete this message.")
# HTMX requests should handle redirection via client-side logic (hx-redirect) # HTMX requests should handle redirection via client-side logic (hx-redirect)
if "HX-Request" in request.headers: if "HX-Request" in request.headers:
# Returning 403 or 400 is ideal, but 200 with an empty body is often accepted # 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. # by HTMX and the message is shown on the next page/refresh.
return HttpResponse(status=403) return HttpResponse(status=403)
# Standard navigation redirect # Standard navigation redirect
return redirect("message_list") return redirect("message_list")
# 3. Handle POST Request (Deletion)
if request.method == "POST": if request.method == "POST":
message.delete() message.delete()
messages.success(request, "Message deleted successfully.") messages.success(request, "Message deleted successfully.")
# Handle HTMX requests # Handle HTMX requests
if "HX-Request" in request.headers: 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
return redirect("message_list") # Standard navigation fallback
return redirect("message_list")
# For GET requests, show confirmation page # @login_required
context = { # def message_delete(request, message_id):
"message": message, # """Delete a 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}?", # Deletes a message using a POST request, primarily designed for HTMX.
"cancel_url": reverse("message_detail", kwargs={"message_id": message_id}), # Redirects to the message list on success (either via standard redirect
} # or HTMX's hx-redirect header).
return render(request, "messages/message_confirm_delete.html", context) # """
# # 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 @login_required
@ -5038,7 +5097,7 @@ def document_upload(request, slug):
if upload_target == 'person': if upload_target == 'person':
return redirect("applicant_portal_dashboard") return redirect("applicant_portal_dashboard")
else: else:
return redirect("applicant_application_detail", application_slug=application.slug) return redirect("applicant_application_detail", slug=application.slug)
# Handle GET request for AJAX # Handle GET request for AJAX
if request.headers.get("X-Requested-With") == "XMLHttpRequest": if request.headers.get("X-Requested-With") == "XMLHttpRequest":
@ -5050,6 +5109,7 @@ def document_upload(request, slug):
def document_delete(request, document_id): def document_delete(request, document_id):
"""Delete a document""" """Delete a document"""
document = get_object_or_404(Document, id=document_id) document = get_object_or_404(Document, id=document_id)
print(document)
# Initialize variables for redirection outside of the complex logic # Initialize variables for redirection outside of the complex logic
is_htmx = "HX-Request" in request.headers is_htmx = "HX-Request" in request.headers
@ -5655,8 +5715,8 @@ def source_update(request, slug):
context = { context = {
"form": form, "form": form,
"source": source, "source": source,
"title": f"Edit Source: {source.name}", "title": _("Edit Source: %(name)s") % {'name': source.name},
"button_text": "Update Source", "button_text": _("Update Source"),
} }
return render(request, "recruitment/source_form.html", context) return render(request, "recruitment/source_form.html", context)
@ -5674,8 +5734,8 @@ def source_delete(request, slug):
context = { context = {
"source": source, "source": source,
"title": "Delete Source", "title": _("Delete Source: %(name)s") % {'name': source.name},
"message": f'Are you sure you want to delete the source "{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}), "cancel_url": reverse("source_detail", kwargs={"slug": source.slug}),
} }
return render(request, "recruitment/source_confirm_delete.html", context) return render(request, "recruitment/source_confirm_delete.html", context)
@ -5775,6 +5835,12 @@ def application_signup(request, slug):
"recruitment/applicant_signup.html", "recruitment/applicant_signup.html",
{"form": form, "job": job}, {"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() form = ApplicantSignupForm()
return render( return render(

View File

@ -279,7 +279,7 @@ def application_detail(request, slug):
@login_required @login_required
@staff_user_required @staff_user_required
def application_resume_template_view(request, slug): 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) application = get_object_or_404(models.Application, slug=slug)
if not request.user.is_staff: if not request.user.is_staff:
@ -398,7 +398,7 @@ def dashboard_view(request):
# --- 2. TIME SERIES: GLOBAL DAILY APPLICANTS --- # --- 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( global_daily_applications_qs = all_applications_queryset.annotate(
date=TruncDate('created_at') date=TruncDate('created_at')
).values('date').annotate( ).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. # # 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)) # annotated_match_score=Coalesce(safe_match_score_cast, Value(0))
# ) # )
@ -637,10 +637,10 @@ def dashboard_view(request):
@login_required @login_required
@staff_user_required @staff_user_required
def applications_offer_view(request, slug): 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) 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 applications = job.offer_applications
# Handle search # Handle search

View File

@ -732,3 +732,6 @@ html[dir="rtl"] .me-auto { margin-right: 0 !important; margin-left: auto !import
content: ">"; content: ">";
color: var(--kaauh-teal); color: var(--kaauh-teal);
} }

View File

@ -403,13 +403,13 @@
} }
.btn-submit { .btn-submit {
background: var(--success); /* Green for submit */ background: var( --kaauh-teal-dark); /* Green for submit */
color: white; color: white;
box-shadow: 0 4px 12px rgba(25, 135, 84, 0.3); box-shadow: 0 4px 12px rgba(25, 135, 84, 0.3);
} }
.btn-submit:hover { .btn-submit:hover {
background: #157347; background: var(--kaauh-teal);
transform: translateY(-2px); transform: translateY(-2px);
} }

View File

@ -201,10 +201,10 @@
</h4> </h4>
{# Tag Badge (Prominent) #} {# 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}}
<i class="fas fa-tag me-1"></i>{% trans "Apply Before: " %}{{job.application_deadline}}
</span> </span>
</div> </div>
{# Department/Context (Sub-text) #} {# Department/Context (Sub-text) #}

View File

@ -8,7 +8,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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 %} {% comment %} Load the correct Bootstrap CSS file for RTL/LTR {% endcomment %}
{% if LANGUAGE_CODE == 'ar' %} {% 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;"> <nav id="topNavbar" class="navbar navbar-expand-lg sticky-top bg-white border-bottom" style="z-index: 1040;">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand text-dark fw-bold" href="{% url 'kaauh_career' %}"> <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> <span style="color:#00636e;">KAAUH Careers</span>
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
@ -320,15 +320,27 @@
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
{% comment %} <li class="nav-item"> {% 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> {% endcomment %}
<li class="nav-item">
<a class="nav-link text-secondary" href="{% url 'applicant_portal_dashboard' %}">{% translate "Profile" %}</a>
</li>
<li class="nav-item">
<a class="nav-link text-secondary" href="{% url 'kaauh_career' %}">{% translate "Careers" %}</a>
</li>
<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 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"> <li class="nav-item dropdown">
<button class="language-toggle-btn dropdown-toggle" type="button" <button class="language-toggle-btn dropdown-toggle" type="button"
data-bs-toggle="dropdown" data-bs-offset="0, 8" aria-expanded="false" data-bs-toggle="dropdown" data-bs-offset="0, 8" aria-expanded="false"

View File

@ -50,10 +50,7 @@
<nav class="navbar navbar-expand-lg navbar-dark sticky-top"> <nav class="navbar navbar-expand-lg navbar-dark sticky-top">
<div class="container-fluid max-width-1600"> <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"> <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;"> <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> <i class="fas fa-envelope"></i>
</a> </a>
</li> {% endcomment %} </li> {% endcomment %}
<li class="nav-item me-2"> <li class="nav-item me-2 d-none d-lg-block">
{% if LANGUAGE_CODE == 'en' %} {% if LANGUAGE_CODE == 'en' %}
<form action="{% url 'set_language' %}" method="post" class="d-inline"> <form action="{% url 'set_language' %}" method="post" class="d-inline">
{% csrf_token %} {% csrf_token %}
@ -137,11 +134,11 @@
</form> </form>
{% endif %} {% endif %}
</li> </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' %}"> <a class="nav-link text-white" href="{% url 'message_list' %}">
<i class="fas fa-envelope"></i> <span>{% trans "Messages" %}</span> <i class="fas fa-envelope"></i> <span>{% trans "Messages" %}</span>
</a> </a>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<button <button
@ -189,6 +186,28 @@
</div> </div>
</li> </li>
<li><hr class="dropdown-divider my-1"></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 %} {% 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> <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>

View 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>

View File

@ -26,7 +26,7 @@
<form <form
method="post" method="post"
enctype="multipart/form-data" enctype="multipart/form-data"
hx-post="{% url 'document_upload' application.id %}" hx-post="{% url 'application_document_upload' application.slug %}"
hx-target="#documents-pane" hx-target="#documents-pane"
hx-select="#documents-pane" hx-select="#documents-pane"
hx-swap="outerHTML" hx-swap="outerHTML"

View File

@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
{{ form.media }} {{ form.media }}
<div class="row"> <div class="row">
<div class="container-fluid"> <div class="container-fluid">
@ -18,7 +18,7 @@
<div class="card-body"> <div class="card-body">
<form hx-boost="true" method="post" id="email-compose-form" action="{% url 'compose_application_email' job.slug %}" <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-target="#messageContent"
hx-select="#messageContent" hx-select="#messageContent"
hx-push-url="false" hx-push-url="false"
@ -126,6 +126,7 @@
</div> </div>
<style> <style>
{{ form.media.css }}
.card { .card {
border: none; border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
@ -173,9 +174,12 @@
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
} }
</style> </style>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('email-compose-form1'); const form = document.getElementById('email-compose-form1');
const sendBtn = document.getElementById('send-email-btn1'); const sendBtn = document.getElementById('send-email-btn1');
@ -388,7 +392,7 @@ document.addEventListener('DOMContentLoaded', function() {
subject: subject.value, subject: subject.value,
message: message.value, message: message.value,
recipients: Array.from(form.querySelectorAll('input[name="{{ form.recipients.name }}"]:checked')).map(cb => cb.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 include_meeting_details: form.querySelector('#{{ form.include_meeting_details.id_for_label }}').checked
}; };
@ -428,8 +432,8 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// Restore checkboxes // Restore checkboxes
if (draft.include_candidate_info) { if (draft.include_application_info) {
form.querySelector('#{{ form.include_candidate_info.id_for_label }}').checked = draft.include_candidate_info; form.querySelector('#{{ form.include_application_info.id_for_label }}').checked = draft.include_application_info;
} }
if (draft.include_meeting_details) { if (draft.include_meeting_details) {
form.querySelector('#{{ form.include_meeting_details.id_for_label }}').checked = 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'); console.log('Email compose form initialized');
}); });
</script>

View File

@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %}
{% block title %}Create Onsite Interview{% endblock %} {% block title %}{% trans "Create Onsite Interview" %}{% endblock %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container mt-4">
@ -15,7 +16,7 @@
<a href="{% url 'interview_create_type_selection' application.slug %}" <a href="{% url 'interview_create_type_selection' application.slug %}"
class="btn btn-outline-primary"> class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i> <i class="fas fa-arrow-left me-2"></i>
Back to Candidate List {% trans "Back to application List" %}
</a> </a>
</div> </div>
<div class="card-body"> <div class="card-body">
@ -41,7 +42,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.interview_date.id_for_label }}" class="form-label"> <label for="{{ form.interview_date.id_for_label }}" class="form-label">
<i class="fas fa-calendar me-1"></i> <i class="fas fa-calendar me-1"></i>
Topic {% trans "Topic" %}
</label> </label>
{{ form.topic }} {{ form.topic }}
{% if form.topic.errors %} {% if form.topic.errors %}
@ -57,7 +58,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.interview_date.id_for_label }}" class="form-label"> <label for="{{ form.interview_date.id_for_label }}" class="form-label">
<i class="fas fa-calendar me-1"></i> <i class="fas fa-calendar me-1"></i>
Interview Date {% trans "Interview Date" %}
</label> </label>
{{ form.interview_date }} {{ form.interview_date }}
{% if form.interview_date.errors %} {% if form.interview_date.errors %}
@ -72,7 +73,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.interview_time.id_for_label }}" class="form-label"> <label for="{{ form.interview_time.id_for_label }}" class="form-label">
<i class="fas fa-clock me-1"></i> <i class="fas fa-clock me-1"></i>
Interview Time {% trans "Interview Time" %}
</label> </label>
{{ form.interview_time }} {{ form.interview_time }}
{% if form.interview_time.errors %} {% if form.interview_time.errors %}
@ -89,7 +90,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.duration.id_for_label }}" class="form-label"> <label for="{{ form.duration.id_for_label }}" class="form-label">
<i class="fas fa-hourglass-half me-1"></i> <i class="fas fa-hourglass-half me-1"></i>
Duration (minutes) {% trans "Duration (minutes)" %}
</label> </label>
{{ form.duration }} {{ form.duration }}
{% if form.duration.errors %} {% if form.duration.errors %}
@ -104,7 +105,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.physical_address.id_for_label }}" class="form-label"> <label for="{{ form.physical_address.id_for_label }}" class="form-label">
<i class="fas fa-map-marker-alt me-1"></i> <i class="fas fa-map-marker-alt me-1"></i>
Physical Address {% trans "Physical Address" %}
</label> </label>
{{ form.physical_address }} {{ form.physical_address }}
{% if form.physical_address.errors %} {% if form.physical_address.errors %}
@ -119,7 +120,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.room_number.id_for_label }}" class="form-label"> <label for="{{ form.room_number.id_for_label }}" class="form-label">
<i class="fas fa-door-open me-1"></i> <i class="fas fa-door-open me-1"></i>
Room Number {% trans "Room Number" %}
</label> </label>
{{ form.room_number }} {{ form.room_number }}
{% if form.room_number.errors %} {% if form.room_number.errors %}
@ -133,7 +134,7 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<button type="submit" class="btn btn-main-action"> <button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-2"></i> <i class="fas fa-save me-2"></i>
Schedule Onsite Interview {% trans "Schedule Onsite Interview" %}
</button> </button>
</div> </div>
</form> </form>
@ -156,7 +157,7 @@ document.addEventListener('DOMContentLoaded', function() {
dateInput.addEventListener('change', function() { dateInput.addEventListener('change', function() {
if (this.value < today) { if (this.value < today) {
this.setCustomValidity('Interview date must be in the future'); this.setCustomValidity('{% trans "Interview date must be in the future" %}');
} else { } else {
this.setCustomValidity(''); this.setCustomValidity('');
} }

View File

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n crispy_forms_tags %} {% load i18n crispy_forms_tags %}
{% block title %}Create Remote Interview{% endblock %} {% block title %}{% trans "Create Remote Interview" %}{% endblock %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container mt-4">
@ -16,7 +16,7 @@
<a href="{% url 'interview_create_type_selection' application.slug %}" <a href="{% url 'interview_create_type_selection' application.slug %}"
class="btn btn-outline-primary"> class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i> <i class="fas fa-arrow-left me-2"></i>
Back to Candidate List {% trans "Back to application List" %}
</a> </a>
</div> </div>
<div class="card-body"> <div class="card-body">
@ -40,7 +40,7 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<button type="submit" class="btn btn-main-action"> <button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-2"></i> <i class="fas fa-save me-2"></i>
Schedule Remote Interview {% trans "Schedule Remote Interview" %}
</button> </button>
</div> </div>
</form> </form>
@ -63,7 +63,7 @@ document.addEventListener('DOMContentLoaded', function() {
dateInput.addEventListener('change', function() { dateInput.addEventListener('change', function() {
if (this.value < today) { if (this.value < today) {
this.setCustomValidity('Interview date must be in the future'); this.setCustomValidity('{% trans "Interview date must be in the future" %}');
} else { } else {
this.setCustomValidity(''); this.setCustomValidity('');
} }

View File

@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %}
{% block title %}Create Interview - Select Type{% endblock %} {% block title %}{% trans "Create Interview - Select Type" %}{% endblock %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container mt-4">
@ -24,8 +25,8 @@
class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none"> class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none">
<div class="text-center"> <div class="text-center">
<i class="fas fa-video me-2"></i> <i class="fas fa-video me-2"></i>
<div class="mt-2">Remote Interview</div> <div class="mt-2">{% trans "Remote Interview" %}</div>
<small class="d-block">Via Zoom/Video Conference</small> <small class="d-block">{% trans "Via Zoom/Video Conference" %}</small>
</div> </div>
</a> </a>
@ -33,8 +34,8 @@
class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none"> class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none">
<div class="text-center"> <div class="text-center">
<i class="fas fa-building me-2"></i> <i class="fas fa-building me-2"></i>
<div class="mt-2">Onsite Interview</div> <div class="mt-2">{% trans "Onsite Interview" %}</div>
<small class="d-block">In-person at our facility</small> <small class="d-block">{% trans "In-person at our facility" %}</small>
</div> </div>
</a> </a>
</div> </div>
@ -44,7 +45,7 @@
<a href="#" <a href="#"
class="btn btn-outline-primary"> class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i> <i class="fas fa-arrow-left me-2"></i>
Back to Candidate List {% trans "Back to application List" %}
</a> </a>
</div> </div>
</div> </div>

View File

@ -31,7 +31,7 @@
/* Filter Controls */ /* Filter Controls */
.filter-controls { .filter-controls {
background-color: #f8f9fa;
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 2rem; margin-bottom: 2rem;

View File

@ -258,8 +258,10 @@
</div> </div>
</div> </div>
</div> </div>
</div>
{# --- START OF JOB LIST CONTAINER --- #} {# --- START OF JOB LIST CONTAINER --- #}
<div id="job-list"> <div id="job-list">
{% include "includes/_list_view_switcher.html" with list_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="table-view d-none d-lg-block active">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="table-responsive"> <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 --- #} {# --- Corrected Multi-Row Header Structure --- #}
<thead> <thead>
@ -287,7 +289,7 @@
<tr class="nested-metrics-row"> <tr class="nested-metrics-row">
<th scope="col">{% trans "All" %}</th> <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 "Exam" %}</th>
<th scope="col">{% trans "Interview" %}</th> <th scope="col">{% trans "Interview" %}</th>
<th scope="col">{% trans "DOC Review" %}</th> <th scope="col">{% trans "DOC Review" %}</th>

View File

@ -1,7 +1,7 @@
{% extends "portal_base.html" %} {% extends "portal_base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block title %}{% if form.instance.pk %}{% trans "Reply to Message"%}{% else %}{% trans"Compose Message"%}{% endif %}{% endblock %} {% block title %}{% if form.instance.pk %}{% trans "Reply to Message" %}{% else %}{% trans "Compose Message" %}{% endif %}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">

View File

@ -22,17 +22,21 @@
</a> </a>
{% if message.recipient == request.user %} {% if message.recipient == request.user %}
<a href="{% url 'message_mark_unread' message.id %}" <a href="{% url 'message_mark_unread' message.id %}"
class="btn btn-outline-warning" class="btn btn-outline-warning"
hx-swap="outerHTML"
hx-post="{% url 'message_mark_unread' message.id %}"> hx-post="{% url 'message_mark_unread' message.id %}">
<i class="fas fa-envelope"></i> {% trans "Mark Unread" %} <i class="fas fa-envelope"></i> {% trans "Mark Unread" %}
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'message_delete' message.id %}" <button type="button"
class="btn btn-outline-danger" class="btn btn-danger btn-lg"
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?' %}"> hx-confirm="{% trans 'Are you sure you want to permanently delete this message? This action cannot be undone.' %}"
<i class="fas fa-trash"></i> {% trans "Delete" %} hx-redirect="{% url 'message_list' %}"
</a> 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"> <a href="{% url 'message_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Back to Messages" %} <i class="fas fa-arrow-left"></i> {% trans "Back to Messages" %}
</a> </a>

View File

@ -8,14 +8,13 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4"> <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"> <a href="{% url 'message_create' %}" class="btn btn-main-action">
<i class="fas fa-plus"></i> {% trans "Compose Message" %} <i class="fas fa-plus"></i> {% trans "Compose Message" %}
</a> </a>
</div> </div>
<!-- Filters --> <div class="card mb-4 border-primary-theme-subtle">
<div class="card mb-4">
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
<div class="col-md-3"> <div class="col-md-3">
@ -41,41 +40,41 @@
<div class="input-group"> <div class="input-group">
<input type="text" name="q" id="q" class="form-control" <input type="text" name="q" id="q" class="form-control"
value="{{ search_query }}" placeholder="{% trans 'Search messages...' %}"> 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> <i class="fas fa-search"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label">&nbsp;</label> <label class="form-label">&nbsp;</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> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Statistics -->
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <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"> <div class="card-body">
<h6 class="card-title">{% trans "Total Messages" %}</h6> <h6 class="card-title text-primary-theme">{% trans "Total Messages" %}</h6>
<h3 class="text-primary">{{ total_messages }}</h3> <h3 class="text-primary-theme">{{ total_messages }}</h3>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card bg-light"> <div class="card bg-warning-subtle border-warning-subtle">
<div class="card-body"> <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> <h3 class="text-warning">{{ unread_messages }}</h3>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Messages List --> <div class="card border-primary-theme-subtle">
<div class="card">
<div class="card-body"> <div class="card-body">
{% if page_obj %} {% if page_obj %}
<div class="table-responsive"> <div class="table-responsive">
@ -93,14 +92,14 @@
</thead> </thead>
<tbody> <tbody>
{% for message in page_obj %} {% 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> <td>
<a href="{% url 'message_detail' message.id %}" <a href="{% url 'message_detail' message.id %}"
class="{% if not message.is_read %}fw-bold {% endif %}"> class="fw-bold text-primary-theme text-decoration-none">
{{ message.subject }} {{ message.subject|truncatechars:50 }}
</a> </a>
{% if message.parent_message %} {% 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 %} {% endif %}
</td> </td>
<td>{{ message.sender.get_full_name|default:message.sender.username }}</td> <td>{{ message.sender.get_full_name|default:message.sender.username }}</td>
@ -120,27 +119,27 @@
<td>{{ message.created_at|date:"M d, Y H:i" }}</td> <td>{{ message.created_at|date:"M d, Y H:i" }}</td>
<td> <td>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<a href="{% url 'message_detail' message.id %}" <a href="{% url 'message_detail' message.id %}"
class="btn btn-sm btn-outline-primary" title="{% trans 'View' %}"> class="btn btn-sm btn-outline-primary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
{% if not message.is_read and message.recipient == request.user %} {% if not message.is_read and message.recipient == request.user %}
<a href="{% url 'message_mark_read' message.id %}" <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 %}" hx-post="{% url 'message_mark_read' message.id %}"
title="{% trans 'Mark as Read' %}"> title="{% trans 'Mark as Read' %}">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'message_reply' message.id %}" <a href="{% url 'message_reply' message.id %}"
class="btn btn-sm btn-outline-primary" title="{% trans 'Reply' %}"> class="btn btn-sm btn-outline-primary" title="{% trans 'Reply' %}">
<i class="fas fa-reply"></i> <i class="fas fa-reply"></i>
</a> </a>
<a href="{% url 'message_delete' message.id %}" <a href="{% url 'message_delete' message.id %}"
class="btn btn-sm btn-outline-danger" 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?' %}" hx-confirm="{% trans 'Are you sure you want to delete this message?' %}"
title="{% trans 'Delete' %}"> title="{% trans 'Delete' %}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</div> </div>
@ -149,7 +148,7 @@
{% empty %} {% empty %}
<tr> <tr>
<td colspan="7" class="text-center text-muted"> <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="mb-0">{% trans "No messages found." %}</p>
<p class="small">{% trans "Try adjusting your filters or compose a new message." %}</p> <p class="small">{% trans "Try adjusting your filters or compose a new message." %}</p>
</td> </td>
@ -159,13 +158,12 @@
</table> </table>
</div> </div>
<!-- Pagination -->
{% if page_obj.has_other_pages %} {% if page_obj.has_other_pages %}
<nav aria-label="Message pagination"> <nav aria-label="Message pagination">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<li class="page-item"> <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> <i class="fas fa-chevron-left"></i>
</a> </a>
</li> </li>
@ -174,18 +172,18 @@
{% for num in page_obj.paginator.page_range %} {% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %} {% if page_obj.number == num %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ num }}</span> <span class="page-link bg-primary-theme border-primary-theme">{{ num }}</span>
</li> </li>
{% else %} {% else %}
<li class="page-item"> <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> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if page_obj.has_next %} {% if page_obj.has_next %}
<li class="page-item"> <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> <i class="fas fa-chevron-right"></i>
</a> </a>
</li> </li>
@ -195,7 +193,7 @@
{% endif %} {% endif %}
{% else %} {% else %}
<div class="text-center text-muted py-5"> <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="mb-0">{% trans "No messages found." %}</p>
<p class="small">{% trans "Try adjusting your filters or compose a new message." %}</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"> <a href="{% url 'message_create' %}" class="btn btn-main-action">
@ -210,7 +208,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block customJS %}
<script> <script>
// Auto-refresh unread count every 30 seconds // Auto-refresh unread count every 30 seconds
setInterval(() => { setInterval(() => {
@ -227,5 +225,4 @@ setInterval(() => {
.catch(error => console.error('Error fetching unread count:', error)); .catch(error => console.error('Error fetching unread count:', error));
}, 30000); }, 30000);
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static i18n %} {% load static i18n %}
{% block title %}{% trans "Delete Person" %} - {{ block.super }}{% endblock %} {% block title %}{% trans "Delete Applicant" %} - {{ block.super }}{% endblock %}
{% block customCSS %} {% block customCSS %}
<style> <style>
@ -50,7 +50,7 @@
margin-bottom: 0; margin-bottom: 0;
} }
/* Person Info Card */ /* Applicant Info Card */
.person-info { .person-info {
background-color: #f8f9fa; background-color: #f8f9fa;
border-radius: 0.75rem; border-radius: 0.75rem;
@ -176,14 +176,14 @@
<div> <div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;"> <h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-exclamation-triangle me-2"></i> <i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Delete Person" %} {% trans "Delete Applicant" %}
</h1> </h1>
<p class="text-muted mb-0"> <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> </p>
</div> </div>
<a href="{% url 'person_detail' object.slug %}" class="btn btn-secondary"> <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> </a>
</div> </div>
@ -196,16 +196,16 @@
</div> </div>
<h3 class="warning-title">{% trans "Warning: This action cannot be undone!" %}</h3> <h3 class="warning-title">{% trans "Warning: This action cannot be undone!" %}</h3>
<p class="warning-text"> <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> </p>
</div> </div>
<!-- Person Information --> <!-- Applicant Information -->
<div class="card kaauh-card mb-4"> <div class="card kaauh-card mb-4">
<div class="card-header bg-white border-bottom"> <div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);"> <h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-user me-2"></i> <i class="fas fa-user me-2"></i>
{% trans "Person to be Deleted" %} {% trans "Applicant to be Deleted" %}
</h5> </h5>
</div> </div>
<div class="card-body"> <div class="card-body">
@ -280,14 +280,14 @@
<div class="card-header bg-white border-bottom"> <div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);"> <h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-list me-2"></i> <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> </h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<ul class="consequence-list"> <ul class="consequence-list">
<li> <li>
<i class="fas fa-times-circle"></i> <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>
<li> <li>
<i class="fas fa-times-circle"></i> <i class="fas fa-times-circle"></i>
@ -334,7 +334,7 @@
id="deleteButton" id="deleteButton"
disabled> disabled>
<i class="fas fa-trash me-2"></i> <i class="fas fa-trash me-2"></i>
{% trans "Delete Person Permanently" %} {% trans "Delete Applicant Permanently" %}
</button> </button>
</div> </div>
</form> </form>

View File

@ -255,14 +255,11 @@
{% if user.is_staff %} {% if user.is_staff %}
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'person_update' person.slug %}" class="btn btn-light"> <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> </a>
<button type="button" class="btn btn-outline-light" <a href="{% url 'person_delete' person.slug %}" class="btn btn-light">
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 me-1"></i> {% trans "Delete" %} <i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %}
</button> </a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -270,7 +267,7 @@
</div> </div>
<div class="row"> <div class="row">
<!-- Personal Information Column --> <!-- Applicantal Information Column -->
<div class="col-lg-6 mb-4"> <div class="col-lg-6 mb-4">
<div class="card h-100"> <div class="card h-100">
<div class="card-body"> <div class="card-body">
@ -535,19 +532,18 @@
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary"> <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> </a>
{% if user.is_staff %} {% if user.is_staff %}
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'person_update' person.slug %}" class="btn btn-main-action"> <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> </a>
<button type="button" class="btn btn-outline-danger" <a href="{% url 'person_delete' person.slug %}" class="btn btn-danger">
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 me-1"></i> {% trans "Delete" %} <i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %}
</button> </a>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -222,8 +222,8 @@
<!-- Table View (Default) --> <!-- Table View (Default) -->
<div class="table-view"> <div class="table-view">
<div class="table-responsive"> <div class="table-responsive d-none d-lg-block">
<table class="table table-hover align-middle mb-0"> <table class="table table-hover align-middle mb-0 ">
<thead> <thead>
<tr> <tr>
<th scope="col">{% trans "Photo" %}</th> <th scope="col">{% trans "Photo" %}</th>
@ -370,13 +370,13 @@
class="btn btn-sm btn-outline-secondary"> class="btn btn-sm btn-outline-secondary">
<i class="fas fa-edit"></i> {% trans "Edit" %} <i class="fas fa-edit"></i> {% trans "Edit" %}
</a> </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' %}" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal" data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'person_delete' person.slug %}" data-delete-url="{% url 'person_delete' person.slug %}"
data-item-name="{{ person.get_full_name }}"> data-item-name="{{ person.get_full_name }}">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
</button> </button> {% endcomment %}
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -112,7 +112,7 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-white" href="{% url 'kaauh_career' %}"> <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> </a>
</li> </li>
{% endif %} {% endif %}

View File

@ -109,6 +109,7 @@
</div> </div>
<div class="row"> <div class="row">
<!-- Assignment Overview --> <!-- Assignment Overview -->
<div class="col-lg-8"> <div class="col-lg-8">
<!-- Assignment Details Card --> <!-- Assignment Details Card -->
@ -224,7 +225,7 @@
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);"> <h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-users me-2"></i> <i class="fas fa-users me-2"></i>
{% trans "Submitted Applications" %} ({{ total_candidates }}) {% trans "Submitted Applications" %} ({{ total_applications }})
</h5> </h5>
{% if access_link %} {% if access_link %}
<a href="{% url 'agency_portal_login' %}" target="_blank" class="btn btn-outline-info btn-sm"> <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 }};"/> style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/>
</svg> </svg>
<div class="position-absolute top-50 start-50 translate-middle text-center"> <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 class="small text-muted text-uppercase">{% trans "of" %} {{ assignment.max_candidates}}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="text-center"> <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 class="text-muted">/ {{ assignment.max_candidates }} {% trans "applications" %}</div>
</div> </div>
<div class="progress mt-3" style="height: 8px;"> <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 class="progress-bar" style="width: {{ progress }}%"></div>
</div> </div>
</div> </div>
@ -353,7 +354,7 @@
</h5> </h5>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<a href="" <a href="{}"
class="btn btn-outline-primary"> class="btn btn-outline-primary">
<i class="fas fa-envelope me-1"></i> {% trans "Send Message" %} <i class="fas fa-envelope me-1"></i> {% trans "Send Message" %}
</a> </a>

View File

@ -232,8 +232,8 @@
} }
.empty-state i { .empty-state i {
font-size: 1.5rem; font-size: 1rem;
margin-bottom: 1rem; margin-bottom: 0.5rem;
opacity: 0.5; opacity: 0.5;
} }
@ -313,7 +313,7 @@
{{ agency.name }} {{ agency.name }}
</h1> </h1>
<p class="text-muted mb-0"> <p class="text-muted mb-0">
{% trans "Hiring Agency Details and Candidate Management" %} {% trans "Hiring Agency Details and Application Management" %}
</p> </p>
</div> </div>
<div> <div>
@ -531,7 +531,7 @@
aria-selected="true" aria-selected="true"
> >
<i class="fas fa-users me-1"></i> <i class="fas fa-users me-1"></i>
{% trans "Recent Candidates" %} {% trans "Recent Applications" %}
</button> </button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
@ -560,25 +560,25 @@
role="tabpanel" role="tabpanel"
aria-labelledby="candidates-tab" aria-labelledby="candidates-tab"
> >
{% if candidates %} {% if applications %}
{% for candidate in candidates %} {% for application in applications %}
<div class="candidate-item"> <div class="candidate-item">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<div class="candidate-name">{{ candidate.name }}</div> <div class="candidate-name">{{ application.name }}</div>
<div class="candidate-details"> <div class="candidate-details">
<i class="fas fa-envelope me-1"></i> {{ candidate.email }} <i class="fas fa-envelope me-1"></i> {{ application.email }}
{% if candidate.phone %} {% if application.phone %}
<span class="ms-3"><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</span> <span class="ms-3"><i class="fas fa-phone me-1"></i> {{ application.phone }}</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="text-end"> <div class="text-end">
<span class="stage-badge stage-{{ candidate.stage }}"> <span class="stage-badge stage-{{ application.stage }}">
{{ candidate.get_stage_display }} {{ application.get_stage_display }}
</span> </span>
<div class="small text-muted mt-1"> <div class="small text-muted mt-1">
{{ candidate.created_at|date:"M d, Y" }} {{ application.created_at|date:"M d, Y" }}
</div> </div>
</div> </div>
</div> </div>
@ -587,8 +587,8 @@
{% else %} {% else %}
<div class="empty-state"> <div class="empty-state">
<i class="fas fa-user-slash"></i> <i class="fas fa-user-slash"></i>
<h6>{% trans "No candidates yet" %}</h6> <h6>{% trans "No applications yet" %}</h6>
<p class="mb-0">{% trans "This agency hasn't submitted any candidates yet." %}</p> <p class="mb-0">{% trans "This agency hasn't submitted any applications yet." %}</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -654,32 +654,32 @@
<div class="card-header bg-white border-bottom"> <div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);"> <h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-chart-bar me-2"></i> <i class="fas fa-chart-bar me-2"></i>
{% trans "Candidate Statistics" %} {% trans "Application Statistics" %}
</h5> </h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div class="row g-3">
<div class="col-6"> <div class="col-6">
<div class="stat-card"> <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 class="stat-label">{% trans "Total" %}</div>
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="stat-card"> <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 class="stat-label">{% trans "Active" %}</div>
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="stat-card"> <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 class="stat-label">{% trans "Hired" %}</div>
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="stat-card"> <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 class="stat-label">{% trans "Rejected" %}</div>
</div> </div>
</div> </div>

View File

@ -161,9 +161,9 @@
<!-- Header --> <!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <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 }} <i class="fas fa-building me-2"></i> {{ title }}
</h1> </h6>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
{% if agency %} {% if agency %}
<a href="{% url 'agency_detail' agency.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'agency_detail' agency.slug %}" class="btn btn-outline-secondary">
@ -186,9 +186,7 @@
<div class="current-profile"> <div class="current-profile">
<h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6> <h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6>
<div class="d-flex align-items-center"> <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> <div>
<h5 class="mb-1">{{ agency.name }}</h5> <h5 class="mb-1">{{ agency.name }}</h5>
{% if agency.contact_person %} {% if agency.contact_person %}

View File

@ -172,8 +172,8 @@
<!-- Table View --> <!-- Table View -->
<div class="table-view"> <div class="table-view">
<div class="table-responsive"> <div class="table-responsive d-none d-lg-block">
<table class="table table-hover align-middle mb-0"> <table class="table table-hover align-middle mb-0 ">
<thead> <thead>
<tr> <tr>
<th scope="col">{% trans "Agency Name" %}</th> <th scope="col">{% trans "Agency Name" %}</th>

View File

@ -43,10 +43,10 @@
border-radius: 0.35rem; border-radius: 0.35rem;
font-weight: 700; font-weight: 700;
} }
.status-ACTIVE { background-color: var(--kaauh-success); color: white; } .status-ACTIVE { background-color: var(--kaauh-teal-dark); color: white; }
.status-EXPIRED { background-color: var(--kaauh-danger); color: white; } .status-EXPIRED { background-color: var(--kaauh-teal-dark); color: white; }
.status-COMPLETED { background-color: var(--kaauh-info); color: white; } .status-COMPLETED { background-color: var(--kaauh-teal-dark); color: white; }
.status-CANCELLED { background-color: var(--kaauh-warning); color: #856404; } .status-CANCELLED { background-color: var(--kaauh-teal-dark); color: white; }
.progress-ring { .progress-ring {
width: 120px; width: 120px;
@ -115,7 +115,7 @@
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Dashboard" %} <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Dashboard" %}
</a> </a>
<a href="{% url 'agency_portal_submit_application_page' assignment.slug %}" class="btn btn-sm btn-main-action {% if assignment.is_full %}disabled{% endif %}" > <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> </a>
{% comment %} <a href="#" class="btn btn-outline-info"> {% comment %} <a href="#" class="btn btn-outline-info">
<i class="fas fa-envelope me-1"></i> {% trans "Messages" %} <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" %} <i class="fas fa-exclamation-triangle me-1"></i>{% trans "Expired" %}
</small> </small>
{% else %} {% else %}
<small class="text-success"> <small class="text-primary-theme">
<i class="fas fa-clock me-1"></i>{{ assignment.days_remaining }} {% trans "days remaining" %} <i class="fas fa-clock me-1"></i>{{ assignment.days_remaining }} {% trans "days remaining" %}
</small> </small>
{% endif %} {% endif %}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="text-muted small">{% trans "Maximum Candidates" %}</label> <label class="text-muted small">{% trans "Maximum applications" %}</label>
<div class="fw-bold">{{ assignment.max_candidates }} {% trans "candidates" %}</div> <div class="fw-bold">{{max_applications }} {% trans "applications" %}</div>
</div> </div>
</div> </div>
</div> </div>
@ -195,18 +195,18 @@
<div class="d-grid gap-2"> <div class="d-grid gap-2">
{% if assignment.can_submit %} {% if assignment.can_submit %}
<a href="{% url 'agency_portal_submit_application_page' assignment.slug %}" class="btn btn-main-action"> <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> </a>
{% else %} {% else %}
<button class="btn btn-outline-secondary" disabled> <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> </button>
<div class="alert alert-warning mt-2"> <div class="alert alert-warning mt-2">
<i class="fas fa-exclamation-triangle me-2"></i> <i class="fas fa-exclamation-triangle me-2"></i>
{% if assignment.is_expired %} {% if assignment.is_expired %}
{% trans "This assignment has expired. Submissions are no longer accepted." %} {% trans "This assignment has expired. Submissions are no longer accepted." %}
{% elif assignment.is_full %} {% elif assignment.is_full %}
{% trans "Maximum candidate limit reached for this assignment." %} {% trans "Maximum application limit reached for this assignment." %}
{% else %} {% else %}
{% trans "This assignment is not currently active." %} {% trans "This assignment is not currently active." %}
{% endif %} {% endif %}
@ -221,14 +221,14 @@
</div> </div>
</div> </div>
<!-- Submitted Candidates --> {% endcomment %} <!-- Submitted applications --> {% endcomment %}
<div class="kaauh-card p-4"> <div class="kaauh-card p-4">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);"> <h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-users me-2"></i> <i class="fas fa-users me-2"></i>
{% trans "Submitted Candidates" %} ({{ total_candidates }}) {% trans "Submitted applications" %} ({{ total_applications }})
</h5> </h5>
<span class="badge bg-info">{{ total_candidates }}/{{ assignment.max_candidates }}</span> <span class="badge bg-primary-theme">{{ total_applications }}/{{ max_applications }}</span>
</div> </div>
{% if page_obj %} {% if page_obj %}
@ -244,27 +244,27 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for candidate in page_obj %} {% for application in page_obj %}
<tr> <tr>
<td> <td>
<div class="fw-bold">{{ candidate.name }}</div> <div class="fw-bold">{{ application.name }}</div>
</td> </td>
<td> <td>
<div class="small"> <div class="small">
<div><i class="fas fa-envelope me-1"></i> {{ candidate.email }}</div> <div><i class="fas fa-envelope me-1"></i> {{ application.email }}</div>
<div><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</div> <div><i class="fas fa-phone me-1"></i> {{ application.phone }}</div>
</div> </div>
</td> </td>
<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>
<td> <td>
<div class="small text-muted"> <div class="small text-muted">
{{ candidate.created_at|date:"Y-m-d H:i" }} {{ application.created_at|date:"Y-m-d H:i" }}
</div> </div>
</td> </td>
<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> <i class="fas fa-eye"></i>
</a> </a>
</td> </td>
@ -311,9 +311,9 @@
{% else %} {% else %}
<div class="text-center py-4"> <div class="text-center py-4">
<i class="fas fa-users fa-2x text-muted mb-3"></i> <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"> <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> </p>
</div> </div>
{% endif %} {% endif %}
@ -348,25 +348,25 @@
style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/> style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/>
</svg> </svg>
<div class="progress-ring-text"> <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 }}% {{ progress|floatformat:0 }}%
</div> </div>
</div> </div>
</div> </div>
<div class="text-center"> <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 "candidates" %}</div> <div class="text-muted">/ {{ assignment.max_candidates }} {% trans "applications" %}</div>
</div> </div>
<div class="progress mt-3" style="height: 8px;"> <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 class="progress-bar bg-primary-theme" style="width: {{ progress }}%"></div>
</div> </div>
<div class="mt-3 text-center"> <div class="mt-3 text-center">
{% if assignment.can_submit %} {% if assignment.can_submit %}
<span class="badge bg-success">{% trans "Can Submit" %}</span> <span class="badge bg-primary-theme">{% trans "Can Submit" %}</span>
{% else %} {% else %}
<span class="badge bg-danger">{% trans "Cannot Submit" %}</span> <span class="badge bg-danger">{% trans "Cannot Submit" %}</span>
{% endif %} {% endif %}
@ -415,7 +415,7 @@
<div class="mb-3"> <div class="mb-3">
<label class="text-muted small">{% trans "Submission Rate" %}</label> <label class="text-muted small">{% trans "Submission Rate" %}</label>
<div class="fw-bold"> <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 }}% {{ progress|floatformat:1 }}%
</div> </div>
</div> </div>
@ -500,7 +500,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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" %} {% trans "Cancel" %}
</button> </button>
<button type="submit" class="btn btn-main-action"> <button type="submit" class="btn btn-main-action">
@ -512,14 +512,14 @@
</div> </div>
</div> </div>
<!-- Edit Candidate Modal --> <!-- Edit application Modal -->
<div class="modal fade" id="editCandidateModal" tabindex="-1"> <div class="modal fade" id="editCandidateModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"> <h5 class="modal-title">
<i class="fas fa-edit me-2"></i> <i class="fas fa-edit me-2"></i>
{% trans "Edit Candidate" %} {% trans "Edit application" %}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
@ -584,7 +584,7 @@
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"> <h5 class="modal-title">
<i class="fas fa-trash me-2"></i> <i class="fas fa-trash me-2"></i>
{% trans "Remove Candidate" %} {% trans "Remove application" %}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
@ -594,16 +594,16 @@
<div class="modal-body"> <div class="modal-body">
<div class="alert alert-warning"> <div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i> <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> </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>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% trans "Cancel" %} {% trans "Cancel" %}
</button> </button>
<button type="submit" class="btn btn-danger"> <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> </button>
</div> </div>
</form> </form>
@ -634,12 +634,12 @@ function editCandidate(candidateId) {
new bootstrap.Modal(document.getElementById('editCandidateModal')).show(); new bootstrap.Modal(document.getElementById('editCandidateModal')).show();
}) })
.catch(error => { .catch(error => {
console.error('Error fetching candidate:', error); console.error('Error fetching Application:', error);
alert('{% trans "Error loading candidate data. Please try again." %}'); alert('{% trans "Error loading Application data. Please try again." %}');
}); });
} }
// Delete Candidate // Delete Application
function deleteCandidate(candidateId, candidateName) { function deleteCandidate(candidateId, candidateName) {
// Update form action URL with candidate ID // Update form action URL with candidate ID
const deleteForm = document.getElementById('deleteCandidateForm'); const deleteForm = document.getElementById('deleteCandidateForm');
@ -670,12 +670,12 @@ document.getElementById('editCandidateForm').addEventListener('submit', function
bootstrap.Modal.getInstance(document.getElementById('editCandidateModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('editCandidateModal')).hide();
location.reload(); location.reload();
} else { } else {
alert(data.message || '{% trans "Error updating candidate. Please try again." %}'); alert(data.message || '{% trans "Error updating Application. Please try again." %}');
} }
}) })
.catch(error => { .catch(error => {
console.error('Error:', 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(); bootstrap.Modal.getInstance(document.getElementById('deleteCandidateModal')).hide();
location.reload(); location.reload();
} else { } else {
alert(data.message || '{% trans "Error removing candidate. Please try again." %}'); alert(data.message || '{% trans "Error removing Application. Please try again." %}');
} }
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
alert('{% trans "Error removing candidate. Please try again." %}'); alert('{% trans "Error removing Application. Please try again." %}');
}); });
}); });

View File

@ -560,7 +560,9 @@
{# Document List #} {# Document List #}
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
{% for document in documents %} {% 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"> <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> <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> <span class="text-muted small">({{ document.file.name|split:"/"|last }})</span>
@ -568,7 +570,15 @@
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="text-muted small me-3">{% trans "Uploaded:" %} {{ document.uploaded_at|date:"d M Y" }}</span> <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="{{ 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> </div>
</li> </li>
{% empty %} {% empty %}

View File

@ -1,35 +1,68 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static i18n %} {% load static i18n %}
{% block title %}{% trans "Delete Application" %} - {{ block.super }}{% endblock %} {% block title %}{% trans "Confirm Delete" %} - {{ block.super }}{% endblock %}
{% block content %} {% block content %}
<div class="card"> <div class="container my-5">
<div class="card-header"> <div class="row justify-content-center">
<h1> <div class="col-lg-8 col-xl-6">
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <div class="card border-danger shadow-lg">
<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="card-header bg-danger text-white">
</svg> <h2 class="h4 mb-0 d-flex align-items-center">
{% trans "Delete Application" %}: {{ object.candidate.full_name }} <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">
</h1> <path d="M10 11V17"></path>
<a href="{% url 'applications_list' %}" class="btn btn-secondary">{% trans "Back to List" %}</a> <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 "Confirm Deletion" %}
</h2>
</div>
<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 class="mb-0"><strong>{% trans "Candidate:" %}</strong> {{ object.candidate.full_name }}</p>
{% endif %}
{% 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 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, Permanently Delete" %}
</button>
</form>
</div>
</div>
</div>
</div> </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 %}
{% if object.candidate %}
<p><strong>{% trans "Candidate:" %}</strong> {{ object.candidate.full_name }}</p>
{% endif %}
<p><strong>{% trans "Application Date:" %}</strong> {{ object.created_at|date:"M d, Y" }}</p>
<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>
</svg>
{% trans "Yes, Delete Application" %}
</button>
</form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -706,7 +706,7 @@
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2"> <div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
<div class="ai-loading-container"> <div class="ai-loading-container">
<i class="fas fa-robot ai-robot-icon"></i> <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>
</div> </div>
{% else %} {% else %}

View File

@ -187,9 +187,9 @@
<!-- Header --> <!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <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" %} <i class="fas fa-user-edit me-2"></i> {% trans "Update Application" %}
</h1> </h3>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'application_detail' object.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'application_detail' object.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-eye me-1"></i> {% trans "View Details" %} <i class="fas fa-eye me-1"></i> {% trans "View Details" %}
@ -255,7 +255,7 @@
{% endfor %} {% endfor %}
{% endif %} {% 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 %} {% csrf_token %}
{{form|crispy}} {{form|crispy}}
</form> </form>

View File

@ -204,7 +204,7 @@
<div class="col-md-6"> <div class="col-md-6">
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label> <label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
<div class="input-group input-group-lg"> <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' %} {% include 'includes/search_form.html' %}
</form> </form>
</div> </div>
@ -269,8 +269,8 @@
{# Table View (Default for Desktop) #} {# Table View (Default for Desktop) #}
<div class="table-view active d-none d-lg-block"> <div class="table-view active d-none d-lg-block">
<div class="table-responsive"> <div class="table-responsive d-none d-lg-block">
<table class="table table-hover align-middle mb-0"> <table class="table table-hover align-middle mb-0 ">
<thead> <thead>
<tr> <tr>
<th scope="col" >{% trans "Name" %}</th> <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' %}"> <a href="{% url 'application_update' candidate.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </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-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'application_delete' candidate.slug %}" 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> <i class="fas fa-trash-alt"></i>
</button> {% endcomment %} </button>
{% endif %} {% endif %}
</div> </div>
</td> </td>
@ -351,7 +351,7 @@
{# Card View (Default for Mobile) #} {# Card View (Default for Mobile) #}
<div class="card-view row g-4 d-lg-none"> <div class="card-view row g-4 d-lg-none">
{% for candidate in applications %} {% 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 candidate-card h-100 shadow-sm">
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between align-items-start mb-3"> <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' %}" <button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal" data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'application_delete' candidate.slug %}" 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> <i class="fas fa-trash-alt"></i>
</button> </button>
{% endif %} {% endif %}
@ -419,4 +419,65 @@
</div> </div>
{% endif %} {% endif %}
</div> </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 %} {% endblock %}

View File

@ -8,8 +8,8 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">{{ title }}</h1> <h4 class="h3 mb-0">{{ title }}</h4>
<a href="{% url 'source_detail' source.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'source_detail' source.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Source <i class="fas fa-arrow-left"></i> Back to Source
</a> </a>
</div> </div>

View File

@ -171,9 +171,9 @@
<!-- Header --> <!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <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 }} <i class="fas fa-plug me-2"></i> {{ title }}
</h1> </h4>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
{% if source %} {% if source %}
<a href="{% url 'source_detail' source.pk %}" class="btn btn-outline-secondary"> <a href="{% url 'source_detail' source.pk %}" class="btn btn-outline-secondary">

View File

@ -68,23 +68,23 @@
{% for source in page_obj %} {% for source in page_obj %}
<tr> <tr>
<td> <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> <strong>{{ source.name }}</strong>
</a> </a>
</td> </td>
<td> <td>
<span class="badge bg-info">{{ source.source_type }}</span> <span class="badge bg-primary-theme">{{ source.source_type }}</span>
</td> </td>
<td> <td>
{% if source.is_active %} {% if source.is_active %}
<span class="badge bg-success">{% trans "Active" %}</span> <span class="badge bg-primary-theme">{% trans "Active" %}</span>
{% else %} {% else %}
<span class="badge bg-secondary">{% trans "Inactive" %}</span> <span class="badge bg-primary-theme">{% trans "Inactive" %}</span>
{% endif %} {% endif %}
</td> </td>
<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>
<td> <td>
<small class="text-muted">{{ source.created_at|date:"M d, Y" }}</small> <small class="text-muted">{{ source.created_at|date:"M d, Y" }}</small>

View File

@ -221,7 +221,7 @@
</a> {% endcomment %} </a> {% endcomment %}
{# 2. Change Password Button (Key Icon) #} {# 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> <i class="fas fa-key"></i>
</a> </a>

View File

@ -112,7 +112,13 @@
<p class="text-muted mb-0">{% trans "Manage your personal details and security." %}</p> <p class="text-muted mb-0">{% trans "Manage your personal details and security." %}</p>
</div> </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;"> <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.first_name %}{{ user.first_name.0 }}{% else %}<i class="fas fa-user"></i>{% endif %} {% 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>
</div> </div>