handle_resume_parsing_and_scoring now supports both english and arabic #40

Merged
ismail merged 9 commits from frontend into main 2025-11-27 15:48:41 +03:00
88 changed files with 3650 additions and 2579 deletions

6
.env
View File

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

View File

@ -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,

View File

@ -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'),

View File

@ -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:

View File

@ -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 = '/'

View File

@ -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)

View File

@ -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')

View File

@ -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'}))

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

View File

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

View File

@ -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))

View File

@ -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",
)

View File

@ -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 (12 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 JSONno 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)

View File

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

View File

@ -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(),

View File

@ -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"),
]

View File

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

View File

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

View File

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

View File

@ -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">

View File

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

View File

@ -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" %}

View File

@ -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 {

View File

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

View File

@ -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 }}

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

View File

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

View File

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

View File

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

View File

@ -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 %}

View File

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

View File

@ -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"

View File

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

View File

@ -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 %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" %}

View File

@ -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">

View File

@ -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">

View File

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

View File

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

View File

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

View File

@ -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 %}">

View File

@ -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;">

View File

@ -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">

View File

@ -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">

View File

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

View File

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

View File

@ -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" %}';
});
});

View File

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

View File

@ -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">

View File

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

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

View File

@ -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 %}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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);">

View File

@ -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">

View File

@ -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;
}
});

View File

@ -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 %}

View File

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

View File

@ -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);">

View File

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

View File

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

View File

@ -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" %}

View File

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

View File

@ -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' }
}

View File

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

View File

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

View File

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

View File

@ -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 %}
--

View File

@ -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 %}
--

View File

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

View File

@ -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');
}

View File

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

View File

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

View File

@ -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." %}');
}
});
}