Compare commits

...

10 Commits

62 changed files with 7989 additions and 4207 deletions

View File

@ -354,7 +354,7 @@ class ScheduledInterview(Base):
candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE, related_name="scheduled_interviews") candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE, related_name="scheduled_interviews")
job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name="scheduled_interviews") job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name="scheduled_interviews")
zoom_meeting = models.OneToOneField(ZoomMeeting, on_delete=models.CASCADE, related_name="interview") zoom_meeting = models.OneToOneField(ZoomMeeting, on_delete=models.CASCADE, related_name="interview")
schedule = models.ForeignKey(InterviewSchedule, on_delete=models.CASCADE, related_name="interviews", null=True, blank=True) schedule = models.ForeignKey(BulkInterviewTemplate, on_delete=models.CASCADE, related_name="interviews", null=True, blank=True)
interview_date = models.DateField() interview_date = models.DateField()
interview_time = models.TimeField() interview_time = models.TimeField()
status = models.CharField(max_length=20, choices=[ status = models.CharField(max_length=20, choices=[
@ -365,9 +365,9 @@ class ScheduledInterview(Base):
], default="scheduled") ], default="scheduled")
``` ```
#### 2.2.11 InterviewSchedule Model #### 2.2.11 BulkInterviewTemplate Model
```python ```python
class InterviewSchedule(Base): class BulkInterviewTemplate(Base):
job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name="interview_schedules") job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name="interview_schedules")
candidates = models.ManyToManyField(Candidate, related_name="interview_schedules", blank=True, null=True) candidates = models.ManyToManyField(Candidate, related_name="interview_schedules", blank=True, null=True)
start_date = models.DateField() start_date = models.DateField()
@ -533,7 +533,7 @@ class CandidateService:
### 5.2 Interview Scheduling Logic ### 5.2 Interview Scheduling Logic
```python ```python
class InterviewScheduler: class BulkInterviewTemplater:
@staticmethod @staticmethod
def get_available_slots(schedule, date): def get_available_slots(schedule, date):
"""Get available interview slots for a specific date""" """Get available interview slots for a specific date"""
@ -915,7 +915,7 @@ class InterviewSchedulingTestCase(TestCase):
phone="9876543210", phone="9876543210",
job=self.job job=self.job
) )
self.schedule = InterviewSchedule.objects.create( self.schedule = BulkInterviewTemplate.objects.create(
job=self.job, job=self.job,
start_date=timezone.now().date(), start_date=timezone.now().date(),
end_date=timezone.now().date() + timedelta(days=7), end_date=timezone.now().date() + timedelta(days=7),
@ -930,7 +930,7 @@ class InterviewSchedulingTestCase(TestCase):
def test_interview_scheduling(self): def test_interview_scheduling(self):
"""Test interview scheduling process""" """Test interview scheduling process"""
# Test slot availability # Test slot availability
available_slots = InterviewScheduler.get_available_slots( available_slots = BulkInterviewTemplater.get_available_slots(
self.schedule, self.schedule,
timezone.now().date() timezone.now().date()
) )
@ -942,7 +942,7 @@ class InterviewSchedulingTestCase(TestCase):
'start_time': timezone.now().time(), 'start_time': timezone.now().time(),
'duration': 60 'duration': 60
} }
interview = InterviewScheduler.schedule_interview( interview = BulkInterviewTemplater.schedule_interview(
self.candidate, self.candidate,
self.job, self.job,
schedule_data schedule_data

View File

@ -86,7 +86,7 @@ The test suite aims for 80% code coverage. Coverage reports are generated in:
- **Candidate**: Stage transitions, relationships - **Candidate**: Stage transitions, relationships
- **ZoomMeeting**: Time validation, status handling - **ZoomMeeting**: Time validation, status handling
- **FormTemplate**: Template integrity, field ordering - **FormTemplate**: Template integrity, field ordering
- **InterviewSchedule**: Scheduling logic, slot generation - **BulkInterviewTemplate**: Scheduling logic, slot generation
### 2. View Testing ### 2. View Testing
- **Job Management**: CRUD operations, search, filtering - **Job Management**: CRUD operations, search, filtering
@ -97,7 +97,7 @@ The test suite aims for 80% code coverage. Coverage reports are generated in:
### 3. Form Testing ### 3. Form Testing
- **JobPostingForm**: Complex validation, field dependencies - **JobPostingForm**: Complex validation, field dependencies
- **CandidateForm**: File upload, validation - **CandidateForm**: File upload, validation
- **InterviewScheduleForm**: Dynamic fields, validation - **BulkInterviewTemplateForm**: Dynamic fields, validation
- **MeetingCommentForm**: Comment creation/editing - **MeetingCommentForm**: Comment creation/editing
### 4. Integration Testing ### 4. Integration Testing

View File

@ -28,13 +28,13 @@ from datetime import datetime, time, timedelta, date
from recruitment.models import ( from recruitment.models import (
JobPosting, Candidate, ZoomMeeting, FormTemplate, FormStage, FormField, JobPosting, Candidate, ZoomMeeting, FormTemplate, FormStage, FormField,
FormSubmission, FieldResponse, InterviewSchedule, ScheduledInterview, FormSubmission, FieldResponse, BulkInterviewTemplate, ScheduledInterview,
TrainingMaterial, Source, HiringAgency, Profile, MeetingComment, JobPostingImage, TrainingMaterial, Source, HiringAgency, Profile, MeetingComment, JobPostingImage,
BreakTime BreakTime
) )
from recruitment.forms import ( from recruitment.forms import (
JobPostingForm, CandidateForm, ZoomMeetingForm, MeetingCommentForm, JobPostingForm, CandidateForm, ZoomMeetingForm, MeetingCommentForm,
CandidateStageForm, InterviewScheduleForm, BreakTimeFormSet CandidateStageForm, BulkInterviewTemplateForm, BreakTimeFormSet
) )
@ -185,7 +185,7 @@ def interview_schedule(staff_user, job):
) )
candidates.append(candidate) candidates.append(candidate)
return InterviewSchedule.objects.create( return BulkInterviewTemplate.objects.create(
job=job, job=job,
created_by=staff_user, created_by=staff_user,
start_date=date.today() + timedelta(days=1), start_date=date.today() + timedelta(days=1),

View File

@ -3,10 +3,10 @@ from django.utils.html import format_html
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from .models import ( from .models import (
JobPosting, Application, TrainingMaterial, ZoomMeetingDetails, JobPosting, Application, TrainingMaterial,
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse, FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
SharedFormTemplate, Source, HiringAgency, IntegrationLog,InterviewSchedule,JobPostingImage,InterviewNote, SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,InterviewNote,
AgencyAccessLink, AgencyJobAssignment AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview
) )
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -158,27 +158,27 @@ class TrainingMaterialAdmin(admin.ModelAdmin):
save_on_top = True save_on_top = True
@admin.register(ZoomMeetingDetails) # @admin.register(ZoomMeetingDetails)
class ZoomMeetingAdmin(admin.ModelAdmin): # class ZoomMeetingAdmin(admin.ModelAdmin):
list_display = ['topic', 'meeting_id', 'start_time', 'duration', 'created_at'] # list_display = ['topic', 'meeting_id', 'start_time', 'duration', 'created_at']
list_filter = ['timezone', 'created_at'] # list_filter = ['timezone', 'created_at']
search_fields = ['topic', 'meeting_id'] # search_fields = ['topic', 'meeting_id']
readonly_fields = ['created_at', 'updated_at'] # readonly_fields = ['created_at', 'updated_at']
fieldsets = ( # fieldsets = (
('Meeting Details', { # ('Meeting Details', {
'fields': ('topic', 'meeting_id', 'start_time', 'duration', 'timezone','status') # 'fields': ('topic', 'meeting_id', 'start_time', 'duration', 'timezone','status')
}), # }),
('Meeting Settings', { # ('Meeting Settings', {
'fields': ('participant_video', 'join_before_host', 'mute_upon_entry', 'waiting_room') # 'fields': ('participant_video', 'join_before_host', 'mute_upon_entry', 'waiting_room')
}), # }),
('Access', { # ('Access', {
'fields': ('join_url',) # 'fields': ('join_url',)
}), # }),
('System Response', { # ('System Response', {
'fields': ('zoom_gateway_response', 'created_at', 'updated_at') # 'fields': ('zoom_gateway_response', 'created_at', 'updated_at')
}), # }),
) # )
save_on_top = True # save_on_top = True
# @admin.register(InterviewNote) # @admin.register(InterviewNote)
@ -241,9 +241,11 @@ admin.site.register(FormStage)
admin.site.register(Application) admin.site.register(Application)
admin.site.register(FormField) admin.site.register(FormField)
admin.site.register(FieldResponse) admin.site.register(FieldResponse)
admin.site.register(InterviewSchedule) admin.site.register(BulkInterviewTemplate)
admin.site.register(AgencyAccessLink) admin.site.register(AgencyAccessLink)
admin.site.register(AgencyJobAssignment) admin.site.register(AgencyJobAssignment)
admin.site.register(Interview)
admin.site.register(ScheduledInterview)
# AgencyMessage admin removed - model has been deleted # AgencyMessage admin removed - model has been deleted

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2025-11-17 09:52 # Generated by Django 5.2.7 on 2025-11-27 15:36
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
@ -49,16 +49,29 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='InterviewLocation', name='Interview',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated 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')), ('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')), ('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')), ('details_url', models.URLField(blank=True, max_length=2048, null=True, verbose_name='Meeting/Location URL')),
('topic', models.CharField(blank=True, help_text="e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'", max_length=255, verbose_name='Location/Meeting Topic')),
('timezone', models.CharField(default='UTC', max_length=50, verbose_name='Timezone')), ('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={ options={
'verbose_name': 'Interview Location', 'verbose_name': 'Interview Location',
@ -121,7 +134,6 @@ class Migration(migrations.Migration):
('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')), ('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')), ('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')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ('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')), ('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')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
@ -129,6 +141,7 @@ class Migration(migrations.Migration):
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Phone')), ('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')), ('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')), ('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')), ('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')), ('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')),
], ],
@ -266,42 +279,22 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='OnsiteLocationDetails', name='InterviewNote',
fields=[ fields=[
('interviewlocation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='recruitment.interviewlocation')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('physical_address', models.CharField(blank=True, max_length=255, null=True, verbose_name='Physical Address')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('room_number', models.CharField(blank=True, max_length=50, null=True, verbose_name='Room Number/Name')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')), ('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')),
('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended'), ('cancelled', 'Cancelled')], db_index=True, default='waiting', max_length=20)), ('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={ options={
'verbose_name': 'Onsite Location Details', 'verbose_name': 'Interview Note',
'verbose_name_plural': 'Onsite Location Details', 'verbose_name_plural': 'Interview Notes',
'ordering': ['created_at'],
}, },
bases=('recruitment.interviewlocation',),
),
migrations.CreateModel(
name='ZoomMeetingDetails',
fields=[
('interviewlocation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='recruitment.interviewlocation')),
('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended'), ('cancelled', 'Cancelled')], db_index=True, default='waiting', max_length=20)),
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')),
('meeting_id', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='External Meeting ID')),
('password', models.CharField(blank=True, max_length=20, null=True, verbose_name='Password')),
('zoom_gateway_response', models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response')),
('participant_video', models.BooleanField(default=True, verbose_name='Participant Video')),
('join_before_host', models.BooleanField(default=False, verbose_name='Join Before Host')),
('host_email', models.CharField(blank=True, null=True)),
('mute_upon_entry', models.BooleanField(default=False, verbose_name='Mute Upon Entry')),
('waiting_room', models.BooleanField(default=False, verbose_name='Waiting Room')),
],
options={
'verbose_name': 'Zoom Meeting Details',
'verbose_name_plural': 'Zoom Meeting Details',
},
bases=('recruitment.interviewlocation',),
), ),
migrations.CreateModel( migrations.CreateModel(
name='JobPosting', name='JobPosting',
@ -312,8 +305,8 @@ class Migration(migrations.Migration):
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('title', models.CharField(max_length=200)), ('title', models.CharField(max_length=200)),
('department', models.CharField(blank=True, max_length=100)), ('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)), ('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)), ('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_city', models.CharField(blank=True, max_length=100)),
('location_state', 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)), ('location_country', models.CharField(default='Saudia Arabia', max_length=100)),
@ -321,7 +314,7 @@ class Migration(migrations.Migration):
('qualifications', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), ('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)), ('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)), ('benefits', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])), ('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_deadline', models.DateField(db_index=True)),
('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), ('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
('internal_job_id', models.CharField(editable=False, max_length=50)), ('internal_job_id', models.CharField(editable=False, max_length=50)),
@ -343,8 +336,10 @@ class Migration(migrations.Migration):
('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')), ('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)), ('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')), ('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')), ('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 candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')), ('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')), ('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={ options={
@ -353,14 +348,18 @@ class Migration(migrations.Migration):
'ordering': ['-created_at'], '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( migrations.CreateModel(
name='InterviewSchedule', name='BulkInterviewTemplate',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated 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')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('schedule_interview_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], default='Remote', max_length=10, verbose_name='Interview Type')),
('start_date', models.DateField(db_index=True, verbose_name='Start Date')), ('start_date', models.DateField(db_index=True, verbose_name='Start Date')),
('end_date', models.DateField(db_index=True, verbose_name='End Date')), ('end_date', models.DateField(db_index=True, verbose_name='End Date')),
('working_days', models.JSONField(verbose_name='Working Days')), ('working_days', models.JSONField(verbose_name='Working Days')),
@ -372,18 +371,13 @@ class Migration(migrations.Migration):
('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')), ('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')),
('applications', models.ManyToManyField(blank=True, related_name='interview_schedules', to='recruitment.application')), ('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)), ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('template_location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interviewlocation', verbose_name='Location Template (Zoom/Onsite)')), ('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')), ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')),
], ],
options={ options={
'abstract': False, 'abstract': False,
}, },
), ),
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.AddField( migrations.AddField(
model_name='application', model_name='application',
name='job', name='job',
@ -474,7 +468,7 @@ class Migration(migrations.Migration):
('first_name', models.CharField(max_length=255, verbose_name='First Name')), ('first_name', models.CharField(max_length=255, verbose_name='First Name')),
('last_name', models.CharField(max_length=255, verbose_name='Last 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')), ('middle_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Middle Name')),
('email', models.EmailField(db_index=True, help_text='Unique email address for the person', max_length=254, unique=True, verbose_name='Email')), ('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')), ('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')), ('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')), ('gender', models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female')], max_length=1, null=True, verbose_name='Gender')),
@ -505,33 +499,16 @@ class Migration(migrations.Migration):
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('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_date', models.DateField(db_index=True, verbose_name='Interview Date')),
('interview_time', models.TimeField(verbose_name='Interview Time')), ('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)), ('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')), ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.application')),
('interview_location', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='scheduled_interview', to='recruitment.interviewlocation', verbose_name='Meeting/Location Details')), ('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')), ('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')), ('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.interviewschedule')), ('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)), ('system_users', models.ManyToManyField(blank=True, related_name='attended_interviews', to=settings.AUTH_USER_MODEL)),
], ],
), ),
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.scheduledinterview', verbose_name='Scheduled Interview')),
],
options={
'verbose_name': 'Interview Note',
'verbose_name_plural': 'Interview Notes',
'ordering': ['created_at'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='SharedFormTemplate', name='SharedFormTemplate',
fields=[ fields=[
@ -656,11 +633,6 @@ class Migration(migrations.Migration):
model_name='formsubmission', model_name='formsubmission',
index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'), index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'),
), ),
migrations.AddField(
model_name='notification',
name='related_meeting',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='recruitment.zoommeetingdetails', verbose_name='Related Meeting'),
),
migrations.AddIndex( migrations.AddIndex(
model_name='formtemplate', model_name='formtemplate',
index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'), index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'),
@ -705,6 +677,14 @@ class Migration(migrations.Migration):
model_name='message', model_name='message',
index=models.Index(fields=['message_type', 'created_at'], name='recruitment_message_f25659_idx'), 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( migrations.AddIndex(
model_name='person', model_name='person',
index=models.Index(fields=['email'], name='recruitment_email_0b1ab1_idx'), index=models.Index(fields=['email'], name='recruitment_email_0b1ab1_idx'),
@ -757,12 +737,4 @@ class Migration(migrations.Migration):
model_name='jobposting', model_name='jobposting',
index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'), index=models.Index(fields=['slug'], name='recruitment_slug_004045_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'),
),
] ]

View File

@ -1,29 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-18 10:24
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='jobposting',
name='job_type',
field=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),
),
migrations.AlterField(
model_name='jobposting',
name='workplace_type',
field=models.CharField(choices=[('On-site', 'On-site'), ('Remote', 'Remote'), ('Hybrid', 'Hybrid')], default='On-site', max_length=20),
),
migrations.AlterField(
model_name='scheduledinterview',
name='interview_location',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interview', to='recruitment.interviewlocation', verbose_name='Meeting/Location Details'),
),
]

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

@ -1,23 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-19 14:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0002_alter_jobposting_job_type_and_more'),
]
operations = [
migrations.AddField(
model_name='jobposting',
name='cv_zip_file',
field=models.FileField(blank=True, null=True, upload_to='job_zips/'),
),
migrations.AddField(
model_name='jobposting',
name='zip_created',
field=models.BooleanField(default=False),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-23 09:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_jobposting_cv_zip_file_jobposting_zip_created'),
]
operations = [
migrations.AlterField(
model_name='interviewschedule',
name='template_location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedule_templates', to='recruitment.interviewlocation', verbose_name='Location Template (Zoom/Onsite)'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-23 09:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0004_alter_interviewschedule_template_location'),
]
operations = [
migrations.AlterField(
model_name='interviewschedule',
name='template_location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interviewlocation', verbose_name='Location Template (Zoom/Onsite)'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-23 12:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0005_alter_interviewschedule_template_location'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='email',
field=models.EmailField(error_messages={'unique': 'A user with this email already exists.'}, max_length=254, unique=True),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-25 12:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0006_alter_customuser_email'),
]
operations = [
migrations.AlterField(
model_name='person',
name='email',
field=models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email'),
),
]

View File

@ -469,6 +469,10 @@ class JobPosting(Base):
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):
job = models.OneToOneField( job = models.OneToOneField(
@ -518,7 +522,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 +548,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")
@ -584,6 +599,8 @@ class Person(Base):
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):
"""Model to store job-specific application data""" """Model to store job-specific application data"""
@ -995,36 +1012,36 @@ class Application(Base):
"""Legacy compatibility - get scheduled interviews for this application""" """Legacy compatibility - get scheduled interviews for this application"""
return self.scheduled_interviews.all() return self.scheduled_interviews.all()
@property # @property
def get_latest_meeting(self): # def get_latest_meeting(self):
""" # """
Retrieves the most specific location details (subclass instance) # Retrieves the most specific location details (subclass instance)
of the latest ScheduledInterview for this application, or None. # of the latest ScheduledInterview for this application, or None.
""" # """
# 1. Get the latest ScheduledInterview # # 1. Get the latest ScheduledInterview
schedule = self.scheduled_interviews.order_by("-created_at").first() # schedule = self.scheduled_interviews.order_by("-created_at").first()
# Check if a schedule exists and if it has an interview location # # Check if a schedule exists and if it has an interview location
if not schedule or not schedule.interview_location: # if not schedule or not schedule.interview_location:
return None # return None
# Get the base location instance # # Get the base location instance
interview_location = schedule.interview_location # interview_location = schedule.interview_location
# 2. Safely retrieve the specific subclass details # # 2. Safely retrieve the specific subclass details
# Determine the expected subclass accessor name based on the location_type # # Determine the expected subclass accessor name based on the location_type
if interview_location.location_type == 'Remote': # if interview_location.location_type == 'Remote':
accessor_name = 'zoommeetingdetails' # accessor_name = 'zoommeetingdetails'
else: # Assumes 'Onsite' or any other type defaults to Onsite # else: # Assumes 'Onsite' or any other type defaults to Onsite
accessor_name = 'onsitelocationdetails' # accessor_name = 'onsitelocationdetails'
# Use getattr to safely retrieve the specific meeting object (subclass instance). # # Use getattr to safely retrieve the specific meeting object (subclass instance).
# If the accessor exists but points to None (because the subclass record was deleted), # # If the accessor exists but points to None (because the subclass record was deleted),
# or if the accessor name is wrong for the object's true type, it will return None. # # or if the accessor name is wrong for the object's true type, it will return None.
meeting_details = getattr(interview_location, accessor_name, None) # meeting_details = getattr(interview_location, accessor_name, None)
return meeting_details # return meeting_details
@property @property
@ -1094,9 +1111,6 @@ class Application(Base):
class TrainingMaterial(Base): class TrainingMaterial(Base):
title = models.CharField(max_length=255, verbose_name=_("Title")) title = models.CharField(max_length=255, verbose_name=_("Title"))
content = CKEditor5Field( content = CKEditor5Field(
@ -1118,17 +1132,155 @@ class TrainingMaterial(Base):
return self.title return self.title
class InterviewLocation(Base): # class InterviewLocation(Base):
""" # """
Base model for all interview location/meeting details (remote or onsite) # Base model for all interview location/meeting details (remote or onsite)
using Multi-Table Inheritance. # using Multi-Table Inheritance.
""" # """
# class LocationType(models.TextChoices):
# REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)')
# ONSITE = 'Onsite', _('In-Person (Physical Location)')
# class Status(models.TextChoices):
# """Defines the possible real-time statuses for any interview location/meeting."""
# WAITING = "waiting", _("Waiting")
# STARTED = "started", _("Started")
# ENDED = "ended", _("Ended")
# CANCELLED = "cancelled", _("Cancelled")
# location_type = models.CharField(
# max_length=10,
# choices=LocationType.choices,
# verbose_name=_("Location Type"),
# db_index=True
# )
# details_url = models.URLField(
# verbose_name=_("Meeting/Location URL"),
# max_length=2048,
# blank=True,
# null=True
# )
# topic = models.CharField( # Renamed from 'description' to 'topic' to match your input
# max_length=255,
# verbose_name=_("Location/Meeting Topic"),
# blank=True,
# help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'")
# )
# timezone = models.CharField(
# max_length=50,
# verbose_name=_("Timezone"),
# default='UTC'
# )
# def __str__(self):
# # Use 'topic' instead of 'description'
# return f"{self.get_location_type_display()} - {self.topic[:50]}"
# class Meta:
# verbose_name = _("Interview Location")
# verbose_name_plural = _("Interview Locations")
# class ZoomMeetingDetails(InterviewLocation):
# """Concrete model for remote interviews (Zoom specifics)."""
# status = models.CharField(
# db_index=True,
# max_length=20,
# choices=InterviewLocation.Status.choices,
# default=InterviewLocation.Status.WAITING,
# )
# start_time = models.DateTimeField(
# db_index=True, verbose_name=_("Start Time")
# )
# duration = models.PositiveIntegerField(
# verbose_name=_("Duration (minutes)")
# )
# meeting_id = models.CharField(
# db_index=True,
# max_length=50,
# unique=True,
# verbose_name=_("External Meeting ID")
# )
# password = models.CharField(
# max_length=20, blank=True, null=True, verbose_name=_("Password")
# )
# zoom_gateway_response = models.JSONField(
# blank=True, null=True, verbose_name=_("Zoom Gateway Response")
# )
# participant_video = models.BooleanField(
# default=True, verbose_name=_("Participant Video")
# )
# join_before_host = models.BooleanField(
# default=False, verbose_name=_("Join Before Host")
# )
# host_email=models.CharField(null=True,blank=True)
# mute_upon_entry = models.BooleanField(
# default=False, verbose_name=_("Mute Upon Entry")
# )
# waiting_room = models.BooleanField(default=False, verbose_name=_("Waiting Room"))
# # *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation ***
# # @classmethod
# # def create(cls, **kwargs):
# # """Factory method to ensure location_type is set to REMOTE."""
# # return cls(location_type=InterviewLocation.LocationType.REMOTE, **kwargs)
# class Meta:
# verbose_name = _("Zoom Meeting Details")
# verbose_name_plural = _("Zoom Meeting Details")
# class OnsiteLocationDetails(InterviewLocation):
# """Concrete model for onsite interviews (Room/Address specifics)."""
# physical_address = models.CharField(
# max_length=255,
# verbose_name=_("Physical Address"),
# blank=True,
# null=True
# )
# room_number = models.CharField(
# max_length=50,
# verbose_name=_("Room Number/Name"),
# blank=True,
# null=True
# )
# start_time = models.DateTimeField(
# db_index=True, verbose_name=_("Start Time")
# )
# duration = models.PositiveIntegerField(
# verbose_name=_("Duration (minutes)")
# )
# status = models.CharField(
# db_index=True,
# max_length=20,
# choices=InterviewLocation.Status.choices,
# default=InterviewLocation.Status.WAITING,
# )
# # *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation ***
# # @classmethod
# # def create(cls, **kwargs):
# # """Factory method to ensure location_type is set to ONSITE."""
# # return cls(location_type=InterviewLocation.LocationType.ONSITE, **kwargs)
# class Meta:
# verbose_name = _("Onsite Location Details")
# verbose_name_plural = _("Onsite Location Details")
class Interview(Base):
class LocationType(models.TextChoices): class LocationType(models.TextChoices):
REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)') REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)')
ONSITE = 'Onsite', _('In-Person (Physical Location)') ONSITE = 'Onsite', _('In-Person (Physical Location)')
class Status(models.TextChoices): class Status(models.TextChoices):
"""Defines the possible real-time statuses for any interview location/meeting."""
WAITING = "waiting", _("Waiting") WAITING = "waiting", _("Waiting")
STARTED = "started", _("Started") STARTED = "started", _("Started")
ENDED = "ended", _("Ended") ENDED = "ended", _("Ended")
@ -1141,137 +1293,73 @@ class InterviewLocation(Base):
db_index=True db_index=True
) )
# Common fields
topic = models.CharField(
max_length=255,
verbose_name=_("Meeting/Location Topic"),
blank=True,
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'")
)
details_url = models.URLField( details_url = models.URLField(
verbose_name=_("Meeting/Location URL"), verbose_name=_("Meeting/Location URL"),
max_length=2048, max_length=2048,
blank=True, blank=True,
null=True null=True
) )
timezone = models.CharField(max_length=50, verbose_name=_("Timezone"), default='UTC')
topic = models.CharField( # Renamed from 'description' to 'topic' to match your input start_time = models.DateTimeField(db_index=True, verbose_name=_("Start Time"))
max_length=255, duration = models.PositiveIntegerField(verbose_name=_("Duration (minutes)"))
verbose_name=_("Location/Meeting Topic"), status = models.CharField(
blank=True, max_length=20,
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'") choices=Status.choices,
default=Status.WAITING,
db_index=True
) )
timezone = models.CharField( # Remote-specific (nullable)
max_length=50, meeting_id = models.CharField(
verbose_name=_("Timezone"), max_length=50, unique=True, null=True, blank=True, verbose_name=_("External Meeting ID")
default='UTC'
) )
password = models.CharField(max_length=20, blank=True, 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(max_length=255, blank=True, null=True)
mute_upon_entry = models.BooleanField(default=False)
waiting_room = models.BooleanField(default=False)
# Onsite-specific (nullable)
physical_address = models.CharField(max_length=255, blank=True, null=True)
room_number = models.CharField(max_length=50, blank=True, null=True)
def __str__(self): def __str__(self):
# Use 'topic' instead of 'description'
return f"{self.get_location_type_display()} - {self.topic[:50]}" return f"{self.get_location_type_display()} - {self.topic[:50]}"
class Meta: class Meta:
verbose_name = _("Interview Location") verbose_name = _("Interview Location")
verbose_name_plural = _("Interview Locations") verbose_name_plural = _("Interview Locations")
def clean(self):
class ZoomMeetingDetails(InterviewLocation): # Optional: add validation
"""Concrete model for remote interviews (Zoom specifics).""" if self.location_type == self.LocationType.REMOTE:
if not self.details_url:
status = models.CharField( raise ValidationError(_("Remote interviews require a meeting URL."))
db_index=True, if not self.meeting_id:
max_length=20, raise ValidationError(_("Meeting ID is required for remote interviews."))
choices=InterviewLocation.Status.choices, elif self.location_type == self.LocationType.ONSITE:
default=InterviewLocation.Status.WAITING, if not (self.physical_address or self.room_number):
) raise ValidationError(_("Onsite interviews require at least an address or room."))
start_time = models.DateTimeField(
db_index=True, verbose_name=_("Start Time")
)
duration = models.PositiveIntegerField(
verbose_name=_("Duration (minutes)")
)
meeting_id = models.CharField(
db_index=True,
max_length=50,
unique=True,
verbose_name=_("External Meeting ID")
)
password = models.CharField(
max_length=20, blank=True, null=True, verbose_name=_("Password")
)
zoom_gateway_response = models.JSONField(
blank=True, null=True, verbose_name=_("Zoom Gateway Response")
)
participant_video = models.BooleanField(
default=True, verbose_name=_("Participant Video")
)
join_before_host = models.BooleanField(
default=False, verbose_name=_("Join Before Host")
)
host_email=models.CharField(null=True,blank=True)
mute_upon_entry = models.BooleanField(
default=False, verbose_name=_("Mute Upon Entry")
)
waiting_room = models.BooleanField(default=False, verbose_name=_("Waiting Room"))
# *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation ***
# @classmethod
# def create(cls, **kwargs):
# """Factory method to ensure location_type is set to REMOTE."""
# return cls(location_type=InterviewLocation.LocationType.REMOTE, **kwargs)
class Meta:
verbose_name = _("Zoom Meeting Details")
verbose_name_plural = _("Zoom Meeting Details")
class OnsiteLocationDetails(InterviewLocation):
"""Concrete model for onsite interviews (Room/Address specifics)."""
physical_address = models.CharField(
max_length=255,
verbose_name=_("Physical Address"),
blank=True,
null=True
)
room_number = models.CharField(
max_length=50,
verbose_name=_("Room Number/Name"),
blank=True,
null=True
)
start_time = models.DateTimeField(
db_index=True, verbose_name=_("Start Time")
)
duration = models.PositiveIntegerField(
verbose_name=_("Duration (minutes)")
)
status = models.CharField(
db_index=True,
max_length=20,
choices=InterviewLocation.Status.choices,
default=InterviewLocation.Status.WAITING,
)
# *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation ***
# @classmethod
# def create(cls, **kwargs):
# """Factory method to ensure location_type is set to ONSITE."""
# return cls(location_type=InterviewLocation.LocationType.ONSITE, **kwargs)
class Meta:
verbose_name = _("Onsite Location Details")
verbose_name_plural = _("Onsite Location Details")
# --- 2. Scheduling Models --- # --- 2. Scheduling Models ---
class InterviewSchedule(Base): class BulkInterviewTemplate(Base):
"""Stores the TEMPLATE criteria for BULK interview generation.""" """Stores the TEMPLATE criteria for BULK interview generation."""
# We need a field to store the template location details linked to this bulk schedule. # We need a field to store the template location details linked to this bulk schedule.
# This location object contains the generic Zoom/Onsite info to be cloned. # This location object contains the generic Zoom/Onsite info to be cloned.
template_location = models.ForeignKey( interview = models.ForeignKey(
InterviewLocation, Interview,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="schedule_templates", related_name="schedule_templates",
null=True, null=True,
@ -1279,15 +1367,6 @@ class InterviewSchedule(Base):
verbose_name=_("Location Template (Zoom/Onsite)") verbose_name=_("Location Template (Zoom/Onsite)")
) )
# NOTE: schedule_interview_type field is needed in the form,
# but not on the model itself if we use template_location.
# If you want to keep it:
schedule_interview_type = models.CharField(
max_length=10,
choices=InterviewLocation.LocationType.choices,
verbose_name=_("Interview Type"),
default=InterviewLocation.LocationType.REMOTE
)
job = models.ForeignKey( job = models.ForeignKey(
JobPosting, JobPosting,
@ -1332,6 +1411,9 @@ class InterviewSchedule(Base):
class ScheduledInterview(Base): class ScheduledInterview(Base):
"""Stores individual scheduled interviews (whether bulk or individually created).""" """Stores individual scheduled interviews (whether bulk or individually created)."""
class InterviewTypeChoice(models.TextChoices):
REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)')
ONSITE = 'Onsite', _('In-Person (Physical Location)')
class InterviewStatus(models.TextChoices): class InterviewStatus(models.TextChoices):
SCHEDULED = "scheduled", _("Scheduled") SCHEDULED = "scheduled", _("Scheduled")
@ -1353,19 +1435,19 @@ class ScheduledInterview(Base):
) )
# Links to the specific, individual location/meeting details for THIS interview # Links to the specific, individual location/meeting details for THIS interview
interview_location = models.OneToOneField( interview = models.OneToOneField(
InterviewLocation, Interview,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="scheduled_interview", related_name="scheduled_interview",
null=True, null=True,
blank=True, blank=True,
db_index=True, db_index=True,
verbose_name=_("Meeting/Location Details") verbose_name=_("Interview/Meeting")
) )
# Link back to the bulk schedule template (optional if individually created) # Link back to the bulk schedule template (optional if individually created)
schedule = models.ForeignKey( schedule = models.ForeignKey(
InterviewSchedule, BulkInterviewTemplate,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="interviews", related_name="interviews",
null=True, null=True,
@ -1378,7 +1460,11 @@ class ScheduledInterview(Base):
interview_date = models.DateField(db_index=True, verbose_name=_("Interview Date")) interview_date = models.DateField(db_index=True, verbose_name=_("Interview Date"))
interview_time = models.TimeField(verbose_name=_("Interview Time")) interview_time = models.TimeField(verbose_name=_("Interview Time"))
interview_type = models.CharField(
max_length=20,
choices=InterviewTypeChoice.choices,
default=InterviewTypeChoice.REMOTE
)
status = models.CharField( status = models.CharField(
db_index=True, db_index=True,
max_length=20, max_length=20,
@ -1420,7 +1506,7 @@ class InterviewNote(Base):
1 1
interview = models.ForeignKey( interview = models.ForeignKey(
ScheduledInterview, Interview,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="notes", related_name="notes",
verbose_name=_("Scheduled Interview"), verbose_name=_("Scheduled Interview"),
@ -1979,6 +2065,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"""
@ -2301,14 +2398,14 @@ class Notification(models.Model):
default=Status.PENDING, default=Status.PENDING,
verbose_name=_("Status"), verbose_name=_("Status"),
) )
related_meeting = models.ForeignKey( # related_meeting = models.ForeignKey(
ZoomMeetingDetails, # ZoomMeetingDetails,
on_delete=models.CASCADE, # on_delete=models.CASCADE,
related_name="notifications", # related_name="notifications",
null=True, # null=True,
blank=True, # blank=True,
verbose_name=_("Related Meeting"), # verbose_name=_("Related Meeting"),
) # )
scheduled_for = models.DateTimeField( scheduled_for = models.DateTimeField(
verbose_name=_("Scheduled Send Time"), verbose_name=_("Scheduled Send Time"),
help_text=_("The date and time this notification is scheduled to be sent."), help_text=_("The date and time this notification is scheduled to be sent."),

View File

@ -12,7 +12,7 @@ from . linkedin_service import LinkedInService
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from . models import JobPosting from . models import JobPosting
from django.utils import timezone from django.utils import timezone
from . models import InterviewSchedule,ScheduledInterview,ZoomMeetingDetails,Message from . models import ScheduledInterview,Interview,Message
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
# Add python-docx import for Word document processing # Add python-docx import for Word document processing
@ -679,20 +679,28 @@ def create_interview_and_meeting(
Synchronous task for a single interview slot, dispatched by django-q. Synchronous task for a single interview slot, dispatched by django-q.
""" """
try: try:
candidate = Application.objects.get(pk=candidate_id) application = Application.objects.get(pk=candidate_id)
job = JobPosting.objects.get(pk=job_id) job = JobPosting.objects.get(pk=job_id)
schedule = InterviewSchedule.objects.get(pk=schedule_id) schedule = ScheduledInterview.objects.get(pk=schedule_id)
interview_datetime = timezone.make_aware(datetime.combine(slot_date, slot_time)) interview_datetime = timezone.make_aware(datetime.combine(slot_date, slot_time))
meeting_topic = f"Interview for {job.title} - {candidate.name}" meeting_topic = f"Interview for {job.title} - {application.name}"
# 1. External API Call (Slow) # 1. External API Call (Slow)
# "status": "success",
# "message": "Meeting created successfully.",
# "meeting_details": {
# "join_url": meeting_data['join_url'],
# "meeting_id": meeting_data['id'],
# "password": meeting_data['password'],
# "host_email": meeting_data['host_email']
# },
# "zoom_gateway_response": meeting_data
# }
result = create_zoom_meeting(meeting_topic, interview_datetime, duration) result = create_zoom_meeting(meeting_topic, interview_datetime, duration)
if result["status"] == "success": if result["status"] == "success":
# 2. Database Writes (Slow) interview = Interview.objects.create(
zoom_meeting = ZoomMeetingDetails.objects.create(
topic=meeting_topic, topic=meeting_topic,
start_time=interview_datetime, start_time=interview_datetime,
duration=duration, duration=duration,
@ -703,14 +711,31 @@ def create_interview_and_meeting(
password=result["meeting_details"]["password"], password=result["meeting_details"]["password"],
location_type="Remote" location_type="Remote"
) )
ScheduledInterview.objects.create( schedule.interviews = interview
application=candidate, schedule.status = "Remote"
job=job,
interview_location=zoom_meeting, schedule.save()
schedule=schedule,
interview_date=slot_date, # 2. Database Writes (Slow)
interview_time=slot_time # zoom_meeting = ZoomMeetingDetails.objects.create(
) # topic=meeting_topic,
# start_time=interview_datetime,
# duration=duration,
# meeting_id=result["meeting_details"]["meeting_id"],
# details_url=result["meeting_details"]["join_url"],
# zoom_gateway_response=result["zoom_gateway_response"],
# host_email=result["meeting_details"]["host_email"],
# password=result["meeting_details"]["password"],
# location_type="Remote"
# )
# ScheduledInterview.objects.create(
# application=candidate,
# job=job,
# interview_location=zoom_meeting,
# schedule=schedule,
# interview_date=slot_date,
# interview_time=slot_time
# )
# Log success or use Django-Q result system for monitoring # Log success or use Django-Q result system for monitoring
logger.info(f"Successfully scheduled interview for {Application.name}") logger.info(f"Successfully scheduled interview for {Application.name}")
@ -745,7 +770,7 @@ def handle_zoom_webhook_event(payload):
try: try:
# Use filter().first() to avoid exceptions if the meeting doesn't exist yet, # Use filter().first() to avoid exceptions if the meeting doesn't exist yet,
# and to simplify the logic flow. # and to simplify the logic flow.
meeting_instance = ZoomMeetingDetails.objects.filter(meeting_id=meeting_id_zoom).first() meeting_instance = ''#TODO:update #ZoomMeetingDetails.objects.filter(meeting_id=meeting_id_zoom).first()
print(meeting_instance) print(meeting_instance)
# --- 1. Creation and Update Events --- # --- 1. Creation and Update Events ---
if event_type == 'meeting.updated': if event_type == 'meeting.updated':
@ -1044,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

@ -11,12 +11,12 @@ User = get_user_model()
from .models import ( from .models import (
JobPosting, Application, Person, ZoomMeeting, FormTemplate, FormStage, FormField, JobPosting, Application, Person, ZoomMeeting, FormTemplate, FormStage, FormField,
FormSubmission, FieldResponse, InterviewSchedule, ScheduledInterview, FormSubmission, FieldResponse, BulkInterviewTemplate, ScheduledInterview,
TrainingMaterial, Source, HiringAgency, MeetingComment TrainingMaterial, Source, HiringAgency, MeetingComment
) )
from .forms import ( from .forms import (
JobPostingForm, CandidateForm, ZoomMeetingForm, MeetingCommentForm, JobPostingForm, CandidateForm, ZoomMeetingForm, MeetingCommentForm,
CandidateStageForm, InterviewScheduleForm, CandidateSignupForm CandidateStageForm, BulkInterviewTemplateForm, CandidateSignupForm
) )
from .views import ( from .views import (
ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view, ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view,
@ -304,7 +304,7 @@ class FormTests(BaseTestCase):
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
def test_interview_schedule_form(self): def test_interview_schedule_form(self):
"""Test InterviewScheduleForm""" """Test BulkInterviewTemplateForm"""
# Update candidate to Interview stage first # Update candidate to Interview stage first
self.candidate.stage = 'Interview' self.candidate.stage = 'Interview'
self.candidate.save() self.candidate.save()
@ -315,7 +315,7 @@ class FormTests(BaseTestCase):
'end_date': (timezone.now() + timedelta(days=7)).date(), 'end_date': (timezone.now() + timedelta(days=7)).date(),
'working_days': [0, 1, 2, 3, 4], # Monday to Friday 'working_days': [0, 1, 2, 3, 4], # Monday to Friday
} }
form = InterviewScheduleForm(slug=self.job.slug, data=form_data) form = BulkInterviewTemplateForm(slug=self.job.slug, data=form_data)
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
def test_candidate_signup_form_valid(self): def test_candidate_signup_form_valid(self):

View File

@ -24,13 +24,13 @@ from PIL import Image
from .models import ( from .models import (
JobPosting, Application, Person, ZoomMeeting, FormTemplate, FormStage, FormField, JobPosting, Application, Person, ZoomMeeting, FormTemplate, FormStage, FormField,
FormSubmission, FieldResponse, InterviewSchedule, ScheduledInterview, FormSubmission, FieldResponse, BulkInterviewTemplate, ScheduledInterview,
TrainingMaterial, Source, HiringAgency, MeetingComment, JobPostingImage, TrainingMaterial, Source, HiringAgency, MeetingComment, JobPostingImage,
BreakTime BreakTime
) )
from .forms import ( from .forms import (
JobPostingForm, ApplicationForm, ZoomMeetingForm, MeetingCommentForm, JobPostingForm, ApplicationForm, ZoomMeetingForm, MeetingCommentForm,
ApplicationStageForm, InterviewScheduleForm, BreakTimeFormSet ApplicationStageForm, BulkInterviewTemplateForm, BreakTimeFormSet
) )
from .views import ( from .views import (
ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view, ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view,
@ -228,7 +228,7 @@ class AdvancedModelTests(TestCase):
'break_end_time': '13:00' 'break_end_time': '13:00'
} }
form = InterviewScheduleForm(slug=self.job.slug, data=schedule_data) form = BulkInterviewTemplateForm(slug=self.job.slug, data=schedule_data)
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
def test_field_response_data_types(self): def test_field_response_data_types(self):
@ -625,7 +625,7 @@ class AdvancedFormTests(TestCase):
def test_form_dependency_validation(self): def test_form_dependency_validation(self):
"""Test validation for dependent form fields""" """Test validation for dependent form fields"""
# Test InterviewScheduleForm with dependent fields # Test BulkInterviewTemplateForm with dependent fields
schedule_data = { schedule_data = {
'candidates': [], # Empty for now 'candidates': [], # Empty for now
'start_date': '2025-01-15', 'start_date': '2025-01-15',
@ -637,7 +637,7 @@ class AdvancedFormTests(TestCase):
'buffer_time': '15' 'buffer_time': '15'
} }
form = InterviewScheduleForm(slug=self.job.slug, data=schedule_data) form = BulkInterviewTemplateForm(slug=self.job.slug, data=schedule_data)
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
self.assertIn('end_date', form.errors) self.assertIn('end_date', form.errors)
@ -667,7 +667,7 @@ class AdvancedFormTests(TestCase):
def test_dynamic_form_fields(self): def test_dynamic_form_fields(self):
"""Test forms with dynamically populated fields""" """Test forms with dynamically populated fields"""
# Test InterviewScheduleForm with dynamic candidate queryset # Test BulkInterviewTemplateForm with dynamic candidate queryset
# Create applications in Interview stage # Create applications in Interview stage
applications = [] applications = []
for i in range(3): for i in range(3):
@ -684,7 +684,7 @@ class AdvancedFormTests(TestCase):
applications.append(application) applications.append(application)
# Form should only show Interview stage applications # Form should only show Interview stage applications
form = InterviewScheduleForm(slug=self.job.slug) form = BulkInterviewTemplateForm(slug=self.job.slug)
self.assertEqual(form.fields['candidates'].queryset.count(), 3) self.assertEqual(form.fields['candidates'].queryset.count(), 3)
for application in applications: for application in applications:

View File

@ -207,21 +207,21 @@ urlpatterns = [
), ),
path( # path(
"jobs/<slug:slug>/<int:application_id>/reschedule_meeting_for_application/<int:meeting_id>/", # "jobs/<slug:slug>/<int:application_id>/reschedule_meeting_for_application/<int:meeting_id>/",
views.reschedule_meeting_for_application, # views.reschedule_meeting_for_application,
name="reschedule_meeting_for_application", # name="reschedule_meeting_for_application",
), # ),
path( path(
"jobs/<slug:slug>/update_application_exam_status/", "jobs/<slug:slug>/update_application_exam_status/",
views.update_application_exam_status, views.update_application_exam_status,
name="update_application_exam_status", name="update_application_exam_status",
), ),
path( # path(
"jobs/<slug:slug>/bulk_update_application_exam_status/", # "jobs/<slug:slug>/bulk_update_application_exam_status/",
views.bulk_update_application_exam_status, # views.bulk_update_application_exam_status,
name="bulk_update_application_exam_status", # name="bulk_update_application_exam_status",
), # ),
path( path(
"htmx/<int:pk>/application_criteria_view/", "htmx/<int:pk>/application_criteria_view/",
views.application_criteria_view_htmx, views.application_criteria_view_htmx,
@ -266,16 +266,16 @@ urlpatterns = [
# path('api/templates/save/', views.save_form_template, name='save_form_template'), # path('api/templates/save/', views.save_form_template, name='save_form_template'),
# path('api/templates/<slug:template_slug>/', views.load_form_template, name='load_form_template'), # path('api/templates/<slug:template_slug>/', views.load_form_template, name='load_form_template'),
# path('api/templates/<slug:template_slug>/delete/', views.delete_form_template, name='delete_form_template'), # path('api/templates/<slug:template_slug>/delete/', views.delete_form_template, name='delete_form_template'),
path( # path(
"jobs/<slug:slug>/calendar/", # "jobs/<slug:slug>/calendar/",
views.interview_calendar_view, # views.interview_calendar_view,
name="interview_calendar", # name="interview_calendar",
), # ),
path( # path(
"jobs/<slug:slug>/calendar/interview/<int:interview_id>/", # "jobs/<slug:slug>/calendar/interview/<int:interview_id>/",
views.interview_detail_view, # views.interview_detail_view,
name="interview_detail", # name="interview_detail",
), # ),
# users urls # users urls
path("user/<int:pk>", views.user_detail, name="user_detail"), path("user/<int:pk>", views.user_detail, name="user_detail"),
@ -333,26 +333,26 @@ urlpatterns = [
name="copy_to_clipboard", name="copy_to_clipboard",
), ),
# Meeting Comments URLs # Meeting Comments URLs
path( # path(
"meetings/<slug:slug>/comments/add/", # "meetings/<slug:slug>/comments/add/",
views.add_meeting_comment, # views.add_meeting_comment,
name="add_meeting_comment", # name="add_meeting_comment",
), # ),
path( # path(
"meetings/<slug:slug>/comments/<int:comment_id>/edit/", # "meetings/<slug:slug>/comments/<int:comment_id>/edit/",
views.edit_meeting_comment, # views.edit_meeting_comment,
name="edit_meeting_comment", # name="edit_meeting_comment",
), # ),
path( # path(
"meetings/<slug:slug>/comments/<int:comment_id>/delete/", # "meetings/<slug:slug>/comments/<int:comment_id>/delete/",
views.delete_meeting_comment, # views.delete_meeting_comment,
name="delete_meeting_comment", # name="delete_meeting_comment",
), # ),
path( # path(
"meetings/<slug:slug>/set_meeting_application/", # "meetings/<slug:slug>/set_meeting_application/",
views.set_meeting_application, # views.set_meeting_application,
name="set_meeting_application", # name="set_meeting_application",
), # ),
# Hiring Agency URLs # Hiring Agency URLs
path("agencies/", views.agency_list, name="agency_list"), path("agencies/", views.agency_list, name="agency_list"),
path("agencies/create/", views.agency_create, name="agency_create"), path("agencies/create/", views.agency_create, name="agency_create"),
@ -440,7 +440,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",
), ),
@ -510,31 +510,31 @@ urlpatterns = [
# path('notifications/mark-all-read/', views.notification_mark_all_read, name='notification_mark_all_read'), # path('notifications/mark-all-read/', views.notification_mark_all_read, name='notification_mark_all_read'),
# path('api/notification-count/', views.api_notification_count, name='api_notification_count'), # path('api/notification-count/', views.api_notification_count, name='api_notification_count'),
# participants urls # participants urls
path( # path(
"participants/", # "participants/",
views_frontend.ParticipantsListView.as_view(), # views_frontend.ParticipantsListView.as_view(),
name="participants_list", # name="participants_list",
), # ),
path( # path(
"participants/create/", # "participants/create/",
views_frontend.ParticipantsCreateView.as_view(), # views_frontend.ParticipantsCreateView.as_view(),
name="participants_create", # name="participants_create",
), # ),
path( # path(
"participants/<slug:slug>/", # "participants/<slug:slug>/",
views_frontend.ParticipantsDetailView.as_view(), # views_frontend.ParticipantsDetailView.as_view(),
name="participants_detail", # name="participants_detail",
), # ),
path( # path(
"participants/<slug:slug>/update/", # "participants/<slug:slug>/update/",
views_frontend.ParticipantsUpdateView.as_view(), # views_frontend.ParticipantsUpdateView.as_view(),
name="participants_update", # name="participants_update",
), # ),
path( # path(
"participants/<slug:slug>/delete/", # "participants/<slug:slug>/delete/",
views_frontend.ParticipantsDeleteView.as_view(), # views_frontend.ParticipantsDeleteView.as_view(),
name="participants_delete", # name="participants_delete",
), # ),
# Email composition URLs # Email composition URLs
path( path(
"jobs/<slug:job_slug>/applications/compose-email/", "jobs/<slug:job_slug>/applications/compose-email/",
@ -563,13 +563,23 @@ urlpatterns = [
path("application/documents/<int:document_id>/download/", views.document_download, name="application_document_download"), path("application/documents/<int:document_id>/download/", views.document_download, name="application_document_download"),
path('jobs/<slug:job_slug>/applications/compose_email/', views.compose_application_email, name='compose_application_email'), path('jobs/<slug:job_slug>/applications/compose_email/', views.compose_application_email, name='compose_application_email'),
path('interview/partcipants/<slug:slug>/',views.create_interview_participants,name='create_interview_participants'), # path('interview/partcipants/<slug:slug>/',views.create_interview_participants,name='create_interview_participants'),
path('interview/email/<slug:slug>/',views.send_interview_email,name='send_interview_email'), # path('interview/email/<slug:slug>/',views.send_interview_email,name='send_interview_email'),
# Candidate Signup # Candidate Signup
path('application/signup/<slug:template_slug>/', views.application_signup, name='application_signup'), path('application/signup/<slug:template_slug>/', views.application_signup, name='application_signup'),
# Password Reset # Password Reset
path('user/<int:pk>/password-reset/', views.portal_password_reset, name='portal_password_reset'), path('user/<int:pk>/password-reset/', views.portal_password_reset, name='portal_password_reset'),
# Interview URLs
path('interviews/', views.interview_list, name='interview_list'),
path('interviews/<slug:slug>/', views.interview_detail, name='interview_detail'),
# Interview Creation URLs
path('interviews/create/<slug:candidate_slug>/', views.interview_create_type_selection, name='interview_create_type_selection'),
path('interviews/create/<slug:candidate_slug>/remote/', views.interview_create_remote, name='interview_create_remote'),
path('interviews/create/<slug:candidate_slug>/onsite/', views.interview_create_onsite, name='interview_create_onsite'),
path('interviews/<slug:job_slug>/get_interview_list', views.get_interview_list, name='get_interview_list'),
# # --- SCHEDULED INTERVIEW URLS (New Centralized Management) --- # # --- SCHEDULED INTERVIEW URLS (New Centralized Management) ---
# path('interview/list/', views.interview_list, name='interview_list'), # path('interview/list/', views.interview_list, name='interview_list'),
# path('interviews/<slug:slug>/', views.ScheduledInterviewDetailView.as_view(), name='scheduled_interview_detail'), # path('interviews/<slug:slug>/', views.ScheduledInterviewDetailView.as_view(), name='scheduled_interview_detail'),
@ -577,64 +587,64 @@ urlpatterns = [
# path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'), # path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'),
#interview and meeting related urls #interview and meeting related urls
path( # path(
"jobs/<slug:slug>/schedule-interviews/", # "jobs/<slug:slug>/schedule-interviews/",
views.schedule_interviews_view, # views.schedule_interviews_view,
name="schedule_interviews", # name="schedule_interviews",
), # ),
path( # path(
"jobs/<slug:slug>/confirm-schedule-interviews/", # "jobs/<slug:slug>/confirm-schedule-interviews/",
views.confirm_schedule_interviews_view, # views.confirm_schedule_interviews_view,
name="confirm_schedule_interviews_view", # name="confirm_schedule_interviews_view",
), # ),
path( # path(
"meetings/create-meeting/", # "meetings/create-meeting/",
views.ZoomMeetingCreateView.as_view(), # views.ZoomMeetingCreateView.as_view(),
name="create_meeting", # name="create_meeting",
), # ),
# path( # path(
# "meetings/meeting-details/<slug:slug>/", # "meetings/meeting-details/<slug:slug>/",
# views.ZoomMeetingDetailsView.as_view(), # views.ZoomMeetingDetailsView.as_view(),
# name="meeting_details", # name="meeting_details",
# ), # ),
path( # path(
"meetings/update-meeting/<slug:slug>/", # "meetings/update-meeting/<slug:slug>/",
views.ZoomMeetingUpdateView.as_view(), # views.ZoomMeetingUpdateView.as_view(),
name="update_meeting", # name="update_meeting",
), # ),
path( # path(
"meetings/delete-meeting/<slug:slug>/", # "meetings/delete-meeting/<slug:slug>/",
views.ZoomMeetingDeleteView, # views.ZoomMeetingDeleteView,
name="delete_meeting", # name="delete_meeting",
), # ),
# Candidate Meeting Scheduling/Rescheduling URLs # Candidate Meeting Scheduling/Rescheduling URLs
path( # path(
"jobs/<slug:job_slug>/applications/<int:application_pk>/schedule-meeting/", # "jobs/<slug:job_slug>/applications/<int:application_pk>/schedule-meeting/",
views.schedule_application_meeting, # views.schedule_application_meeting,
name="schedule_application_meeting", # name="schedule_application_meeting",
), # ),
path( # path(
"api/jobs/<slug:job_slug>/applications/<int:application_pk>/schedule-meeting/", # "api/jobs/<slug:job_slug>/applications/<int:application_pk>/schedule-meeting/",
views.api_schedule_application_meeting, # views.api_schedule_application_meeting,
name="api_schedule_application_meeting", # name="api_schedule_application_meeting",
), # ),
path( # path(
"jobs/<slug:job_slug>/applications/<int:application_pk>/reschedule-meeting/<int:interview_pk>/", # "jobs/<slug:job_slug>/applications/<int:application_pk>/reschedule-meeting/<int:interview_pk>/",
views.reschedule_application_meeting, # views.reschedule_application_meeting,
name="reschedule_application_meeting", # name="reschedule_application_meeting",
), # ),
path( # path(
"api/jobs/<slug:job_slug>/applications/<int:application_pk>/reschedule-meeting/<int:interview_pk>/", # "api/jobs/<slug:job_slug>/applications/<int:application_pk>/reschedule-meeting/<int:interview_pk>/",
views.api_reschedule_application_meeting, # views.api_reschedule_application_meeting,
name="api_reschedule_application_meeting", # name="api_reschedule_application_meeting",
), # ),
# New URL for simple page-based meeting scheduling # New URL for simple page-based meeting scheduling
path( # path(
"jobs/<slug:slug>/applications/<int:application_pk>/schedule-meeting-page/", # "jobs/<slug:slug>/applications/<int:application_pk>/schedule-meeting-page/",
views.schedule_meeting_for_application, # views.schedule_meeting_for_application,
name="schedule_meeting_for_application", # name="schedule_meeting_for_application",
), # ),
# path( # path(
# "jobs/<slug:slug>/applications/<int:application_pk>/delete_meeting_for_application/<int:meeting_id>/", # "jobs/<slug:slug>/applications/<int:application_pk>/delete_meeting_for_application/<int:meeting_id>/",
# views.delete_meeting_for_candidate, # views.delete_meeting_for_candidate,
@ -642,35 +652,35 @@ urlpatterns = [
# ), # ),
path("interviews/meetings/", views.MeetingListView.as_view(), name="list_meetings"), # path("interviews/meetings/", views.MeetingListView.as_view(), name="list_meetings"),
# 1. Onsite Reschedule URL # 1. Onsite Reschedule URL
path( # path(
'<slug:slug>/application/<int:application_id>/onsite/reschedule/<int:meeting_id>/', # '<slug:slug>/application/<int:application_id>/onsite/reschedule/<int:meeting_id>/',
views.reschedule_onsite_meeting, # views.reschedule_onsite_meeting,
name='reschedule_onsite_meeting' # name='reschedule_onsite_meeting'
), # ),
# 2. Onsite Delete URL # 2. Onsite Delete URL
path( # path(
'job/<slug:slug>/applications/<int:application_pk>/delete-onsite-meeting/<int:meeting_id>/', # 'job/<slug:slug>/applications/<int:application_pk>/delete-onsite-meeting/<int:meeting_id>/',
views.delete_onsite_meeting_for_application, # views.delete_onsite_meeting_for_application,
name='delete_onsite_meeting_for_application' # name='delete_onsite_meeting_for_application'
), # ),
path( # path(
'job/<slug:slug>/application/<int:application_pk>/schedule/onsite/', # 'job/<slug:slug>/application/<int:application_pk>/schedule/onsite/',
views.schedule_onsite_meeting_for_application, # views.schedule_onsite_meeting_for_application,
name='schedule_onsite_meeting_for_application' # This is the name used in the button # name='schedule_onsite_meeting_for_application' # This is the name used in the button
), # ),
# Detail View (assuming slug is on ScheduledInterview) # Detail View (assuming slug is on ScheduledInterview)
path("interviews/meetings/<slug:slug>/", views.meeting_details, name="meeting_details"), # path("interviews/meetings/<slug:slug>/", views.meeting_details, name="meeting_details"),
# Email invitation URLs # Email invitation URLs
path("interviews/meetings/<slug:slug>/send-application-invitation/", views.send_application_invitation, name="send_application_invitation"), # path("interviews/meetings/<slug:slug>/send-application-invitation/", views.send_application_invitation, name="send_application_invitation"),
path("interviews/meetings/<slug:slug>/send-participants-invitation/", views.send_participants_invitation, name="send_participants_invitation"), # path("interviews/meetings/<slug:slug>/send-participants-invitation/", views.send_participants_invitation, name="send_participants_invitation"),
] ]

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@ from django.http import JsonResponse, HttpResponse
from django.db.models.fields.json import KeyTextTransform,KeyTransform from django.db.models.fields.json import KeyTextTransform,KeyTransform
from recruitment.utils import json_to_markdown_table from recruitment.utils import json_to_markdown_table
from django.db.models import Count, Avg, F, FloatField from django.db.models import Count, Avg, F, FloatField
from django.db.models.functions import Cast
from django.db.models.functions import Coalesce, Cast, Replace, NullIf from django.db.models.functions import Coalesce, Cast, Replace, NullIf
from . import models from . import models
from django.utils.translation import get_language from django.utils.translation import get_language
@ -280,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:
@ -399,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(
@ -483,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))
# ) # )
@ -638,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
@ -1065,47 +1064,47 @@ def sync_history(request, job_slug=None):
#participants views #participants views
class ParticipantsListView(LoginRequiredMixin, StaffRequiredMixin, ListView): # class ParticipantsListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.Participants # model = models.Participants
template_name = 'participants/participants_list.html' # template_name = 'participants/participants_list.html'
context_object_name = 'participants' # context_object_name = 'participants'
paginate_by = 10 # paginate_by = 10
def get_queryset(self): # def get_queryset(self):
queryset = super().get_queryset() # queryset = super().get_queryset()
# Handle search # # Handle search
search_query = self.request.GET.get('search', '') # search_query = self.request.GET.get('search', '')
if search_query: # if search_query:
queryset = queryset.filter( # queryset = queryset.filter(
Q(name__icontains=search_query) | # Q(name__icontains=search_query) |
Q(email__icontains=search_query) | # Q(email__icontains=search_query) |
Q(phone__icontains=search_query) | # Q(phone__icontains=search_query) |
Q(designation__icontains=search_query) # Q(designation__icontains=search_query)
) # )
# Filter for non-staff users # # Filter for non-staff users
if not self.request.user.is_staff: # if not self.request.user.is_staff:
return models.Participants.objects.none() # Restrict for non-staff # return models.Participants.objects.none() # Restrict for non-staff
return queryset.order_by('-created_at') # return queryset.order_by('-created_at')
def get_context_data(self, **kwargs): # def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) # context = super().get_context_data(**kwargs)
context['search_query'] = self.request.GET.get('search', '') # context['search_query'] = self.request.GET.get('search', '')
return context # return context
class ParticipantsDetailView(LoginRequiredMixin, StaffRequiredMixin, DetailView): # class ParticipantsDetailView(LoginRequiredMixin, StaffRequiredMixin, DetailView):
model = models.Participants # model = models.Participants
template_name = 'participants/participants_detail.html' # template_name = 'participants/participants_detail.html'
context_object_name = 'participant' # context_object_name = 'participant'
slug_url_kwarg = 'slug' # slug_url_kwarg = 'slug'
class ParticipantsCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView): # class ParticipantsCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView):
model = models.Participants # model = models.Participants
form_class = forms.ParticipantsForm # form_class = forms.ParticipantsForm
template_name = 'participants/participants_create.html' # template_name = 'participants/participants_create.html'
success_url = reverse_lazy('job_list') # success_url = reverse_lazy('job_list')
success_message = 'Participant created successfully.' # success_message = 'Participant created successfully.'
# def get_initial(self): # def get_initial(self):
# initial = super().get_initial() # initial = super().get_initial()
@ -1116,17 +1115,17 @@ class ParticipantsCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMess
class ParticipantsUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView): # class ParticipantsUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView):
model = models.Participants # model = models.Participants
form_class = forms.ParticipantsForm # form_class = forms.ParticipantsForm
template_name = 'participants/participants_create.html' # template_name = 'participants/participants_create.html'
success_url = reverse_lazy('job_list') # success_url = reverse_lazy('job_list')
success_message = 'Participant updated successfully.' # success_message = 'Participant updated successfully.'
slug_url_kwarg = 'slug' # slug_url_kwarg = 'slug'
class ParticipantsDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView): # class ParticipantsDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView):
model = models.Participants # model = models.Participants
success_url = reverse_lazy('participants_list') # Redirect to the participants list after success # success_url = reverse_lazy('participants_list') # Redirect to the participants list after success
success_message = 'Participant deleted successfully.' # success_message = 'Participant deleted successfully.'
slug_url_kwarg = 'slug' # slug_url_kwarg = 'slug'

View File

@ -1,146 +1,191 @@
annotated-types amqp==5.3.1
appdirs annotated-types==0.7.0
asgiref anthropic==0.63.0
asteval anyio==4.11.0
astunparse appdirs==1.4.4
attrs arrow==1.3.0
blinker asgiref==3.9.2
blis asteval==1.0.6
boto3 astunparse==1.6.3
botocore attrs==25.3.0
bw-migrations billiard==4.2.2
bw2parameters bleach==6.2.0
bw_processing blessed==1.22.0
cached-property blinker==1.9.0
catalogue blis==1.3.0
certifi boto3==1.40.37
channels botocore==1.40.37
chardet bw-migrations==0.2
charset-normalizer bw2data==4.5
click bw2parameters==1.1.0
cloudpathlib bw_processing==1.0
confection cached-property==2.0.1
constructive_geometries catalogue==2.0.10
country_converter celery==5.5.3
cymem certifi==2025.8.3
dataflows-tabulator channels==4.3.1
datapackage chardet==5.2.0
deepdiff charset-normalizer==3.4.3
Deprecated click==8.3.0
Django click-didyoumean==0.3.1
django-allauth click-plugins==1.1.1.2
django-cors-headers click-repl==0.3.0
django-filter cloudpathlib==0.22.0
django-unfold confection==0.1.5
djangorestframework constructive_geometries==1.0
docopt country_converter==1.3.1
crispy-bootstrap5==2025.6
cymem==2.0.11
dataflows-tabulator==1.54.3
datapackage==1.15.4
datastar-py==0.6.5
deepdiff==7.0.1
Deprecated==1.2.18
distro==1.9.0
Django==5.2.6
django-allauth==65.11.2
django-ckeditor-5==0.2.18
django-cors-headers==4.9.0
django-countries==7.6.1
django-crispy-forms==2.4
django-easy-audit==1.3.7
django-extensions==4.1
django-filter==25.1
django-picklefield==3.3
django-q2==1.8.0
django-summernote==0.8.20.0
django-template-partials==25.2
django-unfold==0.66.0
django-widget-tweaks==1.5.0
django_celery_results==2.6.0
djangorestframework==3.16.1
docopt==0.6.2
en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85 en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
et_xmlfile et_xmlfile==2.0.0
Faker Faker==37.8.0
flexcache flexcache==0.3
flexparser flexparser==0.4
fsspec fsspec==2025.9.0
idna gpt-po-translator==1.3.2
ijson greenlet==3.2.4
isodate h11==0.16.0
Jinja2 httpcore==1.0.9
jmespath httpx==0.28.1
jsonlines idna==3.10
jsonpointer ijson==3.4.0
jsonschema iniconfig==2.1.0
jsonschema-specifications isodate==0.7.2
langcodes isort==5.13.2
language_data Jinja2==3.1.6
linear-tsv jiter==0.11.1
llvmlite jmespath==1.0.1
loguru jsonlines==4.0.0
lxml jsonpointer==3.0.0
marisa-trie jsonschema==4.25.1
markdown-it-py jsonschema-specifications==2025.9.1
MarkupSafe kombu==5.5.4
matrix_utils langcodes==3.5.0
mdurl language_data==1.3.0
morefs linear-tsv==1.1.0
mrio-common-metadata llvmlite==0.45.0
murmurhash loguru==0.7.3
numba lxml==6.0.2
numpy marisa-trie==1.3.1
openpyxl markdown-it-py==4.0.0
ordered-set MarkupSafe==3.0.2
packaging matrix_utils==0.6.2
pandas mdurl==0.1.2
peewee morefs==0.2.2
Pint mrio-common-metadata==0.2.1
platformdirs murmurhash==1.0.13
preshed numba==0.62.0
prettytable numpy==2.3.3
pydantic openai==1.99.9
pydantic-settings openpyxl==3.1.5
pydantic_core ordered-set==4.1.0
pyecospold packaging==25.0
Pygments pandas==2.3.2
PyJWT peewee==3.18.2
PyMuPDF pillow==11.3.0
pyparsing Pint==0.25
PyPrind platformdirs==4.4.0
python-dateutil pluggy==1.6.0
python-dotenv polib==1.2.0
python-json-logger preshed==3.0.10
pytz prettytable==3.16.0
pyxlsb prompt_toolkit==3.0.52
PyYAML psycopg2-binary==2.9.11
randonneur pycountry==24.6.1
randonneur_data pydantic==2.11.9
RapidFuzz pydantic-settings==2.10.1
rdflib pydantic_core==2.33.2
referencing pyecospold==4.0.0
requests Pygments==2.19.2
rfc3986 PyJWT==2.10.1
rich PyMuPDF==1.26.4
rpds-py pyparsing==3.2.5
s3transfer PyPDF2==3.0.1
scipy PyPrind==2.11.3
shellingham pytest==8.3.4
six pytest-django==4.11.1
smart-open python-dateutil==2.9.0.post0
snowflake-id python-docx==1.2.0
spacy python-dotenv==1.0.1
spacy-legacy python-json-logger==3.3.0
spacy-loggers pytz==2025.2
SPARQLWrapper pyxlsb==1.0.10
sparse PyYAML==6.0.2
SQLAlchemy randonneur==0.6.2
sqlparse randonneur_data==0.6
srsly RapidFuzz==3.14.1
stats_arrays rdflib==7.2.1
structlog redis==3.5.3
tableschema referencing==0.36.2
thinc requests==2.32.3
toolz responses==0.25.8
tqdm rfc3986==2.0.0
typer rich==14.1.0
typing-inspection rpds-py==0.27.1
typing_extensions s3transfer==0.14.0
tzdata scipy==1.16.2
unicodecsv setuptools==80.9.0
urllib3 setuptools-scm==8.1.0
voluptuous shellingham==1.5.4
wasabi six==1.17.0
wcwidth smart_open==7.3.1
weasel sniffio==1.3.1
wrapt snowflake-id==1.0.2
wurst spacy==3.8.7
xlrd spacy-legacy==3.0.12
XlsxWriter spacy-loggers==1.0.5
celery[redis] SPARQLWrapper==2.0.0
redis sparse==0.17.0
sentence-transformers SQLAlchemy==2.0.43
torch sqlparse==0.5.3
pdfplumber srsly==2.5.1
python-docx stats_arrays==0.7
PyMuPDF structlog==25.4.0
pytesseract tableschema==1.21.0
Pillow tenacity==9.0.0
python-dotenv thinc==8.3.6
django-countries tomli==2.2.1
django-q2 toolz==1.0.0
tqdm==4.67.1
typer==0.19.2
types-python-dateutil==2.9.0.20251008
typing-inspection==0.4.1
typing_extensions==4.15.0
tzdata==2025.2
unicodecsv==0.14.1
urllib3==2.5.0
vine==5.1.0
voluptuous==0.15.2
wasabi==1.1.3
wcwidth==0.2.14
weasel==0.4.1
webencodings==0.5.1
wheel==0.45.1
wrapt==1.17.3
wurst==0.4
xlrd==2.0.2
xlsxwriter==3.2.9

View File

@ -133,8 +133,8 @@
<h1 class="text-4xl font-weight-bold mb-4" style="font-size: 1.5rem;"> <h1 class="text-4xl font-weight-bold mb-4" style="font-size: 1.5rem;">
<span class="text-white"> <span class="text-white">
<div class="hospital-text text-center text-md-start me-3"> <div class="hospital-text text-center text-md-start me-3">
<div class="ar small">جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية</div> <div class="ar small">جامعة الأميرة نورة بنت عبدالرحمن</div>
<div class="ar small">ومستشفى الملك عبدالله بن عبدالعزيز التخصصي</div> <div class="ar small">ومستشفى الملك عبدالله بن عبدالعزيز الجامعي</div>
<div class="en small">Princess Nourah bint Abdulrahman University</div> <div class="en small">Princess Nourah bint Abdulrahman University</div>
<div class="en small">King Abdullah bin Abdulaziz University Hospital</div> <div class="en small">King Abdullah bin Abdulaziz University Hospital</div>
</div> </div>

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

@ -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 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>
<li class="nav-item"> <li class="nav-item mx-2 mb-1">
<a class="nav-link text-secondary" href="{% url 'kaauh_career' %}">{% translate "Careers" %}</a> <a class="nav-link text-secondary text-primary-theme" href="{% url 'kaauh_career' %}">{% trans "Careers" %}</a>
</li> </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

@ -35,13 +35,10 @@
</div> </div>
<div class="logo-container d-flex gap-2 align-items-center"> <div class="logo-container d-flex gap-2 align-items-center">
<img src="{% static 'image/vision.svg' %}" alt="{% trans 'Saudi Vision 2030' %}" loading="lazy" style="height: 35px; object-fit: contain;"> <img src="{% static 'image/vision.svg' %}" alt="{% trans 'Saudi Vision 2030' %}" loading="lazy" style="height: 35px; object-fit: contain;">
<div class="kaauh-logo-container d-flex flex-column flex-md-row align-items-center gap-2 me-0">
<div class="kaauh-logo-container d-flex flex-column flex-md-row align-items-center gap-2 me-0">
<div class="hospital-text text-center text-md-start me-0"> <div class="hospital-text text-center text-md-start me-0">
<div class="ar text-xs">جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية</div> <div class="en text-xs">{% trans "Princess Nourah bint Abdulrahman University"%}</div>
<div class="ar text-xs">ومستشفى الملك عبدالله بن عبدالرحمن التخصصي</div> <div class="en text-xs">{% trans "King Abdullah bin Abdulaziz University Hospital"%}</div>
<div class="en text-xs">Princess Nourah bint Abdulrahman University</div>
<div class="en text-xs">King Abdullah bin Abdulaziz University Hospital</div>
</div> </div>
</div> </div>
<img src="{% static 'image/kaauh.png' %}" alt="KAAUH Logo" style="max-height: 40px; max-width: 40px;"> <img src="{% static 'image/kaauh.png' %}" alt="KAAUH Logo" style="max-height: 40px; max-width: 40px;">
@ -277,7 +274,7 @@
</a> </a>
</li> </li>
<li class="nav-item me-lg-4"> <li class="nav-item me-lg-4">
<a class="nav-link {% if request.resolver_match.url_name == 'list_meetings' %}active{% endif %}" href="{% url 'list_meetings' %}"> <a class="nav-link {% if request.resolver_match.url_name == 'list_meetings' %}active{% endif %}" href="{% url 'interview_list' %}">
<span class="d-flex align-items-center gap-2"> <span class="d-flex align-items-center gap-2">
<i class="fas fa-calendar-check me-2"></i> <i class="fas fa-calendar-check me-2"></i>
{% trans "Meetings" %} {% trans "Meetings" %}

View File

@ -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"
@ -388,7 +388,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 +428,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;

View File

@ -0,0 +1,238 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Create Onsite Interview" %}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-building me-2"></i>
{% blocktrans %}Create Onsite Interview for {{ application.name }}{% endblocktrans %}
</h4>
<a href="{% url 'interview_create_type_selection' application.slug %}"
class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>
{% trans "Back to application List" %}
</a>
</div>
<div class="card-body">
<p class="text-muted mb-3">
{% blocktrans %}Schedule an onsite interview for <strong>{{ application.name }}</strong>
for the position of <strong>{{ job.title }}</strong>.{% endblocktrans %}
</p>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" action="{% url 'interview_create_onsite' application_slug=application.slug %}">
{% csrf_token %}
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label for="{{ form.interview_date.id_for_label }}" class="form-label">
<i class="fas fa-calendar me-1"></i>
{% trans "Topic" %}
</label>
{{ form.topic }}
{% if form.topic.errors %}
<div class="text-danger small">
{{ form.topic.errors }}
</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.interview_date.id_for_label }}" class="form-label">
<i class="fas fa-calendar me-1"></i>
{% trans "Interview Date" %}
</label>
{{ form.interview_date }}
{% if form.interview_date.errors %}
<div class="text-danger small">
{{ form.interview_date.errors }}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.interview_time.id_for_label }}" class="form-label">
<i class="fas fa-clock me-1"></i>
{% trans "Interview Time" %}
</label>
{{ form.interview_time }}
{% if form.interview_time.errors %}
<div class="text-danger small">
{{ form.interview_time.errors }}
</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.duration.id_for_label }}" class="form-label">
<i class="fas fa-hourglass-half me-1"></i>
{% trans "Duration (minutes)" %}
</label>
{{ form.duration }}
{% if form.duration.errors %}
<div class="text-danger small">
{{ form.duration.errors }}
</div>
{% endif %}
</div>
</div>
{% comment %} <div class="col-md-6">
<div class="mb-3">
<label for="{{ form.interviewer.id_for_label }}" class="form-label">
<i class="fas fa-user me-1"></i>
Interviewer
</label>
{{ form.interviewer }}
{% if form.interviewer.errors %}
<div class="text-danger small">
{{ form.interviewer.errors }}
</div>
{% endif %}
</div>
</div> {% endcomment %}
</div>
{% comment %} <div class="mb-3">
<label for="{{ form.topic.id_for_label }}" class="form-label">
<i class="fas fa-comment me-1"></i>
Meeting Topic
</label>
{{ form.topic }}
{% if form.topic.errors %}
<div class="text-danger small">
{{ form.topic.errors }}
</div>
{% endif %}
</div> {% endcomment %}
<div class="mb-3">
<label for="{{ form.physical_address.id_for_label }}" class="form-label">
<i class="fas fa-map-marker-alt me-1"></i>
{% trans "Physical Address" %}
</label>
{{ form.physical_address }}
{% if form.physical_address.errors %}
<div class="text-danger small">
{{ form.physical_address.errors }}
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.room_number.id_for_label }}" class="form-label">
<i class="fas fa-door-open me-1"></i>
{% trans "Room Number" %}
</label>
{{ form.room_number }}
{% if form.room_number.errors %}
<div class="text-danger small">
{{ form.room_number.errors }}
</div>
{% endif %}
</div>
</div>
{% comment %} <div class="col-md-6">
<div class="mb-3">
<label for="{{ form.floor_number.id_for_label }}" class="form-label">
<i class="fas fa-layer-group me-1"></i>
Floor Number
</label>
{{ form.floor_number }}
{% if form.floor_number.errors %}
<div class="text-danger small">
{{ form.floor_number.errors }}
</div>
{% endif %}
</div>
</div> {% endcomment %}
{% comment %} </div> {% endcomment %}
{% comment %} <div class="mb-3">
<label for="{{ form.parking_info.id_for_label }}" class="form-label">
<i class="fas fa-parking me-1"></i>
Parking Information
</label>
{{ form.parking_info }}
{% if form.parking_info.errors %}
<div class="text-danger small">
{{ form.parking_info.errors }}
</div>
{% endif %}
</div> {% endcomment %}
{% comment %} <div class="mb-3">
<label for="{{ form.notes.id_for_label }}" class="form-label">
<i class="fas fa-sticky-note me-1"></i>
Notes
</label>
{{ form.notes }}
{% if form.notes.errors %}
<div class="text-danger small">
{{ form.notes.errors }}
</div>
{% endif %}
</div> {% endcomment %}
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-2"></i>
{% trans "Schedule Onsite Interview" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add form validation for future dates
const dateInput = document.querySelector('input[type="date"]');
const today = new Date().toISOString().split('T')[0];
if (dateInput) {
dateInput.min = today;
dateInput.addEventListener('change', function() {
if (this.value < today) {
this.setCustomValidity('{% trans "Interview date must be in the future" %}');
} else {
this.setCustomValidity('');
}
});
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,74 @@
{% extends "base.html" %}
{% load i18n crispy_forms_tags %}
{% block title %}{% trans "Create Remote Interview" %}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-video me-2"></i>
{% blocktrans %}Create Remote Interview for {{ application.name }}{% endblocktrans %}
</h4>
<a href="{% url 'interview_create_type_selection' application.slug %}"
class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>
{% trans "Back to application List" %}
</a>
</div>
<div class="card-body">
<p class="text-muted mb-3">
{% blocktrans %}Schedule a remote interview for <strong>{{ application.name }}</strong>
for the position of <strong>{{ job.title }}</strong>.{% endblocktrans %}
</p>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" action="{% url 'interview_create_remote' application_slug=application.slug %}">
{% csrf_token %}
{{form|crispy}}
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-2"></i>
{% trans "Schedule Remote Interview" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add form validation for future dates
const dateInput = document.querySelector('input[type="date"]');
const today = new Date().toISOString().split('T')[0];
if (dateInput) {
dateInput.min = today;
dateInput.addEventListener('change', function() {
if (this.value < today) {
this.setCustomValidity('{% trans "Interview date must be in the future" %}');
} else {
this.setCustomValidity('');
}
});
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Create Interview - Select Type" %}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-calendar-plus me-2"></i>
{% blocktrans %}Create Interview for {{ application.name }}{% endblocktrans %}
</h4>
</div>
<div class="card-body" hx-boost="true" hx-push-url="false" hx-select=".card-body" hx-swap="innerHTML" hx-target="#candidateviewModalBody">
<p class="text-muted mb-3">
{% blocktrans %}Select the type of interview you want to schedule for <strong>{{ application.name }}</strong>
for the position of <strong>{{ job.title }}</strong>.{% endblocktrans %}
</p>
<div class="d-grid gap-3" style="grid-template-columns: 1fr 1fr;">
<a href="{% url 'interview_create_remote' application_slug=application.slug %}"
class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none">
<div class="text-center">
<i class="fas fa-video me-2"></i>
<div class="mt-2">{% trans "Remote Interview" %}</div>
<small class="d-block">{% trans "Via Zoom/Video Conference" %}</small>
</div>
</a>
<a href="{% url 'interview_create_onsite' application_slug=application.slug %}"
class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none">
<div class="text-center">
<i class="fas fa-building me-2"></i>
<div class="mt-2">{% trans "Onsite Interview" %}</div>
<small class="d-block">{% trans "In-person at our facility" %}</small>
</div>
</a>
</div>
</div>
<div class="mt-4">
<a href="{% url 'applications_interview_view' slug=job.slug %}"
class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>
{% trans "Back to application List" %}
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,768 @@
{% extends 'base.html' %}
{% load static i18n %}
{% block title %}{{ interview.application.name }} - {% trans "Interview Details" %} - ATS{% endblock %}
{% block customCSS %}
<style>
/* KAAT-S UI Variables */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-success: #28a745;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Primary Color Overrides */
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
/* Main Container & Card Styling */
.kaauh-card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Button Styling */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Info Panel Styling */
.info-panel {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-left: 4px solid var(--kaauh-teal);
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.info-panel h5 {
color: var(--kaauh-teal-dark);
font-weight: 600;
margin-bottom: 1rem;
}
/* Status Badge Styling */
.status-badge {
font-size: 0.9rem;
padding: 0.4em 0.8em;
border-radius: 0.4rem;
font-weight: 700;
}
.interview-type-badge {
font-size: 0.8rem;
padding: 0.3rem 0.6rem;
border-radius: 0.3rem;
font-weight: 600;
}
/* Status Colors */
.bg-scheduled { background-color: #6c757d !important; color: white; }
.bg-confirmed { background-color: var(--kaauh-info) !important; color: white; }
.bg-cancelled { background-color: var(--kaauh-danger) !important; color: white; }
.bg-completed { background-color: var(--kaauh-success) !important; color: white; }
.bg-remote { background-color: #007bff !important; color: white; }
.bg-onsite { background-color: #6f42c1 !important; color: white; }
/* Timeline Styling */
.timeline {
position: relative;
padding-left: 2rem;
}
.timeline::before {
content: '';
position: absolute;
left: 0.5rem;
top: 0;
bottom: 0;
width: 2px;
background-color: var(--kaauh-border);
}
.timeline-item {
position: relative;
margin-bottom: 1.5rem;
}
.timeline-item::before {
content: '';
position: absolute;
left: -1.5rem;
top: 0.5rem;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: var(--kaauh-teal);
border: 2px solid white;
box-shadow: 0 0 0 2px var(--kaauh-border);
}
.timeline-content {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid var(--kaauh-border);
}
/* Participant List */
.participant-item {
display: flex;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid var(--kaauh-border);
transition: background-color 0.2s ease;
}
.participant-item:hover {
background-color: #f8f9fa;
}
.participant-item:last-child {
border-bottom: none;
}
.participant-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--kaauh-teal);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 1rem;
}
/* Meeting Details */
.meeting-details {
background-color: #f8f9fa;
border-radius: 0.5rem;
padding: 1rem;
border: 1px solid var(--kaauh-border);
}
.meeting-details .detail-item {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
}
.meeting-details .detail-item:last-child {
border-bottom: none;
}
.meeting-details .detail-label {
font-weight: 600;
color: var(--kaauh-primary-text);
}
.meeting-details .detail-value {
color: #6c757d;
}
/* Custom Height Optimization */
.form-control-sm,
.btn-sm {
padding-top: 0.2rem !important;
padding-bottom: 0.2rem !important;
height: 28px !important;
font-size: 0.8rem !important;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.action-buttons {
flex-direction: column;
}
.action-buttons .btn {
width: 100%;
}
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Header Section -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-calendar-alt me-2"></i>
{% trans "Interview Details" %}
</h1>
<h2 class="h5 text-muted mb-0">
{{ interview.application.name }} - {{ interview.job.title }}
</h2>
</div>
<div class="d-flex gap-2">
<a href="{% url 'interview_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Interviews" %}
</a>
<a href="{% url 'job_detail' interview.job.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-briefcase me-1"></i> {% trans "View Job" %}
</a>
</div>
</div>
<div class="row">
<!-- Left Column - Candidate & Interview Info -->
<div class="col-lg-8">
<!-- Candidate Information Panel -->
<div class="kaauh-card shadow-sm p-4 mb-4">
<div class="d-flex align-items-start justify-content-between mb-3">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-user me-2"></i> {% trans "Candidate Information" %}
</h5>
<div class="action-buttons">
{% if interview.application.resume %}
<a href="{{ interview.application.resume.url }}" class="btn btn-outline-primary btn-sm" target="_blank">
<i class="fas fa-download me-1"></i> {% trans "Download Resume" %}
</a>
{% endif %}
<button type="button" class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateModal"
hx-get="{% url 'candidate_criteria_view_htmx' interview.application.pk %}"
hx-target="#candidateModalBody">
<i class="fas fa-eye me-1"></i> {% trans "AI Scoring" %}
</button>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="info-panel">
<h6>{% trans "Personal Details" %}</h6>
<p class="mb-2"><strong>{% trans "Name:" %}</strong> {{ interview.application.name }}</p>
<p class="mb-2"><strong>{% trans "Email:" %}</strong> {{ interview.application.email }}</p>
<p class="mb-2"><strong>{% trans "Phone:" %}</strong> {{ interview.application.phone }}</p>
{% if interview.application.location %}
<p class="mb-0"><strong>{% trans "Location:" %}</strong> {{ interview.application.location }}</p>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="info-panel">
<h6>{% trans "Application Details" %}</h6>
<p class="mb-2"><strong>{% trans "Job:" %}</strong> {{ interview.job.title }}</p>
<p class="mb-2"><strong>{% trans "Department:" %}</strong> {{ interview.job.department }}</p>
<p class="mb-2"><strong>{% trans "Applied Date:" %}</strong> {{ interview.application.created_at|date:"d-m-Y" }}</p>
<p class="mb-0"><strong>{% trans "Current Stage:" %}</strong>
<span class="badge stage-badge stage-{{ interview.application.stage|lower }}">
{{ interview.application.stage }}
</span>
</p>
</div>
</div>
</div>
</div>
<!-- Interview Details Panel -->
<div class="kaauh-card shadow-sm p-4 mb-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-calendar-check me-2"></i> {% trans "Interview Details" %}
</h5>
<div class="d-flex gap-2">
<span class="badge interview-type-badge
{% if interview.interview.location_type == 'Remote' %}bg-remote
{% else %}bg-onsite
{% endif %}">
{% if interview.interview.location_type == 'Remote' %}
<i class="fas fa-video me-1"></i> {% trans "Remote" %}
{% else %}
<i class="fas fa-building me-1"></i> {% trans "Onsite" %}
{% endif %}
</span>
<span class="badge status-badge
{% if interview.status == 'SCHEDULED' %}bg-scheduled
{% elif interview.status == 'CONFIRMED' %}bg-confirmed
{% elif interview.status == 'CANCELLED' %}bg-cancelled
{% elif interview.status == 'COMPLETED' %}bg-completed
{% endif %}">
{{ interview.status }}
</span>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="meeting-details">
<div class="detail-item">
<span class="detail-label">{% trans "Date:" %}</span>
<span class="detail-value">{{ interview.interview_date|date:"d-m-Y" }}</span>
</div>
<div class="detail-item">
<span class="detail-label">{% trans "Time:" %}</span>
<span class="detail-value">{{ interview.interview_time|date:"h:i A" }}</span>
</div>
<div class="detail-item">
<span class="detail-label">{% trans "Duration:" %}</span>
<span class="detail-value">{{ interview.interview.duration }} {% trans "minutes" %}</span>
</div>
</div>
</div>
<div class="col-md-6">
{% if interview.interview.location_type == 'Remote' %}
<div class="meeting-details">
<h6 class="mb-3">{% trans "Remote Meeting Details" %}</h6>
<div class="detail-item">
<span class="detail-label">{% trans "Platform:" %}</span>
<span class="detail-value">Zoom</span>
</div>
{% if interview.interview %}
<div class="detail-item">
<span class="detail-label">{% trans "Meeting ID:" %}</span>
<span class="detail-value">{{ interview.interview.meeting_id }}</span>
</div>
<div class="detail-item">
<span class="detail-label">{% trans "Password:" %}</span>
<span class="detail-value">{{ interview.interview.password }}</span>
</div>
{% if interview.interview.details_url %}
<div class="mt-3">
<a href="{{ interview.interview.zoommeetingdetails.details_url }}"
target="_blank"
class="btn btn-main-action btn-sm w-100">
<i class="fas fa-video me-1"></i> {% trans "Join Meeting" %}
</a>
</div>
{% endif %}
{% endif %}
</div>
{% else %}
<div class="meeting-details">
<h6 class="mb-3">{% trans "Onsite Location Details" %}</h6>
{% if interview.interview %}
<div class="detail-item">
<span class="detail-label">{% trans "Address:" %}</span>
<span class="detail-value">{{ interview.interview.physical_address }}</span>
</div>
<div class="detail-item">
<span class="detail-label">{% trans "Room:" %}</span>
<span class="detail-value">{{ interview.interview.room_number }}</span>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Timeline/History Section -->
<div class="kaauh-card shadow-sm p-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-history me-2"></i> {% trans "Interview Timeline" %}
</h5>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">{% trans "Interview Scheduled" %}</h6>
<p class="mb-0 text-muted">{% trans "Interview was scheduled for" %} {{ interview.interview_date|date:"d-m-Y" }} {{ interview.interview_time|date:"h:i A" }}</p>
</div>
<small class="text-muted">{{ interview.interview.created_at|date:"d-m-Y h:i A" }}</small>
</div>
</div>
</div>
{% if interview.interview.status == 'CONFIRMED' %}
<div class="timeline-item">
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">{% trans "Interview Confirmed" %}</h6>
<p class="mb-0 text-muted">{% trans "Candidate has confirmed attendance" %}</p>
</div>
<small class="text-muted">{% trans "Recently" %}</small>
</div>
</div>
</div>
{% endif %}
{% if interview.interview.status == 'COMPLETED' %}
<div class="timeline-item">
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">{% trans "Interview Completed" %}</h6>
<p class="mb-0 text-muted">{% trans "Interview has been completed" %}</p>
</div>
<small class="text-muted">{% trans "Recently" %}</small>
</div>
</div>
</div>
{% endif %}
{% if interview.interview.status == 'CANCELLED' %}
<div class="timeline-item">
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">{% trans "Interview Cancelled" %}</h6>
<p class="mb-0 text-muted">{% trans "Interview was cancelled" %}</p>
</div>
<small class="text-muted">{% trans "Recently" %}</small>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Right Column - Participants & Actions -->
<div class="col-lg-4">
<!-- Participants Panel -->
<div class="kaauh-card shadow-sm p-4 mb-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-users me-2"></i> {% trans "Participants" %}
</h5>
<!-- Internal Participants -->
{% if interview.participants.exists %}
<h6 class="mb-2 text-muted">{% trans "Internal Participants" %}</h6>
{% for participant in interview.participants.all %}
<div class="participant-item">
<div class="participant-avatar">
{{ participant.first_name.0 }}{{ participant.last_name.0 }}
</div>
<div class="flex-grow-1">
<div class="fw-semibold">{{ participant.get_full_name }}</div>
<div class="text-muted small">{{ participant.email }}</div>
</div>
</div>
{% endfor %}
{% endif %}
<!-- External Participants -->
{% if interview.system_users.exists %}
<h6 class="mb-2 mt-3 text-muted">{% trans "External Participants" %}</h6>
{% for user in interview.system_users.all %}
<div class="participant-item">
<div class="participant-avatar">
{{ user.first_name.0 }}{{ user.last_name.0 }}
</div>
<div class="flex-grow-1">
<div class="fw-semibold">{{ user.get_full_name }}</div>
<div class="text-muted small">{{ user.email }}</div>
</div>
</div>
{% endfor %}
{% endif %}
{% if not interview.participants.exists and not interview.system_users.exists %}
<div class="text-center py-3 text-muted">
<i class="fas fa-users fa-2x mb-2"></i>
<p class="mb-0">{% trans "No participants added yet" %}</p>
</div>
{% endif %}
<button type="button" class="btn btn-outline-secondary btn-sm w-100 mt-3"
data-bs-toggle="modal"
data-bs-target="#participantModal">
<i class="fas fa-user-plus me-1"></i> {% trans "Add Participants" %}
</button>
</div>
<!-- Actions Panel -->
<div class="kaauh-card shadow-sm p-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark); font-weight: 600;">
<i class="fas fa-cog me-2"></i> {% trans "Actions" %}
</h5>
<div class="action-buttons">
{% if interview.status != 'CANCELLED' and interview.status != 'COMPLETED' %}
<button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal"
data-bs-target="#rescheduleModal">
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
</button>
<button type="button" class="btn btn-outline-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#cancelModal">
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
</button>
{% endif %}
<button type="button" class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal"
data-bs-target="#emailModal">
<i class="fas fa-envelope me-1"></i> {% trans "Send Email" %}
</button>
{% if interview.status == 'COMPLETED' %}
<button type="button" class="btn btn-outline-success btn-sm"
data-bs-toggle="modal"
data-bs-target="#resultModal">
<i class="fas fa-check-circle me-1"></i> {% trans "Update Result" %}
</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Candidate Profile Modal -->
<div class="modal fade modal-xl" id="candidateModal" tabindex="-1" aria-labelledby="candidateModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="candidateModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Candidate Profile" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="candidateModalBody" class="modal-body">
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading profile..." %}
</div>
</div>
</div>
</div>
</div>
<!-- Participant Modal -->
<div class="modal fade" id="participantModal" tabindex="-1" aria-labelledby="participantModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="participantModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Add Participants" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="#">
{% csrf_token %}
<div class="mb-3">
<label for="internal_participants" class="form-label">{% trans "Internal Participants" %}</label>
<select multiple class="form-select" id="internal_participants" name="participants">
<!-- Options will be populated dynamically -->
</select>
</div>
<div class="mb-3">
<label for="external_participants" class="form-label">{% trans "External Participants" %}</label>
<select multiple class="form-select" id="external_participants" name="system_users">
<!-- Options will be populated dynamically -->
</select>
</div>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-plus me-1"></i> {% trans "Add Participants" %}
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="emailModalLabel" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-envelope me-2"></i> {% trans "Compose Email" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="#">
{% csrf_token %}
<div class="mb-3">
<label for="email_to" class="form-label">{% trans "To" %}</label>
<input type="email" class="form-control" id="email_to" value="{{ interview.application.email }}" readonly>
</div>
<div class="mb-3">
<label for="email_subject" class="form-label">{% trans "Subject" %}</label>
<input type="text" class="form-control" id="email_subject" name="subject"
value="{% trans 'Interview Details' %} - {{ interview.job.title }}">
</div>
<div class="mb-3">
<label for="email_message" class="form-label">{% trans "Message" %}</label>
<textarea class="form-control" id="email_message" name="message" rows="6">
{% trans "Dear" %} {{ interview.application.name }},
{% trans "Your interview details are as follows:" %}
{% trans "Date:" %} {{ interview.interview_date|date:"d-m-Y" }}
{% trans "Time:" %} {{ interview.interview_time|date:"h:i A" }}
{% trans "Job:" %} {{ interview.job.title }}
{% if interview.interview.location_type == 'Remote' %}
{% trans "This is a remote interview. You will receive the meeting link separately." %}
{% else %}
{% trans "This is an onsite interview. Please arrive 10 minutes early." %}
{% endif %}
{% trans "Best regards," %}
{% trans "HR Team" %}
</textarea>
</div>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-paper-plane me-1"></i> {% trans "Send Email" %}
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Reschedule Modal -->
<div class="modal fade" id="rescheduleModal" tabindex="-1" aria-labelledby="rescheduleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="rescheduleModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Reschedule Interview" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="#">
{% csrf_token %}
<div class="mb-3">
<label for="new_date" class="form-label">{% trans "New Date" %}</label>
<input type="date" class="form-control" id="new_date" name="new_date" required>
</div>
<div class="mb-3">
<label for="new_time" class="form-label">{% trans "New Time" %}</label>
<input type="time" class="form-control" id="new_time" name="new_time" required>
</div>
<div class="mb-3">
<label for="reschedule_reason" class="form-label">{% trans "Reason for Rescheduling" %}</label>
<textarea class="form-control" id="reschedule_reason" name="reason" rows="3"
placeholder="{% trans 'Optional: Provide reason for rescheduling' %}"></textarea>
</div>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Cancel Modal -->
<div class="modal fade" id="cancelModal" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="cancelModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Cancel Interview" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="#">
{% csrf_token %}
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Are you sure you want to cancel this interview? This action cannot be undone." %}
</div>
<div class="mb-3">
<label for="cancel_reason" class="form-label">{% trans "Reason for Cancellation" %}</label>
<textarea class="form-control" id="cancel_reason" name="reason" rows="3" required
placeholder="{% trans 'Please provide a reason for cancellation' %}"></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">
{% trans "Close" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Result Modal -->
<div class="modal fade" id="resultModal" tabindex="-1" aria-labelledby="resultModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="resultModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Update Interview Result" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="#">
{% csrf_token %}
<div class="mb-3">
<label for="interview_result" class="form-label">{% trans "Interview Result" %}</label>
<select class="form-select" id="interview_result" name="result" required>
<option value="">{% trans "Select Result" %}</option>
<option value="passed">{% trans "Passed" %}</option>
<option value="failed">{% trans "Failed" %}</option>
<option value="on_hold">{% trans "On Hold" %}</option>
</select>
</div>
<div class="mb-3">
<label for="result_notes" class="form-label">{% trans "Notes" %}</label>
<textarea class="form-control" id="result_notes" name="notes" rows="4"
placeholder="{% trans 'Add interview feedback and notes' %}"></textarea>
</div>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-check me-1"></i> {% trans "Update Result" %}
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Clear modal content when hidden
const modals = ['candidateModal', 'participantModal', 'emailModal', 'rescheduleModal', 'cancelModal', 'resultModal'];
modals.forEach(modalId => {
const modal = document.getElementById(modalId);
if (modal) {
modal.addEventListener('hidden.bs.modal', function () {
const modalBody = modal.querySelector('.modal-body');
if (modalBody && modalId === 'candidateModal') {
modalBody.innerHTML = `
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading profile..." %}
</div>
`;
}
});
}
});
});
</script>
{% endblock %}

View File

@ -1,80 +1,234 @@
{% extends "base.html" %} {% extends 'base.html' %}
{% load static i18n %} {% load static i18n %}
{% block title %}{% trans "Scheduled Interviews List" %} - {{ block.super }}{% endblock %} {% block title %}{% trans "Interview Management" %} - ATS{% endblock %}
{% block customCSS %} {% block customCSS %}
{# (Your existing CSS is kept here, as it is perfect for the theme) #}
<style> <style>
/* ... (Your CSS styles) ... */ /* KAAT-S UI Variables */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-success: #28a745;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Primary Color Overrides */
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
/* Main Container & Card Styling */
.kaauh-card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Filter Controls */
.filter-controls {
background-color: #f8f9fa;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid var(--kaauh-border);
}
/* Button Styling */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Interview Table Styling */
.interview-table {
table-layout: fixed;
width: 100%;
border-collapse: separate;
border-spacing: 0;
background-color: white;
border-radius: 0.5rem;
overflow: hidden;
}
.interview-table thead {
background-color: var(--kaauh-border);
}
.interview-table th {
padding: 0.75rem 1rem;
font-weight: 600;
color: var(--kaauh-teal-dark);
border-bottom: 2px solid var(--kaauh-teal);
font-size: 0.9rem;
vertical-align: middle;
}
.interview-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle;
font-size: 0.9rem;
}
.interview-table tbody tr:hover {
background-color: #f1f3f4;
}
/* Column Widths */
.interview-table thead th:nth-child(1) { width: 40px; }
.interview-table thead th:nth-child(2) { width: 15%; }
.interview-table thead th:nth-child(3) { width: 12%; }
.interview-table thead th:nth-child(4) { width: 12%; }
.interview-table thead th:nth-child(5) { width: 10%; }
.interview-table thead th:nth-child(6) { width: 8%; }
.interview-table thead th:nth-child(7) { width: 8%; }
.interview-table thead th:nth-child(8) { width: 15%; }
/* Candidate and Job Info */
.candidate-name {
font-weight: 600;
color: var(--kaauh-primary-text);
}
.candidate-details {
font-size: 0.8rem;
color: #6c757d;
}
.job-title {
font-weight: 500;
color: var(--kaauh-teal-dark);
}
/* Badges and Statuses */
.status-badge {
font-size: 0.75rem;
padding: 0.3em 0.7em;
border-radius: 0.35rem;
font-weight: 700;
}
.interview-type-badge {
font-size: 0.7rem;
padding: 0.25rem 0.5rem;
border-radius: 0.3rem;
font-weight: 600;
}
/* Status Colors */
.bg-scheduled { background-color: #6c757d !important; color: white; }
.bg-confirmed { background-color: var(--kaauh-info) !important; color: white; }
.bg-cancelled { background-color: var(--kaauh-danger) !important; color: white; }
.bg-completed { background-color: var(--kaauh-success) !important; color: white; }
.bg-remote { background-color: #007bff !important; color: white; }
.bg-onsite { background-color: #6f42c1 !important; color: white; }
/* Custom Height Optimization */
.form-control-sm,
.btn-sm {
padding-top: 0.2rem !important;
padding-bottom: 0.2rem !important;
height: 28px !important;
font-size: 0.8rem !important;
}
/* Pagination Styling */
.pagination .page-link {
color: var(--kaauh-teal);
border-color: var(--kaauh-border);
}
.pagination .page-item.active .page-link {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
}
.pagination .page-link:hover {
color: var(--kaauh-teal-dark);
background-color: #f8f9fa;
}
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{{interviews}}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<!-- Header Section -->
<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;"> <div>
<i class="fas fa-calendar-alt me-2"></i> {% trans "Scheduled Interviews" %} <h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
</h1> <i class="fas fa-calendar-alt me-2"></i>
{# FIX: Using safe anchor href="#" to prevent the NoReverseMatch crash. #} {% trans "Interview Management" %}
{# Replace '#' with {% url 'create_scheduled_interview' %} once the URL name is defined in urls.py #} </h1>
<a href="#" class="btn btn-main-action"> <h2 class="h5 text-muted mb-0">
<i class="fas fa-plus me-1"></i> {% trans "Schedule Interview" %} {% trans "Total Interviews:" %} <span class="fw-bold">{{ interviews|length }}</span>
</a> </h2>
</div>
<div class="d-flex gap-2">
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Dashboard" %}
</a>
</div>
</div> </div>
<div class="card mb-4 shadow-sm no-hover"> <!-- Filter Controls -->
<div class="card-body"> <div class="filter-controls">
<form method="GET" class="row g-3 align-items-end"> <form method="get" class="row g-3">
{# Search field #} <div class="col-md-3">
<div class="col-md-4"> <label for="job_filter" class="form-label form-label-sm">{% trans "Job" %}</label>
<label for="q" class="form-label small text-muted">{% trans "Search (Candidate/Job)" %}</label> <select name="job" id="job_filter" class="form-select form-select-sm">
<div class="input-group"> <option value="">{% trans "All Jobs" %}</option>
<input type="text" class="form-control form-control-sm" id="q" name="q" placeholder="{% trans 'Search...' %}" value="{{ search_query }}"> {% for job in jobs %}
</div> <option value="{{ job.id }}" {% if request.GET.job == job.id|stringformat:"s" %}selected{% endif %}>
</div> {{ job.title }}
</option>
{# Filter by Status #} {% endfor %}
<div class="col-md-3"> </select>
<label for="status" class="form-label small text-muted">{% trans "Filter by Status" %}</label> </div>
<select name="status" id="status" class="form-select form-select-sm"> <div class="col-md-2">
<option value="">{% trans "All Statuses" %}</option> <label for="status_filter" class="form-label form-label-sm">{% trans "Status" %}</label>
<option value="scheduled" {% if status_filter == 'scheduled' %}selected{% endif %}>{% trans "Scheduled" %}</option> <select name="status" id="status_filter" class="form-select form-select-sm">
<option value="confirmed" {% if status_filter == 'confirmed' %}selected{% endif %}>{% trans "Confirmed" %}</option> <option value="">{% trans "All Status" %}</option>
<option value="completed" {% if status_filter == 'completed' %}selected{% endif %}>{% trans "Completed" %}</option> <option value="scheduled" {% if request.GET.status == "scheduled" %}selected{% endif %}>{% trans "Scheduled" %}</option>
<option value="cancelled" {% if status_filter == 'cancelled' %}selected{% endif %}>{% trans "Cancelled" %}</option> <option value="confirmed" {% if request.GET.status == "confirmed" %}selected{% endif %}>{% trans "Confirmed" %}</option>
</select> <option value="cancelled" {% if request.GET.status == "cancelled" %}selected{% endif %}>{% trans "Cancelled" %}</option>
</div> <option value="completed" {% if request.GET.status == "completed" %}selected{% endif %}>{% trans "Completed" %}</option>
</select>
{# Filter by Interview Type (ONSITE/REMOTE) - This list now correctly populated #} </div>
<div class="col-md-3"> <div class="col-md-2">
<label for="interview_type" class="form-label small text-muted">{% trans "Interview Type" %}</label> <label for="type_filter" class="form-label form-label-sm">{% trans "Type" %}</label>
<select name="interview_type" id="interview_type" class="form-select form-select-sm"> <select name="type" id="type_filter" class="form-select form-select-sm">
<option value="">{% trans "All Types" %}</option> <option value="">{% trans "All Types" %}</option>
{% for type_value, type_label in interview_types %} <option value="remote" {% if request.GET.type == "remote" %}selected{% endif %}>{% trans "Remote" %}</option>
<option value="{{ type_value }}" {% if type_filter == type_value %}selected{% endif %}> <option value="onsite" {% if request.GET.type == "onsite" %}selected{% endif %}>{% trans "Onsite" %}</option>
{{ type_label }} </select>
</option> </div>
{% endfor %} <div class="col-md-3">
</select> <label for="search_filter" class="form-label form-label-sm">{% trans "Search Candidate" %}</label>
</div> <input type="text" name="search" id="search_filter" class="form-control form-control-sm"
value="{{ request.GET.search }}" placeholder="{% trans 'Name or Email' %}">
<div class="col-md-2"> </div>
<div class="filter-buttons"> <div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm me-2">
<i class="fas fa-filter me-1"></i> {% trans "Apply" %} <i class="fas fa-filter me-1"></i> {% trans "Filter" %}
</button> </button>
{% if status_filter or search_query or type_filter %} <a href="{% url 'interview_list' %}" class="btn btn-outline-secondary btn-sm">
{# Assuming 'interview_list' is the URL name for this view #} <i class="fas fa-times me-1"></i> {% trans "Clear" %}
<a href="{% url 'interview_list' %}" class="btn btn-outline-secondary btn-sm"> </a>
<i class="fas fa-times me-1"></i> {% trans "Clear" %} </div>
</a> </form>
{% endif %}
</div>
</div>
</form>
</div>
</div> </div>
{{meetings}} {{meetings}}
{# Using 'meetings' based on the context_object_name provided #} {# Using 'meetings' based on the context_object_name provided #}
@ -149,70 +303,83 @@
{% endfor %} {% endfor %}
</div> </div>
{# Table View (Logic is identical, safe access applied) #} <form id="interview-form">
<div class="table-view"> {% csrf_token %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table interview-table align-middle">
<thead> <thead>
<tr> <tr>
<th scope="col">{% trans "Candidate" %}</th> <th><i class="fas fa-user me-1"></i> {% trans "Candidate" %}</th>
<th scope="col">{% trans "Job" %}</th> <th><i class="fas fa-briefcase me-1"></i> {% trans "Job" %}</th>
<th scope="col">{% trans "Type" %}</th> <th><i class="fas fa-calendar me-1"></i> {% trans "Date & Time" %}</th>
<th scope="col">{% trans "Date/Time" %}</th> <th><i class="fas fa-tag me-1"></i> {% trans "Type" %}</th>
<th scope="col">{% trans "Duration" %}</th> <th><i class="fas fa-info-circle me-1"></i> {% trans "Status" %}</th>
<th scope="col">{% trans "Status" %}</th> {% comment %} <th><i class="fas fa-users me-1"></i> {% trans "Participants" %}</th> {% endcomment %}
<th scope="col" class="text-end">{% trans "Actions" %}</th> <th><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for interview in meetings %} {% for interview in interviews %}
<tr> <tr>
<td> <td>
<strong class="text-primary-theme"> <div class="candidate-name">{{ interview.application.name }}</div>
<a href="{% url 'application_detail' interview.candidate.slug %}" class="text-decoration-none text-primary-theme">{{ interview.candidate.name }}</a> <div class="application-details">
</strong> <i class="fas fa-envelope me-1"></i> {{ interview.application.email }}<br>
<i class="fas fa-phone me-1"></i> {{ interview.application.phone }}
</div>
</td> </td>
<td> <td>
<a class="text-secondary text-decoration-none" href="{% url 'job_detail' interview.job.slug %}">{{ interview.job.title }}</a> <div class="job-title">{{ interview.job.title }}</div>
<div class="candidate-details">{{ interview.job.department }}</div>
</td> </td>
<td> <td>
{{ interview.schedule.get_interview_type_display }} <div class="candidate-details">
<i class="fas fa-calendar-day me-1"></i> {{ interview.interview_date|date:"d-m-Y" }}<br>
<i class="fas fa-clock me-1"></i> {{ interview.interview_time|date:"h:i A" }}
</div>
</td> </td>
<td>{{ interview.interview_date|date:"M d, Y" }} <br>({{ interview.interview_time|time:"H:i" }})</td>
<td>{{ interview.schedule.interview_duration }} min</td>
<td> <td>
<span class="badge bg-{{ interview.status }}"> {% if interview.interview.location_type == 'Remote' %}
{% if interview.status == 'confirmed' %} <span class="badge interview-type-badge bg-remote">
<i class="fas fa-circle me-1 text-white"></i> <i class="fas fa-video me-1"></i> {% trans "Remote" %}
{% endif %} </span>
{{ interview.status|title }} {% else %}
<span class="badge interview-type-badge bg-onsite">
<i class="fas fa-building me-1"></i> {% trans "Onsite" %}
</span>
{% endif %}
</td>
<td>
<span class="badge bg-primary-theme">
{{ interview.status|upper }}
</span> </span>
</td> </td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
{# CRITICAL FIX: Safe access to join URL #} <td>
{% if interview.schedule.interview_type == 'Remote' and interview.zoom_meeting and interview.zoom_meeting.join_url %} <div class="btn-group" role="group">
<a href="{{ interview.zoom_meeting.join_url }}" target="_blank" class="btn btn-main-action" title="{% trans 'Join' %}"> <a href="{% url 'interview_detail' interview.slug %}"
<i class="fas fa-sign-in-alt"></i> class="btn btn-outline-primary btn-sm"
</a> title="{% trans 'View Details' %}">
{% endif %}
<a href="{% url 'scheduled_interview_detail' interview.slug %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'update_scheduled_interview' interview.slug %}" class="btn btn-outline-secondary" title="{% trans 'Update' %}"> {% comment %} {% if interview.status != 'CANCELLED' and interview.status != 'COMPLETED' %}
<i class="fas fa-edit"></i> <button type="button" class="btn btn-outline-secondary btn-sm"
</a> data-bs-toggle="modal"
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}" data-bs-target="#actionModal"
data-bs-toggle="modal" hx-get="#"
data-bs-target="#meetingModal" hx-target="#actionModalBody"
hx-post="{% url 'delete_scheduled_interview' interview.slug %}" title="{% trans 'Reschedule' %}">
hx-target="#meetingModalBody" <i class="fas fa-redo-alt"></i>
hx-swap="outerHTML" </button>
data-item-name="{{ interview.candidate.name }} Interview"> <button type="button" class="btn btn-outline-danger btn-sm"
<i class="fas fa-trash-alt"></i> data-bs-toggle="modal"
</button> data-bs-target="#actionModal"
hx-get="#"
hx-target="#actionModalBody"
title="{% trans 'Cancel' %}">
<i class="fas fa-times"></i>
</button>
{% endif %} {% endcomment %}
</div> </div>
</td> </td>
</tr> </tr>
@ -220,49 +387,138 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</form>
<!-- Pagination -->
{% if is_paginated %}
<nav aria-label="Interview pagination" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">{% trans "First" %}</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">{% trans "Previous" %}</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">{% trans "Next" %}</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">{% trans "Last" %}</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info text-center py-5" role="alert">
<i class="fas fa-info-circle fa-2x mb-3"></i>
<h5>{% trans "No interviews found" %}</h5>
<p class="text-muted mb-0">
{% trans "There are no interviews matching your current filters." %}
<a href="{% url 'interview_list' %}" class="alert-link">{% trans "Clear filters" %}</a>
{% trans "to see all interviews." %}
</p>
</div> </div>
</div>
{# Pagination #}
{% if is_paginated %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}{% if type_filter %}&interview_type={{ type_filter }}{% endif %}">First</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}{% if type_filter %}&interview_type={{ type_filter }}{% endif %}">Previous</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}{% if type_filter %}&interview_type={{ type_filter }}{% endif %}">Next</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}{% if type_filter %}&interview_type={{ type_filter }}{% endif %}">Last</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %} {% endif %}
{% else %} </div>
<div class="text-center py-5 card shadow-sm"> </div>
<div class="card-body">
<i class="fas fa-calendar-alt fa-3x mb-3" style="color: var(--kaauh-teal-dark);"></i> <!-- Action Modal -->
<h3>{% trans "No Interviews found" %}</h3> <div class="modal fade" id="actionModal" tabindex="-1" aria-labelledby="actionModalLabel" aria-hidden="true">
<p class="text-muted">{% trans "Schedule your first interview or adjust your filters." %}</p> <div class="modal-dialog">
{# FIX: Using safe anchor href="#" to prevent the NoReverseMatch crash. #} <div class="modal-content kaauh-card">
<a href="#" class="btn btn-main-action mt-3"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<i class="fas fa-plus me-1"></i> {% trans "Schedule an Interview" %} <h5 class="modal-title" id="actionModalLabel" style="color: var(--kaauh-teal-dark);">
</a> {% trans "Interview Action" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="actionModalBody" class="modal-body">
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading..." %}
</div>
</div> </div>
</div> </div>
{% endif %} </div>
</div> </div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
if (selectAllCheckbox) {
function updateSelectAllState() {
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
const totalCount = rowCheckboxes.length;
if (checkedCount === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedCount === totalCount) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
}
selectAllCheckbox.addEventListener('change', function () {
const isChecked = selectAllCheckbox.checked;
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
rowCheckboxes.forEach(function (checkbox) {
checkbox.checked = isChecked;
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
});
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
updateSelectAllState();
});
rowCheckboxes.forEach(function (checkbox) {
checkbox.addEventListener('change', updateSelectAllState);
});
updateSelectAllState();
}
// Clear modal content when hidden
const actionModal = document.getElementById('actionModal');
actionModal.addEventListener('hidden.bs.modal', function () {
const modalBody = actionModal.querySelector('#actionModalBody');
if (modalBody) {
modalBody.innerHTML = `
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading..." %}
</div>
`;
}
});
});
</script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,42 @@
{% load i18n %}
<table class="table candidate-table align-middle">
<thead>
<tr>
<th style="width: 40%;"><i class="fas fa-user me-1"></i> {% trans "Topic" %}</th>
<th style="width: 15%;"><i class="fas fa-calendar-alt me-1"></i> {% trans "Date" %}</th>
<th style="width: 5%;"><i class="fas fa-map-marker-alt me-1"></i> {% trans "Duration" %}</th>
<th style="width: 10%;"><i class="fas fa-map-marker-alt me-1"></i> {% trans "Location" %}</th>
<th style="width: 10%;"><i class="fas fa-info-circle me-1"></i> {% trans "Status" %}</th>
<th style="width: 10%;"><i class="fas fa-ellipsis-h me-1"></i> {% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for interview in interviews %}
<tr>
<td>{{ interview.interview.topic }}</td>
<td>{{ interview.interview_date }} {{interview.interview_time}}</td>
<td>{{ interview.interview.duration }}</td>
<td>
<span class="badge bg-primary-theme">
{{ interview.interview.location_type }}
</span>
</td>
<td>
<span class="badge bg-primary-theme">
{{ interview.get_status_display }}
</span>
</td>
<td><a class="btn btn-outline-primary btn-sm" href="{% url 'interview_detail' interview.slug %}" target="_blank">View</a></td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center text-muted">
<i class="fas fa-info-circle me-2"></i>
{% trans "No interviews scheduled yet." %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

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

@ -27,12 +27,15 @@
<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

@ -138,7 +138,7 @@
</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>

View File

@ -0,0 +1,372 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{% trans "Delete Applicant" %} - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* KAAT-S UI Variables */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Main Container & Card Styling */
.kaauh-card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Warning Section */
.warning-section {
background: linear-gradient(135deg, #fff3cd 0%, #ffeeba 100%);
border: 1px solid #ffeeba;
border-radius: 0.75rem;
padding: 2rem;
margin-bottom: 2rem;
text-align: center;
}
.warning-icon {
font-size: 4rem;
color: var(--kaauh-warning);
margin-bottom: 1rem;
}
.warning-title {
color: #856404;
font-weight: 700;
margin-bottom: 1rem;
}
.warning-text {
color: #856404;
margin-bottom: 0;
}
/* Applicant Info Card */
.person-info {
background-color: #f8f9fa;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid var(--kaauh-border);
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e9ecef;
}
.info-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.info-icon {
width: 40px;
height: 40px;
background-color: var(--kaauh-teal);
color: white;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
flex-shrink: 0;
}
.info-content {
flex: 1;
}
.info-label {
font-weight: 600;
color: var(--kaauh-primary-text);
margin-bottom: 0.25rem;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-value {
color: #6c757d;
font-size: 1rem;
}
/* Button Styling */
.btn-danger {
background-color: var(--kaauh-danger);
border-color: var(--kaauh-danger);
color: white;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3);
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
color: white;
font-weight: 600;
}
/* Consequence List */
.consequence-list {
list-style: none;
padding: 0;
margin: 0;
}
.consequence-list li {
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
color: #6c757d;
}
.consequence-list li:last-child {
border-bottom: none;
}
.consequence-list li i {
color: var(--kaauh-danger);
margin-right: 0.5rem;
}
/* Person Profile Image */
.person-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--kaauh-teal);
}
.avatar-placeholder {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid var(--kaauh-teal);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Header Section -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Delete Applicant" %}
</h1>
<p class="text-muted mb-0">
{% trans "You are about to delete a Applicant record. This action cannot be undone." %}
</p>
</div>
<a href="{% url 'person_detail' object.slug %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Applicant" %}
</a>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<!-- Warning Section -->
<div class="warning-section">
<div class="warning-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h3 class="warning-title">{% trans "Warning: This action cannot be undone!" %}</h3>
<p class="warning-text">
{% trans "Deleting this Applicant will permanently remove all associated data. Please review the information below carefully before proceeding." %}
</p>
</div>
<!-- Applicant Information -->
<div class="card kaauh-card mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-user me-2"></i>
{% trans "Applicant to be Deleted" %}
</h5>
</div>
<div class="card-body">
<div class="person-info">
<div class="d-flex align-items-center mb-4">
{% if object.profile_image %}
<img src="{{ object.profile_image.url }}" alt="{{ object.get_full_name }}" class="person-avatar me-3">
{% else %}
<div class="avatar-placeholder me-3">
<i class="fas fa-user text-muted fa-2x"></i>
</div>
{% endif %}
<div>
<h4 class="mb-1">{{ object.get_full_name }}</h4>
{% if object.email %}
<p class="text-muted mb-0">{{ object.email }}</p>
{% endif %}
</div>
</div>
{% if object.phone %}
<div class="info-item">
<div class="info-icon">
<i class="fas fa-phone"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Phone" %}</div>
<div class="info-value">{{ object.phone }}</div>
</div>
</div>
{% endif %}
<div class="info-item">
<div class="info-icon">
<i class="fas fa-calendar"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Created On" %}</div>
<div class="info-value">{{ object.created_at|date:"F d, Y" }}</div>
</div>
</div>
{% if object.nationality %}
<div class="info-item">
<div class="info-icon">
<i class="fas fa-globe"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Nationality" %}</div>
<div class="info-value">{{ object.nationality }}</div>
</div>
</div>
{% endif %}
{% if object.gender %}
<div class="info-item">
<div class="info-icon">
<i class="fas fa-venus-mars"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Gender" %}</div>
<div class="info-value">{{ object.get_gender_display }}</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Consequences -->
<div class="card kaauh-card mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-list me-2"></i>
{% trans "What will happen when you delete this Applicant?" %}
</h5>
</div>
<div class="card-body">
<ul class="consequence-list">
<li>
<i class="fas fa-times-circle"></i>
{% trans "The Applicant profile and all Applicantal information will be permanently deleted" %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "All associated applications and documents will be removed" %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "Any interview schedules and history will be deleted" %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "All related data and records will be lost" %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "This action cannot be undone under any circumstances" %}
</li>
</ul>
</div>
</div>
<!-- Confirmation Form -->
<div class="card kaauh-card">
<div class="card-body">
<form method="post" id="deleteForm">
{% csrf_token %}
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirm_delete" name="confirm_delete" required>
<label class="form-check-label" for="confirm_delete">
<strong>{% trans "I understand that this action cannot be undone and I want to permanently delete this person." %}</strong>
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'person_detail' object.slug %}" class="btn btn-secondary btn-lg">
<i class="fas fa-times me-2"></i>
{% trans "Cancel" %}
</a>
<button type="submit"
class="btn btn-danger btn-lg"
id="deleteButton"
disabled>
<i class="fas fa-trash me-2"></i>
{% trans "Delete Applicant Permanently" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const confirmDeleteCheckbox = document.getElementById('confirm_delete');
const deleteButton = document.getElementById('deleteButton');
const deleteForm = document.getElementById('deleteForm');
function validateForm() {
const checkboxChecked = confirmDeleteCheckbox.checked;
deleteButton.disabled = !checkboxChecked;
if (checkboxChecked) {
deleteButton.classList.remove('btn-secondary');
deleteButton.classList.add('btn-danger');
} else {
deleteButton.classList.remove('btn-danger');
deleteButton.classList.add('btn-secondary');
}
}
confirmDeleteCheckbox.addEventListener('change', validateForm);
// Add confirmation before final submission
});
</script>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{% trans "Delete Person" %} - {{ block.super }}{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h4 class="mb-0">
<i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Confirm Deletion" %}
</h4>
</div>
<div class="card-body">
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading">
<i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Warning: This action cannot be undone!" %}
</h5>
<p class="mb-0">
{% trans "You are about to permanently delete this person and all associated data." %}
</p>
</div>
<div class="text-center mb-4">
{% if person.profile_image %}
<img src="{{ person.profile_image.url }}" alt="{{ person.get_full_name }}"
class="rounded-circle mb-3" style="width: 100px; height: 100px; object-fit: cover;">
{% else %}
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-3"
style="width: 100px; height: 100px;">
<i class="fas fa-user text-muted fa-2x"></i>
</div>
{% endif %}
<h5>{{ person.get_full_name }}</h5>
{% if person.email %}
<p class="text-muted mb-0">{{ person.email }}</p>
{% endif %}
</div>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'person_update' person.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-1"></i> {% trans "Delete Person" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

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

@ -287,13 +287,13 @@
class="btn btn-outline-secondary" title="{% trans 'Edit' %}"> class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<button type="button" class="btn btn-outline-danger" {% comment %} <button type="button" class="btn btn-outline-danger"
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>
</td> </td>
@ -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

@ -194,6 +194,9 @@
<a href="{% url 'person_detail' person.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'person_detail' person.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" %}
</a> </a>
<a href="{% url 'person_delete' person.slug %}" class="btn btn-danger">
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
</a>
<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 List" %} <i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
</a> </a>

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

@ -224,7 +224,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 +327,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 +353,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

@ -1,217 +1,478 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% load static i18n %} {% load static i18n widget_tweaks %}
{% block title %}{{ title }} - ATS{% endblock %} {% block title %}{{ title }} - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* UI Variables for the KAAT-S Theme */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-gray-light: #f8f9fa;
}
/* Form Container Styling */
.form-container {
max-width: 800px;
margin: 0 auto;
}
/* Card Styling */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
/* Main Action Button Style */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1.5rem;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Secondary Button Style */
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Form Field Styling */
.form-control:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.form-select:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
/* Breadcrumb Styling */
.breadcrumb {
background-color: transparent;
padding: 0;
margin-bottom: 1rem;
}
.breadcrumb-item + .breadcrumb-item::before {
content: ">";
color: var(--kaauh-teal);
}
/* Alert Styling */
.alert {
border-radius: 0.5rem;
border: none;
}
/* Loading State */
.btn.loading {
position: relative;
pointer-events: none;
opacity: 0.8;
}
.btn.loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
margin: auto;
border: 2px solid transparent;
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Current Profile Section */
.current-profile {
background-color: var(--kaauh-gray-light);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
}
.current-profile h6 {
color: var(--kaauh-teal-dark);
font-weight: 600;
margin-bottom: 0.75rem;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container py-4"> <div class="container-fluid py-4">
<!-- Header --> <div class="form-container">
<div class="d-flex justify-content-between align-items-center mb-4"> <!-- Breadcrumb Navigation -->
<div> <nav aria-label="breadcrumb">
<h1 class="h3 mb-1">{{ title }}</h1> <ol class="breadcrumb">
<p class="text-muted mb-0"> <li class="breadcrumb-item">
<a href="{% url 'agency_list' %}" class="text-decoration-none text-secondary">
<i class="fas fa-building me-1"></i> {% trans "Agencies" %}
</a>
</li>
{% if agency %} {% if agency %}
{% trans "Update the hiring agency information below." %} <li class="breadcrumb-item">
<a href="{% url 'agency_detail' agency.slug %}" class="text-decoration-none text-secondary">
{{ agency.name }}
</a>
</li>
<li class="breadcrumb-item active" aria-current="page"
style="
color: #F43B5E; /* Rosy Accent Color */
font-weight: 600;">{% trans "Update" %}</li>
{% else %} {% else %}
{% trans "Fill in the details to add a new hiring agency." %} <li class="breadcrumb-item active" aria-current="page"
style="
color: #F43B5E; /* Rosy Accent Color */
font-weight: 600;">{% trans "Create" %}</li>
{% endif %} {% endif %}
</p> </ol>
</nav>
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-building me-2"></i> {{ title }}
</h1>
<div class="d-flex gap-2">
{% if agency %}
<a href="{% url 'agency_detail' agency.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
</a>
<a href="{% url 'agency_delete' agency.slug %}" class="btn btn-danger">
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
</a>
{% endif %}
<a href="{% url 'agency_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
</a>
</div>
</div> </div>
<a href="{% url 'agency_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Agencies" %}
</a>
</div>
<!-- Form --> {% if agency %}
<div class="row"> <!-- Current Agency Info -->
<div class="col-lg-8"> <div class="card shadow-sm mb-4">
<div class="card"> <div class="card-body">
<div class="card-body"> <div class="current-profile">
{% if form.non_field_errors %} <h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6>
<div class="alert alert-danger" role="alert"> <div class="d-flex align-items-center">
<h5 class="alert-heading"> <div class="current-image d-flex align-items-center justify-content-center bg-light">
<i class="fas fa-exclamation-triangle me-2"></i> <i class="fas fa-building text-muted"></i>
{% trans "Please correct the errors below:" %}
</h5>
{% for error in form.non_field_errors %}
<p class="mb-0">{{ error }}</p>
{% endfor %}
</div> </div>
{% endif %} <div>
<h5 class="mb-1">{{ agency.name }}</h5>
<form method="post" novalidate> {% if agency.contact_person %}
{% csrf_token %} <p class="text-muted mb-0">{% trans "Contact" %}: {{ agency.contact_person }}</p>
<!-- Name -->
<div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label">
{{ form.name.label }} <span class="text-danger">*</span>
</label>
{{ form.name }}
{% if form.name.errors %}
{% for error in form.name.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %} {% endif %}
{% if form.name.help_text %} {% if agency.email %}
<div class="form-text">{{ form.name.help_text }}</div> <p class="text-muted mb-0">{{ agency.email }}</p>
{% endif %} {% endif %}
<small class="text-muted">
{% trans "Created" %}: {{ agency.created_at|date:"d M Y" }} •
{% trans "Last Updated" %}: {{ agency.updated_at|date:"d M Y" }}
</small>
</div> </div>
</div>
<!-- Contact Person and Phone -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.contact_person.id_for_label }}" class="form-label">
{{ form.contact_person.label }}
</label>
{{ form.contact_person }}
{% if form.contact_person.errors %}
{% for error in form.contact_person.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.contact_person.help_text %}
<div class="form-text">{{ form.contact_person.help_text }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.phone.id_for_label }}" class="form-label">
{{ form.phone.label }}
</label>
{{ form.phone }}
{% if form.phone.errors %}
{% for error in form.phone.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.phone.help_text %}
<div class="form-text">{{ form.phone.help_text }}</div>
{% endif %}
</div>
</div>
<!-- Email and Website -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label">
{{ form.email.label }}
</label>
{{ form.email }}
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.email.help_text %}
<div class="form-text">{{ form.email.help_text }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.website.id_for_label }}" class="form-label">
{{ form.website.label }}
</label>
{{ form.website }}
{% if form.website.errors %}
{% for error in form.website.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.website.help_text %}
<div class="form-text">{{ form.website.help_text }}</div>
{% endif %}
</div>
</div>
<!-- Address -->
<div class="mb-3">
<label for="{{ form.address.id_for_label }}" class="form-label">
{{ form.address.label }}
</label>
{{ form.address }}
{% if form.address.errors %}
{% for error in form.address.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.address.help_text %}
<div class="form-text">{{ form.address.help_text }}</div>
{% endif %}
</div>
<!-- Country and City -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.country.id_for_label }}" class="form-label">
{{ form.country.label }}
</label>
{{ form.country }}
{% if form.country.errors %}
{% for error in form.country.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.country.help_text %}
<div class="form-text">{{ form.country.help_text }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.city.id_for_label }}" class="form-label">
{{ form.city.label }}
</label>
{{ form.city }}
{% if form.city.errors %}
{% for error in form.city.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.city.help_text %}
<div class="form-text">{{ form.city.help_text }}</div>
{% endif %}
</div>
</div>
<!-- Description -->
<div class="mb-4">
<label for="{{ form.description.id_for_label }}" class="form-label">
{{ form.description.label }}
</label>
{{ form.description }}
{% if form.description.errors %}
{% for error in form.description.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.description.help_text %}
<div class="form-text">{{ form.description.help_text }}</div>
{% endif %}
</div>
<!-- Form Actions -->
<div class="d-flex justify-content-between">
<a href="{% url 'agency_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {{ button_text }}
</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
{% endif %}
<!-- Form Card -->
<div class="card shadow-sm">
<div class="card-body p-4">
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading">
<i class="fas fa-exclamation-triangle me-2"></i>{% trans "Error" %}
</h5>
{% for error in form.non_field_errors %}
<p class="mb-0">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" novalidate id="agency-form">
{% csrf_token %}
<!-- Name -->
<div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label">
{{ form.name.label }} <span class="text-danger">*</span>
</label>
{{ form.name|add_class:"form-control" }}
{% if form.name.errors %}
{% for error in form.name.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.name.help_text %}
<div class="form-text">{{ form.name.help_text }}</div>
{% endif %}
</div>
<!-- Contact Person and Phone -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.contact_person.id_for_label }}" class="form-label">
{{ form.contact_person.label }}
</label>
{{ form.contact_person|add_class:"form-control" }}
{% if form.contact_person.errors %}
{% for error in form.contact_person.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.contact_person.help_text %}
<div class="form-text">{{ form.contact_person.help_text }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.phone.id_for_label }}" class="form-label">
{{ form.phone.label }}
</label>
{{ form.phone|add_class:"form-control" }}
{% if form.phone.errors %}
{% for error in form.phone.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.phone.help_text %}
<div class="form-text">{{ form.phone.help_text }}</div>
{% endif %}
</div>
</div>
<!-- Email and Website -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label">
{{ form.email.label }}
</label>
{{ form.email|add_class:"form-control" }}
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.email.help_text %}
<div class="form-text">{{ form.email.help_text }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.website.id_for_label }}" class="form-label">
{{ form.website.label }}
</label>
{{ form.website|add_class:"form-control" }}
{% if form.website.errors %}
{% for error in form.website.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.website.help_text %}
<div class="form-text">{{ form.website.help_text }}</div>
{% endif %}
</div>
</div>
<!-- Address -->
<div class="mb-3">
<label for="{{ form.address.id_for_label }}" class="form-label">
{{ form.address.label }}
</label>
{{ form.address|add_class:"form-control" }}
{% if form.address.errors %}
{% for error in form.address.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.address.help_text %}
<div class="form-text">{{ form.address.help_text }}</div>
{% endif %}
</div>
<!-- Country and City -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.country.id_for_label }}" class="form-label">
{{ form.country.label }}
</label>
{{ form.country|add_class:"form-control" }}
{% if form.country.errors %}
{% for error in form.country.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.country.help_text %}
<div class="form-text">{{ form.country.help_text }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.city.id_for_label }}" class="form-label">
{{ form.city.label }}
</label>
{{ form.city|add_class:"form-control" }}
{% if form.city.errors %}
{% for error in form.city.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.city.help_text %}
<div class="form-text">{{ form.city.help_text }}</div>
{% endif %}
</div>
</div>
<!-- Description -->
<div class="mb-4">
<label for="{{ form.description.id_for_label }}" class="form-label">
{{ form.description.label }}
</label>
{{ form.description|add_class:"form-control" }}
{% if form.description.errors %}
{% for error in form.description.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% endif %}
{% if form.description.help_text %}
<div class="form-text">{{ form.description.help_text }}</div>
{% endif %}
</div>
<div class="d-flex gap-2">
<button form="agency-form" type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {{ button_text }}
</button>
</div>
</form>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %}
{% block customJS %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Add Bootstrap classes to form fields // Form Validation
const formFields = document.querySelectorAll('input[type="text"], input[type="email"], input[type="url"], input[type="tel"], textarea, select'); const form = document.getElementById('agency-form');
formFields.forEach(function(field) { if (form) {
field.classList.add('form-control'); form.addEventListener('submit', function(e) {
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Basic validation
const name = document.getElementById('id_name');
if (name && !name.value.trim()) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "Agency name is required." %}');
return;
}
const email = document.getElementById('id_email');
if (email && email.value.trim() && !isValidEmail(email.value.trim())) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "Please enter a valid email address." %}');
return;
}
const website = document.getElementById('id_website');
if (website && website.value.trim() && !isValidURL(website.value.trim())) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "Please enter a valid website URL." %}');
return;
}
});
}
// Email validation helper
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// URL validation helper
function isValidURL(url) {
try {
new URL(url);
return true;
} catch (_) {
return false;
}
}
// Warn before leaving if changes are made
let formChanged = false;
const formInputs = form ? form.querySelectorAll('input, select, textarea') : [];
formInputs.forEach(input => {
input.addEventListener('change', function() {
formChanged = true;
});
}); });
window.addEventListener('beforeunload', function(e) {
if (formChanged) {
e.preventDefault();
e.returnValue = '{% trans "You have unsaved changes. Are you sure you want to leave?" %}';
return e.returnValue;
}
});
if (form) {
form.addEventListener('submit', function() {
formChanged = false;
});
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -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" %}
@ -170,8 +170,8 @@
{% 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">{{ assignment.max_candidates }} {% 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-info">{{ total_applications }}/{{ assignment.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-info">{{ 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,19 +348,19 @@
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" style="width: {{ progress }}%"></div>
</div> </div>
@ -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>
@ -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

@ -1,43 +1,51 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static i18n crispy_forms_tags %} {% load static i18n crispy_forms_tags %}
{% block title %}Update Candidate - {{ block.super }}{% endblock %} {% block title %}Update {{ object.name }} - {{ block.super }}{% endblock %}
{% block customCSS %} {% block customCSS %}
<style> <style>
/* ================================================= */ /* UI Variables for the KAAT-S Theme */
/* THEME VARIABLES AND GLOBAL STYLES (FROM JOB DETAIL) */
/* ================================================= */
:root { :root {
--kaauh-teal: #00636e; --kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53; --kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3; --kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40; --kaauh-gray-light: #f8f9fa;
} }
/* Primary Color Overrides */ /* Form Container Styling */
.text-primary { color: var(--kaauh-teal) !important; } .form-container {
max-width: 800px;
margin: 0 auto;
}
/* Card Styling */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
/* Main Action Button Style */ /* Main Action Button Style */
.btn-main-action, .btn-primary { .btn-main-action {
background-color: var(--kaauh-teal); background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal); border-color: var(--kaauh-teal);
color: white; color: white;
font-weight: 600; font-weight: 600;
padding: 0.6rem 1.2rem;
transition: all 0.2s ease; transition: all 0.2s ease;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.4rem;
padding: 0.5rem 1.5rem;
} }
.btn-main-action:hover, .btn-primary:hover {
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark); background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark); border-color: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15); box-shadow: 0 4px 8px rgba(0,0,0,0.15);
} }
/* Outlined Button Styles */ /* Secondary Button Style */
.btn-outline-secondary { .btn-outline-secondary {
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal); border-color: var(--kaauh-teal);
@ -48,94 +56,308 @@
border-color: var(--kaauh-teal-dark); border-color: var(--kaauh-teal-dark);
} }
/* Card enhancements */ /* Form Field Styling */
.card { .form-control:focus {
border: 1px solid var(--kaauh-border); border-color: var(--kaauh-teal);
border-radius: 0.75rem; box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
} }
/* Colored Header Card */ .form-select:focus {
.candidate-header-card { border-color: var(--kaauh-teal);
background: linear-gradient(135deg, var(--kaauh-teal), #004d57); box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
color: white;
border-radius: 0.75rem 0.75rem 0 0;
padding: 1.5rem;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
} }
.candidate-header-card h1 {
font-weight: 700; /* Profile Image Upload Styling */
margin: 0; .profile-image-upload {
font-size: 1.8rem; border: 2px dashed var(--kaauh-border);
border-radius: 0.5rem;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
} }
.heroicon {
width: 1.25rem; .profile-image-upload:hover {
height: 1.25rem; border-color: var(--kaauh-teal);
vertical-align: text-bottom; background-color: var(--kaauh-gray-light);
stroke: currentColor; }
margin-right: 0.5rem;
.profile-image-preview {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 50%;
border: 3px solid var(--kaauh-teal);
margin: 0 auto 1rem;
}
.current-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
border: 2px solid var(--kaauh-teal);
margin-right: 1rem;
}
/* Breadcrumb Styling */
.breadcrumb {
background-color: transparent;
padding: 0;
margin-bottom: 1rem;
}
.breadcrumb-item + .breadcrumb-item::before {
content: ">";
color: var(--kaauh-teal);
}
/* Alert Styling */
.alert {
border-radius: 0.5rem;
border: none;
}
/* Loading State */
.btn.loading {
position: relative;
pointer-events: none;
opacity: 0.8;
}
.btn.loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
margin: auto;
border: 2px solid transparent;
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Current Profile Section */
.current-profile {
background-color: var(--kaauh-gray-light);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
}
.current-profile h6 {
color: var(--kaauh-teal-dark);
font-weight: 600;
margin-bottom: 0.75rem;
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<div class="form-container">
<div class="card mb-4"> <!-- Breadcrumb Navigation -->
<div class="candidate-header-card"> <nav aria-label="breadcrumb">
<div class="d-flex justify-content-between align-items-start flex-wrap"> <ol class="breadcrumb">
<div class="flex-grow-1"> <li class="breadcrumb-item">
<h1 class="h3 mb-1"> <a href="{% url 'application_list' %}" class="text-decoration-none text-secondary">
<i class="fas fa-user-edit"></i> <i class="fas fa-users me-1"></i> {% trans "Applications" %}
{% trans "Update Candidate:" %} {{ object.name }}
</h1>
<p class="text-white opacity-75 mb-0">{% trans "Edit candidate information and details" %}</p>
</div>
<div class="d-flex gap-2 mt-1">
<a href="{% url 'application_list' %}" class="btn btn-outline-light btn-sm" title="{% trans 'Back to List' %}">
<i class="fas fa-arrow-left"></i>
<span class="d-none d-sm-inline">{% trans "Back to List" %}</span>
</a> </a>
{% if object.slug %} </li>
<a href="{% url 'application_detail' object.slug %}" class="btn btn-outline-light btn-sm" title="{% trans 'View Candidate' %}"> <li class="breadcrumb-item">
<i class="fas fa-eye"></i> <a href="{% url 'application_detail' object.slug %}" class="text-decoration-none text-secondary">
<span class="d-none d-sm-inline">{% trans "View" %}</span> {{ object.name }}
</a> </a>
{% endif %} </li>
<li class="breadcrumb-item active" aria-current="page"
style="
color: #F43B5E; /* Rosy Accent Color */
font-weight: 600;">{% trans "Update" %}</li>
</ol>
</nav>
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 style="color: var(--kaauh-teal-dark);">
<i class="fas fa-user-edit me-2"></i> {% trans "Update Application" %}
</h3>
<div class="d-flex gap-2">
<a href="{% url 'application_detail' object.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
</a>
<a href="{% url 'application_delete' object.slug %}" class="btn btn-danger">
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
</a>
<a href="{% url 'application_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
</a>
</div>
</div>
<!-- Current Profile Info -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="current-profile">
<h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6>
<div class="d-flex align-items-center">
{% if object.profile_image %}
<img src="{{ object.profile_image.url }}" alt="{{ object.name }}"
class="current-image">
{% else %}
<div class="current-image d-flex align-items-center justify-content-center bg-light">
<i class="fas fa-user text-muted"></i>
</div>
{% endif %}
<div>
<h5 class="mb-1">{{ object.name }}</h5>
{% if object.email %}
<p class="text-muted mb-0">{{ object.email }}</p>
{% endif %}
<small class="text-muted">
{% trans "Created" %}: {{ object.created_at|date:"d M Y" }} •
{% trans "Last Updated" %}: {{ object.updated_at|date:"d M Y" }}
</small>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="card shadow-sm"> <!-- Form Card -->
<div class="card-header bg-white border-bottom"> <div class="card shadow-sm">
<h2 class="h5 mb-0 text-primary"> <div class="card-body p-4">
<i class="fas fa-file-alt me-1"></i> {% if form.non_field_errors %}
{% trans "Candidate Form" %} <div class="alert alert-danger" role="alert">
</h2> <h5 class="alert-heading">
</div> <i class="fas fa-exclamation-triangle me-2"></i>{% trans "Error" %}
<div class="card-body"> </h5>
<form method="post" enctype="multipart/form-data"> {% for error in form.non_field_errors %}
{% csrf_token %} <p class="mb-0">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
{# Use Crispy Forms to render fields. The two-column layout is applied to the main form content #} {% if messages %}
<div class="row g-4"> {% for message in messages %}
{% for field in form %} <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
<div class="col-md-6"> {{ message }}
{{ field|as_crispy_field }} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
</div> </div>
{% endfor %} {% endfor %}
</div> {% endif %}
<hr class="mt-4 mb-4"> <form method="post" action="{% url 'application_update' object.slug %}" enctype="multipart/form-data" id="candidate-form">
<button class="btn btn-main-action" type="submit"> {% csrf_token %}
<i class="fas fa-save me-1"></i> {{form|crispy}}
{% trans "Update Candidate" %} </form>
</button> <div class="d-flex gap-2">
</form> <button form="candidate-form" type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Update" %}
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Profile Image Preview
const profileImageInput = document.getElementById('id_profile_image');
const imagePreviewContainer = document.getElementById('image-preview-container');
const originalImage = imagePreviewContainer ? imagePreviewContainer.innerHTML : '';
if (profileImageInput) {
profileImageInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
if (imagePreviewContainer) {
imagePreviewContainer.innerHTML = `
<img src="${e.target.result}" alt="Profile Preview" class="profile-image-preview">
<h5 class="text-muted mt-3">${file.name}</h5>
<p class="text-muted small">{% trans "New photo selected" %}</p>
`;
}
};
reader.readAsDataURL(file);
} else if (!file && imagePreviewContainer) {
// Reset to original if no file selected
imagePreviewContainer.innerHTML = originalImage;
}
});
}
// Form Validation
const form = document.getElementById('candidate-form');
if (form) {
form.addEventListener('submit', function(e) {
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Basic validation
const name = document.getElementById('id_name');
if (name && !name.value.trim()) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "Name is required." %}');
return;
}
const email = document.getElementById('id_email');
if (email && email.value.trim() && !isValidEmail(email.value.trim())) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "Please enter a valid email address." %}');
return;
}
});
}
// Email validation helper
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Warn before leaving if changes are made
let formChanged = false;
const formInputs = form ? form.querySelectorAll('input, select, textarea') : [];
formInputs.forEach(input => {
input.addEventListener('change', function() {
formChanged = true;
});
});
window.addEventListener('beforeunload', function(e) {
if (formChanged) {
e.preventDefault();
e.returnValue = '{% trans "You have unsaved changes. Are you sure you want to leave?" %}';
return e.returnValue;
}
});
if (form) {
form.addEventListener('submit', function() {
formChanged = false;
});
}
});
</script>
{% endblock %}

View File

@ -286,7 +286,7 @@
{# Separator (Vertical Rule) - Aligns automatically at the bottom with align-items-end #} {# Separator (Vertical Rule) - Aligns automatically at the bottom with align-items-end #}
<div class="vr" style="height: 28px;"></div> <div class="vr" style="height: 28px;"></div>
<button type="button" class="btn btn-outline-info btn-sm" <button type="button" class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"

View File

@ -229,7 +229,7 @@
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %} <i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button> </button>
<button type="button" class="btn btn-outline-info btn-sm" <button type="button" class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"

View File

@ -252,7 +252,7 @@
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %} <i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button> </button>
{# email button#} {# email button#}
<button type="button" class="btn btn-outline-info btn-sm" <button type="button" class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"

View File

@ -422,27 +422,37 @@
{% endif %} {% endif %}
{% else %} {% else %}
<button type="button" class="btn btn-main-action btn-sm" {% comment %} <a href="{% url 'interview_create_type_selection' candidate_slug=candidate.slug %}"
class="btn btn-main-action btn-sm"
title="Schedule Interview">
<i class="fas fa-calendar-plus me-1"></i>
Schedule
</a> {% endcomment %}
<button type="button" class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" data-bs-target="#candidateviewModal"
hx-get="{% url 'schedule_meeting_for_application' job.slug application.pk %}" hx-get="{% url 'interview_create_type_selection' application_slug=application.slug %}"
hx-target="#candidateviewModalBody" hx-select=".card-body"
data-modal-title="{% trans 'Schedule Interview' %}" hx-swap="innerHTML"
title="Schedule Interview"> hx-target="#candidateviewModalBody">
<i class="fas fa-video"></i> <i class="fas fa-calendar-plus me-1"></i>
</button> Schedule
<button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
{# UPDATED: Points to the specific Onsite scheduling URL #}
hx-get="{% url 'schedule_onsite_meeting_for_application' job.slug application.pk %}"
hx-target="#candidateviewModalBody"
data-modal-title="{% trans 'Schedule Onsite Interview' %}"
title="Schedule Onsite Interview">
<i class="fas fa-building"></i>
</button> </button>
{% comment %} <a href="{% url 'interview_create_type_selection' candidate_slug=candidate.slug %}"
class="btn btn-main-action btn-sm"
title="Schedule Interview">
<i class="fas fa-calendar-plus me-1"></i>
Schedule
</a> {% endcomment %}
{% endif %} {% endif %}
<button type="button" class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'get_interview_list' application.slug %}"
hx-target="#candidateviewModalBody">
<i class="fas fa-list"></i>
</button>
{{candidate.get_interviews}}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -463,7 +473,7 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);"> <div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);"> <h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Application Details / Bulk Action Form" %} {% comment %} {% trans "Candidate Details / Bulk Action Form" %} {% endcomment %}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
@ -476,11 +486,9 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Email Modal --> <!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true"> <div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">

View File

@ -335,7 +335,7 @@
<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> </button>
{% endif %} {% endif %}
@ -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 border-danger shadow-lg">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" 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-secondary" 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

@ -231,7 +231,7 @@
{# Separator (Vertical Rule) #} {# Separator (Vertical Rule) #}
<div class="vr" style="height: 28px;"></div> <div class="vr" style="height: 28px;"></div>
<button type="button" class="btn btn-outline-info btn-sm" <button type="button" class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"

View File

@ -344,7 +344,7 @@
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %} <i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button> </button>
{# email button#} {# email button#}
<button type="button" class="btn btn-outline-info btn-sm" <button type="button" class="btn btn-outline-primary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"

View File

@ -0,0 +1,386 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{% trans "Delete Candidate" %} - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* KAAT-S UI Variables */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Main Container & Card Styling */
.kaauh-card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Warning Section */
.warning-section {
background: linear-gradient(135deg, #fff3cd 0%, #ffeeba 100%);
border: 1px solid #ffeeba;
border-radius: 0.75rem;
padding: 2rem;
margin-bottom: 2rem;
text-align: center;
}
.warning-icon {
font-size: 4rem;
color: var(--kaauh-warning);
margin-bottom: 1rem;
}
.warning-title {
color: #856404;
font-weight: 700;
margin-bottom: 1rem;
}
.warning-text {
color: #856404;
margin-bottom: 0;
}
/* Candidate Info Card */
.candidate-info {
background-color: #f8f9fa;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid var(--kaauh-border);
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e9ecef;
}
.info-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.info-icon {
width: 40px;
height: 40px;
background-color: var(--kaauh-teal);
color: white;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
flex-shrink: 0;
}
.info-content {
flex: 1;
}
.info-label {
font-weight: 600;
color: var(--kaauh-primary-text);
margin-bottom: 0.25rem;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-value {
color: #6c757d;
font-size: 1rem;
}
/* Button Styling */
.btn-danger {
background-color: var(--kaauh-danger);
border-color: var(--kaauh-danger);
color: white;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3);
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
color: white;
font-weight: 600;
}
/* Consequence List */
.consequence-list {
list-style: none;
padding: 0;
margin: 0;
}
.consequence-list li {
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
color: #6c757d;
}
.consequence-list li:last-child {
border-bottom: none;
}
.consequence-list li i {
color: var(--kaauh-danger);
margin-right: 0.5rem;
}
/* Candidate Profile Image */
.candidate-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--kaauh-teal);
}
.avatar-placeholder {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid var(--kaauh-teal);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Header Section -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Delete Candidate" %}
</h1>
<p class="text-muted mb-0">
{% trans "You are about to delete a candidate application. This action cannot be undone." %}
</p>
</div>
<a href="{% url 'candidate_detail' object.slug %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Candidate" %}
</a>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<!-- Warning Section -->
<div class="warning-section">
<div class="warning-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h3 class="warning-title">{% trans "Warning: This action cannot be undone!" %}</h3>
<p class="warning-text">
{% trans "Deleting this candidate will permanently remove all associated data. Please review the information below carefully before proceeding." %}
</p>
</div>
<!-- Candidate Information -->
<div class="card kaauh-card mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-user me-2"></i>
{% trans "Candidate to be Deleted" %}
</h5>
</div>
<div class="card-body">
<div class="candidate-info">
<div class="d-flex align-items-center mb-4">
{% if object.profile_image %}
<img src="{{ object.profile_image.url }}" alt="{{ object.name }}" class="candidate-avatar me-3">
{% else %}
<div class="avatar-placeholder me-3">
<i class="fas fa-user text-muted fa-2x"></i>
</div>
{% endif %}
<div>
<h4 class="mb-1">{{ object.name }}</h4>
{% if object.email %}
<p class="text-muted mb-0">{{ object.email }}</p>
{% endif %}
</div>
</div>
<div class="info-item">
<div class="info-icon">
<i class="fas fa-briefcase"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Position Applied" %}</div>
<div class="info-value">
{% if object.job_posting %}
{{ object.job_posting.title }}
{% else %}
{% trans "Not specified" %}
{% endif %}
</div>
</div>
</div>
{% if object.phone %}
<div class="info-item">
<div class="info-icon">
<i class="fas fa-phone"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Phone" %}</div>
<div class="info-value">{{ object.phone }}</div>
</div>
</div>
{% endif %}
<div class="info-item">
<div class="info-icon">
<i class="fas fa-calendar"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Applied On" %}</div>
<div class="info-value">{{ object.created_at|date:"F d, Y" }}</div>
</div>
</div>
<div class="info-item">
<div class="info-icon">
<i class="fas fa-info-circle"></i>
</div>
<div class="info-content">
<div class="info-label">{% trans "Status" %}</div>
<div class="info-value">
{% if object.status %}
<span class="badge bg-info">{{ object.get_status_display }}</span>
{% else %}
{% trans "Not specified" %}
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Consequences -->
<div class="card kaauh-card mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-list me-2"></i>
{% trans "What will happen when you delete this candidate?" %}
</h5>
</div>
<div class="card-body">
<ul class="consequence-list">
<li>
<i class="fas fa-times-circle"></i>
{% trans "The candidate profile and all personal information will be permanently deleted" %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "All application data and documents will be removed" %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "Interview schedules and history will be deleted" %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "Any associated notes and communications will be lost" %}
</li>
<li>
<i class="fas fa-times-circle"></i>
{% trans "This action cannot be undone under any circumstances" %}
</li>
</ul>
</div>
</div>
<!-- Confirmation Form -->
<div class="card kaauh-card">
<div class="card-body">
<form method="post" id="deleteForm">
{% csrf_token %}
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirm_delete" name="confirm_delete" required>
<label class="form-check-label" for="confirm_delete">
<strong>{% trans "I understand that this action cannot be undone and I want to permanently delete this candidate." %}</strong>
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'candidate_detail' object.slug %}" class="btn btn-secondary btn-lg">
<i class="fas fa-times me-2"></i>
{% trans "Cancel" %}
</a>
<button type="submit"
class="btn btn-danger btn-lg"
id="deleteButton"
disabled>
<i class="fas fa-trash me-2"></i>
{% trans "Delete Candidate Permanently" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const confirmDeleteCheckbox = document.getElementById('confirm_delete');
const deleteButton = document.getElementById('deleteButton');
const deleteForm = document.getElementById('deleteForm');
function validateForm() {
const checkboxChecked = confirmDeleteCheckbox.checked;
deleteButton.disabled = !checkboxChecked;
if (checkboxChecked) {
deleteButton.classList.remove('btn-secondary');
deleteButton.classList.add('btn-danger');
} else {
deleteButton.classList.remove('btn-danger');
deleteButton.classList.add('btn-secondary');
}
}
confirmDeleteCheckbox.addEventListener('change', validateForm);
// Add confirmation before final submission
/*deleteForm.addEventListener('submit', function(e) {
const confirmMessage = "{% trans 'Are you absolutely sure you want to delete this candidate? This action cannot be undone.' %}";
if (!confirm(confirmMessage)) {
e.preventDefault();
}
});
*/
});
</script>
{% endblock %}

View File

@ -2,196 +2,409 @@
{% load static i18n %} {% load static i18n %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }} - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* UI Variables for the KAAT-S Theme */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-gray-light: #f8f9fa;
}
/* Form Container Styling */
.form-container {
max-width: 800px;
margin: 0 auto;
}
/* Card Styling */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
/* Main Action Button Style */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1.5rem;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Secondary Button Style */
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Form Field Styling */
.form-control:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.form-select:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
/* Breadcrumb Styling */
.breadcrumb {
background-color: transparent;
padding: 0;
margin-bottom: 1rem;
}
.breadcrumb-item + .breadcrumb-item::before {
content: ">";
color: var(--kaauh-teal);
}
/* Alert Styling */
.alert {
border-radius: 0.5rem;
border: none;
}
/* Loading State */
.btn.loading {
position: relative;
pointer-events: none;
opacity: 0.8;
}
.btn.loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
margin: auto;
border: 2px solid transparent;
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Current Profile Section */
.current-profile {
background-color: var(--kaauh-gray-light);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
}
.current-profile h6 {
color: var(--kaauh-teal-dark);
font-weight: 600;
margin-bottom: 0.75rem;
}
.current-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
border: 2px solid var(--kaauh-teal);
margin-right: 1rem;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid py-4">
<div class="row"> <div class="form-container">
<div class="col-12"> <!-- Breadcrumb Navigation -->
<div class="d-flex justify-content-between align-items-center mb-4"> <nav aria-label="breadcrumb">
<h1 class="h3 mb-0">{{ title }}</h1> <ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'source_list' %}" class="text-decoration-none text-secondary">
<i class="fas fa-plug me-1"></i> {% trans "Sources" %}
</a>
</li>
{% if source %}
<li class="breadcrumb-item">
<a href="{% url 'source_detail' source.pk %}" class="text-decoration-none text-secondary">
{{ source.name }}
</a>
</li>
<li class="breadcrumb-item active" aria-current="page"
style="
color: #F43B5E; /* Rosy Accent Color */
font-weight: 600;">{% trans "Update" %}</li>
{% else %}
<li class="breadcrumb-item active" aria-current="page"
style="
color: #F43B5E; /* Rosy Accent Color */
font-weight: 600;">{% trans "Create" %}</li>
{% endif %}
</ol>
</nav>
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-plug me-2"></i> {{ title }}
</h1>
<div class="d-flex gap-2">
{% if source %}
<a href="{% url 'source_detail' source.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
</a>
<a href="{% url 'source_delete' source.pk %}" class="btn btn-danger">
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
</a>
{% endif %}
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary"> <a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Back to Sources" %} <i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
</a> </a>
</div> </div>
</div>
<div class="card"> {% if source %}
<div class="card-body"> <!-- Current Source Info -->
<form method="post" novalidate> <div class="card shadow-sm mb-4">
{% csrf_token %} <div class="card-body">
<div class="current-profile">
<h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6>
<div class="d-flex align-items-center">
<div class="current-image d-flex align-items-center justify-content-center bg-light">
<i class="fas fa-plug text-muted"></i>
</div>
<div>
<h5 class="mb-1">{{ source.name }}</h5>
{% if source.source_type %}
<p class="text-muted mb-0">{% trans "Type" %}: {{ source.get_source_type_display }}</p>
{% endif %}
{% if source.ip_address %}
<p class="text-muted mb-0">{% trans "IP Address" %}: {{ source.ip_address }}</p>
{% endif %}
<small class="text-muted">
{% trans "Created" %}: {{ source.created_at|date:"d M Y" }} •
{% trans "Last Updated" %}: {{ source.updated_at|date:"d M Y" }}
</small>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if form.non_field_errors %} <!-- Form Card -->
<div class="alert alert-danger"> <div class="card shadow-sm">
{% for error in form.non_field_errors %} <div class="card-body p-4">
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading">
<i class="fas fa-exclamation-triangle me-2"></i>{% trans "Error" %}
</h5>
{% for error in form.non_field_errors %}
<p class="mb-0">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" novalidate id="source-form">
{% csrf_token %}
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label">
{{ form.name.label }} <span class="text-danger">*</span>
</label>
{{ form.name|add_class:"form-control" }}
{% if form.name.errors %}
<div class="invalid-feedback d-block">
{% for error in form.name.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.name.help_text }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.source_type.id_for_label }}" class="form-label">
{{ form.source_type.label }} <span class="text-danger">*</span>
</label>
{{ form.source_type|add_class:"form-select" }}
{% if form.source_type.errors %}
<div class="invalid-feedback d-block">
{% for error in form.source_type.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.source_type.help_text }}</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
{{ form.ip_address.label }} <span class="text-danger">*</span>
</label>
{{ form.ip_address|add_class:"form-control" }}
{% if form.ip_address.errors %}
<div class="invalid-feedback d-block">
{% for error in form.ip_address.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.ip_address.help_text }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.trusted_ips.id_for_label }}" class="form-label">
{{ form.trusted_ips.label }}
</label>
{{ form.trusted_ips|add_class:"form-control" }}
{% if form.trusted_ips.errors %}
<div class="invalid-feedback d-block">
{% for error in form.trusted_ips.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.trusted_ips.help_text }}</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">
{{ form.description.label }}
</label>
{{ form.description|add_class:"form-control" }}
{% if form.description.errors %}
<div class="invalid-feedback d-block">
{% for error in form.description.errors %}
{{ error }} {{ error }}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
<div class="form-text">{{ form.description.help_text }}</div>
</div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label"> <div class="form-check">
{{ form.name.label }} <span class="text-danger">*</span> {{ form.is_active|add_class:"form-check-input" }}
<label for="{{ form.is_active.id_for_label }}" class="form-check-label">
{{ form.is_active.label }}
</label> </label>
{{ form.name|add_class:"form-control" }}
{% if form.name.errors %}
<div class="invalid-feedback d-block">
{% for error in form.name.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.name.help_text }}</div>
</div> </div>
</div> {% if form.is_active.errors %}
<div class="col-md-6"> <div class="invalid-feedback d-block">
<div class="mb-3"> {% for error in form.is_active.errors %}
<label for="{{ form.source_type.id_for_label }}" class="form-label"> {{ error }}
{{ form.source_type.label }} <span class="text-danger">*</span> {% endfor %}
</label>
{{ form.source_type }}
{% if form.source_type.errors %}
<div class="invalid-feedback d-block">
{% for error in form.source_type.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.source_type.help_text }}</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
{{ form.ip_address.label }} <span class="text-danger">*</span>
</label>
{{ form.ip_address|add_class:"form-control" }}
{% if form.ip_address.errors %}
<div class="invalid-feedback d-block">
{% for error in form.ip_address.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.ip_address.help_text }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
{{ form.trusted_ips.label }} <span class="text-danger">*</span>
</label>
{{ form.trusted_ips|add_class:"form-control" }}
{% if form.trusted_ips.errors %}
<div class="invalid-feedback d-block">
{% for error in form.trusted_ips.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.trusted_ips.help_text }}</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">
{{ form.description.label }}
</label>
{{ form.description|add_class:"form-control" }}
{% if form.description.errors %}
<div class="invalid-feedback d-block">
{% for error in form.description.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.description.help_text }}</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
{{ form.is_active|add_class:"form-check-input bg-primary-theme" }}
<label for="{{ form.is_active.id_for_label }}" class="form-check-label">
{{ form.is_active.label }}
</label>
</div> </div>
{% if form.is_active.errors %} {% endif %}
<div class="invalid-feedback d-block"> <div class="form-text">{{ form.is_active.help_text }}</div>
{% for error in form.is_active.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.is_active.help_text }}</div>
</div>
</div> </div>
</div> </div>
</div>
<!-- API Credentials Section -->
<!-- API Credentials Section --> {% if source %}
{% if source %} <div class="card bg-light mb-4">
<div class="card bg-light mb-4"> <div class="card-header">
<div class="card-header"> <h6 class="mb-0">{% trans "API Credentials" %}</h6>
<h6 class="mb-0">{% trans "API Credentials" %}</h6> </div>
</div> <div class="card-body">
<div class="card-body"> <div class="row">
<div class="row"> <div class="col-md-6">
<div class="col-md-6"> <div class="mb-3">
<div class="mb-3"> <label class="form-label">{% trans "API Key" %}</label>
<label class="form-label">{% trans "API Key" %}</label> <div class="input-group">
<div class="input-group"> <input type="text" class="form-control" value="{{ source.api_key }}" readonly>
<input type="text" class="form-control" value="{{ source.api_key }}" readonly> <button type="button" class="btn btn-outline-secondary"
<button type="button" class="btn btn-outline-secondary" hx-post="{% url 'copy_to_clipboard' %}"
hx-post="{% url 'copy_to_clipboard' %}" hx-vals='{"text": "{{ source.api_key }}"}'
hx-vals='{"text": "{{ source.api_key }}"}' title="{% trans 'Copy to clipboard' %}">
title="{% trans 'Copy to clipboard' %}"> <i class="fas fa-copy"></i>
<i class="fas fa-copy"></i> </button>
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">{% trans "API Secret" %}</label>
<div class="input-group">
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
<i class="fas fa-eye" id="secret-toggle-icon"></i>
</button>
<button type="button" class="btn btn-outline-secondary"
hx-post="{% url 'copy_to_clipboard' %}"
hx-vals='{"text": "{{ source.api_secret }}"}'
title="{% trans 'Copy to clipboard' %}">
<i class="fas fa-copy"></i>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="text-end"> <div class="col-md-6">
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning"> <div class="mb-3">
<i class="fas fa-key"></i> {% trans "Generate New Keys" %} <label class="form-label">{% trans "API Secret" %}</label>
</a> <div class="input-group">
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
<i class="fas fa-eye" id="secret-toggle-icon"></i>
</button>
<button type="button" class="btn btn-outline-secondary"
hx-post="{% url 'copy_to_clipboard' %}"
hx-vals='{"text": "{{ source.api_secret }}"}'
title="{% trans 'Copy to clipboard' %}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div> </div>
</div> </div>
<div class="text-end">
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
<i class="fas fa-key"></i> {% trans "Generate New Keys" %}
</a>
</div>
</div> </div>
{% endif %}
<div class="d-flex justify-content-between">
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Save" %}
</button>
</div> </div>
</form> {% endif %}
</div>
<div class="d-flex gap-2">
<button form="source-form" type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Save" %}
</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
@ -200,6 +413,67 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() {
// Form Validation
const form = document.getElementById('source-form');
if (form) {
form.addEventListener('submit', function(e) {
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Basic validation
const name = document.getElementById('id_name');
if (name && !name.value.trim()) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "Source name is required." %}');
return;
}
const ipAddress = document.getElementById('id_ip_address');
if (ipAddress && ipAddress.value.trim() && !isValidIP(ipAddress.value.trim())) {
e.preventDefault();
submitBtn.classList.remove('loading');
submitBtn.disabled = false;
alert('{% trans "Please enter a valid IP address." %}');
return;
}
});
}
// IP validation helper
function isValidIP(ip) {
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
return ipRegex.test(ip);
}
// Warn before leaving if changes are made
let formChanged = false;
const formInputs = form ? form.querySelectorAll('input, select, textarea') : [];
formInputs.forEach(input => {
input.addEventListener('change', function() {
formChanged = true;
});
});
window.addEventListener('beforeunload', function(e) {
if (formChanged) {
e.preventDefault();
e.returnValue = '{% trans "You have unsaved changes. Are you sure you want to leave?" %}';
return e.returnValue;
}
});
if (form) {
form.addEventListener('submit', function() {
formChanged = false;
});
}
});
function toggleSecretVisibility() { function toggleSecretVisibility() {
const secretInput = document.getElementById('api-secret'); const secretInput = document.getElementById('api-secret');
const toggleIcon = document.getElementById('secret-toggle-icon'); const toggleIcon = document.getElementById('secret-toggle-icon');

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>