Compare commits
12 Commits
8bc3747afe
...
2486eb791e
| Author | SHA1 | Date | |
|---|---|---|---|
| 2486eb791e | |||
| 8b51416e10 | |||
| 1ad85b82e1 | |||
| 473dc8e00f | |||
| 45c6d2c1a5 | |||
| 5b114b630e | |||
| 6961a27985 | |||
| 4e40d56412 | |||
| 9bf0125121 | |||
| 5983dc75ff | |||
| 3b26ed34fd | |||
| a8fae9011c |
@ -25,8 +25,8 @@ urlpatterns = [
|
||||
|
||||
path('application/<slug:template_slug>/', views.application_submit_form, name='application_submit_form'),
|
||||
path('application/<slug:template_slug>/submit/', views.application_submit, name='application_submit'),
|
||||
path('application/<slug:slug>/apply/', views.application_detail, name='application_detail'),
|
||||
path('application/<slug:slug>/signup/', views.candidate_signup, name='candidate_signup'),
|
||||
path('application/<slug:slug>/apply/', views.job_application_detail, name='job_application_detail'),
|
||||
path('application/<slug:slug>/signup/', views.application_signup, name='application_signup'),
|
||||
path('application/<slug:slug>/success/', views.application_success, name='application_success'),
|
||||
# path('application/applicant/profile', views.applicant_profile, name='applicant_profile'),
|
||||
|
||||
|
||||
@ -79,10 +79,10 @@ def debug_url_routing():
|
||||
print(f"Error with document_upload URL: {e}")
|
||||
|
||||
try:
|
||||
url2 = reverse('candidate_document_upload', kwargs={'slug': application.slug})
|
||||
print(f"URL pattern 2 (candidate_document_upload): {url2}")
|
||||
url2 = reverse('pplication_document_upload', kwargs={'slug': application.slug})
|
||||
print(f"URL pattern 2 (pplication_document_upload): {url2}")
|
||||
except Exception as e:
|
||||
print(f"Error with candidate_document_upload URL: {e}")
|
||||
print(f"Error with pplication_document_upload URL: {e}")
|
||||
|
||||
# Test GET request to see if the URL is accessible
|
||||
try:
|
||||
|
||||
9882
django.po.bkp
Normal file
9882
django.po.bkp
Normal file
File diff suppressed because it is too large
Load Diff
9885
django2.po
Normal file
9885
django2.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -24,7 +24,7 @@ class CustomAuthenticationBackend(AuthenticationBackend):
|
||||
elif user.user_type == 'agency':
|
||||
redirect_url = reverse('agency_portal_dashboard')
|
||||
elif user.user_type == 'candidate':
|
||||
redirect_url = reverse('candidate_portal_dashboard')
|
||||
redirect_url = reverse('applicant_portal_dashboard')
|
||||
else:
|
||||
# Fallback to default redirect URL if user type is unknown
|
||||
redirect_url = '/'
|
||||
|
||||
@ -35,7 +35,7 @@ class CandidateSyncService:
|
||||
}
|
||||
|
||||
# Get all hired candidates for this job
|
||||
hired_candidates = list(job.hired_candidates.select_related('job'))
|
||||
hired_candidates = list(job.hired_applications.select_related('job'))
|
||||
|
||||
results['total_candidates'] = len(hired_candidates)
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ def user_type_required(allowed_types=None, login_url=None):
|
||||
if user.user_type == 'agency':
|
||||
return redirect('agency_portal_dashboard')
|
||||
elif user.user_type == 'candidate':
|
||||
return redirect('candidate_portal_dashboard')
|
||||
return redirect('applicant_portal_dashboard')
|
||||
else:
|
||||
return redirect('dashboard')
|
||||
|
||||
@ -92,7 +92,7 @@ class UserTypeRequiredMixin(AccessMixin):
|
||||
if request.user.user_type == 'agency':
|
||||
return redirect('agency_portal_dashboard')
|
||||
elif request.user.user_type == 'candidate':
|
||||
return redirect('candidate_portal_dashboard')
|
||||
return redirect('applicant_portal_dashboard')
|
||||
else:
|
||||
return redirect('dashboard')
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ class ERPIntegrationService:
|
||||
Validate the incoming request from ERP system
|
||||
Returns: (is_valid, error_message)
|
||||
"""
|
||||
|
||||
# Check if source is active
|
||||
if not self.source.is_active:
|
||||
return False, "Source is not active"
|
||||
@ -70,6 +71,7 @@ class ERPIntegrationService:
|
||||
try:
|
||||
# Map ERP fields to JobPosting fields
|
||||
job_data = {
|
||||
'internal_job_id': request_data.get('job_id', '').strip(),
|
||||
'title': request_data.get('title', '').strip(),
|
||||
'department': request_data.get('department', '').strip(),
|
||||
'job_type': self.map_job_type(request_data.get('job_type', 'FULL_TIME')),
|
||||
|
||||
@ -269,7 +269,7 @@ class SourceAdvancedForm(forms.ModelForm):
|
||||
class PersonForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","nationality","address","gender"]
|
||||
fields = ["first_name","middle_name", "last_name", "email", "phone","date_of_birth","nationality","gender","address"]
|
||||
widgets = {
|
||||
"first_name": forms.TextInput(attrs={'class': 'form-control'}),
|
||||
"middle_name": forms.TextInput(attrs={'class': 'form-control'}),
|
||||
@ -293,6 +293,7 @@ class ApplicationForm(forms.ModelForm):
|
||||
"resume",
|
||||
]
|
||||
labels = {
|
||||
"person":_("Applicant"),
|
||||
"resume": _("Resume"),
|
||||
"hiring_source": _("Hiring Type"),
|
||||
"hiring_agency": _("Hiring Agency"),
|
||||
@ -591,7 +592,6 @@ class JobPostingForm(forms.ModelForm):
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"min": 1,
|
||||
"placeholder": "Maximum number of applicants",
|
||||
}
|
||||
),
|
||||
}
|
||||
@ -834,9 +834,9 @@ class ProfileImageUploadForm(forms.ModelForm):
|
||||
|
||||
|
||||
class StaffUserCreationForm(UserCreationForm):
|
||||
email = forms.EmailField(required=True)
|
||||
first_name = forms.CharField(max_length=30, required=True)
|
||||
last_name = forms.CharField(max_length=150, required=True)
|
||||
email = forms.EmailField(label=_("Email"), required=True)
|
||||
first_name = forms.CharField(label=_("First Name"),max_length=30, required=True)
|
||||
last_name = forms.CharField(label=_("Last Name"),max_length=150, required=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
@ -904,7 +904,7 @@ class FormTemplateIsActiveForm(forms.ModelForm):
|
||||
fields = ["is_active"]
|
||||
|
||||
|
||||
class CandidateExamDateForm(forms.ModelForm):
|
||||
class ApplicationExamDateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Application
|
||||
fields = ["exam_date"]
|
||||
@ -1078,7 +1078,6 @@ class AgencyJobAssignmentForm(forms.ModelForm):
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"min": 1,
|
||||
"placeholder": "Maximum number of candidates",
|
||||
}
|
||||
),
|
||||
"deadline_date": forms.DateTimeInput(
|
||||
@ -1090,7 +1089,6 @@ class AgencyJobAssignmentForm(forms.ModelForm):
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"rows": 3,
|
||||
"placeholder": "Internal notes about this assignment",
|
||||
}
|
||||
),
|
||||
}
|
||||
@ -2347,7 +2345,7 @@ class MessageForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
|
||||
class CandidateSignupForm(forms.ModelForm):
|
||||
class ApplicantSignupForm(forms.ModelForm):
|
||||
password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'form-control'}))
|
||||
confirm_password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'form-control'}))
|
||||
|
||||
|
||||
18
recruitment/migrations/0006_alter_customuser_email.py
Normal file
18
recruitment/migrations/0006_alter_customuser_email.py
Normal file
@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
18
recruitment/migrations/0007_alter_person_email.py
Normal file
18
recruitment/migrations/0007_alter_person_email.py
Normal file
@ -0,0 +1,18 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@ -47,6 +47,12 @@ class CustomUser(AbstractUser):
|
||||
designation = models.CharField(
|
||||
max_length=100, blank=True, null=True, verbose_name=_("Designation")
|
||||
)
|
||||
email = models.EmailField(
|
||||
unique=True,
|
||||
error_messages={
|
||||
"unique": _("A user with this email already exists."),
|
||||
},
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User")
|
||||
@ -126,7 +132,7 @@ class JobPosting(Base):
|
||||
# Application Information ---job detail apply link for the candidates
|
||||
application_url = models.URLField(
|
||||
validators=[URLValidator()],
|
||||
help_text="URL where candidates apply",
|
||||
help_text="URL where applicants apply",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
@ -216,7 +222,7 @@ class JobPosting(Base):
|
||||
related_name="jobs",
|
||||
verbose_name=_("Hiring Agency"),
|
||||
help_text=_(
|
||||
"External agency responsible for sourcing candidates for this role"
|
||||
"External agency responsible for sourcing applicants for this role"
|
||||
),
|
||||
)
|
||||
cancel_reason = models.TextField(
|
||||
@ -247,7 +253,7 @@ class JobPosting(Base):
|
||||
)
|
||||
# Field to store the generated zip file
|
||||
cv_zip_file = models.FileField(upload_to='job_zips/', null=True, blank=True)
|
||||
|
||||
|
||||
# Field to track if the background task has completed
|
||||
zip_created = models.BooleanField(default=False)
|
||||
|
||||
@ -359,7 +365,7 @@ class JobPosting(Base):
|
||||
|
||||
@property
|
||||
def current_applications_count(self):
|
||||
"""Returns the current number of candidates associated with this job."""
|
||||
"""Returns the current number of applications associated with this job."""
|
||||
return self.applications.count()
|
||||
|
||||
@property
|
||||
@ -371,7 +377,7 @@ class JobPosting(Base):
|
||||
return self.current_applications_count >= self.max_applications
|
||||
|
||||
@property
|
||||
def all_candidates(self):
|
||||
def all_applications(self):
|
||||
# 1. Define the safe JSON extraction and conversion expression
|
||||
safe_score_expression = Cast(
|
||||
Coalesce(
|
||||
@ -391,68 +397,70 @@ class JobPosting(Base):
|
||||
).order_by("-sortable_score")
|
||||
|
||||
@property
|
||||
def screening_candidates(self):
|
||||
return self.all_candidates.filter(stage="Applied")
|
||||
def screening_applications(self):
|
||||
return self.all_applications.filter(stage="Applied")
|
||||
|
||||
@property
|
||||
def exam_candidates(self):
|
||||
return self.all_candidates.filter(stage="Exam")
|
||||
def exam_applications(self):
|
||||
return self.all_applications.filter(stage="Exam")
|
||||
|
||||
@property
|
||||
def interview_candidates(self):
|
||||
return self.all_candidates.filter(stage="Interview")
|
||||
def interview_applications(self):
|
||||
return self.all_applications.filter(stage="Interview")
|
||||
|
||||
@property
|
||||
def document_review_candidates(self):
|
||||
return self.all_candidates.filter(stage="Document Review")
|
||||
def document_review_applications(self):
|
||||
return self.all_applications.filter(stage="Document Review")
|
||||
|
||||
@property
|
||||
def offer_candidates(self):
|
||||
return self.all_candidates.filter(stage="Offer")
|
||||
def offer_applications(self):
|
||||
return self.all_applications.filter(stage="Offer")
|
||||
|
||||
@property
|
||||
def accepted_candidates(self):
|
||||
return self.all_candidates.filter(offer_status="Accepted")
|
||||
def accepted_applications(self):
|
||||
return self.all_applications.filter(offer_status="Accepted")
|
||||
|
||||
@property
|
||||
def hired_candidates(self):
|
||||
return self.all_candidates.filter(stage="Hired")
|
||||
def hired_applications(self):
|
||||
return self.all_applications.filter(stage="Hired")
|
||||
|
||||
# counts
|
||||
@property
|
||||
def all_candidates_count(self):
|
||||
return self.all_candidates.count()
|
||||
def all_applications_count(self):
|
||||
return self.all_applications.count()
|
||||
|
||||
|
||||
@property
|
||||
def screening_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Applied").count() or 0
|
||||
def screening_applications_count(self):
|
||||
return self.all_applications.filter(stage="Applied").count() or 0
|
||||
|
||||
@property
|
||||
def exam_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Exam").count() or 0
|
||||
def exam_applications_count(self):
|
||||
return self.all_applications.filter(stage="Exam").count() or 0
|
||||
|
||||
@property
|
||||
def interview_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Interview").count() or 0
|
||||
def interview_applications_count(self):
|
||||
return self.all_applications.filter(stage="Interview").count() or 0
|
||||
|
||||
@property
|
||||
def document_review_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Document Review").count() or 0
|
||||
def document_review_applications_count(self):
|
||||
return self.all_applications.filter(stage="Document Review").count() or 0
|
||||
|
||||
@property
|
||||
def offer_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Offer").count() or 0
|
||||
def offer_applications_count(self):
|
||||
return self.all_applications.filter(stage="Offer").count() or 0
|
||||
|
||||
@property
|
||||
def hired_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Hired").count() or 0
|
||||
def hired_applications_count(self):
|
||||
return self.all_applications.filter(stage="Hired").count() or 0
|
||||
|
||||
@property
|
||||
def vacancy_fill_rate(self):
|
||||
total_positions = self.open_positions
|
||||
print(total_positions)
|
||||
|
||||
no_of_positions_filled = self.applications.filter(stage__in=["HIRED"]).count()
|
||||
no_of_positions_filled = self.applications.filter(stage__in=["Hired"]).count()
|
||||
print(no_of_positions_filled)
|
||||
|
||||
if total_positions > 0:
|
||||
vacancy_fill_rate = no_of_positions_filled / total_positions
|
||||
@ -487,7 +495,6 @@ class Person(Base):
|
||||
unique=True,
|
||||
db_index=True,
|
||||
verbose_name=_("Email"),
|
||||
help_text=_("Unique email address for the person"),
|
||||
)
|
||||
phone = models.CharField(
|
||||
max_length=20, blank=True, null=True, verbose_name=_("Phone")
|
||||
@ -1068,14 +1075,14 @@ class Application(Base):
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.__class__)
|
||||
return Document.objects.filter(content_type=content_type, object_id=self.id)
|
||||
|
||||
|
||||
@property
|
||||
def belong_to_an_agency(self):
|
||||
if self.hiring_agency:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
deadline=self.job.application_deadline
|
||||
@ -1084,7 +1091,7 @@ class Application(Base):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1253,7 +1260,7 @@ class OnsiteLocationDetails(InterviewLocation):
|
||||
verbose_name_plural = _("Onsite Location Details")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# --- 2. Scheduling Models ---
|
||||
|
||||
@ -866,7 +866,7 @@ def sync_hired_candidates_task(job_slug):
|
||||
# action=IntegrationLog.ActionChoices.SYNC,
|
||||
# endpoint="multi_source_sync",
|
||||
# method="BACKGROUND_TASK",
|
||||
# request_data={"job_slug": job_slug, "candidate_count": job.accepted_candidates.count()},
|
||||
# request_data={"job_slug": job_slug, "candidate_count": job.accepted_applications.count()},
|
||||
# response_data=results,
|
||||
# status_code="SUCCESS" if results.get('summary', {}).get('failed', 0) == 0 else "PARTIAL",
|
||||
# ip_address="127.0.0.1", # Background task
|
||||
@ -984,7 +984,7 @@ from django.utils.html import strip_tags
|
||||
|
||||
def _task_send_individual_email(subject, body_message, recipient, attachments,sender,job):
|
||||
"""Internal helper to create and send a single email."""
|
||||
|
||||
|
||||
|
||||
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
|
||||
is_html = '<' in body_message and '>' in body_message
|
||||
@ -1006,7 +1006,7 @@ def _task_send_individual_email(subject, body_message, recipient, attachments,se
|
||||
result=email_obj.send(fail_silently=False)
|
||||
|
||||
if result==1 and sender and job: # job is none when email sent after message creation
|
||||
|
||||
|
||||
try:
|
||||
user=get_object_or_404(User,email=recipient)
|
||||
new_message = Message.objects.create(
|
||||
@ -1086,7 +1086,7 @@ def generate_and_save_cv_zip(job_posting_id):
|
||||
"""
|
||||
job = JobPosting.objects.get(id=job_posting_id)
|
||||
entries = Application.objects.filter(job=job)
|
||||
|
||||
|
||||
zip_buffer = io.BytesIO()
|
||||
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
@ -1112,7 +1112,7 @@ def generate_and_save_cv_zip(job_posting_id):
|
||||
zip_buffer.seek(0)
|
||||
now = str(timezone.now())
|
||||
zip_filename = f"all_cvs_for_{job.slug}_{job.title}_{now}.zip"
|
||||
|
||||
|
||||
# Use ContentFile to save the bytes stream into the FileField
|
||||
job.cv_zip_file.save(zip_filename, ContentFile(zip_buffer.read()))
|
||||
job.zip_created = True # Assuming you added a BooleanField for tracking completion
|
||||
|
||||
@ -19,11 +19,11 @@ from .forms import (
|
||||
CandidateStageForm, InterviewScheduleForm, CandidateSignupForm
|
||||
)
|
||||
from .views import (
|
||||
ZoomMeetingListView, ZoomMeetingCreateView, job_detail, candidate_screening_view,
|
||||
candidate_exam_view, candidate_interview_view, api_schedule_candidate_meeting
|
||||
ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view,
|
||||
applications_exam_view, applications_interview_view, api_schedule_application_meeting
|
||||
)
|
||||
from .views_frontend import CandidateListView, JobListView
|
||||
from .utils import create_zoom_meeting, get_candidates_from_request
|
||||
from .utils import create_zoom_meeting, get_applications_from_request
|
||||
|
||||
|
||||
class BaseTestCase(TestCase):
|
||||
@ -189,32 +189,32 @@ class ViewTests(BaseTestCase):
|
||||
|
||||
def test_candidate_screening_view(self):
|
||||
"""Test candidate_screening_view"""
|
||||
response = self.client.get(reverse('candidate_screening_view', kwargs={'slug': self.job.slug}))
|
||||
response = self.client.get(reverse('applications_screening_view', kwargs={'slug': self.job.slug}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'John Doe')
|
||||
|
||||
def test_candidate_screening_view_filters(self):
|
||||
"""Test candidate_screening_view with filters"""
|
||||
response = self.client.get(
|
||||
reverse('candidate_screening_view', kwargs={'slug': self.job.slug}),
|
||||
reverse('applications_screening_view', kwargs={'slug': self.job.slug}),
|
||||
{'min_ai_score': '50', 'tier1_count': '5'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_candidate_exam_view(self):
|
||||
"""Test candidate_exam_view"""
|
||||
response = self.client.get(reverse('candidate_exam_view', kwargs={'slug': self.job.slug}))
|
||||
response = self.client.get(reverse('applications_exam_view', kwargs={'slug': self.job.slug}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'John Doe')
|
||||
|
||||
def test_candidate_interview_view(self):
|
||||
"""Test candidate_interview_view"""
|
||||
response = self.client.get(reverse('candidate_interview_view', kwargs={'slug': self.job.slug}))
|
||||
"""Test applications_interview_view"""
|
||||
response = self.client.get(reverse('applications_interview_view', kwargs={'slug': self.job.slug}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@patch('recruitment.views.create_zoom_meeting')
|
||||
def test_schedule_candidate_meeting(self, mock_create_zoom):
|
||||
"""Test api_schedule_candidate_meeting view"""
|
||||
"""Test api_schedule_application_meeting view"""
|
||||
mock_create_zoom.return_value = {
|
||||
'status': 'success',
|
||||
'meeting_details': {
|
||||
@ -231,7 +231,7 @@ class ViewTests(BaseTestCase):
|
||||
'duration': 60
|
||||
}
|
||||
response = self.client.post(
|
||||
reverse('api_schedule_candidate_meeting',
|
||||
reverse('api_schedule_application_meeting',
|
||||
kwargs={'job_slug': self.job.slug, 'candidate_pk': self.candidate.pk}),
|
||||
data
|
||||
)
|
||||
@ -478,7 +478,7 @@ class PerformanceTests(BaseTestCase):
|
||||
)
|
||||
|
||||
# Test pagination
|
||||
response = self.client.get(reverse('candidate_list'))
|
||||
response = self.client.get(reverse('application_list'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Candidate')
|
||||
|
||||
@ -586,8 +586,8 @@ class UtilityFunctionTests(BaseTestCase):
|
||||
self.assertEqual(result['status'], 'success')
|
||||
self.assertIn('meeting_id', result['meeting_details'])
|
||||
|
||||
def test_get_candidates_from_request(self):
|
||||
"""Test the get_candidates_from_request utility function"""
|
||||
def get_applications_from_request(self):
|
||||
"""Test the get_applications_from_request utility function"""
|
||||
# This would be tested with a request that has candidate_ids
|
||||
pass
|
||||
|
||||
|
||||
@ -33,15 +33,15 @@ from .forms import (
|
||||
ApplicationStageForm, InterviewScheduleForm, BreakTimeFormSet
|
||||
)
|
||||
from .views import (
|
||||
ZoomMeetingListView, ZoomMeetingCreateView, job_detail, candidate_screening_view,
|
||||
candidate_exam_view, candidate_interview_view, api_schedule_candidate_meeting,
|
||||
ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view,
|
||||
applications_exam_view, applications_interview_view, api_schedule_application_meeting,
|
||||
schedule_interviews_view, confirm_schedule_interviews_view, _handle_preview_submission,
|
||||
_handle_confirm_schedule, _handle_get_request
|
||||
)
|
||||
# from .views_frontend import CandidateListView, JobListView, JobCreateView
|
||||
from .utils import (
|
||||
create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting,
|
||||
get_zoom_meeting_details, get_candidates_from_request,
|
||||
get_zoom_meeting_details, get_applications_from_request,
|
||||
get_available_time_slots
|
||||
)
|
||||
# from .zoom_api import ZoomAPIError
|
||||
@ -421,27 +421,27 @@ class AdvancedViewTests(TestCase):
|
||||
)
|
||||
|
||||
# Test search by name
|
||||
response = self.client.get(reverse('candidate_list'), {
|
||||
response = self.client.get(reverse('application_list'), {
|
||||
'search': 'Jane'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Jane Smith')
|
||||
|
||||
# Test search by email
|
||||
response = self.client.get(reverse('candidate_list'), {
|
||||
response = self.client.get(reverse('application_list'), {
|
||||
'search': 'bob@example.com'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Bob Johnson')
|
||||
|
||||
# Test filter by job
|
||||
response = self.client.get(reverse('candidate_list'), {
|
||||
response = self.client.get(reverse('application_list'), {
|
||||
'job': self.job.slug
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test filter by stage
|
||||
response = self.client.get(reverse('candidate_list'), {
|
||||
response = self.client.get(reverse('application_list'), {
|
||||
'stage': 'Exam'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@ -521,7 +521,7 @@ class AdvancedViewTests(TestCase):
|
||||
"""Test HTMX responses for partial updates"""
|
||||
# Test HTMX request for candidate screening
|
||||
response = self.client.get(
|
||||
reverse('candidate_screening_view', kwargs={'slug': self.job.slug}),
|
||||
reverse('applications_screening_view', kwargs={'slug': self.job.slug}),
|
||||
HTTP_HX_REQUEST='true'
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@ -557,7 +557,7 @@ class AdvancedViewTests(TestCase):
|
||||
# This would be tested via a form submission
|
||||
# For now, we test the view logic directly
|
||||
request = self.client.post(
|
||||
reverse('candidate_update_status', kwargs={'slug': self.job.slug}),
|
||||
reverse('application_update_status', kwargs={'slug': self.job.slug}),
|
||||
data={'candidate_ids': application_ids, 'mark_as': 'Exam'}
|
||||
)
|
||||
# Should redirect back to the view
|
||||
@ -954,7 +954,7 @@ class AdvancedIntegrationTests(TransactionTestCase):
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('api_schedule_candidate_meeting',
|
||||
reverse('api_schedule_application_meeting',
|
||||
kwargs={'job_slug': job.slug, 'candidate_pk': application.pk}),
|
||||
data={
|
||||
'start_time': (timezone.now() + timedelta(hours=1)).isoformat(),
|
||||
|
||||
@ -41,52 +41,52 @@ urlpatterns = [
|
||||
|
||||
# Candidate URLs
|
||||
path(
|
||||
"candidates/", views_frontend.ApplicationListView.as_view(), name="candidate_list"
|
||||
"applications/", views_frontend.ApplicationListView.as_view(), name="application_list"
|
||||
),
|
||||
path(
|
||||
"candidates/create/",
|
||||
"application/create/",
|
||||
views_frontend.ApplicationCreateView.as_view(),
|
||||
name="candidate_create",
|
||||
name="application_create",
|
||||
),
|
||||
path(
|
||||
"candidates/create/<slug:slug>/",
|
||||
"application/create/<slug:slug>/",
|
||||
views_frontend.ApplicationCreateView.as_view(),
|
||||
name="candidate_create_for_job",
|
||||
name="application_create_for_job",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/candidates/",
|
||||
"jobs/<slug:slug>/application/",
|
||||
views_frontend.JobApplicationListView.as_view(),
|
||||
name="job_candidates_list",
|
||||
name="job_applications_list",
|
||||
),
|
||||
path(
|
||||
"candidates/<slug:slug>/update/",
|
||||
"applications/<slug:slug>/update/",
|
||||
views_frontend.ApplicationUpdateView.as_view(),
|
||||
name="candidate_update",
|
||||
name="application_update",
|
||||
),
|
||||
path(
|
||||
"candidates/<slug:slug>/delete/",
|
||||
"application/<slug:slug>/delete/",
|
||||
views_frontend.ApplicationDeleteView.as_view(),
|
||||
name="candidate_delete",
|
||||
name="application_delete",
|
||||
),
|
||||
path(
|
||||
"candidate/<slug:slug>/view/",
|
||||
views_frontend.candidate_detail,
|
||||
name="candidate_detail",
|
||||
"application/<slug:slug>/view/",
|
||||
views_frontend.application_detail,
|
||||
name="application_detail",
|
||||
),
|
||||
path(
|
||||
"candidate/<slug:slug>/resume-template/",
|
||||
views_frontend.candidate_resume_template_view,
|
||||
name="candidate_resume_template",
|
||||
"application/<slug:slug>/resume-template/",
|
||||
views_frontend.application_resume_template_view,
|
||||
name="application_resume_template",
|
||||
),
|
||||
path(
|
||||
"candidate/<slug:slug>/update-stage/",
|
||||
views_frontend.candidate_update_stage,
|
||||
name="candidate_update_stage",
|
||||
"application/<slug:slug>/update-stage/",
|
||||
views_frontend.application_update_stage,
|
||||
name="application_update_stage",
|
||||
),
|
||||
path(
|
||||
"candidate/<slug:slug>/retry-scoring/",
|
||||
"application/<slug:slug>/retry-scoring/",
|
||||
views_frontend.retry_scoring_view,
|
||||
name="candidate_retry_scoring",
|
||||
name="application_retry_scoring",
|
||||
),
|
||||
# Training URLs
|
||||
path("training/", views_frontend.TrainingListView.as_view(), name="training_list"),
|
||||
@ -155,85 +155,87 @@ urlpatterns = [
|
||||
name="edit_linkedin_post_content",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/candidate_screening_view/",
|
||||
views.candidate_screening_view,
|
||||
name="candidate_screening_view",
|
||||
"jobs/<slug:slug>/applications_screening_view/",
|
||||
views.applications_screening_view,
|
||||
name="applications_screening_view",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/candidate_exam_view/",
|
||||
views.candidate_exam_view,
|
||||
name="candidate_exam_view",
|
||||
"jobs/<slug:slug>/applications_exam_view/",
|
||||
views.applications_exam_view,
|
||||
name="applications_exam_view",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/candidate_interview_view/",
|
||||
views.candidate_interview_view,
|
||||
name="candidate_interview_view",
|
||||
"jobs/<slug:slug>/applications_interview_view/",
|
||||
views.applications_interview_view,
|
||||
name="applications_interview_view",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/candidate_document_review_view/",
|
||||
views.candidate_document_review_view,
|
||||
name="candidate_document_review_view",
|
||||
"jobs/<slug:slug>/applications_document_review_view/",
|
||||
views.applications_document_review_view,
|
||||
name="applications_document_review_view",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/candidate_offer_view/",
|
||||
views_frontend.candidate_offer_view,
|
||||
name="candidate_offer_view",
|
||||
"jobs/<slug:slug>/applications_offer_view/",
|
||||
views_frontend.applications_offer_view,
|
||||
name="applications_offer_view",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/candidate_hired_view/",
|
||||
views_frontend.candidate_hired_view,
|
||||
name="candidate_hired_view",
|
||||
"jobs/<slug:slug>/applications_hired_view/",
|
||||
views_frontend.applications_hired_view,
|
||||
name="applications_hired_view",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:job_slug>/export/<str:stage>/csv/",
|
||||
views_frontend.export_candidates_csv,
|
||||
name="export_candidates_csv",
|
||||
views_frontend.export_applications_csv,
|
||||
name="export_applications_csv",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:job_slug>/candidates/<slug:candidate_slug>/update_status/<str:stage_type>/<str:status>/",
|
||||
views_frontend.update_candidate_status,
|
||||
name="update_candidate_status",
|
||||
"jobs/<slug:job_slug>/application/<slug:application_slug>/update_status/<str:stage_type>/<str:status>/",
|
||||
views_frontend.update_application_status,
|
||||
name="update_application_status",
|
||||
),
|
||||
# Sync URLs
|
||||
# Sync URLs (check)
|
||||
path(
|
||||
"jobs/<slug:job_slug>/sync-hired-candidates/",
|
||||
views_frontend.sync_hired_candidates,
|
||||
name="sync_hired_candidates",
|
||||
"jobs/<slug:job_slug>/sync-hired-applications/",
|
||||
views_frontend.sync_hired_applications,
|
||||
name="sync_hired_applications",
|
||||
),
|
||||
path(
|
||||
"sources/<int:source_id>/test-connection/",
|
||||
views_frontend.test_source_connection,
|
||||
name="test_source_connection",
|
||||
),
|
||||
|
||||
|
||||
path(
|
||||
"jobs/<slug:slug>/<int:candidate_id>/reschedule_meeting_for_candidate/<int:meeting_id>/",
|
||||
views.reschedule_meeting_for_candidate,
|
||||
name="reschedule_meeting_for_candidate",
|
||||
"jobs/<slug:slug>/<int:application_id>/reschedule_meeting_for_application/<int:meeting_id>/",
|
||||
views.reschedule_meeting_for_application,
|
||||
name="reschedule_meeting_for_application",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/update_candidate_exam_status/",
|
||||
views.update_candidate_exam_status,
|
||||
name="update_candidate_exam_status",
|
||||
"jobs/<slug:slug>/update_application_exam_status/",
|
||||
views.update_application_exam_status,
|
||||
name="update_application_exam_status",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/bulk_update_candidate_exam_status/",
|
||||
views.bulk_update_candidate_exam_status,
|
||||
name="bulk_update_candidate_exam_status",
|
||||
"jobs/<slug:slug>/bulk_update_application_exam_status/",
|
||||
views.bulk_update_application_exam_status,
|
||||
name="bulk_update_application_exam_status",
|
||||
),
|
||||
path(
|
||||
"htmx/<int:pk>/candidate_criteria_view/",
|
||||
views.candidate_criteria_view_htmx,
|
||||
name="candidate_criteria_view_htmx",
|
||||
"htmx/<int:pk>/application_criteria_view/",
|
||||
views.application_criteria_view_htmx,
|
||||
name="application_criteria_view_htmx",
|
||||
),
|
||||
path(
|
||||
"htmx/<slug:slug>/candidate_set_exam_date/",
|
||||
views.candidate_set_exam_date,
|
||||
name="candidate_set_exam_date",
|
||||
"htmx/<slug:slug>/application_set_exam_date/",
|
||||
views.application_set_exam_date,
|
||||
name="application_set_exam_date",
|
||||
),
|
||||
path(
|
||||
"htmx/<slug:slug>/candidate_update_status/",
|
||||
views.candidate_update_status,
|
||||
name="candidate_update_status",
|
||||
"htmx/<slug:slug>/application_update_status/",
|
||||
views.application_update_status,
|
||||
name="application_update_status",
|
||||
),
|
||||
# path('forms/form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
|
||||
# path('forms/form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
|
||||
@ -347,9 +349,9 @@ urlpatterns = [
|
||||
name="delete_meeting_comment",
|
||||
),
|
||||
path(
|
||||
"meetings/<slug:slug>/set_meeting_candidate/",
|
||||
views.set_meeting_candidate,
|
||||
name="set_meeting_candidate",
|
||||
"meetings/<slug:slug>/set_meeting_application/",
|
||||
views.set_meeting_application,
|
||||
name="set_meeting_application",
|
||||
),
|
||||
# Hiring Agency URLs
|
||||
path("agencies/", views.agency_list, name="agency_list"),
|
||||
@ -357,10 +359,10 @@ urlpatterns = [
|
||||
path("agencies/<slug:slug>/", views.agency_detail, name="agency_detail"),
|
||||
path("agencies/<slug:slug>/update/", views.agency_update, name="agency_update"),
|
||||
path("agencies/<slug:slug>/delete/", views.agency_delete, name="agency_delete"),
|
||||
path(
|
||||
"agencies/<slug:slug>/candidates/",
|
||||
views.agency_candidates,
|
||||
name="agency_candidates",
|
||||
path( #check the html of this url it is not used anywhere
|
||||
"agencies/<slug:slug>/applications/",
|
||||
views.agency_applications,
|
||||
name="agency_applications",
|
||||
),
|
||||
# path('agencies/<slug:slug>/send-message/', views.agency_detail_send_message, name='agency_detail_send_message'),
|
||||
# Agency Assignment Management URLs
|
||||
@ -369,12 +371,12 @@ urlpatterns = [
|
||||
views.agency_assignment_list,
|
||||
name="agency_assignment_list",
|
||||
),
|
||||
path(
|
||||
path( #check
|
||||
"agency-assignments/create/",
|
||||
views.agency_assignment_create,
|
||||
name="agency_assignment_create",
|
||||
),
|
||||
path(
|
||||
path(#check
|
||||
"agency-assignments/<slug:slug>/create/",
|
||||
views.agency_assignment_create,
|
||||
name="agency_assignment_create",
|
||||
@ -423,7 +425,7 @@ urlpatterns = [
|
||||
# path('admin/messages/<int:message_id>/mark-read/', views.admin_mark_message_read, name='admin_mark_message_read'),
|
||||
# path('admin/messages/<int:message_id>/delete/', views.admin_delete_message, name='admin_delete_message'),
|
||||
# Agency Portal URLs (for external agencies)
|
||||
path("portal/login/", views.agency_portal_login, name="agency_portal_login"),
|
||||
# path("portal/login/", views.agency_portal_login, name="agency_portal_login"),
|
||||
path("portal/<int:pk>/reset/", views.portal_password_reset, name="portal_password_reset"),
|
||||
path(
|
||||
"portal/dashboard/",
|
||||
@ -433,19 +435,19 @@ urlpatterns = [
|
||||
# Unified Portal URLs
|
||||
path("login/", views.portal_login, name="portal_login"),
|
||||
path(
|
||||
"candidate/dashboard/",
|
||||
views.candidate_portal_dashboard,
|
||||
name="candidate_portal_dashboard",
|
||||
"applicant/dashboard/",
|
||||
views.applicant_portal_dashboard,
|
||||
name="applicant_portal_dashboard",
|
||||
),
|
||||
path(
|
||||
"candidate/applications/<slug:slug>/",
|
||||
views.candidate_application_detail,
|
||||
name="candidate_application_detail",
|
||||
"applications/applications/<slug:slug>/",
|
||||
views.applicant_application_detail,
|
||||
name="applicant_application_detail",
|
||||
),
|
||||
# path(
|
||||
# "candidate/<slug:application_slug>/applications/<slug:person_slug>/detail/<slug:agency_slug>/",
|
||||
# views.candidate_application_detail,
|
||||
# name="candidate_application_detail",
|
||||
# views.applicant_application_detail,
|
||||
# name="applicant_application_detail",
|
||||
# ),
|
||||
path(
|
||||
"portal/dashboard/",
|
||||
@ -463,35 +465,35 @@ urlpatterns = [
|
||||
name="agency_portal_assignment_detail",
|
||||
),
|
||||
path(
|
||||
"portal/assignment/<slug:slug>/submit-candidate/",
|
||||
views.agency_portal_submit_candidate_page,
|
||||
name="agency_portal_submit_candidate_page",
|
||||
"portal/assignment/<slug:slug>/submit-application/",
|
||||
views.agency_portal_submit_application_page,
|
||||
name="agency_portal_submit_application_page",
|
||||
),
|
||||
path(
|
||||
"portal/submit-candidate/",
|
||||
views.agency_portal_submit_candidate,
|
||||
name="agency_portal_submit_candidate",
|
||||
"portal/submit-application/",
|
||||
views.agency_portal_submit_application,
|
||||
name="agency_portal_submit_application",
|
||||
),
|
||||
path("portal/logout/", views.portal_logout, name="portal_logout"),
|
||||
# Agency Portal Candidate Management URLs
|
||||
path(
|
||||
"portal/candidates/<int:candidate_id>/edit/",
|
||||
views.agency_portal_edit_candidate,
|
||||
name="agency_portal_edit_candidate",
|
||||
"portal/applications/<int:application_id>/edit/",
|
||||
views.agency_portal_edit_application,
|
||||
name="agency_portal_edit_application",
|
||||
),
|
||||
path(
|
||||
"portal/candidates/<int:candidate_id>/delete/",
|
||||
views.agency_portal_delete_candidate,
|
||||
name="agency_portal_delete_candidate",
|
||||
"portal/applications/<int:application_id>/delete/",
|
||||
views.agency_portal_delete_application,
|
||||
name="agency_portal_delete_application",
|
||||
),
|
||||
# API URLs for messaging (removed)
|
||||
# path('api/agency/messages/<int:message_id>/', views.api_agency_message_detail, name='api_agency_message_detail'),
|
||||
# path('api/agency/messages/<int:message_id>/mark-read/', views.api_agency_mark_message_read, name='api_agency_mark_message_read'),
|
||||
# API URLs for candidate management
|
||||
path(
|
||||
"api/candidate/<int:candidate_id>/",
|
||||
views.api_candidate_detail,
|
||||
name="api_candidate_detail",
|
||||
"api/application/<int:application_id>/",
|
||||
views.api_application_detail,
|
||||
name="api_application_detail",
|
||||
),
|
||||
# # Admin Notification API
|
||||
# path('api/admin/notification-count/', views.api_notification_count, name='admin_notification_count'),
|
||||
@ -535,10 +537,11 @@ urlpatterns = [
|
||||
),
|
||||
# Email composition URLs
|
||||
path(
|
||||
"jobs/<slug:job_slug>/candidates/compose-email/",
|
||||
views.compose_candidate_email,
|
||||
name="compose_candidate_email",
|
||||
"jobs/<slug:job_slug>/applications/compose-email/",
|
||||
views.compose_application_email,
|
||||
name="compose_application_email",
|
||||
),
|
||||
|
||||
# Message URLs
|
||||
path("messages/", views.message_list, name="message_list"),
|
||||
path("messages/create/", views.message_create, name="message_create"),
|
||||
@ -555,15 +558,15 @@ urlpatterns = [
|
||||
path("documents/<int:document_id>/delete/", views.document_delete, name="document_delete"),
|
||||
path("documents/<int:document_id>/download/", views.document_download, name="document_download"),
|
||||
# Candidate Document Management URLs
|
||||
path("candidate/documents/upload/<slug:slug>/", views.document_upload, name="candidate_document_upload"),
|
||||
path("candidate/documents/<int:document_id>/delete/", views.document_delete, name="candidate_document_delete"),
|
||||
path("candidate/documents/<int:document_id>/download/", views.document_download, name="candidate_document_download"),
|
||||
path('jobs/<slug:job_slug>/candidates/compose_email/', views.compose_candidate_email, name='compose_candidate_email'),
|
||||
path("application/documents/upload/<slug:slug>/", views.document_upload, name="application_document_upload"),
|
||||
path("application/documents/<int:document_id>/delete/", views.document_delete, name="application_document_delete"),
|
||||
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('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'),
|
||||
# Candidate Signup
|
||||
path('candidate/signup/<slug:template_slug>/', views.candidate_signup, name='candidate_signup'),
|
||||
path('application/signup/<slug:template_slug>/', views.application_signup, name='application_signup'),
|
||||
# Password Reset
|
||||
path('user/<int:pk>/password-reset/', views.portal_password_reset, name='portal_password_reset'),
|
||||
|
||||
@ -607,43 +610,43 @@ urlpatterns = [
|
||||
),
|
||||
# Candidate Meeting Scheduling/Rescheduling URLs
|
||||
path(
|
||||
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/",
|
||||
views.schedule_candidate_meeting,
|
||||
name="schedule_candidate_meeting",
|
||||
"jobs/<slug:job_slug>/applications/<int:application_pk>/schedule-meeting/",
|
||||
views.schedule_application_meeting,
|
||||
name="schedule_application_meeting",
|
||||
),
|
||||
path(
|
||||
"api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/",
|
||||
views.api_schedule_candidate_meeting,
|
||||
name="api_schedule_candidate_meeting",
|
||||
"api/jobs/<slug:job_slug>/applications/<int:application_pk>/schedule-meeting/",
|
||||
views.api_schedule_application_meeting,
|
||||
name="api_schedule_application_meeting",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/",
|
||||
views.reschedule_candidate_meeting,
|
||||
name="reschedule_candidate_meeting",
|
||||
"jobs/<slug:job_slug>/applications/<int:application_pk>/reschedule-meeting/<int:interview_pk>/",
|
||||
views.reschedule_application_meeting,
|
||||
name="reschedule_application_meeting",
|
||||
),
|
||||
path(
|
||||
"api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/",
|
||||
views.api_reschedule_candidate_meeting,
|
||||
name="api_reschedule_candidate_meeting",
|
||||
"api/jobs/<slug:job_slug>/applications/<int:application_pk>/reschedule-meeting/<int:interview_pk>/",
|
||||
views.api_reschedule_application_meeting,
|
||||
name="api_reschedule_application_meeting",
|
||||
),
|
||||
# New URL for simple page-based meeting scheduling
|
||||
path(
|
||||
"jobs/<slug:slug>/candidates/<int:candidate_pk>/schedule-meeting-page/",
|
||||
views.schedule_meeting_for_candidate,
|
||||
name="schedule_meeting_for_candidate",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/candidates/<int:candidate_pk>/delete_meeting_for_candidate/<int:meeting_id>/",
|
||||
views.delete_meeting_for_candidate,
|
||||
name="delete_meeting_for_candidate",
|
||||
"jobs/<slug:slug>/applications/<int:application_pk>/schedule-meeting-page/",
|
||||
views.schedule_meeting_for_application,
|
||||
name="schedule_meeting_for_application",
|
||||
),
|
||||
# path(
|
||||
# "jobs/<slug:slug>/applications/<int:application_pk>/delete_meeting_for_application/<int:meeting_id>/",
|
||||
# views.delete_meeting_for_candidate,
|
||||
# name="delete_meeting_for_candidate",
|
||||
# ),
|
||||
|
||||
|
||||
path("interviews/meetings/", views.MeetingListView.as_view(), name="list_meetings"),
|
||||
|
||||
# 1. Onsite Reschedule URL
|
||||
path(
|
||||
'<slug:slug>/candidate/<int:candidate_id>/onsite/reschedule/<int:meeting_id>/',
|
||||
'<slug:slug>/application/<int:application_id>/onsite/reschedule/<int:meeting_id>/',
|
||||
views.reschedule_onsite_meeting,
|
||||
name='reschedule_onsite_meeting'
|
||||
),
|
||||
@ -651,15 +654,15 @@ urlpatterns = [
|
||||
# 2. Onsite Delete URL
|
||||
|
||||
path(
|
||||
'job/<slug:slug>/candidates/<int:candidate_pk>/delete-onsite-meeting/<int:meeting_id>/',
|
||||
views.delete_onsite_meeting_for_candidate,
|
||||
name='delete_onsite_meeting_for_candidate'
|
||||
'job/<slug:slug>/applications/<int:application_pk>/delete-onsite-meeting/<int:meeting_id>/',
|
||||
views.delete_onsite_meeting_for_application,
|
||||
name='delete_onsite_meeting_for_application'
|
||||
),
|
||||
|
||||
path(
|
||||
'job/<slug:slug>/candidate/<int:candidate_pk>/schedule/onsite/',
|
||||
views.schedule_onsite_meeting_for_candidate,
|
||||
name='schedule_onsite_meeting_for_candidate' # This is the name used in the button
|
||||
'job/<slug:slug>/application/<int:application_pk>/schedule/onsite/',
|
||||
views.schedule_onsite_meeting_for_application,
|
||||
name='schedule_onsite_meeting_for_application' # This is the name used in the button
|
||||
),
|
||||
|
||||
|
||||
@ -667,7 +670,7 @@ urlpatterns = [
|
||||
path("interviews/meetings/<slug:slug>/", views.meeting_details, name="meeting_details"),
|
||||
|
||||
# Email invitation URLs
|
||||
path("interviews/meetings/<slug:slug>/send-candidate-invitation/", views.send_candidate_invitation, name="send_candidate_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"),
|
||||
|
||||
]
|
||||
|
||||
@ -571,10 +571,10 @@ def json_to_markdown_table(data_list):
|
||||
return markdown
|
||||
|
||||
|
||||
def get_candidates_from_request(request):
|
||||
def get_applications_from_request(request):
|
||||
for c in request.POST.items():
|
||||
try:
|
||||
yield models.Candidate.objects.get(pk=c[0])
|
||||
yield models.Application.objects.get(pk=c[0])
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
yield None
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -25,14 +25,14 @@ from django.urls import reverse_lazy
|
||||
from django.db.models import FloatField
|
||||
from django.db.models import F, IntegerField, Count, Avg, Sum, Q, ExpressionWrapper, fields, Value,CharField
|
||||
from django.db.models.functions import Cast, Coalesce, TruncDate
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
import json
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Add imports for user type restrictions
|
||||
from recruitment.decorators import StaffRequiredMixin, staff_user_required
|
||||
from recruitment.decorators import StaffRequiredMixin, staff_user_required,candidate_user_required,staff_or_candidate_required
|
||||
|
||||
|
||||
from datastar_py.django import (
|
||||
@ -91,7 +91,7 @@ class JobUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin,
|
||||
form_class = forms.JobPostingForm
|
||||
template_name = 'jobs/edit_job.html'
|
||||
success_url = reverse_lazy('job_list')
|
||||
success_message = 'Job updated successfully.'
|
||||
success_message = _('Job updated successfully.')
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
|
||||
@ -99,12 +99,12 @@ class JobDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin,
|
||||
model = models.JobPosting
|
||||
template_name = 'jobs/partials/delete_modal.html'
|
||||
success_url = reverse_lazy('job_list')
|
||||
success_message = 'Job deleted successfully.'
|
||||
success_message = _('Job deleted successfully.')
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
class JobApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
model = models.Application
|
||||
template_name = 'jobs/job_candidates_list.html'
|
||||
template_name = 'jobs/job_applications_list.html'
|
||||
context_object_name = 'applications'
|
||||
paginate_by = 10
|
||||
|
||||
@ -146,13 +146,13 @@ class JobApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
|
||||
class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
model = models.Application
|
||||
template_name = 'recruitment/candidate_list.html'
|
||||
template_name = 'recruitment/applications_list.html'
|
||||
context_object_name = 'applications'
|
||||
paginate_by = 100
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
|
||||
# Handle search
|
||||
search_query = self.request.GET.get('search', '')
|
||||
job = self.request.GET.get('job', '')
|
||||
@ -162,7 +162,7 @@ class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
Q(person__first_name__icontains=search_query) |
|
||||
Q(person__last_name__icontains=search_query) |
|
||||
Q(person__email__icontains=search_query) |
|
||||
Q(person__phone__icontains=search_query)
|
||||
Q(person__phone__icontains=search_query)
|
||||
)
|
||||
if job:
|
||||
queryset = queryset.filter(job__slug=job)
|
||||
@ -186,10 +186,10 @@ class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = models.Application
|
||||
form_class = forms.ApplicationForm
|
||||
template_name = 'recruitment/candidate_create.html'
|
||||
success_url = reverse_lazy('candidate_list')
|
||||
success_message = 'Candidate created successfully.'
|
||||
|
||||
template_name = 'recruitment/application_create.html'
|
||||
success_url = reverse_lazy('application_list')
|
||||
success_message = _('Application created successfully.')
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
if 'slug' in self.kwargs:
|
||||
@ -202,27 +202,30 @@ class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessa
|
||||
job = get_object_or_404(models.JobPosting, slug=self.kwargs['slug'])
|
||||
form.instance.job = job
|
||||
return super().form_valid(form)
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, f"{form.errors.as_text()}")
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.request.method == 'GET':
|
||||
context['person_form'] = forms.PersonForm()
|
||||
# if self.request.method == 'GET':
|
||||
context['person_form'] = forms.PersonForm()
|
||||
return context
|
||||
|
||||
class ApplicationUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = models.Application
|
||||
form_class = forms.ApplicationForm
|
||||
template_name = 'recruitment/candidate_update.html'
|
||||
success_url = reverse_lazy('candidate_list')
|
||||
success_message = 'Candidate updated successfully.'
|
||||
template_name = 'recruitment/application_update.html'
|
||||
success_url = reverse_lazy('application_list')
|
||||
success_message = _('Application updated successfully.')
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
|
||||
class ApplicationDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
model = models.Application
|
||||
template_name = 'recruitment/candidate_delete.html'
|
||||
success_url = reverse_lazy('candidate_list')
|
||||
success_message = 'Candidate deleted successfully.'
|
||||
template_name = 'recruitment/application_delete.html'
|
||||
success_url = reverse_lazy('application_list')
|
||||
success_message = _('Application deleted successfully.')
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
|
||||
@ -237,7 +240,7 @@ def retry_scoring_view(request,slug):
|
||||
hook='recruitment.hooks.callback_ai_parsing',
|
||||
sync=True,
|
||||
)
|
||||
return redirect('candidate_detail', slug=application.slug)
|
||||
return redirect('application_detail', slug=application.slug)
|
||||
|
||||
|
||||
|
||||
@ -250,11 +253,11 @@ def training_list(request):
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def candidate_detail(request, slug):
|
||||
def application_detail(request, slug):
|
||||
from rich.json import JSON
|
||||
candidate = get_object_or_404(models.Application, slug=slug)
|
||||
application = get_object_or_404(models.Application, slug=slug)
|
||||
try:
|
||||
parsed = ast.literal_eval(candidate.parsed_summary)
|
||||
parsed = ast.literal_eval(application.parsed_summary)
|
||||
except:
|
||||
parsed = {}
|
||||
|
||||
@ -267,8 +270,8 @@ def candidate_detail(request, slug):
|
||||
|
||||
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False)
|
||||
# parsed = json_to_markdown_table([parsed])
|
||||
return render(request, 'recruitment/candidate_detail.html', {
|
||||
'candidate': candidate,
|
||||
return render(request, 'recruitment/application_detail.html', {
|
||||
'application': application,
|
||||
'parsed': parsed,
|
||||
'stage_form': stage_form,
|
||||
})
|
||||
@ -276,21 +279,21 @@ def candidate_detail(request, slug):
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def candidate_resume_template_view(request, slug):
|
||||
def application_resume_template_view(request, slug):
|
||||
"""Display formatted resume template for a candidate"""
|
||||
application = get_object_or_404(models.Application, slug=slug)
|
||||
|
||||
if not request.user.is_staff:
|
||||
messages.error(request, _("You don't have permission to view this page."))
|
||||
return redirect('candidate_list')
|
||||
return redirect('application_list')
|
||||
|
||||
return render(request, 'recruitment/candidate_resume_template.html', {
|
||||
return render(request, 'recruitment/application_resume_template.html', {
|
||||
'application': application
|
||||
})
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def candidate_update_stage(request, slug):
|
||||
def application_update_stage(request, slug):
|
||||
"""Handle HTMX stage update requests"""
|
||||
application = get_object_or_404(models.Application, slug=slug)
|
||||
form = forms.ApplicationStageForm(request.POST, instance=application)
|
||||
@ -298,8 +301,8 @@ def candidate_update_stage(request, slug):
|
||||
stage_value = form.cleaned_data['stage']
|
||||
application.stage = stage_value
|
||||
application.save(update_fields=['stage'])
|
||||
messages.success(request,"application Stage Updated")
|
||||
return redirect("candidate_detail",slug=application.slug)
|
||||
messages.success(request,_("application Stage Updated"))
|
||||
return redirect("application_detail",slug=application.slug)
|
||||
|
||||
class TrainingListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
model = models.TrainingMaterial
|
||||
@ -383,7 +386,7 @@ def dashboard_view(request):
|
||||
# --- 1. BASE QUERYSETS & GLOBAL METRICS (UNFILTERED) ---
|
||||
|
||||
all_jobs_queryset = models.JobPosting.objects.all().order_by('-created_at')
|
||||
all_candidates_queryset = models.Application.objects.all()
|
||||
all_applications_queryset = models.Application.objects.all()
|
||||
|
||||
# Global KPI Card Metrics
|
||||
total_jobs_global = all_jobs_queryset.count()
|
||||
@ -397,7 +400,7 @@ def dashboard_view(request):
|
||||
# --- 2. TIME SERIES: GLOBAL DAILY APPLICANTS ---
|
||||
|
||||
# Group ALL candidates by creation date
|
||||
global_daily_applications_qs = all_candidates_queryset.annotate(
|
||||
global_daily_applications_qs = all_applications_queryset.annotate(
|
||||
date=TruncDate('created_at')
|
||||
).values('date').annotate(
|
||||
count=Count('pk')
|
||||
@ -409,14 +412,14 @@ def dashboard_view(request):
|
||||
|
||||
# --- 3. FILTERING LOGIC: Determine the scope for scoped metrics ---
|
||||
|
||||
candidate_queryset = all_candidates_queryset
|
||||
application_queryset = all_applications_queryset
|
||||
job_scope_queryset = all_jobs_queryset
|
||||
interview_queryset = models.ScheduledInterview.objects.all()
|
||||
|
||||
current_job = None
|
||||
if selected_job_pk:
|
||||
# Filter all base querysets
|
||||
candidate_queryset = candidate_queryset.filter(job__pk=selected_job_pk)
|
||||
application_queryset = application_queryset.filter(job__pk=selected_job_pk)
|
||||
interview_queryset = interview_queryset.filter(job__pk=selected_job_pk)
|
||||
|
||||
try:
|
||||
@ -431,7 +434,7 @@ def dashboard_view(request):
|
||||
scoped_dates = []
|
||||
scoped_counts = []
|
||||
if selected_job_pk:
|
||||
scoped_daily_applications_qs = candidate_queryset.annotate(
|
||||
scoped_daily_applications_qs = application_queryset.annotate(
|
||||
date=TruncDate('created_at')
|
||||
).values('date').annotate(
|
||||
count=Count('pk')
|
||||
@ -443,7 +446,7 @@ def dashboard_view(request):
|
||||
|
||||
# --- 5. SCOPED CORE AGGREGATIONS (FILTERED OR ALL) ---
|
||||
|
||||
total_candidates = candidate_queryset.count()
|
||||
total_applications = application_queryset.count()
|
||||
|
||||
|
||||
score_expression = Cast(
|
||||
@ -458,7 +461,7 @@ def dashboard_view(request):
|
||||
)
|
||||
|
||||
# 2. ANNOTATE the queryset with the new field
|
||||
candidates_with_score_query = candidate_queryset.annotate(
|
||||
applications_with_score_query = application_queryset.annotate(
|
||||
annotated_match_score=score_expression
|
||||
)
|
||||
|
||||
@ -469,9 +472,9 @@ def dashboard_view(request):
|
||||
# NullIf(
|
||||
# # 2. Use Replace to remove the literal double quotes (") that might be present.
|
||||
# Replace(
|
||||
# # 1. Use the double-underscore path (which uses the ->> operator for the final value)
|
||||
# # 1. Use the double-underscore path (which uses the ->> operator for the final value)
|
||||
# # and cast to CharField for text-based cleanup functions.
|
||||
# Cast(SCORE_PATH, output_field=CharField()),
|
||||
# Cast(SCORE_PATH, output_field=CharField()),
|
||||
# Value('"'), Value('') # Replace the double quote character with an empty string
|
||||
# ),
|
||||
# Value('') # Value to check for (empty string)
|
||||
@ -479,10 +482,10 @@ def dashboard_view(request):
|
||||
# output_field=IntegerField() # 4. Cast the clean, non-empty string (or NULL) to an integer.
|
||||
# )
|
||||
|
||||
|
||||
|
||||
# candidates_with_score_query= candidate_queryset.filter(is_resume_parsed=True).annotate(
|
||||
# # The Coalesce handles NULL values (from missing data, non-numeric data, or NullIf) and sets them to 0.
|
||||
# annotated_match_score=Coalesce(safe_match_score_cast, Value(0))
|
||||
# annotated_match_score=Coalesce(safe_match_score_cast, Value(0))
|
||||
# )
|
||||
|
||||
|
||||
@ -490,24 +493,24 @@ def dashboard_view(request):
|
||||
# A. Pipeline & Volume Metrics (Scoped)
|
||||
total_active_jobs = job_scope_queryset.filter(status="ACTIVE").count()
|
||||
last_week = timezone.now() - timedelta(days=7)
|
||||
new_candidates_7days = candidate_queryset.filter(created_at__gte=last_week).count()
|
||||
new_applications_7days = application_queryset.filter(created_at__gte=last_week).count()
|
||||
|
||||
open_positions_agg = job_scope_queryset.filter(status="ACTIVE").aggregate(total_open=Sum('open_positions'))
|
||||
total_open_positions = open_positions_agg['total_open'] or 0
|
||||
average_applications_result = job_scope_queryset.annotate(
|
||||
candidate_count=Count('applications', distinct=True)
|
||||
).aggregate(avg_apps=Avg('candidate_count'))['avg_apps']
|
||||
applications_count=Count('applications', distinct=True)
|
||||
).aggregate(avg_apps=Avg('applications_count'))['avg_apps']
|
||||
average_applications = round(average_applications_result or 0, 2)
|
||||
|
||||
|
||||
# B. Efficiency & Conversion Metrics (Scoped)
|
||||
hired_candidates = candidate_queryset.filter(
|
||||
hired_applications = application_queryset.filter(
|
||||
stage='Hired'
|
||||
)
|
||||
|
||||
lst=[c.time_to_hire_days for c in hired_candidates]
|
||||
lst=[c.time_to_hire_days for c in hired_applications]
|
||||
|
||||
time_to_hire_query = hired_candidates.annotate(
|
||||
time_to_hire_query = hired_applications.annotate(
|
||||
time_diff=ExpressionWrapper(
|
||||
F('join_date') - F('created_at__date'),
|
||||
output_field=fields.DurationField()
|
||||
@ -524,11 +527,11 @@ def dashboard_view(request):
|
||||
)
|
||||
print(avg_time_to_hire_days)
|
||||
|
||||
applied_count = candidate_queryset.filter(stage='Applied').count()
|
||||
advanced_count = candidate_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count()
|
||||
applied_count = application_queryset.filter(stage='Applied').count()
|
||||
advanced_count = application_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count()
|
||||
screening_pass_rate = round( (advanced_count / applied_count) * 100, 1 ) if applied_count > 0 else 0
|
||||
offers_extended_count = candidate_queryset.filter(stage='Offer').count()
|
||||
offers_accepted_count = candidate_queryset.filter(offer_status='Accepted').count()
|
||||
offers_extended_count = application_queryset.filter(stage='Offer').count()
|
||||
offers_accepted_count = application_queryset.filter(offer_status='Accepted').count()
|
||||
offers_accepted_rate = round( (offers_accepted_count / offers_extended_count) * 100, 1 ) if offers_extended_count > 0 else 0
|
||||
filled_positions = offers_accepted_count
|
||||
vacancy_fill_rate = round( (filled_positions / total_open_positions) * 100, 1 ) if total_open_positions > 0 else 0
|
||||
@ -539,21 +542,21 @@ def dashboard_view(request):
|
||||
meetings_scheduled_this_week = interview_queryset.filter(
|
||||
interview_date__week=current_week, interview_date__year=current_year
|
||||
).count()
|
||||
avg_match_score_result = candidates_with_score_query.aggregate(avg_score=Avg('annotated_match_score'))['avg_score']
|
||||
avg_match_score_result = applications_with_score_query.aggregate(avg_score=Avg('annotated_match_score'))['avg_score']
|
||||
avg_match_score = round(avg_match_score_result or 0, 1)
|
||||
high_potential_count = candidates_with_score_query.filter(annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD).count()
|
||||
high_potential_ratio = round( (high_potential_count / total_candidates) * 100, 1 ) if total_candidates > 0 else 0
|
||||
total_scored_candidates = candidates_with_score_query.count()
|
||||
scored_ratio = round( (total_scored_candidates / total_candidates) * 100, 1 ) if total_candidates > 0 else 0
|
||||
high_potential_count = applications_with_score_query.filter(annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD).count()
|
||||
high_potential_ratio = round( (high_potential_count / total_applications) * 100, 1 ) if total_applications > 0 else 0
|
||||
total_scored_candidates = applications_with_score_query.count()
|
||||
scored_ratio = round( (total_scored_candidates / total_applications) * 100, 1 ) if total_applications > 0 else 0
|
||||
|
||||
|
||||
# --- 6. CHART DATA PREPARATION ---
|
||||
|
||||
# A. Pipeline Funnel (Scoped)
|
||||
stage_counts = candidate_queryset.values('stage').annotate(count=Count('stage'))
|
||||
stage_counts = application_queryset.values('stage').annotate(count=Count('stage'))
|
||||
stage_map = {item['stage']: item['count'] for item in stage_counts}
|
||||
candidate_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired']
|
||||
candidates_count = [
|
||||
application_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired']
|
||||
application_count = [
|
||||
stage_map.get('Applied', 0), stage_map.get('Exam', 0), stage_map.get('Interview', 0),
|
||||
stage_map.get('Offer', 0), stage_map.get('Hired',0)
|
||||
]
|
||||
@ -567,9 +570,9 @@ def dashboard_view(request):
|
||||
rotation_degrees_final = round(min(rotation_degrees, 180), 1) # Ensure max 180 degrees
|
||||
|
||||
#
|
||||
hiring_source_counts = candidate_queryset.values('hiring_source').annotate(count=Count('stage'))
|
||||
hiring_source_counts = application_queryset.values('hiring_source').annotate(count=Count('stage'))
|
||||
source_map= {item['hiring_source']: item['count'] for item in hiring_source_counts}
|
||||
candidates_count_in_each_source = [
|
||||
applications_count_in_each_source = [
|
||||
source_map.get('Public', 0), source_map.get('Internal', 0), source_map.get('Agency', 0),
|
||||
|
||||
]
|
||||
@ -586,8 +589,8 @@ def dashboard_view(request):
|
||||
|
||||
# Scoped KPIs
|
||||
'total_active_jobs': total_active_jobs,
|
||||
'total_candidates': total_candidates,
|
||||
'new_candidates_7days': new_candidates_7days,
|
||||
'total_applications': total_applications,
|
||||
'new_applications_7days': new_applications_7days,
|
||||
'total_open_positions': total_open_positions,
|
||||
'average_applications': average_applications,
|
||||
'avg_time_to_hire_days': avg_time_to_hire_days,
|
||||
@ -601,8 +604,8 @@ def dashboard_view(request):
|
||||
'scored_ratio': scored_ratio,
|
||||
|
||||
# Chart Data
|
||||
'candidate_stage': json.dumps(candidate_stage),
|
||||
'candidates_count': json.dumps(candidates_count),
|
||||
'application_stage': json.dumps(application_stage),
|
||||
'application_count': json.dumps(application_count),
|
||||
'job_titles': json.dumps(job_titles),
|
||||
'job_app_counts': json.dumps(job_app_counts),
|
||||
# 'source_volume_chart_data' is intentionally REMOVED
|
||||
@ -625,7 +628,7 @@ def dashboard_view(request):
|
||||
'current_job': current_job,
|
||||
|
||||
|
||||
'candidates_count_in_each_source': json.dumps(candidates_count_in_each_source),
|
||||
'applications_count_in_each_source': json.dumps(applications_count_in_each_source),
|
||||
'all_hiring_sources': json.dumps(all_hiring_sources),
|
||||
}
|
||||
|
||||
@ -634,100 +637,100 @@ def dashboard_view(request):
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def candidate_offer_view(request, slug):
|
||||
def applications_offer_view(request, slug):
|
||||
"""View for candidates in the Offer stage"""
|
||||
job = get_object_or_404(models.JobPosting, slug=slug)
|
||||
|
||||
# Filter candidates for this specific job and stage
|
||||
candidates = job.offer_candidates
|
||||
applications = job.offer_applications
|
||||
|
||||
# Handle search
|
||||
search_query = request.GET.get('search', '')
|
||||
if search_query:
|
||||
candidates = candidates.filter(
|
||||
applications = applications.filter(
|
||||
Q(first_name__icontains=search_query) |
|
||||
Q(last_name__icontains=search_query) |
|
||||
Q(email__icontains=search_query) |
|
||||
Q(phone__icontains=search_query)
|
||||
)
|
||||
|
||||
candidates = candidates.order_by('-created_at')
|
||||
applications = applications.order_by('-created_at')
|
||||
|
||||
context = {
|
||||
'job': job,
|
||||
'candidates': candidates,
|
||||
'applications': applications,
|
||||
'search_query': search_query,
|
||||
'current_stage': 'Offer',
|
||||
}
|
||||
return render(request, 'recruitment/candidate_offer_view.html', context)
|
||||
return render(request, 'recruitment/applications_offer_view.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def candidate_hired_view(request, slug):
|
||||
"""View for hired candidates"""
|
||||
def applications_hired_view(request, slug):
|
||||
"""View for hired applications"""
|
||||
job = get_object_or_404(models.JobPosting, slug=slug)
|
||||
|
||||
# Filter candidates with offer_status = 'Accepted'
|
||||
candidates = job.hired_candidates
|
||||
# Filter applications with offer_status = 'Accepted'
|
||||
applications = job.hired_applications
|
||||
|
||||
# Handle search
|
||||
search_query = request.GET.get('search', '')
|
||||
if search_query:
|
||||
candidates = candidates.filter(
|
||||
applications = applications.filter(
|
||||
Q(first_name__icontains=search_query) |
|
||||
Q(last_name__icontains=search_query) |
|
||||
Q(email__icontains=search_query) |
|
||||
Q(phone__icontains=search_query)
|
||||
)
|
||||
|
||||
candidates = candidates.order_by('-created_at')
|
||||
applications = applications.order_by('-created_at')
|
||||
|
||||
context = {
|
||||
'job': job,
|
||||
'candidates': candidates,
|
||||
'applications': applications,
|
||||
'search_query': search_query,
|
||||
'current_stage': 'Hired',
|
||||
}
|
||||
return render(request, 'recruitment/candidate_hired_view.html', context)
|
||||
return render(request, 'recruitment/applications_hired_view.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def update_candidate_status(request, job_slug, candidate_slug, stage_type, status):
|
||||
def update_application_status(request, job_slug, application_slug, stage_type, status):
|
||||
"""Handle exam/interview/offer status updates"""
|
||||
from django.utils import timezone
|
||||
|
||||
job = get_object_or_404(models.JobPosting, slug=job_slug)
|
||||
candidate = get_object_or_404(models.Application, slug=candidate_slug, job=job)
|
||||
application = get_object_or_404(models.Application, slug=application_slug, job=job)
|
||||
|
||||
if request.method == "POST":
|
||||
if stage_type == 'exam':
|
||||
status = request.POST.get("exam_status")
|
||||
score = request.POST.get("exam_score")
|
||||
candidate.exam_status = status
|
||||
candidate.exam_score = score
|
||||
candidate.exam_date = timezone.now()
|
||||
candidate.save(update_fields=['exam_status','exam_score', 'exam_date'])
|
||||
return render(request,'recruitment/partials/exam-results.html',{'candidate':candidate,'job':job})
|
||||
application.exam_status = status
|
||||
application.exam_score = score
|
||||
application.exam_date = timezone.now()
|
||||
application.save(update_fields=['exam_status','exam_score', 'exam_date'])
|
||||
return render(request,'recruitment/partials/exam-results.html',{'application':application,'job':job})
|
||||
elif stage_type == 'interview':
|
||||
candidate.interview_status = status
|
||||
candidate.interview_date = timezone.now()
|
||||
candidate.save(update_fields=['interview_status', 'interview_date'])
|
||||
return render(request,'recruitment/partials/interview-results.html',{'candidate':candidate,'job':job})
|
||||
application.interview_status = status
|
||||
application.interview_date = timezone.now()
|
||||
application.save(update_fields=['interview_status', 'interview_date'])
|
||||
return render(request,'recruitment/partials/interview-results.html',{'application':application,'job':job})
|
||||
elif stage_type == 'offer':
|
||||
candidate.offer_status = status
|
||||
candidate.offer_date = timezone.now()
|
||||
candidate.save(update_fields=['offer_status', 'offer_date'])
|
||||
return render(request,'recruitment/partials/offer-results.html',{'candidate':candidate,'job':job})
|
||||
return redirect('candidate_detail', candidate.slug)
|
||||
application.offer_status = status
|
||||
application.offer_date = timezone.now()
|
||||
application.save(update_fields=['offer_status', 'offer_date'])
|
||||
return render(request,'recruitment/partials/offer-results.html',{'application':application,'job':job})
|
||||
return redirect('application_detail', application.slug)
|
||||
else:
|
||||
if stage_type == 'exam':
|
||||
return render(request,"includes/candidate_update_exam_form.html",{'candidate':candidate,'job':job})
|
||||
return render(request,"includes/applications_update_exam_form.html",{'application':application,'job':job})
|
||||
elif stage_type == 'interview':
|
||||
return render(request,"includes/candidate_update_interview_form.html",{'candidate':candidate,'job':job})
|
||||
return render(request,"includes/applications_update_interview_form.html",{'application':application,'job':job})
|
||||
elif stage_type == 'offer':
|
||||
return render(request,"includes/candidate_update_offer_form.html",{'candidate':candidate,'job':job})
|
||||
return render(request,"includes/applications_update_offer_form.html",{'application':application,'job':job})
|
||||
|
||||
|
||||
# Stage configuration for CSV export
|
||||
@ -762,8 +765,8 @@ STAGE_CONFIG = {
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def export_candidates_csv(request, job_slug, stage):
|
||||
"""Export candidates for a specific stage as CSV"""
|
||||
def export_applications_csv(request, job_slug, stage):
|
||||
"""Export applications for a specific stage as CSV"""
|
||||
job = get_object_or_404(models.JobPosting, slug=job_slug)
|
||||
|
||||
# Validate stage
|
||||
@ -773,23 +776,23 @@ def export_candidates_csv(request, job_slug, stage):
|
||||
|
||||
config = STAGE_CONFIG[stage]
|
||||
|
||||
# Filter candidates based on stage
|
||||
# Filter applications based on stage
|
||||
if stage == 'hired':
|
||||
candidates = job.applications.filter(**config['filter'])
|
||||
applications = job.applications.filter(**config['filter'])
|
||||
else:
|
||||
candidates = job.applications.filter(**config['filter'])
|
||||
applications = job.applications.filter(**config['filter'])
|
||||
|
||||
# Handle search if provided
|
||||
search_query = request.GET.get('search', '')
|
||||
if search_query:
|
||||
candidates = candidates.filter(
|
||||
applications = applications.filter(
|
||||
Q(first_name__icontains=search_query) |
|
||||
Q(last_name__icontains=search_query) |
|
||||
Q(email__icontains=search_query) |
|
||||
Q(phone__icontains=search_query)
|
||||
)
|
||||
|
||||
candidates = candidates.order_by('-created_at')
|
||||
applications = applications.order_by('-created_at')
|
||||
|
||||
# Create CSV response
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
@ -806,87 +809,87 @@ def export_candidates_csv(request, job_slug, stage):
|
||||
headers.extend(['Job Title', 'Department'])
|
||||
writer.writerow(headers)
|
||||
|
||||
# Write candidate data
|
||||
for candidate in candidates:
|
||||
# Write application data
|
||||
for application in applications:
|
||||
row = []
|
||||
|
||||
# Extract data based on stage configuration
|
||||
for field in config['fields']:
|
||||
if field == 'name':
|
||||
row.append(candidate.name)
|
||||
row.append(application.name)
|
||||
elif field == 'email':
|
||||
row.append(candidate.email)
|
||||
row.append(application.email)
|
||||
elif field == 'phone':
|
||||
row.append(candidate.phone)
|
||||
row.append(application.phone)
|
||||
elif field == 'created_at':
|
||||
row.append(candidate.created_at.strftime('%Y-%m-%d %H:%M') if candidate.created_at else '')
|
||||
row.append(application.created_at.strftime('%Y-%m-%d %H:%M') if application.created_at else '')
|
||||
elif field == 'stage':
|
||||
row.append(candidate.stage or '')
|
||||
row.append(application.stage or '')
|
||||
elif field == 'exam_status':
|
||||
row.append(candidate.exam_status or '')
|
||||
row.append(application.exam_status or '')
|
||||
elif field == 'exam_date':
|
||||
row.append(candidate.exam_date.strftime('%Y-%m-%d %H:%M') if candidate.exam_date else '')
|
||||
row.append(application.exam_date.strftime('%Y-%m-%d %H:%M') if application.exam_date else '')
|
||||
elif field == 'interview_status':
|
||||
row.append(candidate.interview_status or '')
|
||||
row.append(application.interview_status or '')
|
||||
elif field == 'interview_date':
|
||||
row.append(candidate.interview_date.strftime('%Y-%m-%d %H:%M') if candidate.interview_date else '')
|
||||
row.append(application.interview_date.strftime('%Y-%m-%d %H:%M') if application.interview_date else '')
|
||||
elif field == 'offer_status':
|
||||
row.append(candidate.offer_status or '')
|
||||
row.append(application.offer_status or '')
|
||||
elif field == 'offer_date':
|
||||
row.append(candidate.offer_date.strftime('%Y-%m-%d %H:%M') if candidate.offer_date else '')
|
||||
row.append(application.offer_date.strftime('%Y-%m-%d %H:%M') if application.offer_date else '')
|
||||
elif field == 'ai_score':
|
||||
# Extract AI score using model property
|
||||
try:
|
||||
score = candidate.match_score
|
||||
score = application.match_score
|
||||
row.append(f"{score}%" if score else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'years_experience':
|
||||
# Extract years of experience using model property
|
||||
try:
|
||||
years = candidate.years_of_experience
|
||||
years = application.years_of_experience
|
||||
row.append(f"{years}" if years else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'screening_rating':
|
||||
# Extract screening rating using model property
|
||||
try:
|
||||
rating = candidate.screening_stage_rating
|
||||
rating = application.screening_stage_rating
|
||||
row.append(rating if rating else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'professional_category':
|
||||
# Extract professional category using model property
|
||||
try:
|
||||
category = candidate.professional_category
|
||||
category = application.professional_category
|
||||
row.append(category if category else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'top_skills':
|
||||
# Extract top 3 skills using model property
|
||||
try:
|
||||
skills = candidate.top_3_keywords
|
||||
skills = application.top_3_keywords
|
||||
row.append(', '.join(skills) if skills else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'strengths':
|
||||
# Extract strengths using model property
|
||||
try:
|
||||
strengths = candidate.strengths
|
||||
strengths = application.strengths
|
||||
row.append(strengths if strengths else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'weaknesses':
|
||||
# Extract weaknesses using model property
|
||||
try:
|
||||
weaknesses = candidate.weaknesses
|
||||
weaknesses = application.weaknesses
|
||||
row.append(weaknesses if weaknesses else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'join_date':
|
||||
row.append(candidate.join_date.strftime('%Y-%m-%d') if candidate.join_date else '')
|
||||
row.append(application.join_date.strftime('%Y-%m-%d') if application.join_date else '')
|
||||
else:
|
||||
row.append(getattr(candidate, field, ''))
|
||||
row.append(getattr(application, field, ''))
|
||||
|
||||
# Add job information
|
||||
row.extend([job.title, job.department or ''])
|
||||
@ -902,8 +905,8 @@ def export_candidates_csv(request, job_slug, stage):
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def sync_hired_candidates(request, job_slug):
|
||||
"""Sync hired candidates to external sources using Django-Q"""
|
||||
def sync_hired_applications(request, job_slug):
|
||||
"""Sync hired applications to external sources using Django-Q"""
|
||||
from django_q.tasks import async_task
|
||||
from .tasks import sync_hired_candidates_task
|
||||
|
||||
|
||||
@ -81,6 +81,17 @@ class ERPIntegrationView(View):
|
||||
'message': 'Source not found'
|
||||
}, status=404)
|
||||
|
||||
job_id = data.get('job_id')
|
||||
if not job_id:
|
||||
return JsonResponse({
|
||||
'status': 'error',
|
||||
'message': 'Job ID is required and must be unique'
|
||||
})
|
||||
if JobPosting.objects.filter(internal_job_id=job_id).exists():
|
||||
return JsonResponse({
|
||||
'status': 'error',
|
||||
'message': 'Job with this ID already exists'
|
||||
}, status=400)
|
||||
# Create integration service
|
||||
service = ERPIntegrationService(source)
|
||||
|
||||
@ -144,6 +155,7 @@ class ERPIntegrationView(View):
|
||||
def _create_job(self, service: ERPIntegrationService, data: Dict[str, Any]) -> tuple[Dict[str, Any], str]:
|
||||
"""Create a new job from ERP data"""
|
||||
# Validate ERP data
|
||||
# print(data)
|
||||
is_valid, error_msg = service.validate_erp_data(data)
|
||||
if not is_valid:
|
||||
return None, error_msg
|
||||
@ -152,7 +164,6 @@ class ERPIntegrationView(View):
|
||||
job, error_msg = service.create_job_from_erp(data)
|
||||
if error_msg:
|
||||
return None, error_msg
|
||||
|
||||
# Prepare response data
|
||||
response_data = {
|
||||
'job_id': job.internal_job_id,
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApplicantConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'applicant'
|
||||
@ -1,22 +0,0 @@
|
||||
from django import forms
|
||||
from .models import ApplicantForm, FormField
|
||||
|
||||
class ApplicantFormCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ApplicantForm
|
||||
fields = ['name', 'description']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
}
|
||||
|
||||
class FormFieldForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FormField
|
||||
fields = ['label', 'field_type', 'required', 'help_text', 'choices']
|
||||
widgets = {
|
||||
'label': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'field_type': forms.Select(attrs={'class': 'form-control'}),
|
||||
'help_text': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
||||
'choices': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Option1, Option2, Option3'}),
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
from django import forms
|
||||
from .models import FormField
|
||||
|
||||
# applicant/forms_builder.py
|
||||
def create_dynamic_form(form_instance):
|
||||
fields = {}
|
||||
|
||||
for field in form_instance.fields.all():
|
||||
field_kwargs = {
|
||||
'label': field.label,
|
||||
'required': field.required,
|
||||
'help_text': field.help_text
|
||||
}
|
||||
|
||||
# Use stable field_name instead of database ID
|
||||
field_key = field.field_name
|
||||
|
||||
if field.field_type == 'text':
|
||||
fields[field_key] = forms.CharField(**field_kwargs)
|
||||
elif field.field_type == 'email':
|
||||
fields[field_key] = forms.EmailField(**field_kwargs)
|
||||
elif field.field_type == 'phone':
|
||||
fields[field_key] = forms.CharField(**field_kwargs)
|
||||
elif field.field_type == 'number':
|
||||
fields[field_key] = forms.IntegerField(**field_kwargs)
|
||||
elif field.field_type == 'date':
|
||||
fields[field_key] = forms.DateField(**field_kwargs)
|
||||
elif field.field_type == 'textarea':
|
||||
fields[field_key] = forms.CharField(
|
||||
widget=forms.Textarea,
|
||||
**field_kwargs
|
||||
)
|
||||
elif field.field_type in ['select', 'radio']:
|
||||
choices = [(c.strip(), c.strip()) for c in field.choices.split(',') if c.strip()]
|
||||
if not choices:
|
||||
choices = [('', '---')]
|
||||
if field.field_type == 'select':
|
||||
fields[field_key] = forms.ChoiceField(choices=choices, **field_kwargs)
|
||||
else:
|
||||
fields[field_key] = forms.ChoiceField(
|
||||
choices=choices,
|
||||
widget=forms.RadioSelect,
|
||||
**field_kwargs
|
||||
)
|
||||
elif field.field_type == 'checkbox':
|
||||
field_kwargs['required'] = False
|
||||
fields[field_key] = forms.BooleanField(**field_kwargs)
|
||||
|
||||
return type('DynamicApplicantForm', (forms.Form,), fields)
|
||||
@ -1,70 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-01 21:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('jobs', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ApplicantForm',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text="Form version name (e.g., 'Version A', 'Version B' etc)", max_length=200)),
|
||||
('description', models.TextField(blank=True, help_text='Optional description of this form version')),
|
||||
('is_active', models.BooleanField(default=False, help_text='Only one form can be active per job')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('job_posting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applicant_forms', to='jobs.jobposting')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Application Form',
|
||||
'verbose_name_plural': 'Application Forms',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('job_posting', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ApplicantSubmission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('submitted_at', models.DateTimeField(auto_now_add=True)),
|
||||
('data', models.JSONField()),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('score', models.FloatField(default=0, help_text='Ranking score for the applicant submission')),
|
||||
('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='applicant.applicantform')),
|
||||
('job_posting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='jobs.jobposting')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Applicant Submission',
|
||||
'verbose_name_plural': 'Applicant Submissions',
|
||||
'ordering': ['-submitted_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormField',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('label', models.CharField(max_length=255)),
|
||||
('field_type', models.CharField(choices=[('text', 'Text'), ('email', 'Email'), ('phone', 'Phone'), ('number', 'Number'), ('date', 'Date'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkbox'), ('textarea', 'Paragraph Text'), ('file', 'File Upload'), ('image', 'Image Upload')], max_length=20)),
|
||||
('required', models.BooleanField(default=True)),
|
||||
('help_text', models.TextField(blank=True)),
|
||||
('choices', models.TextField(blank=True, help_text='Comma-separated options for select/radio fields')),
|
||||
('order', models.IntegerField(default=0)),
|
||||
('field_name', models.CharField(blank=True, max_length=100)),
|
||||
('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='applicant.applicantform')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Field',
|
||||
'verbose_name_plural': 'Form Fields',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,144 +0,0 @@
|
||||
# models.py
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from jobs.models import JobPosting
|
||||
from django.urls import reverse
|
||||
|
||||
class ApplicantForm(models.Model):
|
||||
"""Multiple dynamic forms per job posting, only one active at a time"""
|
||||
job_posting = models.ForeignKey(
|
||||
JobPosting,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='applicant_forms'
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=200,
|
||||
help_text="Form version name (e.g., 'Version A', 'Version B' etc)"
|
||||
)
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
help_text="Optional description of this form version"
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Only one form can be active per job"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('job_posting', 'name')
|
||||
ordering = ['-created_at']
|
||||
verbose_name = "Application Form"
|
||||
verbose_name_plural = "Application Forms"
|
||||
|
||||
def __str__(self):
|
||||
status = "(Active)" if self.is_active else "(Inactive)"
|
||||
return f"{self.name} for {self.job_posting.title} {status}"
|
||||
|
||||
def clean(self):
|
||||
"""Ensure only one active form per job"""
|
||||
if self.is_active:
|
||||
existing_active = self.job_posting.applicant_forms.filter(
|
||||
is_active=True
|
||||
).exclude(pk=self.pk)
|
||||
if existing_active.exists():
|
||||
raise ValidationError(
|
||||
"Only one active application form is allowed per job posting."
|
||||
)
|
||||
super().clean()
|
||||
|
||||
def activate(self):
|
||||
"""Set this form as active and deactivate others"""
|
||||
self.is_active = True
|
||||
self.save()
|
||||
# Deactivate other forms
|
||||
self.job_posting.applicant_forms.exclude(pk=self.pk).update(
|
||||
is_active=False
|
||||
)
|
||||
|
||||
def get_public_url(self):
|
||||
"""Returns the public application URL for this job's active form"""
|
||||
return reverse('applicant:apply_form', args=[self.job_posting.internal_job_id])
|
||||
|
||||
|
||||
class FormField(models.Model):
|
||||
FIELD_TYPES = [
|
||||
('text', 'Text'),
|
||||
('email', 'Email'),
|
||||
('phone', 'Phone'),
|
||||
('number', 'Number'),
|
||||
('date', 'Date'),
|
||||
('select', 'Dropdown'),
|
||||
('radio', 'Radio Buttons'),
|
||||
('checkbox', 'Checkbox'),
|
||||
('textarea', 'Paragraph Text'),
|
||||
('file', 'File Upload'),
|
||||
('image', 'Image Upload'),
|
||||
]
|
||||
|
||||
form = models.ForeignKey(
|
||||
ApplicantForm,
|
||||
related_name='fields',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
label = models.CharField(max_length=255)
|
||||
field_type = models.CharField(max_length=20, choices=FIELD_TYPES)
|
||||
required = models.BooleanField(default=True)
|
||||
help_text = models.TextField(blank=True)
|
||||
choices = models.TextField(
|
||||
blank=True,
|
||||
help_text="Comma-separated options for select/radio fields"
|
||||
)
|
||||
order = models.IntegerField(default=0)
|
||||
field_name = models.CharField(max_length=100, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['order']
|
||||
verbose_name = "Form Field"
|
||||
verbose_name_plural = "Form Fields"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.label} ({self.field_type}) in {self.form.name}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.field_name:
|
||||
# Create a stable field name from label (e.g., "Full Name" → "full_name")
|
||||
import re
|
||||
# Use Unicode word characters, including Arabic, for field_name
|
||||
self.field_name = re.sub(
|
||||
r'[^\w]+',
|
||||
'_',
|
||||
self.label.lower(),
|
||||
flags=re.UNICODE
|
||||
).strip('_')
|
||||
# Ensure uniqueness within the form
|
||||
base_name = self.field_name
|
||||
counter = 1
|
||||
while FormField.objects.filter(
|
||||
form=self.form,
|
||||
field_name=self.field_name
|
||||
).exists():
|
||||
self.field_name = f"{base_name}_{counter}"
|
||||
counter += 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ApplicantSubmission(models.Model):
|
||||
job_posting = models.ForeignKey(JobPosting, on_delete=models.CASCADE)
|
||||
form = models.ForeignKey(ApplicantForm, on_delete=models.CASCADE)
|
||||
submitted_at = models.DateTimeField(auto_now_add=True)
|
||||
data = models.JSONField()
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
score = models.FloatField(
|
||||
default=0,
|
||||
help_text="Ranking score for the applicant submission"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-submitted_at']
|
||||
verbose_name = "Applicant Submission"
|
||||
verbose_name_plural = "Applicant Submissions"
|
||||
|
||||
def __str__(self):
|
||||
return f"Submission for {self.job_posting.title} at {self.submitted_at}"
|
||||
@ -1,94 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Apply: {{ job.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
|
||||
{# --- 1. Job Header and Overview (Fixed/Static Info) --- #}
|
||||
<div class="card bg-light-subtle mb-4 p-4 border-0 rounded-3 shadow-sm">
|
||||
<h1 class="h2 fw-bold text-primary mb-1">{{ job.title }}</h1>
|
||||
|
||||
<p class="mb-3 text-muted">
|
||||
Your final step to apply for this position.
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-4 small text-secondary">
|
||||
<div>
|
||||
<i class="fas fa-building me-1"></i>
|
||||
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
|
||||
</div>
|
||||
<div>
|
||||
<i class="fas fa-map-marker-alt me-1"></i>
|
||||
<strong>Location:</strong> {{ job.get_location_display }}
|
||||
</div>
|
||||
<div>
|
||||
<i class="fas fa-briefcase me-1"></i>
|
||||
<strong>Type:</strong> {{ job.get_job_type_display }} • {{ job.get_workplace_type_display }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- 2. Application Form Section --- #}
|
||||
<div class="card p-5 border-0 rounded-3 shadow">
|
||||
<h2 class="h3 fw-semibold mb-3">Application Details</h2>
|
||||
|
||||
{% if applicant_form.description %}
|
||||
<p class="text-muted mb-4">{{ applicant_form.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="form-group mb-4">
|
||||
{# Label Tag #}
|
||||
<label for="{{ field.id_for_label }}" class="form-label">
|
||||
{{ field.label }}
|
||||
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
|
||||
</label>
|
||||
|
||||
{# The Field Widget (Assumes form-control is applied in backend) #}
|
||||
{{ field }}
|
||||
|
||||
{# Field Errors #}
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback d-block">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
|
||||
{# Help Text #}
|
||||
{% if field.help_text %}
|
||||
<div class="form-text">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{# General Form Errors (Non-field errors) #}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger mb-4">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg mt-3 w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i> Submit Application
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<footer class="mt-4 text-center">
|
||||
<a href="{% url 'applicant:review_job_detail' job.internal_job_id %}"
|
||||
class="btn btn-link text-secondary">
|
||||
← Review Job Details
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@ -1,68 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Define Form for {{ job.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8 col-md-10">
|
||||
|
||||
<div class="card shadow-lg border-0 p-4 p-md-5">
|
||||
|
||||
<h2 class="card-title text-center mb-4 text-dark">
|
||||
🛠️ New Application Form Configuration
|
||||
</h2>
|
||||
|
||||
<p class="text-center text-muted mb-4 border-bottom pb-3">
|
||||
You are creating a new form structure for job: <strong>{{ job.title }}</strong>
|
||||
</p>
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<fieldset class="mb-5">
|
||||
<legend class="h5 mb-3 text-secondary">Form Metadata</legend>
|
||||
|
||||
<div class="form-group mb-4">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label required">
|
||||
Form Name
|
||||
</label>
|
||||
{# The field should already have form-control applied from the backend #}
|
||||
{{ form.name }}
|
||||
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger small mt-1">{{ form.name.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-4">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||
Description
|
||||
</label>
|
||||
{# The field should already have form-control applied from the backend #}
|
||||
{{ form.description}}
|
||||
|
||||
{% if form.description.errors %}
|
||||
<div class="text-danger small mt-1">{{ form.description.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="d-flex justify-content-end gap-3 pt-3">
|
||||
<a href="{% url 'applicant:job_forms_list' job.internal_job_id %}"
|
||||
class="btn btn-outline-secondary">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn univ-color btn-lg">
|
||||
Create Form & Continue →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,103 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Manage Forms | {{ job.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
|
||||
<header class="mb-5 pb-3 border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center">
|
||||
<div>
|
||||
<h2 class="h3 mb-1 ">
|
||||
<i class="fas fa-clipboard-list me-2 text-secondary"></i>
|
||||
Application Forms for <span class="text-success fw-bold">"{{ job.title }}"</span>
|
||||
</h2>
|
||||
<p class="text-muted small">
|
||||
Internal Job ID: **{{ job.internal_job_id }}**
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Primary Action Button using the theme color #}
|
||||
<a href="{% url 'applicant:create_form' job_id=job.internal_job_id %}"
|
||||
class="btn univ-color btn-lg shadow-sm mt-3 mt-md-0">
|
||||
<i class="fas fa-plus me-1"></i> Create New Form
|
||||
</a>
|
||||
</header>
|
||||
|
||||
{% if forms %}
|
||||
|
||||
<div class="list-group">
|
||||
{% for form in forms %}
|
||||
|
||||
{# Custom styling based on active state #}
|
||||
<div class="list-group-item d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center p-3 mb-3 rounded shadow-sm
|
||||
{% if form.is_active %}border-success border-3 bg-light{% else %}border-secondary border-1{% endif %}">
|
||||
|
||||
{# Left Section: Form Details #}
|
||||
<div class="flex-grow-1 me-4 mb-2 mb-sm-0">
|
||||
<h4 class="h5 mb-1 d-inline-block">
|
||||
{{ form.name }}
|
||||
</h4>
|
||||
|
||||
{# Status Badge #}
|
||||
{% if form.is_active %}
|
||||
<span class="badge bg-success ms-2">
|
||||
<i class="fas fa-check-circle me-1"></i> Active Form
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary ms-2">
|
||||
<i class="fas fa-times-circle me-1"></i> Inactive
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<p class="text-muted mt-1 mb-1 small">
|
||||
{{ form.description|default:"— No description provided. —" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Right Section: Actions #}
|
||||
<div class="d-flex gap-2 align-items-center flex-wrap">
|
||||
|
||||
{# Edit Structure Button #}
|
||||
<a href="{% url 'applicant:edit_form' form.id %}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-pen me-1"></i> Edit Structure
|
||||
</a>
|
||||
|
||||
{# Conditional Activation Button #}
|
||||
{% if not form.is_active %}
|
||||
<a href="{% url 'applicant:activate_form' form.id %}"
|
||||
class="btn btn-sm univ-color">
|
||||
<i class="fas fa-bolt me-1"></i> Activate Form
|
||||
</a>
|
||||
{% else %}
|
||||
{# Active indicator/Deactivate button placeholder #}
|
||||
<a href="#" class="btn btn-sm btn-outline-success" disabled>
|
||||
<i class="fas fa-star me-1"></i> Current Form
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5 bg-light rounded shadow-sm">
|
||||
<i class="fas fa-file-alt fa-4x text-muted mb-3"></i>
|
||||
<p class="lead mb-0">No application forms have been created yet for this job.</p>
|
||||
<p class="mt-2 mb-0 text-secondary">Click the button above to define a new form structure.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<footer class="text-end mt-5 pt-3 border-top">
|
||||
<a href="{% url 'jobs:job_detail' job.internal_job_id %}"
|
||||
class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> Back to Job Details
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,129 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ job.title }} - University ATS{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-5">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2>{{ job.title }}</h2>
|
||||
<span class="badge bg-{{ job.status|lower }} status-badge">
|
||||
{{ job.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Job Details -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Position Number:</strong> {{ job.position_number|default:"Not specified" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Job Type:</strong> {{ job.get_job_type_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Workplace:</strong> {{ job.get_workplace_type_display }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Location:</strong> {{ job.get_location_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Created By:</strong> {{ job.created_by|default:"Not specified" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if job.salary_range %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<strong>Salary Range:</strong> {{ job.salary_range }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if job.start_date %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<strong>Start Date:</strong> {{ job.start_date }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if job.application_deadline %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<strong>Application Deadline:</strong> {{ job.application_deadline }}
|
||||
{% if job.is_expired %}
|
||||
<span class="badge bg-danger">EXPIRED</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Description -->
|
||||
{% if job.description %}
|
||||
<div class="mb-3">
|
||||
<h5>Description</h5>
|
||||
<div>{{ job.description|linebreaks }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if job.qualifications %}
|
||||
<div class="mb-3">
|
||||
<h5>Qualifications</h5>
|
||||
<div>{{ job.qualifications|linebreaks }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if job.benefits %}
|
||||
<div class="mb-3">
|
||||
<h5>Benefits</h5>
|
||||
<div>{{ job.benefits|linebreaks }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if job.application_instructions %}
|
||||
<div class="mb-3">
|
||||
<h5>Application Instructions</h5>
|
||||
<div>{{ job.application_instructions|linebreaks }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
|
||||
<!-- Add this section below your existing job details -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5><i class="fas fa-file-signature"></i> Ready to Apply?</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Review the job details on the left, then click the button below to submit your application.</p>
|
||||
<a href="{% url 'applicant:apply_form' job.internal_job_id %}" class="btn btn-success btn-lg w-100">
|
||||
<i class="fas fa-paper-plane"></i> Apply for this Position
|
||||
</a>
|
||||
<p class="text-muted mt-2">
|
||||
<small>You'll be redirected to our secure application form where you can upload your resume and provide additional details.</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@ -1,35 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Application Submitted - {{ job.title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div style="text-align: center; padding: 30px 0;">
|
||||
<div style="width: 80px; height: 80px; background: #d4edda; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 20px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="#28a745" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 style="color: #28a745; margin-bottom: 15px;">Thank You!</h1>
|
||||
<h2>Your application has been submitted successfully</h2>
|
||||
|
||||
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 25px 0; text-align: left;">
|
||||
<p><strong>Position:</strong> {{ job.title }}</p>
|
||||
<p><strong>Job ID:</strong> {{ job.internal_job_id }}</p>
|
||||
<p><strong>Department:</strong> {{ job.department|default:"Not specified" }}</p>
|
||||
{% if job.application_deadline %}
|
||||
<p><strong>Application Deadline:</strong> {{ job.application_deadline|date:"F j, Y" }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<p style="font-size: 18px; line-height: 1.6;">
|
||||
We appreciate your interest in joining our team. Our hiring team will review your application
|
||||
and contact you if there's a potential match for this position.
|
||||
</p>
|
||||
|
||||
{% comment %} <div style="margin-top: 30px;">
|
||||
<a href="/" class="btn btn-primary" style="margin-right: 10px;">Apply to Another Position</a>
|
||||
<a href="{% url 'jobs:job_detail' job.internal_job_id %}" class="btn btn-outline">View Job Details</a>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,24 +0,0 @@
|
||||
import json
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter(name='from_json')
|
||||
def from_json(json_string):
|
||||
"""
|
||||
Safely loads a JSON string into a Python object (list or dict).
|
||||
"""
|
||||
try:
|
||||
# The JSON string comes from the context and needs to be parsed
|
||||
return json.loads(json_string)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
# Handle cases where the string is invalid or None/empty
|
||||
return []
|
||||
|
||||
|
||||
@register.filter(name='split')
|
||||
def split_string(value, key=None):
|
||||
"""Splits a string by the given key (default is space)."""
|
||||
if key is None:
|
||||
return value.split()
|
||||
return value.split(key)
|
||||
@ -1,14 +0,0 @@
|
||||
# from django.db.models.signals import post_save
|
||||
# from django.dispatch import receiver
|
||||
# from . import models
|
||||
#
|
||||
# @receiver(post_save, sender=models.Candidate)
|
||||
# def parse_resume(sender, instance, created, **kwargs):
|
||||
# if instance.resume and not instance.summary:
|
||||
# from .utils import extract_summary_from_pdf,match_resume_with_job_description
|
||||
# summary = extract_summary_from_pdf(instance.resume.path)
|
||||
# if 'error' not in summary:
|
||||
# instance.summary = summary
|
||||
# instance.save()
|
||||
#
|
||||
# # match_resume_with_job_description
|
||||
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@ -1,18 +0,0 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'applicant'
|
||||
|
||||
urlpatterns = [
|
||||
# Form Management
|
||||
path('job/<str:job_id>/forms/', views.job_forms_list, name='job_forms_list'),
|
||||
path('job/<str:job_id>/forms/create/', views.create_form_for_job, name='create_form'),
|
||||
path('form/<int:form_id>/edit/', views.edit_form, name='edit_form'),
|
||||
path('field/<int:field_id>/delete/', views.delete_field, name='delete_field'),
|
||||
path('form/<int:form_id>/activate/', views.activate_form, name='activate_form'),
|
||||
|
||||
# Public Application
|
||||
path('apply/<str:job_id>/', views.apply_form_view, name='apply_form'),
|
||||
path('review/job/detail/<str:job_id>/',views.review_job_detail, name="review_job_detail"),
|
||||
path('apply/<str:job_id>/thank-you/', views.thank_you_view, name='thank_you'),
|
||||
]
|
||||
@ -1,34 +0,0 @@
|
||||
import os
|
||||
import fitz # PyMuPDF
|
||||
import spacy
|
||||
import requests
|
||||
from recruitment import models
|
||||
from django.conf import settings
|
||||
|
||||
nlp = spacy.load("en_core_web_sm")
|
||||
|
||||
def extract_text_from_pdf(pdf_path):
|
||||
text = ""
|
||||
with fitz.open(pdf_path) as doc:
|
||||
for page in doc:
|
||||
text += page.get_text()
|
||||
return text
|
||||
|
||||
def extract_summary_from_pdf(pdf_path):
|
||||
if not os.path.exists(pdf_path):
|
||||
return {'error': 'File not found'}
|
||||
|
||||
text = extract_text_from_pdf(pdf_path)
|
||||
doc = nlp(text)
|
||||
summary = {
|
||||
'name': doc.ents[0].text if doc.ents else '',
|
||||
'skills': [chunk.text for chunk in doc.noun_chunks if len(chunk.text.split()) > 1],
|
||||
'summary': text[:500]
|
||||
}
|
||||
return summary
|
||||
|
||||
def match_resume_with_job_description(resume, job_description,prompt=""):
|
||||
resume_doc = nlp(resume)
|
||||
job_doc = nlp(job_description)
|
||||
similarity = resume_doc.similarity(job_doc)
|
||||
return similarity
|
||||
@ -1,175 +0,0 @@
|
||||
# applicant/views.py (Updated edit_form function)
|
||||
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.contrib import messages
|
||||
from django.http import Http404, JsonResponse # <-- Import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt # <-- Needed for JSON POST if not using FormData
|
||||
import json # <-- Import json
|
||||
from django.db import transaction # <-- Import transaction
|
||||
|
||||
# (Keep all your existing imports)
|
||||
from .models import ApplicantForm, FormField, ApplicantSubmission
|
||||
from .forms import ApplicantFormCreateForm, FormFieldForm
|
||||
from jobs.models import JobPosting
|
||||
from .forms_builder import create_dynamic_form
|
||||
|
||||
# ... (Keep all other functions like job_forms_list, create_form_for_job, etc.)
|
||||
# ...
|
||||
|
||||
|
||||
|
||||
# === FORM MANAGEMENT VIEWS ===
|
||||
|
||||
def job_forms_list(request, job_id):
|
||||
"""List all forms for a specific job"""
|
||||
job = get_object_or_404(JobPosting, internal_job_id=job_id)
|
||||
forms = job.applicant_forms.all()
|
||||
return render(request, 'applicant/job_forms_list.html', {
|
||||
'job': job,
|
||||
'forms': forms
|
||||
})
|
||||
|
||||
def create_form_for_job(request, job_id):
|
||||
"""Create a new form for a job"""
|
||||
job = get_object_or_404(JobPosting, internal_job_id=job_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ApplicantFormCreateForm(request.POST)
|
||||
if form.is_valid():
|
||||
applicant_form = form.save(commit=False)
|
||||
applicant_form.job_posting = job
|
||||
applicant_form.save()
|
||||
messages.success(request, 'Form created successfully!')
|
||||
return redirect('applicant:job_forms_list', job_id=job_id)
|
||||
else:
|
||||
form = ApplicantFormCreateForm()
|
||||
|
||||
return render(request, 'applicant/create_form.html', {
|
||||
'job': job,
|
||||
'form': form
|
||||
})
|
||||
|
||||
|
||||
@transaction.atomic # Ensures all fields are saved or none are
|
||||
def edit_form(request, form_id):
|
||||
"""Edit form details and manage fields, including dynamic builder save."""
|
||||
applicant_form = get_object_or_404(ApplicantForm, id=form_id)
|
||||
job = applicant_form.job_posting
|
||||
|
||||
if request.method == 'POST':
|
||||
# --- 1. Handle JSON data from the Form Builder (JavaScript) ---
|
||||
if request.content_type == 'application/json':
|
||||
try:
|
||||
field_data = json.loads(request.body)
|
||||
|
||||
# Clear existing fields for this form
|
||||
applicant_form.fields.all().delete()
|
||||
|
||||
# Create new fields from the JSON data
|
||||
for field_config in field_data:
|
||||
# Sanitize/ensure required fields are present
|
||||
FormField.objects.create(
|
||||
form=applicant_form,
|
||||
label=field_config.get('label', 'New Field'),
|
||||
field_type=field_config.get('field_type', 'text'),
|
||||
required=field_config.get('required', True),
|
||||
help_text=field_config.get('help_text', ''),
|
||||
choices=field_config.get('choices', ''),
|
||||
order=field_config.get('order', 0),
|
||||
# field_name will be auto-generated/re-generated on save() if needed
|
||||
)
|
||||
|
||||
return JsonResponse({'status': 'success', 'message': 'Form structure saved successfully!'})
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({'status': 'error', 'message': 'Invalid JSON data.'}, status=400)
|
||||
except Exception as e:
|
||||
return JsonResponse({'status': 'error', 'message': f'Server error: {str(e)}'}, status=500)
|
||||
|
||||
# --- 2. Handle standard POST requests (e.g., saving form details) ---
|
||||
elif 'save_form_details' in request.POST: # Changed the button name for clarity
|
||||
form_details = ApplicantFormCreateForm(request.POST, instance=applicant_form)
|
||||
if form_details.is_valid():
|
||||
form_details.save()
|
||||
messages.success(request, 'Form details updated successfully!')
|
||||
return redirect('applicant:edit_form', form_id=form_id)
|
||||
|
||||
# Note: The 'add_field' branch is now redundant since we use the builder,
|
||||
# but you can keep it if you want the old manual way too.
|
||||
|
||||
# --- GET Request (or unsuccessful POST) ---
|
||||
form_details = ApplicantFormCreateForm(instance=applicant_form)
|
||||
# Get initial fields to load into the JS builder
|
||||
initial_fields_json = list(applicant_form.fields.values(
|
||||
'label', 'field_type', 'required', 'help_text', 'choices', 'order', 'field_name'
|
||||
))
|
||||
|
||||
return render(request, 'applicant/edit_form.html', {
|
||||
'applicant_form': applicant_form,
|
||||
'job': job,
|
||||
'form_details': form_details,
|
||||
'initial_fields_json': json.dumps(initial_fields_json)
|
||||
})
|
||||
|
||||
def delete_field(request, field_id):
|
||||
"""Delete a form field"""
|
||||
field = get_object_or_404(FormField, id=field_id)
|
||||
form_id = field.form.id
|
||||
field.delete()
|
||||
messages.success(request, 'Field deleted successfully!')
|
||||
return redirect('applicant:edit_form', form_id=form_id)
|
||||
|
||||
def activate_form(request, form_id):
|
||||
"""Activate a form (deactivates others automatically)"""
|
||||
applicant_form = get_object_or_404(ApplicantForm, id=form_id)
|
||||
applicant_form.activate()
|
||||
messages.success(request, f'Form "{applicant_form.name}" is now active!')
|
||||
return redirect('applicant:job_forms_list', job_id=applicant_form.job_posting.internal_job_id)
|
||||
|
||||
# === PUBLIC VIEWS (for applicants) ===
|
||||
|
||||
def apply_form_view(request, job_id):
|
||||
"""Public application form - serves active form"""
|
||||
job = get_object_or_404(JobPosting, internal_job_id=job_id, status='ACTIVE')
|
||||
|
||||
if job.is_expired():
|
||||
raise Http404("Application deadline has passed")
|
||||
|
||||
try:
|
||||
applicant_form = job.applicant_forms.get(is_active=True)
|
||||
except ApplicantForm.DoesNotExist:
|
||||
raise Http404("No active application form configured for this job")
|
||||
|
||||
DynamicForm = create_dynamic_form(applicant_form)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = DynamicForm(request.POST)
|
||||
if form.is_valid():
|
||||
ApplicantSubmission.objects.create(
|
||||
job_posting=job,
|
||||
form=applicant_form,
|
||||
data=form.cleaned_data,
|
||||
ip_address=request.META.get('REMOTE_ADDR')
|
||||
)
|
||||
return redirect('applicant:thank_you', job_id=job_id)
|
||||
else:
|
||||
form = DynamicForm()
|
||||
|
||||
return render(request, 'applicant/apply_form.html', {
|
||||
'form': form,
|
||||
'job': job,
|
||||
'applicant_form': applicant_form
|
||||
})
|
||||
|
||||
def review_job_detail(request,job_id):
|
||||
"""Public job detail view for applicants"""
|
||||
job = get_object_or_404(JobPosting, internal_job_id=job_id, status='ACTIVE')
|
||||
if job.is_expired():
|
||||
raise Http404("This job posting has expired.")
|
||||
return render(request,'applicant/review_job_detail.html',{'job':job})
|
||||
|
||||
|
||||
|
||||
|
||||
def thank_you_view(request, job_id):
|
||||
job = get_object_or_404(JobPosting, internal_job_id=job_id)
|
||||
return render(request, 'applicant/thank_you.html', {'job': job})
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 231 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 226 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB |
@ -1,120 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.425 132.744" style="version:1">
|
||||
<switch>
|
||||
<foreignObject width="1" height="1" x="0" y="0" requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"/>
|
||||
<g>
|
||||
<path fill="#ADA9AE" d="M141.468 75.372v0.145c0 9.575-6.977 16.611-18.315 16.611 -9.157 0-15.48-3.627-19.84-8.777l7.704-7.327c3.488 3.918 7.122 6.093 12.282 6.093 4.215 0 7.195-2.393 7.195-6.165 -0.056-0.1 0.822-6.6-9.957-6.6h-4.651l-1.745-7.11 16.32-16.385 -6.54 4.2h-17.628v-9.574h34.375v8.414l-12.863 12.258C134.709 62.316 141.468 65.942 141.468 75.372M109.563 17.265c0-4.597 3.302-8.297 7.936-8.297h5.222l-1.367 0.681c0.062 12.6-0.25 14.187 0.556 16.029l-4.353-0.001c-4.634 0-7.994-3.758-7.994-8.354V17.265zM117.499 27.788h4.637v-1.667c0.338 0.586 0.793 1.041 1.464 1.359 1.475 0.7 0.403 0.26 42.703 0.39V6.858h-2.231v18.785c0 0-38.678 0.149-39.413-0.181 -0.638-0.287-0.87-0.943-0.976-1.642 -0.169-1.135-0.066-1.156-0.097-16.962h-6.029c-6.169 0-10.369 4.885-10.369 10.465v0.058C107.188 22.96 111.33 27.788 117.499 27.788M182.746 4.63h-4.888c0.106-1.616-0.642-4.63 2.651-4.63h1.574v1.552c-1.524 0.113-2.401-0.371-2.401 0.846v0.515h3.064V4.63zM188.396 28.097c-0.595 0-1.2-0.083-1.813-0.251l0.586-2.143c1.009 0.287 2.299 0.304 2.831-0.598 0.621-1.068 0.399-1.444 0.432-18.247h2.231c-0.064 15.685 0.154 16.123-0.209 17.941C192.026 26.926 190.736 28.097 188.396 28.097M123.586 4.63h-2.232V2.403h2.232V4.63zM172.692 12.314c1.047-2.039 3.245-3.174 5.492-3.198V9.112h3.605l-1.274 0.635v10c-1.756-0.052-2.99 0.167-4.616-0.349 -2.22-0.698-3.724-2.406-3.724-4.885C172.175 13.716 172.347 12.982 172.692 12.314M172.692 20.206c2.905 2.07 6.866 1.728 7.073 1.767l0.75-1.571c-0.121 1.523 0.419 3.651-1.004 4.717 -1.203 0.899-3.348 0.87-4.672 0.556 -0.548-0.129-1.176-0.352-1.883-0.667l-0.697 2.143c1.414 0.521 2.766 0.946 4.267 0.946 2.176 0 3.756-0.515 4.742-1.544 0.985-1.031 1.478-2.418 1.478-4.162V6.858c-2.746 0.086-4.829-0.277-7.335 0.585C168.696 9.752 168.562 17.266 172.692 20.206M119.123 4.63h-2.232V2.403h2.232V4.63zM96.028 6.834h2.304v20.911h-1.886L81.607 8.936l1.312 3.048v15.761h-2.305V6.834h2.215l14.17 17.99 -0.971-2.566V6.834zM9.009 27.894L0 6.834h2.634c0.134 0.324 7.314 17.679 8.042 19.439l-0.228-2.298 7.121-17.141h2.543l-9.008 21.06H9.009zM48.932 6.834h2.364v20.91h-2.364V6.834zM74.29 17.349c0 4.75-3.411 8.573-8.2 8.573s-8.261-3.883-8.261-8.632v-0.061c0-4.749 3.413-8.572 8.201-8.572 4.789 0 8.26 3.883 8.26 8.633V17.349zM66.09 6.476c-6.375 0-10.715 5.048-10.715 10.814v0.059c0 5.765 4.28 10.754 10.655 10.754s10.715-5.048 10.715-10.813v-0.061C76.745 11.465 72.465 6.476 66.09 6.476M45.031 22.069v0.059c0 3.584-2.993 5.915-7.153 5.915 -3.322 0-6.045-1.105-8.559-3.346l1.466-1.732c2.185 1.971 4.28 2.956 7.184 2.956 2.813 0 4.668-1.492 4.668-3.554v-0.059c0-5.836-12.48-1.986-12.48-10.008v-0.059c0-3.286 2.903-5.706 6.884-5.706 3.052 0 5.237 0.867 7.362 2.57l-1.376 1.821c-4.75-3.864-10.506-2.384-10.506 1.106v0.06c0 1.971 1.077 3.076 5.687 4.062C42.877 17.17 45.031 18.873 45.031 22.069M25.688 27.745h-2.364V6.835h2.364V27.745zM163.293 30.096h2.232v2.227h-2.232V30.096zM158.83 30.096h2.232v2.227h-2.232V30.096zM18.71 81.609h19.398v9.649H0.461v-8.85c18.811-15.964 25.728-19.084 25.728-26.04 0-4.28-2.835-6.601-6.832-6.601 -3.925 0-6.614 2.175-10.393 6.817l-7.849-6.309C6.13 43.456 11 39.756 20.084 39.756c10.538 0 17.515 6.166 17.515 15.669v0.144c0 11.916-9.021 15.757-24.49 28.223l-2.16 1.664L18.71 81.609zM186.725 66.015c0 8.704-6.25 15.813-15.263 15.813 -9.012 0-15.407-7.254-15.407-15.959v-0.145c0-8.704 6.251-15.812 15.262-15.812 9.011 0 15.408 7.254 15.408 15.957V66.015zM171.462 39.612c-15.697 0-27.108 11.823-27.108 26.257v0.146c0 14.435 11.264 26.113 26.963 26.113s27.108-11.824 27.108-26.259v-0.145C198.425 51.29 187.16 39.612 171.462 39.612"/>
|
||||
<path fill="#29367B" d="M68.518 36.272L71.095 38.74 68.622 41.311 66.046 38.843"/>
|
||||
<path fill="#B1AEB3" d="M74.415 38.843L76.99 41.311 79.463 38.74 76.887 36.272"/>
|
||||
<path fill="#D0DA33" d="M70.787 42.084L72.794 44.005 74.718 42.003 72.713 40.082"/>
|
||||
<path fill="#71BA44" d="M78.142 43.183L80.148 45.105 82.072 43.103 80.066 41.181"/>
|
||||
<path fill="#D0DA33" d="M67.507 45.145L69.513 47.067 71.437 45.065 69.433 43.143"/>
|
||||
<path fill="#27B8BE" d="M74.238 45.378L76.243 47.3 78.168 45.298 76.162 43.376"/>
|
||||
<path fill="#71BA44" d="M71.605 47.681L73.016 49.033 74.37 47.624 72.959 46.272"/>
|
||||
<path fill="#58B75E" d="M76.928 48.742L78.339 50.094 79.694 48.685 78.283 47.334"/>
|
||||
<path fill="#3088C8" d="M88.918 37.593H93.36800000000001V42.035000000000004H88.918z" transform="rotate(-10.39 91.135 39.818)"/>
|
||||
<path fill="#1E3871" d="M83.133 39.508H86.70299999999999V43.072H83.133z" transform="rotate(-10.376 84.927 41.294)"/>
|
||||
<path fill="#27B8BE" d="M89.92 44.395H93.49V47.959H89.92z" transform="rotate(-10.39 91.7 46.17)"/>
|
||||
<path fill="#D0DA33" d="M85.014 44.984H87.794V47.758H85.014z" transform="rotate(-10.382 86.413 46.375)"/>
|
||||
<path fill="#B1AEB3" d="M90.334 50.169H93.114V52.943999999999996H90.334z" transform="rotate(-10.382 91.733 51.56)"/>
|
||||
<path fill="#27B8BE" d="M80.917 46.094H83.69800000000001V48.869H80.917z" transform="rotate(-10.382 82.315 47.485)"/>
|
||||
<path fill="#71BA44" d="M85.88 49.669H88.66V52.443999999999996H85.88z" transform="rotate(-10.382 87.28 51.06)"/>
|
||||
<path fill="#B1AEB3" d="M82.329 50.08H84.285V52.032H82.329z" transform="rotate(-10.39 83.314 51.066)"/>
|
||||
<path fill="#58B75E" d="M86.024 54.05H87.979V56.001H86.024z" transform="rotate(-10.36 87.01 55.033)"/>
|
||||
<path fill="#29367B" d="M101.572 52.342L99.659 56.354 103.679 58.262 105.59 54.251"/>
|
||||
<path fill="#27B8BE" d="M95.867 50.489L94.333 53.707 97.557 55.238 99.091 52.02"/>
|
||||
<path fill="#71BA44" d="M98.505 58.416L96.971 61.635 100.196 63.165 101.73 59.947"/>
|
||||
<path fill="#D0DA33" d="M94.28 56.002L93.086 58.508 95.597 59.7 96.792 57.194"/>
|
||||
<path fill="#D0DA33" d="M95.555 63.315L94.36 65.821 96.87 67.013 98.066 64.507"/>
|
||||
<path fill="#3088C8" d="M90.306 54.51L89.111 57.016 91.622 58.208 92.817 55.702"/>
|
||||
<path fill="#B1AEB3" d="M92.237 60.308L91.042 62.814 93.552 64.006 94.748 61.5"/>
|
||||
<path fill="#71BA44" d="M89.217 58.541L88.376 60.304 90.142 61.142 90.983 59.38"/>
|
||||
<path fill="#27B8BE" d="M89.372 64.241H91.324V66.196H89.372z" transform="rotate(-64.552 90.342 65.217)"/>
|
||||
<path fill="#3088C8" d="M103.737 71.551L99.835 73.687 101.976 77.582 105.878 75.446"/>
|
||||
<path fill="#27B8BE" d="M100.198 66.717L97.068 68.431 98.785 71.555 101.915 69.841"/>
|
||||
<path fill="#27B8BE" d="M97.691 74.686L94.56 76.4 96.278 79.525 99.408 77.811"/>
|
||||
<path fill="#29367B" d="M95.679 70.261L93.242 71.596 94.578 74.028 97.017 72.694"/>
|
||||
<path fill="#58B75E" d="M92.426 76.936L89.988 78.271 91.325 80.703 93.763 79.369"/>
|
||||
<path fill="#1E3871" d="M93.331 66.731L90.893 68.065 92.23 70.499 94.669 69.164"/>
|
||||
<path fill="#D0DA33" d="M91.498 72.56L89.06 73.895 90.397 76.327 92.835 74.993"/>
|
||||
<path fill="#B1AEB3" d="M90.084 69.364L88.369 70.303 89.31 72.014 91.024 71.076"/>
|
||||
<path fill="#27B8BE" d="M87.475 74.114L85.76 75.053 86.7 76.764 88.415 75.826"/>
|
||||
<path fill="#71BA44" d="M89.529 88.1H93.972V92.55099999999999H89.529z" transform="rotate(-82.958 91.74 90.32)"/>
|
||||
<path fill="#1E3871" d="M90.424 82.17H93.988V85.74H90.424z" transform="rotate(-82.935 92.21 83.955)"/>
|
||||
<path fill="#27B8BE" d="M83.719 87.169H87.28299999999999V90.74H83.719z" transform="rotate(-82.935 85.504 88.955)"/>
|
||||
<path fill="#71BA44" d="M85.516 82.459H88.29100000000001V85.239H85.516z" transform="rotate(-82.952 86.912 83.852)"/>
|
||||
<path fill="#B1AEB3" d="M78.966 85.97H81.741V88.75H78.966z" transform="rotate(-82.952 80.36 87.365)"/>
|
||||
<path fill="#B1AEB3" d="M85.681 78.224H88.457V81.00500000000001H85.681z" transform="rotate(-82.935 87.072 79.615)"/>
|
||||
<path fill="#71BA44" d="M80.778 81.88H83.55300000000001V84.661H80.778z" transform="rotate(-82.935 82.168 83.27)"/>
|
||||
<path fill="#B1AEB3" d="M82.377 78.518H84.329V80.473H82.377z" transform="rotate(-82.946 83.36 79.498)"/>
|
||||
<path fill="#B1AEB3" d="M77.476 80.846H79.428V82.802H77.476z" transform="rotate(-82.952 78.455 81.827)"/>
|
||||
<path fill="#D0DA33" d="M76.652 96.502L73.39 93.479 70.362 96.735 73.624 99.757"/>
|
||||
<path fill="#71BA44" d="M76.051 34.279L72.789 31.256 69.761 34.512 73.023 37.534"/>
|
||||
<path fill="#27B8BE" d="M80.133 91.625L77.516 89.2 75.087 91.812 77.704 94.236"/>
|
||||
<path fill="#71BA44" d="M71.765 91.764L69.147 89.339 66.718 91.951 69.335 94.375"/>
|
||||
<path fill="#3088C8" d="M75.337 88.463L73.299 86.575 71.408 88.608 73.445 90.496"/>
|
||||
<path fill="#D0DA33" d="M67.965 87.486L65.928 85.598 64.037 87.631 66.075 89.52"/>
|
||||
<path fill="#D0DA33" d="M77.954 85.126L75.916 83.238 74.025 85.271 76.062 87.159"/>
|
||||
<path fill="#B1AEB3" d="M71.833 85.227L69.795 83.339 67.904 85.372 69.941 87.26"/>
|
||||
<path fill="#1E3871" d="M74.426 82.881L72.993 81.552 71.663 82.983 73.096 84.311"/>
|
||||
<path fill="#B1AEB3" d="M69.087 81.907L67.654 80.579 66.323 82.009 67.756 83.337"/>
|
||||
<path fill="#3088C8" d="M52.797 88.848H57.248V93.28999999999999H52.797z" transform="rotate(-11.333 55.022 91.072)"/>
|
||||
<path fill="#27B8BE" d="M59.437 87.709H63.007V91.273H59.437z" transform="rotate(-11.333 61.207 89.493)"/>
|
||||
<path fill="#71BA44" d="M52.569 82.934H56.139V86.49799999999999H52.569z" transform="rotate(-11.344 54.34 84.698)"/>
|
||||
<path fill="#D0DA33" d="M58.262 83.048H61.042V85.82300000000001H58.262z" transform="rotate(-11.333 59.64 84.44)"/>
|
||||
<path fill="#29367B" d="M62.34 81.869H65.12V84.645H62.34z" transform="rotate(-11.333 63.72 83.263)"/>
|
||||
<path fill="#3088C8" d="M57.317 78.377H60.097V81.152H57.317z" transform="rotate(-11.333 58.696 79.77)"/>
|
||||
<path fill="#71BA44" d="M61.693 78.724H63.649V80.676H61.693z" transform="rotate(-11.314 62.682 79.71)"/>
|
||||
<path fill="#B1AEB3" d="M57.934 74.815H59.888999999999996V76.767H57.934z" transform="rotate(-11.344 58.916 75.795)"/>
|
||||
<path fill="#27B8BE" d="M44.388 78.715L46.233 74.673 42.182 72.831 40.337 76.873"/>
|
||||
<path fill="#D0DA33" d="M50.122 80.474L51.602 77.231 48.353 75.754 46.872 78.997"/>
|
||||
<path fill="#3088C8" d="M47.351 72.592L48.832 69.349 45.582 67.871 44.102 71.115"/>
|
||||
<path fill="#27B8BE" d="M49.54 71.708H52.316V74.487H49.54z" transform="rotate(-65.485 50.93 73.102)"/>
|
||||
<path fill="#B1AEB3" d="M55.002 81.249L56.155 78.724 53.625 77.574 52.472 80.099"/>
|
||||
<path fill="#29367B" d="M48.145 64.418H50.92100000000001V67.197H48.145z" transform="rotate(-65.474 49.535 65.806)"/>
|
||||
<path fill="#58B75E" d="M53.538 73.134H56.31399999999999V75.913H53.538z" transform="rotate(-65.474 54.928 74.522)"/>
|
||||
<path fill="#D0DA33" d="M51.512 67.369H54.288V70.148H51.512z" transform="rotate(-65.455 52.905 68.762)"/>
|
||||
<path fill="#1E3871" d="M56.637 72.312L57.448 70.536 55.668 69.726 54.857 71.503"/>
|
||||
<path fill="#71BA44" d="M55.879 66.948L56.69 65.171 54.91 64.362 54.099 66.139"/>
|
||||
<path fill="#27B8BE" d="M41.902 59.544L45.769 57.344 43.563 53.485 39.697 55.685"/>
|
||||
<path fill="#71BA44" d="M45.522 64.32L48.624 62.555 46.854 59.459 43.753 61.225"/>
|
||||
<path fill="#1E3871" d="M47.897 56.31L50.999 54.545 49.229 51.448 46.128 53.214"/>
|
||||
<path fill="#3088C8" d="M49.982 60.701L52.396 59.326 51.019 56.916 48.604 58.29"/>
|
||||
<path fill="#71BA44" d="M53.124 53.974L55.538 52.599 54.161 50.189 51.746 51.563"/>
|
||||
<path fill="#27B8BE" d="M52.387 64.192L54.803 62.818 53.426 60.407 51.01 61.782"/>
|
||||
<path fill="#D0DA33" d="M54.124 58.334L56.539 56.96 55.162 54.549 52.747 55.924"/>
|
||||
<path fill="#B1AEB3" d="M55.591 61.506L57.29 60.539 56.321 58.844 54.622 59.81"/>
|
||||
<path fill="#71BA44" d="M58.12 56.713L59.82 55.746 58.851 54.051 57.151 55.018"/>
|
||||
<path fill="#71BA44" d="M51.354 38.35H55.796V42.800000000000004H51.354z" transform="rotate(-83.898 53.578 40.576)"/>
|
||||
<path fill="#B1AEB3" d="M51.445 45.166H55.008V48.736999999999995H51.445z" transform="rotate(-83.892 53.227 46.953)"/>
|
||||
<path fill="#27B8BE" d="M58.066 40.057H61.63V43.628H58.066z" transform="rotate(-83.892 59.848 41.844)"/>
|
||||
<path fill="#D0DA33" d="M57.142 45.581H59.917V48.361000000000004H57.142z" transform="rotate(-83.886 58.533 46.972)"/>
|
||||
<path fill="#3088C8" d="M63.634 41.961H66.409V44.741H63.634z" transform="rotate(-83.91 65.016 43.35)"/>
|
||||
<path fill="#27B8BE" d="M57.047 49.818H59.821999999999996V52.598H57.047z" transform="rotate(-83.886 58.435 51.21)"/>
|
||||
<path fill="#71BA44" d="M61.889 46.08H64.664V48.86H61.889z" transform="rotate(-83.886 63.278 47.47)"/>
|
||||
<path fill="#1E3871" d="M61.176 50.287H63.128V52.243H61.176z" transform="rotate(-83.892 62.153 51.266)"/>
|
||||
<path fill="#B1AEB3" d="M66.039 47.877H67.991V49.834H66.039z" transform="rotate(-83.927 67.016 48.854)"/>
|
||||
<path fill="#727A82" d="M86.941 68.235c-2.33-1.342-5.572-1.732-8.931-0.502 -1.468 0.555-2.938 1.524-4.178 2.473 -0.781-4.735-0.4-9.13-0.218-11.289 0.001-0.012 0.015-0.018 0.026-0.011 1.677 1.055 1.207 4.003 0.855 4.846 -0.007 0.017 0.018 0.028 0.03 0.014 2.09-2.468 1.069-5.193 0.64-5.883 -0.078-0.125 0.395 0.254 0.783 0.751 1.461 1.86 0.786 3.949 0.949 3.9 0.004-0.001 0.009 0 0.011-0.005 0.971-1.905 0.659-4.321-1.338-6.066 -0.091-0.077 0.265 0.039 0.378 0.079 1.703 0.591 2.591 2.345 2.579 3.909 0 0.344 1.15-2.938-1.63-4.606 -0.52-0.311-1.054-0.482-1.403-0.524 -0.272-0.029 1.489-0.294 2.726 0.544 0.743 0.501 0.938 1.107 0.919 0.937 -0.216-2.197-2.806-3.065-4.376-2.406 -0.166 0.083 0.618-1.079 2.134-0.989 0.292 0.017 0.562 0.08 0.778 0.183 0.015 0.007 0.029-0.013 0.018-0.026 -1.145-1.309-3.029-1.336-4.029 0.178 -0.007 0.01-0.022 0.01-0.028 0 -0.379-0.679-0.696-1.548-0.833-2.368 -0.033-0.21-0.103 1.015-0.863 2.368 -0.006 0.01-0.02 0.01-0.027 0 -1.003-1.518-2.885-1.488-4.029-0.178 -0.012 0.013 0.002 0.033 0.018 0.026 0.764-0.363 2.207-0.256 2.93 0.781 0.009 0.014-0.004 0.032-0.02 0.025 -1.618-0.68-4.16 0.243-4.375 2.406 -0.001 0.018 0.023 0.028 0.032 0.012 0.072-0.135 0.25-0.423 0.583-0.715 1.294-1.14 3.329-0.81 3.03-0.778 -0.795 0.098-2.427 0.806-3.03 2.424 -0.493 1.322-0.018 3.063 0 2.632 0.016-1.471 0.859-3.441 2.94-3.943 0.016-0.004 0.028 0.018 0.015 0.029 -3.234 2.843-1.278 6.334-1.308 6.058 -0.1-1.127-0.183-3.139 1.686-4.658 0.014-0.011 0.036 0.004 0.026 0.02 -0.429 0.69-1.449 3.415 0.64 5.883 0.012 0.014 0.037 0.003 0.03-0.014 -0.351-0.843-0.821-3.791 0.855-4.846 0.011-0.007 0.025-0.001 0.026 0.011 0.183 2.159 0.564 6.554-0.218 11.289 -2.656-2.03-5.271-3.361-8.639-3.213 -1.694 0.074-3.301 0.567-4.47 1.242 -0.042 0.024-0.011 0.085 0.034 0.071 4.899-1.566 9.139 0.334 13.081 3.614 -1.991 1.735-3.798 3.551-4.848 4.099 -0.245-0.335-0.67-0.528-1.135-0.427 -0.876 0.193-1.085 1.29-0.61 1.762 1.721 1.717 4.412-1.69 7.631-4.538 3.098 2.738 5.913 6.254 7.631 4.538 0.137-0.137 0.296-0.589 0.214-0.948 -0.207-0.896-1.398-1.155-1.959-0.387 -1.01-0.509-2.783-2.301-4.848-4.099 3.97-3.309 8.164-5.156 13.08-3.614C86.952 68.32 86.983 68.259 86.941 68.235"/>
|
||||
<path fill="#ADA9AE" d="M71.265 115.108h1.277v-11.436h-1.277V115.108zM37.804 113.833l-1.995-4.891c2.064-1.747 4.625-1.426 6.273 0l-1.995 4.891H37.804zM30.764 113.833c-1.004-0.03-1.71 0.095-2.642-0.198 -3.451-1.09-2.513-5.888 1.349-5.888h1.293V113.833zM66.268 113.833c-2.057-0.063-3.91 0.363-4.853-1.274 0.186-0.818 0.084-1.552 0.112-5.306H60.25v4.318c0 3.169-3.824 2.963-4.726 1.06 -0.362-0.765-0.141-1.085-0.207-5.378h-1.278v4.414c0 1.44-0.901 2.166-2.155 2.166H49.41v-6.58h-1.277v6.58h-6.705l2.235-5.337c-1.268-1.108-3.033-2.151-4.725-2.151 -1.719 0-3.45 0.998-4.726 2.151l2.251 5.337h-4.422v-7.376h-2.043c-2.888 0-5.285 1.66-5.285 4.381 0 4.843 5.941 4.246 6.051 4.27 -0.02 0.157 0.159 1.251-0.575 1.801 -0.686 0.514-1.911 0.499-2.674 0.319 -0.314-0.075-0.673-0.202-1.077-0.383l-0.399 1.227c0.81 0.298 1.583 0.542 2.442 0.542 2.434 0 3.56-1.214 3.56-3.267v-0.239c20.842-0.131 20.226 0.3 21.496-0.398 0.484-0.265 0.875-0.621 1.173-1.068 0.309 0.51 0.729 0.909 1.261 1.196 1.117 0.599 2.821 0.543 3.856 0.031 0.537-0.265 0.95-0.674 1.237-1.227 0.493 0.842 1.258 1.383 2.794 1.451v0.015h3.687v-11.436h-1.277V113.833zM180.858 113.268c-1.13 2.641-5.02 2.717-6.146-0.04 -0.331-0.809-0.331-1.768 0-2.589 1.363-3.378 6.41-2.408 6.41 1.299C181.122 112.415 181.034 112.859 180.858 113.268M168.47 113.268c-0.977 2.27-3.921 2.606-5.46 1.02 -1.263-1.306-1.226-3.433-0.007-4.716 0.993-1.055 2.524-1.225 3.695-0.742C168.544 109.6 169.159 111.667 168.47 113.268M137.412 113.833c-0.977-0.03-1.685 0.098-2.611-0.206 -3.363-1.105-2.561-5.896 1.349-5.896h1.262V113.833zM186.389 113.833H182c1.37-3.007-0.758-6.532-4.263-6.532 -3.228 0-5.486 3.308-4.167 6.532h-3.959c1.335-2.919-0.688-6.532-4.262-6.532 -3.23 0-5.486 3.309-4.167 6.532h-4.055v-10.161h-1.277v10.161h-4.07c-0.07-1.309 0.297-3.246-0.799-4.469 -0.743-0.838-1.57-1.099-2.698-1.099h-5.651l2.404-4.593h-1.389l-2.452 4.577c0.079 0.508 0.233 0.905 0.575 1.29 6.387 0.054 6.804-0.134 7.559 0.184 0.592 0.245 0.953 0.692 1.078 1.235 0.152 0.654 0.075 1.146 0.096 2.875h-11.814v-7.376h-2.012c-3.384 0-5.268 2.013-5.268 4.333 0 2.907 2.338 4.318 5.268 4.318h25.335c1.879 2.024 5.042 1.852 6.769 0h5.619c1.873 2.018 5.033 1.859 6.77 0h6.497v-11.436h-1.278V113.833zM191.386 115.108h1.277v-11.436h-1.277V115.108zM97.288 117.914H98.6v-1.309h-1.312V117.914zM109.774 113.833l-1.996-4.891c2.048-1.734 4.605-1.441 6.274 0l-1.995 4.891H109.774zM118.825 113.833h-5.427l2.234-5.337c-1.271-1.114-3.042-2.151-4.725-2.151 -1.725 0-3.451 1.003-4.726 2.151l2.251 5.337h-4.756v-7.376h-1.278v8.922c0 1.127-0.082 1.96-1.213 1.96 -0.263 0-0.402-0.023-0.654-0.096l-0.335 1.228c1.067 0.292 2.327 0.194 2.953-0.726 0.566-0.833 0.499-1.983 0.527-2.637h16.427v-11.436h-1.278V113.833zM21.872 107.572c-1.42-1.55-3.064-0.988-3.624-1.115v1.274c0.932 0.036 1.363-0.103 1.995 0.199 0.532 0.256 0.908 0.669 1.062 1.251 0.173 0.669 0.076 1.191 0.104 4.652h-5.987v1.275h7.264c-0.053-4.83 0.132-5.342-0.2-6.421C22.353 108.252 22.148 107.88 21.872 107.572M92.619 117.929h1.312v-1.309h-1.312V117.929zM84.854 113.833c-0.975-0.03-1.684 0.098-2.611-0.206 -3.367-1.106-2.556-5.896 1.349-5.896h1.262V113.833zM98.583 106.457h-1.277v7.376h-4.821v-7.376h-1.277v7.376h-5.077v-7.376c-1.606 0.05-4.002-0.325-5.843 1.234 -2.61 2.211-1.937 7.417 3.832 7.417h14.463V106.457zM90.382 117.929h1.312v-1.309h-1.312V117.929zM5.037 103.672H3.725v1.309h1.312V103.672zM136.446 103.672h-1.312v1.309h1.312V103.672zM86.125 103.672h-1.312v1.309h1.312V103.672zM12.571 117.929h1.312v-1.309h-1.312V117.929zM83.888 103.672h-1.312v1.309h1.312V103.672zM7.274 103.672H5.962v1.309h1.312V103.672zM10.334 117.929h1.312v-1.309h-1.312V117.929zM6.003 113.833c-0.973-0.03-1.686 0.098-2.61-0.206 -3.361-1.102-2.567-5.896 1.349-5.896h1.261V113.833zM13.634 106.457h-1.277v7.376H7.28v-7.376c-1.531 0.047-2.748-0.153-4.183 0.326 -1.203 0.404-2.186 1.147-2.722 2.287 -0.558 1.186-0.457 2.696 0.057 3.664 0.925 1.746 2.86 2.374 4.836 2.374h8.366V106.457zM123.822 115.108h1.277v-11.436h-1.277V115.108zM138.682 103.672h-1.312v1.309h1.312V103.672z"/>
|
||||
<path fill="#ADA9AE" d="M1.251 123.484L2.854 123.484 2.854 128.062 7.193 123.484 9.161 123.484 5.369 127.399 9.33 132.588 7.389 132.588 4.275 128.491 2.854 129.948 2.854 132.588 1.251 132.588"/>
|
||||
<path fill="#ADA9AE" d="M10.792 123.484H12.395V132.588H10.792z"/>
|
||||
<path fill="#ADA9AE" d="M14.863 123.484L16.349 123.484 21.236 129.779 21.236 123.484 22.812 123.484 22.812 132.588 21.47 132.588 16.44 126.111 16.44 132.588 14.863 132.588"/>
|
||||
<path fill="#ADA9AE" d="M24.797 128.062v-0.026c0-2.549 1.955-4.709 4.704-4.709 1.59 0 2.568 0.443 3.506 1.236l-1.016 1.21c-2.47-2.098-5.512-0.587-5.512 2.237v0.026c0 1.873 1.237 3.251 3.1 3.251 0.861 0 1.643-0.273 2.203-0.689v-1.704h-2.333v-1.391h3.884v3.823C30.071 134.113 24.797 132.681 24.797 128.062"/>
|
||||
<path fill="#ADA9AE" d="M38.948 131.131c4.254 0 4.203-6.191 0-6.191H37.15v6.191H38.948zM35.547 123.484h3.401c6.508 0 6.42 9.104 0 9.104h-3.401V123.484z"/>
|
||||
<path fill="#ADA9AE" d="M53.155 128.062v-0.026c0-1.769-1.29-3.238-3.102-3.238 -1.811 0-3.075 1.443-3.075 3.212v0.026c0 1.769 1.29 3.239 3.101 3.239C51.891 131.275 53.155 129.831 53.155 128.062M45.297 128.062v-0.026c0-2.562 1.981-4.709 4.782-4.709 2.802 0 4.757 2.121 4.757 4.683v0.026c0 2.562-1.98 4.708-4.783 4.708C47.252 132.744 45.297 130.624 45.297 128.062"/>
|
||||
<path fill="#ADA9AE" d="M56.827 123.484L58.534 123.484 61.31 127.789 64.085 123.484 65.792 123.484 65.792 132.588 64.189 132.588 64.189 126.059 61.31 130.351 61.258 130.351 58.404 126.085 58.404 132.588 56.827 132.588"/>
|
||||
<path fill="#ADA9AE" d="M79.506 128.062v-0.026c0-1.769-1.29-3.238-3.102-3.238 -1.811 0-3.075 1.443-3.075 3.212v0.026c0 1.769 1.29 3.239 3.101 3.239C78.242 131.275 79.506 129.831 79.506 128.062M71.648 128.062v-0.026c0-2.562 1.981-4.709 4.782-4.709 2.802 0 4.757 2.121 4.757 4.683v0.026c0 2.562-1.981 4.708-4.783 4.708C73.603 132.744 71.648 130.624 71.648 128.062"/>
|
||||
<path fill="#ADA9AE" d="M83.179 123.484L89.968 123.484 89.968 124.941 84.782 124.941 84.782 127.425 89.382 127.425 89.382 128.881 84.782 128.881 84.782 132.588 83.179 132.588"/>
|
||||
<path fill="#ADA9AE" d="M95.027 131.262l0.964-1.145c0.873 0.755 1.746 1.184 2.88 1.184 0.99 0 1.616-0.455 1.616-1.145v-0.025c0-2.034-5.096-0.646-5.096-4.111v-0.026c0-2.794 3.911-3.507 6.425-1.495l-0.86 1.209c-2.098-1.561-3.961-0.947-3.961 0.144v0.026c0 2.005 5.095 0.737 5.095 4.096V130C102.09 133.192 97.564 133.521 95.027 131.262"/>
|
||||
<path fill="#ADA9AE" d="M109.296 128.973l-1.577-3.642 -1.564 3.642H109.296zM107.002 123.419h1.485l4.014 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L107.002 123.419z"/>
|
||||
<path fill="#ADA9AE" d="M113.444 128.726v-5.242h1.603v5.177c0 1.691 0.873 2.601 2.306 2.601 1.421 0 2.294-0.858 2.294-2.536v-5.242h1.603v5.164c0 2.718-1.538 4.084-3.923 4.084C114.955 132.732 113.444 131.366 113.444 128.726"/>
|
||||
<path fill="#ADA9AE" d="M126.997 131.131c4.254 0 4.204-6.191 0-6.191h-1.798v6.191H126.997zM123.596 123.484h3.401c6.508 0 6.42 9.104 0 9.104h-3.401V123.484z"/>
|
||||
<path fill="#ADA9AE" d="M133.875 123.484H135.478V132.588H133.875z"/>
|
||||
<path fill="#ADA9AE" d="M147.352 128.973l-1.577-3.642 -1.564 3.642H147.352zM145.058 123.419h1.485l4.014 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L145.058 123.419z"/>
|
||||
<path fill="#ADA9AE" d="M156.02 127.997c1.147 0 1.877-0.598 1.877-1.522v-0.026c0-0.975-0.704-1.509-1.89-1.509h-2.332v3.057H156.02zM152.072 123.484h4.065c1.928 0 3.389 0.969 3.389 2.874 -0.037 0.1 0.19 2.108-2.177 2.784l2.463 3.446h-1.89l-2.241-3.174c-0.175 0-2.109 0-2.006 0v3.174h-1.603V123.484z"/>
|
||||
<path fill="#ADA9AE" d="M167.299 128.973l-1.577-3.642 -1.564 3.642H167.299zM165.005 123.419h1.486l4.013 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L165.005 123.419z"/>
|
||||
<path fill="#ADA9AE" d="M176.253 131.171c1.095 0 1.759-0.43 1.759-1.249v-0.026c0-0.767-0.612-1.223-1.876-1.223h-2.541v2.498H176.253zM175.784 127.321c1.029 0 1.72-0.403 1.72-1.236v-0.026c0-0.715-0.573-1.157-1.603-1.157h-2.306v2.419H175.784zM172.018 123.484h4.092c1.604 0 2.997 0.75 2.997 2.315 -0.037 0.1 0.164 1.294-1.303 2.055 1.069 0.364 1.811 0.976 1.811 2.211v0.026c0 1.626-1.342 2.497-3.375 2.497h-4.222V123.484z"/>
|
||||
<path fill="#ADA9AE" d="M181.6 123.484H183.203V132.588H181.6z"/>
|
||||
<path fill="#ADA9AE" d="M191.041 128.973l-1.577-3.642 -1.564 3.642H191.041zM188.747 123.419h1.485l4.014 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L188.747 123.419z"/>
|
||||
</g>
|
||||
</switch>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 23 KiB |
@ -191,7 +191,7 @@
|
||||
|
||||
{% for job in active_jobs %}
|
||||
{# Optimized Job Listing Card #}
|
||||
<a href="{% url 'application_detail' job.slug %}"
|
||||
<a href="{% url 'job_application_detail' job.slug %}"
|
||||
class="card d-block text-decoration-none text-dark job-listing-card p-4 border-2 shadow-hover transition-all">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
|
||||
@ -2,31 +2,31 @@
|
||||
{% load static i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
{# ------------------------------------------------ #}
|
||||
{# 🚀 TOP NAV BAR (Sticky and Themed) #}
|
||||
{# ------------------------------------------------ #}
|
||||
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top border-bottom"
|
||||
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top border-bottom"
|
||||
style="background-color: var(--kaauh-teal); z-index: 1030; height: 50px;">
|
||||
<div class="container-fluid container-lg">
|
||||
<span class="navbar-text text-white fw-bold fs-6">{% trans "Job Overview" %}</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
{# ------------------------------------------------ #}
|
||||
{# 🔔 DJANGO MESSAGES (Refined placement and styling) #}
|
||||
{# ------------------------------------------------ #}
|
||||
|
||||
|
||||
|
||||
{# ------------------------------------------------ #}
|
||||
{# 💻 MAIN CONTENT CONTAINER #}
|
||||
{# ------------------------------------------------ #}
|
||||
<div class="container mt-4 mb-5">
|
||||
<div class="row g-4 main-content-area">
|
||||
|
||||
|
||||
{# 📌 RIGHT COLUMN: Sticky Apply Card (Desktop Only) #}
|
||||
<div class="col-lg-4 order-lg-2 d-none d-lg-block">
|
||||
<div class="card shadow-lg border-0" style="position: sticky; top: 70px;">
|
||||
<div class="card shadow-lg border-0" style="position: sticky; top: 70px;">
|
||||
<div class="card-header bg-white border-bottom p-3">
|
||||
<h5 class="mb-0 fw-bold text-kaauh-teal">
|
||||
<i class="fas fa-file-signature me-2"></i>{% trans "Ready to Apply?" %}
|
||||
@ -34,14 +34,24 @@
|
||||
</div>
|
||||
<div class="card-body text-center p-4">
|
||||
<p class="text-muted small mb-3">{% trans "Review the full job details below before submitting your application." %}</p>
|
||||
|
||||
|
||||
{% if job.form_template %}
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100 shadow-sm">
|
||||
{% if user.is_authenticated and already_applied %}
|
||||
<button class="btn btn-main-action btn-lg w-100" disabled>
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% comment %} <a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100 shadow-sm">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
{% elif not job.is_expired %}
|
||||
<p class="text-danger fw-bold">{% trans "Application form is unavailable." %}</p>
|
||||
{% endif %}
|
||||
{% endif %} {% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -49,32 +59,32 @@
|
||||
{# 📝 LEFT COLUMN: Job Details #}
|
||||
<div class="col-lg-8 order-lg-1">
|
||||
<article class="card shadow-lg border-0">
|
||||
|
||||
|
||||
{# Job Title Header #}
|
||||
<header class="card-header bg-white border-bottom p-4">
|
||||
<h1 class="h2 mb-0 fw-bolder text-kaauh-teal">{{ job.title }}</h1>
|
||||
</header>
|
||||
|
||||
<div class="card-body p-4">
|
||||
|
||||
|
||||
<h4 class="mb-4 fw-bold text-muted border-bottom pb-2">{% trans "Summary" %}</h4>
|
||||
|
||||
|
||||
{# Job Metadata/Overview Grid #}
|
||||
<section class="row row-cols-1 row-cols-md-2 g-3 mb-5 small text-secondary p-3 rounded bg-light-subtle border">
|
||||
|
||||
|
||||
{# SALARY #}
|
||||
{% if job.salary_range %}
|
||||
<div class="col">
|
||||
<i class="fas fa-money-bill-wave text-success me-2 fa-fw"></i>
|
||||
<strong>{% trans "Salary:" %}</strong>
|
||||
<strong>{% trans "Salary:" %}</strong>
|
||||
<span class="fw-bold text-success">{{ job.salary_range }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# DEADLINE #}
|
||||
<div class="col">
|
||||
<i class="fas fa-calendar-alt text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Deadline:" %}</strong>
|
||||
<strong>{% trans "Deadline:" %}</strong>
|
||||
{% if job.application_deadline %}
|
||||
<time datetime="{{ job.application_deadline|date:'Y-m-d' }}">
|
||||
{{ job.application_deadline|date:"M d, Y" }}
|
||||
@ -86,50 +96,50 @@
|
||||
<span class="text-muted">{% trans "Ongoing" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{# JOB TYPE #}
|
||||
<div class="col">
|
||||
<i class="fas fa-briefcase text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
|
||||
<div class="col">
|
||||
<i class="fas fa-briefcase text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
|
||||
</div>
|
||||
|
||||
|
||||
{# LOCATION #}
|
||||
<div class="col">
|
||||
<i class="fas fa-map-marker-alt text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
|
||||
<div class="col">
|
||||
<i class="fas fa-map-marker-alt text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
|
||||
</div>
|
||||
|
||||
|
||||
{# DEPARTMENT #}
|
||||
<div class="col">
|
||||
<i class="fas fa-building text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
|
||||
<div class="col">
|
||||
<i class="fas fa-building text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
|
||||
{# JOB ID #}
|
||||
<div class="col">
|
||||
<i class="fas fa-hashtag text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "JOB ID:" %}</strong> {{ job.internal_job_id|default:"N/A" }}
|
||||
<div class="col">
|
||||
<i class="fas fa-hashtag text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "JOB ID:" %}</strong> {{ job.internal_job_id|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
|
||||
{# WORKPLACE TYPE #}
|
||||
<div class="col">
|
||||
<i class="fas fa-laptop-house text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
|
||||
<div class="col">
|
||||
<i class="fas fa-laptop-house text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
{# Detailed Accordion Section #}
|
||||
<div class="accordion accordion-flush" id="jobDetailAccordion">
|
||||
|
||||
|
||||
{% with active_collapse="collapseOne" %}
|
||||
|
||||
|
||||
{# JOB DESCRIPTION #}
|
||||
{% if job.has_description_content %}
|
||||
<div class="accordion-item border-top border-bottom">
|
||||
<h2 class="accordion-header" id="headingOne">
|
||||
<button class="accordion-button fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#{{ active_collapse }}" aria-expanded="true"
|
||||
<button class="accordion-button fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#{{ active_collapse }}" aria-expanded="true"
|
||||
aria-controls="{{ active_collapse }}">
|
||||
<i class="fas fa-info-circle me-3 fa-fw"></i> {% trans "Job Description" %}
|
||||
</button>
|
||||
@ -141,12 +151,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# QUALIFICATIONS #}
|
||||
{% if job.has_qualifications_content %}
|
||||
<div class="accordion-item border-bottom">
|
||||
<h2 class="accordion-header" id="headingTwo">
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
|
||||
<i class="fas fa-graduation-cap me-3 fa-fw"></i> {% trans "Qualifications" %}
|
||||
</button>
|
||||
@ -158,12 +168,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# BENEFITS #}
|
||||
{% if job.has_benefits_content %}
|
||||
<div class="accordion-item border-bottom">
|
||||
<h2 class="accordion-header" id="headingThree">
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
|
||||
<i class="fas fa-hand-holding-usd me-3 fa-fw"></i> {% trans "Benefits" %}
|
||||
</button>
|
||||
@ -180,7 +190,7 @@
|
||||
{% if job.has_application_instructions_content %}
|
||||
<div class="accordion-item border-bottom">
|
||||
<h2 class="accordion-header" id="headingFour">
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
|
||||
<i class="fas fa-file-alt me-3 fa-fw"></i> {% trans "Application Instructions" %}
|
||||
</button>
|
||||
@ -192,25 +202,30 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 📱 MOBILE FIXED APPLY BAR (Replaced inline style with utility classes) #}
|
||||
{% if job.form_template %}
|
||||
<footer class="fixed-bottom d-lg-none bg-white border-top shadow-lg p-3">
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
</footer>
|
||||
<footer class="fixed-bottom d-lg-none bg-white border-top shadow-lg p-3">
|
||||
{% if user.is_authenticated and already_applied %}
|
||||
<button class="btn btn-main-action btn-lg w-100" disabled>
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% endblock content%}
|
||||
@ -323,7 +323,7 @@
|
||||
<a class="nav-link text-secondary" href="{% url 'applicant_profile' %}">{% translate "Applications" %}</a>
|
||||
</li> {% endcomment %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="{% url 'candidate_portal_dashboard' %}">{% translate "Profile" %}</a>
|
||||
<a class="nav-link text-secondary" href="{% url 'applicant_portal_dashboard' %}">{% translate "Profile" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="{% url 'kaauh_career' %}">{% translate "Careers" %}</a>
|
||||
|
||||
@ -123,14 +123,16 @@
|
||||
</li> {% endcomment %}
|
||||
<li class="nav-item me-2">
|
||||
{% if LANGUAGE_CODE == 'en' %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="ar" class="btn bg-primary-theme text-white" type="submit">
|
||||
<span class="me-2">🇸🇦</span> العربية
|
||||
</button>
|
||||
</form>
|
||||
{% elif LANGUAGE_CODE == 'ar' %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="en" class="btn bg-primary-theme text-white" type="submit">
|
||||
<span class="me-2">🇺🇸</span> English
|
||||
@ -195,8 +197,8 @@
|
||||
|
||||
|
||||
{% if request.user.is_superuser %}
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'admin_settings' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Settings" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'source_list' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Integration" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'admin_settings' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Staff Settings" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'source_list' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Integration Settings" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'easy_logs' %}"><i class="fas fa-history me-3 fs-5"></i> <span>{% trans "Activity Log" %}</span></a></li>
|
||||
{% comment %} <li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-question-circle me-3 text-primary fs-5"></i> <span>{% trans "Help & Support" %}</span></a></li> {% endcomment %}
|
||||
{% endif %}
|
||||
@ -251,7 +253,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item me-lg-4">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'candidate_list' %}active{% endif %}" href="{% url 'candidate_list' %}">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'application_list' %}active{% endif %}" href="{% url 'application_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<i class="fas fa-user-tie me-2"></i>
|
||||
{% trans "Applications" %}
|
||||
@ -312,7 +314,7 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
{% comment %} <li class="nav-item dropdown ms-lg-2">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
||||
data-bs-offset="0, 8" data-bs-auto-close="outside">
|
||||
|
||||
@ -1249,7 +1249,7 @@ const elements = {
|
||||
|
||||
if (result.success) {
|
||||
state.templateId = result.template_slug;
|
||||
window.location.href = "{% url 'form_templates_list' %}";
|
||||
window.location.href = "{% url 'job_detail' template.job.slug %}";
|
||||
|
||||
} else {
|
||||
alert('Error saving form template: ' + result.error);
|
||||
|
||||
@ -184,8 +184,8 @@
|
||||
background-color: var(--kaauh-gray-light);
|
||||
}
|
||||
.empty-state i {
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--kaauh-teal-dark);
|
||||
}
|
||||
.empty-state .btn-main-action .fas {
|
||||
|
||||
@ -154,8 +154,8 @@
|
||||
background-color: var(--kaauh-gray-light);
|
||||
}
|
||||
.empty-state i {
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--kaauh-teal-dark);
|
||||
}
|
||||
.empty-state .btn-main-action .fas {
|
||||
@ -331,7 +331,7 @@
|
||||
<p class="text-muted mb-4">
|
||||
{% trans "There are no submissions for this form template yet." %}
|
||||
</p>
|
||||
<a href="{% url 'form_templates_list' %}" class="btn btn-main-action btn-sm">
|
||||
<a href="{% url 'form_templates_list' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% url 'update_candidate_exam_status' slug=candidate.slug as url %}
|
||||
{% url 'update_application_exam_status' slug=application.slug as url %}
|
||||
<form data-on-submit="@post('{{url}}', {contentType: 'form', headers: {'X-CSRFToken': '{{ csrf_token }}'}})">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
@ -2,7 +2,7 @@
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
{% if LANGUAGE_CODE == 'en' %}
|
||||
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ candidate.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ candidate.professional_category }} </span></h5>
|
||||
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ application.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ application.professional_category }} </span></h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
@ -10,7 +10,7 @@
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i>
|
||||
<small class="text-muted">{% trans "Job Fit" %}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ candidate.job_fit_narrative }}</p>
|
||||
<p class="mb-1">{{ application.job_fit_narrative }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
@ -18,7 +18,7 @@
|
||||
<small class="text-muted">{% trans "Top Keywords" %}</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for keyword in candidate.top_3_keywords %}
|
||||
{% for keyword in application.top_3_keywords %}
|
||||
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -31,18 +31,18 @@
|
||||
<i class="fas fa-clock me-2 text-info"></i>
|
||||
<small class="text-muted">{% trans "Experience" %}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>{{ candidate.years_of_experience }}</strong> {% trans "years" %}</p>
|
||||
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ candidate.most_recent_job_title }}</p>
|
||||
<p class="mb-1"><strong>{{ application.years_of_experience }}</strong> {% trans "years" %}</p>
|
||||
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ application.most_recent_job_title }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-chart-line me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Skills" %}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ candidate.soft_skills_score }}%</p>
|
||||
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ application.soft_skills_score }}%</p>
|
||||
<p class="mb-0"><strong>{% trans "Industry Match:" %}</strong>
|
||||
<span class="badge {% if candidate.industry_match_score >= 70 %}bg-success{% elif candidate.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||
{{ candidate.industry_match_score }}%
|
||||
<span class="badge {% if application.industry_match_score >= 70 %}bg-success{% elif application.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||
{{ application.industry_match_score }}%
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -50,17 +50,17 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-comment me-1 text-info"></i> {% trans "Recommendation" %}</label>
|
||||
<textarea class="form-control" rows="6" readonly>{{ candidate.recommendation }}</textarea>
|
||||
<textarea class="form-control" rows="6" readonly>{{ application.recommendation }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-up me-1 text-success"></i> {% trans "Strengths" %}</label>
|
||||
<textarea class="form-control" rows="4" readonly>{{ candidate.strengths }}</textarea>
|
||||
<textarea class="form-control" rows="4" readonly>{{ application.strengths }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-down me-1 text-danger"></i> {% trans "Weaknesses" %}</label>
|
||||
<textarea class="form-control" rows="4" readonly>{{ candidate.weaknesses }}</textarea>
|
||||
<textarea class="form-control" rows="4" readonly>{{ application.weaknesses }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@ -74,7 +74,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for criterion, status in candidate.criteria_checklist.items %}
|
||||
{% for criterion, status in application.criteria_checklist.items %}
|
||||
<tr>
|
||||
<td>{{ criterion }}</td>
|
||||
<td>
|
||||
@ -99,7 +99,7 @@
|
||||
<i class="fas fa-check-circle me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Minimum Requirements" %}</small>
|
||||
</div>
|
||||
{% if candidate.min_requirements_met %}
|
||||
{% if application.min_requirements_met %}
|
||||
<span class="badge bg-success">{% trans "Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Not Met" %}</span>
|
||||
@ -110,15 +110,15 @@
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Screening Rating" %}</small>
|
||||
</div>
|
||||
<span class="badge bg-secondary">{{ candidate.screening_stage_rating }}</span>
|
||||
<span class="badge bg-secondary">{{ application.screening_stage_rating }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if candidate.language_fluency %}
|
||||
{% if application.language_fluency %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-language me-1 text-info"></i> {% trans "Language Fluency" %}</label>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for language in candidate.language_fluency %}
|
||||
{% for language in application.language_fluency %}
|
||||
<span class="badge bg-light text-dark">{{ language }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -126,7 +126,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ candidate.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ candidate.professional_category_ar }} </span></h5>
|
||||
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ application.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ application.professional_category_ar }} </span></h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
@ -134,7 +134,7 @@
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i>
|
||||
<small class="text-muted">{% trans "Job Fit" %}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ candidate.job_fit_narrative_ar }}</p>
|
||||
<p class="mb-1">{{ application.job_fit_narrative_ar }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
@ -142,7 +142,7 @@
|
||||
<small class="text-muted">{% trans "Top Keywords" %}</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for keyword in candidate.top_3_keywords_ar %}
|
||||
{% for keyword in application.top_3_keywords_ar %}
|
||||
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -155,18 +155,18 @@
|
||||
<i class="fas fa-clock me-2 text-info"></i>
|
||||
<small class="text-muted">{% trans "Experience" %}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>{{ candidate.years_of_experience }}</strong> {% trans "years" %}</p>
|
||||
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ candidate.most_recent_job_title_ar }}</p>
|
||||
<p class="mb-1"><strong>{{ application.years_of_experience }}</strong> {% trans "years" %}</p>
|
||||
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ application.most_recent_job_title_ar }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-chart-line me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Skills" %}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ candidate.soft_skills_score }}%</p>
|
||||
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ application.soft_skills_score }}%</p>
|
||||
<p class="mb-0"><strong>{% trans "Industry Match:" %}</strong>
|
||||
<span class="badge {% if candidate.industry_match_score >= 70 %}bg-success{% elif candidate.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||
{{ candidate.industry_match_score }}%
|
||||
<span class="badge {% if application.industry_match_score >= 70 %}bg-success{% elif application.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||
{{ application.industry_match_score }}%
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -174,17 +174,17 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-comment me-1 text-info"></i> {% trans "Recommendation" %}</label>
|
||||
<textarea class="form-control" rows="6" readonly>{{ candidate.recommendation_ar }}</textarea>
|
||||
<textarea class="form-control" rows="6" readonly>{{ application.recommendation_ar }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-up me-1 text-success"></i> {% trans "Strengths" %}</label>
|
||||
<textarea class="form-control" rows="4" readonly>{{ candidate.strengths_ar }}</textarea>
|
||||
<textarea class="form-control" rows="4" readonly>{{ application.strengths_ar }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-down me-1 text-danger"></i> {% trans "Weaknesses" %}</label>
|
||||
<textarea class="form-control" rows="4" readonly>{{ candidate.weaknesses_ar }}</textarea>
|
||||
<textarea class="form-control" rows="4" readonly>{{ application.weaknesses_ar }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@ -198,7 +198,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for criterion, status in candidate.criteria_checklist_ar.items %}
|
||||
{% for criterion, status in application.criteria_checklist_ar.items %}
|
||||
<tr>
|
||||
<td>{{ criterion }}</td>
|
||||
<td>
|
||||
@ -223,7 +223,7 @@
|
||||
<i class="fas fa-check-circle me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Minimum Requirements" %}</small>
|
||||
</div>
|
||||
{% if candidate.min_requirements_met_ar %}
|
||||
{% if application.min_requirements_met_ar %}
|
||||
<span class="badge bg-success">{% trans "Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Not Met" %}</span>
|
||||
@ -234,15 +234,15 @@
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Screening Rating" %}</small>
|
||||
</div>
|
||||
<span class="badge bg-secondary">{{ candidate.screening_stage_rating_ar }}</span>
|
||||
<span class="badge bg-secondary">{{ application.screening_stage_rating_ar }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if candidate.language_fluency_ar %}
|
||||
{% if application.language_fluency_ar %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-language me-1 text-info"></i> {% trans "Language Fluency" %}</label>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for language in candidate.language_fluency_ar %}
|
||||
{% for language in application.language_fluency_ar %}
|
||||
<span class="badge bg-light text-dark">{{ language }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -1,15 +1,15 @@
|
||||
{% load i18n %}
|
||||
<form id="exam-update-form" hx-post="{% url 'update_candidate_status' job.slug candidate.slug 'exam' 'Failed' %}" hx-swap='outerHTML' hx-target="#status-result-{{ candidate.pk }}"
|
||||
<form id="exam-update-form" hx-post="{% url 'update_application_status' job.slug application.slug 'exam' 'Failed' %}" hx-swap='outerHTML' hx-target="#status-result-{{ application.pk }}"
|
||||
hx-on::after-request="const modal = bootstrap.Modal.getInstance(document.getElementById('candidateviewModal')); if (modal) { modal.hide(); }">
|
||||
<div class="d-flex justify-content-center align-items-center gap-2">
|
||||
<div class="form-check d-flex align-items-center gap-2">
|
||||
<input class="form-check-input" type="radio" name="exam_status" id="exam_passed" value="Passed" {% if candidate.exam_status == 'Passed' %}checked{% endif %}>
|
||||
<input class="form-check-input" type="radio" name="exam_status" id="exam_passed" value="Passed" {% if application.exam_status == 'Passed' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="exam_passed">
|
||||
<i class="fas fa-check me-1"></i> {% trans "Passed" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check d-flex align-items-center gap-2">
|
||||
<input class="form-check-input" type="radio" name="exam_status" id="exam_failed" value="Failed" {% if candidate.exam_status == 'Failed' %}checked{% endif %}>
|
||||
<input class="form-check-input" type="radio" name="exam_status" id="exam_failed" value="Failed" {% if application.exam_status == 'Failed' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="exam_failed">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Failed" %}
|
||||
</label>
|
||||
@ -20,7 +20,7 @@
|
||||
<label for="exam_score" class="form-label small text-muted">{% trans "Exam Score" %}</label>
|
||||
</div>
|
||||
<div class="w-25">
|
||||
<input type="number" class="form-control form-control-sm" id="exam_score" name="exam_score" min="0" max="100" required value="{{ candidate.exam_score }}">
|
||||
<input type="number" class="form-control form-control-sm" id="exam_score" name="exam_score" min="0" max="100" required value="{{ application.exam_score }}">
|
||||
</div>
|
||||
<div class="w-25 text-start ps-none">
|
||||
</div>
|
||||
@ -1,10 +1,10 @@
|
||||
{% load i18n %}
|
||||
<div class="d-flex justify-content-center align-items-center gap-2" hx-swap='outerHTML' hx-target="#interview-result-{{ candidate.pk }}"
|
||||
<div class="d-flex justify-content-center align-items-center gap-2" hx-swap='outerHTML' hx-target="#interview-result-{{ application.pk }}"
|
||||
hx-on::after-request="const modal = bootstrap.Modal.getInstance(document.getElementById('candidateviewModal')); if (modal) { modal.hide(); }">
|
||||
<a hx-post="{% url 'update_candidate_status' job.slug candidate.slug 'interview' 'Passed' %}" class="btn btn-outline-secondary">
|
||||
<a hx-post="{% url 'update_application_status' job.slug application.slug 'interview' 'Passed' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-check me-1"></i> {% trans "Passed" %}
|
||||
</a>
|
||||
<a hx-post="{% url 'update_candidate_status' job.slug candidate.slug 'interview' 'Failed' %}" class="btn btn-danger">
|
||||
<a hx-post="{% url 'update_application_status' job.slug application.slug 'interview' 'Failed' %}" class="btn btn-danger">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Failed" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -1,10 +1,10 @@
|
||||
{% load i18n %}
|
||||
<div class="d-flex justify-content-center align-items-center gap-2" hx-swap='outerHTML' hx-target="#status-result-{{ candidate.pk }}"
|
||||
<div class="d-flex justify-content-center align-items-center gap-2" hx-swap='outerHTML' hx-target="#status-result-{{ application.pk }}"
|
||||
hx-on::after-request="const modal = bootstrap.Modal.getInstance(document.getElementById('candidateviewModal')); if (modal) { modal.hide(); }">
|
||||
<a hx-post="{% url 'update_candidate_status' job.slug candidate.slug 'offer' 'Accepted' %}" class="btn btn-outline-secondary">
|
||||
<a hx-post="{% url 'update_application_status' job.slug application.slug 'offer' 'Accepted' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-check me-1"></i> {% trans "Accepted" %}
|
||||
</a>
|
||||
<a hx-post="{% url 'update_candidate_status' job.slug candidate.slug 'offer' 'Rejected' %}" class="btn btn-danger">
|
||||
<a hx-post="{% url 'update_application_status' job.slug application.slug 'offer' 'Rejected' %}" class="btn btn-danger">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Rejected" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -26,7 +26,7 @@
|
||||
<form
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
hx-post="{% url 'document_upload' candidate.id %}"
|
||||
hx-post="{% url 'document_upload' application.id %}"
|
||||
hx-target="#documents-pane"
|
||||
hx-select="#documents-pane"
|
||||
hx-swap="outerHTML"
|
||||
@ -108,7 +108,7 @@
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
|
||||
{% if user.is_superuser or candidate.job.assigned_to == user %}
|
||||
{% if user.is_superuser or application.job.assigned_to == user %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
@ -125,7 +125,7 @@
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-file-alt fa-3x mb-3"></i>
|
||||
<p class="mb-2">{% trans "No documents uploaded yet." %}</p>
|
||||
<p class="small">{% trans "Click \"Upload Document\" to add files for this candidate." %}</p>
|
||||
<p class="small">{% trans "Click \"Upload Document\" to add files for this application." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
<div class="card">
|
||||
|
||||
<div class="card-body">
|
||||
<form hx-boost="true" method="post" id="email-compose-form" action="{% url 'compose_candidate_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-target="#messageContent"
|
||||
hx-select="#messageContent"
|
||||
|
||||
@ -91,7 +91,7 @@
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="card-title flex-grow-1 me-3">
|
||||
<a href="{% url 'candidate_detail' interview.candidate.slug %}" class="text-decoration-none text-primary-theme">{{ interview.candidate.name }}</a>
|
||||
<a href="{% url 'application_detail' interview.candidate.slug %}" class="text-decoration-none text-primary-theme">{{ interview.candidate.name }}</a>
|
||||
</h5>
|
||||
<span class="status-badge bg-{{ interview.status }}">
|
||||
{{ interview.status|title }}
|
||||
@ -169,7 +169,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="text-primary-theme">
|
||||
<a href="{% url 'candidate_detail' interview.candidate.slug %}" class="text-decoration-none text-primary-theme">{{ interview.candidate.name }}</a>
|
||||
<a href="{% url 'application_detail' interview.candidate.slug %}" class="text-decoration-none text-primary-theme">{{ interview.candidate.name }}</a>
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Create New Job Post - {{ block.super }}{% endblock %}
|
||||
{% block title %}{% trans "Create New Job Post" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
|
||||
|
||||
@ -127,7 +127,7 @@
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<a href="{% url 'application_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus"></i> {% trans "Add New Applicant" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -186,7 +186,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<a href="{% url 'job_candidates_list' job.slug %}" class="btn btn-outline-secondary w-100">
|
||||
<a href="{% url 'job_applications_list' job.slug %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-times"></i> {% trans "Clear Filters" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -250,16 +250,16 @@
|
||||
<td class="text-secondary small">{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||
<td class="text-center pe-3">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'View' %}">
|
||||
<a href="{% url 'application_detail' candidate.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}">
|
||||
<a href="{% url 'application_update' candidate.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-delete-url="{% url 'application_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@ -345,17 +345,17 @@
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-secondary w-100">
|
||||
<a href="{% url 'application_detail' candidate.slug %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-eye"></i> {% trans "View Profile" %}
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<a href="{% url 'application_update' candidate.slug %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-delete-url="{% url 'application_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@ -375,7 +375,7 @@
|
||||
<i class="fas fa-user-slash fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">{% trans "No applicants found" %}</h4>
|
||||
<p class="text-secondary">{% trans "There are no candidates who have applied for this position yet." %}</p>
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action mt-3">
|
||||
<a href="{% url 'application_create_for_job' job.slug %}" class="btn btn-main-action mt-3">
|
||||
<i class="fas fa-user-plus"></i> {% trans "Add First Applicant" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -150,12 +150,12 @@
|
||||
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-decoration-none text-secondary">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_list' %}" class="text-decoration-none text-secondary">Jobs</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-decoration-none text-secondary">{% trans "Home" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_list' %}" class="text-decoration-none text-secondary">{% trans "Jobs" %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;
|
||||
">Job Detail</li>
|
||||
">{% trans "Job Detail" %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row g-4">
|
||||
@ -218,19 +218,19 @@
|
||||
<div class="card-body">
|
||||
|
||||
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action btn-sm"><li class="fa fa-edit"></li>{% trans "Edit JOb" %}</a>
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action btn-sm"><li class="fa fa-edit"></li>{% trans "Edit Job" %}</a>
|
||||
<div class="float-end">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Assigned to :" %} </strong> {{ job.assigned_to|default:"N/A" }}
|
||||
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Assigned to :" %} </strong> {{ job.assigned_to|default:"" }}
|
||||
</div>
|
||||
</div>
|
||||
</h5>
|
||||
<div class="row g-3 mb-4 border-bottom pb-3 small text-secondary">
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-building me-2 text-primary"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
|
||||
<i class="fas fa-building me-2 text-primary"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-hashtag me-2 text-primary"></i> <strong>{% trans "Position No:" %}</strong> {{ job.position_number|default:"N/A" }}
|
||||
<i class="fas fa-hashtag me-2 text-primary"></i> <strong>{% trans "Position No:" %}</strong> {{ job.position_number|default:"" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
|
||||
@ -242,16 +242,16 @@
|
||||
<i class="fas fa-globe me-2 text-primary"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fa-solid fa-money-bill me-2 text-primary"></i> <strong>{% trans "Salary:" %}</strong> {{ job.salary_range |default:"N/A" }}
|
||||
<i class="fa-solid fa-money-bill me-2 text-primary"></i> <strong>{% trans "Salary:" %}</strong> {{ job.salary_range |default:"" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Created By:" %}</strong> {{ job.created_by|default:"N/A" }}
|
||||
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Created By:" %}</strong> {{ job.created_by|default:"" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-plus me-2 text-primary"></i> <strong>{% trans "Created At:" %}</strong> {{ job.created_at|default:"N/A" }}
|
||||
<i class="fas fa-plus me-2 text-primary"></i> <strong>{% trans "Created At:" %}</strong> {{ job.created_at|default:"" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-edit me-2 text-primary"></i> <strong>{% trans "Updated At:" %}</strong> {{ job.updated_at|default:"N/A" }}
|
||||
<i class="fas fa-edit me-2 text-primary"></i> <strong>{% trans "Updated At:" %}</strong> {{ job.updated_at|default:"" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -293,7 +293,7 @@
|
||||
<ul class="nav nav-tabs" id="rightJobTabs" role="tablist">
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link active" id="applicants-tab" data-bs-toggle="tab" data-bs-target="#applicants-pane" type="button" role="tab" aria-controls="applicants-pane" aria-selected="true">
|
||||
<i class="fas fa-users me-1"></i> {% trans "Applicants" %}
|
||||
<i class="fas fa-users me-1"></i> {% trans "Applications" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
@ -322,30 +322,30 @@
|
||||
|
||||
{# TAB 1: APPLICANTS CONTENT #}
|
||||
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab">
|
||||
<h5 class="mb-3">{% trans "Total Applicants" %} (<span id="total_candidates">{{ total_applicants }}</span>)</h5>
|
||||
<h5 class="mb-3">{% trans "Total Applications" %} (<span id="total_candidates">{{ total_applications }}</span>)</h5>
|
||||
|
||||
<div class="d-grid gap-3">
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Create Applicant" %}
|
||||
<a href="{% url 'application_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Create Application" %}
|
||||
</a>
|
||||
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applicants" %}
|
||||
<a href="{% url 'applications_screening_view' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applications" %}
|
||||
</a>
|
||||
|
||||
|
||||
<a href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fa-solid fa-download me-1"></i> {% trans "Generate All CVs" %}
|
||||
</a>
|
||||
|
||||
|
||||
<a href="{% url 'download_ready_cvs' job.slug %}" class="btn btn-outline-primary">
|
||||
<i class="fa-solid fa-eye me-1"></i> {% trans "View All CVs" %}
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# TAB 2: TRACKING CONTENT #}
|
||||
<div class="tab-pane fade" id="tracking-pane" role="tabpanel" aria-labelledby="tracking-tab">
|
||||
<h5 class="mb-3"><i class="fas fa-project-diagram me-2 text-primary"></i>{% trans "Applicant Stages" %}</h5>
|
||||
<h5 class="mb-3"><i class="fas fa-project-diagram me-2 text-primary"></i>{% trans "Applications Stages" %}</h5>
|
||||
{% include 'jobs/partials/applicant_tracking.html' %}
|
||||
<p class="text-muted small">
|
||||
{% trans "The applicant tracking flow is defined by the attached Form Template. View the Form Template tab to manage stages and fields." %}
|
||||
@ -361,7 +361,10 @@
|
||||
{% trans "Manage the custom application forms associated with this job posting." %}
|
||||
</p>
|
||||
|
||||
{% if not job.form_template %}
|
||||
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-list-alt me-1"></i> {% trans "Manage Job Form" %}
|
||||
</a>
|
||||
{% comment %} {% if not job.form_template %}
|
||||
<a href="{% url 'create_form_template' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
|
||||
</a>
|
||||
@ -377,7 +380,7 @@
|
||||
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endif %} {% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -488,12 +491,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Card 2: Candidate Category Chart #}
|
||||
{# Card 2: Application Category Chart #}
|
||||
<div class="card shadow-sm no-hover mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-chart-pie me-2 text-primary"></i>
|
||||
{% trans "Candidate Categories & Scores" %}
|
||||
{% trans "Application Categories & Scores" %}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
@ -532,7 +535,7 @@
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
<div class="h4 mb-0 text-success fw-bold">{{ high_potential_count }}</div>
|
||||
<small class="text-muted d-block">{% trans "High Potential" %}</small>
|
||||
<small class="text-muted d-block">{% trans "High Potential (score>=75)" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -613,14 +616,24 @@
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{# Translated JS strings used by the inline scripts below. Using `as` + `escapejs` ensures safe JS embedding. #}
|
||||
{% trans "Copy failed. Please copy the URL manually:" as copy_failed_msg %}
|
||||
{% trans "Number of Applications" as chart_label_applications %}
|
||||
{% trans " application(s)" as chart_tooltip_suffix %}
|
||||
{% trans "No application category data available for this job." as no_chart_data_msg %}
|
||||
|
||||
{% block customJS%}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
const COPY_FAILED_MSG = "{{ copy_failed_msg|escapejs }}";
|
||||
const CHART_LABEL_NUMBER_OF_APPLICATIONS = "{{ chart_label_applications|escapejs }}";
|
||||
const CHART_TOOLTIP_SUFFIX = "{{ chart_tooltip_suffix|escapejs }}";
|
||||
const NO_CHART_DATA_MSG = "{{ no_chart_data_msg|escapejs }}";
|
||||
|
||||
// Pass data from Django to JavaScript safely
|
||||
window.jobChartData = {
|
||||
categories: {{ categories|safe|default:"[]" }},
|
||||
candidate_counts: {{ candidate_counts|safe|default:"[]" }},
|
||||
applications_count: {{ applications_count|safe|default:"[]" }},
|
||||
avg_scores: {{ avg_scores|safe|default:"[]" }}
|
||||
};
|
||||
|
||||
@ -643,7 +656,7 @@
|
||||
}).catch(err => {
|
||||
// Fallback for older browsers or security issues
|
||||
console.error('Could not copy text: ', err);
|
||||
alert("Copy failed. Please copy the URL manually: " + urlToCopy);
|
||||
alert(COPY_FAILED_MSG + ' ' + urlToCopy);
|
||||
});
|
||||
});
|
||||
|
||||
@ -658,10 +671,10 @@
|
||||
|
||||
// Safely get job_category_data from Django context
|
||||
// Using window.jobChartData to avoid template parsing issues
|
||||
const jobChartData = window.jobChartData || { categories: [], candidate_counts: [], avg_scores: []};
|
||||
const jobChartData = window.jobChartData || { categories: [], applications_count: [], avg_scores: []};
|
||||
|
||||
const categories = jobChartData.categories || [];
|
||||
const candidateCounts = jobChartData.candidate_counts || [];
|
||||
const candidateCounts = jobChartData.applications_count || [];
|
||||
const avgScores = jobChartData.avg_scores || [];
|
||||
|
||||
if (categories.length > 0) { // Only render if there's data
|
||||
@ -669,9 +682,9 @@
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: categories,
|
||||
datasets: [
|
||||
datasets: [
|
||||
{
|
||||
label: 'Number of Candidates',
|
||||
label: CHART_LABEL_NUMBER_OF_APPLICATIONS,
|
||||
data: candidateCounts,
|
||||
backgroundColor: [
|
||||
'rgba(0, 99, 110, 0.7)', // --kaauh-teal
|
||||
@ -712,7 +725,7 @@
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += context.parsed + ' candidate(s)';
|
||||
label += context.parsed + CHART_TOOLTIP_SUFFIX;
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@ -721,8 +734,8 @@
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Display a message if no data is available
|
||||
chartCtx.canvas.parentNode.innerHTML = '<p class="text-center text-muted mt-4">No candidate category data available for this job.</p>';
|
||||
// Display a message if no data is available (translated)
|
||||
chartCtx.canvas.parentNode.innerHTML = '<p class="text-center text-muted mt-4">' + NO_CHART_DATA_MSG + '</p>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
--kaauh-danger: #dc3545;
|
||||
--kaauh-info: #17a2b8;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa; /* Subtle light background */
|
||||
font-family: 'Inter', sans-serif;
|
||||
@ -106,7 +106,7 @@
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #f0f4f7;
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* OPTIMIZED MAIN TABLE COLUMN WIDTHS (Total must be 100%)
|
||||
* --------------------------------------------------------
|
||||
@ -155,12 +155,12 @@
|
||||
font-size: 0.9rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Strong visual separator before metrics data */
|
||||
.table tbody tr td:nth-child(6) {
|
||||
border-left: 2px solid var(--kaauh-teal-dark) !important;
|
||||
border-left: 2px solid var(--kaauh-teal-dark) !important;
|
||||
}
|
||||
|
||||
|
||||
/* Subtle vertical lines between metric data cells */
|
||||
.table tbody td.candidate-data-cell:not(:first-child) {
|
||||
border-left: 1px solid #f0f4f7;
|
||||
@ -207,7 +207,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
|
||||
{# --- MAIN HEADER AND ACTION BUTTON --- #}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
@ -276,8 +276,9 @@
|
||||
<th scope="col" rowspan="2">{% trans "Source" %}</th>
|
||||
<th scope="col" rowspan="2">{% trans "Max Apps" %}</th>
|
||||
<th scope="col" rowspan="2">{% trans "Deadline" %}</th>
|
||||
<th scope="col" rowspan="2">{% trans "Submission" %}</th>
|
||||
|
||||
<th scope="col" rowspan="2">{% trans "Assigned To" %}</th>
|
||||
<th scope="col" rowspan="2"></th>
|
||||
|
||||
|
||||
<th scope="col" colspan="6" class="candidate-management-header-title">
|
||||
{% trans "Applicants Metrics (Current Stage Count)" %}
|
||||
@ -306,26 +307,35 @@
|
||||
<td>{{ job.get_source }}</td>
|
||||
<td>{{ job.max_applications }}</td>
|
||||
<td>{{ job.application_deadline|date:"d-m-Y" }}</td>
|
||||
{% if job.assigned_to %}
|
||||
<td>
|
||||
<span class="badge bg-primary-theme">
|
||||
{{ job.assigned_to.first_name|capfirst }} {{ job.assigned_to.last_name|capfirst }}
|
||||
</span>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>{% trans "Unassigned" %}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if job.form_template %}
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'form_template_submissions_list' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'All Application Submissions' %}">
|
||||
<i class="fas fa-file-alt text-primary-theme"></i>
|
||||
<i class="fas fa-file-alt text-primary-theme"></i>{% trans "Submissions" %}
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-muted small"></span>
|
||||
{% endif%}
|
||||
</td>
|
||||
|
||||
|
||||
{# CANDIDATE MANAGEMENT DATA #}
|
||||
<td class="candidate-data-cell text-primary-theme"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-primary-theme">{% if job.all_candidates.count %}{{ job.all_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-info"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-info">{% if job.screening_candidates.count %}{{ job.screening_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_exam_view' job.slug %}" class="text-success">{% if job.exam_candidates.count %}{{ job.exam_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_interview_view' job.slug %}" class="text-success">{% if job.interview_candidates.count %}{{ job.interview_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_document_review_view' job.slug %}" class="text-success">{% if job.document_review_candidates.count %}{{ job.document_review_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_offer_view' job.slug %}" class="text-success">{% if job.offer_candidates.count %}{{ job.offer_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-primary-theme"><a href="{% url 'applications_screening_view' job.slug %}" class="text-primary-theme">{% if job.all_applications.count %}{{ job.all_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-info"><a href="{% url 'applications_screening_view' job.slug %}" class="text-info">{% if job.screening_applications.count %}{{ job.screening_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'applications_exam_view' job.slug %}" class="text-success">{% if job.exam_applications.count %}{{ job.exam_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'applications_interview_view' job.slug %}" class="text-success">{% if job.interview_applications.count %}{{ job.interview_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'applications_document_review_view' job.slug %}" class="text-success">{% if job.document_review_applications.count %}{{ job.document_review_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'applications_offer_view' job.slug %}" class="text-success">{% if job.offer_applications.count %}{{ job.offer_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -350,8 +360,8 @@
|
||||
|
||||
<ul class="list-unstyled small mb-3">
|
||||
<li><i class="fas fa-calendar-alt text-primary-theme me-2"></i>{% trans "Deadline" %}: {{ job.application_deadline|date:"d-m-Y" }}</li>
|
||||
<li><i class="fas fa-users text-primary-theme me-2"></i>{% trans "Total Applicants" %}: {{ job.all_candidates.count|default:"0" }}</li>
|
||||
<li><i class="fas fa-clipboard-check text-success me-2"></i> {% trans "Offers Made" %}: {{ job.offer_candidates.count|default:"0" }}</li>
|
||||
<li><i class="fas fa-users text-primary-theme me-2"></i>{% trans "Total Applicants" %}: {{ job.all_applications.count|default:"0" }}</li>
|
||||
<li><i class="fas fa-clipboard-check text-success me-2"></i> {% trans "Offers Made" %}: {{ job.offer_applications.count|default:"0" }}</li>
|
||||
<li><i class="fas fa-file-alt text-info me-2"></i> {% trans "Form" %}:
|
||||
{% if job.form_template %}
|
||||
<a href="{% url 'form_template_submissions_list' job.form_template.slug %}" class="text-info">{{ job.form_template.name }} ({{ job.form_template.submissions.count }} submissions)</a>
|
||||
@ -366,7 +376,7 @@
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Job Details" %}
|
||||
</a>
|
||||
<div class="btn-group btn-group-sm">
|
||||
|
||||
|
||||
{% if job.form_template %}
|
||||
<a href="{% url 'form_template_submissions_list' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Submissions' %}">
|
||||
<i class="fas fa-file-alt me-1"></i>{% trans "Submissions" %}
|
||||
@ -382,9 +392,9 @@
|
||||
{# --- END CARD VIEW --- #}
|
||||
</div>
|
||||
{# --- END OF JOB LIST CONTAINER --- #}
|
||||
|
||||
|
||||
{% include "includes/paginator.html" %}
|
||||
|
||||
|
||||
{% if not jobs and not job_list_data and not page_obj %}
|
||||
<div class="text-center py-5 card shadow-sm mt-4">
|
||||
<div class="card-body">
|
||||
|
||||
@ -102,84 +102,84 @@
|
||||
<div class="progress-stages">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
{% comment %} STAGE 1: Applied {% endcomment %}
|
||||
<a href="{% url 'candidate_screening_view' job.slug %}"
|
||||
<a href="{% url 'applications_screening_view' job.slug %}"
|
||||
class="stage-item {% if current_stage == 'Applied' %}active{% endif %}"
|
||||
data-stage="Applied">
|
||||
<div class="stage-icon">
|
||||
<i class="fas fa-file-signature cd_screening"></i>
|
||||
</div>
|
||||
<div class="stage-label cd_screening">{% trans "Screened" %}</div>
|
||||
<div class="stage-count">{{ job.screening_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count">{{ job.screening_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 1 -> 2 {% endcomment %}
|
||||
<div class="stage-connector {% if current_stage == 'Exam' or current_stage == 'Interview' or current_stage == 'Document Review' or current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
|
||||
|
||||
{% comment %} STAGE 2: Exam {% endcomment %}
|
||||
<a href="{% url 'candidate_exam_view' job.slug %}"
|
||||
<a href="{% url 'applications_exam_view' job.slug %}"
|
||||
class="stage-item {% if current_stage == 'Exam' %}active{% endif %}"
|
||||
data-stage="Exam">
|
||||
<div class="stage-icon">
|
||||
<i class="fas fa-clipboard-check cd_exam"></i>
|
||||
</div>
|
||||
<div class="stage-label cd_exam">{% trans "Exam" %}</div>
|
||||
<div class="stage-count ">{{ job.exam_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count ">{{ job.exam_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 2 -> 3 {% endcomment %}
|
||||
<div class="stage-connector {% if current_stage == 'Interview' or current_stage == 'Document Review' or current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
|
||||
|
||||
{% comment %} STAGE 3: Interview {% endcomment %}
|
||||
<a href="{% url 'candidate_interview_view' job.slug %}"
|
||||
<a href="{% url 'applications_interview_view' job.slug %}"
|
||||
class="stage-item {% if current_stage == 'Interview' %}active{% endif %}"
|
||||
data-stage="Interview">
|
||||
<div class="stage-icon">
|
||||
<i class="fas fa-comments cd_interview"></i>
|
||||
</div>
|
||||
<div class="stage-label cd_interview">{% trans "Interview" %}</div>
|
||||
<div class="stage-count">{{ job.interview_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count">{{ job.interview_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 3 -> 4 {% endcomment %}
|
||||
<div class="stage-connector {% if current_stage == 'Document Review' or current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
|
||||
|
||||
{% comment %} STAGE 4: Document Review {% endcomment %}
|
||||
<a href="{% url 'candidate_document_review_view' job.slug %}"
|
||||
<a href="{% url 'applications_document_review_view' job.slug %}"
|
||||
class="stage-item {% if current_stage == 'Document Review' %}active{% endif %} {% if current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"
|
||||
data-stage="Document Review">
|
||||
<div class="stage-icon">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
</div>
|
||||
<div class="stage-label">{% trans "Document Review" %}</div>
|
||||
<div class="stage-count">{{ job.document_review_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count">{{ job.document_review_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 4 -> 5 {% endcomment %}
|
||||
<div class="stage-connector {% if current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
|
||||
|
||||
{% comment %} STAGE 5: Offer {% endcomment %}
|
||||
<a href="{% url 'candidate_offer_view' job.slug %}"
|
||||
<a href="{% url 'applications_offer_view' job.slug %}"
|
||||
class="stage-item {% if current_stage == 'Offer' %}active{% endif %} {% if current_stage == 'Hired' %}completed{% endif %}"
|
||||
data-stage="Offer">
|
||||
<div class="stage-icon">
|
||||
<i class="fas fa-handshake"></i>
|
||||
</div>
|
||||
<div class="stage-label">{% trans "Offer" %}</div>
|
||||
<div class="stage-count">{{ job.offer_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count">{{ job.offer_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 5 -> 6 {% endcomment %}
|
||||
<div class="stage-connector {% if current_stage == 'Hired' %}completed{% endif %}"></div>
|
||||
|
||||
{% comment %} STAGE 6: Hired {% endcomment %}
|
||||
<a href="{% url 'candidate_hired_view' job.slug %}"
|
||||
<a href="{% url 'applications_hired_view' job.slug %}"
|
||||
class="stage-item {% if current_stage == 'Hired' %}active{% endif %}"
|
||||
data-stage="Hired">
|
||||
<div class="stage-icon">
|
||||
<i class="fas fa-trophy"></i>
|
||||
</div>
|
||||
<div class="stage-label">{% trans "Hired" %}</div>
|
||||
<div class="stage-count">{{ job.hired_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count">{{ job.hired_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -290,7 +290,7 @@
|
||||
<span class="status-badge bg-primary-theme text-white">{{ meeting.type|title }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-primary text-decoration-none" href="{% url 'candidate_detail' meeting.interview.application.person.slug %}">{{ meeting.interview.application.person.full_name }} <i class="fas fa-link"></i></a>
|
||||
<a class="text-primary text-decoration-none" href="{% url 'application_detail' meeting.interview.application.person.slug %}">{{ meeting.interview.application.person.full_name }} <i class="fas fa-link"></i></a>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-primary text-decoration-none" href="{% url 'job_detail' meeting.interview.job.slug %}">{{ meeting.interview.job.title }} <i class="fas fa-link"></i></a>
|
||||
|
||||
@ -218,7 +218,7 @@ body {
|
||||
</a>
|
||||
|
||||
{# Send Candidate Invitation Button #}
|
||||
<form method="post" action="{% url 'send_candidate_invitation' meeting.slug %}" style="display: inline;">
|
||||
<form method="post" action="{% url 'send_application_invitation' meeting.slug %}" style="display: inline;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-outline-info btn-sm" onclick="return confirm('{% trans "Send invitation email to the candidate?" %}')">
|
||||
<i class="fas fa-envelope me-1"></i> {% trans "Send Candidate Invitation" %}
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form method="post" id="updateMeeting" action="{% url 'reschedule_meeting_for_candidate' job.slug candidate.pk meeting.pk %}">
|
||||
<form method="post" id="updateMeeting" action="{% url 'reschedule_meeting_for_application' job.slug candidate.pk meeting.pk %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form method="post" action="{% url 'schedule_meeting_for_candidate' job.slug candidate.pk %}">
|
||||
<form method="post" action="{% url 'schedule_meeting_for_application' job.slug candidate.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.topic.id_for_label }}" class="form-label small">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% load i18n crispy_forms_tags %}
|
||||
<form action="{% url 'set_meeting_candidate' meeting.slug %}" method="post" class="d-flex flex-column gap-2 py-2 px-4">
|
||||
<form action="{% url 'set_meeting_application' meeting.slug %}" method="post" class="d-flex flex-column gap-2 py-2 px-4">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button type="submit" class="btn bg-primary-theme text-white">{% trans "Save" %}</button>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{{ message.subject }}{% endblock %}
|
||||
|
||||
@ -13,69 +13,69 @@
|
||||
<h5 class="mb-0">
|
||||
{{ message.subject }}
|
||||
{% if message.parent_message %}
|
||||
<span class="badge bg-secondary ms-2">Reply</span>
|
||||
<span class="badge bg-secondary ms-2">{% trans "Reply" %}</span>
|
||||
{% endif %}
|
||||
</h5>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'message_reply' message.id %}" class="btn btn-outline-info">
|
||||
<i class="fas fa-reply"></i> Reply
|
||||
<i class="fas fa-reply"></i> {% trans "Reply" %}
|
||||
</a>
|
||||
{% if message.recipient == request.user %}
|
||||
<a href="{% url 'message_mark_unread' message.id %}"
|
||||
class="btn btn-outline-warning"
|
||||
hx-post="{% url 'message_mark_unread' message.id %}">
|
||||
<i class="fas fa-envelope"></i> Mark Unread
|
||||
<i class="fas fa-envelope"></i> {% trans "Mark Unread" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'message_delete' message.id %}"
|
||||
class="btn btn-outline-danger"
|
||||
hx-get="{% url 'message_delete' message.id %}"
|
||||
hx-confirm="Are you sure you want to delete this message?">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
hx-confirm="{% trans 'Are you sure you want to delete this message?' %}">
|
||||
<i class="fas fa-trash"></i> {% trans "Delete" %}
|
||||
</a>
|
||||
<a href="{% url 'message_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Messages
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to Messages" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>From:</strong>
|
||||
<strong>{% trans "From:" %}</strong>
|
||||
<span class="text-primary">{{ message.sender.get_full_name|default:message.sender.username }}</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>To:</strong>
|
||||
<strong>{% trans "To:" %}</strong>
|
||||
<span class="text-primary">{{ message.recipient.get_full_name|default:message.recipient.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Type:</strong>
|
||||
<strong>{% trans "Type:" %}</strong>
|
||||
<span class="badge bg-{{ message.message_type|lower }}">
|
||||
{{ message.get_message_type_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Status:</strong>
|
||||
<strong>{% trans "Status:" %}</strong>
|
||||
{% if message.is_read %}
|
||||
<span class="badge bg-success">Read</span>
|
||||
<span class="badge bg-success">{% trans "Read" %}</span>
|
||||
{% if message.read_at %}
|
||||
<small class="text-muted">({{ message.read_at|date:"M d, Y H:i" }})</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Unread</span>
|
||||
<span class="badge bg-warning">{% trans "Unread" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Created:</strong>
|
||||
<strong>{% trans "Created:" %}</strong>
|
||||
<span>{{ message.created_at|date:"M d, Y H:i" }}</span>
|
||||
</div>
|
||||
{% if message.job %}
|
||||
<div class="col-md-6">
|
||||
<strong>Related Job:</strong>
|
||||
<div class="col-md-6">
|
||||
<strong>{% trans "Related Job:" %}</strong>
|
||||
<a href="{% url 'job_detail' message.job.slug %}" class="text-primary">
|
||||
{{ message.job.title }}
|
||||
</a>
|
||||
@ -84,13 +84,13 @@
|
||||
</div>
|
||||
{% if message.parent_message %}
|
||||
<div class="alert alert-info">
|
||||
<strong>In reply to:</strong>
|
||||
<strong>{% trans "In reply to:" %}</strong>
|
||||
<a href="{% url 'message_detail' message.parent_message.id %}">
|
||||
{{ message.parent_message.subject }}
|
||||
</a>
|
||||
<small class="text-muted d-block">
|
||||
From {{ message.parent_message.sender.get_full_name|default:message.parent_message.sender.username }}
|
||||
on {{ message.parent_message.created_at|date:"M d, Y H:i" }}
|
||||
{% trans "From" %} {{ message.parent_message.sender.get_full_name|default:message.parent_message.sender.username }}
|
||||
{% trans "on" %} {{ message.parent_message.created_at|date:"M d, Y H:i" }}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -100,7 +100,7 @@
|
||||
<!-- Message Content -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Message</h6>
|
||||
<h6 class="mb-0">{% trans "Message" %}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="message-content">
|
||||
@ -114,7 +114,7 @@
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-comments"></i> Replies ({{ message.replies.count }})
|
||||
<i class="fas fa-comments"></i> {% trans "Replies" %} ({{ message.replies.count }})
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@ -136,7 +136,7 @@
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<a href="{% url 'message_reply' reply.id %}" class="btn btn-sm btn-outline-info">
|
||||
<i class="fas fa-reply"></i> Reply to this
|
||||
<i class="fas fa-reply"></i> {% trans "Reply to this" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n crispy_forms_tags %}
|
||||
|
||||
{% block title %}Create Person - {{ block.super }}{% endblock %}
|
||||
{% block title %}Create Applicant - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
@ -184,9 +184,9 @@
|
||||
|
||||
<form method="post" enctype="multipart/form-data" id="person-form">
|
||||
{% csrf_token %}
|
||||
|
||||
{{form|crispy}}
|
||||
<!-- Profile Image Section -->
|
||||
<div class="row mb-4">
|
||||
{% comment %} <div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="profile-image-upload" onclick="document.getElementById('id_profile_image').click()">
|
||||
<div id="image-preview-container">
|
||||
@ -261,7 +261,7 @@
|
||||
<div class="col-12">
|
||||
{{ form.address }}
|
||||
</div>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
|
||||
<!-- LinkedIn Profile Section -->
|
||||
{% comment %} <div class="row mb-4">
|
||||
@ -292,11 +292,8 @@
|
||||
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="reset" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-undo me-1"></i> {% trans "Reset" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-save me-1"></i> {% trans "Create Person" %}
|
||||
<i class="fas fa-save me-1"></i> {% trans "Create Applicant" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -308,141 +305,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% 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');
|
||||
|
||||
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) {
|
||||
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 "Click to change photo" %}</p>
|
||||
`;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Form Validation
|
||||
const form = document.getElementById('person-form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.classList.add('loading');
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Basic validation
|
||||
const firstName = document.getElementById('id_first_name').value.trim();
|
||||
const lastName = document.getElementById('id_last_name').value.trim();
|
||||
const email = document.getElementById('id_email').value.trim();
|
||||
|
||||
if (!firstName || !lastName) {
|
||||
e.preventDefault();
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
alert('{% trans "First name and last name are required." %}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (email && !isValidEmail(email)) {
|
||||
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);
|
||||
}
|
||||
|
||||
// LinkedIn URL validation
|
||||
const linkedinInput = document.getElementById('id_linkedin_profile');
|
||||
linkedinInput.addEventListener('blur', function() {
|
||||
const value = this.value.trim();
|
||||
if (value && !isValidLinkedInURL(value)) {
|
||||
this.classList.add('is-invalid');
|
||||
if (!this.nextElementSibling || !this.nextElementSibling.classList.contains('invalid-feedback')) {
|
||||
const feedback = document.createElement('div');
|
||||
feedback.className = 'invalid-feedback';
|
||||
feedback.textContent = '{% trans "Please enter a valid LinkedIn URL" %}';
|
||||
this.parentNode.appendChild(feedback);
|
||||
}
|
||||
} else {
|
||||
this.classList.remove('is-invalid');
|
||||
const feedback = this.parentNode.querySelector('.invalid-feedback');
|
||||
if (feedback) feedback.remove();
|
||||
}
|
||||
});
|
||||
|
||||
function isValidLinkedInURL(url) {
|
||||
const linkedinRegex = /^https?:\/\/(www\.)?linkedin\.com\/.+/i;
|
||||
return linkedinRegex.test(url);
|
||||
}
|
||||
|
||||
// Drag and Drop functionality
|
||||
const uploadArea = document.querySelector('.profile-image-upload');
|
||||
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, highlight, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, unhighlight, false);
|
||||
});
|
||||
|
||||
function highlight(e) {
|
||||
uploadArea.style.borderColor = 'var(--kaauh-teal)';
|
||||
uploadArea.style.backgroundColor = 'var(--kaauh-gray-light)';
|
||||
}
|
||||
|
||||
function unhighlight(e) {
|
||||
uploadArea.style.borderColor = 'var(--kaauh-border)';
|
||||
uploadArea.style.backgroundColor = 'transparent';
|
||||
}
|
||||
|
||||
uploadArea.addEventListener('drop', handleDrop, false);
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
|
||||
if (files.length > 0) {
|
||||
profileImageInput.files = files;
|
||||
const event = new Event('change', { bubbles: true });
|
||||
profileImageInput.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" />
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.select2').select2();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -412,7 +412,7 @@
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1">
|
||||
<a href="{% url 'candidate_detail' application.slug %}"
|
||||
<a href="{% url 'application_detail' application.slug %}"
|
||||
class="text-decoration-none text-secondary">
|
||||
{{ application.job.title }}
|
||||
</a>
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
|
||||
<div class="container-fluid" style="max-width: 1600px;">
|
||||
{% if request.user.user_type == 'candidate' %}
|
||||
<a class="navbar-brand text-white" href="{% url 'candidate_portal_dashboard' %}" aria-label="Applicant Dashboard">
|
||||
<a class="navbar-brand text-white" href="{% url 'applicant_portal_dashboard' %}" aria-label="Applicant Dashboard">
|
||||
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 40px; height: 40px;">
|
||||
<span class="ms-3 d-none d-md-inline fw-semibold">{% trans "Applicant Portal" %}</span>
|
||||
</a>
|
||||
@ -75,7 +75,7 @@
|
||||
<div class="collapse navbar-collapse" id="agencyNavbar">
|
||||
|
||||
<div class="navbar-nav ms-auto">
|
||||
<li class="nav-item me-2">
|
||||
<li class="nav-item me-2">
|
||||
{% if LANGUAGE_CODE == 'en' %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
@ -106,7 +106,7 @@
|
||||
</li>
|
||||
{% elif request.user.user_type == 'candidate' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="{% url 'candidate_portal_dashboard' %}">
|
||||
<a class="nav-link text-white" href="{% url 'applicant_portal_dashboard' %}">
|
||||
<i class="fas fa-tachometer-alt me-1"></i> {% trans "Dashboard" %}
|
||||
</a>
|
||||
</li>
|
||||
@ -120,8 +120,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
{% comment %} <li class="nav-item dropdown">
|
||||
{% comment %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown"
|
||||
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
|
||||
<i class="fas fa-globe me-1"></i>
|
||||
@ -145,8 +145,8 @@
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endcomment %}
|
||||
</li> {% endcomment %}
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="{% url 'user_detail' request.user.pk %}">
|
||||
|
||||
@ -207,7 +207,7 @@
|
||||
|
||||
<div class="alert alert-info mx-2">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
{% trans "Share these credentials securely with the agency. They can use this information to log in and submit candidates." %}
|
||||
{% trans "Share these credentials securely with the agency. They can use this information to log in and submit applications." %}
|
||||
</div>
|
||||
|
||||
{% if access_link %}
|
||||
@ -219,12 +219,12 @@
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
|
||||
<!-- Candidates Card -->
|
||||
<!-- Applications Card -->
|
||||
<div class="kaauh-card p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-users me-2"></i>
|
||||
{% trans "Submitted Candidates" %} ({{ total_candidates }})
|
||||
{% trans "Submitted Applications" %} ({{ total_candidates }})
|
||||
</h5>
|
||||
{% if access_link %}
|
||||
<a href="{% url 'agency_portal_login' %}" target="_blank" class="btn btn-outline-info btn-sm">
|
||||
@ -233,12 +233,12 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Candidate"%}</th>
|
||||
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Application"%}</th>
|
||||
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Contact" %}
|
||||
</th>
|
||||
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Stage" %}
|
||||
@ -248,36 +248,36 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold">{{ candidate.name }}</div>
|
||||
<div class="fw-bold">{{ application.name }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="small">
|
||||
<div><i class="fas fa-envelope me-1"></i> {{ candidate.email }}</div>
|
||||
<div><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</div>
|
||||
<div><i class="fas fa-envelope me-1"></i> {{ application.email }}</div>
|
||||
<div><i class="fas fa-phone me-1"></i> {{ application.phone }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ candidate.get_stage_display }}</span>
|
||||
<span class="badge bg-info">{{ application.get_stage_display }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="small text-muted">
|
||||
<div class="mb-1"><i class="fas fa-envelope me-2 w-20"></i>
|
||||
{{candidate.email }}</div>
|
||||
<div><i class="fas fa-phone me-2 w-20"></i>{{ candidate.phone }}</div>
|
||||
{{application.email }}</div>
|
||||
<div><i class="fas fa-phone me-2 w-20"></i>{{ application.phone }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4">
|
||||
<span class="badge bg-soft-info text-info rounded-pill px-3">
|
||||
{{candidate.get_stage_display }}</span>
|
||||
{{application.get_stage_display }}</span>
|
||||
</td>
|
||||
<td class="px-4">
|
||||
<span class="small text-muted">{{ candidate.created_at|date:"M d, Y" }}</span>
|
||||
<span class="small text-muted">{{ application.created_at|date:"M d, Y" }}</span>
|
||||
</td>
|
||||
<td class="px-4 text-end">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}"
|
||||
<a href="{% url 'application_detail' application.slug %}"
|
||||
class="btn btn-sm btn-outline-primary" title="{% trans 'View Details' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
@ -290,9 +290,9 @@
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-users fa-2x text-muted mb-3"></i>
|
||||
<h6 class="text-muted">{% trans "No candidates submitted yet" %}</h6>
|
||||
<h6 class="text-muted">{% trans "No applications submitted yet" %}</h6>
|
||||
<p class="text-muted small">
|
||||
{% trans "Candidates will appear here once the agency submits them through their portal." %}
|
||||
{% trans "Applications will appear here once the agency submits them through their portal." %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -335,7 +335,7 @@
|
||||
|
||||
<div class="text-center">
|
||||
<div class="h4 mb-1">{{ total_candidates }}</div>
|
||||
<div class="text-muted">/ {{ assignment.max_candidates }} {% trans "candidates" %}</div>
|
||||
<div class="text-muted">/ {{ assignment.max_candidates }} {% trans "applications" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="progress mt-3" style="height: 8px;">
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
--kaauh-danger: #dc3545;
|
||||
--kaauh-warning: #ffc107;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa; /* Light background for better contrast */
|
||||
}
|
||||
@ -45,7 +45,7 @@
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
|
||||
/* Secondary Action Button (Teal Outline) */
|
||||
.btn-outline-primary-teal {
|
||||
color: var(--kaauh-teal);
|
||||
@ -69,7 +69,7 @@
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
|
||||
|
||||
/* Applying Bootstrap classes to Django fields if not done in the form definition */
|
||||
.kaauh-field-control > input,
|
||||
.kaauh-field-control > textarea,
|
||||
@ -104,7 +104,7 @@
|
||||
<div>
|
||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-tasks me-2"></i>
|
||||
{{ title }}
|
||||
{% trans "Create New Assignment" %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
{% trans "Assign a job to an external hiring agency" %}
|
||||
@ -120,22 +120,22 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.agency.id_for_label }}" class="form-label">
|
||||
{{ form.agency.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{# Wrapper Div for styling consistency (Assumes agency is a SELECT field) #}
|
||||
<div class="kaauh-field-control">
|
||||
{{ form.agency|attr:'class:form-select' }}
|
||||
{{ form.agency|attr:'class:form-select' }}
|
||||
</div>
|
||||
{% if form.agency.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{% for error in form.agency.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.job.id_for_label }}" class="form-label">
|
||||
{{ form.job.label }} <span class="text-danger">*</span>
|
||||
@ -213,7 +213,7 @@
|
||||
<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 }}
|
||||
<i class="fas fa-save me-1"></i> {% trans "Create Assignment" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -227,10 +227,10 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// --- Consistency Check: Ensure Django widgets have the Bootstrap classes ---
|
||||
// If your form fields are NOT already adding classes via widget attrs in the Django form,
|
||||
// If your form fields are NOT already adding classes via widget attrs in the Django form,
|
||||
// you MUST add the following utility filter to your project to make this template work:
|
||||
// `|attr:'class:form-control'`
|
||||
|
||||
|
||||
// Auto-populate agency field when job is selected
|
||||
const jobSelect = document.getElementById('{{ form.job.id_for_label }}');
|
||||
const agencySelect = document.getElementById('{{ form.agency.id_for_label }}');
|
||||
|
||||
@ -114,7 +114,7 @@
|
||||
<a href="{% url 'agency_portal_dashboard' %}" class="btn btn-outline-secondary btn-sm me-2">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Dashboard" %}
|
||||
</a>
|
||||
<a href="{% url 'agency_portal_submit_candidate_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" %}
|
||||
</a>
|
||||
{% comment %} <a href="#" class="btn btn-outline-info">
|
||||
@ -194,7 +194,7 @@
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
{% if assignment.can_submit %}
|
||||
<a href="{% url 'agency_portal_submit_candidate_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" %}
|
||||
</a>
|
||||
{% else %}
|
||||
@ -264,7 +264,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'candidate_application_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Profile' %}">
|
||||
<a href="{% url 'applicant_application_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Profile' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
@ -523,7 +523,7 @@
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="editCandidateForm" method="post" action="{% url 'agency_portal_edit_candidate' 0 %}">
|
||||
<form id="editCandidateForm" method="post" action="{% url 'agency_portal_edit_application' 0 %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="edit_candidate_id" name="candidate_id">
|
||||
<div class="modal-body">
|
||||
@ -588,7 +588,7 @@
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="deleteCandidateForm" method="post" action="{% url 'agency_portal_delete_candidate' 0 %}">
|
||||
<form id="deleteCandidateForm" method="post" action="{% url 'agency_portal_delete_application' 0 %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="delete_candidate_id" name="candidate_id">
|
||||
<div class="modal-body">
|
||||
|
||||
@ -49,8 +49,8 @@
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{% comment %} <a href="{% url 'agency_portal_submit_candidate' %}" class="btn btn-main-action me-2">
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Submit Candidate" %}
|
||||
{% comment %} <a href="{% url 'agency_portal_submit_application' %}" class="btn btn-main-action me-2">
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Submit Application" %}
|
||||
</a>
|
||||
<a href="#" class="btn btn-outline-secondary position-relative">
|
||||
<i class="fas fa-envelope me-1"></i> {% trans "Messages" %}
|
||||
@ -93,8 +93,8 @@
|
||||
<div class="text-info mb-2">
|
||||
<i class="fas fa-users fa-2x"></i>
|
||||
</div>
|
||||
<h4 class="card-title">{{ total_candidates }}</h4>
|
||||
<p class="card-text text-muted">{% trans "Total Candidates" %}</p>
|
||||
<h4 class="card-title">{{ total_applications }}</h4>
|
||||
<p class="card-text text-muted">{% trans "Total Applications" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -169,8 +169,8 @@
|
||||
</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted d-block">{% trans "Candidates" %}</small>
|
||||
<strong>{{ stats.candidate_count }} / {{ stats.assignment.max_candidates }}</strong>
|
||||
<small class="text-muted d-block">{% trans "Applications" %}</small>
|
||||
<strong>{{ stats.application_count }} / {{ stats.assignment.max_applications }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -178,10 +178,10 @@
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<small class="text-muted">{% trans "Submission Progress" %}</small>
|
||||
<small class="text-muted">{{ stats.candidate_count }}/{{ stats.assignment.max_candidates }}</small>
|
||||
<small class="text-muted">{{ stats.application_count }}/{{ stats.assignment.max_applications }}</small>
|
||||
</div>
|
||||
<div class="progress" style="height: 6px;">
|
||||
{% with progress=stats.candidate_count %}
|
||||
{% with progress=stats.application_count %}
|
||||
<div class="progress-bar {% if progress >= 90 %}bg-danger{% elif progress >= 70 %}bg-warning{% else %}bg-success{% endif %}"
|
||||
style="width: {{ progress|floatformat:0 }}%"></div>
|
||||
{% endwith %}
|
||||
@ -192,9 +192,9 @@
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
{% if stats.can_submit %}
|
||||
<a href="{% url 'agency_portal_submit_candidate_page' stats.assignment.slug %}"
|
||||
<a href="{% url 'agency_portal_submit_application_page' stats.assignment.slug %}"
|
||||
class="btn btn-sm btn-main-action">
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Submit Candidate" %}
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Submit Application" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-secondary" disabled>
|
||||
|
||||
@ -235,7 +235,7 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if not search_query and not stage_filter and agency.assignments.exists %}
|
||||
<a href="{% url 'agency_portal_submit_candidate_page' agency.assignments.first.slug %}"
|
||||
<a href="{% url 'agency_portal_submit_application_page' agency.assignments.first.slug %}"
|
||||
class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Add First Person" %}
|
||||
</a>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'portal_base.html' %}
|
||||
{% load static i18n crispy_forms_tags %}
|
||||
|
||||
{% block title %}{% trans "Submit Candidate" %} - {{ assignment.job.title }} - Agency Portal{% endblock %}
|
||||
{% block title %}{% trans "Submit Application" %} - {{ assignment.job.title }} - Agency Portal{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
@ -97,13 +97,13 @@
|
||||
</li>
|
||||
|
||||
{% comment %} <li class="breadcrumb-item active" aria-current="page">
|
||||
{% trans "Submit Candidate" %}
|
||||
{% trans "Submit Application" %}
|
||||
</li> {% endcomment %}
|
||||
|
||||
<li class="breadcrumb-item active" aria-current="page" style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600; ">
|
||||
{% trans "Submit Candidate" %}</li>
|
||||
{% trans "Submit Application" %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@ -112,11 +112,11 @@
|
||||
<div>
|
||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-user-plus me-2"></i>
|
||||
{% trans "Submit New Candidate" %}
|
||||
{% trans "Submit New Application" %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
<!-- Button trigger modal -->
|
||||
{% trans "Submit a candidate for" %}
|
||||
{% trans "Submit a Application for" %}
|
||||
{{ assignment.job.title }}
|
||||
|
||||
</p>
|
||||
@ -171,16 +171,16 @@
|
||||
<div class="kaauh-card p-4">
|
||||
<h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-user-edit me-2"></i>
|
||||
{% trans "Candidate Information" %}
|
||||
{% trans "Application Information" %}
|
||||
</h5>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" id="candidateForm"
|
||||
action="{% url 'agency_portal_submit_candidate_page' assignment.slug %}">
|
||||
<form method="post" enctype="multipart/form-data" id="ApplicationForm"
|
||||
action="{% url 'agency_portal_submit_application_page' assignment.slug %}">
|
||||
{% csrf_token %}
|
||||
{{form|crispy}}
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus me-2"></i>
|
||||
{% trans "Submit Candidate" %}
|
||||
{% trans "Submit Application" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -188,14 +188,14 @@
|
||||
<div class="kaauh-card p-4">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-exclamation-triangle fa-4x text-warning mb-4"></i>
|
||||
<h4 class="text-warning mb-3">{% trans "Cannot Submit Candidates" %}</h4>
|
||||
<h4 class="text-warning mb-3">{% trans "Cannot Submit Applications" %}</h4>
|
||||
<div class="alert alert-warning d-inline-block">
|
||||
{% if assignment.is_expired %}
|
||||
<i class="fas fa-clock me-2"></i>
|
||||
{% trans "This assignment has expired. Submissions are no longer accepted." %}
|
||||
{% elif assignment.is_full %}
|
||||
<i class="fas fa-users me-2"></i>
|
||||
{% trans "Maximum candidate limit reached for this assignment." %}
|
||||
{% trans "Maximum Application limit reached for this assignment." %}
|
||||
{% else %}
|
||||
<i class="fas fa-pause me-2"></i>
|
||||
{% trans "This assignment is not currently active." %}
|
||||
@ -222,7 +222,7 @@
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">{% trans "Loading..." %}</span>
|
||||
</div>
|
||||
<h6>{% trans "Submitting candidate..." %}</h6>
|
||||
<h6>{% trans "Submitting Application..." %}</h6>
|
||||
<p class="text-muted small">{% trans "Please wait while we process your submission." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -240,7 +240,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
const fileName = document.getElementById('fileName');
|
||||
const removeFileBtn = document.getElementById('removeFile');
|
||||
const form = document.getElementById('candidateForm');
|
||||
const form = document.getElementById('ApplicationForm');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
// File upload area click handler
|
||||
@ -340,7 +340,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
successAlert.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
successAlert.innerHTML = `
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
{% trans "Candidate submitted successfully!" %}
|
||||
{% trans "Application submitted successfully!" %}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(successAlert);
|
||||
@ -371,7 +371,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
errorAlert.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
errorAlert.innerHTML = `
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
${data.message || '{% trans "Error submitting candidate. Please try again." %}'}
|
||||
${data.message || '{% trans "Error submitting Application. Please try again." %}'}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(errorAlert);
|
||||
@ -401,7 +401,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
.finally(() => {
|
||||
// Re-enable submit button
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-1"></i> {% trans "Submit Candidate" %}';
|
||||
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-1"></i> {% trans "Submit Application" %}';
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -184,10 +184,10 @@
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'candidate_portal_dashboard' %}" class=" text-decoration-none text-secondary">{% trans "Dashboard" %}</a>
|
||||
<a href="{% url 'applicant_portal_dashboard' %}" class=" text-decoration-none text-secondary">{% trans "Dashboard" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'candidate_portal_dashboard' %}#applications" class="text-secondary text-decoration-none">{% trans "My Applications" %}</a>
|
||||
<a href="{% url 'applicant_portal_dashboard' %}#applications" class="text-secondary text-decoration-none">{% trans "My Applications" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page" style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
@ -315,13 +315,13 @@
|
||||
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-6 col-6">
|
||||
<a href="{% url 'candidate_portal_dashboard' %}" class="text-decoration-none text-dark">
|
||||
<a href="{% url 'applicant_portal_dashboard' %}" class="text-decoration-none text-dark">
|
||||
<div class="kaauh-card h-50 shadow-sm action-card">
|
||||
<div class="card-body text-center mb-4">
|
||||
<i class="fas fa-arrow-left fa-2x text-primary-theme mb-3"></i>
|
||||
<h6>{% trans "Go to Dashboard" %}</h6>
|
||||
<p class="text-muted small">{% trans "View all applications" %}</p>
|
||||
<a href="{% url 'candidate_portal_dashboard' %}" class="btn btn-main-action btn-sm w-100">
|
||||
<a href="{% url 'applicant_portal_dashboard' %}" class="btn btn-main-action btn-sm w-100">
|
||||
{% trans "Dashboard" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -292,23 +292,23 @@
|
||||
{# Header: Larger, more dynamic on large screens. Stacks cleanly on mobile. #}
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5">
|
||||
<h1 class="display-6 display-md-5 fw-extrabold mb-3 mb-md-0" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Your Candidate Dashboard" %}
|
||||
{% trans "Your Applicant Dashboard" %}
|
||||
</h1>
|
||||
{% comment %} <a href="#profile-details" data-bs-toggle="tab" class="btn btn-main-action btn-sm btn-md-lg px-4 py-2 rounded-pill shadow-sm shadow-md-lg">
|
||||
<i class="fas fa-edit me-2"></i> {% trans "Update Profile" %}
|
||||
</a> {% endcomment %}
|
||||
</div>
|
||||
|
||||
{# Candidate Quick Overview Card: Use a softer background color #}
|
||||
{# Applicant Quick Overview Card: Use a softer background color #}
|
||||
<div class="card kaauh-card mb-5 p-4 bg-white">
|
||||
<div class="d-flex align-items-center flex-column flex-sm-row text-center text-sm-start">
|
||||
<img src="{% if candidate.user.profile_image %}{{ candidate.user.profile_image.url }}{% else %}{% static 'image/default_avatar.png' %}{% endif %}"
|
||||
<img src="{% if applicant.user.profile_image %}{{ applicant.user.profile_image.url }}{% else %}{% static 'image/default_avatar.png' %}{% endif %}"
|
||||
alt="{% trans 'Profile Picture' %}"
|
||||
class="rounded-circle me-sm-4 mb-3 mb-sm-0 shadow-lg"
|
||||
style="width: 80px; height: 80px; object-fit: cover; border: 4px solid var(--kaauh-teal-accent);">
|
||||
<div>
|
||||
<h3 class="card-title mb-1 fw-bold text-dark">{{ candidate.full_name|default:"Candidate Name" }}</h3>
|
||||
<p class="text-gray-subtle mb-0">{{ candidate.email }}</p>
|
||||
<h3 class="card-title mb-1 fw-bold text-dark">{{ applicant.full_name|default:"Applicant Name" }}</h3>
|
||||
<p class="text-gray-subtle mb-0">{{ applicant.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -356,21 +356,21 @@
|
||||
<ul class="list-unstyled profile-data-list p-0">
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fas fa-id-card me-2 text-primary-theme"></i> <strong>{% trans "First Name" %}</strong></div>
|
||||
<span class="text-end">{{ candidate.first_name|default:"" }}</span>
|
||||
<span class="text-end">{{ applicant.first_name|default:"" }}</span>
|
||||
</li>
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fas fa-id-card me-2 text-primary-theme"></i> <strong>{% trans "Last Name" %}</strong></div>
|
||||
<span class="text-end">{{ candidate.last_name|default:"" }}</span>
|
||||
<span class="text-end">{{ applicant.last_name|default:"" }}</span>
|
||||
</li>
|
||||
{% if candidate.middle_name %}
|
||||
{% if applicant.middle_name %}
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fas fa-id-card me-2 text-primary-theme"></i> <strong>{% trans "Middle Name" %}</strong></div>
|
||||
<span class="text-end">{{ candidate.middle_name }}</span>
|
||||
<span class="text-end">{{ applicant.middle_name }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fas fa-envelope me-2 text-primary-theme"></i> <strong>{% trans "Email" %}</strong></div>
|
||||
<span class="text-end">{{ candidate.email|default:"" }}</span>
|
||||
<span class="text-end">{{ applicant.email|default:"" }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -383,19 +383,19 @@
|
||||
<ul class="list-unstyled profile-data-list p-0">
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fas fa-phone-alt me-2 text-primary-theme"></i> <strong>{% trans "Phone" %}</strong></div>
|
||||
<span class="text-end">{{ candidate.phone|default:"" }}</span>
|
||||
<span class="text-end">{{ applicant.phone|default:"" }}</span>
|
||||
</li>
|
||||
{% if candidate.address %}
|
||||
{% if applicant.address %}
|
||||
<li class="d-flex align-items-start">
|
||||
<div class="mb-1"><i class="fas fa-map-marker-alt me-2 text-primary-theme"></i> <strong>{% trans "Address" %}</strong></div>
|
||||
<span class="text-end text-break">{{ candidate.address|linebreaksbr }}</span>
|
||||
<span class="text-end text-break">{{ applicant.address|linebreaksbr }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if candidate.linkedin_profile %}
|
||||
{% if applicant.linkedin_profile %}
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fab fa-linkedin me-2 text-primary-theme"></i> <strong>{% trans "LinkedIn Profile" %}</strong></div>
|
||||
<span class="text-end">
|
||||
<a href="{{ candidate.linkedin_profile }}" target="_blank" class="text-primary-theme text-decoration-none">
|
||||
<a href="{{ applicant.linkedin_profile }}" target="_blank" class="text-primary-theme text-decoration-none">
|
||||
{% trans "View Profile" %} <i class="fas fa-external-link-alt ms-1"></i>
|
||||
</a>
|
||||
</span>
|
||||
@ -412,15 +412,15 @@
|
||||
<ul class="list-unstyled profile-data-list p-0">
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fas fa-calendar-alt me-2 text-primary-theme"></i> <strong>{% trans "Date of Birth" %}</strong></div>
|
||||
<span class="text-end">{{ candidate.date_of_birth|date:"M d, Y"|default:"" }}</span>
|
||||
<span class="text-end">{{ applicant.date_of_birth|date:"M d, Y"|default:"" }}</span>
|
||||
</li>
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fas fa-venus-mars me-2 text-primary-theme"></i> <strong>{% trans "Gender" %}</strong></div>
|
||||
<span class="text-end">{{ candidate.get_gender_display|default:"" }}</span>
|
||||
<span class="text-end">{{ applicant.get_gender_display|default:"" }}</span>
|
||||
</li>
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fas fa-globe me-2 text-primary-theme"></i> <strong>{% trans "Nationality" %}</strong></div>
|
||||
<span class="text-end">{{ candidate.get_nationality_display|default:"" }}</span>
|
||||
<span class="text-end">{{ applicant.get_nationality_display|default:"" }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -431,16 +431,16 @@
|
||||
<i class="fas fa-briefcase me-2 text-primary-theme"></i>{% trans "Professional Information" %}
|
||||
</h4>
|
||||
<ul class="list-unstyled profile-data-list p-0">
|
||||
{% if candidate.user.designation %}
|
||||
{% if applicant.user.designation %}
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fas fa-user-tie me-2 text-primary-theme"></i> <strong>{% trans "Designation" %}</strong></div>
|
||||
<span class="text-end">{{ candidate.user.designation }}</span>
|
||||
<span class="text-end">{{ applicant.user.designation }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if candidate.gpa %}
|
||||
{% if applicant.gpa %}
|
||||
<li class="d-flex justify-content-between align-items-center">
|
||||
<div><i class="fas fa-graduation-cap me-2 text-primary-theme"></i> <strong>{% trans "GPA" %}</strong></div>
|
||||
<span class="text-end">{{ candidate.gpa }}</span>
|
||||
<span class="text-end">{{ applicant.gpa }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
@ -492,7 +492,7 @@
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="card-title fw-bold mb-1">
|
||||
<a href="{% url 'candidate_application_detail' application.slug %}"
|
||||
<a href="{% url 'applicant_application_detail' application.slug %}"
|
||||
class="text-decoration-none text-primary-theme hover:text-primary-theme-dark">
|
||||
{{ application.job.title }}
|
||||
</a>
|
||||
@ -524,7 +524,7 @@
|
||||
|
||||
<!-- Action Button -->
|
||||
<div class="mt-auto">
|
||||
<a href="{% url 'candidate_application_detail' application.slug %}"
|
||||
<a href="{% url 'applicant_application_detail' application.slug %}"
|
||||
class="btn btn-main-action w-100 rounded-pill">
|
||||
<i class="fas fa-eye me-2"></i> {% trans "View Details" %}
|
||||
</a>
|
||||
@ -568,7 +568,7 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-muted small me-3">{% trans "Uploaded:" %} {{ document.uploaded_at|date:"d M Y" }}</span>
|
||||
<a href="{{ document.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary me-2"><i class="fas fa-eye"></i></a>
|
||||
<a href="{% url 'candidate_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>
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
{% empty %}
|
||||
@ -626,7 +626,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="passwordModalBody">
|
||||
<form action="{% url 'portal_password_reset' candidate.pk %}" method="post">
|
||||
<form action="{% url 'portal_password_reset' applicant.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ password_reset_form|crispy }}
|
||||
<button type="submit" class="btn btn-main-action">{% trans "Change Password" %}</button>
|
||||
@ -645,7 +645,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" action="{% url 'user_profile_image_update' candidate.pk %}" enctype="multipart/form-data" >
|
||||
<form method="post" action="{% url 'user_profile_image_update' applicant.pk %}" enctype="multipart/form-data" >
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
@ -709,7 +709,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" action="{% url 'document_upload' candidate.id %}" enctype="multipart/form-data" id="documentUploadForm">
|
||||
<form method="post" action="{% url 'document_upload' applicant.id %}" enctype="multipart/form-data" id="documentUploadForm">
|
||||
<input type="hidden" name="upload_target" value="person">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
@ -100,7 +100,7 @@
|
||||
<i class="fas fa-user-plus me-1"></i>
|
||||
<span class="d-none d-sm-inline">{% trans "Create New Person" %}</span>
|
||||
</button>
|
||||
<a href="{% url 'candidate_list' %}" class="btn btn-outline-light btn-sm" title="{% trans 'Back to List' %}">
|
||||
<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>
|
||||
35
templates/recruitment/application_delete.html
Normal file
35
templates/recruitment/application_delete.html
Normal file
@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "Delete Application" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1>
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
{% trans "Delete Application" %}: {{ object.candidate.full_name }}
|
||||
</h1>
|
||||
<a href="{% url 'applications_list' %}" class="btn btn-secondary">{% trans "Back to List" %}</a>
|
||||
</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>
|
||||
{% endblock %}
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static humanize i18n %}
|
||||
{% load static i18n humanize%}
|
||||
|
||||
{% block title %}{{ candidate.name }} - {{ block.super }}{% endblock %}
|
||||
{% block title %}{{ application.name }} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
@ -269,7 +269,7 @@
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-secondary text-decoration-none">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-secondary text-decoration-none">Job:({{candidate.job.title}})</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_detail' application.job.slug %}" class="text-secondary text-decoration-none">Job:({{application.job.title}})</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" class="text-secondary" style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;
|
||||
@ -277,7 +277,7 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{# LEFT COLUMN: MAIN CANDIDATE DETAILS AND TABS #}
|
||||
{# LEFT COLUMN: MAIN application DETAILS AND TABS #}
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm no-hover">
|
||||
|
||||
@ -285,16 +285,16 @@
|
||||
<div class="candidate-header-card">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap">
|
||||
<div>
|
||||
<h1 class="h3 mb-2">{{ candidate.name }}</h1>
|
||||
<h1 class="h3 mb-2">{{ application.name }}</h1>
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
|
||||
<span id="stageDisplay" class="badge">
|
||||
{% trans "Stage:" %}
|
||||
<span>{{ candidate.stage }}</span>
|
||||
<span>{{ application.stage }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<small class="text-white opacity-75">
|
||||
{% trans "Applied for:" %} <strong>{{ candidate.job.title }}</strong>
|
||||
{% trans "Applied for:" %} <strong>{{ application.job.title }}</strong>
|
||||
</small>
|
||||
</div>
|
||||
{# Change Stage button #}
|
||||
@ -341,7 +341,7 @@
|
||||
<i class="fas fa-envelope fa-2x text-muted me-3"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">{% trans "Email" %}</small>
|
||||
<strong>{{ candidate.email }}</strong>
|
||||
<strong>{{ application.email }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -350,7 +350,7 @@
|
||||
<i class="fas fa-briefcase fa-2x text-muted me-3"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">{% trans "Position Applied" %}</small>
|
||||
<strong>{{ candidate.job.title }}</strong>
|
||||
<strong>{{ application.job.title }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -360,10 +360,10 @@
|
||||
<div>
|
||||
<small class="text-muted d-block">{% trans "Applied Date" %}</small>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<strong>{{ candidate.created_at|date:"M d, Y H:i" }}</strong>
|
||||
<strong>{{ application.created_at|date:"M d, Y H:i" }}</strong>
|
||||
<span class="badge bg-light text-dark">
|
||||
<i class="far fa-clock me-1"></i>
|
||||
{{ candidate.created_at|naturaltime }}
|
||||
{{ application.created_at|naturaltime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -380,15 +380,15 @@
|
||||
{# ENHANCED: CANDIDATE JOURNEY TIMELINE CARD #}
|
||||
<div class="card shadow-sm timeline-card">
|
||||
<div class="card-header bg-white border-bottom py-3">
|
||||
<h5 class="mb-0 text-muted"><i class="fas fa-route me-2"></i>{% trans "Candidate Journey" %}</h5>
|
||||
<h5 class="mb-0 text-muted"><i class="fas fa-route me-2"></i>{% trans "Application Journey" %}</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
|
||||
<h6 class="text-uppercase text-secondary mb-3">{% trans "Current Stage" %}</h6>
|
||||
<div class="p-3 mb-4 rounded current-stage">
|
||||
<p class="mb-0 fw-bold fs-5 text-primary">{{ candidate.stage }}</p>
|
||||
<p class="mb-0 fw-bold fs-5 text-primary">{{ application.stage }}</p>
|
||||
<small class="text-muted d-block mt-1">
|
||||
{% trans "Latest status update:" %} {{ candidate.updated_at|date:"M d, Y" }}
|
||||
{% trans "Latest status update:" %} {{ application.updated_at|date:"M d, Y" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@ -402,50 +402,50 @@
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Application Submitted" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.created_at|date:"M d, Y" }}
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ application.created_at|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.created_at|date:"h:i A" }}
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ application.created_at|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% if candidate.exam_date %}
|
||||
{% if application.exam_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fa-clipboard-check"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Exam" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.exam_date|date:"M d, Y" }}
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ application.exam_date|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.exam_date|date:"h:i A" }}
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ application.exam_date|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.get_interview_date %}
|
||||
{% if application.get_interview_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.get_interview_date}}
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ application.get_interview_date}}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.get_interview_time}}
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ application.get_interview_time}}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.offer_date %}
|
||||
{% if application.offer_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.offer_date|date:"M d, Y" }}
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ application.offer_date|date:"M d, Y" }}
|
||||
|
||||
</small>
|
||||
</div>
|
||||
@ -453,13 +453,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.hired_date %}
|
||||
{% if application.hired_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.hired_date|date:"M d, Y" }}
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ application.hired_date|date:"M d, Y" }}
|
||||
|
||||
</small>
|
||||
</div>
|
||||
@ -473,28 +473,28 @@
|
||||
|
||||
{# TAB 4 CONTENT: DOCUMENTS #}
|
||||
<div class="tab-pane fade" id="documents-pane" role="tabpanel" aria-labelledby="documents-tab">
|
||||
{% with documents=candidate.documents %}
|
||||
{% with documents=application.documents %}
|
||||
{% include 'includes/document_list.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{# TAB 5 CONTENT: PARSED SUMMARY #}
|
||||
{% if candidate.parsed_summary %}
|
||||
{% if application.parsed_summary %}
|
||||
|
||||
<div class="tab-pane fade" id="summary-pane" role="tabpanel" aria-labelledby="summary-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5>
|
||||
<div class="border-start border-primary ps-3 pt-1 pb-1">
|
||||
{% include 'includes/candidate_modal_body.html' %}
|
||||
{% include 'includes/application_modal_body.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# TAB 5 CONTENT: AI ANALYSIS #}
|
||||
{% if candidate.is_resume_parsed %}
|
||||
{% if application.is_resume_parsed %}
|
||||
<div class="tab-pane fade" id="analysis-pane" role="tabpanel" aria-labelledby="analysis-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "AI Analysis Report" %}</h5>
|
||||
<div class="border-start border-primary ps-3 pt-1 pb-1">
|
||||
{% with analysis=candidate.ai_analysis_data %}
|
||||
{% with analysis=application.ai_analysis_data %}
|
||||
{# Match Score Card #}
|
||||
<div class="mb-4 p-3 rounded" style="background-color: {% if analysis.match_score >= 70 %}rgba(40, 167, 69, 0.1){% elif analysis.match_score >= 40 %}rgba(255, 193, 7, 0.1){% else %}rgba(220, 53, 69, 0.1){% endif %}">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
@ -626,7 +626,7 @@
|
||||
<span class="visually-hidden">{% trans "Loading..." %}</span>
|
||||
</div>
|
||||
<h5 class="text-primary">{% trans "Resume is being parsed" %}</h5>
|
||||
<p class="text-muted">{% trans "Our AI is analyzing the candidate's resume to generate insights. This may take a few moments." %}</p>
|
||||
<p class="text-muted">{% trans "Our AI is analyzing the application's resume to generate insights. This may take a few moments." %}</p>
|
||||
<div class="progress mt-3" style="height: 6px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 75%"></div>
|
||||
@ -648,28 +648,28 @@
|
||||
<div class="card shadow-sm mb-2 p-2">
|
||||
<h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
|
||||
<div class="d-grid gap-2">
|
||||
{% comment %} <a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary">
|
||||
{% comment %} <a href="{% url 'application_update' application.slug %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-edit"></i> {% trans "Edit Details" %}
|
||||
</a> {% endcomment %}
|
||||
{% comment %} <a href="{% url 'candidate_delete' candidate.slug %}" class="btn btn-outline-danger" onclick="return confirm('{% trans "Are you sure you want to delete this candidate?" %}')">
|
||||
{% comment %} <a href="{% url 'application_delete' application.slug %}" class="btn btn-outline-danger" onclick="return confirm('{% trans "Are you sure you want to delete this candidate?" %}')">
|
||||
<i class="fas fa-trash-alt"></i> {% trans "Delete Candidate" %}
|
||||
</a> {% endcomment %}
|
||||
<a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary">
|
||||
<a href="{% url 'application_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
|
||||
</a>
|
||||
{% if candidate.resume %}
|
||||
{% if application.resume %}
|
||||
|
||||
{% comment %} <a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
||||
{% comment %} <a href="{{ application.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="fas fa-eye me-1"></i>
|
||||
{% trans "View Actual Resume" %}
|
||||
</a> {% endcomment %}
|
||||
|
||||
<a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary">
|
||||
<a href="{{ application.resume.url }}" download class="btn btn-outline-primary">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% trans "Download Resume" %}
|
||||
</a>
|
||||
|
||||
{% comment %} <a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
|
||||
{% comment %} <a href="{% url 'application_resume_template' application.slug %}" class="btn btn-outline-info">
|
||||
<i class="fas fa-file-alt me-1"></i>
|
||||
{% trans "View Resume AI Overview" %}
|
||||
</a> {% endcomment %}
|
||||
@ -681,7 +681,7 @@
|
||||
<div class="card shadow-sm mb-4 p-2">
|
||||
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire:" %}
|
||||
|
||||
{% with days=candidate.time_to_hire_days %}
|
||||
{% with days=application.time_to_hire_days %}
|
||||
{% if days > 0 %}
|
||||
{{ days }} day{{ days|pluralize }}
|
||||
{% else %}
|
||||
@ -699,10 +699,10 @@
|
||||
</div>
|
||||
|
||||
<div class="resume-parsed-section">
|
||||
{% if candidate.is_resume_parsed %}
|
||||
{% include 'recruitment/candidate_resume_template.html' %}
|
||||
{% if application.is_resume_parsed %}
|
||||
{% include 'recruitment/application_resume_template.html' %}
|
||||
{% else %}
|
||||
{% if candidate.scoring_timeout %}
|
||||
{% if application.scoring_timeout %}
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
||||
<div class="ai-loading-container">
|
||||
<i class="fas fa-robot ai-robot-icon"></i>
|
||||
@ -711,7 +711,7 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
||||
<button type="submit" class="btn btn-sm btn-main-action" hx-get="{% url 'candidate_retry_scoring' candidate.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume , Please Wait.. <i class='fa-solid fa-spinner fa-spin'></i>`">
|
||||
<button type="submit" class="btn btn-sm btn-main-action" hx-get="{% url 'application_retry_scoring' application.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume , Please Wait.. <i class='fa-solid fa-spinner fa-spin'></i>`">
|
||||
<i class="fas fa-redo-alt me-1"></i>
|
||||
{% trans "Unable to Parse Resume , click to retry" %}
|
||||
</button>
|
||||
@ -724,6 +724,6 @@
|
||||
|
||||
|
||||
{% if user.is_staff %}
|
||||
{% include "recruitment/partials/stage_update_modal.html" with candidate=candidate form=stage_form %}
|
||||
{% include "recruitment/partials/stage_update_modal.html" with application=application form=stage_form %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -7,9 +7,9 @@
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
{% if LANGUAGE_CODE == 'ar' %}
|
||||
<title>{{ candidate.resume_data_ar.full_name|default:"Candidate" }} - Candidate Profile</title>
|
||||
<title>{{ application.resume_data_ar.full_name|default:"Application" }} - Application Profile</title>
|
||||
{% else %}
|
||||
<title>{{ candidate.resume_data_en.full_name|default:"Candidate" }} - Candidate Profile</title>
|
||||
<title>{{ application.resume_data_en.full_name|default:"Application" }} - Application Profile</title>
|
||||
{% endif %}
|
||||
<!-- Use a modern icon set -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
@ -532,40 +532,40 @@
|
||||
<!-- Header Section -->
|
||||
<header class="header-box">
|
||||
<div class="header-info">
|
||||
<h1>{{ candidate.resume_data.full_name|default:"Candidate Name" }}</h1>
|
||||
<p>{{ candidate.resume_data.current_title|default:"Professional Title" }}</p>
|
||||
<h1>{{ application.resume_data.full_name|default:"Application Name" }}</h1>
|
||||
<p>{{ application.resume_data.current_title|default:"Professional Title" }}</p>
|
||||
<div class="contact-details">
|
||||
<div class="contact-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span>{{ candidate.resume_data.location|default:"Location" }}</span>
|
||||
<span>{{ application.resume_data.location|default:"Location" }}</span>
|
||||
</div>
|
||||
<!-- Displaying the raw contact string (which contains both phone and email in the example) -->
|
||||
<div class="contact-item">
|
||||
<i class="fas fa-id-card"></i>
|
||||
<span title="Contact Information: Phone and Email">{{ candidate.resume_data.contact|default:"Contact Information" }}</span>
|
||||
<span title="Contact Information: Phone and Email">{{ application.resume_data.contact|default:"Contact Information" }}</span>
|
||||
</div>
|
||||
<!-- GitHub and LinkedIn links for quick access (null in example but included for completeness) -->
|
||||
{% if candidate.resume_data.linkedin %}
|
||||
{% if application.resume_data.linkedin %}
|
||||
<div class="contact-item">
|
||||
|
||||
<a href="{{ candidate.resume_data.linkedin }}" target="_blank"><i class="fab fa-linkedin text-white"></i></a>
|
||||
<a href="{{ application.resume_data.linkedin }}" target="_blank"><i class="fab fa-linkedin text-white"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if candidate.resume_data.github %}
|
||||
{% if application.resume_data.github %}
|
||||
<div class="contact-item">
|
||||
|
||||
<a href="{{ candidate.resume_data.github }}" target="_blank"><i class="fab fa-github text-white"></i></a>
|
||||
<a href="{{ application.resume_data.github }}" target="_blank"><i class="fab fa-github text-white"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-box">
|
||||
<div class="score-value">{{ candidate.analysis_data.match_score|default:0 }}%</div>
|
||||
<div class="score-value">{{ application.analysis_data.match_score|default:0 }}%</div>
|
||||
<div class="score-text">Match Score</div>
|
||||
<div class="assessment-rating
|
||||
{% if candidate.analysis_data.match_score|default:0 < 50 %}score-red{% elif candidate.analysis_data.match_score|default:0 < 75 %}score-yellow{% else %}score-green{% endif %}">
|
||||
{% if application.analysis_data.match_score|default:0 < 50 %}score-red{% elif application.analysis_data.match_score|default:0 < 75 %}score-yellow{% else %}score-green{% endif %}">
|
||||
<!-- scoring_data.screening_stage_rating -->
|
||||
{{ candidate.analysis_data.screening_stage_rating|default:"Assessment" }}
|
||||
{{ application.analysis_data.screening_stage_rating|default:"Assessment" }}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -582,8 +582,8 @@
|
||||
Summary
|
||||
</h2>
|
||||
<p class="summary-text">
|
||||
<!-- candidate.resume_data.summary, falling back to scoring_data.job_fit_narrative -->
|
||||
{{ candidate.resume_data.summary|default:"Professional summary not available." }}
|
||||
<!-- application.resume_data.summary, falling back to scoring_data.job_fit_narrative -->
|
||||
{{ application.resume_data.summary|default:"Professional summary not available." }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@ -593,7 +593,7 @@
|
||||
<i class="fas fa-briefcase"></i>
|
||||
Experience
|
||||
</h2>
|
||||
{% for experience in candidate.resume_data.experience %}
|
||||
{% for experience in application.resume_data.experience %}
|
||||
<div class="experience-item">
|
||||
<div class="experience-header">
|
||||
<div>
|
||||
@ -608,7 +608,7 @@
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
{% if experience.start_date %}{{ experience.start_date }}{% endif %} -
|
||||
{% if experience.end_date and experience.end_date != "Present" %}{{ experience.end_date }}{% else %}Present{% endif %}
|
||||
<!-- candidate.resume_data.experience[].location -->
|
||||
<!-- application.resume_data.experience[].location -->
|
||||
{% if experience.location %}<span style="margin-left: 1rem;"><i class="fas fa-map-pin"></i>{{ experience.location }}</span>{% endif %}
|
||||
</p>
|
||||
{% if experience.key_achievements %}
|
||||
@ -628,7 +628,7 @@
|
||||
<i class="fas fa-graduation-cap"></i>
|
||||
Education
|
||||
</h2>
|
||||
{% for education in candidate.resume_data.education %}
|
||||
{% for education in application.resume_data.education %}
|
||||
<div class="education-item">
|
||||
<div class="icon-badge">
|
||||
<i class="fas fa-certificate"></i>
|
||||
@ -642,7 +642,7 @@
|
||||
{% if education.gpa %}
|
||||
<p class="meta"><i class="fas fa-award"></i> GPA: {{ education.gpa }}</p>
|
||||
{% endif %}
|
||||
<!-- candidate.resume_data.education[].relevant_courses -->
|
||||
<!-- application.resume_data.education[].relevant_courses -->
|
||||
{% if education.relevant_courses %}
|
||||
<p class="meta" style="margin-top: 0.25rem;">Courses: {{ education.relevant_courses|join:", " }}</p>
|
||||
{% endif %}
|
||||
@ -657,7 +657,7 @@
|
||||
<i class="fas fa-project-diagram"></i>
|
||||
Projects
|
||||
</h2>
|
||||
{% for project in candidate.resume_data.projects %}
|
||||
{% for project in application.resume_data.projects %}
|
||||
<div class="project-item">
|
||||
<h3>{{ project.name }}</h3>
|
||||
<p class="description">{{ project.brief_description }}</p>
|
||||
@ -670,37 +670,37 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not candidate.resume_data.projects %}
|
||||
{% if not application.resume_data.projects %}
|
||||
<p style="color: var(--color-gray-500); font-size: 0.875rem;">No projects detailed in the resume.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Keywords Section (NOW IN THE LEFT COLUMN) -->
|
||||
{% if candidate.analysis_data.top_3_keywords or candidate.analysis_data.cultural_fit_keywords %}
|
||||
{% if application.analysis_data.top_3_keywords or application.analysis_data.cultural_fit_keywords %}
|
||||
<section class="card-section" style="margin-top: 0;">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-tags"></i>
|
||||
Keywords
|
||||
</h2>
|
||||
|
||||
{% if candidate.analysis_data.top_3_keywords %}
|
||||
{% if application.analysis_data.top_3_keywords %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<h3 class="keyword-subheader"><i class="fas fa-key"></i>Top Keywords (Job Match)</h3>
|
||||
<div class="tag-list">
|
||||
<!-- scoring_data.top_3_keywords -->
|
||||
{% for keyword in candidate.analysis_data.top_3_keywords %}
|
||||
{% for keyword in application.analysis_data.top_3_keywords %}
|
||||
<span class="keyword-tag">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.cultural_fit_keywords %}
|
||||
{% if application.analysis_data.cultural_fit_keywords %}
|
||||
<div>
|
||||
<h3 class="keyword-subheader"><i class="fas fa-users"></i>Cultural Fit Keywords</h3>
|
||||
<div class="tag-list">
|
||||
<!-- scoring_data.cultural_fit_keywords -->
|
||||
{% for keyword in candidate.analysis_data.cultural_fit_keywords %}
|
||||
{% for keyword in application.analysis_data.cultural_fit_keywords %}
|
||||
<span class="cultural-tag">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -719,57 +719,57 @@
|
||||
Analysis
|
||||
</h2>
|
||||
|
||||
{% if candidate.analysis_data.category %}
|
||||
{% if application.analysis_data.category %}
|
||||
<div class="analysis-metric" style="margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-gray-100);">
|
||||
<span class="metric-title">Target Role Category:</span>
|
||||
<!-- scoring_data.category -->
|
||||
<span class="metric-value" style="color: var(--kaauh-teal);">{{ candidate.analysis_data.category }}</span>
|
||||
<span class="metric-value" style="color: var(--kaauh-teal);">{{ application.analysis_data.category }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if candidate.analysis_data.red_flags %}
|
||||
{% if application.analysis_data.red_flags %}
|
||||
<div class="narrative-box">
|
||||
<h3 class="flag-title red"><i class="fas fa-flag"></i>Red Flags</h3>
|
||||
<!-- scoring_data.red_flags -->
|
||||
<p class="narrative-text">{{ candidate.analysis_data.red_flags|join:". "|default:"None." }}</p>
|
||||
<p class="narrative-text">{{ application.analysis_data.red_flags|join:". "|default:"None." }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.strengths %}
|
||||
{% if application.analysis_data.strengths %}
|
||||
<div class="narrative-box strength-box">
|
||||
<h3 class="flag-title green"><i class="fas fa-circle-check"></i>Strengths</h3>
|
||||
<!-- scoring_data.strengths -->
|
||||
<p class="narrative-text">{{ candidate.analysis_data.strengths }}</p>
|
||||
<p class="narrative-text">{{ application.analysis_data.strengths }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.weaknesses %}
|
||||
{% if application.analysis_data.weaknesses %}
|
||||
<div class="narrative-box" style="margin-bottom: 1rem;">
|
||||
<h3 class="flag-title red"><i class="fas fa-triangle-exclamation"></i>Weaknesses</h3>
|
||||
<!-- scoring_data.weaknesses -->
|
||||
<p class="narrative-text">{{ candidate.analysis_data.weaknesses }}</p>
|
||||
<p class="narrative-text">{{ application.analysis_data.weaknesses }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.recommendation %}
|
||||
{% if application.analysis_data.recommendation %}
|
||||
<div class="analysis-summary">
|
||||
<h3 style="font-size: 0.875rem;">Recommendation</h3>
|
||||
<!-- scoring_data.recommendation -->
|
||||
<p style="font-size: 0.875rem;">{{ candidate.analysis_data.recommendation }}</p>
|
||||
<p style="font-size: 0.875rem;">{{ application.analysis_data.recommendation }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Criteria Checklist Section -->
|
||||
{% if candidate.analysis_data.criteria_checklist %}
|
||||
{% if application.analysis_data.criteria_checklist %}
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-list-check"></i>
|
||||
Required Criteria Check
|
||||
</h2>
|
||||
<div style="margin-top: 0.75rem;">
|
||||
{% for criterion, status in candidate.analysis_data.criteria_checklist.items %}
|
||||
{% for criterion, status in application.analysis_data.criteria_checklist.items %}
|
||||
<div class="criteria-item">
|
||||
<span class="text-gray-700">{{ criterion }}</span>
|
||||
<span class="metric-value" style="font-size: 0.875rem;">
|
||||
@ -790,10 +790,10 @@
|
||||
<i class="fas fa-tools"></i>
|
||||
Skills
|
||||
</h2>
|
||||
{% if candidate.resume_data.skills %}
|
||||
{% for category, skills in candidate.resume_data.skills.items %}
|
||||
{% if application.resume_data.skills %}
|
||||
{% for category, skills in application.resume_data.skills.items %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<!-- candidate.resume_data.skills -->
|
||||
<!-- application.resume_data.skills -->
|
||||
<h3 class="keyword-subheader" style="color: var(--color-gray-700);"><i class="fas fa-list-alt" style="color: transparent;"></i>{{ category|cut:"_"|title }}</h3>
|
||||
<div class="tag-list">
|
||||
{% for skill in skills %}
|
||||
@ -813,8 +813,8 @@
|
||||
<i class="fas fa-language"></i>
|
||||
Languages
|
||||
</h2>
|
||||
{% if candidate.analysis_data.language_fluency %}
|
||||
{% for language in candidate.analysis_data.language_fluency %}
|
||||
{% if application.analysis_data.language_fluency %}
|
||||
{% for language in application.analysis_data.language_fluency %}
|
||||
<div style="margin-bottom: 0.75rem;">
|
||||
<div class="analysis-metric" style="margin-bottom: 0.25rem; border-bottom: none;">
|
||||
<!-- scoring_data.language_fluency -->
|
||||
@ -842,69 +842,69 @@
|
||||
Key Metrics
|
||||
</h2>
|
||||
<div style="margin-top: 0.75rem;">
|
||||
{% if candidate.analysis_data.min_req_met_bool is not none %}
|
||||
{% if application.analysis_data.min_req_met_bool is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-shield-halved"></i>Min Requirements Met:</span>
|
||||
<!-- scoring_data.min_req_met_bool -->
|
||||
<span class="metric-value {% if candidate.analysis_data.min_req_met_bool %}text-green-check{% else %}text-red-x{% endif %}">
|
||||
{% if candidate.analysis_data.min_req_met_bool %}<i class="fas fa-check-circle"></i> Yes{% else %}<i class="fas fa-times-circle"></i> No{% endif %}
|
||||
<span class="metric-value {% if application.analysis_data.min_req_met_bool %}text-green-check{% else %}text-red-x{% endif %}">
|
||||
{% if application.analysis_data.min_req_met_bool %}<i class="fas fa-check-circle"></i> Yes{% else %}<i class="fas fa-times-circle"></i> No{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.years_of_experience is not none %}
|
||||
{% if application.analysis_data.years_of_experience is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-clock"></i>Total Experience:</span>
|
||||
<!-- scoring_data.years_of_experience -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.years_of_experience|floatformat:1 }} years</span>
|
||||
<span class="metric-value">{{ application.analysis_data.years_of_experience|floatformat:1 }} years</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.most_recent_job_title %}
|
||||
{% if application.analysis_data.most_recent_job_title %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-id-badge"></i>Most Recent Title (Scoring):</span>
|
||||
<!-- scoring_data.most_recent_job_title (explicitly added) -->
|
||||
<span class="metric-value max-w-50-percent" style="text-align: right;">{{ candidate.analysis_data.most_recent_job_title }}</span>
|
||||
<span class="metric-value max-w-50-percent" style="text-align: right;">{{ application.analysis_data.most_recent_job_title }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.seniority_level_match is not none %}
|
||||
{% if application.analysis_data.seniority_level_match is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-user-tie"></i>Seniority Match:</span>
|
||||
<!-- scoring_data.seniority_level_match -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.seniority_level_match|default:0 }}/100</span>
|
||||
<span class="metric-value">{{ application.analysis_data.seniority_level_match|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.soft_skills_score is not none %}
|
||||
{% if application.analysis_data.soft_skills_score is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-handshake"></i>Soft Skills Score:</span>
|
||||
<!-- scoring_data.soft_skills_score -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.soft_skills_score|default:0 }}/100</span>
|
||||
<span class="metric-value">{{ application.analysis_data.soft_skills_score|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.employment_stability_score is not none %}
|
||||
{% if application.analysis_data.employment_stability_score is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-anchor"></i>Stability Score:</span>
|
||||
<!-- scoring_data.employment_stability_score -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.employment_stability_score|default:0 }}/100</span>
|
||||
<span class="metric-value">{{ application.analysis_data.employment_stability_score|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.experience_industry_match is not none %}
|
||||
{% if application.analysis_data.experience_industry_match is not none %}
|
||||
<div class="analysis-metric" style="border-bottom: none; padding-bottom: 0;">
|
||||
<span class="metric-label"><i class="fas fa-industry"></i>Industry Match:</span>
|
||||
<!-- scoring_data.experience_industry_match -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.experience_industry_match|default:0 }}/100</span>
|
||||
<span class="metric-value">{{ application.analysis_data.experience_industry_match|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if candidate.analysis_data.transferable_skills_narrative %}
|
||||
{% if application.analysis_data.transferable_skills_narrative %}
|
||||
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-gray-100); font-size: 0.875rem; color: var(--color-gray-500);">
|
||||
<i class="fas fa-puzzle-piece" style="margin-right: 0.25rem;"></i> Transferable Skills:
|
||||
<!-- scoring_data.transferable_skills_narrative -->
|
||||
{{ candidate.analysis_data.transferable_skills_narrative }}
|
||||
{{ application.analysis_data.transferable_skills_narrative }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
@ -914,7 +914,7 @@
|
||||
|
||||
|
||||
{% if LANGUAGE_CODE == 'ar' %}
|
||||
{% with data_source=candidate.resume_data_ar analysis_source=candidate.analysis_data_ar %}
|
||||
{% with data_source=application.resume_data_ar analysis_source=application.analysis_data_ar %}
|
||||
<div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto; direction: rtl; text-align: right;">
|
||||
|
||||
{% include 'recruitment/partials/ai_overview_breadcromb.html' %}
|
||||
@ -1263,13 +1263,13 @@
|
||||
{% endwith %}
|
||||
|
||||
{% else %}
|
||||
{% with data_source=candidate.resume_data_en analysis_source=candidate.analysis_data_en %}
|
||||
{% with data_source=application.resume_data_en analysis_source=application.analysis_data_en %}
|
||||
<div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
|
||||
|
||||
{% include 'recruitment/partials/ai_overview_breadcromb.html' %}
|
||||
<header class="header-box">
|
||||
<div class="header-info">
|
||||
<h1>{{ data_source.full_name|default:"Candidate Name" }}</h1>
|
||||
<h1>{{ data_source.full_name|default:"Application Name" }}</h1>
|
||||
<p>{{ data_source.current_title|default:"Professional Title" }}</p>
|
||||
<div class="contact-details">
|
||||
<div class="contact-item">
|
||||
@ -94,12 +94,12 @@
|
||||
<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 'candidate_list' %}" class="btn btn-outline-light btn-sm" title="{% trans 'Back to List' %}">
|
||||
<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>
|
||||
{% if object.slug %}
|
||||
<a href="{% url 'candidate_detail' object.slug %}" class="btn btn-outline-light btn-sm" title="{% trans 'View Candidate' %}">
|
||||
<a href="{% url 'application_detail' object.slug %}" class="btn btn-outline-light btn-sm" title="{% trans 'View Candidate' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
<span class="d-none d-sm-inline">{% trans "View" %}</span>
|
||||
</a>
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Document Review - {{ job.title }} - University ATS{% endblock %}
|
||||
{% block title %}{% blocktrans %} Document Review - {{ job.title }} - University ATS{%endblocktrans %}{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
@ -60,8 +60,8 @@
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -70,10 +70,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -81,24 +81,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -118,7 +118,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -211,9 +211,9 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% comment %} <a href="{% url 'export_candidates_csv' job.slug 'document_review' %}"
|
||||
{% comment %} <a href="{% url 'export_applications_csv' job.slug 'document_review' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export document review candidates to CSV' %}">
|
||||
title="{% trans 'Export document review applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a> {% endcomment %}
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -229,7 +229,7 @@
|
||||
<!-- Search and Filter Controls -->
|
||||
<div class="filter-controls">
|
||||
<h4 class="h6 mb-3 fw-bold">
|
||||
<i class="fas fa-search me-1"></i> {% trans "Search Candidates" %}
|
||||
<i class="fas fa-search me-1"></i> {% trans "Search Applications" %}
|
||||
</h4>
|
||||
<form method="GET" class="mb-0">
|
||||
<div class="row g-3 align-items-end">
|
||||
@ -251,18 +251,18 @@
|
||||
</div>
|
||||
|
||||
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
|
||||
<i class="fas fa-users me-1"></i> {% trans "Candidates Ready for Document Review" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ candidates|length }}</span>
|
||||
<i class="fas fa-users me-1"></i> {% trans "Applications Ready for Document Review" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ applications|length }}</span>
|
||||
</h2>
|
||||
|
||||
<div class="kaauh-card p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
{# Use d-flex to align the entire contents (two forms and the separator) horizontally #}
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
|
||||
{# Form 1: Status Update #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
||||
@ -290,9 +290,9 @@
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_candidate_email' job.slug %}"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -300,12 +300,12 @@
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
|
||||
<table class="table candidate-table align-middle">
|
||||
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 2%;">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
</div>
|
||||
@ -329,21 +329,21 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
<input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-name">
|
||||
{{ candidate.person.first_name }} {{ candidate.person.last_name }}
|
||||
<div class="application-name">
|
||||
{{ application.person.first_name }} {{ application.person.last_name }}
|
||||
</div>
|
||||
</td>
|
||||
<td><div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.person.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.person.phone|default:"--" }}
|
||||
<td><div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.person.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.person.phone|default:"--" }}
|
||||
</div></td>
|
||||
<td>
|
||||
<span class="stage-badge stage-Interview">
|
||||
@ -351,7 +351,7 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% with documents=candidate.documents.all %}
|
||||
{% with documents=application.documents.all %}
|
||||
{% if documents %}
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
@ -398,7 +398,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
<i class="fas fa-eye ms-1"></i>
|
||||
@ -412,7 +412,7 @@
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates are currently ready for document review." %}
|
||||
{% trans "No applications are currently ready for document review." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
@ -425,7 +425,7 @@
|
||||
<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="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Candidate Details / Bulk Action Form" %}
|
||||
{% trans "Application Details / Bulk Action Form" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Candidate Tier Management - {{ job.title }} - ATS{% endblock %}
|
||||
{% block title %}{% blocktrans %}Application Tier Management - {{ job.title }} - ATS {% endblocktrans %}{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
@ -71,8 +71,8 @@
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -81,10 +81,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -92,24 +92,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -129,7 +129,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -174,13 +174,13 @@
|
||||
{% trans "Exam Management" %} - {{ job.title }}
|
||||
</h1>
|
||||
<h2 class="h5 text-muted mb-0">
|
||||
{% trans "Candidates in Exam Stage:" %} <span class="fw-bold">{{ total_candidates }}</span>
|
||||
{% trans "Applications in Exam Stage:" %} <span class="fw-bold">{{ total_candidates }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'export_candidates_csv' job.slug 'exam' %}"
|
||||
<a href="{% url 'export_applications_csv' job.slug 'exam' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export exam candidates to CSV' %}">
|
||||
title="{% trans 'Export exam applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -194,15 +194,15 @@
|
||||
</div>
|
||||
|
||||
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
|
||||
{% trans "Candidate List" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ candidates|length }} / {{ total_candidates }} Total</span>
|
||||
{% trans "Application List" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ applications|length }} / {{ total_candidates }} Total</span>
|
||||
<small class="text-muted fw-normal ms-2">({% trans "Sorted by AI Score" %})</small>
|
||||
</h2>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Using d-flex for horizontal alignment and align-items-end to align items to the bottom baseline #}
|
||||
@ -233,9 +233,9 @@
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_candidate_email' job.slug %}"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -245,13 +245,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" method="post">
|
||||
<form id="application-form" method="post">
|
||||
{% csrf_token %}
|
||||
<table class="table candidate-table align-middle">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2%;">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
@ -268,56 +268,56 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input
|
||||
name="candidate_ids"
|
||||
value="{{ candidate.id }}"
|
||||
type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
value="{{ application.id }}"
|
||||
type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-name">
|
||||
{{ candidate.name }}
|
||||
<div class="application-name">
|
||||
{{ application.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
|
||||
<div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge ai-score-badge">{{ candidate.match_score|default:"0" }}%</span>
|
||||
<span class="badge ai-score-badge">{{ application.match_score|default:"0" }}%</span>
|
||||
</td>
|
||||
<td>
|
||||
{{candidate.exam_date|date:"d-m-Y h:i A"|default:"--"}}
|
||||
{{application.exam_date|date:"d-m-Y h:i A"|default:"--"}}
|
||||
</td>
|
||||
<td id="exam-score-{{ candidate.pk}}">
|
||||
{{candidate.exam_score|default:"--"}}
|
||||
<td id="exam-score-{{ application.pk}}">
|
||||
{{application.exam_score|default:"--"}}
|
||||
</td>
|
||||
|
||||
<td class="text-center" id="status-result-{{ candidate.pk}}">
|
||||
{% if not candidate.exam_status %}
|
||||
<td class="text-center" id="status-result-{{ application.pk}}">
|
||||
{% if not application.exam_status %}
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'exam' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'exam' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if candidate.exam_status %}
|
||||
<button type="button" class="btn btn-{% if candidate.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
{% if application.exam_status %}
|
||||
<button type="button" class="btn btn-{% if application.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'exam' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'exam' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
{{ candidate.exam_status }}
|
||||
{{ application.exam_status }}
|
||||
</button>
|
||||
{% else %}
|
||||
--
|
||||
@ -329,7 +329,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
<i class="fas fa-eye"></i>
|
||||
@ -339,10 +339,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not candidates %}
|
||||
{% if not applications %}
|
||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates are currently in the Exam stage for this job." %}
|
||||
{% trans "No applications are currently in the Exam stage for this job." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
@ -355,14 +355,14 @@
|
||||
<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);">
|
||||
{% trans "Candidate Details & Exam Update" %}
|
||||
{% trans "Application Details & Exam Update" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div id="candidateviewModalBody" class="modal-body">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||
{% trans "Loading candidate data..." %}
|
||||
{% trans "Loading application data..." %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="border-top: 1px solid var(--kaauh-border);">
|
||||
@ -60,8 +60,8 @@
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -70,10 +70,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -81,24 +81,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -118,7 +118,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -189,22 +189,22 @@
|
||||
<div>
|
||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-trophy me-2"></i>
|
||||
{% trans "Hired Candidates" %} - {{ job.title }}
|
||||
{% trans "Hired Applications" %} - {{ job.title }}
|
||||
</h1>
|
||||
<h2 class="h5 text-muted mb-0">
|
||||
{% trans "Successfully Hired:" %} <span class="fw-bold">{{ candidates|length }}</span>
|
||||
{% trans "Successfully Hired:" %} <span class="fw-bold">{{ applications|length }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button"
|
||||
class="btn btn-main-action"
|
||||
onclick="syncHiredCandidates()"
|
||||
title="{% trans 'Sync hired candidates to external sources' %}">
|
||||
title="{% trans 'Sync hired applications to external sources' %}">
|
||||
<i class="fas fa-sync me-1"></i> {% trans "Sync to Sources" %}
|
||||
</button>
|
||||
<a href="{% url 'export_candidates_csv' job.slug 'hired' %}"
|
||||
<a href="{% url 'export_applications_csv' job.slug 'hired' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export hired candidates to CSV' %}">
|
||||
title="{% trans 'Export hired applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -217,7 +217,7 @@
|
||||
<div class="success-header">
|
||||
<i class="fas fa-check-circle fa-3x mb-3"></i>
|
||||
<h3 class="mb-2">{% trans "Congratulations!" %}</h3>
|
||||
<p class="mb-0">{% trans "These candidates have successfully completed the hiring process and joined your team." %}</p>
|
||||
<p class="mb-0">{% trans "These applications have successfully completed the hiring process and joined your team." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="applicant-tracking-timeline">
|
||||
@ -225,9 +225,9 @@
|
||||
</div>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# MODIFIED: Using d-flex for horizontal alignment and align-items-end to align everything based on the baseline of the button/select #}
|
||||
@ -256,9 +256,9 @@
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_candidate_email' job.slug %}"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -269,13 +269,13 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
|
||||
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
{% csrf_token %}
|
||||
<table class="table candidate-table align-middle">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2%">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
@ -291,36 +291,36 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
<input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-name">
|
||||
{{ candidate.name }}
|
||||
<div class="application-name">
|
||||
{{ application.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
|
||||
<div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<div class="application-details">
|
||||
<strong>{{ job.title }}</strong><br>
|
||||
<small class="text-muted">{{ job.department }}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="hired-date">
|
||||
{% if candidate.offer_date %}
|
||||
{% if application.offer_date %}
|
||||
<i class="fas fa-calendar me-1"></i>
|
||||
{{ candidate.offer_date|date:"M d, Y" }}
|
||||
{{ application.offer_date|date:"M d, Y" }}
|
||||
{% else %}
|
||||
<span class="text-muted">--</span>
|
||||
{% endif %}
|
||||
@ -337,12 +337,12 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<a href="{% url 'candidate_resume_template' candidate.slug %}"
|
||||
<a href="{% url 'application_resume_template' application.slug %}"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="View Resume Template">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
@ -356,10 +356,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
{% if not candidates %}
|
||||
{% if not applications %}
|
||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates have been hired for this position yet." %}
|
||||
{% trans "No applications have been hired for this position yet." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -372,7 +372,7 @@
|
||||
<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);">
|
||||
{% trans "Hired Candidate Details" %}
|
||||
{% trans "Hired Application Details" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -399,7 +399,7 @@
|
||||
<div id="syncResultsModalBody" class="modal-body">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||
{% trans "Syncing candidates..." %}
|
||||
{% trans "Syncing applications..." %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -491,8 +491,8 @@
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5>{% trans "Syncing hired candidates..." %}</h5>
|
||||
<p class="text-muted">{% trans "Please wait while we sync candidates to external sources." %}</p>
|
||||
<h5>{% trans "Syncing hired applications..." %}</h5>
|
||||
<p class="text-muted">{% trans "Please wait while we sync applications to external sources." %}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -503,7 +503,7 @@
|
||||
syncButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> {% trans "Syncing..." %}';
|
||||
|
||||
// Perform sync request
|
||||
fetch(`{% url 'sync_hired_candidates' job.slug %}`, {
|
||||
fetch(`{% url 'sync_hired_applications' job.slug %}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -553,7 +553,7 @@
|
||||
<strong>{% trans "Failed:" %}</strong> <span class="text-danger">${results.failed_syncs}</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>{% trans "Candidates Synced:" %}</strong> ${results.total_candidates}
|
||||
<strong>{% trans "Applications Synced:" %}</strong> ${results.total_candidates}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -579,7 +579,7 @@
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">{% trans "Candidates Processed:" %}</small>
|
||||
<small class="text-muted">{% trans "Applications Processed:" %}</small>
|
||||
<div class="fw-bold">${source.candidates_processed}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}- {{ job.title }} - ATS{% endblock %}
|
||||
{% block title %}{% blocktrans %}Application Tier Management - {{ job.title }} - ATS {% endblocktrans %}{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
@ -71,8 +71,8 @@
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -81,10 +81,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -92,24 +92,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -129,7 +129,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -177,13 +177,13 @@
|
||||
{% trans "Interview Management" %} - {{ job.title }}
|
||||
</h1>
|
||||
<h2 class="h5 text-muted mb-0">
|
||||
{% trans "Candidates in Interview Stage:" %} <span class="fw-bold">{{ candidates|length }}</span>
|
||||
{% trans "Applications in Interview Stage:" %} <span class="fw-bold">{{ applications|length }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'export_candidates_csv' job.slug 'interview' %}"
|
||||
<a href="{% url 'export_applications_csv' job.slug 'interview' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export interview candidates to CSV' %}">
|
||||
title="{% trans 'Export interview applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -196,13 +196,13 @@
|
||||
</div>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
{# Use d-flex to align the entire contents (two forms and the separator) horizontally #}
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
|
||||
{# Form 1: Status Update #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
||||
@ -227,7 +227,7 @@
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
|
||||
{# Form 2: Schedule Interviews #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
|
||||
</button>
|
||||
@ -239,9 +239,9 @@
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_candidate_email' job.slug %}"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -251,13 +251,13 @@
|
||||
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
|
||||
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
{% csrf_token %}
|
||||
<table class="table candidate-table align-middle">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2%">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
@ -276,11 +276,11 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
<input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
|
||||
</td>
|
||||
@ -288,35 +288,35 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
{{ candidate.name }}
|
||||
{{ application.name }}
|
||||
</button>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
|
||||
<div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="candidate-details text-muted">
|
||||
{% if candidate.get_latest_meeting %}
|
||||
{{ candidate.get_latest_meeting }}
|
||||
<td class="application-details text-muted">
|
||||
{% if application.get_latest_meeting %}
|
||||
{{ application.get_latest_meeting }}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="candidate-details text-muted"><div class="d-block">
|
||||
{% if candidate.get_latest_meeting.duration %}
|
||||
{{ candidate.get_latest_meeting.duration }} {% trans _("Minutes") %}
|
||||
<td class="application-details text-muted"><div class="d-block">
|
||||
{% if application.get_latest_meeting.duration %}
|
||||
{{ application.get_latest_meeting.duration }} {% trans _("Minutes") %}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
</div></td>
|
||||
<td class="candidate-details text-muted">
|
||||
{% with latest_meeting=candidate.get_latest_meeting %}
|
||||
<td class="application-details text-muted">
|
||||
{% with latest_meeting=application.get_latest_meeting %}
|
||||
{% if latest_meeting %}
|
||||
{{ latest_meeting.start_time|date:"d-m-Y h:i A" }}
|
||||
{% else %}
|
||||
@ -325,7 +325,7 @@
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td>
|
||||
{% with latest_meeting=candidate.get_latest_meeting %}
|
||||
{% with latest_meeting=application.get_latest_meeting %}
|
||||
{% if latest_meeting and latest_meeting.details_url %}
|
||||
<a href="{{ latest_meeting.details_url }}" target="_blank" class="btn btn-sm bg-primary-theme text-white" title="Join Interview"
|
||||
{% if latest_meeting.status == 'ended' %}disabled{% endif %}>
|
||||
@ -339,7 +339,7 @@
|
||||
</td>
|
||||
<td>
|
||||
{{ latest_meeting.status }}
|
||||
{% with latest_meeting=candidate.get_latest_meeting %}
|
||||
{% with latest_meeting=application.get_latest_meeting %}
|
||||
{% if latest_meeting %}
|
||||
<span class="badge {% if latest_meeting.status == 'waiting' %}bg-warning{% elif latest_meeting.status == 'started' %}bg-success{% elif latest_meeting.status == 'ended' %}bg-danger{% endif %}">
|
||||
{% if latest_meeting.status == 'started' %}
|
||||
@ -352,25 +352,25 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="text-center" id="interview-result-{{ candidate.pk }}">
|
||||
{% if not candidate.interview_status %}
|
||||
<td class="text-center" id="interview-result-{{ application.pk }}">
|
||||
{% if not application.interview_status %}
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'interview' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if candidate.interview_status %}
|
||||
<button type="button" class="btn btn-{% if candidate.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
{% if application.interview_status %}
|
||||
<button type="button" class="btn btn-{% if application.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'interview' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
{{ candidate.interview_status }}
|
||||
{{ application.interview_status }}
|
||||
</button>
|
||||
{% else %}
|
||||
--
|
||||
@ -379,13 +379,13 @@
|
||||
</td>
|
||||
<td>
|
||||
|
||||
{% if candidate.get_latest_meeting %}
|
||||
{% if candidate.get_latest_meeting.location_type == 'Remote'%}
|
||||
{% if application.get_latest_meeting %}
|
||||
{% if application.get_latest_meeting.location_type == 'Remote'%}
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'reschedule_meeting_for_candidate' job.slug candidate.pk candidate.get_latest_meeting.pk %}"
|
||||
hx-get="{% url 'reschedule_meeting_for_application' job.slug application.pk application.get_latest_meeting.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Reschedule">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
@ -394,7 +394,7 @@
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'delete_meeting_for_candidate' job.slug candidate.pk candidate.get_latest_meeting.pk %}"
|
||||
hx-get="{% url 'schedule_meeting_for_application' job.slug application.pk application.get_latest_meeting.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Delete Meeting">
|
||||
<i class="fas fa-trash"></i>
|
||||
@ -404,7 +404,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'reschedule_onsite_meeting' job.slug candidate.pk candidate.get_latest_meeting.pk %}"
|
||||
hx-get="{% url 'reschedule_onsite_meeting' job.slug application.pk application.get_latest_meeting.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Reschedule">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
@ -413,7 +413,7 @@
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'delete_onsite_meeting_for_candidate' job.slug candidate.pk candidate.get_latest_meeting.pk %}"
|
||||
hx-get="{% url 'delete_onsite_meeting_for_application' job.slug application.pk application.get_latest_meeting.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Delete Meeting">
|
||||
<i class="fas fa-trash"></i>
|
||||
@ -425,7 +425,7 @@
|
||||
<button type="button" class="btn btn-main-action btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'schedule_meeting_for_candidate' job.slug candidate.pk %}"
|
||||
hx-get="{% url 'schedule_meeting_for_application' job.slug application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
data-modal-title="{% trans 'Schedule Interview' %}"
|
||||
title="Schedule Interview">
|
||||
@ -435,7 +435,7 @@
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
{# UPDATED: Points to the specific Onsite scheduling URL #}
|
||||
hx-get="{% url 'schedule_onsite_meeting_for_candidate' job.slug candidate.pk %}"
|
||||
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">
|
||||
@ -448,10 +448,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not candidates %}
|
||||
{% if not applications %}
|
||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates are currently in the Interview stage for this job." %}
|
||||
{% trans "No applications are currently in the Interview stage for this job." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
@ -463,7 +463,7 @@
|
||||
<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="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Candidate Details / Bulk Action Form" %}
|
||||
{% trans "Application Details / Bulk Action Form" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -598,7 +598,7 @@
|
||||
</div>
|
||||
`;
|
||||
// Reset the modal title to its default state for next use
|
||||
const defaultTitle = "{% trans "Candidate Details / Bulk Action Form" %}";
|
||||
const defaultTitle = "{% trans "Application Details / Bulk Action Form" %}";
|
||||
candidateviewModalLabel.textContent = defaultTitle;
|
||||
}
|
||||
});
|
||||
@ -15,7 +15,7 @@
|
||||
--kaauh-success: #28a745;
|
||||
--kaauh-danger: #dc3545;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
@ -146,9 +146,9 @@
|
||||
/* ------------------------------------------- */
|
||||
.kaats-spinner {
|
||||
animation: kaats-spinner-rotate 1.5s linear infinite;
|
||||
width: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@ -160,7 +160,7 @@
|
||||
.kaats-spinner circle {
|
||||
stroke: var(--kaauh-border, #e9ecef);
|
||||
fill: none;
|
||||
stroke-width: 5;
|
||||
stroke-width: 5;
|
||||
}
|
||||
|
||||
@keyframes kaats-spinner-rotate {
|
||||
@ -193,7 +193,7 @@
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-users me-2"></i> {% trans "Applications List" %}
|
||||
</h1>
|
||||
<a href="{% url 'candidate_create' %}" class="btn btn-main-action">
|
||||
<a href="{% url 'application_create' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Add New Application" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -211,7 +211,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
{% url 'candidate_list' as candidate_list_url %}
|
||||
{% url 'application_list' as candidate_list_url %}
|
||||
|
||||
<form method="GET" class="row g-3 align-items-end h-100">
|
||||
{% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
|
||||
@ -225,14 +225,14 @@
|
||||
<option value="{{ job.slug }}" {% if job_filter == job.slug %}selected{% endif %}>{{ job.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="stage_filter" class="form-label small text-muted">{% trans "Filter by Stages" %}</label>
|
||||
<div class="d-flex gap-2">
|
||||
|
||||
|
||||
<select name="stage" id="stage_filter" class="form-select form-select-sm">
|
||||
<option value="">{% trans "All Stages" %}</option>
|
||||
<option value="Applied" {% if stage_filter == 'Applied' %}selected{% endif %}>{% trans "Applied" %}</option>
|
||||
@ -250,7 +250,7 @@
|
||||
<i class="fas fa-filter me-1"></i> {% trans "Apply Filters" %}
|
||||
</button>
|
||||
{% if job_filter or stage_filter or search_query %}
|
||||
<a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<a href="{% url 'application_list' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Clear" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -279,14 +279,14 @@
|
||||
<th scope="col" >{% trans "Major" %}</th>
|
||||
<th scope="col" >{% trans "Stage" %}</th>
|
||||
<th scope="col">{% trans "Hiring Source" %}</th>
|
||||
<th scope="col" >{% trans "created At" %}</th>
|
||||
<th scope="col" >{% trans "Created At" %}</th>
|
||||
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in applications %}
|
||||
<tr>
|
||||
<td class="fw-medium"><a href="{% url 'candidate_detail' candidate.slug %}" class="text-decoration-none link-secondary">{{ candidate.name }}<a></td>
|
||||
<td class="fw-medium"><a href="{% url 'application_detail' candidate.slug %}" class="text-decoration-none link-secondary">{{ candidate.name }}<a></td>
|
||||
<td>{{ candidate.email }}</td>
|
||||
<td> <span class="badge bg-primary"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-decoration-none text-white">{{ candidate.job.title }}</a></span></td>
|
||||
<td>
|
||||
@ -297,7 +297,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'candidate_list' %}" class="text-decoration-none d-flex align-items-center gap-2">
|
||||
<a href="{% url 'application_list' %}" class="text-decoration-none d-flex align-items-center gap-2">
|
||||
<svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;">
|
||||
<circle cx="25" cy="25" r="20"></circle>
|
||||
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
|
||||
@ -325,16 +325,16 @@
|
||||
<td>{{ candidate.created_at|date:"d-m-Y" }}</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-secondary" title="{% trans 'View' %}">
|
||||
<a href="{% url 'application_detail' candidate.slug %}" class="btn btn-outline-secondary" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<a href="{% url 'application_update' candidate.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-delete-url="{% url 'application_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.name }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
@ -356,7 +356,7 @@
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h5 class="card-title flex-grow-1 me-3">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="text-decoration-none text-primary-theme ">{{ candidate.name }}</a>
|
||||
<a href="{% url 'application_detail' candidate.slug %}" class="text-decoration-none text-primary-theme ">{{ candidate.name }}</a>
|
||||
</h5>
|
||||
<span class="badge bg-primary">{{ candidate.stage }}</span>
|
||||
</div>
|
||||
@ -365,11 +365,11 @@
|
||||
<span class="d-block mb-1"><i class="fas fa-envelope"></i> {{ candidate.email }}</span>
|
||||
<span class="d-block mb-1"><i class="fas fa-phone-alt"></i> {{ candidate.phone|default:"N/A" }}</span>
|
||||
<span class="d-block mb-1"><i class="fas fa-calendar-alt"></i> {{ candidate.created_at|date:"d-m-Y" }}</span>
|
||||
<span class="d-block mt-2"><i class="fas fa-briefcase"></i>
|
||||
<span class="d-block mt-2"><i class="fas fa-briefcase"></i>
|
||||
<span class="badge bg-primary"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-decoration-none text-white">{{ candidate.job.title }}</a></span>
|
||||
</span>
|
||||
{% if candidate.hiring_agency %}
|
||||
<span class="d-block mt-2"><i class="fas fa-building"></i>
|
||||
<span class="d-block mt-2"><i class="fas fa-building"></i>
|
||||
<a href="{% url 'agency_detail' candidate.hiring_agency.slug %}" class="text-decoration-none">
|
||||
<span class="badge bg-info">{{ candidate.hiring_agency.name }}</span>
|
||||
</a>
|
||||
@ -379,16 +379,16 @@
|
||||
|
||||
<div class="mt-auto pt-3 border-top">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-sm btn-main-action">
|
||||
<a href="{% url 'application_detail' candidate.slug %}" class="btn btn-sm btn-main-action">
|
||||
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-sm btn-outline-secondary">
|
||||
<a href="{% url 'application_update' candidate.slug %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-edit"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-delete-url="{% url 'application_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.name }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
@ -411,7 +411,7 @@
|
||||
<h3>{% trans "No application found" %}</h3>
|
||||
<p class="text-muted">{% trans "Create your first application." %}</p>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'candidate_create' %}" class="btn btn-main-action mt-3">
|
||||
<a href="{% url 'application_create' %}" class="btn btn-main-action mt-3">
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Add Application" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -1,7 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}- {{ job.title }} - ATS{% endblock %}
|
||||
{% block title %}{% blocktrans %}Application Tier Management - {{ job.title }} - ATS {% endblocktrans %}{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
@ -71,8 +70,8 @@
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -81,10 +80,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -92,24 +91,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -129,7 +128,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -176,13 +175,13 @@
|
||||
{% trans "Offer Management" %} - {{ job.title }}
|
||||
</h1>
|
||||
<h2 class="h5 text-muted mb-0">
|
||||
{% trans "Candidates in Offer Stage:" %} <span class="fw-bold">{{ candidates|length }}</span>
|
||||
{% trans "Applications in Offer Stage:" %} <span class="fw-bold">{{ applications|length }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'export_candidates_csv' job.slug 'offer' %}"
|
||||
<a href="{% url 'export_applications_csv' job.slug 'offer' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export offer candidates to CSV' %}">
|
||||
title="{% trans 'Export offer applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -195,13 +194,13 @@
|
||||
</div>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
{# Use d-flex and align-items-end on the container to align the form and the separator #}
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
|
||||
{# Form: Hired/Rejected Status Update #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Select element #}
|
||||
@ -236,9 +235,9 @@
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_candidate_email' job.slug %}"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -247,13 +246,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
|
||||
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
{% csrf_token %}
|
||||
<table class="table candidate-table align-middle">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2%">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
@ -270,43 +269,43 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
<input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-name">
|
||||
{{ candidate.name }}
|
||||
<div class="application-name">
|
||||
{{ application.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
|
||||
<div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center" id="status-result-{{ candidate.pk}}">
|
||||
{% if not candidate.offer_status %}
|
||||
<td class="text-center" id="status-result-{{ application.pk}}">
|
||||
{% if not application.offer_status %}
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'offer' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'offer' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if candidate.offer_status %}
|
||||
<button type="button" class="btn btn-{% if candidate.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm"
|
||||
{% if application.offer_status %}
|
||||
<button type="button" class="btn btn-{% if application.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_candidate_status' job.slug candidate.slug 'offer' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'offer' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
{{ candidate.offer_status }}
|
||||
{{ application.offer_status }}
|
||||
</button>
|
||||
{% else %}
|
||||
--
|
||||
@ -314,7 +313,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% with documents=candidate.documents.all %}
|
||||
{% with documents=application.documents.all %}
|
||||
{% if documents %}
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
@ -361,7 +360,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
<i class="fas fa-eye"></i>
|
||||
@ -371,10 +370,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not candidates %}
|
||||
{% if not applications %}
|
||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates are currently in the Offer stage for this job." %}
|
||||
{% trans "No applications are currently in the Offer stage for this job." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
@ -387,7 +386,7 @@
|
||||
<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="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Candidate Details / Bulk Action Form" %}
|
||||
{% trans "Application Details / Bulk Action Form" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Candidate Management - {{ job.title }} - University ATS{% endblock %}
|
||||
{% block title %}Application Management - {{ job.title }} - University ATS{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
@ -71,8 +71,8 @@
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -81,10 +81,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -92,24 +92,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -129,7 +129,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -216,7 +216,7 @@
|
||||
<div>
|
||||
<h1 class="h3 mb-1 page-header">
|
||||
<i class="fas fa-layer-group me-2"></i>
|
||||
{% trans "Applicant Screening" %}
|
||||
{% trans "Application Screening" %}
|
||||
</h1>
|
||||
<h2 class="h5 text-muted mb-0">
|
||||
{% trans "Job:" %} {{ job.title }}
|
||||
@ -224,9 +224,9 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'export_candidates_csv' job.slug 'screening' %}"
|
||||
<a href="{% url 'export_applications_csv' job.slug 'screening' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export screening candidates to CSV' %}">
|
||||
title="{% trans 'Export screening applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -241,7 +241,7 @@
|
||||
|
||||
<div class="filter-controls">
|
||||
<h4 class="h6 mb-3 fw-bold">
|
||||
<i class="fas fa-sort-numeric-up me-1"></i> {% trans "AI Scoring & Top Candidate Filter" %}
|
||||
<i class="fas fa-sort-numeric-up me-1"></i> {% trans "AI Scoring & Top Application Filter" %}
|
||||
</h4>
|
||||
|
||||
<form method="GET" class="mb-0">
|
||||
@ -296,10 +296,10 @@
|
||||
|
||||
<div class="col-auto">
|
||||
<label for="tier1_count" class="form-label small text-muted mb-1">
|
||||
{% trans "Top N Candidates" %}
|
||||
{% trans "Top N applications" %}
|
||||
</label>
|
||||
<input type="number" name="tier1_count" id="tier1_count" class="form-control form-control-sm"
|
||||
value="{{ tier1_count }}" min="1" max="{{ total_candidates }}" style="width: 120px;">
|
||||
value="{{ tier1_count }}" min="1" max="{{ total_applications }}" style="width: 120px;">
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
@ -312,14 +312,14 @@
|
||||
</div>
|
||||
|
||||
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
|
||||
<i class="fas fa-users me-1"></i> {% trans "Candidate List" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ candidates|length }} / {{ total_candidates }} Total</span>
|
||||
<i class="fas fa-users me-1"></i> {% trans "Application List" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ applications|length }} / {{ total_applications }} {% trans "Total" %}</span>
|
||||
</h2>
|
||||
|
||||
<div class="kaauh-card p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# MODIFIED: Using d-flex for horizontal alignment and align-items-end to align everything based on the baseline of the button/select #}
|
||||
@ -348,9 +348,9 @@
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_candidate_email' job.slug %}"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -361,13 +361,13 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post">
|
||||
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<table class="table candidate-table align-middle">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 2%;">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
@ -401,33 +401,33 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input
|
||||
name="candidate_ids"
|
||||
value="{{ candidate.id }}"
|
||||
type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
value="{{ application.id }}"
|
||||
type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="candidate-name text-primary-theme text-decoration-none">
|
||||
{{ candidate.name }}
|
||||
<a href="#" class="application-name text-primary-theme text-decoration-none">
|
||||
{{ application.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
|
||||
<div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">{{candidate.person.gpa|default:"0"}}</td>
|
||||
<td class="text-center">{{application.person.gpa|default:"0"}}</td>
|
||||
<td class="text-center">
|
||||
{% if candidate.is_resume_parsed %}
|
||||
{% if candidate.match_score %}
|
||||
{% if application.is_resume_parsed %}
|
||||
{% if application.match_score %}
|
||||
<span class="badge ai-score-badge">
|
||||
{{ candidate.match_score|default:"0" }}%
|
||||
{{ application.match_score|default:"0" }}%
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
@ -442,23 +442,23 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if candidate.screening_stage_rating %}
|
||||
{% if application.screening_stage_rating %}
|
||||
<span class="badge ai-score-badge d-inline-block text-wrap">
|
||||
{{ candidate.screening_stage_rating|default:"--" }}
|
||||
{{ application.screening_stage_rating|default:"--" }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if candidate.professional_category %}
|
||||
{% if application.professional_category %}
|
||||
<span class="badge ai-score-badge">
|
||||
{{ candidate.professional_category }}
|
||||
{{ application.professional_category }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if candidate.top_3_keywords %}
|
||||
{% if application.top_3_keywords %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for skill in candidate.top_3_keywords %}
|
||||
{% for skill in application.top_3_keywords %}
|
||||
<span class="badge ai-score-badge" style="font-size: smaller;">
|
||||
{{ skill }}
|
||||
</span>
|
||||
@ -471,9 +471,9 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Candidate Profile and Criteria">
|
||||
title="View Application Profile and Criteria">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
@ -481,10 +481,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not candidates %}
|
||||
{% if not applications %}
|
||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates match the current stage and filter criteria." %}
|
||||
{% trans "No applications match the current stage and filter criteria." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
@ -498,14 +498,14 @@
|
||||
<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);">
|
||||
{% trans "Candidate Criteria Review" %}
|
||||
{% trans "Application Criteria Review" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div id="candidateviewModalBody" class="modal-body">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||
{% trans "Loading candidate data..." %}
|
||||
{% trans "Loading application data..." %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="border-top: 1px solid var(--kaauh-border);">
|
||||
@ -1,11 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete Candidate - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1>
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2
|
||||
@ -71,7 +71,7 @@
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
{% if document.file %}
|
||||
<a href="{% url 'candidate_document_download' document.id %}"
|
||||
<a href="{% url 'application_document_download' document.id %}"
|
||||
class="text-green-600 hover:text-green-800 mr-3"
|
||||
title="Download document">
|
||||
<i class="fas fa-download"></i>
|
||||
|
||||
@ -184,7 +184,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'candidate_application_detail' application.slug %}"
|
||||
<a href="{% url 'applicant_application_detail' application.slug %}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye me-1"></i>
|
||||
{% trans "View Details" %}
|
||||
|
||||
@ -167,7 +167,7 @@
|
||||
}
|
||||
|
||||
/* Funnel Specific Styles */
|
||||
#candidate_funnel_chart {
|
||||
#application_funnel_chart {
|
||||
max-height: 400px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
@ -224,7 +224,7 @@
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
<i class="fas fa-chart-area stat-icon"></i>
|
||||
{% trans "Daily Candidate Applications Trend" %}
|
||||
{% trans "Daily Applications Trend" %}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
@ -261,7 +261,7 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="chart-container d-flex justify-content-center align-items-center">
|
||||
<canvas id="candidate_funnel_chart"></canvas>
|
||||
<canvas id="application_funnel_chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -289,11 +289,11 @@
|
||||
|
||||
<script>
|
||||
// Pass context data safely to JavaScript
|
||||
const totalCandidatesScoped = parseInt('{{ total_candidates|default:0 }}');
|
||||
const totalCandidatesScoped = parseInt('{{ total_applications|default:0 }}');
|
||||
const jobTitles = JSON.parse('{{ job_titles|escapejs }}').slice(0, 5);
|
||||
const jobAppCounts = JSON.parse('{{ job_app_counts|escapejs }}').slice(0, 5);
|
||||
const stages = JSON.parse('{{ candidate_stage|escapejs }}');
|
||||
const counts = JSON.parse('{{ candidates_count|safe }}');
|
||||
const stages = JSON.parse('{{ application_stage|escapejs }}');
|
||||
const counts = JSON.parse('{{ application_count|safe }}');
|
||||
|
||||
// --- 1. BAR CHART configuration (Top 5 Applications) ---
|
||||
const ctxBar = document.getElementById('applicationsChart').getContext('2d');
|
||||
@ -358,7 +358,7 @@
|
||||
// Slice and use the first N shades based on the number of stages
|
||||
const stageColors = tealShades.slice(tealShades.length - stages.length);
|
||||
|
||||
const ctxFunnel = document.getElementById('candidate_funnel_chart').getContext('2d');
|
||||
const ctxFunnel = document.getElementById('application_funnel_chart').getContext('2d');
|
||||
|
||||
new Chart(ctxFunnel, {
|
||||
type: 'bar',
|
||||
@ -377,7 +377,7 @@
|
||||
},
|
||||
// 2. VISIBLE CANDIDATE COUNT DATASET
|
||||
{
|
||||
label: '{% trans "Candidate Count" %}',
|
||||
label: '{% trans "Application Count" %}',
|
||||
data: counts,
|
||||
backgroundColor: stageColors,
|
||||
barThickness: 50
|
||||
@ -490,7 +490,7 @@
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: '{% trans "New Candidates" %}' },
|
||||
title: { display: true, text: '{% trans "New Applications" %}' },
|
||||
ticks: { precision: 0 },
|
||||
grid: { color: '#e0e0e0' }
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user