handle_resume_parsing_and_scoring now supports both english and arabic #40
6
.env
6
.env
@ -1,3 +1,3 @@
|
||||
DB_NAME=norahuniversity
|
||||
DB_USER=norahuniversity
|
||||
DB_PASSWORD=norahuniversity
|
||||
DB_NAME=haikal_db
|
||||
DB_USER=faheed
|
||||
DB_PASSWORD=Faheed@215
|
||||
@ -298,7 +298,7 @@ Q_CLUSTER = {
|
||||
"name": "KAAUH_CLUSTER",
|
||||
"workers": 2,
|
||||
"recycle": 500,
|
||||
"timeout": 60,
|
||||
"timeout": 120,
|
||||
"max_attempts": 1,
|
||||
"compress": True,
|
||||
"save_limit": 250,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -293,6 +293,7 @@ class ApplicationForm(forms.ModelForm):
|
||||
"resume",
|
||||
]
|
||||
labels = {
|
||||
"person":_("Applicant"),
|
||||
"resume": _("Resume"),
|
||||
"hiring_source": _("Hiring Type"),
|
||||
"hiring_agency": _("Hiring Agency"),
|
||||
@ -903,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"]
|
||||
@ -2344,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/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'),
|
||||
),
|
||||
]
|
||||
@ -18,7 +18,9 @@ from .validators import validate_hash_tags, validate_image_size
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from django.db.models import F, Value, IntegerField, CharField
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.db.models.fields.json import KeyTransform, KeyTextTransform
|
||||
|
||||
class CustomUser(AbstractUser):
|
||||
"""Custom user model extending AbstractUser"""
|
||||
@ -130,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,
|
||||
)
|
||||
@ -220,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(
|
||||
@ -363,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
|
||||
@ -375,88 +377,90 @@ class JobPosting(Base):
|
||||
return self.current_applications_count >= self.max_applications
|
||||
|
||||
@property
|
||||
def all_candidates(self):
|
||||
return self.applications.annotate(
|
||||
sortable_score=Coalesce(
|
||||
Cast(
|
||||
"ai_analysis_data__analysis_data__match_score",
|
||||
output_field=IntegerField(),
|
||||
def all_applications(self):
|
||||
# 1. Define the safe JSON extraction and conversion expression
|
||||
safe_score_expression = Cast(
|
||||
Coalesce(
|
||||
# Extract the score explicitly as a text string (KeyTextTransform)
|
||||
KeyTextTransform(
|
||||
'match_score',
|
||||
KeyTransform('analysis_data_en', 'ai_analysis_data')
|
||||
),
|
||||
0,
|
||||
)
|
||||
Value('0'), # Replace SQL NULL (from missing score) with the string '0'
|
||||
),
|
||||
output_field=IntegerField() # Cast the resulting string ('90' or '0') to an integer
|
||||
)
|
||||
|
||||
# 2. Annotate the score using the safe expression
|
||||
return self.applications.annotate(
|
||||
sortable_score=safe_score_expression
|
||||
).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.candidates.annotate(
|
||||
sortable_score=Cast(
|
||||
"ai_analysis_data__match_score", output_field=CharField()
|
||||
)
|
||||
)
|
||||
.order_by("-sortable_score")
|
||||
.count()
|
||||
or 0
|
||||
)
|
||||
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
|
||||
@ -751,88 +755,162 @@ class Application(Base):
|
||||
# ====================================================================
|
||||
# ✨ PROPERTIES (GETTERS) - Migrated from Candidate
|
||||
# ====================================================================
|
||||
@property
|
||||
def resume_data(self):
|
||||
return self.ai_analysis_data.get("resume_data", {})
|
||||
# @property
|
||||
# def resume_data(self):
|
||||
# return self.ai_analysis_data.get("resume_data", {})
|
||||
|
||||
# @property
|
||||
# def analysis_data(self):
|
||||
# return self.ai_analysis_data.get("analysis_data", {})
|
||||
|
||||
@property
|
||||
def analysis_data(self):
|
||||
return self.ai_analysis_data.get("analysis_data", {})
|
||||
def resume_data_en(self):
|
||||
return self.ai_analysis_data.get("resume_data_en", {})
|
||||
|
||||
@property
|
||||
def resume_data_ar(self):
|
||||
return self.ai_analysis_data.get("resume_data_ar", {})
|
||||
|
||||
@property
|
||||
def analysis_data_en(self):
|
||||
return self.ai_analysis_data.get("analysis_data_en", {})
|
||||
@property
|
||||
def analysis_data_ar(self):
|
||||
return self.ai_analysis_data.get("analysis_data_ar", {})
|
||||
|
||||
|
||||
@property
|
||||
def match_score(self) -> int:
|
||||
"""1. A score from 0 to 100 representing how well the candidate fits the role."""
|
||||
return self.analysis_data.get("match_score", 0)
|
||||
return self.analysis_data_en.get("match_score", 0)
|
||||
|
||||
@property
|
||||
def years_of_experience(self) -> float:
|
||||
"""4. The total number of years of professional experience as a numerical value."""
|
||||
return self.analysis_data.get("years_of_experience", 0.0)
|
||||
return self.analysis_data_en.get("years_of_experience", 0.0)
|
||||
|
||||
@property
|
||||
def soft_skills_score(self) -> int:
|
||||
"""15. A score (0-100) for inferred non-technical skills."""
|
||||
return self.analysis_data.get("soft_skills_score", 0)
|
||||
return self.analysis_data_en.get("soft_skills_score", 0)
|
||||
|
||||
@property
|
||||
def industry_match_score(self) -> int:
|
||||
"""16. A score (0-100) for the relevance of the candidate's industry experience."""
|
||||
return self.analysis_data.get("experience_industry_match", 0)
|
||||
return self.analysis_data_en.get("experience_industry_match", 0)
|
||||
|
||||
@property
|
||||
def min_requirements_met(self) -> bool:
|
||||
"""14. Boolean (true/false) indicating if all non-negotiable minimum requirements are met."""
|
||||
return self.analysis_data.get("min_req_met_bool", False)
|
||||
return self.analysis_data_en.get("min_req_met_bool", False)
|
||||
|
||||
@property
|
||||
def screening_stage_rating(self) -> str:
|
||||
"""13. A standardized rating (e.g., "A - Highly Qualified", "B - Qualified")."""
|
||||
return self.analysis_data.get("screening_stage_rating", "N/A")
|
||||
return self.analysis_data_en.get("screening_stage_rating", "N/A")
|
||||
|
||||
@property
|
||||
def top_3_keywords(self) -> List[str]:
|
||||
"""10. A list of the three most dominant and relevant technical skills or technologies."""
|
||||
return self.analysis_data.get("top_3_keywords", [])
|
||||
return self.analysis_data_en.get("top_3_keywords", [])
|
||||
|
||||
@property
|
||||
def most_recent_job_title(self) -> str:
|
||||
"""8. The candidate's most recent or current professional job title."""
|
||||
return self.analysis_data.get("most_recent_job_title", "N/A")
|
||||
return self.analysis_data_en.get("most_recent_job_title", "N/A")
|
||||
|
||||
@property
|
||||
def criteria_checklist(self) -> Dict[str, str]:
|
||||
"""5 & 6. An object rating the candidate's match for each specific criterion."""
|
||||
return self.analysis_data.get("criteria_checklist", {})
|
||||
return self.analysis_data_en.get("criteria_checklist", {})
|
||||
|
||||
@property
|
||||
def professional_category(self) -> str:
|
||||
"""7. The most fitting professional field or category for the individual."""
|
||||
return self.analysis_data.get("category", "N/A")
|
||||
return self.analysis_data_en.get("category", "N/A")
|
||||
|
||||
@property
|
||||
def language_fluency(self) -> List[Dict[str, str]]:
|
||||
"""12. A list of languages and their fluency levels mentioned."""
|
||||
return self.analysis_data.get("language_fluency", [])
|
||||
return self.analysis_data_en.get("language_fluency", [])
|
||||
|
||||
@property
|
||||
def strengths(self) -> str:
|
||||
"""2. A brief summary of why the candidate is a strong fit."""
|
||||
return self.analysis_data.get("strengths", "")
|
||||
return self.analysis_data_en.get("strengths", "")
|
||||
|
||||
@property
|
||||
def weaknesses(self) -> str:
|
||||
"""3. A brief summary of where the candidate falls short or what criteria are missing."""
|
||||
return self.analysis_data.get("weaknesses", "")
|
||||
return self.analysis_data_en.get("weaknesses", "")
|
||||
|
||||
@property
|
||||
def job_fit_narrative(self) -> str:
|
||||
"""11. A single, concise sentence summarizing the core fit."""
|
||||
return self.analysis_data.get("job_fit_narrative", "")
|
||||
return self.analysis_data_en.get("job_fit_narrative", "")
|
||||
|
||||
@property
|
||||
def recommendation(self) -> str:
|
||||
"""9. Provide a detailed final recommendation for the candidate."""
|
||||
return self.analysis_data.get("recommendation", "")
|
||||
return self.analysis_data_en.get("recommendation", "")
|
||||
|
||||
#for arabic
|
||||
|
||||
@property
|
||||
def min_requirements_met_ar(self) -> bool:
|
||||
"""14. Boolean (true/false) indicating if all non-negotiable minimum requirements are met."""
|
||||
return self.analysis_data_ar.get("min_req_met_bool", False)
|
||||
|
||||
@property
|
||||
def screening_stage_rating_ar(self) -> str:
|
||||
"""13. A standardized rating (e.g., "A - Highly Qualified", "B - Qualified")."""
|
||||
return self.analysis_data_ar.get("screening_stage_rating", "N/A")
|
||||
|
||||
@property
|
||||
def top_3_keywords_ar(self) -> List[str]:
|
||||
"""10. A list of the three most dominant and relevant technical skills or technologies."""
|
||||
return self.analysis_data_ar.get("top_3_keywords", [])
|
||||
|
||||
@property
|
||||
def most_recent_job_title_ar(self) -> str:
|
||||
"""8. The candidate's most recent or current professional job title."""
|
||||
return self.analysis_data_ar.get("most_recent_job_title", "N/A")
|
||||
|
||||
@property
|
||||
def criteria_checklist_ar(self) -> Dict[str, str]:
|
||||
"""5 & 6. An object rating the candidate's match for each specific criterion."""
|
||||
return self.analysis_data_ar.get("criteria_checklist", {})
|
||||
|
||||
@property
|
||||
def professional_category_ar(self) -> str:
|
||||
"""7. The most fitting professional field or category for the individual."""
|
||||
return self.analysis_data_ar.get("category", "N/A")
|
||||
|
||||
@property
|
||||
def language_fluency_ar(self) -> List[Dict[str, str]]:
|
||||
"""12. A list of languages and their fluency levels mentioned."""
|
||||
return self.analysis_data_ar.get("language_fluency", [])
|
||||
|
||||
@property
|
||||
def strengths_ar(self) -> str:
|
||||
"""2. A brief summary of why the candidate is a strong fit."""
|
||||
return self.analysis_data_ar.get("strengths", "")
|
||||
|
||||
@property
|
||||
def weaknesses_ar(self) -> str:
|
||||
"""3. A brief summary of where the candidate falls short or what criteria are missing."""
|
||||
return self.analysis_data_ar.get("weaknesses", "")
|
||||
|
||||
@property
|
||||
def job_fit_narrative_ar(self) -> str:
|
||||
"""11. A single, concise sentence summarizing the core fit."""
|
||||
return self.analysis_data_ar.get("job_fit_narrative", "")
|
||||
|
||||
@property
|
||||
def recommendation_ar(self) -> str:
|
||||
"""9. Provide a detailed final recommendation for the candidate."""
|
||||
return self.analysis_data_ar.get("recommendation", "")
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# 🔄 HELPER METHODS
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
from django.db.models import Value, IntegerField, CharField, F
|
||||
from django.db.models.functions import Coalesce, Cast, Replace, NullIf, KeyTextTransform
|
||||
|
||||
# Define the path to the match score
|
||||
# Based on your tracebacks, the path is: ai_analysis_data -> analysis_data -> match_score
|
||||
SCORE_PATH_RAW = F('ai_analysis_data__analysis_data__match_score')
|
||||
|
||||
# Define a robust annotation expression for safely extracting and casting the match score.
|
||||
# This sequence handles three common failure points:
|
||||
# 1. Missing keys (handled by Coalesce).
|
||||
# 2. Textual scores (e.g., "N/A" or "") (handled by NullIf).
|
||||
# 3. Quoted numeric scores (e.g., "50") from JSONB extraction (handled by Replace).
|
||||
def get_safe_score_annotation():
|
||||
"""
|
||||
Returns a Django Expression object that safely extracts a score from the
|
||||
JSONField, cleans it, and casts it to an IntegerField.
|
||||
"""
|
||||
|
||||
# 1. Extract the JSON value as text and force a CharField for cleaning functions
|
||||
# Using the double-underscore path is equivalent to the KeyTextTransform
|
||||
# for the final nested key in a PostgreSQL JSONField.
|
||||
extracted_text = Cast(SCORE_PATH_RAW, output_field=CharField())
|
||||
|
||||
# 2. Clean up any residual double-quotes that sometimes remain if the data
|
||||
# was stored as a quoted string (e.g., "50")
|
||||
cleaned_text = Replace(extracted_text, Value('"'), Value(''))
|
||||
|
||||
# 3. Use NullIf to convert the cleaned text to NULL if it is an empty string
|
||||
# (or if it was a non-numeric string like "N/A" after quote removal)
|
||||
null_if_empty = NullIf(cleaned_text, Value(''))
|
||||
|
||||
# 4. Cast the result (which is now either a clean numeric string or NULL) to an IntegerField.
|
||||
final_cast = Cast(null_if_empty, output_field=IntegerField())
|
||||
|
||||
# 5. Use Coalesce to ensure NULL scores (from errors or missing data) default to 0.
|
||||
return Coalesce(final_cast, Value(0))
|
||||
@ -86,7 +86,7 @@ def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
if instance.resume and not instance.is_resume_parsed:
|
||||
logger.info(f"Scoring resume for candidate {instance.pk}")
|
||||
async_task(
|
||||
"recruitment.tasks.handle_reume_parsing_and_scoring",
|
||||
"recruitment.tasks.handle_resume_parsing_and_scoring",
|
||||
instance.pk,
|
||||
hook="recruitment.hooks.callback_ai_parsing",
|
||||
)
|
||||
|
||||
@ -238,14 +238,219 @@ def safe_cast_to_float(value, default=0.0):
|
||||
return default
|
||||
return default
|
||||
|
||||
def handle_reume_parsing_and_scoring(pk):
|
||||
# def handle_resume_parsing_and_scoring(pk):
|
||||
# """
|
||||
# Optimized Django-Q task to parse a resume, score the candidate against a job,
|
||||
# and atomically save the results.
|
||||
# """
|
||||
|
||||
# # --- 1. Robust Object Retrieval (Prevents looping on DoesNotExist) ---
|
||||
# try:
|
||||
# instance = Application.objects.get(pk=pk)
|
||||
# except Application.DoesNotExist:
|
||||
# # Exit gracefully if the candidate was deleted after the task was queued
|
||||
# logger.warning(f"Candidate matching query does not exist for pk={pk}. Exiting task.")
|
||||
# print(f"Candidate matching query does not exist for pk={pk}. Exiting task.")
|
||||
# return
|
||||
|
||||
# logger.info(f"Scoring resume for candidate {pk}")
|
||||
# print(f"Scoring resume for candidate {pk}")
|
||||
|
||||
# # --- 2. I/O and Initial Data Check ---
|
||||
# try:
|
||||
# file_path = instance.resume.path
|
||||
# if not os.path.exists(file_path):
|
||||
# logger.warning(f"Resume file not found: {file_path}")
|
||||
# print(f"Resume file not found: {file_path}")
|
||||
# # Consider marking the task as unsuccessful but don't re-queue
|
||||
# return
|
||||
|
||||
# # Use the new unified document parser
|
||||
# resume_text = extract_text_from_document(file_path)
|
||||
# job_detail = f"{instance.job.description} {instance.job.qualifications}"
|
||||
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error during initial data retrieval/parsing for candidate {instance.pk}: {e}")
|
||||
# print(f"Error during initial data retrieval/parsing for candidate {instance.pk}: {e}")
|
||||
# return
|
||||
# print(resume_text)
|
||||
# # --- 3. Single, Combined LLM Prompt (Major Cost & Latency Optimization) ---
|
||||
# prompt = f"""
|
||||
# You are an expert AI system functioning as both a Resume Parser and a Technical Recruiter.
|
||||
|
||||
# Your task is to:
|
||||
# 1. **PARSE**: Extract all key-value information from the provided RESUME TEXT into a clean JSON structure under the key 'resume_data', preserving the original text and it's formatting and dont add any extra text.
|
||||
# 2. **SCORE**: Analyze the parsed data against the JOB CRITERIA and generate a comprehensive score and analysis under the key 'analysis_data'.
|
||||
|
||||
# **JOB CRITERIA:**
|
||||
# {job_detail}
|
||||
|
||||
# **RESUME TEXT:**
|
||||
# {resume_text}
|
||||
|
||||
# **STRICT JSON OUTPUT INSTRUCTIONS:**
|
||||
# Output a single, valid JSON object with ONLY the following two top-level keys:
|
||||
|
||||
|
||||
# 1. "resume_data": {{
|
||||
# "full_name": "Full name of the candidate",
|
||||
# "current_title": "Most recent or current job title",
|
||||
# "location": "City and state",
|
||||
# "contact": "Phone number and email",
|
||||
# "linkedin": "LinkedIn profile URL",
|
||||
# "github": "GitHub or portfolio URL",
|
||||
# "summary": "Brief professional profile or summary (1–2 sentences)",
|
||||
# "education": [{{
|
||||
# "institution": "Institution name",
|
||||
# "degree": "Degree name",
|
||||
# "year": "Year of graduation" (if provided) or '',
|
||||
# "gpa": "GPA (if provided)",
|
||||
# "relevant_courses": ["list", "of", "courses"](if provided) or []
|
||||
# }}],
|
||||
# "skills": {{
|
||||
# "category_1": ["skill_a", "skill_b"],
|
||||
# "uncategorized": ["tool_x"]
|
||||
# }},
|
||||
# "experience": [{{
|
||||
# "company": "Company name",
|
||||
# "job_title": "Job Title",
|
||||
# "location": "Location",
|
||||
# "start_date": "YYYY-MM",
|
||||
# "end_date": "YYYY-MM or Present",
|
||||
# "key_achievements": ["concise bullet points"] (if provided) or []
|
||||
# }}],
|
||||
# "projects": [{{
|
||||
# "name": "Project name",
|
||||
# "year": "Year",
|
||||
# "technologies_used": ["list", "of", "tech"] (if provided) or [],
|
||||
# "brief_description": "description"
|
||||
# }}]
|
||||
# }}
|
||||
|
||||
# 2. "analysis_data": {{
|
||||
# "match_score": "Integer Score 0-100",
|
||||
# "strengths": "Brief summary of strengths",
|
||||
# "weaknesses": "Brief summary of weaknesses",
|
||||
# "years_of_experience": "Total years of experience (float, e.g., 6.5)",
|
||||
# "criteria_checklist": List of job requirements if any {{ "Python": "Met", "AWS": "Not Met"}} only output the criteria_checklist in one of ('Met','Not Met') don't output any extra text,
|
||||
# "category": "Most fitting professional field (e.g., Data Science), only output the category name and no other text example ('Software Development', 'correct') , ('Software Development and devops','wrong') ('Software Development / Backend Development','wrong')",
|
||||
# "most_recent_job_title": "Candidate's most recent job title",
|
||||
# "recommendation": "Detailed hiring recommendation narrative",
|
||||
# "top_3_keywords": ["keyword1", "keyword2", "keyword3"],
|
||||
# "job_fit_narrative": "Single, concise summary sentence",
|
||||
# "language_fluency": ["language: fluency_level"],
|
||||
# "screening_stage_rating": "Standardized rating (Highly Qualified, Qualified , Partially Qualified, Not Qualified)",
|
||||
# "min_req_met_bool": "Boolean (true/false)",
|
||||
# "soft_skills_score": "Integer Score 0-100 for inferred non-technical skills",
|
||||
# "experience_industry_match": "Integer Score 0-100 for industry relevance",
|
||||
# "seniority_level_match": "Integer Score 0-100 for alignment with JD's seniority level",
|
||||
# "red_flags": ["List of any potential concerns (if any): e.g., 'Employment gap 1 year', 'Frequent job hopping', 'Missing required certification'"],
|
||||
# "employment_stability_score": "Integer Score 0-100 (Higher is more stable/longer tenure) (if possible)",
|
||||
# "transferable_skills_narrative": "A brief sentence describing the relevance of non-core experience (if applicable).",
|
||||
# "cultural_fit_keywords": ["A list of 3-5 keywords extracted from the resume (if possible) (e.g., 'team-player', 'mentored', 'cross-functional')"]
|
||||
# }}
|
||||
|
||||
# If a top-level key or its required fields are missing, set the field to null, an empty list, or an empty object as appropriate.
|
||||
|
||||
# Output only valid JSON—no markdown, no extra text.
|
||||
# """
|
||||
|
||||
# try:
|
||||
# result = ai_handler(prompt)
|
||||
# if result['status'] == 'error':
|
||||
# logger.error(f"AI handler returned error for candidate {instance.pk}")
|
||||
# print(f"AI handler returned error for candidate {instance.pk}")
|
||||
# return
|
||||
# # Ensure the result is parsed as a Python dict (if ai_handler returns a JSON string)
|
||||
# data = result['data']
|
||||
|
||||
# if isinstance(data, str):
|
||||
# data = json.loads(data)
|
||||
# print(data)
|
||||
|
||||
# # parsed_summary = data.get('parsed_data', {})
|
||||
# # scoring_result = data.get('scoring_data', {})
|
||||
|
||||
# except Exception as e:
|
||||
# logger.error(f"AI handler failed for candidate {instance.pk}: {e}")
|
||||
# print(f"AI handler failed for candidate {instance.pk}: {e}")
|
||||
# return
|
||||
|
||||
# # --- 4. Atomic Database Update (Ensures data integrity) ---
|
||||
# with transaction.atomic():
|
||||
|
||||
# # Map JSON keys to model fields with appropriate defaults
|
||||
# # update_map = {
|
||||
# # 'match_score': ('match_score', 0),
|
||||
# # 'years_of_experience': ('years_of_experience', 0.0),
|
||||
# # 'soft_skills_score': ('soft_skills_score', 0),
|
||||
# # 'experience_industry_match': ('experience_industry_match', 0),
|
||||
|
||||
# # 'min_req_met_bool': ('min_req_met_bool', False),
|
||||
# # 'screening_stage_rating': ('screening_stage_rating', 'N/A'),
|
||||
# # 'most_recent_job_title': ('most_recent_job_title', 'N/A'),
|
||||
# # 'top_3_keywords': ('top_3_keywords', []),
|
||||
|
||||
# # 'strengths': ('strengths', ''),
|
||||
# # 'weaknesses': ('weaknesses', ''),
|
||||
# # 'job_fit_narrative': ('job_fit_narrative', ''),
|
||||
# # 'recommendation': ('recommendation', ''),
|
||||
|
||||
# # 'criteria_checklist': ('criteria_checklist', {}),
|
||||
# # 'language_fluency': ('language_fluency', []),
|
||||
# # 'category': ('category', 'N/A'),
|
||||
# # }
|
||||
|
||||
# # Apply scoring results to the instance
|
||||
# # for model_field, (json_key, default_value) in update_map.items():
|
||||
# # instance.ai_analysis_data[model_field] = scoring_result.get(json_key, default_value)
|
||||
# # instance.set_field(model_field, scoring_result.get(json_key, default_value))
|
||||
# # instance.set_field("match_score" , int(safe_cast_to_float(scoring_result.get('match_score', 0), default=0)))
|
||||
# # instance.set_field("years_of_experience" , safe_cast_to_float(scoring_result.get('years_of_experience', 0.0)))
|
||||
# # instance.set_field("soft_skills_score" , int(safe_cast_to_float(scoring_result.get('soft_skills_score', 0), default=0)))
|
||||
# # instance.set_field("experience_industry_match" , int(safe_cast_to_float(scoring_result.get('experience_industry_match', 0), default=0)))
|
||||
|
||||
# # # Other Model Fields
|
||||
# # instance.set_field("min_req_met_bool" , scoring_result.get('min_req_met_bool', False))
|
||||
# # instance.set_field("screening_stage_rating" , scoring_result.get('screening_stage_rating', 'N/A'))
|
||||
# # instance.set_field("category" , scoring_result.get('category', 'N/A'))
|
||||
# # instance.set_field("most_recent_job_title" , scoring_result.get('most_recent_job_title', 'N/A'))
|
||||
# # instance.set_field("top_3_keywords" , scoring_result.get('top_3_keywords', []))
|
||||
# # instance.set_field("strengths" , scoring_result.get('strengths', ''))
|
||||
# # instance.set_field("weaknesses" , scoring_result.get('weaknesses', ''))
|
||||
# # instance.set_field("job_fit_narrative" , scoring_result.get('job_fit_narrative', ''))
|
||||
# # instance.set_field("recommendation" , scoring_result.get('recommendation', ''))
|
||||
# # instance.set_field("criteria_checklist" , scoring_result.get('criteria_checklist', {}))
|
||||
# # instance.set_field("language_fluency" , scoring_result.get('language_fluency', []))
|
||||
|
||||
|
||||
# # 2. Update the Full JSON Field (ai_analysis_data)
|
||||
# if instance.ai_analysis_data is None:
|
||||
# instance.ai_analysis_data = {}
|
||||
|
||||
# # Save both structured outputs into the single JSONField for completeness
|
||||
# instance.ai_analysis_data = data
|
||||
# # instance.ai_analysis_data['parsed_data'] = parsed_summary
|
||||
# # instance.ai_analysis_data['scoring_data'] = scoring_result
|
||||
|
||||
# # Apply parsing results
|
||||
# # instance.parsed_summary = json.dumps(parsed_summary)
|
||||
# instance.is_resume_parsed = True
|
||||
|
||||
# instance.save(update_fields=['ai_analysis_data', 'is_resume_parsed'])
|
||||
|
||||
# logger.info(f"Successfully scored and saved analysis for candidate {instance.id}")
|
||||
# print(f"Successfully scored and saved analysis for candidate {instance.id}")
|
||||
|
||||
def handle_resume_parsing_and_scoring(pk: int):
|
||||
"""
|
||||
Optimized Django-Q task to parse a resume, score the candidate against a job,
|
||||
Optimized Django-Q task to parse a resume in English and Arabic, score the candidate,
|
||||
and atomically save the results.
|
||||
"""
|
||||
|
||||
# --- 1. Robust Object Retrieval (Prevents looping on DoesNotExist) ---
|
||||
try:
|
||||
# NOTE: Replace 'Application.objects.get' with your actual model manager call
|
||||
instance = Application.objects.get(pk=pk)
|
||||
except Application.DoesNotExist:
|
||||
# Exit gracefully if the candidate was deleted after the task was queued
|
||||
@ -258,11 +463,11 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
|
||||
# --- 2. I/O and Initial Data Check ---
|
||||
try:
|
||||
# Assuming instance.resume is a Django FileField
|
||||
file_path = instance.resume.path
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"Resume file not found: {file_path}")
|
||||
print(f"Resume file not found: {file_path}")
|
||||
# Consider marking the task as unsuccessful but don't re-queue
|
||||
return
|
||||
|
||||
# Use the new unified document parser
|
||||
@ -276,11 +481,13 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
print(resume_text)
|
||||
# --- 3. Single, Combined LLM Prompt (Major Cost & Latency Optimization) ---
|
||||
prompt = f"""
|
||||
You are an expert AI system functioning as both a Resume Parser and a Technical Recruiter.
|
||||
You are an expert AI system functioning as both a Resume Parser and a Technical Recruiter, capable of multi-language output.
|
||||
|
||||
Your task is to:
|
||||
1. **PARSE**: Extract all key-value information from the provided RESUME TEXT into a clean JSON structure under the key 'resume_data', preserving the original text and it's formatting and dont add any extra text.
|
||||
2. **SCORE**: Analyze the parsed data against the JOB CRITERIA and generate a comprehensive score and analysis under the key 'analysis_data'.
|
||||
1. **PARSE (English)**: Extract all key-value information from the RESUME TEXT into a clean JSON structure under the key **'resume_data_en'**.
|
||||
2. **PARSE (Arabic)**: Translate and output the exact same parsed data structure into Arabic under the key **'resume_data_ar'**. The keys must remain in English, but the values (names, titles, summaries, descriptions) must be in Arabic.
|
||||
3. **SCORE (English)**: Analyze the data against the JOB CRITERIA and generate a comprehensive score and analysis under **'analysis_data_en'**, including an English narrative/recommendation.
|
||||
4. **SCORE (Arabic)**: Output an identical analysis structure under **'analysis_data_ar'**, but ensure the narrative fields (**recommendation**, **job_fit_narrative**, **strengths**, **weaknesses**, **transferable_skills_narrative**) are translated into Arabic. All numerical and list fields (scores, checklist, keywords) must be identical to the English analysis.
|
||||
|
||||
**JOB CRITERIA:**
|
||||
{job_detail}
|
||||
@ -289,10 +496,16 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
{resume_text}
|
||||
|
||||
**STRICT JSON OUTPUT INSTRUCTIONS:**
|
||||
Output a single, valid JSON object with ONLY the following two top-level keys:
|
||||
You MUST output a single, valid JSON object.
|
||||
This object MUST contain ONLY the following four top-level keys:
|
||||
1. "resume_data_en"
|
||||
2. "resume_data_ar"
|
||||
3. "analysis_data_en"
|
||||
4. "analysis_data_ar"
|
||||
|
||||
**ABSOLUTELY DO NOT use generic keys like "resume_data" or "analysis_data" at the top level.**
|
||||
|
||||
1. "resume_data": {{
|
||||
1. "resume_data_en": {{ /* English Parsed Data */
|
||||
"full_name": "Full name of the candidate",
|
||||
"current_title": "Most recent or current job title",
|
||||
"location": "City and state",
|
||||
@ -327,7 +540,43 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
}}]
|
||||
}}
|
||||
|
||||
2. "analysis_data": {{
|
||||
2. "resume_data_ar": {{ /* Arabic Translated Parsed Data (Keys in English, Values in Arabic) */
|
||||
"full_name": "الاسم الكامل للمرشح",
|
||||
"current_title": "أحدث أو الحالي مسمى وظيفي",
|
||||
"location": "المدينة والدولة",
|
||||
"contact": "رقم الهاتف والبريد الإلكتروني",
|
||||
"linkedin": "رابط ملف LinkedIn الشخصي",
|
||||
"github": "رابط GitHub أو ملف الأعمال",
|
||||
"summary": "ملف تعريفي مهني موجز أو ملخص (جملة واحدة أو جملتين)",
|
||||
"education": [{{
|
||||
"institution": "اسم المؤسسة",
|
||||
"degree": "اسم الدرجة العلمية",
|
||||
"year": "سنة التخرج (إذا توفرت) أو ''",
|
||||
"gpa": "المعدل التراكمي (إذا توفر)",
|
||||
"relevant_courses": ["قائمة", "بالدورات", "ذات", "الصلة"](إذا توفرت) أو []
|
||||
}}],
|
||||
"skills": {{
|
||||
"category_1": ["مهارة_أ", "مهارة_ب"],
|
||||
"uncategorized": ["أداة_س"]
|
||||
}},
|
||||
"experience": [{{
|
||||
"company": "اسم الشركة",
|
||||
"job_title": "المسمى الوظيفي",
|
||||
"location": "الموقع",
|
||||
"start_date": "السنة-الشهر (YYYY-MM)",
|
||||
"end_date": "السنة-الشهر (YYYY-MM) أو Present",
|
||||
"key_achievements": ["نقاط", "رئيسية", "موجزة", "للإنجازات"] (إذا توفرت) أو []
|
||||
}}],
|
||||
"projects": [{{
|
||||
"name": "اسم المشروع",
|
||||
"year": "السنة",
|
||||
"technologies_used": ["قائمة", "بالتقنيات", "المستخدمة"] (إذا توفرت) أو [],
|
||||
"brief_description": "وصف موجز"
|
||||
}}]
|
||||
}}
|
||||
|
||||
|
||||
3. "analysis_data_en": {{ /* English Analysis and Narratives */
|
||||
"match_score": "Integer Score 0-100",
|
||||
"strengths": "Brief summary of strengths",
|
||||
"weaknesses": "Brief summary of weaknesses",
|
||||
@ -350,26 +599,48 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
"cultural_fit_keywords": ["A list of 3-5 keywords extracted from the resume (if possible) (e.g., 'team-player', 'mentored', 'cross-functional')"]
|
||||
}}
|
||||
|
||||
4. "analysis_data_ar": {{ /* Identical Analysis structure, but with Arabic Translated Narratives */
|
||||
"match_score": "Integer Score 0-100",
|
||||
"strengths": "ملخص موجز لنقاط القوة",
|
||||
"weaknesses": "ملخص موجز لنقاط الضعف",
|
||||
"years_of_experience": "Total years of experience (float, e.g., 6.5)",
|
||||
"criteria_checklist": List of job requirements if any {{ "Python": "Met", "AWS": "Not Met"}} only output the criteria_checklist in one of ('Met','Not Met') don't output any extra text,
|
||||
"category": "Most fitting professional field (e.g., Data Science), only output the category name and no other text example ('Software Development', 'correct') , ('Software Development and devops','wrong') ('Software Development / Backend Development','wrong')",
|
||||
"most_recent_job_title": "Candidate's most recent job title",
|
||||
"recommendation": "سرد تفصيلي بتوصية التوظيف",
|
||||
"top_3_keywords": ["keyword1", "keyword2", "keyword3"],
|
||||
"job_fit_narrative": "جملة واحدة موجزة تلخص مدى ملاءمة الوظيفة",
|
||||
"language_fluency": ["language: fluency_level"],
|
||||
"screening_stage_rating": "Standardized rating (Highly Qualified, Qualified , Partially Qualified, Not Qualified)",
|
||||
"min_req_met_bool": "Boolean (true/false)",
|
||||
"soft_skills_score": "Integer Score 0-100 for inferred non-technical skills",
|
||||
"experience_industry_match": "Integer Score 0-100 for industry relevance",
|
||||
"seniority_level_match": "Integer Score 0-100 for alignment with JD's seniority level",
|
||||
"red_flags": ["List of any potential concerns (if any): e.g., 'Employment gap 1 year', 'Frequent job hopping', 'Missing required certification'"],
|
||||
"employment_stability_score": "Integer Score 0-100 (Higher is more stable/longer tenure) (if possible)",
|
||||
"transferable_skills_narrative": "جملة موجزة تصف أهمية الخبرة غير الأساسية (إذا انطبقت).",
|
||||
"cultural_fit_keywords": ["A list of 3-5 keywords extracted from the resume (if possible) (e.g., 'team-player', 'mentored', 'cross-functional')"]
|
||||
}}
|
||||
|
||||
If a top-level key or its required fields are missing, set the field to null, an empty list, or an empty object as appropriate.
|
||||
|
||||
Output only valid JSON—no markdown, no extra text.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Call the AI handler
|
||||
result = ai_handler(prompt)
|
||||
if result['status'] == 'error':
|
||||
logger.error(f"AI handler returned error for candidate {instance.pk}")
|
||||
print(f"AI handler returned error for candidate {instance.pk}")
|
||||
return
|
||||
# Ensure the result is parsed as a Python dict (if ai_handler returns a JSON string)
|
||||
|
||||
# Ensure the result is parsed as a Python dict
|
||||
data = result['data']
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
print(data)
|
||||
|
||||
# parsed_summary = data.get('parsed_data', {})
|
||||
# scoring_result = data.get('scoring_data', {})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AI handler failed for candidate {instance.pk}: {e}")
|
||||
print(f"AI handler failed for candidate {instance.pk}: {e}")
|
||||
@ -377,69 +648,22 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
|
||||
# --- 4. Atomic Database Update (Ensures data integrity) ---
|
||||
with transaction.atomic():
|
||||
|
||||
# Map JSON keys to model fields with appropriate defaults
|
||||
# update_map = {
|
||||
# 'match_score': ('match_score', 0),
|
||||
# 'years_of_experience': ('years_of_experience', 0.0),
|
||||
# 'soft_skills_score': ('soft_skills_score', 0),
|
||||
# 'experience_industry_match': ('experience_industry_match', 0),
|
||||
|
||||
# 'min_req_met_bool': ('min_req_met_bool', False),
|
||||
# 'screening_stage_rating': ('screening_stage_rating', 'N/A'),
|
||||
# 'most_recent_job_title': ('most_recent_job_title', 'N/A'),
|
||||
# 'top_3_keywords': ('top_3_keywords', []),
|
||||
|
||||
# 'strengths': ('strengths', ''),
|
||||
# 'weaknesses': ('weaknesses', ''),
|
||||
# 'job_fit_narrative': ('job_fit_narrative', ''),
|
||||
# 'recommendation': ('recommendation', ''),
|
||||
|
||||
# 'criteria_checklist': ('criteria_checklist', {}),
|
||||
# 'language_fluency': ('language_fluency', []),
|
||||
# 'category': ('category', 'N/A'),
|
||||
# }
|
||||
|
||||
# Apply scoring results to the instance
|
||||
# for model_field, (json_key, default_value) in update_map.items():
|
||||
# instance.ai_analysis_data[model_field] = scoring_result.get(json_key, default_value)
|
||||
# instance.set_field(model_field, scoring_result.get(json_key, default_value))
|
||||
# instance.set_field("match_score" , int(safe_cast_to_float(scoring_result.get('match_score', 0), default=0)))
|
||||
# instance.set_field("years_of_experience" , safe_cast_to_float(scoring_result.get('years_of_experience', 0.0)))
|
||||
# instance.set_field("soft_skills_score" , int(safe_cast_to_float(scoring_result.get('soft_skills_score', 0), default=0)))
|
||||
# instance.set_field("experience_industry_match" , int(safe_cast_to_float(scoring_result.get('experience_industry_match', 0), default=0)))
|
||||
|
||||
# # Other Model Fields
|
||||
# instance.set_field("min_req_met_bool" , scoring_result.get('min_req_met_bool', False))
|
||||
# instance.set_field("screening_stage_rating" , scoring_result.get('screening_stage_rating', 'N/A'))
|
||||
# instance.set_field("category" , scoring_result.get('category', 'N/A'))
|
||||
# instance.set_field("most_recent_job_title" , scoring_result.get('most_recent_job_title', 'N/A'))
|
||||
# instance.set_field("top_3_keywords" , scoring_result.get('top_3_keywords', []))
|
||||
# instance.set_field("strengths" , scoring_result.get('strengths', ''))
|
||||
# instance.set_field("weaknesses" , scoring_result.get('weaknesses', ''))
|
||||
# instance.set_field("job_fit_narrative" , scoring_result.get('job_fit_narrative', ''))
|
||||
# instance.set_field("recommendation" , scoring_result.get('recommendation', ''))
|
||||
# instance.set_field("criteria_checklist" , scoring_result.get('criteria_checklist', {}))
|
||||
# instance.set_field("language_fluency" , scoring_result.get('language_fluency', []))
|
||||
|
||||
|
||||
# 2. Update the Full JSON Field (ai_analysis_data)
|
||||
if instance.ai_analysis_data is None:
|
||||
instance.ai_analysis_data = {}
|
||||
|
||||
# Save both structured outputs into the single JSONField for completeness
|
||||
# Save all four structured outputs into the single JSONField
|
||||
instance.ai_analysis_data = data
|
||||
# instance.ai_analysis_data['parsed_data'] = parsed_summary
|
||||
# instance.ai_analysis_data['scoring_data'] = scoring_result
|
||||
|
||||
# Apply parsing results
|
||||
# instance.parsed_summary = json.dumps(parsed_summary)
|
||||
instance.is_resume_parsed = True
|
||||
|
||||
# Save changes to the database
|
||||
# NOTE: If you extract individual fields (like match_score) to separate columns,
|
||||
# ensure those are handled here, using data.get('analysis_data_en', {}).get('match_score').
|
||||
instance.save(update_fields=['ai_analysis_data', 'is_resume_parsed'])
|
||||
|
||||
logger.info(f"Successfully scored and saved analysis for candidate {instance.id}")
|
||||
print(f"Successfully scored and saved analysis for candidate {instance.id}")
|
||||
logger.info(f"Successfully scored and saved analysis (EN/AR) for candidate {instance.id}")
|
||||
print(f"Successfully scored and saved analysis (EN/AR) for candidate {instance.id}")
|
||||
|
||||
|
||||
|
||||
from django.utils import timezone
|
||||
@ -642,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
|
||||
@ -799,10 +1023,6 @@ def _task_send_individual_email(subject, body_message, recipient, attachments,se
|
||||
logger.error(f"Email sent to {recipient}, but failed to store in DB: {str(e)}")
|
||||
|
||||
|
||||
else:
|
||||
logger.error("failed to send email")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
|
||||
|
||||
|
||||
@ -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
@ -4,7 +4,7 @@ from datetime import datetime
|
||||
from django.shortcuts import render, get_object_or_404,redirect
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
from django.db.models.fields.json import KeyTextTransform
|
||||
from django.db.models.fields.json import KeyTextTransform,KeyTransform
|
||||
from recruitment.utils import json_to_markdown_table
|
||||
from django.db.models import Count, Avg, F, FloatField
|
||||
from django.db.models.functions import Cast
|
||||
@ -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', '')
|
||||
@ -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:
|
||||
@ -215,17 +215,17 @@ class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessa
|
||||
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'
|
||||
|
||||
|
||||
@ -235,12 +235,12 @@ def retry_scoring_view(request,slug):
|
||||
application = get_object_or_404(models.Application, slug=slug)
|
||||
|
||||
async_task(
|
||||
'recruitment.tasks.handle_reume_parsing_and_scoring',
|
||||
'recruitment.tasks.handle_resume_parsing_and_scoring',
|
||||
application.pk,
|
||||
hook='recruitment.hooks.callback_ai_parsing',
|
||||
sync=True,
|
||||
)
|
||||
return redirect('candidate_detail', slug=application.slug)
|
||||
return redirect('application_detail', slug=application.slug)
|
||||
|
||||
|
||||
|
||||
@ -253,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 = {}
|
||||
|
||||
@ -266,12 +266,12 @@ def candidate_detail(request, slug):
|
||||
if request.user.is_staff:
|
||||
stage_form = forms.ApplicationStageForm()
|
||||
|
||||
|
||||
|
||||
|
||||
# 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,
|
||||
})
|
||||
@ -279,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)
|
||||
@ -301,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
|
||||
@ -386,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()
|
||||
@ -400,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')
|
||||
@ -412,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:
|
||||
@ -434,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')
|
||||
@ -446,17 +446,27 @@ def dashboard_view(request):
|
||||
|
||||
# --- 5. SCOPED CORE AGGREGATIONS (FILTERED OR ALL) ---
|
||||
|
||||
total_candidates = candidate_queryset.count()
|
||||
total_applications = application_queryset.count()
|
||||
|
||||
candidates_with_score_query = candidate_queryset.filter(
|
||||
is_resume_parsed=True
|
||||
).annotate(
|
||||
annotated_match_score=Coalesce(
|
||||
Cast(SCORE_PATH, output_field=IntegerField()),
|
||||
0
|
||||
)
|
||||
|
||||
score_expression = Cast(
|
||||
Coalesce(
|
||||
KeyTextTransform(
|
||||
'match_score',
|
||||
KeyTransform('analysis_data_en', 'ai_analysis_data')
|
||||
),
|
||||
Value('0'),
|
||||
),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
|
||||
# 2. ANNOTATE the queryset with the new field
|
||||
applications_with_score_query = application_queryset.annotate(
|
||||
annotated_match_score=score_expression
|
||||
)
|
||||
|
||||
|
||||
|
||||
# safe_match_score_cast = Cast(
|
||||
# # 3. If the result after stripping quotes is an empty string (''), convert it to NULL.
|
||||
# NullIf(
|
||||
@ -483,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()
|
||||
@ -517,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
|
||||
@ -532,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)
|
||||
]
|
||||
@ -560,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),
|
||||
|
||||
]
|
||||
@ -579,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,
|
||||
@ -594,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
|
||||
@ -618,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),
|
||||
}
|
||||
|
||||
@ -627,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
|
||||
@ -755,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
|
||||
@ -766,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')
|
||||
@ -799,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 ''])
|
||||
@ -895,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
|
||||
|
||||
|
||||
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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -197,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 %}
|
||||
@ -253,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" %}
|
||||
|
||||
@ -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 }}
|
||||
252
templates/includes/application_modal_body.html
Normal file
252
templates/includes/application_modal_body.html
Normal file
@ -0,0 +1,252 @@
|
||||
{% load i18n %}
|
||||
{% 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> {{ 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">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i>
|
||||
<small class="text-muted">{% trans "Job Fit" %}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ application.job_fit_narrative }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Top Keywords" %}</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for keyword in application.top_3_keywords %}
|
||||
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-clock me-2 text-info"></i>
|
||||
<small class="text-muted">{% trans "Experience" %}</small>
|
||||
</div>
|
||||
<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> {{ application.soft_skills_score }}%</p>
|
||||
<p class="mb-0"><strong>{% trans "Industry Match:" %}</strong>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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>{{ 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>{{ 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>{{ application.weaknesses }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-list-check me-1"></i> {% trans "Criteria Assessment" %}</label>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Criteria" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for criterion, status in application.criteria_checklist.items %}
|
||||
<tr>
|
||||
<td>{{ criterion }}</td>
|
||||
<td>
|
||||
{% if status == "Met" %}
|
||||
<span class="badge bg-success"><i class="fas fa-check me-1"></i> {% trans "Met" %}</span>
|
||||
{% elif status == "Not Met" %}
|
||||
<span class="badge bg-danger"><i class="fas fa-times me-1"></i> {% trans "Not Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-check-circle me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Minimum Requirements" %}</small>
|
||||
</div>
|
||||
{% if application.min_requirements_met %}
|
||||
<span class="badge bg-success">{% trans "Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Not Met" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Screening Rating" %}</small>
|
||||
</div>
|
||||
<span class="badge bg-secondary">{{ application.screening_stage_rating }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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 application.language_fluency %}
|
||||
<span class="badge bg-light text-dark">{{ language }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<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">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i>
|
||||
<small class="text-muted">{% trans "Job Fit" %}</small>
|
||||
</div>
|
||||
<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">
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Top Keywords" %}</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for keyword in application.top_3_keywords_ar %}
|
||||
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-clock me-2 text-info"></i>
|
||||
<small class="text-muted">{% trans "Experience" %}</small>
|
||||
</div>
|
||||
<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> {{ application.soft_skills_score }}%</p>
|
||||
<p class="mb-0"><strong>{% trans "Industry Match:" %}</strong>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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>{{ 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>{{ 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>{{ application.weaknesses_ar }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-list-check me-1"></i> {% trans "Criteria Assessment" %}</label>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Criteria" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for criterion, status in application.criteria_checklist_ar.items %}
|
||||
<tr>
|
||||
<td>{{ criterion }}</td>
|
||||
<td>
|
||||
{% if status == "Met" %}
|
||||
<span class="badge bg-success"><i class="fas fa-check me-1"></i> {% trans "Met" %}</span>
|
||||
{% elif status == "Not Met" %}
|
||||
<span class="badge bg-danger"><i class="fas fa-times me-1"></i> {% trans "Not Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-check-circle me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Minimum Requirements" %}</small>
|
||||
</div>
|
||||
{% if application.min_requirements_met_ar %}
|
||||
<span class="badge bg-success">{% trans "Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Not Met" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Screening Rating" %}</small>
|
||||
</div>
|
||||
<span class="badge bg-secondary">{{ application.screening_stage_rating_ar }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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 application.language_fluency_ar %}
|
||||
<span class="badge bg-light text-dark">{{ language }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
@ -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>
|
||||
@ -1,123 +0,0 @@
|
||||
{% load i18n %}
|
||||
<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>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Top Keywords" %}</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for keyword in candidate.top_3_keywords %}
|
||||
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<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>
|
||||
</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-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>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-list-check me-1"></i> {% trans "Criteria Assessment" %}</label>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Criteria" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for criterion, status in candidate.criteria_checklist.items %}
|
||||
<tr>
|
||||
<td>{{ criterion }}</td>
|
||||
<td>
|
||||
{% if status == "Met" %}
|
||||
<span class="badge bg-success"><i class="fas fa-check me-1"></i> {% trans "Met" %}</span>
|
||||
{% elif status == "Not Met" %}
|
||||
<span class="badge bg-danger"><i class="fas fa-times me-1"></i> {% trans "Not Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<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 %}
|
||||
<span class="badge bg-success">{% trans "Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Not Met" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if candidate.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 %}
|
||||
<span class="badge bg-light text-dark">{{ language }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -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,14 +322,14 @@
|
||||
|
||||
{# 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">
|
||||
@ -345,7 +345,7 @@
|
||||
|
||||
{# 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." %}
|
||||
@ -491,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">
|
||||
@ -535,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>
|
||||
@ -616,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:"[]" }}
|
||||
};
|
||||
|
||||
@ -646,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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -661,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
|
||||
@ -672,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
|
||||
@ -715,7 +725,7 @@
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += context.parsed + ' candidate(s)';
|
||||
label += context.parsed + CHART_TOOLTIP_SUFFIX;
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@ -724,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>
|
||||
|
||||
@ -330,12 +330,12 @@
|
||||
</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>
|
||||
@ -360,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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,7 +120,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
{% 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' %}">
|
||||
@ -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;">
|
||||
|
||||
@ -604,19 +604,22 @@
|
||||
from your Django view context to populate this section.
|
||||
{% endcomment %}
|
||||
|
||||
{% if assigned_jobs %}
|
||||
{% for assignment in assigned_jobs %}
|
||||
{% if job_assignments %}
|
||||
{% for assignment in job_assignments%}
|
||||
<div class="job-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="job-title">
|
||||
<a href="{% url 'job_details' assignment.job.slug %}" class="text-decoration-none text-primary-theme">
|
||||
<a href="{% url 'job_detail' assignment.job.slug %}" class="text-decoration-none text-primary-theme">
|
||||
{{ assignment.job.title }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="job-details">
|
||||
<i class="fas fa-map-pin me-1"></i> {{ assignment.job.location }}
|
||||
<span class="ms-3"><i class="fas fa-user-tie me-1"></i> {{ assignment.job.department.name }}</span>
|
||||
<i class="fas fa-map-pin me-1"></i> {{ assignment.job.get_location_display }}
|
||||
{% if assessment.job.department %}
|
||||
<span class="ms-3"><i class="fas fa-user-tie me-1"></i> {{ assignment.job.department|default:"" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
value="{{ search_query }}"
|
||||
placeholder="{% trans 'Search by name, email, phone, or job title...' %}">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% comment %} <div class="col-md-3">
|
||||
<label for="stage" class="form-label fw-semibold">
|
||||
<i class="fas fa-filter me-1"></i>{% trans "Stage" %}
|
||||
</label>
|
||||
@ -111,7 +111,7 @@
|
||||
<button type="submit" class="btn btn-main-action w-100">
|
||||
<i class="fas fa-search me-1"></i> {% trans "Search" %}
|
||||
</button>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -154,7 +154,7 @@
|
||||
<th scope="col">{% trans "Name" %}</th>
|
||||
<th scope="col">{% trans "Email" %}</th>
|
||||
<th scope="col">{% trans "Phone" %}</th>
|
||||
<th scope="col">{% trans "Job" %}</th>
|
||||
<th scope="col">{% trans "Created At" %}</th>
|
||||
{% comment %} <th scope="col">{% trans "Stage" %}</th>
|
||||
<th scope="col">{% trans "Applied Date" %}</th> {% endcomment %}
|
||||
<th scope="col" class="text-center">{% trans "Actions" %}</th>
|
||||
@ -202,7 +202,7 @@
|
||||
</span>
|
||||
{% endwith %}
|
||||
</td> {% endcomment %}
|
||||
<td>{{ person.created_at|date:"Y-m-d" }}</td>
|
||||
<td>{{ person.created_at|date:"d-m-Y" }}</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" data-bs-toggle="modal" data-bs-target="#updateModal"
|
||||
@ -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,27 +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">
|
||||
@ -625,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>
|
||||
@ -647,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 %}
|
||||
@ -680,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 %}
|
||||
@ -698,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>
|
||||
@ -710,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>
|
||||
@ -723,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 %}
|
||||
1637
templates/recruitment/application_resume_template.html
Normal file
1637
templates/recruitment/application_resume_template.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -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,11 +211,11 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<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>
|
||||
</a> {% endcomment %}
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Job" %}
|
||||
</a>
|
||||
@ -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,37 +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>
|
||||
{% comment %} <div class="candidate-name">
|
||||
{{ candidate.name }}
|
||||
</div> {% endcomment %}
|
||||
|
||||
</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 %}
|
||||
@ -327,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 %}>
|
||||
@ -341,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' %}
|
||||
@ -354,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 %}
|
||||
--
|
||||
@ -381,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>
|
||||
@ -396,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>
|
||||
@ -406,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>
|
||||
@ -415,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>
|
||||
@ -427,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">
|
||||
@ -437,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">
|
||||
@ -450,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>
|
||||
@ -465,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>
|
||||
@ -600,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;
|
||||
}
|
||||
});
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
@ -286,7 +286,7 @@
|
||||
<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>
|
||||
@ -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" %}
|
||||
|
||||
@ -1,925 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
{% load i18n %}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ candidate.resume_data.full_name|default:"Candidate" }} - Candidate Profile</title>
|
||||
<!-- Use a modern icon set -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
/* --- Color Variables --- */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-light-bg: #f9fbfd;
|
||||
--kaauh-border: #eaeff3;
|
||||
--score-green: #10b981;
|
||||
--score-yellow: #f59e0b;
|
||||
--score-red: #ef4444;
|
||||
--cert-gold: #d97706;
|
||||
|
||||
--color-white: #ffffff;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
}
|
||||
|
||||
/* --- General Layout and Typography --- */
|
||||
body {
|
||||
background-color: var(--kaauh-light-bg);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1280px; /* max-w-7xl */
|
||||
margin: 0 auto;
|
||||
padding: 1rem; /* p-4 */
|
||||
}
|
||||
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem; /* gap-6 */
|
||||
}
|
||||
|
||||
/* Responsive Layout for large screens (lg:grid-cols-3) */
|
||||
@media (min-width: 1024px) {
|
||||
.content-grid {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Card Styles --- */
|
||||
.card-section {
|
||||
background-color: var(--color-white);
|
||||
border-radius: 0.75rem; /* rounded-xl */
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); /* shadow-md */
|
||||
padding: 1.5rem; /* p-6 */
|
||||
border: 1px solid var(--kaauh-border);
|
||||
margin-bottom: 1.5rem; /* space-y-6 */
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem; /* text-2xl */
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-800);
|
||||
margin-bottom: 1rem; /* mb-4 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-title i {
|
||||
color: var(--kaauh-teal);
|
||||
margin-right: 0.75rem; /* mr-3 */
|
||||
}
|
||||
|
||||
/* --- Header Section --- */
|
||||
.header-box {
|
||||
background: linear-gradient(145deg, var(--kaauh-teal), var(--kaauh-teal-dark));
|
||||
color: var(--color-white);
|
||||
padding: 2rem; /* p-8 */
|
||||
border-radius: 0.75rem; /* rounded-xl */
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); /* shadow-2xl */
|
||||
margin-bottom: 1.5rem; /* mb-6 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.header-box {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.header-info h1 {
|
||||
font-size: 2.25rem; /* text-4xl */
|
||||
font-weight: 800; /* font-extrabold */
|
||||
margin-bottom: 0.25rem; /* mb-1 */
|
||||
}
|
||||
|
||||
.header-info p {
|
||||
font-size: 1.25rem; /* text-xl */
|
||||
color: rgba(204, 251, 252, 0.9); /* text-teal-100 */
|
||||
margin-bottom: 1rem; /* mb-4 */
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem; /* gap-6 */
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.contact-item i {
|
||||
margin-right: 0.5rem; /* mr-2 */
|
||||
}
|
||||
|
||||
.contact-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Match Score Box */
|
||||
.score-box {
|
||||
background-color: rgba(255, 255, 255, 0.2); /* bg-white bg-opacity-20 */
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem; /* p-4 */
|
||||
text-align: center;
|
||||
width: 8rem; /* w-32 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); /* shadow-inner */
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 2.25rem; /* text-4xl */
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
}
|
||||
|
||||
.assessment-rating {
|
||||
font-size: 0.75rem; /* text-xs */
|
||||
margin-top: 0.25rem; /* mt-1 */
|
||||
font-weight: 600; /* font-semibold */
|
||||
}
|
||||
|
||||
/* --- Dynamic Color Classes (Match Score) --- */
|
||||
.score-red { color: var(--score-red); }
|
||||
.score-yellow { color: var(--score-yellow); }
|
||||
.score-green { color: var(--score-green); }
|
||||
.text-green-check { color: var(--score-green); }
|
||||
.text-yellow-exclaim { color: var(--score-yellow); }
|
||||
.text-red-x { color: var(--score-red); }
|
||||
|
||||
|
||||
/* --- Summary Section --- */
|
||||
.summary-text {
|
||||
color: var(--color-gray-700);
|
||||
line-height: 1.625; /* leading-relaxed */
|
||||
border-left: 4px solid var(--kaauh-teal-dark);
|
||||
padding-left: 1rem; /* pl-4 */
|
||||
}
|
||||
|
||||
/* --- Experience Section --- */
|
||||
.experience-item {
|
||||
margin-bottom: 1.5rem; /* mb-6 */
|
||||
padding-bottom: 1.5rem; /* pb-6 */
|
||||
border-bottom: 1px solid var(--color-gray-100);
|
||||
}
|
||||
|
||||
.experience-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.experience-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.5rem; /* mb-2 */
|
||||
}
|
||||
|
||||
.experience-header h3 {
|
||||
font-size: 1.25rem; /* text-xl */
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
|
||||
.experience-header p {
|
||||
color: var(--kaauh-teal);
|
||||
font-weight: 600; /* font-semibold */
|
||||
}
|
||||
|
||||
.experience-tag {
|
||||
background-color: var(--kaauh-light-bg);
|
||||
color: var(--kaauh-teal);
|
||||
padding: 0.25rem 0.75rem; /* px-3 py-1 */
|
||||
border-radius: 9999px; /* rounded-full */
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
font-weight: 500; /* font-medium */
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
.experience-meta {
|
||||
color: var(--color-gray-600);
|
||||
margin-bottom: 0.75rem; /* mb-3 */
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
}
|
||||
|
||||
.experience-meta i {
|
||||
margin-right: 0.5rem; /* mr-2 */
|
||||
}
|
||||
|
||||
.experience-meta span {
|
||||
margin-left: 1rem; /* ml-4 */
|
||||
}
|
||||
|
||||
.achievement-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
|
||||
.achievement-list li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.achievement-list i {
|
||||
color: var(--kaauh-teal-dark);
|
||||
margin-top: 0.25rem; /* mt-1 */
|
||||
margin-right: 0.5rem; /* mr-2 */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* --- Education Section --- */
|
||||
.education-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--color-gray-100);
|
||||
transition: background-color 0.15s ease-in-out; /* transition duration-150 */
|
||||
padding: 0.5rem; /* p-2 */
|
||||
margin: -0.5rem; /* -m-2 */
|
||||
border-radius: 0.5rem; /* rounded-lg */
|
||||
}
|
||||
|
||||
.education-item:hover {
|
||||
background-color: var(--kaauh-light-bg); /* hover:bg-kaauh-light-bg */
|
||||
}
|
||||
|
||||
.education-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.icon-badge {
|
||||
background-color: #e0f2f7; /* teal-100 */
|
||||
padding: 0.75rem; /* p-3 */
|
||||
border-radius: 9999px;
|
||||
margin-right: 1rem; /* mr-4 */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-badge i {
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
.education-details {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.education-details h3 {
|
||||
font-size: 1.125rem; /* text-lg */
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
|
||||
.education-details p {
|
||||
color: var(--kaauh-teal);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.education-details .meta {
|
||||
color: var(--color-gray-600);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.education-details .meta i {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* --- Projects Section --- */
|
||||
.project-item {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--color-gray-100);
|
||||
}
|
||||
|
||||
.project-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.project-item h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
|
||||
.project-item .description {
|
||||
color: var(--color-gray-600);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem; /* gap-2 */
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
background-color: #e0f2f7; /* bg-teal-100 */
|
||||
color: var(--kaauh-teal);
|
||||
padding: 0.25rem 0.75rem; /* px-3 py-1 */
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem; /* text-xs */
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* --- Analysis Section --- */
|
||||
.analysis-summary {
|
||||
background-color: #fffbeb; /* bg-yellow-50 */
|
||||
border-left: 4px solid var(--score-yellow);
|
||||
padding: 1rem; /* p-4 */
|
||||
border-radius: 0 0.5rem 0.5rem 0; /* rounded-r-lg */
|
||||
}
|
||||
|
||||
.analysis-summary h3 {
|
||||
font-weight: 600;
|
||||
color: #b45309; /* text-yellow-800 */
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.analysis-summary p {
|
||||
color: #a16207; /* text-yellow-700 */
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.analysis-metric, .criteria-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 0.25rem;
|
||||
border-bottom: 1px solid var(--color-gray-100);
|
||||
}
|
||||
|
||||
.analysis-metric:last-child, .criteria-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: var(--color-gray-600);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metric-label i {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
background-color: var(--color-gray-200);
|
||||
border-radius: 9999px;
|
||||
height: 0.75rem; /* h-3 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 0.75rem;
|
||||
border-radius: 9999px;
|
||||
transition: width 1s ease-in-out;
|
||||
}
|
||||
|
||||
/* Language fluency bar */
|
||||
.language-bar {
|
||||
height: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
background-color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* --- Strength/Weakness/Flag Boxes --- */
|
||||
.narrative-box {
|
||||
padding-top: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.red-flag-box {
|
||||
border-top: 1px solid #fee2e2; /* border-red-100 */
|
||||
}
|
||||
|
||||
.strength-box {
|
||||
border-top: 1px solid #d1fae5; /* border-green-100 */
|
||||
}
|
||||
|
||||
.flag-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.flag-title i {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.flag-title.red { color: #b91c1c; } /* text-red-700 */
|
||||
.flag-title.green { color: #065f46; } /* text-green-700 */
|
||||
|
||||
.flag-title.red i { color: #ef4444; } /* text-red-500 */
|
||||
|
||||
.narrative-text {
|
||||
color: var(--color-gray-700);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* --- Keywords Section --- */
|
||||
.keyword-tag {
|
||||
background-color: #e0f2f7; /* bg-teal-100 */
|
||||
color: var(--kaauh-teal-dark);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
.cultural-tag {
|
||||
background-color: #f3e8ff; /* bg-purple-100 */
|
||||
color: #6b21a8; /* text-purple-800 */
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.keyword-subheader {
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-700);
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.keyword-subheader i {
|
||||
margin-right: 0.5rem;
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* Specific Fixes */
|
||||
.max-w-50-percent {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
/* Animation Re-implementation */
|
||||
@keyframes fade-in-grow {
|
||||
from { width: 0%; }
|
||||
}
|
||||
|
||||
.progress-bar-animated {
|
||||
animation: fade-in-grow 1s ease-in-out forwards;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-kaauh-light-bg font-sans">
|
||||
|
||||
<div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
|
||||
|
||||
{% include 'recruitment/partials/ai_overview_breadcromb.html' %}
|
||||
<!-- 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>
|
||||
<div class="contact-details">
|
||||
<div class="contact-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span>{{ candidate.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>
|
||||
</div>
|
||||
<!-- GitHub and LinkedIn links for quick access (null in example but included for completeness) -->
|
||||
{% if candidate.resume_data.linkedin %}
|
||||
<div class="contact-item">
|
||||
|
||||
<a href="{{ candidate.resume_data.linkedin }}" target="_blank"><i class="fab fa-linkedin text-white"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if candidate.resume_data.github %}
|
||||
<div class="contact-item">
|
||||
|
||||
<a href="{{ candidate.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-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 %}">
|
||||
<!-- scoring_data.screening_stage_rating -->
|
||||
{{ candidate.analysis_data.screening_stage_rating|default:"Assessment" }}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="content-grid">
|
||||
<!-- Left Column: Primary Content -->
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Summary Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
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." }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Experience Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title" style="margin-bottom: 1.5rem;">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
Experience
|
||||
</h2>
|
||||
{% for experience in candidate.resume_data.experience %}
|
||||
<div class="experience-item">
|
||||
<div class="experience-header">
|
||||
<div>
|
||||
<h3>{{ experience.job_title }}</h3>
|
||||
<p>{{ experience.company }}</p>
|
||||
</div>
|
||||
<span class="experience-tag">
|
||||
{% if experience.end_date == "Present" %}Present{% else %}{{ experience.end_date|default:"Current" }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<p class="experience-meta">
|
||||
<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 -->
|
||||
{% if experience.location %}<span style="margin-left: 1rem;"><i class="fas fa-map-pin"></i>{{ experience.location }}</span>{% endif %}
|
||||
</p>
|
||||
{% if experience.key_achievements %}
|
||||
<ul class="achievement-list">
|
||||
{% for achievement in experience.key_achievements %}
|
||||
<li><i class="fas fa-caret-right"></i>{{ achievement }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<!-- Education Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title" style="margin-bottom: 1.5rem;">
|
||||
<i class="fas fa-graduation-cap"></i>
|
||||
Education
|
||||
</h2>
|
||||
{% for education in candidate.resume_data.education %}
|
||||
<div class="education-item">
|
||||
<div class="icon-badge">
|
||||
<i class="fas fa-certificate"></i>
|
||||
</div>
|
||||
<div class="education-details">
|
||||
<h3>{{ education.degree }}</h3>
|
||||
<p>{{ education.institution }}</p>
|
||||
{% if education.year %}
|
||||
<p class="meta"><i class="fas fa-calendar-alt"></i> {{ education.year }}</p>
|
||||
{% endif %}
|
||||
{% if education.gpa %}
|
||||
<p class="meta"><i class="fas fa-award"></i> GPA: {{ education.gpa }}</p>
|
||||
{% endif %}
|
||||
<!-- candidate.resume_data.education[].relevant_courses -->
|
||||
{% if education.relevant_courses %}
|
||||
<p class="meta" style="margin-top: 0.25rem;">Courses: {{ education.relevant_courses|join:", " }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<!-- Projects Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-project-diagram"></i>
|
||||
Projects
|
||||
</h2>
|
||||
{% for project in candidate.resume_data.projects %}
|
||||
<div class="project-item">
|
||||
<h3>{{ project.name }}</h3>
|
||||
<p class="description">{{ project.brief_description }}</p>
|
||||
{% if project.technologies_used %}
|
||||
<div class="tag-list">
|
||||
{% for tech in project.technologies_used %}
|
||||
<span class="tag-item">{{ tech }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not candidate.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 %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
<span class="keyword-tag">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.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 %}
|
||||
<span class="cultural-tag">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Skills & Analysis -->
|
||||
<div class="space-y-6">
|
||||
<!-- Analysis Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
Analysis
|
||||
</h2>
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Criteria Checklist Section -->
|
||||
{% if candidate.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 %}
|
||||
<div class="criteria-item">
|
||||
<span class="text-gray-700">{{ criterion }}</span>
|
||||
<span class="metric-value" style="font-size: 0.875rem;">
|
||||
{% if status == 'Met' %}<span class="text-green-check"><i class="fas fa-check-circle"></i> Met</span>
|
||||
{% elif status == 'Not Mentioned' %}<span class="text-yellow-exclaim"><i class="fas fa-exclamation-circle"></i> Not Mentioned</span>
|
||||
{% else %}<span class="text-red-x"><i class="fas fa-times-circle"></i> {{ status }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- Skills Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-tools"></i>
|
||||
Skills
|
||||
</h2>
|
||||
{% if candidate.resume_data.skills %}
|
||||
{% for category, skills in candidate.resume_data.skills.items %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<!-- candidate.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 %}
|
||||
<span class="tag-item" style="color: var(--kaauh-teal-dark);">{{ skill }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color: var(--color-gray-500); font-size: 0.875rem;">Skills information not available.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Languages Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-language"></i>
|
||||
Languages
|
||||
</h2>
|
||||
{% if candidate.analysis_data.language_fluency %}
|
||||
{% for language in candidate.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 -->
|
||||
<span class="metric-title">{{ language }}</span>
|
||||
</div>
|
||||
<div class="progress-container" style="height: 0.5rem;">
|
||||
{% with fluency_check=language|lower %}
|
||||
<div class="language-bar"
|
||||
style="width: {% if 'native' in fluency_check %}100{% elif 'fluent' in fluency_check %}85{% elif 'intermediate' in fluency_check %}50{% elif 'basic' in fluency_check %}25{% else %}10{% endif %}%">
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color: var(--color-gray-500); font-size: 0.875rem;">Language information not available.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
Key Metrics
|
||||
</h2>
|
||||
<div style="margin-top: 0.75rem;">
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if candidate.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 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simple progress bar animation
|
||||
window.addEventListener('load', () => {
|
||||
const progressBars = document.querySelectorAll('.progress-bar');
|
||||
progressBars.forEach(bar => {
|
||||
// The width is already set in the style attribute via DTL
|
||||
const width = bar.style.width;
|
||||
// Temporarily set to 0 to prepare for animation
|
||||
bar.style.width = '0%';
|
||||
// Add class to trigger CSS animation defined in the style block
|
||||
bar.classList.add('progress-bar-animated');
|
||||
});
|
||||
|
||||
// Note: The progress-bar-animated class with the CSS keyframe will handle the width transition.
|
||||
// The original setTimeout logic is replaced by the CSS animation for smoother performance.
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -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' }
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
<button type="button" class="btn btn-main-action"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' candidate.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="{% trans 'View Details and Score Breakdown' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% with request.resolver_match as resolved %}
|
||||
{% if 'resume-template' in resolved.route and resolved.kwargs.slug == candidate.slug %}
|
||||
<nav aria-label="breadcrumb" style="font-family: 'Inter', sans-serif; margin-bottom:1.5rem;">
|
||||
@ -59,7 +60,7 @@
|
||||
</li>
|
||||
<li style="display: flex; align-items: center;">
|
||||
<a
|
||||
href="{% url 'candidate_list' %}"
|
||||
href="{% url 'application_list' %}"
|
||||
style="
|
||||
color: #6c757d; /* text-secondary/gray */
|
||||
text-decoration: none;
|
||||
@ -81,7 +82,7 @@
|
||||
</li>
|
||||
<li style="display: flex; align-items: center;">
|
||||
<a
|
||||
href="{% url 'candidate_detail' candidate.slug %}"
|
||||
href="{% url 'application_detail' candidate.slug %}"
|
||||
style="
|
||||
color: #6c757d; /* text-secondary/gray */
|
||||
text-decoration: none;
|
||||
@ -90,7 +91,11 @@
|
||||
onmouseover="this.style.color='#000000';"
|
||||
onmouseout="this.style.color='#6c757d';"
|
||||
>
|
||||
{{candidate.resume_data.full_name}}
|
||||
{% if LANGUAGE_CODE == 'en' %}
|
||||
{{candidate.resume_data_en.full_name}}
|
||||
{% else%}
|
||||
{{candidate.resume_data_ar.full_name}}
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
<td class="text-center" id="status-result-{{ candidate.pk}}">
|
||||
{% if candidate.exam_status %}
|
||||
<button type="button" class="btn btn-{% if candidate.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
<td class="text-center" id="status-result-{{ application.pk}}">
|
||||
{% 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 %}
|
||||
--
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td id="exam-score-{{ candidate.pk}}" hx-swap-oob="true">
|
||||
{{candidate.exam_score|default:"--"}}
|
||||
<td id="exam-score-{{ application.pk}}" hx-swap-oob="true">
|
||||
{{application.exam_score|default:"--"}}
|
||||
</td>
|
||||
@ -1,22 +1,22 @@
|
||||
<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 %}
|
||||
--
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
<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' 'Accepted' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'offer' 'Accepted' %}"
|
||||
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' 'Rejected' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'offer' 'Rejected' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
{{ candidate.offer_status }}
|
||||
{{ application.offer_status }}
|
||||
</button>
|
||||
{% else %}
|
||||
--
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!-- Stage Update Form with Errors -->
|
||||
<form id="stageUpdateForm" hx-post="{% url 'candidate_update_stage' candidate.slug %}">
|
||||
<form id="stageUpdateForm" hx-post="{% url 'application_update_stage' candidate.slug %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Stage Selection -->
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
<!-- Stage Update Modal -->
|
||||
{% load i18n%}
|
||||
<div class="modal fade" id="stageUpdateModal" tabindex="-1" aria-labelledby="stageUpdateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-top">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="stageUpdateModalLabel">
|
||||
<i class="fas fa-exchange-alt me-2"></i>Update Application Stage
|
||||
<i class="fas fa-exchange-alt me-2"></i>{% trans "Update Application Stage" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -16,19 +17,19 @@
|
||||
<div class="alert alert-info mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Current Stage:</strong>
|
||||
<span class="badge bg-primary ms-2" id="currentStageBadge">{{candidate.stage}}</span>
|
||||
<strong>{% trans "Current Stage:" %}</strong>
|
||||
<span class="badge bg-primary ms-2" id="currentStageBadge">{{application.stage}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Container -->
|
||||
<div id="stageUpdateFormContainer">
|
||||
<form id="stageUpdateForm" action="{% url 'candidate_update_stage' candidate.slug %}" method="post">
|
||||
<form id="stageUpdateForm" action="{% url 'application_update_stage' application.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<!-- Stage Selection -->
|
||||
<div class="mb-3">
|
||||
<label for="id_stage" class="form-label">
|
||||
<i class="fas fa-arrow-right me-1"></i>Move to Stage
|
||||
<i class="fas fa-arrow-right me-1"></i>{% trans "Move to Stage" %}
|
||||
</label>
|
||||
|
||||
<select id="id_stage" name="stage" class="form-select">
|
||||
@ -42,19 +43,19 @@
|
||||
<div class="alert alert-light border" id="availableStagesInfo">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-lightbulb me-1"></i>
|
||||
<span id="availableStagesText">Select a stage above to see transition information.</span>
|
||||
<span id="availableStagesText">{% trans "Select a stage above to see transition information." %}</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
<i class="fas fa-times me-1"></i>{% trans "Cancel" %}
|
||||
</button>
|
||||
|
||||
<button type="submit" class="btn btn-main-action" id="stageUpdateSubmit">
|
||||
<i class="fas fa-save me-1"></i>
|
||||
<span class="ms-2">Update</span>
|
||||
<span class="ms-2">{% trans "Update" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -73,26 +74,26 @@ function updateStageInfo(selectedValue, currentStage) {
|
||||
|
||||
if (selectedValue && selectedValue !== currentStage) {
|
||||
const stageLabels = {
|
||||
'Applied': 'Applied',
|
||||
'Exam': 'Exam',
|
||||
'Interview': 'Interview',
|
||||
'Offer': 'Offer'
|
||||
'Applied': '{% trans "Applied" %}',
|
||||
'Exam': '{% trans "Exam" %}',
|
||||
'Interview': '{% trans "Interview" %}',
|
||||
'Offer': '{% trans "Offer" %}'
|
||||
};
|
||||
|
||||
if (availableStagesText) {
|
||||
availableStagesText.textContent = `Moving from "${currentStage}" to "${stageLabels[selectedValue]}". This transition is allowed.`;
|
||||
availableStagesText.textContent = `{% trans "Moving from" %} "${currentStage}" {% trans "to" %} "${stageLabels[selectedValue]}". {% trans "This transition is allowed." %}`;
|
||||
availableStagesText.parentElement.parentElement.classList.remove('alert-warning', 'alert-light');
|
||||
availableStagesText.parentElement.parentElement.classList.add('alert-success');
|
||||
}
|
||||
} else if (selectedValue === currentStage) {
|
||||
if (availableStagesText) {
|
||||
availableStagesText.textContent = 'No stage change selected. The candidate is already in this stage.';
|
||||
availableStagesText.textContent = '{% trans "No stage change selected. The candidate is already in this stage." %}';
|
||||
availableStagesText.parentElement.parentElement.classList.remove('alert-success', 'alert-warning');
|
||||
availableStagesText.parentElement.parentElement.classList.add('alert-info');
|
||||
}
|
||||
} else {
|
||||
if (availableStagesText) {
|
||||
availableStagesText.textContent = 'Select a stage above to see transition information.';
|
||||
availableStagesText.textContent = '{% trans "Select a stage above to see transition information." %}';
|
||||
availableStagesText.parentElement.parentElement.classList.remove('alert-success', 'alert-warning');
|
||||
availableStagesText.parentElement.parentElement.classList.add('alert-light');
|
||||
}
|
||||
|
||||
@ -25,9 +25,9 @@
|
||||
{# SCOPED - 3. Total Candidates #}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users stat-icon"></i> {% trans "Total Candidates" %}</h3>
|
||||
<h3><i class="fas fa-users stat-icon"></i> {% trans "Total Applications" %}</h3>
|
||||
</div>
|
||||
<div class="stat-value">{{ total_candidates }}</div>
|
||||
<div class="stat-value">{{ total_applications }}</div>
|
||||
<div class="stat-caption">{% trans "Total applications" %}</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -10,20 +10,20 @@
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="fas fa-calendar-plus me-2"></i>
|
||||
{% if has_future_meeting %}
|
||||
{% trans "Update Interview" %} for {{ candidate.name }}
|
||||
{% trans "Update Interview" %} for {{ application.name }}
|
||||
{% else %}
|
||||
{% trans "Schedule Interview" %} for {{ candidate.name }}
|
||||
{% trans "Schedule Interview" %} for {{ application.name }}
|
||||
{% endif %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">{% trans "Job" %}: {{ job.title }}</p>
|
||||
{% if has_future_meeting %}
|
||||
<div class="alert alert-info mt-2 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "This candidate has upcoming interviews. You are updating an existing schedule." %}
|
||||
{% trans "This application has upcoming interviews. You are updating an existing schedule." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'candidate_interview_view' job.slug %}" class="btn btn-outline-secondary">
|
||||
<a href="{% url 'applications_interview_view' job.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Candidates" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -85,7 +85,7 @@
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i> {% trans "Schedule Meeting" %}
|
||||
</button>
|
||||
<a href="{% url 'candidate_interview_view' job.slug %}" class="btn btn-secondary">
|
||||
<a href="{% url 'applications_interview_view' job.slug %}" class="btn btn-secondary">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Assign Staff to {{ job.title }}{% endblock %}
|
||||
{% block title %}{% trans "Assign Staff to" %} {{ job.title }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
@ -142,34 +142,34 @@
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="page-header h3 mb-1">Assign Staff to Job</h1>
|
||||
<p class="text-secondary mb-0">Job: {{ job.title }} ({{ job.internal_job_id }})</p>
|
||||
<h1 class="page-header h3 mb-1">{% trans "Assign Staff to Job" %}</h1>
|
||||
<p class="text-secondary mb-0">{% trans "Job:" %} {{ job.title }} ({{ job.internal_job_id }})</p>
|
||||
</div>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Back to Job Details
|
||||
<i class="fas fa-arrow-left me-2"></i>{% trans "Back to Job Details" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Job Information Card -->
|
||||
<div class="kaauh-card mb-4">
|
||||
<div class="job-header-card">
|
||||
<h5 class="mb-0"><i class="fas fa-briefcase me-2"></i>Job Information</h5>
|
||||
<h5 class="mb-0"><i class="fas fa-briefcase me-2"></i>{% trans "Job Information" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mx-2 my-2">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Department:</strong> {{ job.get_department_display|default:job.department }}</p>
|
||||
<p><strong>Job Type:</strong> {{ job.get_job_type_display }}</p>
|
||||
<p><strong>Workplace Type:</strong> {{ job.get_workplace_type_display }}</p>
|
||||
<p><strong>{% trans "Department:" %}</strong> {{ job.get_department_display|default:job.department }}</p>
|
||||
<p><strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}</p>
|
||||
<p><strong>{% trans "Workplace Type:" %}</strong> {{ job.get_workplace_type_display }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Status:</strong>
|
||||
<p><strong>{% trans "Status:" %}</strong>
|
||||
<span class="badge {% if job.status == 'ACTIVE' %}bg-success{% elif job.status == 'CLOSED' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||
{{ job.get_status_display }}
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>Applications:</strong> {{ applications.count }}</p>
|
||||
<p><strong>Created:</strong> {{ job.created_at|date:"M d, Y" }}</p>
|
||||
<p><strong>{% trans "Applications:" %}</strong> {{ applications.count }}</p>
|
||||
<p><strong>{% trans "Created:" %}</strong> {{ job.created_at|date:"M d, Y" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -178,7 +178,7 @@
|
||||
<!-- Staff Assignment Form -->
|
||||
<div class="kaauh-card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-user-tie me-2"></i>Staff Assignment</h5>
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-user-tie me-2"></i>{% trans "Staff Assignment" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
@ -248,10 +248,10 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus me-2"></i>Assign Staff to Job
|
||||
<i class="fas fa-user-plus me-2"></i>{% trans "Assign Staff to Job" %}
|
||||
</button>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary ms-2">
|
||||
<i class="fas fa-times me-2"></i>Cancel
|
||||
<i class="fas fa-times me-2"></i>{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -263,18 +263,18 @@
|
||||
{% if job.staff_assignments.exists %}
|
||||
<div class="kaauh-card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-users me-2"></i>Current Staff Assignments</h5>
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-users me-2"></i>{% trans "Current Staff Assignments" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Staff Member</th>
|
||||
<th>Email</th>
|
||||
<th>Assignment Date</th>
|
||||
<th>Notes</th>
|
||||
<th>Status</th>
|
||||
<th>{% trans "Staff Member" %}</th>
|
||||
<th>{% trans "Email" %}</th>
|
||||
<th>{% trans "Assignment Date" %}</th>
|
||||
<th>{% trans "Notes" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -283,16 +283,16 @@
|
||||
<td>
|
||||
<strong>{{ assignment.staff.get_full_name|default:assignment.staff.username }}</strong>
|
||||
{% if assignment.staff.is_active %}
|
||||
<span class="badge bg-success ms-2">Active</span>
|
||||
<span class="badge bg-success ms-2">{% trans "Active" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger ms-2">Inactive</span>
|
||||
<span class="badge bg-danger ms-2">{% trans "Inactive" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ assignment.staff.email }}</td>
|
||||
<td>{{ assignment.assignment_date|date:"M d, Y" }}</td>
|
||||
<td>{{ assignment.notes|default:"-" }}</td>
|
||||
<td>
|
||||
<span class="badge bg-primary">Assigned</span>
|
||||
<span class="badge bg-primary">{% trans "Assigned" %}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -306,7 +306,7 @@
|
||||
<!-- Available Staff Members -->
|
||||
<div class="kaauh-card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-users me-2"></i>Available Staff Members</h5>
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-users me-2"></i>{% trans "Available Staff Members" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
@ -329,11 +329,11 @@
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{% if staff_user.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
<span class="badge bg-success">{% trans "Active" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
<span class="badge bg-danger">{% trans "Inactive" %}</span>
|
||||
{% endif %}
|
||||
<span class="badge bg-primary ms-1">Staff</span>
|
||||
<span class="badge bg-primary ms-1">{% trans "Staff" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -342,7 +342,7 @@
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
No staff members available. Please create staff accounts first.
|
||||
{% trans "No staff members available. Please create staff accounts first." %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@ -359,18 +359,18 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="staffAssignmentModalLabel">
|
||||
<i class="fas fa-user-plus me-2"></i>Confirm Staff Assignment
|
||||
<i class="fas fa-user-plus me-2"></i>{% trans "Confirm Staff Assignment" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to assign this staff member to the job <strong>{{ job.title }}</strong>?</p>
|
||||
<p>{% trans "Are you sure you want to assign this staff member to the job" %} <strong>{{ job.title }}</strong>?</p>
|
||||
<div id="selectedStaffInfo"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="button" class="btn btn-main-action" id="confirmAssignmentBtn">
|
||||
<i class="fas fa-check me-2"></i>Confirm Assignment
|
||||
<i class="fas fa-check me-2"></i>{% trans "Confirm Assignment" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -398,16 +398,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Show selected staff info in modal
|
||||
selectedStaffInfo.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<strong>Selected Staff Member:</strong><br>
|
||||
Name: ${selectedOption.text}<br>
|
||||
This assignment will take effect immediately.
|
||||
<strong>{% trans "Selected Staff Member:" %}</strong><br>
|
||||
{% trans "Name:" %} ${selectedOption.text}<br>
|
||||
{% trans "This assignment will take effect immediately." %}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show modal
|
||||
modal.show();
|
||||
} else {
|
||||
alert('Please select a staff member.');
|
||||
alert('{% trans "Please select a staff member." %}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user