Merge pull request 'handle_resume_parsing_and_scoring now supports both english and arabic' (#40) from frontend into main

Reviewed-on: #40
This commit is contained in:
ismail 2025-11-27 15:48:41 +03:00
commit 1fad9839af
88 changed files with 3650 additions and 2579 deletions

6
.env
View File

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

View File

@ -298,7 +298,7 @@ Q_CLUSTER = {
"name": "KAAUH_CLUSTER", "name": "KAAUH_CLUSTER",
"workers": 2, "workers": 2,
"recycle": 500, "recycle": 500,
"timeout": 60, "timeout": 120,
"max_attempts": 1, "max_attempts": 1,
"compress": True, "compress": True,
"save_limit": 250, "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>/', views.application_submit_form, name='application_submit_form'),
path('application/<slug:template_slug>/submit/', views.application_submit, name='application_submit'), 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>/apply/', views.job_application_detail, name='job_application_detail'),
path('application/<slug:slug>/signup/', views.candidate_signup, name='candidate_signup'), path('application/<slug:slug>/signup/', views.application_signup, name='application_signup'),
path('application/<slug:slug>/success/', views.application_success, name='application_success'), path('application/<slug:slug>/success/', views.application_success, name='application_success'),
# path('application/applicant/profile', views.applicant_profile, name='applicant_profile'), # 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}") print(f"Error with document_upload URL: {e}")
try: try:
url2 = reverse('candidate_document_upload', kwargs={'slug': application.slug}) url2 = reverse('pplication_document_upload', kwargs={'slug': application.slug})
print(f"URL pattern 2 (candidate_document_upload): {url2}") print(f"URL pattern 2 (pplication_document_upload): {url2}")
except Exception as e: 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 # Test GET request to see if the URL is accessible
try: try:

View File

@ -24,7 +24,7 @@ class CustomAuthenticationBackend(AuthenticationBackend):
elif user.user_type == 'agency': elif user.user_type == 'agency':
redirect_url = reverse('agency_portal_dashboard') redirect_url = reverse('agency_portal_dashboard')
elif user.user_type == 'candidate': elif user.user_type == 'candidate':
redirect_url = reverse('candidate_portal_dashboard') redirect_url = reverse('applicant_portal_dashboard')
else: else:
# Fallback to default redirect URL if user type is unknown # Fallback to default redirect URL if user type is unknown
redirect_url = '/' redirect_url = '/'

View File

@ -35,7 +35,7 @@ class CandidateSyncService:
} }
# Get all hired candidates for this job # 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) 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': if user.user_type == 'agency':
return redirect('agency_portal_dashboard') return redirect('agency_portal_dashboard')
elif user.user_type == 'candidate': elif user.user_type == 'candidate':
return redirect('candidate_portal_dashboard') return redirect('applicant_portal_dashboard')
else: else:
return redirect('dashboard') return redirect('dashboard')
@ -92,7 +92,7 @@ class UserTypeRequiredMixin(AccessMixin):
if request.user.user_type == 'agency': if request.user.user_type == 'agency':
return redirect('agency_portal_dashboard') return redirect('agency_portal_dashboard')
elif request.user.user_type == 'candidate': elif request.user.user_type == 'candidate':
return redirect('candidate_portal_dashboard') return redirect('applicant_portal_dashboard')
else: else:
return redirect('dashboard') return redirect('dashboard')

View File

@ -293,6 +293,7 @@ class ApplicationForm(forms.ModelForm):
"resume", "resume",
] ]
labels = { labels = {
"person":_("Applicant"),
"resume": _("Resume"), "resume": _("Resume"),
"hiring_source": _("Hiring Type"), "hiring_source": _("Hiring Type"),
"hiring_agency": _("Hiring Agency"), "hiring_agency": _("Hiring Agency"),
@ -903,7 +904,7 @@ class FormTemplateIsActiveForm(forms.ModelForm):
fields = ["is_active"] fields = ["is_active"]
class CandidateExamDateForm(forms.ModelForm): class ApplicationExamDateForm(forms.ModelForm):
class Meta: class Meta:
model = Application model = Application
fields = ["exam_date"] 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'})) password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'form-control'}))
confirm_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.auth.models import AbstractUser
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType 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): class CustomUser(AbstractUser):
"""Custom user model extending AbstractUser""" """Custom user model extending AbstractUser"""
@ -130,7 +132,7 @@ class JobPosting(Base):
# Application Information ---job detail apply link for the candidates # Application Information ---job detail apply link for the candidates
application_url = models.URLField( application_url = models.URLField(
validators=[URLValidator()], validators=[URLValidator()],
help_text="URL where candidates apply", help_text="URL where applicants apply",
null=True, null=True,
blank=True, blank=True,
) )
@ -220,7 +222,7 @@ class JobPosting(Base):
related_name="jobs", related_name="jobs",
verbose_name=_("Hiring Agency"), verbose_name=_("Hiring Agency"),
help_text=_( help_text=_(
"External agency responsible for sourcing candidates for this role" "External agency responsible for sourcing applicants for this role"
), ),
) )
cancel_reason = models.TextField( cancel_reason = models.TextField(
@ -363,7 +365,7 @@ class JobPosting(Base):
@property @property
def current_applications_count(self): 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() return self.applications.count()
@property @property
@ -375,88 +377,90 @@ class JobPosting(Base):
return self.current_applications_count >= self.max_applications return self.current_applications_count >= self.max_applications
@property @property
def all_candidates(self): def all_applications(self):
return self.applications.annotate( # 1. Define the safe JSON extraction and conversion expression
sortable_score=Coalesce( safe_score_expression = Cast(
Cast( Coalesce(
"ai_analysis_data__analysis_data__match_score", # Extract the score explicitly as a text string (KeyTextTransform)
output_field=IntegerField(), 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") ).order_by("-sortable_score")
@property @property
def screening_candidates(self): def screening_applications(self):
return self.all_candidates.filter(stage="Applied") return self.all_applications.filter(stage="Applied")
@property @property
def exam_candidates(self): def exam_applications(self):
return self.all_candidates.filter(stage="Exam") return self.all_applications.filter(stage="Exam")
@property @property
def interview_candidates(self): def interview_applications(self):
return self.all_candidates.filter(stage="Interview") return self.all_applications.filter(stage="Interview")
@property @property
def document_review_candidates(self): def document_review_applications(self):
return self.all_candidates.filter(stage="Document Review") return self.all_applications.filter(stage="Document Review")
@property @property
def offer_candidates(self): def offer_applications(self):
return self.all_candidates.filter(stage="Offer") return self.all_applications.filter(stage="Offer")
@property @property
def accepted_candidates(self): def accepted_applications(self):
return self.all_candidates.filter(offer_status="Accepted") return self.all_applications.filter(offer_status="Accepted")
@property @property
def hired_candidates(self): def hired_applications(self):
return self.all_candidates.filter(stage="Hired") return self.all_applications.filter(stage="Hired")
# counts # counts
@property @property
def all_candidates_count(self): def all_applications_count(self):
return ( return self.all_applications.count()
self.candidates.annotate(
sortable_score=Cast(
"ai_analysis_data__match_score", output_field=CharField()
)
)
.order_by("-sortable_score")
.count()
or 0
)
@property @property
def screening_candidates_count(self): def screening_applications_count(self):
return self.all_candidates.filter(stage="Applied").count() or 0 return self.all_applications.filter(stage="Applied").count() or 0
@property @property
def exam_candidates_count(self): def exam_applications_count(self):
return self.all_candidates.filter(stage="Exam").count() or 0 return self.all_applications.filter(stage="Exam").count() or 0
@property @property
def interview_candidates_count(self): def interview_applications_count(self):
return self.all_candidates.filter(stage="Interview").count() or 0 return self.all_applications.filter(stage="Interview").count() or 0
@property @property
def document_review_candidates_count(self): def document_review_applications_count(self):
return self.all_candidates.filter(stage="Document Review").count() or 0 return self.all_applications.filter(stage="Document Review").count() or 0
@property @property
def offer_candidates_count(self): def offer_applications_count(self):
return self.all_candidates.filter(stage="Offer").count() or 0 return self.all_applications.filter(stage="Offer").count() or 0
@property @property
def hired_candidates_count(self): def hired_applications_count(self):
return self.all_candidates.filter(stage="Hired").count() or 0 return self.all_applications.filter(stage="Hired").count() or 0
@property @property
def vacancy_fill_rate(self): def vacancy_fill_rate(self):
total_positions = self.open_positions 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: if total_positions > 0:
vacancy_fill_rate = no_of_positions_filled / total_positions vacancy_fill_rate = no_of_positions_filled / total_positions
@ -751,88 +755,162 @@ class Application(Base):
# ==================================================================== # ====================================================================
# ✨ PROPERTIES (GETTERS) - Migrated from Candidate # ✨ PROPERTIES (GETTERS) - Migrated from Candidate
# ==================================================================== # ====================================================================
@property # @property
def resume_data(self): # def resume_data(self):
return self.ai_analysis_data.get("resume_data", {}) # return self.ai_analysis_data.get("resume_data", {})
# @property
# def analysis_data(self):
# return self.ai_analysis_data.get("analysis_data", {})
@property @property
def analysis_data(self): def resume_data_en(self):
return self.ai_analysis_data.get("analysis_data", {}) 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 @property
def match_score(self) -> int: def match_score(self) -> int:
"""1. A score from 0 to 100 representing how well the candidate fits the role.""" """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 @property
def years_of_experience(self) -> float: def years_of_experience(self) -> float:
"""4. The total number of years of professional experience as a numerical value.""" """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 @property
def soft_skills_score(self) -> int: def soft_skills_score(self) -> int:
"""15. A score (0-100) for inferred non-technical skills.""" """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 @property
def industry_match_score(self) -> int: def industry_match_score(self) -> int:
"""16. A score (0-100) for the relevance of the candidate's industry experience.""" """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 @property
def min_requirements_met(self) -> bool: def min_requirements_met(self) -> bool:
"""14. Boolean (true/false) indicating if all non-negotiable minimum requirements are met.""" """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 @property
def screening_stage_rating(self) -> str: def screening_stage_rating(self) -> str:
"""13. A standardized rating (e.g., "A - Highly Qualified", "B - Qualified").""" """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 @property
def top_3_keywords(self) -> List[str]: def top_3_keywords(self) -> List[str]:
"""10. A list of the three most dominant and relevant technical skills or technologies.""" """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 @property
def most_recent_job_title(self) -> str: def most_recent_job_title(self) -> str:
"""8. The candidate's most recent or current professional job title.""" """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 @property
def criteria_checklist(self) -> Dict[str, str]: def criteria_checklist(self) -> Dict[str, str]:
"""5 & 6. An object rating the candidate's match for each specific criterion.""" """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 @property
def professional_category(self) -> str: def professional_category(self) -> str:
"""7. The most fitting professional field or category for the individual.""" """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 @property
def language_fluency(self) -> List[Dict[str, str]]: def language_fluency(self) -> List[Dict[str, str]]:
"""12. A list of languages and their fluency levels mentioned.""" """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 @property
def strengths(self) -> str: def strengths(self) -> str:
"""2. A brief summary of why the candidate is a strong fit.""" """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 @property
def weaknesses(self) -> str: def weaknesses(self) -> str:
"""3. A brief summary of where the candidate falls short or what criteria are missing.""" """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 @property
def job_fit_narrative(self) -> str: def job_fit_narrative(self) -> str:
"""11. A single, concise sentence summarizing the core fit.""" """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 @property
def recommendation(self) -> str: def recommendation(self) -> str:
"""9. Provide a detailed final recommendation for the candidate.""" """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 # 🔄 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: if instance.resume and not instance.is_resume_parsed:
logger.info(f"Scoring resume for candidate {instance.pk}") logger.info(f"Scoring resume for candidate {instance.pk}")
async_task( async_task(
"recruitment.tasks.handle_reume_parsing_and_scoring", "recruitment.tasks.handle_resume_parsing_and_scoring",
instance.pk, instance.pk,
hook="recruitment.hooks.callback_ai_parsing", 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
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. and atomically save the results.
""" """
# --- 1. Robust Object Retrieval (Prevents looping on DoesNotExist) --- # --- 1. Robust Object Retrieval (Prevents looping on DoesNotExist) ---
try: try:
# NOTE: Replace 'Application.objects.get' with your actual model manager call
instance = Application.objects.get(pk=pk) instance = Application.objects.get(pk=pk)
except Application.DoesNotExist: except Application.DoesNotExist:
# Exit gracefully if the candidate was deleted after the task was queued # 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 --- # --- 2. I/O and Initial Data Check ---
try: try:
# Assuming instance.resume is a Django FileField
file_path = instance.resume.path file_path = instance.resume.path
if not os.path.exists(file_path): if not os.path.exists(file_path):
logger.warning(f"Resume file not found: {file_path}") logger.warning(f"Resume file not found: {file_path}")
print(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 return
# Use the new unified document parser # Use the new unified document parser
@ -276,11 +481,13 @@ def handle_reume_parsing_and_scoring(pk):
print(resume_text) print(resume_text)
# --- 3. Single, Combined LLM Prompt (Major Cost & Latency Optimization) --- # --- 3. Single, Combined LLM Prompt (Major Cost & Latency Optimization) ---
prompt = f""" 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: 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. 1. **PARSE (English)**: Extract all key-value information from the RESUME TEXT into a clean JSON structure under the key **'resume_data_en'**.
2. **SCORE**: Analyze the parsed data against the JOB CRITERIA and generate a comprehensive score and analysis under the key 'analysis_data'. 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 CRITERIA:**
{job_detail} {job_detail}
@ -289,10 +496,16 @@ def handle_reume_parsing_and_scoring(pk):
{resume_text} {resume_text}
**STRICT JSON OUTPUT INSTRUCTIONS:** **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", "full_name": "Full name of the candidate",
"current_title": "Most recent or current job title", "current_title": "Most recent or current job title",
"location": "City and state", "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", "match_score": "Integer Score 0-100",
"strengths": "Brief summary of strengths", "strengths": "Brief summary of strengths",
"weaknesses": "Brief summary of weaknesses", "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')"] "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. 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. Output only valid JSONno markdown, no extra text.
""" """
try: try:
# Call the AI handler
result = ai_handler(prompt) result = ai_handler(prompt)
if result['status'] == 'error': if result['status'] == 'error':
logger.error(f"AI handler returned error for candidate {instance.pk}") logger.error(f"AI handler returned error for candidate {instance.pk}")
print(f"AI handler returned error for candidate {instance.pk}") print(f"AI handler returned error for candidate {instance.pk}")
return 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'] data = result['data']
if isinstance(data, str): if isinstance(data, str):
data = json.loads(data) data = json.loads(data)
print(data) print(data)
# parsed_summary = data.get('parsed_data', {})
# scoring_result = data.get('scoring_data', {})
except Exception as e: except Exception as e:
logger.error(f"AI handler failed for candidate {instance.pk}: {e}") logger.error(f"AI handler failed for candidate {instance.pk}: {e}")
print(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) --- # --- 4. Atomic Database Update (Ensures data integrity) ---
with transaction.atomic(): 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) # 2. Update the Full JSON Field (ai_analysis_data)
if instance.ai_analysis_data is None: if instance.ai_analysis_data is None:
instance.ai_analysis_data = {} 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 = 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.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']) instance.save(update_fields=['ai_analysis_data', 'is_resume_parsed'])
logger.info(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 for candidate {instance.id}") print(f"Successfully scored and saved analysis (EN/AR) for candidate {instance.id}")
from django.utils import timezone from django.utils import timezone
@ -642,7 +866,7 @@ def sync_hired_candidates_task(job_slug):
# action=IntegrationLog.ActionChoices.SYNC, # action=IntegrationLog.ActionChoices.SYNC,
# endpoint="multi_source_sync", # endpoint="multi_source_sync",
# method="BACKGROUND_TASK", # 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, # response_data=results,
# status_code="SUCCESS" if results.get('summary', {}).get('failed', 0) == 0 else "PARTIAL", # status_code="SUCCESS" if results.get('summary', {}).get('failed', 0) == 0 else "PARTIAL",
# ip_address="127.0.0.1", # Background task # 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)}") 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: except Exception as e:
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True) 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 CandidateStageForm, InterviewScheduleForm, CandidateSignupForm
) )
from .views import ( from .views import (
ZoomMeetingListView, ZoomMeetingCreateView, job_detail, candidate_screening_view, ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view,
candidate_exam_view, candidate_interview_view, api_schedule_candidate_meeting applications_exam_view, applications_interview_view, api_schedule_application_meeting
) )
from .views_frontend import CandidateListView, JobListView 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): class BaseTestCase(TestCase):
@ -189,32 +189,32 @@ class ViewTests(BaseTestCase):
def test_candidate_screening_view(self): def test_candidate_screening_view(self):
"""Test candidate_screening_view""" """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.assertEqual(response.status_code, 200)
self.assertContains(response, 'John Doe') self.assertContains(response, 'John Doe')
def test_candidate_screening_view_filters(self): def test_candidate_screening_view_filters(self):
"""Test candidate_screening_view with filters""" """Test candidate_screening_view with filters"""
response = self.client.get( 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'} {'min_ai_score': '50', 'tier1_count': '5'}
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_candidate_exam_view(self): def test_candidate_exam_view(self):
"""Test candidate_exam_view""" """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.assertEqual(response.status_code, 200)
self.assertContains(response, 'John Doe') self.assertContains(response, 'John Doe')
def test_candidate_interview_view(self): def test_candidate_interview_view(self):
"""Test candidate_interview_view""" """Test applications_interview_view"""
response = self.client.get(reverse('candidate_interview_view', kwargs={'slug': self.job.slug})) response = self.client.get(reverse('applications_interview_view', kwargs={'slug': self.job.slug}))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@patch('recruitment.views.create_zoom_meeting') @patch('recruitment.views.create_zoom_meeting')
def test_schedule_candidate_meeting(self, mock_create_zoom): 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 = { mock_create_zoom.return_value = {
'status': 'success', 'status': 'success',
'meeting_details': { 'meeting_details': {
@ -231,7 +231,7 @@ class ViewTests(BaseTestCase):
'duration': 60 'duration': 60
} }
response = self.client.post( 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}), kwargs={'job_slug': self.job.slug, 'candidate_pk': self.candidate.pk}),
data data
) )
@ -478,7 +478,7 @@ class PerformanceTests(BaseTestCase):
) )
# Test pagination # Test pagination
response = self.client.get(reverse('candidate_list')) response = self.client.get(reverse('application_list'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Candidate') self.assertContains(response, 'Candidate')
@ -586,8 +586,8 @@ class UtilityFunctionTests(BaseTestCase):
self.assertEqual(result['status'], 'success') self.assertEqual(result['status'], 'success')
self.assertIn('meeting_id', result['meeting_details']) self.assertIn('meeting_id', result['meeting_details'])
def test_get_candidates_from_request(self): def get_applications_from_request(self):
"""Test the get_candidates_from_request utility function""" """Test the get_applications_from_request utility function"""
# This would be tested with a request that has candidate_ids # This would be tested with a request that has candidate_ids
pass pass

View File

@ -33,15 +33,15 @@ from .forms import (
ApplicationStageForm, InterviewScheduleForm, BreakTimeFormSet ApplicationStageForm, InterviewScheduleForm, BreakTimeFormSet
) )
from .views import ( from .views import (
ZoomMeetingListView, ZoomMeetingCreateView, job_detail, candidate_screening_view, ZoomMeetingListView, ZoomMeetingCreateView, job_detail, applications_screening_view,
candidate_exam_view, candidate_interview_view, api_schedule_candidate_meeting, applications_exam_view, applications_interview_view, api_schedule_application_meeting,
schedule_interviews_view, confirm_schedule_interviews_view, _handle_preview_submission, schedule_interviews_view, confirm_schedule_interviews_view, _handle_preview_submission,
_handle_confirm_schedule, _handle_get_request _handle_confirm_schedule, _handle_get_request
) )
# from .views_frontend import CandidateListView, JobListView, JobCreateView # from .views_frontend import CandidateListView, JobListView, JobCreateView
from .utils import ( from .utils import (
create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting, 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 get_available_time_slots
) )
# from .zoom_api import ZoomAPIError # from .zoom_api import ZoomAPIError
@ -421,27 +421,27 @@ class AdvancedViewTests(TestCase):
) )
# Test search by name # Test search by name
response = self.client.get(reverse('candidate_list'), { response = self.client.get(reverse('application_list'), {
'search': 'Jane' 'search': 'Jane'
}) })
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Jane Smith') self.assertContains(response, 'Jane Smith')
# Test search by email # Test search by email
response = self.client.get(reverse('candidate_list'), { response = self.client.get(reverse('application_list'), {
'search': 'bob@example.com' 'search': 'bob@example.com'
}) })
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Bob Johnson') self.assertContains(response, 'Bob Johnson')
# Test filter by job # Test filter by job
response = self.client.get(reverse('candidate_list'), { response = self.client.get(reverse('application_list'), {
'job': self.job.slug 'job': self.job.slug
}) })
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Test filter by stage # Test filter by stage
response = self.client.get(reverse('candidate_list'), { response = self.client.get(reverse('application_list'), {
'stage': 'Exam' 'stage': 'Exam'
}) })
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -521,7 +521,7 @@ class AdvancedViewTests(TestCase):
"""Test HTMX responses for partial updates""" """Test HTMX responses for partial updates"""
# Test HTMX request for candidate screening # Test HTMX request for candidate screening
response = self.client.get( 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' HTTP_HX_REQUEST='true'
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -557,7 +557,7 @@ class AdvancedViewTests(TestCase):
# This would be tested via a form submission # This would be tested via a form submission
# For now, we test the view logic directly # For now, we test the view logic directly
request = self.client.post( 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'} data={'candidate_ids': application_ids, 'mark_as': 'Exam'}
) )
# Should redirect back to the view # Should redirect back to the view
@ -954,7 +954,7 @@ class AdvancedIntegrationTests(TransactionTestCase):
) )
response = self.client.post( response = self.client.post(
reverse('api_schedule_candidate_meeting', reverse('api_schedule_application_meeting',
kwargs={'job_slug': job.slug, 'candidate_pk': application.pk}), kwargs={'job_slug': job.slug, 'candidate_pk': application.pk}),
data={ data={
'start_time': (timezone.now() + timedelta(hours=1)).isoformat(), 'start_time': (timezone.now() + timedelta(hours=1)).isoformat(),

View File

@ -41,52 +41,52 @@ urlpatterns = [
# Candidate URLs # Candidate URLs
path( path(
"candidates/", views_frontend.ApplicationListView.as_view(), name="candidate_list" "applications/", views_frontend.ApplicationListView.as_view(), name="application_list"
), ),
path( path(
"candidates/create/", "application/create/",
views_frontend.ApplicationCreateView.as_view(), views_frontend.ApplicationCreateView.as_view(),
name="candidate_create", name="application_create",
), ),
path( path(
"candidates/create/<slug:slug>/", "application/create/<slug:slug>/",
views_frontend.ApplicationCreateView.as_view(), views_frontend.ApplicationCreateView.as_view(),
name="candidate_create_for_job", name="application_create_for_job",
), ),
path( path(
"jobs/<slug:slug>/candidates/", "jobs/<slug:slug>/application/",
views_frontend.JobApplicationListView.as_view(), views_frontend.JobApplicationListView.as_view(),
name="job_candidates_list", name="job_applications_list",
), ),
path( path(
"candidates/<slug:slug>/update/", "applications/<slug:slug>/update/",
views_frontend.ApplicationUpdateView.as_view(), views_frontend.ApplicationUpdateView.as_view(),
name="candidate_update", name="application_update",
), ),
path( path(
"candidates/<slug:slug>/delete/", "application/<slug:slug>/delete/",
views_frontend.ApplicationDeleteView.as_view(), views_frontend.ApplicationDeleteView.as_view(),
name="candidate_delete", name="application_delete",
), ),
path( path(
"candidate/<slug:slug>/view/", "application/<slug:slug>/view/",
views_frontend.candidate_detail, views_frontend.application_detail,
name="candidate_detail", name="application_detail",
), ),
path( path(
"candidate/<slug:slug>/resume-template/", "application/<slug:slug>/resume-template/",
views_frontend.candidate_resume_template_view, views_frontend.application_resume_template_view,
name="candidate_resume_template", name="application_resume_template",
), ),
path( path(
"candidate/<slug:slug>/update-stage/", "application/<slug:slug>/update-stage/",
views_frontend.candidate_update_stage, views_frontend.application_update_stage,
name="candidate_update_stage", name="application_update_stage",
), ),
path( path(
"candidate/<slug:slug>/retry-scoring/", "application/<slug:slug>/retry-scoring/",
views_frontend.retry_scoring_view, views_frontend.retry_scoring_view,
name="candidate_retry_scoring", name="application_retry_scoring",
), ),
# Training URLs # Training URLs
path("training/", views_frontend.TrainingListView.as_view(), name="training_list"), path("training/", views_frontend.TrainingListView.as_view(), name="training_list"),
@ -155,85 +155,87 @@ urlpatterns = [
name="edit_linkedin_post_content", name="edit_linkedin_post_content",
), ),
path( path(
"jobs/<slug:slug>/candidate_screening_view/", "jobs/<slug:slug>/applications_screening_view/",
views.candidate_screening_view, views.applications_screening_view,
name="candidate_screening_view", name="applications_screening_view",
), ),
path( path(
"jobs/<slug:slug>/candidate_exam_view/", "jobs/<slug:slug>/applications_exam_view/",
views.candidate_exam_view, views.applications_exam_view,
name="candidate_exam_view", name="applications_exam_view",
), ),
path( path(
"jobs/<slug:slug>/candidate_interview_view/", "jobs/<slug:slug>/applications_interview_view/",
views.candidate_interview_view, views.applications_interview_view,
name="candidate_interview_view", name="applications_interview_view",
), ),
path( path(
"jobs/<slug:slug>/candidate_document_review_view/", "jobs/<slug:slug>/applications_document_review_view/",
views.candidate_document_review_view, views.applications_document_review_view,
name="candidate_document_review_view", name="applications_document_review_view",
), ),
path( path(
"jobs/<slug:slug>/candidate_offer_view/", "jobs/<slug:slug>/applications_offer_view/",
views_frontend.candidate_offer_view, views_frontend.applications_offer_view,
name="candidate_offer_view", name="applications_offer_view",
), ),
path( path(
"jobs/<slug:slug>/candidate_hired_view/", "jobs/<slug:slug>/applications_hired_view/",
views_frontend.candidate_hired_view, views_frontend.applications_hired_view,
name="candidate_hired_view", name="applications_hired_view",
), ),
path( path(
"jobs/<slug:job_slug>/export/<str:stage>/csv/", "jobs/<slug:job_slug>/export/<str:stage>/csv/",
views_frontend.export_candidates_csv, views_frontend.export_applications_csv,
name="export_candidates_csv", name="export_applications_csv",
), ),
path( path(
"jobs/<slug:job_slug>/candidates/<slug:candidate_slug>/update_status/<str:stage_type>/<str:status>/", "jobs/<slug:job_slug>/application/<slug:application_slug>/update_status/<str:stage_type>/<str:status>/",
views_frontend.update_candidate_status, views_frontend.update_application_status,
name="update_candidate_status", name="update_application_status",
), ),
# Sync URLs # Sync URLs (check)
path( path(
"jobs/<slug:job_slug>/sync-hired-candidates/", "jobs/<slug:job_slug>/sync-hired-applications/",
views_frontend.sync_hired_candidates, views_frontend.sync_hired_applications,
name="sync_hired_candidates", name="sync_hired_applications",
), ),
path( path(
"sources/<int:source_id>/test-connection/", "sources/<int:source_id>/test-connection/",
views_frontend.test_source_connection, views_frontend.test_source_connection,
name="test_source_connection", name="test_source_connection",
), ),
path( path(
"jobs/<slug:slug>/<int:candidate_id>/reschedule_meeting_for_candidate/<int:meeting_id>/", "jobs/<slug:slug>/<int:application_id>/reschedule_meeting_for_application/<int:meeting_id>/",
views.reschedule_meeting_for_candidate, views.reschedule_meeting_for_application,
name="reschedule_meeting_for_candidate", name="reschedule_meeting_for_application",
), ),
path( path(
"jobs/<slug:slug>/update_candidate_exam_status/", "jobs/<slug:slug>/update_application_exam_status/",
views.update_candidate_exam_status, views.update_application_exam_status,
name="update_candidate_exam_status", name="update_application_exam_status",
), ),
path( path(
"jobs/<slug:slug>/bulk_update_candidate_exam_status/", "jobs/<slug:slug>/bulk_update_application_exam_status/",
views.bulk_update_candidate_exam_status, views.bulk_update_application_exam_status,
name="bulk_update_candidate_exam_status", name="bulk_update_application_exam_status",
), ),
path( path(
"htmx/<int:pk>/candidate_criteria_view/", "htmx/<int:pk>/application_criteria_view/",
views.candidate_criteria_view_htmx, views.application_criteria_view_htmx,
name="candidate_criteria_view_htmx", name="application_criteria_view_htmx",
), ),
path( path(
"htmx/<slug:slug>/candidate_set_exam_date/", "htmx/<slug:slug>/application_set_exam_date/",
views.candidate_set_exam_date, views.application_set_exam_date,
name="candidate_set_exam_date", name="application_set_exam_date",
), ),
path( path(
"htmx/<slug:slug>/candidate_update_status/", "htmx/<slug:slug>/application_update_status/",
views.candidate_update_status, views.application_update_status,
name="candidate_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>/submit/', views.submit_form, name='submit_form'),
# path('forms/form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'), # path('forms/form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
@ -347,9 +349,9 @@ urlpatterns = [
name="delete_meeting_comment", name="delete_meeting_comment",
), ),
path( path(
"meetings/<slug:slug>/set_meeting_candidate/", "meetings/<slug:slug>/set_meeting_application/",
views.set_meeting_candidate, views.set_meeting_application,
name="set_meeting_candidate", name="set_meeting_application",
), ),
# Hiring Agency URLs # Hiring Agency URLs
path("agencies/", views.agency_list, name="agency_list"), path("agencies/", views.agency_list, name="agency_list"),
@ -357,10 +359,10 @@ urlpatterns = [
path("agencies/<slug:slug>/", views.agency_detail, name="agency_detail"), 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>/update/", views.agency_update, name="agency_update"),
path("agencies/<slug:slug>/delete/", views.agency_delete, name="agency_delete"), path("agencies/<slug:slug>/delete/", views.agency_delete, name="agency_delete"),
path( path( #check the html of this url it is not used anywhere
"agencies/<slug:slug>/candidates/", "agencies/<slug:slug>/applications/",
views.agency_candidates, views.agency_applications,
name="agency_candidates", name="agency_applications",
), ),
# path('agencies/<slug:slug>/send-message/', views.agency_detail_send_message, name='agency_detail_send_message'), # path('agencies/<slug:slug>/send-message/', views.agency_detail_send_message, name='agency_detail_send_message'),
# Agency Assignment Management URLs # Agency Assignment Management URLs
@ -369,12 +371,12 @@ urlpatterns = [
views.agency_assignment_list, views.agency_assignment_list,
name="agency_assignment_list", name="agency_assignment_list",
), ),
path( path( #check
"agency-assignments/create/", "agency-assignments/create/",
views.agency_assignment_create, views.agency_assignment_create,
name="agency_assignment_create", name="agency_assignment_create",
), ),
path( path(#check
"agency-assignments/<slug:slug>/create/", "agency-assignments/<slug:slug>/create/",
views.agency_assignment_create, views.agency_assignment_create,
name="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>/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'), # path('admin/messages/<int:message_id>/delete/', views.admin_delete_message, name='admin_delete_message'),
# Agency Portal URLs (for external agencies) # 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/<int:pk>/reset/", views.portal_password_reset, name="portal_password_reset"),
path( path(
"portal/dashboard/", "portal/dashboard/",
@ -433,19 +435,19 @@ urlpatterns = [
# Unified Portal URLs # Unified Portal URLs
path("login/", views.portal_login, name="portal_login"), path("login/", views.portal_login, name="portal_login"),
path( path(
"candidate/dashboard/", "applicant/dashboard/",
views.candidate_portal_dashboard, views.applicant_portal_dashboard,
name="candidate_portal_dashboard", name="applicant_portal_dashboard",
), ),
path( path(
"candidate/applications/<slug:slug>/", "applications/applications/<slug:slug>/",
views.candidate_application_detail, views.applicant_application_detail,
name="candidate_application_detail", name="applicant_application_detail",
), ),
# path( # path(
# "candidate/<slug:application_slug>/applications/<slug:person_slug>/detail/<slug:agency_slug>/", # "candidate/<slug:application_slug>/applications/<slug:person_slug>/detail/<slug:agency_slug>/",
# views.candidate_application_detail, # views.applicant_application_detail,
# name="candidate_application_detail", # name="applicant_application_detail",
# ), # ),
path( path(
"portal/dashboard/", "portal/dashboard/",
@ -463,35 +465,35 @@ urlpatterns = [
name="agency_portal_assignment_detail", name="agency_portal_assignment_detail",
), ),
path( path(
"portal/assignment/<slug:slug>/submit-candidate/", "portal/assignment/<slug:slug>/submit-application/",
views.agency_portal_submit_candidate_page, views.agency_portal_submit_application_page,
name="agency_portal_submit_candidate_page", name="agency_portal_submit_application_page",
), ),
path( path(
"portal/submit-candidate/", "portal/submit-application/",
views.agency_portal_submit_candidate, views.agency_portal_submit_application,
name="agency_portal_submit_candidate", name="agency_portal_submit_application",
), ),
path("portal/logout/", views.portal_logout, name="portal_logout"), path("portal/logout/", views.portal_logout, name="portal_logout"),
# Agency Portal Candidate Management URLs # Agency Portal Candidate Management URLs
path( path(
"portal/candidates/<int:candidate_id>/edit/", "portal/applications/<int:application_id>/edit/",
views.agency_portal_edit_candidate, views.agency_portal_edit_application,
name="agency_portal_edit_candidate", name="agency_portal_edit_application",
), ),
path( path(
"portal/candidates/<int:candidate_id>/delete/", "portal/applications/<int:application_id>/delete/",
views.agency_portal_delete_candidate, views.agency_portal_delete_application,
name="agency_portal_delete_candidate", name="agency_portal_delete_application",
), ),
# API URLs for messaging (removed) # 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>/', 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'), # 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 # API URLs for candidate management
path( path(
"api/candidate/<int:candidate_id>/", "api/application/<int:application_id>/",
views.api_candidate_detail, views.api_application_detail,
name="api_candidate_detail", name="api_application_detail",
), ),
# # Admin Notification API # # Admin Notification API
# path('api/admin/notification-count/', views.api_notification_count, name='admin_notification_count'), # path('api/admin/notification-count/', views.api_notification_count, name='admin_notification_count'),
@ -535,10 +537,11 @@ urlpatterns = [
), ),
# Email composition URLs # Email composition URLs
path( path(
"jobs/<slug:job_slug>/candidates/compose-email/", "jobs/<slug:job_slug>/applications/compose-email/",
views.compose_candidate_email, views.compose_application_email,
name="compose_candidate_email", name="compose_application_email",
), ),
# Message URLs # Message URLs
path("messages/", views.message_list, name="message_list"), path("messages/", views.message_list, name="message_list"),
path("messages/create/", views.message_create, name="message_create"), 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>/delete/", views.document_delete, name="document_delete"),
path("documents/<int:document_id>/download/", views.document_download, name="document_download"), path("documents/<int:document_id>/download/", views.document_download, name="document_download"),
# Candidate Document Management URLs # Candidate Document Management URLs
path("candidate/documents/upload/<slug:slug>/", views.document_upload, name="candidate_document_upload"), path("application/documents/upload/<slug:slug>/", views.document_upload, name="application_document_upload"),
path("candidate/documents/<int:document_id>/delete/", views.document_delete, name="candidate_document_delete"), path("application/documents/<int:document_id>/delete/", views.document_delete, name="application_document_delete"),
path("candidate/documents/<int:document_id>/download/", views.document_download, name="candidate_document_download"), path("application/documents/<int:document_id>/download/", views.document_download, name="application_document_download"),
path('jobs/<slug:job_slug>/candidates/compose_email/', views.compose_candidate_email, name='compose_candidate_email'), path('jobs/<slug:job_slug>/applications/compose_email/', views.compose_application_email, name='compose_application_email'),
path('interview/partcipants/<slug:slug>/',views.create_interview_participants,name='create_interview_participants'), path('interview/partcipants/<slug:slug>/',views.create_interview_participants,name='create_interview_participants'),
path('interview/email/<slug:slug>/',views.send_interview_email,name='send_interview_email'), path('interview/email/<slug:slug>/',views.send_interview_email,name='send_interview_email'),
# Candidate Signup # Candidate Signup
path('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 # Password Reset
path('user/<int:pk>/password-reset/', views.portal_password_reset, name='portal_password_reset'), path('user/<int:pk>/password-reset/', views.portal_password_reset, name='portal_password_reset'),
@ -607,43 +610,43 @@ urlpatterns = [
), ),
# Candidate Meeting Scheduling/Rescheduling URLs # Candidate Meeting Scheduling/Rescheduling URLs
path( path(
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/", "jobs/<slug:job_slug>/applications/<int:application_pk>/schedule-meeting/",
views.schedule_candidate_meeting, views.schedule_application_meeting,
name="schedule_candidate_meeting", name="schedule_application_meeting",
), ),
path( path(
"api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/", "api/jobs/<slug:job_slug>/applications/<int:application_pk>/schedule-meeting/",
views.api_schedule_candidate_meeting, views.api_schedule_application_meeting,
name="api_schedule_candidate_meeting", name="api_schedule_application_meeting",
), ),
path( path(
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/", "jobs/<slug:job_slug>/applications/<int:application_pk>/reschedule-meeting/<int:interview_pk>/",
views.reschedule_candidate_meeting, views.reschedule_application_meeting,
name="reschedule_candidate_meeting", name="reschedule_application_meeting",
), ),
path( path(
"api/jobs/<slug:job_slug>/candidates/<int:candidate_pk>/reschedule-meeting/<int:interview_pk>/", "api/jobs/<slug:job_slug>/applications/<int:application_pk>/reschedule-meeting/<int:interview_pk>/",
views.api_reschedule_candidate_meeting, views.api_reschedule_application_meeting,
name="api_reschedule_candidate_meeting", name="api_reschedule_application_meeting",
), ),
# New URL for simple page-based meeting scheduling # New URL for simple page-based meeting scheduling
path( path(
"jobs/<slug:slug>/candidates/<int:candidate_pk>/schedule-meeting-page/", "jobs/<slug:slug>/applications/<int:application_pk>/schedule-meeting-page/",
views.schedule_meeting_for_candidate, views.schedule_meeting_for_application,
name="schedule_meeting_for_candidate", name="schedule_meeting_for_application",
),
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",
), ),
# 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"), path("interviews/meetings/", views.MeetingListView.as_view(), name="list_meetings"),
# 1. Onsite Reschedule URL # 1. Onsite Reschedule URL
path( 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, views.reschedule_onsite_meeting,
name='reschedule_onsite_meeting' name='reschedule_onsite_meeting'
), ),
@ -651,15 +654,15 @@ urlpatterns = [
# 2. Onsite Delete URL # 2. Onsite Delete URL
path( path(
'job/<slug:slug>/candidates/<int:candidate_pk>/delete-onsite-meeting/<int:meeting_id>/', 'job/<slug:slug>/applications/<int:application_pk>/delete-onsite-meeting/<int:meeting_id>/',
views.delete_onsite_meeting_for_candidate, views.delete_onsite_meeting_for_application,
name='delete_onsite_meeting_for_candidate' name='delete_onsite_meeting_for_application'
), ),
path( path(
'job/<slug:slug>/candidate/<int:candidate_pk>/schedule/onsite/', 'job/<slug:slug>/application/<int:application_pk>/schedule/onsite/',
views.schedule_onsite_meeting_for_candidate, views.schedule_onsite_meeting_for_application,
name='schedule_onsite_meeting_for_candidate' # This is the name used in the button 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"), path("interviews/meetings/<slug:slug>/", views.meeting_details, name="meeting_details"),
# Email invitation URLs # 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"), 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 return markdown
def get_candidates_from_request(request): def get_applications_from_request(request):
for c in request.POST.items(): for c in request.POST.items():
try: try:
yield models.Candidate.objects.get(pk=c[0]) yield models.Application.objects.get(pk=c[0])
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
yield None 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.shortcuts import render, get_object_or_404,redirect
from django.contrib import messages from django.contrib import messages
from django.http import JsonResponse, HttpResponse 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 recruitment.utils import json_to_markdown_table
from django.db.models import Count, Avg, F, FloatField from django.db.models import Count, Avg, F, FloatField
from django.db.models.functions import Cast from django.db.models.functions import Cast
@ -25,14 +25,14 @@ from django.urls import reverse_lazy
from django.db.models import FloatField 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 import F, IntegerField, Count, Avg, Sum, Q, ExpressionWrapper, fields, Value,CharField
from django.db.models.functions import Cast, Coalesce, TruncDate from django.db.models.functions import Cast, Coalesce, TruncDate
from django.contrib.auth.decorators import login_required
from django.shortcuts import render from django.shortcuts import render
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
import json import json
from django.utils.translation import gettext_lazy as _
# Add imports for user type restrictions # 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 ( from datastar_py.django import (
@ -91,7 +91,7 @@ class JobUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin,
form_class = forms.JobPostingForm form_class = forms.JobPostingForm
template_name = 'jobs/edit_job.html' template_name = 'jobs/edit_job.html'
success_url = reverse_lazy('job_list') success_url = reverse_lazy('job_list')
success_message = 'Job updated successfully.' success_message = _('Job updated successfully.')
slug_url_kwarg = 'slug' slug_url_kwarg = 'slug'
@ -99,12 +99,12 @@ class JobDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin,
model = models.JobPosting model = models.JobPosting
template_name = 'jobs/partials/delete_modal.html' template_name = 'jobs/partials/delete_modal.html'
success_url = reverse_lazy('job_list') success_url = reverse_lazy('job_list')
success_message = 'Job deleted successfully.' success_message = _('Job deleted successfully.')
slug_url_kwarg = 'slug' slug_url_kwarg = 'slug'
class JobApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView): class JobApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.Application model = models.Application
template_name = 'jobs/job_candidates_list.html' template_name = 'jobs/job_applications_list.html'
context_object_name = 'applications' context_object_name = 'applications'
paginate_by = 10 paginate_by = 10
@ -146,7 +146,7 @@ class JobApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView): class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.Application model = models.Application
template_name = 'recruitment/candidate_list.html' template_name = 'recruitment/applications_list.html'
context_object_name = 'applications' context_object_name = 'applications'
paginate_by = 100 paginate_by = 100
@ -186,9 +186,9 @@ class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView): class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView):
model = models.Application model = models.Application
form_class = forms.ApplicationForm form_class = forms.ApplicationForm
template_name = 'recruitment/candidate_create.html' template_name = 'recruitment/application_create.html'
success_url = reverse_lazy('candidate_list') success_url = reverse_lazy('application_list')
success_message = 'Candidate created successfully.' success_message = _('Application created successfully.')
def get_initial(self): def get_initial(self):
initial = super().get_initial() initial = super().get_initial()
@ -215,17 +215,17 @@ class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessa
class ApplicationUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView): class ApplicationUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView):
model = models.Application model = models.Application
form_class = forms.ApplicationForm form_class = forms.ApplicationForm
template_name = 'recruitment/candidate_update.html' template_name = 'recruitment/application_update.html'
success_url = reverse_lazy('candidate_list') success_url = reverse_lazy('application_list')
success_message = 'Candidate updated successfully.' success_message = _('Application updated successfully.')
slug_url_kwarg = 'slug' slug_url_kwarg = 'slug'
class ApplicationDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView): class ApplicationDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView):
model = models.Application model = models.Application
template_name = 'recruitment/candidate_delete.html' template_name = 'recruitment/application_delete.html'
success_url = reverse_lazy('candidate_list') success_url = reverse_lazy('application_list')
success_message = 'Candidate deleted successfully.' success_message = _('Application deleted successfully.')
slug_url_kwarg = 'slug' slug_url_kwarg = 'slug'
@ -235,12 +235,12 @@ def retry_scoring_view(request,slug):
application = get_object_or_404(models.Application, slug=slug) application = get_object_or_404(models.Application, slug=slug)
async_task( async_task(
'recruitment.tasks.handle_reume_parsing_and_scoring', 'recruitment.tasks.handle_resume_parsing_and_scoring',
application.pk, application.pk,
hook='recruitment.hooks.callback_ai_parsing', hook='recruitment.hooks.callback_ai_parsing',
sync=True, 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 @login_required
@staff_user_required @staff_user_required
def candidate_detail(request, slug): def application_detail(request, slug):
from rich.json import JSON from rich.json import JSON
candidate = get_object_or_404(models.Application, slug=slug) application = get_object_or_404(models.Application, slug=slug)
try: try:
parsed = ast.literal_eval(candidate.parsed_summary) parsed = ast.literal_eval(application.parsed_summary)
except: except:
parsed = {} parsed = {}
@ -270,8 +270,8 @@ def candidate_detail(request, slug):
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False) # parsed = JSON(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]) # parsed = json_to_markdown_table([parsed])
return render(request, 'recruitment/candidate_detail.html', { return render(request, 'recruitment/application_detail.html', {
'candidate': candidate, 'application': application,
'parsed': parsed, 'parsed': parsed,
'stage_form': stage_form, 'stage_form': stage_form,
}) })
@ -279,21 +279,21 @@ def candidate_detail(request, slug):
@login_required @login_required
@staff_user_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""" """Display formatted resume template for a candidate"""
application = get_object_or_404(models.Application, slug=slug) application = get_object_or_404(models.Application, slug=slug)
if not request.user.is_staff: if not request.user.is_staff:
messages.error(request, _("You don't have permission to view this page.")) 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 'application': application
}) })
@login_required @login_required
@staff_user_required @staff_user_required
def candidate_update_stage(request, slug): def application_update_stage(request, slug):
"""Handle HTMX stage update requests""" """Handle HTMX stage update requests"""
application = get_object_or_404(models.Application, slug=slug) application = get_object_or_404(models.Application, slug=slug)
form = forms.ApplicationStageForm(request.POST, instance=application) form = forms.ApplicationStageForm(request.POST, instance=application)
@ -301,8 +301,8 @@ def candidate_update_stage(request, slug):
stage_value = form.cleaned_data['stage'] stage_value = form.cleaned_data['stage']
application.stage = stage_value application.stage = stage_value
application.save(update_fields=['stage']) application.save(update_fields=['stage'])
messages.success(request,"application Stage Updated") messages.success(request,_("application Stage Updated"))
return redirect("candidate_detail",slug=application.slug) return redirect("application_detail",slug=application.slug)
class TrainingListView(LoginRequiredMixin, StaffRequiredMixin, ListView): class TrainingListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = models.TrainingMaterial model = models.TrainingMaterial
@ -386,7 +386,7 @@ def dashboard_view(request):
# --- 1. BASE QUERYSETS & GLOBAL METRICS (UNFILTERED) --- # --- 1. BASE QUERYSETS & GLOBAL METRICS (UNFILTERED) ---
all_jobs_queryset = models.JobPosting.objects.all().order_by('-created_at') 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 # Global KPI Card Metrics
total_jobs_global = all_jobs_queryset.count() total_jobs_global = all_jobs_queryset.count()
@ -400,7 +400,7 @@ def dashboard_view(request):
# --- 2. TIME SERIES: GLOBAL DAILY APPLICANTS --- # --- 2. TIME SERIES: GLOBAL DAILY APPLICANTS ---
# Group ALL candidates by creation date # Group ALL candidates by creation date
global_daily_applications_qs = all_candidates_queryset.annotate( global_daily_applications_qs = all_applications_queryset.annotate(
date=TruncDate('created_at') date=TruncDate('created_at')
).values('date').annotate( ).values('date').annotate(
count=Count('pk') count=Count('pk')
@ -412,14 +412,14 @@ def dashboard_view(request):
# --- 3. FILTERING LOGIC: Determine the scope for scoped metrics --- # --- 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 job_scope_queryset = all_jobs_queryset
interview_queryset = models.ScheduledInterview.objects.all() interview_queryset = models.ScheduledInterview.objects.all()
current_job = None current_job = None
if selected_job_pk: if selected_job_pk:
# Filter all base querysets # 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) interview_queryset = interview_queryset.filter(job__pk=selected_job_pk)
try: try:
@ -434,7 +434,7 @@ def dashboard_view(request):
scoped_dates = [] scoped_dates = []
scoped_counts = [] scoped_counts = []
if selected_job_pk: if selected_job_pk:
scoped_daily_applications_qs = candidate_queryset.annotate( scoped_daily_applications_qs = application_queryset.annotate(
date=TruncDate('created_at') date=TruncDate('created_at')
).values('date').annotate( ).values('date').annotate(
count=Count('pk') count=Count('pk')
@ -446,17 +446,27 @@ def dashboard_view(request):
# --- 5. SCOPED CORE AGGREGATIONS (FILTERED OR ALL) --- # --- 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 score_expression = Cast(
).annotate( Coalesce(
annotated_match_score=Coalesce( KeyTextTransform(
Cast(SCORE_PATH, output_field=IntegerField()), 'match_score',
0 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( # safe_match_score_cast = Cast(
# # 3. If the result after stripping quotes is an empty string (''), convert it to NULL. # # 3. If the result after stripping quotes is an empty string (''), convert it to NULL.
# NullIf( # NullIf(
@ -483,24 +493,24 @@ def dashboard_view(request):
# A. Pipeline & Volume Metrics (Scoped) # A. Pipeline & Volume Metrics (Scoped)
total_active_jobs = job_scope_queryset.filter(status="ACTIVE").count() total_active_jobs = job_scope_queryset.filter(status="ACTIVE").count()
last_week = timezone.now() - timedelta(days=7) 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')) 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 total_open_positions = open_positions_agg['total_open'] or 0
average_applications_result = job_scope_queryset.annotate( average_applications_result = job_scope_queryset.annotate(
candidate_count=Count('applications', distinct=True) applications_count=Count('applications', distinct=True)
).aggregate(avg_apps=Avg('candidate_count'))['avg_apps'] ).aggregate(avg_apps=Avg('applications_count'))['avg_apps']
average_applications = round(average_applications_result or 0, 2) average_applications = round(average_applications_result or 0, 2)
# B. Efficiency & Conversion Metrics (Scoped) # B. Efficiency & Conversion Metrics (Scoped)
hired_candidates = candidate_queryset.filter( hired_applications = application_queryset.filter(
stage='Hired' 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( time_diff=ExpressionWrapper(
F('join_date') - F('created_at__date'), F('join_date') - F('created_at__date'),
output_field=fields.DurationField() output_field=fields.DurationField()
@ -517,11 +527,11 @@ def dashboard_view(request):
) )
print(avg_time_to_hire_days) print(avg_time_to_hire_days)
applied_count = candidate_queryset.filter(stage='Applied').count() applied_count = application_queryset.filter(stage='Applied').count()
advanced_count = candidate_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).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 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_extended_count = application_queryset.filter(stage='Offer').count()
offers_accepted_count = candidate_queryset.filter(offer_status='Accepted').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 offers_accepted_rate = round( (offers_accepted_count / offers_extended_count) * 100, 1 ) if offers_extended_count > 0 else 0
filled_positions = offers_accepted_count filled_positions = offers_accepted_count
vacancy_fill_rate = round( (filled_positions / total_open_positions) * 100, 1 ) if total_open_positions > 0 else 0 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( meetings_scheduled_this_week = interview_queryset.filter(
interview_date__week=current_week, interview_date__year=current_year interview_date__week=current_week, interview_date__year=current_year
).count() ).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) 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_count = applications_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 high_potential_ratio = round( (high_potential_count / total_applications) * 100, 1 ) if total_applications > 0 else 0
total_scored_candidates = candidates_with_score_query.count() total_scored_candidates = applications_with_score_query.count()
scored_ratio = round( (total_scored_candidates / total_candidates) * 100, 1 ) if total_candidates > 0 else 0 scored_ratio = round( (total_scored_candidates / total_applications) * 100, 1 ) if total_applications > 0 else 0
# --- 6. CHART DATA PREPARATION --- # --- 6. CHART DATA PREPARATION ---
# A. Pipeline Funnel (Scoped) # 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} stage_map = {item['stage']: item['count'] for item in stage_counts}
candidate_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired'] application_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired']
candidates_count = [ application_count = [
stage_map.get('Applied', 0), stage_map.get('Exam', 0), stage_map.get('Interview', 0), 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) 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 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} 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), 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 # Scoped KPIs
'total_active_jobs': total_active_jobs, 'total_active_jobs': total_active_jobs,
'total_candidates': total_candidates, 'total_applications': total_applications,
'new_candidates_7days': new_candidates_7days, 'new_applications_7days': new_applications_7days,
'total_open_positions': total_open_positions, 'total_open_positions': total_open_positions,
'average_applications': average_applications, 'average_applications': average_applications,
'avg_time_to_hire_days': avg_time_to_hire_days, 'avg_time_to_hire_days': avg_time_to_hire_days,
@ -594,8 +604,8 @@ def dashboard_view(request):
'scored_ratio': scored_ratio, 'scored_ratio': scored_ratio,
# Chart Data # Chart Data
'candidate_stage': json.dumps(candidate_stage), 'application_stage': json.dumps(application_stage),
'candidates_count': json.dumps(candidates_count), 'application_count': json.dumps(application_count),
'job_titles': json.dumps(job_titles), 'job_titles': json.dumps(job_titles),
'job_app_counts': json.dumps(job_app_counts), 'job_app_counts': json.dumps(job_app_counts),
# 'source_volume_chart_data' is intentionally REMOVED # 'source_volume_chart_data' is intentionally REMOVED
@ -618,7 +628,7 @@ def dashboard_view(request):
'current_job': current_job, '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), 'all_hiring_sources': json.dumps(all_hiring_sources),
} }
@ -627,100 +637,100 @@ def dashboard_view(request):
@login_required @login_required
@staff_user_required @staff_user_required
def candidate_offer_view(request, slug): def applications_offer_view(request, slug):
"""View for candidates in the Offer stage""" """View for candidates in the Offer stage"""
job = get_object_or_404(models.JobPosting, slug=slug) job = get_object_or_404(models.JobPosting, slug=slug)
# Filter candidates for this specific job and stage # Filter candidates for this specific job and stage
candidates = job.offer_candidates applications = job.offer_applications
# Handle search # Handle search
search_query = request.GET.get('search', '') search_query = request.GET.get('search', '')
if search_query: if search_query:
candidates = candidates.filter( applications = applications.filter(
Q(first_name__icontains=search_query) | Q(first_name__icontains=search_query) |
Q(last_name__icontains=search_query) | Q(last_name__icontains=search_query) |
Q(email__icontains=search_query) | Q(email__icontains=search_query) |
Q(phone__icontains=search_query) Q(phone__icontains=search_query)
) )
candidates = candidates.order_by('-created_at') applications = applications.order_by('-created_at')
context = { context = {
'job': job, 'job': job,
'candidates': candidates, 'applications': applications,
'search_query': search_query, 'search_query': search_query,
'current_stage': 'Offer', 'current_stage': 'Offer',
} }
return render(request, 'recruitment/candidate_offer_view.html', context) return render(request, 'recruitment/applications_offer_view.html', context)
@login_required @login_required
@staff_user_required @staff_user_required
def candidate_hired_view(request, slug): def applications_hired_view(request, slug):
"""View for hired candidates""" """View for hired applications"""
job = get_object_or_404(models.JobPosting, slug=slug) job = get_object_or_404(models.JobPosting, slug=slug)
# Filter candidates with offer_status = 'Accepted' # Filter applications with offer_status = 'Accepted'
candidates = job.hired_candidates applications = job.hired_applications
# Handle search # Handle search
search_query = request.GET.get('search', '') search_query = request.GET.get('search', '')
if search_query: if search_query:
candidates = candidates.filter( applications = applications.filter(
Q(first_name__icontains=search_query) | Q(first_name__icontains=search_query) |
Q(last_name__icontains=search_query) | Q(last_name__icontains=search_query) |
Q(email__icontains=search_query) | Q(email__icontains=search_query) |
Q(phone__icontains=search_query) Q(phone__icontains=search_query)
) )
candidates = candidates.order_by('-created_at') applications = applications.order_by('-created_at')
context = { context = {
'job': job, 'job': job,
'candidates': candidates, 'applications': applications,
'search_query': search_query, 'search_query': search_query,
'current_stage': 'Hired', 'current_stage': 'Hired',
} }
return render(request, 'recruitment/candidate_hired_view.html', context) return render(request, 'recruitment/applications_hired_view.html', context)
@login_required @login_required
@staff_user_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""" """Handle exam/interview/offer status updates"""
from django.utils import timezone from django.utils import timezone
job = get_object_or_404(models.JobPosting, slug=job_slug) 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 request.method == "POST":
if stage_type == 'exam': if stage_type == 'exam':
status = request.POST.get("exam_status") status = request.POST.get("exam_status")
score = request.POST.get("exam_score") score = request.POST.get("exam_score")
candidate.exam_status = status application.exam_status = status
candidate.exam_score = score application.exam_score = score
candidate.exam_date = timezone.now() application.exam_date = timezone.now()
candidate.save(update_fields=['exam_status','exam_score', 'exam_date']) application.save(update_fields=['exam_status','exam_score', 'exam_date'])
return render(request,'recruitment/partials/exam-results.html',{'candidate':candidate,'job':job}) return render(request,'recruitment/partials/exam-results.html',{'application':application,'job':job})
elif stage_type == 'interview': elif stage_type == 'interview':
candidate.interview_status = status application.interview_status = status
candidate.interview_date = timezone.now() application.interview_date = timezone.now()
candidate.save(update_fields=['interview_status', 'interview_date']) application.save(update_fields=['interview_status', 'interview_date'])
return render(request,'recruitment/partials/interview-results.html',{'candidate':candidate,'job':job}) return render(request,'recruitment/partials/interview-results.html',{'application':application,'job':job})
elif stage_type == 'offer': elif stage_type == 'offer':
candidate.offer_status = status application.offer_status = status
candidate.offer_date = timezone.now() application.offer_date = timezone.now()
candidate.save(update_fields=['offer_status', 'offer_date']) application.save(update_fields=['offer_status', 'offer_date'])
return render(request,'recruitment/partials/offer-results.html',{'candidate':candidate,'job':job}) return render(request,'recruitment/partials/offer-results.html',{'application':application,'job':job})
return redirect('candidate_detail', candidate.slug) return redirect('application_detail', application.slug)
else: else:
if stage_type == 'exam': 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': 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': 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 # Stage configuration for CSV export
@ -755,8 +765,8 @@ STAGE_CONFIG = {
@login_required @login_required
@staff_user_required @staff_user_required
def export_candidates_csv(request, job_slug, stage): def export_applications_csv(request, job_slug, stage):
"""Export candidates for a specific stage as CSV""" """Export applications for a specific stage as CSV"""
job = get_object_or_404(models.JobPosting, slug=job_slug) job = get_object_or_404(models.JobPosting, slug=job_slug)
# Validate stage # Validate stage
@ -766,23 +776,23 @@ def export_candidates_csv(request, job_slug, stage):
config = STAGE_CONFIG[stage] config = STAGE_CONFIG[stage]
# Filter candidates based on stage # Filter applications based on stage
if stage == 'hired': if stage == 'hired':
candidates = job.applications.filter(**config['filter']) applications = job.applications.filter(**config['filter'])
else: else:
candidates = job.applications.filter(**config['filter']) applications = job.applications.filter(**config['filter'])
# Handle search if provided # Handle search if provided
search_query = request.GET.get('search', '') search_query = request.GET.get('search', '')
if search_query: if search_query:
candidates = candidates.filter( applications = applications.filter(
Q(first_name__icontains=search_query) | Q(first_name__icontains=search_query) |
Q(last_name__icontains=search_query) | Q(last_name__icontains=search_query) |
Q(email__icontains=search_query) | Q(email__icontains=search_query) |
Q(phone__icontains=search_query) Q(phone__icontains=search_query)
) )
candidates = candidates.order_by('-created_at') applications = applications.order_by('-created_at')
# Create CSV response # Create CSV response
response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type='text/csv')
@ -799,87 +809,87 @@ def export_candidates_csv(request, job_slug, stage):
headers.extend(['Job Title', 'Department']) headers.extend(['Job Title', 'Department'])
writer.writerow(headers) writer.writerow(headers)
# Write candidate data # Write application data
for candidate in candidates: for application in applications:
row = [] row = []
# Extract data based on stage configuration # Extract data based on stage configuration
for field in config['fields']: for field in config['fields']:
if field == 'name': if field == 'name':
row.append(candidate.name) row.append(application.name)
elif field == 'email': elif field == 'email':
row.append(candidate.email) row.append(application.email)
elif field == 'phone': elif field == 'phone':
row.append(candidate.phone) row.append(application.phone)
elif field == 'created_at': 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': elif field == 'stage':
row.append(candidate.stage or '') row.append(application.stage or '')
elif field == 'exam_status': elif field == 'exam_status':
row.append(candidate.exam_status or '') row.append(application.exam_status or '')
elif field == 'exam_date': 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': elif field == 'interview_status':
row.append(candidate.interview_status or '') row.append(application.interview_status or '')
elif field == 'interview_date': 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': elif field == 'offer_status':
row.append(candidate.offer_status or '') row.append(application.offer_status or '')
elif field == 'offer_date': 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': elif field == 'ai_score':
# Extract AI score using model property # Extract AI score using model property
try: try:
score = candidate.match_score score = application.match_score
row.append(f"{score}%" if score else '') row.append(f"{score}%" if score else '')
except: except:
row.append('') row.append('')
elif field == 'years_experience': elif field == 'years_experience':
# Extract years of experience using model property # Extract years of experience using model property
try: try:
years = candidate.years_of_experience years = application.years_of_experience
row.append(f"{years}" if years else '') row.append(f"{years}" if years else '')
except: except:
row.append('') row.append('')
elif field == 'screening_rating': elif field == 'screening_rating':
# Extract screening rating using model property # Extract screening rating using model property
try: try:
rating = candidate.screening_stage_rating rating = application.screening_stage_rating
row.append(rating if rating else '') row.append(rating if rating else '')
except: except:
row.append('') row.append('')
elif field == 'professional_category': elif field == 'professional_category':
# Extract professional category using model property # Extract professional category using model property
try: try:
category = candidate.professional_category category = application.professional_category
row.append(category if category else '') row.append(category if category else '')
except: except:
row.append('') row.append('')
elif field == 'top_skills': elif field == 'top_skills':
# Extract top 3 skills using model property # Extract top 3 skills using model property
try: try:
skills = candidate.top_3_keywords skills = application.top_3_keywords
row.append(', '.join(skills) if skills else '') row.append(', '.join(skills) if skills else '')
except: except:
row.append('') row.append('')
elif field == 'strengths': elif field == 'strengths':
# Extract strengths using model property # Extract strengths using model property
try: try:
strengths = candidate.strengths strengths = application.strengths
row.append(strengths if strengths else '') row.append(strengths if strengths else '')
except: except:
row.append('') row.append('')
elif field == 'weaknesses': elif field == 'weaknesses':
# Extract weaknesses using model property # Extract weaknesses using model property
try: try:
weaknesses = candidate.weaknesses weaknesses = application.weaknesses
row.append(weaknesses if weaknesses else '') row.append(weaknesses if weaknesses else '')
except: except:
row.append('') row.append('')
elif field == 'join_date': 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: else:
row.append(getattr(candidate, field, '')) row.append(getattr(application, field, ''))
# Add job information # Add job information
row.extend([job.title, job.department or '']) row.extend([job.title, job.department or ''])
@ -895,8 +905,8 @@ def export_candidates_csv(request, job_slug, stage):
@login_required @login_required
@staff_user_required @staff_user_required
def sync_hired_candidates(request, job_slug): def sync_hired_applications(request, job_slug):
"""Sync hired candidates to external sources using Django-Q""" """Sync hired applications to external sources using Django-Q"""
from django_q.tasks import async_task from django_q.tasks import async_task
from .tasks import sync_hired_candidates_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 %} {% for job in active_jobs %}
{# Optimized Job Listing Card #} {# 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"> 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"> <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> <a class="nav-link text-secondary" href="{% url 'applicant_profile' %}">{% translate "Applications" %}</a>
</li> {% endcomment %} </li> {% endcomment %}
<li class="nav-item"> <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>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-secondary" href="{% url 'kaauh_career' %}">{% translate "Careers" %}</a> <a class="nav-link text-secondary" href="{% url 'kaauh_career' %}">{% translate "Careers" %}</a>

View File

@ -197,8 +197,8 @@
{% if request.user.is_superuser %} {% 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 '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" %}</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> <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 %} {% 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 %} {% endif %}
@ -253,7 +253,7 @@
</a> </a>
</li> </li>
<li class="nav-item me-lg-4"> <li class="nav-item me-lg-4">
<a class="nav-link {% if request.resolver_match.url_name == '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"> <span class="d-flex align-items-center gap-2">
<i class="fas fa-user-tie me-2"></i> <i class="fas fa-user-tie me-2"></i>
{% trans "Applications" %} {% trans "Applications" %}

View File

@ -184,8 +184,8 @@
background-color: var(--kaauh-gray-light); background-color: var(--kaauh-gray-light);
} }
.empty-state i { .empty-state i {
font-size: 3.5rem; font-size: 1rem;
margin-bottom: 1rem; margin-bottom: 0.5rem;
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
} }
.empty-state .btn-main-action .fas { .empty-state .btn-main-action .fas {

View File

@ -154,8 +154,8 @@
background-color: var(--kaauh-gray-light); background-color: var(--kaauh-gray-light);
} }
.empty-state i { .empty-state i {
font-size: 3.5rem; font-size: 1rem;
margin-bottom: 1rem; margin-bottom: 0.5rem;
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
} }
.empty-state .btn-main-action .fas { .empty-state .btn-main-action .fas {
@ -331,7 +331,7 @@
<p class="text-muted mb-4"> <p class="text-muted mb-4">
{% trans "There are no submissions for this form template yet." %} {% trans "There are no submissions for this form template yet." %}
</p> </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" %} <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
</a> </a>
</div> </div>

View File

@ -1,5 +1,5 @@
{% load i18n %} {% 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 }}'}})"> <form data-on-submit="@post('{{url}}', {contentType: 'form', headers: {'X-CSRFToken': '{{ csrf_token }}'}})">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ 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 %} {% 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(); }"> 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="d-flex justify-content-center align-items-center gap-2">
<div class="form-check d-flex 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"> <label class="form-check-label" for="exam_passed">
<i class="fas fa-check me-1"></i> {% trans "Passed" %} <i class="fas fa-check me-1"></i> {% trans "Passed" %}
</label> </label>
</div> </div>
<div class="form-check d-flex 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_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"> <label class="form-check-label" for="exam_failed">
<i class="fas fa-times me-1"></i> {% trans "Failed" %} <i class="fas fa-times me-1"></i> {% trans "Failed" %}
</label> </label>
@ -20,7 +20,7 @@
<label for="exam_score" class="form-label small text-muted">{% trans "Exam Score" %}</label> <label for="exam_score" class="form-label small text-muted">{% trans "Exam Score" %}</label>
</div> </div>
<div class="w-25"> <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>
<div class="w-25 text-start ps-none"> <div class="w-25 text-start ps-none">
</div> </div>

View File

@ -1,10 +1,10 @@
{% load i18n %} {% 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(); }"> 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" %} <i class="fas fa-check me-1"></i> {% trans "Passed" %}
</a> </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" %} <i class="fas fa-times me-1"></i> {% trans "Failed" %}
</a> </a>
</div> </div>

View File

@ -1,10 +1,10 @@
{% load i18n %} {% 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(); }"> 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" %} <i class="fas fa-check me-1"></i> {% trans "Accepted" %}
</a> </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" %} <i class="fas fa-times me-1"></i> {% trans "Rejected" %}
</a> </a>
</div> </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 <form
method="post" method="post"
enctype="multipart/form-data" enctype="multipart/form-data"
hx-post="{% url 'document_upload' candidate.id %}" hx-post="{% url 'document_upload' application.id %}"
hx-target="#documents-pane" hx-target="#documents-pane"
hx-select="#documents-pane" hx-select="#documents-pane"
hx-swap="outerHTML" hx-swap="outerHTML"
@ -108,7 +108,7 @@
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
</a> </a>
{% if user.is_superuser or candidate.job.assigned_to == user %} {% if user.is_superuser or application.job.assigned_to == user %}
<button <button
type="button" type="button"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
@ -125,7 +125,7 @@
<div class="text-center py-5 text-muted"> <div class="text-center py-5 text-muted">
<i class="fas fa-file-alt fa-3x mb-3"></i> <i class="fas fa-file-alt fa-3x mb-3"></i>
<p class="mb-2">{% trans "No documents uploaded yet." %}</p> <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> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -17,7 +17,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <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-include="#candidate-form"
hx-target="#messageContent" hx-target="#messageContent"
hx-select="#messageContent" hx-select="#messageContent"

View File

@ -91,7 +91,7 @@
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title flex-grow-1 me-3"> <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> </h5>
<span class="status-badge bg-{{ interview.status }}"> <span class="status-badge bg-{{ interview.status }}">
{{ interview.status|title }} {{ interview.status|title }}
@ -169,7 +169,7 @@
<tr> <tr>
<td> <td>
<strong class="text-primary-theme"> <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> </strong>
</td> </td>
<td> <td>

View File

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static i18n %} {% load static i18n %}
{% block title %}Create New Job Post - {{ block.super }}{% endblock %} {% block title %}{% trans "Create New Job Post" %} - {{ block.super }}{% endblock %}
{% block customCSS %} {% block customCSS %}

View File

@ -127,7 +127,7 @@
</nav> </nav>
</div> </div>
<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" %} <i class="fas fa-user-plus"></i> {% trans "Add New Applicant" %}
</a> </a>
</div> </div>
@ -186,7 +186,7 @@
</button> </button>
</div> </div>
<div class="col-md-3 d-flex align-items-end"> <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" %} <i class="fas fa-times"></i> {% trans "Clear Filters" %}
</a> </a>
</div> </div>
@ -250,16 +250,16 @@
<td class="text-secondary small">{{ candidate.created_at|date:"M d, Y" }}</td> <td class="text-secondary small">{{ candidate.created_at|date:"M d, Y" }}</td>
<td class="text-center pe-3"> <td class="text-center pe-3">
<div class="btn-group" role="group"> <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> <i class="fas fa-eye"></i>
</a> </a>
{% if user.is_staff %} {% 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> <i class="fas fa-edit"></i>
</a> </a>
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}" <button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal" data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'candidate_delete' candidate.slug %}" data-delete-url="{% url 'application_delete' candidate.slug %}"
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}"> data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
@ -345,17 +345,17 @@
</div> </div>
<div class="card-footer"> <div class="card-footer">
<div class="d-flex gap-2"> <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" %} <i class="fas fa-eye"></i> {% trans "View Profile" %}
</a> </a>
{% if user.is_staff %} {% if user.is_staff %}
<div class="btn-group" role="group"> <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> <i class="fas fa-edit"></i>
</a> </a>
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}" <button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal" data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'candidate_delete' candidate.slug %}" data-delete-url="{% url 'application_delete' candidate.slug %}"
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}"> data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
@ -375,7 +375,7 @@
<i class="fas fa-user-slash fa-3x text-muted mb-3"></i> <i class="fas fa-user-slash fa-3x text-muted mb-3"></i>
<h4 class="text-muted">{% trans "No applicants found" %}</h4> <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> <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" %} <i class="fas fa-user-plus"></i> {% trans "Add First Applicant" %}
</a> </a>
</div> </div>

View File

@ -150,12 +150,12 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="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 '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">Jobs</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=" <li class="breadcrumb-item active" aria-current="page" style="
color: #F43B5E; /* Rosy Accent Color */ color: #F43B5E; /* Rosy Accent Color */
font-weight: 600; font-weight: 600;
">Job Detail</li> ">{% trans "Job Detail" %}</li>
</ol> </ol>
</nav> </nav>
<div class="row g-4"> <div class="row g-4">
@ -218,19 +218,19 @@
<div class="card-body"> <div class="card-body">
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %} <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="float-end">
<div class="d-flex align-items-center"> <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>
</div> </div>
</h5> </h5>
<div class="row g-3 mb-4 border-bottom pb-3 small text-secondary"> <div class="row g-3 mb-4 border-bottom pb-3 small text-secondary">
<div class="col-md-6"> <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>
<div class="col-md-6"> <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>
<div class="col-md-6"> <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 }} <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 }} <i class="fas fa-globe me-2 text-primary"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
</div> </div>
<div class="col-md-6"> <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>
<div class="col-md-6"> <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>
<div class="col-md-6"> <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>
<div class="col-md-6"> <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>
</div> </div>
@ -293,7 +293,7 @@
<ul class="nav nav-tabs" id="rightJobTabs" role="tablist"> <ul class="nav nav-tabs" id="rightJobTabs" role="tablist">
<li class="nav-item flex-fill" role="presentation"> <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"> <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> </button>
</li> </li>
<li class="nav-item flex-fill" role="presentation"> <li class="nav-item flex-fill" role="presentation">
@ -322,14 +322,14 @@
{# TAB 1: APPLICANTS CONTENT #} {# TAB 1: APPLICANTS CONTENT #}
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab"> <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"> <div class="d-grid gap-3">
<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 me-1"></i> {% trans "Create Applicant" %} <i class="fas fa-user-plus me-1"></i> {% trans "Create Application" %}
</a> </a>
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action"> <a href="{% url 'applications_screening_view' job.slug %}" class="btn btn-main-action">
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applicants" %} <i class="fas fa-layer-group me-1"></i> {% trans "Manage Applications" %}
</a> </a>
<a href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action"> <a href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action">
@ -345,7 +345,7 @@
{# TAB 2: TRACKING CONTENT #} {# TAB 2: TRACKING CONTENT #}
<div class="tab-pane fade" id="tracking-pane" role="tabpanel" aria-labelledby="tracking-tab"> <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' %} {% include 'jobs/partials/applicant_tracking.html' %}
<p class="text-muted small"> <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." %} {% 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>
</div> </div>
{# Card 2: Candidate Category Chart #} {# Card 2: Application Category Chart #}
<div class="card shadow-sm no-hover mb-4"> <div class="card shadow-sm no-hover mb-4">
<div class="card-header"> <div class="card-header">
<h6 class="mb-0"> <h6 class="mb-0">
<i class="fas fa-chart-pie me-2 text-primary"></i> <i class="fas fa-chart-pie me-2 text-primary"></i>
{% trans "Candidate Categories & Scores" %} {% trans "Application Categories & Scores" %}
</h6> </h6>
</div> </div>
<div class="card-body p-4"> <div class="card-body p-4">
@ -535,7 +535,7 @@
<div class="card-body p-2"> <div class="card-body p-2">
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i> <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> <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> </div>
</div> </div>
@ -616,14 +616,24 @@
{% endblock %} {% 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%} {% block customJS%}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<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 // Pass data from Django to JavaScript safely
window.jobChartData = { window.jobChartData = {
categories: {{ categories|safe|default:"[]" }}, categories: {{ categories|safe|default:"[]" }},
candidate_counts: {{ candidate_counts|safe|default:"[]" }}, applications_count: {{ applications_count|safe|default:"[]" }},
avg_scores: {{ avg_scores|safe|default:"[]" }} avg_scores: {{ avg_scores|safe|default:"[]" }}
}; };
@ -646,7 +656,7 @@
}).catch(err => { }).catch(err => {
// Fallback for older browsers or security issues // Fallback for older browsers or security issues
console.error('Could not copy text: ', err); 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 // Safely get job_category_data from Django context
// Using window.jobChartData to avoid template parsing issues // 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 categories = jobChartData.categories || [];
const candidateCounts = jobChartData.candidate_counts || []; const candidateCounts = jobChartData.applications_count || [];
const avgScores = jobChartData.avg_scores || []; const avgScores = jobChartData.avg_scores || [];
if (categories.length > 0) { // Only render if there's data if (categories.length > 0) { // Only render if there's data
@ -672,9 +682,9 @@
type: 'doughnut', type: 'doughnut',
data: { data: {
labels: categories, labels: categories,
datasets: [ datasets: [
{ {
label: 'Number of Candidates', label: CHART_LABEL_NUMBER_OF_APPLICATIONS,
data: candidateCounts, data: candidateCounts,
backgroundColor: [ backgroundColor: [
'rgba(0, 99, 110, 0.7)', // --kaauh-teal 'rgba(0, 99, 110, 0.7)', // --kaauh-teal
@ -715,7 +725,7 @@
if (label) { if (label) {
label += ': '; label += ': ';
} }
label += context.parsed + ' candidate(s)'; label += context.parsed + CHART_TOOLTIP_SUFFIX;
return label; return label;
} }
} }
@ -724,8 +734,8 @@
} }
}); });
} else { } else {
// Display a message if no data is available // Display a message if no data is available (translated)
chartCtx.canvas.parentNode.innerHTML = '<p class="text-center text-muted mt-4">No candidate category data available for this job.</p>'; chartCtx.canvas.parentNode.innerHTML = '<p class="text-center text-muted mt-4">' + NO_CHART_DATA_MSG + '</p>';
} }
}); });
</script> </script>

View File

@ -330,12 +330,12 @@
</td> </td>
{# CANDIDATE MANAGEMENT DATA #} {# 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-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 '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-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 '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 '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 '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 '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 '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 '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 '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-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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -360,8 +360,8 @@
<ul class="list-unstyled small mb-3"> <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-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-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_candidates.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" %}: <li><i class="fas fa-file-alt text-info me-2"></i> {% trans "Form" %}:
{% if job.form_template %} {% 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> <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="progress-stages">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
{% comment %} STAGE 1: Applied {% endcomment %} {% 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 %}" class="stage-item {% if current_stage == 'Applied' %}active{% endif %}"
data-stage="Applied"> data-stage="Applied">
<div class="stage-icon"> <div class="stage-icon">
<i class="fas fa-file-signature cd_screening"></i> <i class="fas fa-file-signature cd_screening"></i>
</div> </div>
<div class="stage-label cd_screening">{% trans "Screened" %}</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> </a>
{% comment %} CONNECTOR 1 -> 2 {% endcomment %} {% 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> <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 %} {% 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 %}" class="stage-item {% if current_stage == 'Exam' %}active{% endif %}"
data-stage="Exam"> data-stage="Exam">
<div class="stage-icon"> <div class="stage-icon">
<i class="fas fa-clipboard-check cd_exam"></i> <i class="fas fa-clipboard-check cd_exam"></i>
</div> </div>
<div class="stage-label cd_exam">{% trans "Exam" %}</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> </a>
{% comment %} CONNECTOR 2 -> 3 {% endcomment %} {% 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> <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 %} {% 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 %}" class="stage-item {% if current_stage == 'Interview' %}active{% endif %}"
data-stage="Interview"> data-stage="Interview">
<div class="stage-icon"> <div class="stage-icon">
<i class="fas fa-comments cd_interview"></i> <i class="fas fa-comments cd_interview"></i>
</div> </div>
<div class="stage-label cd_interview">{% trans "Interview" %}</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> </a>
{% comment %} CONNECTOR 3 -> 4 {% endcomment %} {% 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> <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 %} {% 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 %}" class="stage-item {% if current_stage == 'Document Review' %}active{% endif %} {% if current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"
data-stage="Document Review"> data-stage="Document Review">
<div class="stage-icon"> <div class="stage-icon">
<i class="fas fa-file-alt"></i> <i class="fas fa-file-alt"></i>
</div> </div>
<div class="stage-label">{% trans "Document Review" %}</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> </a>
{% comment %} CONNECTOR 4 -> 5 {% endcomment %} {% comment %} CONNECTOR 4 -> 5 {% endcomment %}
<div class="stage-connector {% if current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div> <div class="stage-connector {% if current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
{% comment %} STAGE 5: Offer {% endcomment %} {% 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 %}" class="stage-item {% if current_stage == 'Offer' %}active{% endif %} {% if current_stage == 'Hired' %}completed{% endif %}"
data-stage="Offer"> data-stage="Offer">
<div class="stage-icon"> <div class="stage-icon">
<i class="fas fa-handshake"></i> <i class="fas fa-handshake"></i>
</div> </div>
<div class="stage-label">{% trans "Offer" %}</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> </a>
{% comment %} CONNECTOR 5 -> 6 {% endcomment %} {% comment %} CONNECTOR 5 -> 6 {% endcomment %}
<div class="stage-connector {% if current_stage == 'Hired' %}completed{% endif %}"></div> <div class="stage-connector {% if current_stage == 'Hired' %}completed{% endif %}"></div>
{% comment %} STAGE 6: Hired {% endcomment %} {% 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 %}" class="stage-item {% if current_stage == 'Hired' %}active{% endif %}"
data-stage="Hired"> data-stage="Hired">
<div class="stage-icon"> <div class="stage-icon">
<i class="fas fa-trophy"></i> <i class="fas fa-trophy"></i>
</div> </div>
<div class="stage-label">{% trans "Hired" %}</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> </a>
</div> </div>
</div> </div>

View File

@ -290,7 +290,7 @@
<span class="status-badge bg-primary-theme text-white">{{ meeting.type|title }}</span> <span class="status-badge bg-primary-theme text-white">{{ meeting.type|title }}</span>
</td> </td>
<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>
<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> <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> </a>
{# Send Candidate Invitation Button #} {# 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 %} {% csrf_token %}
<button type="submit" class="btn btn-outline-info btn-sm" onclick="return confirm('{% trans "Send invitation email to the candidate?" %}')"> <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" %} <i class="fas fa-envelope me-1"></i> {% trans "Send Candidate Invitation" %}

View File

@ -19,7 +19,7 @@
</div> </div>
<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 %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">

View File

@ -20,7 +20,7 @@
</div> </div>
<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 %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.topic.id_for_label }}" class="form-label small"> <label for="{{ form.topic.id_for_label }}" class="form-label small">

View File

@ -1,5 +1,5 @@
{% load i18n crispy_forms_tags %} {% 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 %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<button type="submit" class="btn bg-primary-theme text-white">{% trans "Save" %}</button> <button type="submit" class="btn bg-primary-theme text-white">{% trans "Save" %}</button>

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static i18n %}
{% block title %}{{ message.subject }}{% endblock %} {% block title %}{{ message.subject }}{% endblock %}
@ -13,69 +13,69 @@
<h5 class="mb-0"> <h5 class="mb-0">
{{ message.subject }} {{ message.subject }}
{% if message.parent_message %} {% if message.parent_message %}
<span class="badge bg-secondary ms-2">Reply</span> <span class="badge bg-secondary ms-2">{% trans "Reply" %}</span>
{% endif %} {% endif %}
</h5> </h5>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<a href="{% url 'message_reply' message.id %}" class="btn btn-outline-info"> <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> </a>
{% if message.recipient == request.user %} {% if message.recipient == request.user %}
<a href="{% url 'message_mark_unread' message.id %}" <a href="{% url 'message_mark_unread' message.id %}"
class="btn btn-outline-warning" class="btn btn-outline-warning"
hx-post="{% url 'message_mark_unread' message.id %}"> 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> </a>
{% endif %} {% endif %}
<a href="{% url 'message_delete' message.id %}" <a href="{% url 'message_delete' message.id %}"
class="btn btn-outline-danger" class="btn btn-outline-danger"
hx-get="{% url 'message_delete' message.id %}" hx-get="{% url 'message_delete' message.id %}"
hx-confirm="Are you sure you want to delete this message?"> hx-confirm="{% trans 'Are you sure you want to delete this message?' %}">
<i class="fas fa-trash"></i> Delete <i class="fas fa-trash"></i> {% trans "Delete" %}
</a> </a>
<a href="{% url 'message_list' %}" class="btn btn-outline-secondary"> <a href="{% url 'message_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Messages <i class="fas fa-arrow-left"></i> {% trans "Back to Messages" %}
</a> </a>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <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> <span class="text-primary">{{ message.sender.get_full_name|default:message.sender.username }}</span>
</div> </div>
<div class="col-md-6"> <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> <span class="text-primary">{{ message.recipient.get_full_name|default:message.recipient.username }}</span>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<strong>Type:</strong> <strong>{% trans "Type:" %}</strong>
<span class="badge bg-{{ message.message_type|lower }}"> <span class="badge bg-{{ message.message_type|lower }}">
{{ message.get_message_type_display }} {{ message.get_message_type_display }}
</span> </span>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<strong>Status:</strong> <strong>{% trans "Status:" %}</strong>
{% if message.is_read %} {% if message.is_read %}
<span class="badge bg-success">Read</span> <span class="badge bg-success">{% trans "Read" %}</span>
{% if message.read_at %} {% if message.read_at %}
<small class="text-muted">({{ message.read_at|date:"M d, Y H:i" }})</small> <small class="text-muted">({{ message.read_at|date:"M d, Y H:i" }})</small>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="badge bg-warning">Unread</span> <span class="badge bg-warning">{% trans "Unread" %}</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<strong>Created:</strong> <strong>{% trans "Created:" %}</strong>
<span>{{ message.created_at|date:"M d, Y H:i" }}</span> <span>{{ message.created_at|date:"M d, Y H:i" }}</span>
</div> </div>
{% if message.job %} {% if message.job %}
<div class="col-md-6"> <div class="col-md-6">
<strong>Related Job:</strong> <strong>{% trans "Related Job:" %}</strong>
<a href="{% url 'job_detail' message.job.slug %}" class="text-primary"> <a href="{% url 'job_detail' message.job.slug %}" class="text-primary">
{{ message.job.title }} {{ message.job.title }}
</a> </a>
@ -84,13 +84,13 @@
</div> </div>
{% if message.parent_message %} {% if message.parent_message %}
<div class="alert alert-info"> <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 %}"> <a href="{% url 'message_detail' message.parent_message.id %}">
{{ message.parent_message.subject }} {{ message.parent_message.subject }}
</a> </a>
<small class="text-muted d-block"> <small class="text-muted d-block">
From {{ message.parent_message.sender.get_full_name|default:message.parent_message.sender.username }} {% trans "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 "on" %} {{ message.parent_message.created_at|date:"M d, Y H:i" }}
</small> </small>
</div> </div>
{% endif %} {% endif %}
@ -100,7 +100,7 @@
<!-- Message Content --> <!-- Message Content -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h6 class="mb-0">Message</h6> <h6 class="mb-0">{% trans "Message" %}</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="message-content"> <div class="message-content">
@ -114,7 +114,7 @@
<div class="card mt-4"> <div class="card mt-4">
<div class="card-header"> <div class="card-header">
<h6 class="mb-0"> <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> </h6>
</div> </div>
<div class="card-body"> <div class="card-body">
@ -136,7 +136,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<a href="{% url 'message_reply' reply.id %}" class="btn btn-sm btn-outline-info"> <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> </a>
</div> </div>
</div> </div>

View File

@ -412,7 +412,7 @@
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<div> <div>
<h6 class="mb-1"> <h6 class="mb-1">
<a href="{% url 'candidate_detail' application.slug %}" <a href="{% url 'application_detail' application.slug %}"
class="text-decoration-none text-secondary"> class="text-decoration-none text-secondary">
{{ application.job.title }} {{ application.job.title }}
</a> </a>

View File

@ -55,7 +55,7 @@
<nav class="navbar navbar-expand-lg navbar-dark sticky-top"> <nav class="navbar navbar-expand-lg navbar-dark sticky-top">
<div class="container-fluid" style="max-width: 1600px;"> <div class="container-fluid" style="max-width: 1600px;">
{% if request.user.user_type == 'candidate' %} {% 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;"> <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> <span class="ms-3 d-none d-md-inline fw-semibold">{% trans "Applicant Portal" %}</span>
</a> </a>
@ -75,7 +75,7 @@
<div class="collapse navbar-collapse" id="agencyNavbar"> <div class="collapse navbar-collapse" id="agencyNavbar">
<div class="navbar-nav ms-auto"> <div class="navbar-nav ms-auto">
<li class="nav-item me-2"> <li class="nav-item me-2">
{% if LANGUAGE_CODE == 'en' %} {% if LANGUAGE_CODE == 'en' %}
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %} <form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}"> <input name="next" type="hidden" value="{{ request.get_full_path }}">
@ -106,7 +106,7 @@
</li> </li>
{% elif request.user.user_type == 'candidate' %} {% elif request.user.user_type == 'candidate' %}
<li class="nav-item"> <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" %} <i class="fas fa-tachometer-alt me-1"></i> {% trans "Dashboard" %}
</a> </a>
</li> </li>
@ -120,7 +120,7 @@
{% comment %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="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' %}"> data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
@ -145,8 +145,8 @@
</form> </form>
</li> </li>
</ul> </ul>
</li> </li> {% endcomment %}
{% endcomment %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-white" href="{% url 'user_detail' request.user.pk %}"> <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"> <div class="alert alert-info mx-2">
<i class="fas fa-info-circle me-2"></i> <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> </div>
{% if access_link %} {% if access_link %}
@ -219,12 +219,12 @@
</div> </div>
</div> {% endcomment %} </div> {% endcomment %}
<!-- Candidates Card --> <!-- Applications Card -->
<div class="kaauh-card p-4"> <div class="kaauh-card p-4">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);"> <h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-users me-2"></i> <i class="fas fa-users me-2"></i>
{% trans "Submitted Candidates" %} ({{ total_candidates }}) {% trans "Submitted Applications" %} ({{ total_candidates }})
</h5> </h5>
{% if access_link %} {% if access_link %}
<a href="{% url 'agency_portal_login' %}" target="_blank" class="btn btn-outline-info btn-sm"> <a href="{% url 'agency_portal_login' %}" target="_blank" class="btn btn-outline-info btn-sm">
@ -233,12 +233,12 @@
{% endif %} {% endif %}
</div> </div>
{% if candidates %} {% if applications %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <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 class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Contact" %}
</th> </th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Stage" %} <th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Stage" %}
@ -248,36 +248,36 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for candidate in candidates %} {% for application in applications %}
<tr> <tr>
<td> <td>
<div class="fw-bold">{{ candidate.name }}</div> <div class="fw-bold">{{ application.name }}</div>
</td> </td>
<td> <td>
<div class="small"> <div class="small">
<div><i class="fas fa-envelope me-1"></i> {{ candidate.email }}</div> <div><i class="fas fa-envelope me-1"></i> {{ application.email }}</div>
<div><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</div> <div><i class="fas fa-phone me-1"></i> {{ application.phone }}</div>
</div> </div>
</td> </td>
<td> <td>
<span class="badge bg-info">{{ candidate.get_stage_display }}</span> <span class="badge bg-info">{{ application.get_stage_display }}</span>
</td> </td>
<td> <td>
<div class="small text-muted"> <div class="small text-muted">
<div class="mb-1"><i class="fas fa-envelope me-2 w-20"></i> <div class="mb-1"><i class="fas fa-envelope me-2 w-20"></i>
{{candidate.email }}</div> {{application.email }}</div>
<div><i class="fas fa-phone me-2 w-20"></i>{{ candidate.phone }}</div> <div><i class="fas fa-phone me-2 w-20"></i>{{ application.phone }}</div>
</div> </div>
</td> </td>
<td class="px-4"> <td class="px-4">
<span class="badge bg-soft-info text-info rounded-pill px-3"> <span class="badge bg-soft-info text-info rounded-pill px-3">
{{candidate.get_stage_display }}</span> {{application.get_stage_display }}</span>
</td> </td>
<td class="px-4"> <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>
<td class="px-4 text-end"> <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' %}"> class="btn btn-sm btn-outline-primary" title="{% trans 'View Details' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
@ -290,9 +290,9 @@
{% else %} {% else %}
<div class="text-center py-4"> <div class="text-center py-4">
<i class="fas fa-users fa-2x text-muted mb-3"></i> <i class="fas fa-users fa-2x text-muted mb-3"></i>
<h6 class="text-muted">{% trans "No candidates submitted yet" %}</h6> <h6 class="text-muted">{% trans "No applications submitted yet" %}</h6>
<p class="text-muted small"> <p class="text-muted small">
{% trans "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> </p>
</div> </div>
{% endif %} {% endif %}
@ -335,7 +335,7 @@
<div class="text-center"> <div class="text-center">
<div class="h4 mb-1">{{ total_candidates }}</div> <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>
<div class="progress mt-3" style="height: 8px;"> <div class="progress mt-3" style="height: 8px;">

View File

@ -604,19 +604,22 @@
from your Django view context to populate this section. from your Django view context to populate this section.
{% endcomment %} {% endcomment %}
{% if assigned_jobs %} {% if job_assignments %}
{% for assignment in assigned_jobs %} {% for assignment in job_assignments%}
<div class="job-item"> <div class="job-item">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<div class="job-title"> <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 }} {{ assignment.job.title }}
</a> </a>
</div> </div>
<div class="job-details"> <div class="job-details">
<i class="fas fa-map-pin me-1"></i> {{ assignment.job.location }} <i class="fas fa-map-pin me-1"></i> {{ assignment.job.get_location_display }}
<span class="ms-3"><i class="fas fa-user-tie me-1"></i> {{ assignment.job.department.name }}</span> {% 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> </div>
<div class="text-end"> <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"> <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" %} <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Dashboard" %}
</a> </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" %} <i class="fas fa-user-plus me-1"></i> {% trans "Submit New Candidate" %}
</a> </a>
{% comment %} <a href="#" class="btn btn-outline-info"> {% comment %} <a href="#" class="btn btn-outline-info">
@ -194,7 +194,7 @@
<div class="d-grid gap-2"> <div class="d-grid gap-2">
{% if assignment.can_submit %} {% if assignment.can_submit %}
<a href="{% url 'agency_portal_submit_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" %} <i class="fas fa-user-plus me-1"></i> {% trans "Submit New Candidate" %}
</a> </a>
{% else %} {% else %}
@ -264,7 +264,7 @@
</div> </div>
</td> </td>
<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> <i class="fas fa-eye"></i>
</a> </a>
</td> </td>
@ -523,7 +523,7 @@
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<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 %} {% csrf_token %}
<input type="hidden" id="edit_candidate_id" name="candidate_id"> <input type="hidden" id="edit_candidate_id" name="candidate_id">
<div class="modal-body"> <div class="modal-body">
@ -588,7 +588,7 @@
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<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 %} {% csrf_token %}
<input type="hidden" id="delete_candidate_id" name="candidate_id"> <input type="hidden" id="delete_candidate_id" name="candidate_id">
<div class="modal-body"> <div class="modal-body">

View File

@ -49,8 +49,8 @@
</p> </p>
</div> </div>
<div> <div>
{% comment %} <a href="{% url 'agency_portal_submit_candidate' %}" class="btn btn-main-action me-2"> {% 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 Candidate" %} <i class="fas fa-user-plus me-1"></i> {% trans "Submit Application" %}
</a> </a>
<a href="#" class="btn btn-outline-secondary position-relative"> <a href="#" class="btn btn-outline-secondary position-relative">
<i class="fas fa-envelope me-1"></i> {% trans "Messages" %} <i class="fas fa-envelope me-1"></i> {% trans "Messages" %}
@ -93,8 +93,8 @@
<div class="text-info mb-2"> <div class="text-info mb-2">
<i class="fas fa-users fa-2x"></i> <i class="fas fa-users fa-2x"></i>
</div> </div>
<h4 class="card-title">{{ total_candidates }}</h4> <h4 class="card-title">{{ total_applications }}</h4>
<p class="card-text text-muted">{% trans "Total Candidates" %}</p> <p class="card-text text-muted">{% trans "Total Applications" %}</p>
</div> </div>
</div> </div>
</div> </div>
@ -169,8 +169,8 @@
</strong> </strong>
</div> </div>
<div class="col-6"> <div class="col-6">
<small class="text-muted d-block">{% trans "Candidates" %}</small> <small class="text-muted d-block">{% trans "Applications" %}</small>
<strong>{{ stats.candidate_count }} / {{ stats.assignment.max_candidates }}</strong> <strong>{{ stats.application_count }} / {{ stats.assignment.max_applications }}</strong>
</div> </div>
</div> </div>
@ -178,10 +178,10 @@
<div class="mb-3"> <div class="mb-3">
<div class="d-flex justify-content-between mb-1"> <div class="d-flex justify-content-between mb-1">
<small class="text-muted">{% trans "Submission Progress" %}</small> <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>
<div class="progress" style="height: 6px;"> <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 %}" <div class="progress-bar {% if progress >= 90 %}bg-danger{% elif progress >= 70 %}bg-warning{% else %}bg-success{% endif %}"
style="width: {{ progress|floatformat:0 }}%"></div> style="width: {{ progress|floatformat:0 }}%"></div>
{% endwith %} {% endwith %}
@ -192,9 +192,9 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
{% if stats.can_submit %} {% 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"> 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> </a>
{% else %} {% else %}
<button class="btn btn-sm btn-secondary" disabled> <button class="btn btn-sm btn-secondary" disabled>

View File

@ -93,7 +93,7 @@
value="{{ search_query }}" value="{{ search_query }}"
placeholder="{% trans 'Search by name, email, phone, or job title...' %}"> placeholder="{% trans 'Search by name, email, phone, or job title...' %}">
</div> </div>
<div class="col-md-3"> {% comment %} <div class="col-md-3">
<label for="stage" class="form-label fw-semibold"> <label for="stage" class="form-label fw-semibold">
<i class="fas fa-filter me-1"></i>{% trans "Stage" %} <i class="fas fa-filter me-1"></i>{% trans "Stage" %}
</label> </label>
@ -111,7 +111,7 @@
<button type="submit" class="btn btn-main-action w-100"> <button type="submit" class="btn btn-main-action w-100">
<i class="fas fa-search me-1"></i> {% trans "Search" %} <i class="fas fa-search me-1"></i> {% trans "Search" %}
</button> </button>
</div> </div> {% endcomment %}
</div> </div>
</form> </form>
</div> </div>
@ -154,7 +154,7 @@
<th scope="col">{% trans "Name" %}</th> <th scope="col">{% trans "Name" %}</th>
<th scope="col">{% trans "Email" %}</th> <th scope="col">{% trans "Email" %}</th>
<th scope="col">{% trans "Phone" %}</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> {% comment %} <th scope="col">{% trans "Stage" %}</th>
<th scope="col">{% trans "Applied Date" %}</th> {% endcomment %} <th scope="col">{% trans "Applied Date" %}</th> {% endcomment %}
<th scope="col" class="text-center">{% trans "Actions" %}</th> <th scope="col" class="text-center">{% trans "Actions" %}</th>
@ -202,7 +202,7 @@
</span> </span>
{% endwith %} {% endwith %}
</td> {% endcomment %} </td> {% endcomment %}
<td>{{ person.created_at|date:"Y-m-d" }}</td> <td>{{ person.created_at|date:"d-m-Y" }}</td>
<td class="text-center"> <td class="text-center">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="button" data-bs-toggle="modal" data-bs-target="#updateModal" <button type="button" data-bs-toggle="modal" data-bs-target="#updateModal"
@ -235,7 +235,7 @@
{% endif %} {% endif %}
</p> </p>
{% if not search_query and not stage_filter and agency.assignments.exists %} {% 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"> class="btn btn-main-action">
<i class="fas fa-user-plus me-1"></i> {% trans "Add First Person" %} <i class="fas fa-user-plus me-1"></i> {% trans "Add First Person" %}
</a> </a>

View File

@ -1,7 +1,7 @@
{% extends 'portal_base.html' %} {% extends 'portal_base.html' %}
{% load static i18n crispy_forms_tags %} {% 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 %} {% block customCSS %}
<style> <style>
@ -97,13 +97,13 @@
</li> </li>
{% comment %} <li class="breadcrumb-item active" aria-current="page"> {% comment %} <li class="breadcrumb-item active" aria-current="page">
{% trans "Submit Candidate" %} {% trans "Submit Application" %}
</li> {% endcomment %} </li> {% endcomment %}
<li class="breadcrumb-item active" aria-current="page" style=" <li class="breadcrumb-item active" aria-current="page" style="
color: #F43B5E; /* Rosy Accent Color */ color: #F43B5E; /* Rosy Accent Color */
font-weight: 600; "> font-weight: 600; ">
{% trans "Submit Candidate" %}</li> {% trans "Submit Application" %}</li>
</ol> </ol>
</nav> </nav>
@ -112,11 +112,11 @@
<div> <div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;"> <h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-user-plus me-2"></i> <i class="fas fa-user-plus me-2"></i>
{% trans "Submit New Candidate" %} {% trans "Submit New Application" %}
</h1> </h1>
<p class="text-muted mb-0"> <p class="text-muted mb-0">
<!-- Button trigger modal --> <!-- Button trigger modal -->
{% trans "Submit a candidate for" %} {% trans "Submit a Application for" %}
{{ assignment.job.title }} {{ assignment.job.title }}
</p> </p>
@ -171,16 +171,16 @@
<div class="kaauh-card p-4"> <div class="kaauh-card p-4">
<h5 class="mb-4" style="color: var(--kaauh-teal-dark);"> <h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-user-edit me-2"></i> <i class="fas fa-user-edit me-2"></i>
{% trans "Candidate Information" %} {% trans "Application Information" %}
</h5> </h5>
<form method="post" enctype="multipart/form-data" id="candidateForm" <form method="post" enctype="multipart/form-data" id="ApplicationForm"
action="{% url 'agency_portal_submit_candidate_page' assignment.slug %}"> action="{% url 'agency_portal_submit_application_page' assignment.slug %}">
{% csrf_token %} {% csrf_token %}
{{form|crispy}} {{form|crispy}}
<button type="submit" class="btn btn-main-action"> <button type="submit" class="btn btn-main-action">
<i class="fas fa-user-plus me-2"></i> <i class="fas fa-user-plus me-2"></i>
{% trans "Submit Candidate" %} {% trans "Submit Application" %}
</button> </button>
</form> </form>
</div> </div>
@ -188,14 +188,14 @@
<div class="kaauh-card p-4"> <div class="kaauh-card p-4">
<div class="text-center py-5"> <div class="text-center py-5">
<i class="fas fa-exclamation-triangle fa-4x text-warning mb-4"></i> <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"> <div class="alert alert-warning d-inline-block">
{% if assignment.is_expired %} {% if assignment.is_expired %}
<i class="fas fa-clock me-2"></i> <i class="fas fa-clock me-2"></i>
{% trans "This assignment has expired. Submissions are no longer accepted." %} {% trans "This assignment has expired. Submissions are no longer accepted." %}
{% elif assignment.is_full %} {% elif assignment.is_full %}
<i class="fas fa-users me-2"></i> <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 %} {% else %}
<i class="fas fa-pause me-2"></i> <i class="fas fa-pause me-2"></i>
{% trans "This assignment is not currently active." %} {% trans "This assignment is not currently active." %}
@ -222,7 +222,7 @@
<div class="spinner-border text-primary mb-3" role="status"> <div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">{% trans "Loading..." %}</span> <span class="visually-hidden">{% trans "Loading..." %}</span>
</div> </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> <p class="text-muted small">{% trans "Please wait while we process your submission." %}</p>
</div> </div>
</div> </div>
@ -240,7 +240,7 @@ document.addEventListener('DOMContentLoaded', function() {
const filePreview = document.getElementById('filePreview'); const filePreview = document.getElementById('filePreview');
const fileName = document.getElementById('fileName'); const fileName = document.getElementById('fileName');
const removeFileBtn = document.getElementById('removeFile'); const removeFileBtn = document.getElementById('removeFile');
const form = document.getElementById('candidateForm'); const form = document.getElementById('ApplicationForm');
const submitBtn = document.getElementById('submitBtn'); const submitBtn = document.getElementById('submitBtn');
// File upload area click handler // 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.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
successAlert.innerHTML = ` successAlert.innerHTML = `
<i class="fas fa-check-circle me-2"></i> <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> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`; `;
document.body.appendChild(successAlert); 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.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
errorAlert.innerHTML = ` errorAlert.innerHTML = `
<i class="fas fa-exclamation-triangle me-2"></i> <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> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`; `;
document.body.appendChild(errorAlert); document.body.appendChild(errorAlert);
@ -401,7 +401,7 @@ document.addEventListener('DOMContentLoaded', function() {
.finally(() => { .finally(() => {
// Re-enable submit button // Re-enable submit button
submitBtn.disabled = false; 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"> <nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <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>
<li class="breadcrumb-item"> <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>
<li class="breadcrumb-item active" aria-current="page" style=" <li class="breadcrumb-item active" aria-current="page" style="
color: #F43B5E; /* Rosy Accent Color */ color: #F43B5E; /* Rosy Accent Color */
@ -315,13 +315,13 @@
<div class="row mb-5"> <div class="row mb-5">
<div class="col-md-6 col-6"> <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="kaauh-card h-50 shadow-sm action-card">
<div class="card-body text-center mb-4"> <div class="card-body text-center mb-4">
<i class="fas fa-arrow-left fa-2x text-primary-theme mb-3"></i> <i class="fas fa-arrow-left fa-2x text-primary-theme mb-3"></i>
<h6>{% trans "Go to Dashboard" %}</h6> <h6>{% trans "Go to Dashboard" %}</h6>
<p class="text-muted small">{% trans "View all applications" %}</p> <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" %} {% trans "Dashboard" %}
</a> </a>
</div> </div>

View File

@ -292,23 +292,23 @@
{# Header: Larger, more dynamic on large screens. Stacks cleanly on mobile. #} {# 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"> <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);"> <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> </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"> {% 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" %} <i class="fas fa-edit me-2"></i> {% trans "Update Profile" %}
</a> {% endcomment %} </a> {% endcomment %}
</div> </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="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"> <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' %}" alt="{% trans 'Profile Picture' %}"
class="rounded-circle me-sm-4 mb-3 mb-sm-0 shadow-lg" 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);"> style="width: 80px; height: 80px; object-fit: cover; border: 4px solid var(--kaauh-teal-accent);">
<div> <div>
<h3 class="card-title mb-1 fw-bold text-dark">{{ candidate.full_name|default:"Candidate Name" }}</h3> <h3 class="card-title mb-1 fw-bold text-dark">{{ applicant.full_name|default:"Applicant Name" }}</h3>
<p class="text-gray-subtle mb-0">{{ candidate.email }}</p> <p class="text-gray-subtle mb-0">{{ applicant.email }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -356,21 +356,21 @@
<ul class="list-unstyled profile-data-list p-0"> <ul class="list-unstyled profile-data-list p-0">
<li class="d-flex justify-content-between align-items-center"> <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> <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>
<li class="d-flex justify-content-between align-items-center"> <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> <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> </li>
{% if candidate.middle_name %} {% if applicant.middle_name %}
<li class="d-flex justify-content-between align-items-center"> <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> <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> </li>
{% endif %} {% endif %}
<li class="d-flex justify-content-between align-items-center"> <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> <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> </li>
</ul> </ul>
</div> </div>
@ -383,19 +383,19 @@
<ul class="list-unstyled profile-data-list p-0"> <ul class="list-unstyled profile-data-list p-0">
<li class="d-flex justify-content-between align-items-center"> <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> <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> </li>
{% if candidate.address %} {% if applicant.address %}
<li class="d-flex align-items-start"> <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> <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> </li>
{% endif %} {% endif %}
{% if candidate.linkedin_profile %} {% if applicant.linkedin_profile %}
<li class="d-flex justify-content-between align-items-center"> <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> <div><i class="fab fa-linkedin me-2 text-primary-theme"></i> <strong>{% trans "LinkedIn Profile" %}</strong></div>
<span class="text-end"> <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> {% trans "View Profile" %} <i class="fas fa-external-link-alt ms-1"></i>
</a> </a>
</span> </span>
@ -412,15 +412,15 @@
<ul class="list-unstyled profile-data-list p-0"> <ul class="list-unstyled profile-data-list p-0">
<li class="d-flex justify-content-between align-items-center"> <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> <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>
<li class="d-flex justify-content-between align-items-center"> <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> <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>
<li class="d-flex justify-content-between align-items-center"> <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> <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> </li>
</ul> </ul>
</div> </div>
@ -431,16 +431,16 @@
<i class="fas fa-briefcase me-2 text-primary-theme"></i>{% trans "Professional Information" %} <i class="fas fa-briefcase me-2 text-primary-theme"></i>{% trans "Professional Information" %}
</h4> </h4>
<ul class="list-unstyled profile-data-list p-0"> <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"> <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> <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> </li>
{% endif %} {% endif %}
{% if candidate.gpa %} {% if applicant.gpa %}
<li class="d-flex justify-content-between align-items-center"> <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> <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> </li>
{% endif %} {% endif %}
</ul> </ul>
@ -492,7 +492,7 @@
<div class="d-flex align-items-start mb-3"> <div class="d-flex align-items-start mb-3">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h5 class="card-title fw-bold mb-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"> class="text-decoration-none text-primary-theme hover:text-primary-theme-dark">
{{ application.job.title }} {{ application.job.title }}
</a> </a>
@ -524,7 +524,7 @@
<!-- Action Button --> <!-- Action Button -->
<div class="mt-auto"> <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"> class="btn btn-main-action w-100 rounded-pill">
<i class="fas fa-eye me-2"></i> {% trans "View Details" %} <i class="fas fa-eye me-2"></i> {% trans "View Details" %}
</a> </a>
@ -568,7 +568,7 @@
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="text-muted small me-3">{% trans "Uploaded:" %} {{ document.uploaded_at|date:"d M Y" }}</span> <span class="text-muted small me-3">{% trans "Uploaded:" %} {{ document.uploaded_at|date:"d M Y" }}</span>
<a href="{{ document.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary me-2"><i class="fas fa-eye"></i></a> <a href="{{ document.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary me-2"><i class="fas fa-eye"></i></a>
<a href="{% url '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> </div>
</li> </li>
{% empty %} {% empty %}
@ -626,7 +626,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body" id="passwordModalBody"> <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 %} {% csrf_token %}
{{ password_reset_form|crispy }} {{ password_reset_form|crispy }}
<button type="submit" class="btn btn-main-action">{% trans "Change Password" %}</button> <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> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <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 %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">
@ -709,7 +709,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <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"> <input type="hidden" name="upload_target" value="person">
{% csrf_token %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">

View File

@ -100,7 +100,7 @@
<i class="fas fa-user-plus me-1"></i> <i class="fas fa-user-plus me-1"></i>
<span class="d-none d-sm-inline">{% trans "Create New Person" %}</span> <span class="d-none d-sm-inline">{% trans "Create New Person" %}</span>
</button> </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> <i class="fas fa-arrow-left"></i>
<span class="d-none d-sm-inline">{% trans "Back to List" %}</span> <span class="d-none d-sm-inline">{% trans "Back to List" %}</span>
</a> </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" %} {% 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 %} {% block customCSS %}
<style> <style>
@ -269,7 +269,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="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 '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=" <li class="breadcrumb-item active" aria-current="page" class="text-secondary" style="
color: #F43B5E; /* Rosy Accent Color */ color: #F43B5E; /* Rosy Accent Color */
font-weight: 600; font-weight: 600;
@ -277,7 +277,7 @@
</ol> </ol>
</nav> </nav>
{# LEFT COLUMN: MAIN CANDIDATE DETAILS AND TABS #} {# LEFT COLUMN: MAIN application DETAILS AND TABS #}
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card shadow-sm no-hover"> <div class="card shadow-sm no-hover">
@ -285,16 +285,16 @@
<div class="candidate-header-card"> <div class="candidate-header-card">
<div class="d-flex justify-content-between align-items-start flex-wrap"> <div class="d-flex justify-content-between align-items-start flex-wrap">
<div> <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"> <div class="d-flex align-items-center gap-2 mb-2">
<span id="stageDisplay" class="badge"> <span id="stageDisplay" class="badge">
{% trans "Stage:" %} {% trans "Stage:" %}
<span>{{ candidate.stage }}</span> <span>{{ application.stage }}</span>
</span> </span>
</div> </div>
<small class="text-white opacity-75"> <small class="text-white opacity-75">
{% trans "Applied for:" %} <strong>{{ candidate.job.title }}</strong> {% trans "Applied for:" %} <strong>{{ application.job.title }}</strong>
</small> </small>
</div> </div>
{# Change Stage button #} {# Change Stage button #}
@ -341,7 +341,7 @@
<i class="fas fa-envelope fa-2x text-muted me-3"></i> <i class="fas fa-envelope fa-2x text-muted me-3"></i>
<div> <div>
<small class="text-muted d-block">{% trans "Email" %}</small> <small class="text-muted d-block">{% trans "Email" %}</small>
<strong>{{ candidate.email }}</strong> <strong>{{ application.email }}</strong>
</div> </div>
</div> </div>
</div> </div>
@ -350,7 +350,7 @@
<i class="fas fa-briefcase fa-2x text-muted me-3"></i> <i class="fas fa-briefcase fa-2x text-muted me-3"></i>
<div> <div>
<small class="text-muted d-block">{% trans "Position Applied" %}</small> <small class="text-muted d-block">{% trans "Position Applied" %}</small>
<strong>{{ candidate.job.title }}</strong> <strong>{{ application.job.title }}</strong>
</div> </div>
</div> </div>
</div> </div>
@ -360,10 +360,10 @@
<div> <div>
<small class="text-muted d-block">{% trans "Applied Date" %}</small> <small class="text-muted d-block">{% trans "Applied Date" %}</small>
<div class="d-flex align-items-center gap-2"> <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"> <span class="badge bg-light text-dark">
<i class="far fa-clock me-1"></i> <i class="far fa-clock me-1"></i>
{{ candidate.created_at|naturaltime }} {{ application.created_at|naturaltime }}
</span> </span>
</div> </div>
</div> </div>
@ -380,15 +380,15 @@
{# ENHANCED: CANDIDATE JOURNEY TIMELINE CARD #} {# ENHANCED: CANDIDATE JOURNEY TIMELINE CARD #}
<div class="card shadow-sm timeline-card"> <div class="card shadow-sm timeline-card">
<div class="card-header bg-white border-bottom py-3"> <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>
<div class="card-body p-4"> <div class="card-body p-4">
<h6 class="text-uppercase text-secondary mb-3">{% trans "Current Stage" %}</h6> <h6 class="text-uppercase text-secondary mb-3">{% trans "Current Stage" %}</h6>
<div class="p-3 mb-4 rounded current-stage"> <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"> <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> </small>
</div> </div>
@ -402,50 +402,50 @@
<div class="timeline-content"> <div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Application Submitted" %}</p> <p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Application Submitted" %}</p>
<small class="text-muted"> <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> <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> </small>
</div> </div>
</div> </div>
{% if candidate.exam_date %} {% if application.exam_date %}
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fa-clipboard-check"></i></div> <div class="timeline-icon timeline-bg-applied"><i class="fas fa-clipboard-check"></i></div>
<div class="timeline-content"> <div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Exam" %}</p> <p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Exam" %}</p>
<small class="text-muted"> <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> <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> </small>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if candidate.get_interview_date %} {% if application.get_interview_date %}
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div> <div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
<div class="timeline-content"> <div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p> <p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
<small class="text-muted"> <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> <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> </small>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if candidate.offer_date %} {% if application.offer_date %}
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div> <div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
<div class="timeline-content"> <div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p> <p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
<small class="text-muted"> <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> </small>
</div> </div>
@ -453,13 +453,13 @@
</div> </div>
{% endif %} {% endif %}
{% if candidate.hired_date %} {% if application.hired_date %}
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div> <div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
<div class="timeline-content"> <div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p> <p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
<small class="text-muted"> <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> </small>
</div> </div>
@ -473,27 +473,28 @@
{# TAB 4 CONTENT: DOCUMENTS #} {# TAB 4 CONTENT: DOCUMENTS #}
<div class="tab-pane fade" id="documents-pane" role="tabpanel" aria-labelledby="documents-tab"> <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' %} {% include 'includes/document_list.html' %}
{% endwith %} {% endwith %}
</div> </div>
{# TAB 5 CONTENT: PARSED SUMMARY #} {# 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"> <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> <h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5>
<div class="border-start border-primary ps-3 pt-1 pb-1"> <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>
</div> </div>
{% endif %} {% endif %}
{# TAB 5 CONTENT: AI ANALYSIS #} {# 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"> <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> <h5 class="text-primary mb-4">{% trans "AI Analysis Report" %}</h5>
<div class="border-start border-primary ps-3 pt-1 pb-1"> <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 #} {# 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="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"> <div class="d-flex justify-content-between align-items-center mb-2">
@ -625,7 +626,7 @@
<span class="visually-hidden">{% trans "Loading..." %}</span> <span class="visually-hidden">{% trans "Loading..." %}</span>
</div> </div>
<h5 class="text-primary">{% trans "Resume is being parsed" %}</h5> <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 mt-3" style="height: 6px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" <div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 75%"></div> role="progressbar" style="width: 75%"></div>
@ -647,28 +648,28 @@
<div class="card shadow-sm mb-2 p-2"> <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> <h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
<div class="d-grid gap-2"> <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" %} <i class="fas fa-edit"></i> {% trans "Edit Details" %}
</a> {% endcomment %} </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" %} <i class="fas fa-trash-alt"></i> {% trans "Delete Candidate" %}
</a> {% endcomment %} </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" %} <i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
</a> </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> <i class="fas fa-eye me-1"></i>
{% trans "View Actual Resume" %} {% trans "View Actual Resume" %}
</a> {% endcomment %} </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> <i class="fas fa-download me-1"></i>
{% trans "Download Resume" %} {% trans "Download Resume" %}
</a> </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> <i class="fas fa-file-alt me-1"></i>
{% trans "View Resume AI Overview" %} {% trans "View Resume AI Overview" %}
</a> {% endcomment %} </a> {% endcomment %}
@ -680,7 +681,7 @@
<div class="card shadow-sm mb-4 p-2"> <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:" %} <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 %} {% if days > 0 %}
{{ days }} day{{ days|pluralize }} {{ days }} day{{ days|pluralize }}
{% else %} {% else %}
@ -698,10 +699,10 @@
</div> </div>
<div class="resume-parsed-section"> <div class="resume-parsed-section">
{% if candidate.is_resume_parsed %} {% if application.is_resume_parsed %}
{% include 'recruitment/candidate_resume_template.html' %} {% include 'recruitment/application_resume_template.html' %}
{% else %} {% 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 style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
<div class="ai-loading-container"> <div class="ai-loading-container">
<i class="fas fa-robot ai-robot-icon"></i> <i class="fas fa-robot ai-robot-icon"></i>
@ -710,7 +711,7 @@
</div> </div>
{% else %} {% else %}
<div style="display: flex; justify-content: center; align-items: center; height: 100%;"> <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> <i class="fas fa-redo-alt me-1"></i>
{% trans "Unable to Parse Resume , click to retry" %} {% trans "Unable to Parse Resume , click to retry" %}
</button> </button>
@ -723,6 +724,6 @@
{% if user.is_staff %} {% 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 %} {% endif %}
{% endblock %} {% 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> <p class="text-white opacity-75 mb-0">{% trans "Edit candidate information and details" %}</p>
</div> </div>
<div class="d-flex gap-2 mt-1"> <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> <i class="fas fa-arrow-left"></i>
<span class="d-none d-sm-inline">{% trans "Back to List" %}</span> <span class="d-none d-sm-inline">{% trans "Back to List" %}</span>
</a> </a>
{% if object.slug %} {% 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> <i class="fas fa-eye"></i>
<span class="d-none d-sm-inline">{% trans "View" %}</span> <span class="d-none d-sm-inline">{% trans "View" %}</span>
</a> </a>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static i18n %} {% 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 %} {% block customCSS %}
<style> <style>
/* KAAT-S UI Variables */ /* KAAT-S UI Variables */
@ -60,8 +60,8 @@
border-color: var(--kaauh-teal-dark); border-color: var(--kaauh-teal-dark);
} }
/* 3. Candidate Table Styling (Aligned with KAAT-S) */ /* 3. Application Table Styling (Aligned with KAAT-S) */
.candidate-table { .application-table {
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
border-collapse: separate; border-collapse: separate;
@ -70,10 +70,10 @@
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.candidate-table thead { .application-table thead {
background-color: var(--kaauh-border); background-color: var(--kaauh-border);
} }
.candidate-table th { .application-table th {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
font-weight: 600; font-weight: 600;
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
@ -81,24 +81,24 @@
font-size: 0.9rem; font-size: 0.9rem;
vertical-align: middle; vertical-align: middle;
} }
.candidate-table td { .application-table td {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border); border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle; vertical-align: middle;
font-size: 0.9rem; font-size: 0.9rem;
} }
.candidate-table tbody tr:hover { .application-table tbody tr:hover {
background-color: #f1f3f4; background-color: #f1f3f4;
} }
.candidate-table thead th:nth-child(1) { width: 40px; } .application-table thead th:nth-child(1) { width: 40px; }
.candidate-table thead th:nth-child(4) { width: 10%; } .application-table thead th:nth-child(4) { width: 10%; }
.candidate-table thead th:nth-child(7) { width: 100px; } .application-table thead th:nth-child(7) { width: 100px; }
.candidate-name { .application-name {
font-weight: 600; font-weight: 600;
color: var(--kaauh-primary-text); color: var(--kaauh-primary-text);
} }
.candidate-details { .application-details {
font-size: 0.8rem; font-size: 0.8rem;
color: #6c757d; color: #6c757d;
} }
@ -118,7 +118,7 @@
font-weight: 700; font-weight: 700;
} }
.bg-applicant { background-color: #6c757d !important; color: white; } .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 Badges */
.stage-badge { .stage-badge {
@ -211,11 +211,11 @@
</h2> </h2>
</div> </div>
<div class="d-flex gap-2"> <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" 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" %} <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"> <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" %} <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Job" %}
</a> </a>
@ -229,7 +229,7 @@
<!-- Search and Filter Controls --> <!-- Search and Filter Controls -->
<div class="filter-controls"> <div class="filter-controls">
<h4 class="h6 mb-3 fw-bold"> <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> </h4>
<form method="GET" class="mb-0"> <form method="GET" class="mb-0">
<div class="row g-3 align-items-end"> <div class="row g-3 align-items-end">
@ -251,18 +251,18 @@
</div> </div>
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);"> <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" %} <i class="fas fa-users me-1"></i> {% trans "Applications Ready for Document Review" %}
<span class="badge bg-primary-theme ms-2">{{ candidates|length }}</span> <span class="badge bg-primary-theme ms-2">{{ applications|length }}</span>
</h2> </h2>
<div class="kaauh-card p-3"> <div class="kaauh-card p-3">
{% if candidates %} {% if applications %}
<div class="bulk-action-bar p-3 bg-light border-bottom"> <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 #} {# Use d-flex to align the entire contents (two forms and the separator) horizontally #}
<div class="d-flex align-items-end gap-3"> <div class="d-flex align-items-end gap-3">
{# Form 1: Status Update #} {# 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 %} {% csrf_token %}
{# Select Input Group - No label needed for this one, so we just flex the select and button #} {# 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" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug %}" hx-get="{% url 'compose_application_email' job.slug %}"
hx-target="#emailModalBody" hx-target="#emailModalBody"
hx-include="#candidate-form" hx-include="#application-form"
title="Email Participants"> title="Email Participants">
<i class="fas fa-envelope"></i> <i class="fas fa-envelope"></i>
</button> </button>
@ -300,12 +300,12 @@
</div> </div>
<div class="table-responsive"> <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">
<table class="table candidate-table align-middle"> <table class="table application-table align-middle">
<thead> <thead>
<tr> <tr>
<th scope="col" style="width: 2%;"> <th scope="col" style="width: 2%;">
{% if candidates %} {% if applications %}
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" id="selectAllCheckbox"> <input type="checkbox" class="form-check-input" id="selectAllCheckbox">
</div> </div>
@ -329,21 +329,21 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for candidate in candidates %} {% for application in applications %}
<tr> <tr>
<td> <td>
<div class="form-check"> <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> </div>
</td> </td>
<td> <td>
<div class="candidate-name"> <div class="application-name">
{{ candidate.person.first_name }} {{ candidate.person.last_name }} {{ application.person.first_name }} {{ application.person.last_name }}
</div> </div>
</td> </td>
<td><div class="candidate-details"> <td><div class="application-details">
<i class="fas fa-envelope me-1"></i> {{ candidate.person.email }}<br> <i class="fas fa-envelope me-1"></i> {{ application.person.email }}<br>
<i class="fas fa-phone me-1"></i> {{ candidate.person.phone|default:"--" }} <i class="fas fa-phone me-1"></i> {{ application.person.phone|default:"--" }}
</div></td> </div></td>
<td> <td>
<span class="stage-badge stage-Interview"> <span class="stage-badge stage-Interview">
@ -351,7 +351,7 @@
</span> </span>
</td> </td>
<td> <td>
{% with documents=candidate.documents.all %} {% with documents=application.documents.all %}
{% if documents %} {% if documents %}
<table class="table table-sm table-hover"> <table class="table table-sm table-hover">
<thead> <thead>
@ -398,7 +398,7 @@
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="View Profile"> title="View Profile">
<i class="fas fa-eye ms-1"></i> <i class="fas fa-eye ms-1"></i>
@ -412,7 +412,7 @@
{% else %} {% else %}
<div class="alert alert-info text-center" role="alert"> <div class="alert alert-info text-center" role="alert">
<i class="fas fa-info-circle me-1"></i> <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> </div>
{% endif %} {% endif %}
</form> </form>
@ -425,7 +425,7 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);"> <div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);"> <h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Candidate Details / Bulk Action Form" %} {% trans "Application Details / Bulk Action Form" %}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static i18n %} {% 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 %} {% block customCSS %}
<style> <style>
/* KAAT-S UI Variables */ /* KAAT-S UI Variables */
@ -71,8 +71,8 @@
border-color: #00363e; border-color: #00363e;
} }
/* 3. Candidate Table Styling (Aligned with KAAT-S) */ /* 3. Application Table Styling (Aligned with KAAT-S) */
.candidate-table { .application-table {
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
border-collapse: separate; border-collapse: separate;
@ -81,10 +81,10 @@
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.candidate-table thead { .application-table thead {
background-color: var(--kaauh-border); background-color: var(--kaauh-border);
} }
.candidate-table th { .application-table th {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
font-weight: 600; font-weight: 600;
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
@ -92,24 +92,24 @@
font-size: 0.9rem; font-size: 0.9rem;
vertical-align: middle; vertical-align: middle;
} }
.candidate-table td { .application-table td {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border); border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle; vertical-align: middle;
font-size: 0.9rem; font-size: 0.9rem;
} }
.candidate-table tbody tr:hover { .application-table tbody tr:hover {
background-color: #f1f3f4; background-color: #f1f3f4;
} }
.candidate-table thead th:nth-child(1) { width: 40px; } .application-table thead th:nth-child(1) { width: 40px; }
.candidate-table thead th:nth-child(4) { width: 10%; } .application-table thead th:nth-child(4) { width: 10%; }
.candidate-table thead th:nth-child(7) { width: 100px; } .application-table thead th:nth-child(7) { width: 100px; }
.candidate-name { .application-name {
font-weight: 600; font-weight: 600;
color: var(--kaauh-primary-text); color: var(--kaauh-primary-text);
} }
.candidate-details { .application-details {
font-size: 0.8rem; font-size: 0.8rem;
color: #6c757d; color: #6c757d;
} }
@ -129,7 +129,7 @@
font-weight: 700; font-weight: 700;
} }
.bg-applicant { background-color: #6c757d !important; color: white; } .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 Badges */
.stage-badge { .stage-badge {
@ -174,13 +174,13 @@
{% trans "Exam Management" %} - {{ job.title }} {% trans "Exam Management" %} - {{ job.title }}
</h1> </h1>
<h2 class="h5 text-muted mb-0"> <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> </h2>
</div> </div>
<div class="d-flex gap-2"> <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" 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" %} <i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a> </a>
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
@ -194,15 +194,15 @@
</div> </div>
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);"> <h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
{% trans "Candidate List" %} {% trans "Application List" %}
<span class="badge bg-primary-theme ms-2">{{ candidates|length }} / {{ total_candidates }} Total</span> <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> <small class="text-muted fw-normal ms-2">({% trans "Sorted by AI Score" %})</small>
</h2> </h2>
<div class="kaauh-card shadow-sm p-3"> <div class="kaauh-card shadow-sm p-3">
{% if candidates %} {% if applications %}
<div class="bulk-action-bar p-3 bg-light border-bottom"> <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 %} {% csrf_token %}
{# Using d-flex for horizontal alignment and align-items-end to align items to the bottom baseline #} {# 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" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug %}" hx-get="{% url 'compose_application_email' job.slug %}"
hx-target="#emailModalBody" hx-target="#emailModalBody"
hx-include="#candidate-form" hx-include="#application-form"
title="Email Participants"> title="Email Participants">
<i class="fas fa-envelope"></i> <i class="fas fa-envelope"></i>
</button> </button>
@ -245,13 +245,13 @@
</div> </div>
{% endif %} {% endif %}
<div class="table-responsive"> <div class="table-responsive">
<form id="candidate-form" method="post"> <form id="application-form" method="post">
{% csrf_token %} {% csrf_token %}
<table class="table candidate-table align-middle"> <table class="table application-table align-middle">
<thead> <thead>
<tr> <tr>
<th style="width: 2%;"> <th style="width: 2%;">
{% if candidates %} {% if applications %}
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" class="form-check-input" id="selectAllCheckbox"> type="checkbox" class="form-check-input" id="selectAllCheckbox">
@ -268,56 +268,56 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for candidate in candidates %} {% for application in applications %}
<tr> <tr>
<td> <td>
<div class="form-check"> <div class="form-check">
<input <input
name="candidate_ids" name="candidate_ids"
value="{{ candidate.id }}" value="{{ application.id }}"
type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}"> type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
</div> </div>
</td> </td>
<td> <td>
<div class="candidate-name"> <div class="application-name">
{{ candidate.name }} {{ application.name }}
</div> </div>
</td> </td>
<td> <td>
<div class="candidate-details"> <div class="application-details">
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br> <i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
<i class="fas fa-phone me-1"></i> {{ candidate.phone }} <i class="fas fa-phone me-1"></i> {{ application.phone }}
</div> </div>
</td> </td>
<td class="text-center"> <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>
<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>
<td id="exam-score-{{ candidate.pk}}"> <td id="exam-score-{{ application.pk}}">
{{candidate.exam_score|default:"--"}} {{application.exam_score|default:"--"}}
</td> </td>
<td class="text-center" id="status-result-{{ candidate.pk}}"> <td class="text-center" id="status-result-{{ application.pk}}">
{% if not candidate.exam_status %} {% if not application.exam_status %}
<button type="button" class="btn btn-warning btn-sm" <button type="button" class="btn btn-warning btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
{% else %} {% else %}
{% if candidate.exam_status %} {% if application.exam_status %}
<button type="button" class="btn btn-{% if candidate.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm" <button type="button" class="btn btn-{% if application.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
{{ candidate.exam_status }} {{ application.exam_status }}
</button> </button>
{% else %} {% else %}
-- --
@ -329,7 +329,7 @@
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="View Profile"> title="View Profile">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
@ -339,10 +339,10 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if not candidates %} {% if not applications %}
<div class="alert alert-info text-center mt-3 mb-0" role="alert"> <div class="alert alert-info text-center mt-3 mb-0" role="alert">
<i class="fas fa-info-circle me-1"></i> <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> </div>
{% endif %} {% endif %}
</form> </form>
@ -355,14 +355,14 @@
<div class="modal-content kaauh-card"> <div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);"> <h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Candidate Details & Exam Update" %} {% trans "Application Details & Exam Update" %}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div id="candidateviewModalBody" class="modal-body"> <div id="candidateviewModalBody" class="modal-body">
<div class="text-center py-5 text-muted"> <div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br> <i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading candidate data..." %} {% trans "Loading application data..." %}
</div> </div>
</div> </div>
<div class="modal-footer" style="border-top: 1px solid var(--kaauh-border);"> <div class="modal-footer" style="border-top: 1px solid var(--kaauh-border);">

View File

@ -60,8 +60,8 @@
border-color: var(--kaauh-teal-dark); border-color: var(--kaauh-teal-dark);
} }
/* 3. Candidate Table Styling (Aligned with KAAT-S) */ /* 3. Application Table Styling (Aligned with KAAT-S) */
.candidate-table { .application-table {
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
border-collapse: separate; border-collapse: separate;
@ -70,10 +70,10 @@
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.candidate-table thead { .application-table thead {
background-color: var(--kaauh-border); background-color: var(--kaauh-border);
} }
.candidate-table th { .application-table th {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
font-weight: 600; font-weight: 600;
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
@ -81,24 +81,24 @@
font-size: 0.9rem; font-size: 0.9rem;
vertical-align: middle; vertical-align: middle;
} }
.candidate-table td { .application-table td {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border); border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle; vertical-align: middle;
font-size: 0.9rem; font-size: 0.9rem;
} }
.candidate-table tbody tr:hover { .application-table tbody tr:hover {
background-color: #f1f3f4; background-color: #f1f3f4;
} }
.candidate-table thead th:nth-child(1) { width: 40px; } .application-table thead th:nth-child(1) { width: 40px; }
.candidate-table thead th:nth-child(4) { width: 10%; } .application-table thead th:nth-child(4) { width: 10%; }
.candidate-table thead th:nth-child(7) { width: 100px; } .application-table thead th:nth-child(7) { width: 100px; }
.candidate-name { .application-name {
font-weight: 600; font-weight: 600;
color: var(--kaauh-primary-text); color: var(--kaauh-primary-text);
} }
.candidate-details { .application-details {
font-size: 0.8rem; font-size: 0.8rem;
color: #6c757d; color: #6c757d;
} }
@ -118,7 +118,7 @@
font-weight: 700; font-weight: 700;
} }
.bg-applicant { background-color: #6c757d !important; color: white; } .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 Badges */
.stage-badge { .stage-badge {
@ -189,22 +189,22 @@
<div> <div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;"> <h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-trophy me-2"></i> <i class="fas fa-trophy me-2"></i>
{% trans "Hired Candidates" %} - {{ job.title }} {% trans "Hired Applications" %} - {{ job.title }}
</h1> </h1>
<h2 class="h5 text-muted mb-0"> <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> </h2>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="button" <button type="button"
class="btn btn-main-action" class="btn btn-main-action"
onclick="syncHiredCandidates()" 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" %} <i class="fas fa-sync me-1"></i> {% trans "Sync to Sources" %}
</button> </button>
<a href="{% url 'export_candidates_csv' job.slug 'hired' %}" <a href="{% url 'export_applications_csv' job.slug 'hired' %}"
class="btn btn-outline-secondary" 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" %} <i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a> </a>
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
@ -217,7 +217,7 @@
<div class="success-header"> <div class="success-header">
<i class="fas fa-check-circle fa-3x mb-3"></i> <i class="fas fa-check-circle fa-3x mb-3"></i>
<h3 class="mb-2">{% trans "Congratulations!" %}</h3> <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>
<div class="applicant-tracking-timeline"> <div class="applicant-tracking-timeline">
@ -225,9 +225,9 @@
</div> </div>
<div class="kaauh-card shadow-sm p-3"> <div class="kaauh-card shadow-sm p-3">
{% if candidates %} {% if applications %}
<div class="bulk-action-bar p-3 bg-light border-bottom"> <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 %} {% csrf_token %}
{# MODIFIED: Using d-flex for horizontal alignment and align-items-end to align everything based on the baseline of the button/select #} {# 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" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug %}" hx-get="{% url 'compose_application_email' job.slug %}"
hx-target="#emailModalBody" hx-target="#emailModalBody"
hx-include="#candidate-form" hx-include="#application-form"
title="Email Participants"> title="Email Participants">
<i class="fas fa-envelope"></i> <i class="fas fa-envelope"></i>
</button> </button>
@ -269,13 +269,13 @@
{% endif %} {% endif %}
<div class="table-responsive"> <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 %} {% csrf_token %}
<table class="table candidate-table align-middle"> <table class="table application-table align-middle">
<thead> <thead>
<tr> <tr>
<th style="width: 2%"> <th style="width: 2%">
{% if candidates %} {% if applications %}
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" class="form-check-input" id="selectAllCheckbox"> type="checkbox" class="form-check-input" id="selectAllCheckbox">
@ -291,36 +291,36 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for candidate in candidates %} {% for application in applications %}
<tr> <tr>
<td> <td>
<div class="form-check"> <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> </div>
</td> </td>
<td> <td>
<div class="candidate-name"> <div class="application-name">
{{ candidate.name }} {{ application.name }}
</div> </div>
</td> </td>
<td> <td>
<div class="candidate-details"> <div class="application-details">
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br> <i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
<i class="fas fa-phone me-1"></i> {{ candidate.phone }} <i class="fas fa-phone me-1"></i> {{ application.phone }}
</div> </div>
</td> </td>
<td> <td>
<div class="candidate-details"> <div class="application-details">
<strong>{{ job.title }}</strong><br> <strong>{{ job.title }}</strong><br>
<small class="text-muted">{{ job.department }}</small> <small class="text-muted">{{ job.department }}</small>
</div> </div>
</td> </td>
<td class="text-center"> <td class="text-center">
<div class="hired-date"> <div class="hired-date">
{% if candidate.offer_date %} {% if application.offer_date %}
<i class="fas fa-calendar me-1"></i> <i class="fas fa-calendar me-1"></i>
{{ candidate.offer_date|date:"M d, Y" }} {{ application.offer_date|date:"M d, Y" }}
{% else %} {% else %}
<span class="text-muted">--</span> <span class="text-muted">--</span>
{% endif %} {% endif %}
@ -337,12 +337,12 @@
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="View Profile"> title="View Profile">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
<a href="{% url 'candidate_resume_template' candidate.slug %}" <a href="{% url 'application_resume_template' application.slug %}"
class="btn btn-outline-primary btn-sm" class="btn btn-outline-primary btn-sm"
title="View Resume Template"> title="View Resume Template">
<i class="fas fa-file-alt"></i> <i class="fas fa-file-alt"></i>
@ -356,10 +356,10 @@
</tbody> </tbody>
</table> </table>
</form> </form>
{% if not candidates %} {% if not applications %}
<div class="alert alert-info text-center mt-3 mb-0" role="alert"> <div class="alert alert-info text-center mt-3 mb-0" role="alert">
<i class="fas fa-info-circle me-1"></i> <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> </div>
{% endif %} {% endif %}
</div> </div>
@ -372,7 +372,7 @@
<div class="modal-content kaauh-card"> <div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);"> <h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Hired Candidate Details" %} {% trans "Hired Application Details" %}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
@ -399,7 +399,7 @@
<div id="syncResultsModalBody" class="modal-body"> <div id="syncResultsModalBody" class="modal-body">
<div class="text-center py-5 text-muted"> <div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br> <i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Syncing candidates..." %} {% trans "Syncing applications..." %}
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -491,8 +491,8 @@
<div class="spinner-border text-primary mb-3" role="status"> <div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
<h5>{% trans "Syncing hired candidates..." %}</h5> <h5>{% trans "Syncing hired applications..." %}</h5>
<p class="text-muted">{% trans "Please wait while we sync candidates to external sources." %}</p> <p class="text-muted">{% trans "Please wait while we sync applications to external sources." %}</p>
</div> </div>
`; `;
@ -503,7 +503,7 @@
syncButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> {% trans "Syncing..." %}'; syncButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> {% trans "Syncing..." %}';
// Perform sync request // Perform sync request
fetch(`{% url 'sync_hired_candidates' job.slug %}`, { fetch(`{% url 'sync_hired_applications' job.slug %}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -553,7 +553,7 @@
<strong>{% trans "Failed:" %}</strong> <span class="text-danger">${results.failed_syncs}</span> <strong>{% trans "Failed:" %}</strong> <span class="text-danger">${results.failed_syncs}</span>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<strong>{% trans "Candidates Synced:" %}</strong> ${results.total_candidates} <strong>{% trans "Applications Synced:" %}</strong> ${results.total_candidates}
</div> </div>
</div> </div>
</div> </div>
@ -579,7 +579,7 @@
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <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 class="fw-bold">${source.candidates_processed}</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static i18n %} {% load static i18n %}
{% block title %}- {{ job.title }} - ATS{% endblock %} {% block title %}{% blocktrans %}Application Tier Management - {{ job.title }} - ATS {% endblocktrans %}{% endblock %}
{% block customCSS %} {% block customCSS %}
<style> <style>
/* KAAT-S UI Variables */ /* KAAT-S UI Variables */
@ -71,8 +71,8 @@
border-color: #00363e; border-color: #00363e;
} }
/* 3. Candidate Table Styling (Aligned with KAAT-S) */ /* 3. Application Table Styling (Aligned with KAAT-S) */
.candidate-table { .application-table {
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
border-collapse: separate; border-collapse: separate;
@ -81,10 +81,10 @@
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.candidate-table thead { .application-table thead {
background-color: var(--kaauh-border); background-color: var(--kaauh-border);
} }
.candidate-table th { .application-table th {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
font-weight: 600; font-weight: 600;
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
@ -92,24 +92,24 @@
font-size: 0.9rem; font-size: 0.9rem;
vertical-align: middle; vertical-align: middle;
} }
.candidate-table td { .application-table td {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border); border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle; vertical-align: middle;
font-size: 0.9rem; font-size: 0.9rem;
} }
.candidate-table tbody tr:hover { .application-table tbody tr:hover {
background-color: #f1f3f4; background-color: #f1f3f4;
} }
.candidate-table thead th:nth-child(1) { width: 40px; } .application-table thead th:nth-child(1) { width: 40px; }
.candidate-table thead th:nth-child(4) { width: 10%; } .application-table thead th:nth-child(4) { width: 10%; }
.candidate-table thead th:nth-child(7) { width: 100px; } .application-table thead th:nth-child(7) { width: 100px; }
.candidate-name { .application-name {
font-weight: 600; font-weight: 600;
color: var(--kaauh-primary-text); color: var(--kaauh-primary-text);
} }
.candidate-details { .application-details {
font-size: 0.8rem; font-size: 0.8rem;
color: #6c757d; color: #6c757d;
} }
@ -129,7 +129,7 @@
font-weight: 700; font-weight: 700;
} }
.bg-applicant { background-color: #6c757d !important; color: white; } .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 Badges */
.stage-badge { .stage-badge {
@ -177,13 +177,13 @@
{% trans "Interview Management" %} - {{ job.title }} {% trans "Interview Management" %} - {{ job.title }}
</h1> </h1>
<h2 class="h5 text-muted mb-0"> <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> </h2>
</div> </div>
<div class="d-flex gap-2"> <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" 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" %} <i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a> </a>
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
@ -196,13 +196,13 @@
</div> </div>
<div class="kaauh-card shadow-sm p-3"> <div class="kaauh-card shadow-sm p-3">
{% if candidates %} {% if applications %}
<div class="bulk-action-bar p-3 bg-light border-bottom"> <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 #} {# Use d-flex to align the entire contents (two forms and the separator) horizontally #}
<div class="d-flex align-items-end gap-3"> <div class="d-flex align-items-end gap-3">
{# Form 1: Status Update #} {# 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 %} {% csrf_token %}
{# Select Input Group - No label needed for this one, so we just flex the select and button #} {# 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> <div class="vr" style="height: 28px;"></div>
{# Form 2: Schedule Interviews #} {# 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"> <button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %} <i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
</button> </button>
@ -239,9 +239,9 @@
data-bs-toggle="modal" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug %}" hx-get="{% url 'compose_application_email' job.slug %}"
hx-target="#emailModalBody" hx-target="#emailModalBody"
hx-include="#candidate-form" hx-include="#application-form"
title="Email Participants"> title="Email Participants">
<i class="fas fa-envelope"></i> <i class="fas fa-envelope"></i>
</button> </button>
@ -251,13 +251,13 @@
</div> </div>
<div class="table-responsive"> <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 %} {% csrf_token %}
<table class="table candidate-table align-middle"> <table class="table application-table align-middle">
<thead> <thead>
<tr> <tr>
<th style="width: 2%"> <th style="width: 2%">
{% if candidates %} {% if applications %}
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" class="form-check-input" id="selectAllCheckbox"> type="checkbox" class="form-check-input" id="selectAllCheckbox">
@ -276,11 +276,11 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for candidate in candidates %} {% for application in applications %}
<tr> <tr>
<td> <td>
<div class="form-check"> <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> </div>
</td> </td>
@ -288,37 +288,35 @@
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="View Profile"> title="View Profile">
{{ candidate.name }} {{ application.name }}
</button> </button>
{% comment %} <div class="candidate-name">
{{ candidate.name }}
</div> {% endcomment %}
</td> </td>
<td> <td>
<div class="candidate-details"> <div class="application-details">
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br> <i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
<i class="fas fa-phone me-1"></i> {{ candidate.phone }} <i class="fas fa-phone me-1"></i> {{ application.phone }}
</div> </div>
</td> </td>
<td class="candidate-details text-muted"> <td class="application-details text-muted">
{% if candidate.get_latest_meeting %} {% if application.get_latest_meeting %}
{{ candidate.get_latest_meeting }} {{ application.get_latest_meeting }}
{% else %} {% else %}
-- --
{% endif %} {% endif %}
</td> </td>
<td class="candidate-details text-muted"><div class="d-block"> <td class="application-details text-muted"><div class="d-block">
{% if candidate.get_latest_meeting.duration %} {% if application.get_latest_meeting.duration %}
{{ candidate.get_latest_meeting.duration }} {% trans _("Minutes") %} {{ application.get_latest_meeting.duration }} {% trans _("Minutes") %}
{% else %} {% else %}
-- --
{% endif %} {% endif %}
</div></td> </div></td>
<td class="candidate-details text-muted"> <td class="application-details text-muted">
{% with latest_meeting=candidate.get_latest_meeting %} {% with latest_meeting=application.get_latest_meeting %}
{% if latest_meeting %} {% if latest_meeting %}
{{ latest_meeting.start_time|date:"d-m-Y h:i A" }} {{ latest_meeting.start_time|date:"d-m-Y h:i A" }}
{% else %} {% else %}
@ -327,7 +325,7 @@
{% endwith %} {% endwith %}
</td> </td>
<td> <td>
{% with latest_meeting=candidate.get_latest_meeting %} {% with latest_meeting=application.get_latest_meeting %}
{% if latest_meeting and latest_meeting.details_url %} {% 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" <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 %}> {% if latest_meeting.status == 'ended' %}disabled{% endif %}>
@ -341,7 +339,7 @@
</td> </td>
<td> <td>
{{ latest_meeting.status }} {{ latest_meeting.status }}
{% with latest_meeting=candidate.get_latest_meeting %} {% with latest_meeting=application.get_latest_meeting %}
{% if 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 %}"> <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' %} {% if latest_meeting.status == 'started' %}
@ -354,25 +352,25 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</td> </td>
<td class="text-center" id="interview-result-{{ candidate.pk }}"> <td class="text-center" id="interview-result-{{ application.pk }}">
{% if not candidate.interview_status %} {% if not application.interview_status %}
<button type="button" class="btn btn-warning btn-sm" <button type="button" class="btn btn-warning btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
{% else %} {% else %}
{% if candidate.interview_status %} {% if application.interview_status %}
<button type="button" class="btn btn-{% if candidate.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm" <button type="button" class="btn btn-{% if application.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
{{ candidate.interview_status }} {{ application.interview_status }}
</button> </button>
{% else %} {% else %}
-- --
@ -381,13 +379,13 @@
</td> </td>
<td> <td>
{% if candidate.get_latest_meeting %} {% if application.get_latest_meeting %}
{% if candidate.get_latest_meeting.location_type == 'Remote'%} {% if application.get_latest_meeting.location_type == 'Remote'%}
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Reschedule"> title="Reschedule">
<i class="fas fa-redo-alt"></i> <i class="fas fa-redo-alt"></i>
@ -396,7 +394,7 @@
<button type="button" class="btn btn-outline-danger btn-sm" <button type="button" class="btn btn-outline-danger btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Delete Meeting"> title="Delete Meeting">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
@ -406,7 +404,7 @@
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Reschedule"> title="Reschedule">
<i class="fas fa-redo-alt"></i> <i class="fas fa-redo-alt"></i>
@ -415,7 +413,7 @@
<button type="button" class="btn btn-outline-danger btn-sm" <button type="button" class="btn btn-outline-danger btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Delete Meeting"> title="Delete Meeting">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
@ -427,7 +425,7 @@
<button type="button" class="btn btn-main-action btn-sm" <button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
data-modal-title="{% trans 'Schedule Interview' %}" data-modal-title="{% trans 'Schedule Interview' %}"
title="Schedule Interview"> title="Schedule Interview">
@ -437,7 +435,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" data-bs-target="#candidateviewModal"
{# UPDATED: Points to the specific Onsite scheduling URL #} {# 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" hx-target="#candidateviewModalBody"
data-modal-title="{% trans 'Schedule Onsite Interview' %}" data-modal-title="{% trans 'Schedule Onsite Interview' %}"
title="Schedule Onsite Interview"> title="Schedule Onsite Interview">
@ -450,10 +448,10 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if not candidates %} {% if not applications %}
<div class="alert alert-info text-center mt-3 mb-0" role="alert"> <div class="alert alert-info text-center mt-3 mb-0" role="alert">
<i class="fas fa-info-circle me-1"></i> <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> </div>
{% endif %} {% endif %}
</form> </form>
@ -465,7 +463,7 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);"> <div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);"> <h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Candidate Details / Bulk Action Form" %} {% trans "Application Details / Bulk Action Form" %}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
@ -600,7 +598,7 @@
</div> </div>
`; `;
// Reset the modal title to its default state for next use // 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; candidateviewModalLabel.textContent = defaultTitle;
} }
}); });

View File

@ -193,7 +193,7 @@
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;"> <h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-users me-2"></i> {% trans "Applications List" %} <i class="fas fa-users me-2"></i> {% trans "Applications List" %}
</h1> </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" %} <i class="fas fa-plus me-1"></i> {% trans "Add New Application" %}
</a> </a>
</div> </div>
@ -211,7 +211,7 @@
</div> </div>
<div class="col-md-6"> <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"> <form method="GET" class="row g-3 align-items-end h-100">
{% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %} {% 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" %} <i class="fas fa-filter me-1"></i> {% trans "Apply Filters" %}
</button> </button>
{% if job_filter or stage_filter or search_query %} {% 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" %} <i class="fas fa-times me-1"></i> {% trans "Clear" %}
</a> </a>
{% endif %} {% endif %}
@ -286,7 +286,7 @@
<tbody> <tbody>
{% for candidate in applications %} {% for candidate in applications %}
<tr> <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>{{ 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> <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> <td>
@ -297,7 +297,7 @@
</span> </span>
{% endif %} {% endif %}
{% else %} {% 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;"> <svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;">
<circle cx="25" cy="25" r="20"></circle> <circle cx="25" cy="25" r="20"></circle>
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5" <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>{{ candidate.created_at|date:"d-m-Y" }}</td>
<td class="text-end"> <td class="text-end">
<div class="btn-group btn-group-sm" role="group"> <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> <i class="fas fa-eye"></i>
</a> </a>
{% if user.is_staff %} {% 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> <i class="fas fa-edit"></i>
</a> </a>
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}" <button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal" data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'candidate_delete' candidate.slug %}" data-delete-url="{% url 'application_delete' candidate.slug %}"
data-item-name="{{ candidate.name }}"> data-item-name="{{ candidate.name }}">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
</button> </button>
@ -356,7 +356,7 @@
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between align-items-start mb-3"> <div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title flex-grow-1 me-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> </h5>
<span class="badge bg-primary">{{ candidate.stage }}</span> <span class="badge bg-primary">{{ candidate.stage }}</span>
</div> </div>
@ -379,16 +379,16 @@
<div class="mt-auto pt-3 border-top"> <div class="mt-auto pt-3 border-top">
<div class="d-flex gap-2"> <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" %} <i class="fas fa-eye"></i> {% trans "View" %}
</a> </a>
{% if user.is_staff %} {% 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" %} <i class="fas fa-edit"></i> {% trans "Edit" %}
</a> </a>
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}" <button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal" data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'candidate_delete' candidate.slug %}" data-delete-url="{% url 'application_delete' candidate.slug %}"
data-item-name="{{ candidate.name }}"> data-item-name="{{ candidate.name }}">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
</button> </button>
@ -411,7 +411,7 @@
<h3>{% trans "No application found" %}</h3> <h3>{% trans "No application found" %}</h3>
<p class="text-muted">{% trans "Create your first application." %}</p> <p class="text-muted">{% trans "Create your first application." %}</p>
{% if user.is_staff %} {% 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" %} <i class="fas fa-plus me-1"></i> {% trans "Add Application" %}
</a> </a>
{% endif %} {% endif %}

View File

@ -1,7 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static i18n %} {% load static i18n %}
{% block title %}{% blocktrans %}Application Tier Management - {{ job.title }} - ATS {% endblocktrans %}{% endblock %}
{% block title %}- {{ job.title }} - ATS{% endblock %}
{% block customCSS %} {% block customCSS %}
<style> <style>
/* KAAT-S UI Variables */ /* KAAT-S UI Variables */
@ -71,8 +70,8 @@
border-color: #00363e; border-color: #00363e;
} }
/* 3. Candidate Table Styling (Aligned with KAAT-S) */ /* 3. Application Table Styling (Aligned with KAAT-S) */
.candidate-table { .application-table {
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
border-collapse: separate; border-collapse: separate;
@ -81,10 +80,10 @@
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.candidate-table thead { .application-table thead {
background-color: var(--kaauh-border); background-color: var(--kaauh-border);
} }
.candidate-table th { .application-table th {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
font-weight: 600; font-weight: 600;
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
@ -92,24 +91,24 @@
font-size: 0.9rem; font-size: 0.9rem;
vertical-align: middle; vertical-align: middle;
} }
.candidate-table td { .application-table td {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border); border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle; vertical-align: middle;
font-size: 0.9rem; font-size: 0.9rem;
} }
.candidate-table tbody tr:hover { .application-table tbody tr:hover {
background-color: #f1f3f4; background-color: #f1f3f4;
} }
.candidate-table thead th:nth-child(1) { width: 40px; } .application-table thead th:nth-child(1) { width: 40px; }
.candidate-table thead th:nth-child(4) { width: 10%; } .application-table thead th:nth-child(4) { width: 10%; }
.candidate-table thead th:nth-child(7) { width: 100px; } .application-table thead th:nth-child(7) { width: 100px; }
.candidate-name { .application-name {
font-weight: 600; font-weight: 600;
color: var(--kaauh-primary-text); color: var(--kaauh-primary-text);
} }
.candidate-details { .application-details {
font-size: 0.8rem; font-size: 0.8rem;
color: #6c757d; color: #6c757d;
} }
@ -129,7 +128,7 @@
font-weight: 700; font-weight: 700;
} }
.bg-applicant { background-color: #6c757d !important; color: white; } .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 Badges */
.stage-badge { .stage-badge {
@ -176,13 +175,13 @@
{% trans "Offer Management" %} - {{ job.title }} {% trans "Offer Management" %} - {{ job.title }}
</h1> </h1>
<h2 class="h5 text-muted mb-0"> <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> </h2>
</div> </div>
<div class="d-flex gap-2"> <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" 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" %} <i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a> </a>
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
@ -195,13 +194,13 @@
</div> </div>
<div class="kaauh-card shadow-sm p-3"> <div class="kaauh-card shadow-sm p-3">
{% if candidates %} {% if applications %}
<div class="bulk-action-bar p-3 bg-light border-bottom"> <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 #} {# 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"> <div class="d-flex align-items-end gap-3">
{# Form: Hired/Rejected Status Update #} {# 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 %} {% csrf_token %}
{# Select element #} {# Select element #}
@ -236,9 +235,9 @@
data-bs-toggle="modal" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug %}" hx-get="{% url 'compose_application_email' job.slug %}"
hx-target="#emailModalBody" hx-target="#emailModalBody"
hx-include="#candidate-form" hx-include="#application-form"
title="Email Participants"> title="Email Participants">
<i class="fas fa-envelope"></i> <i class="fas fa-envelope"></i>
</button> </button>
@ -247,13 +246,13 @@
</div> </div>
{% endif %} {% endif %}
<div class="table-responsive"> <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 %} {% csrf_token %}
<table class="table candidate-table align-middle"> <table class="table application-table align-middle">
<thead> <thead>
<tr> <tr>
<th style="width: 2%"> <th style="width: 2%">
{% if candidates %} {% if applications %}
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" class="form-check-input" id="selectAllCheckbox"> type="checkbox" class="form-check-input" id="selectAllCheckbox">
@ -270,43 +269,43 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for candidate in candidates %} {% for application in applications %}
<tr> <tr>
<td> <td>
<div class="form-check"> <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> </div>
</td> </td>
<td> <td>
<div class="candidate-name"> <div class="application-name">
{{ candidate.name }} {{ application.name }}
</div> </div>
</td> </td>
<td> <td>
<div class="candidate-details"> <div class="application-details">
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br> <i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
<i class="fas fa-phone me-1"></i> {{ candidate.phone }} <i class="fas fa-phone me-1"></i> {{ application.phone }}
</div> </div>
</td> </td>
<td class="text-center" id="status-result-{{ candidate.pk}}"> <td class="text-center" id="status-result-{{ application.pk}}">
{% if not candidate.offer_status %} {% if not application.offer_status %}
<button type="button" class="btn btn-warning btn-sm" <button type="button" class="btn btn-warning btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
{% else %} {% else %}
{% if candidate.offer_status %} {% if application.offer_status %}
<button type="button" class="btn btn-{% if candidate.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm" <button type="button" class="btn btn-{% if application.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
{{ candidate.offer_status }} {{ application.offer_status }}
</button> </button>
{% else %} {% else %}
-- --
@ -314,7 +313,7 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% with documents=candidate.documents.all %} {% with documents=application.documents.all %}
{% if documents %} {% if documents %}
<table class="table table-sm table-hover"> <table class="table table-sm table-hover">
<thead> <thead>
@ -361,7 +360,7 @@
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="View Profile"> title="View Profile">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
@ -371,10 +370,10 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if not candidates %} {% if not applications %}
<div class="alert alert-info text-center mt-3 mb-0" role="alert"> <div class="alert alert-info text-center mt-3 mb-0" role="alert">
<i class="fas fa-info-circle me-1"></i> <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> </div>
{% endif %} {% endif %}
</form> </form>
@ -387,7 +386,7 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);"> <div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);"> <h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Candidate Details / Bulk Action Form" %} {% trans "Application Details / Bulk Action Form" %}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static i18n %} {% load static i18n %}
{% block title %}Candidate Management - {{ job.title }} - University ATS{% endblock %} {% block title %}Application Management - {{ job.title }} - University ATS{% endblock %}
{% block customCSS %} {% block customCSS %}
<style> <style>
/* KAAT-S UI Variables */ /* KAAT-S UI Variables */
@ -71,8 +71,8 @@
border-color: #00363e; border-color: #00363e;
} }
/* 3. Candidate Table Styling (Aligned with KAAT-S) */ /* 3. Application Table Styling (Aligned with KAAT-S) */
.candidate-table { .application-table {
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
border-collapse: separate; border-collapse: separate;
@ -81,10 +81,10 @@
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.candidate-table thead { .application-table thead {
background-color: var(--kaauh-border); background-color: var(--kaauh-border);
} }
.candidate-table th { .application-table th {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
font-weight: 600; font-weight: 600;
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
@ -92,24 +92,24 @@
font-size: 0.9rem; font-size: 0.9rem;
vertical-align: middle; vertical-align: middle;
} }
.candidate-table td { .application-table td {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border); border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle; vertical-align: middle;
font-size: 0.9rem; font-size: 0.9rem;
} }
.candidate-table tbody tr:hover { .application-table tbody tr:hover {
background-color: #f1f3f4; background-color: #f1f3f4;
} }
.candidate-table thead th:nth-child(1) { width: 40px; } .application-table thead th:nth-child(1) { width: 40px; }
.candidate-table thead th:nth-child(4) { width: 10%; } .application-table thead th:nth-child(4) { width: 10%; }
.candidate-table thead th:nth-child(7) { width: 100px; } .application-table thead th:nth-child(7) { width: 100px; }
.candidate-name { .application-name {
font-weight: 600; font-weight: 600;
color: var(--kaauh-primary-text); color: var(--kaauh-primary-text);
} }
.candidate-details { .application-details {
font-size: 0.8rem; font-size: 0.8rem;
color: #6c757d; color: #6c757d;
} }
@ -129,7 +129,7 @@
font-weight: 700; font-weight: 700;
} }
.bg-applicant { background-color: #6c757d !important; color: white; } .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 Badges */
.stage-badge { .stage-badge {
@ -216,7 +216,7 @@
<div> <div>
<h1 class="h3 mb-1 page-header"> <h1 class="h3 mb-1 page-header">
<i class="fas fa-layer-group me-2"></i> <i class="fas fa-layer-group me-2"></i>
{% trans "Applicant Screening" %} {% trans "Application Screening" %}
</h1> </h1>
<h2 class="h5 text-muted mb-0"> <h2 class="h5 text-muted mb-0">
{% trans "Job:" %} {{ job.title }} {% trans "Job:" %} {{ job.title }}
@ -224,9 +224,9 @@
</h2> </h2>
</div> </div>
<div class="d-flex gap-2"> <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" 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" %} <i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a> </a>
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
@ -241,7 +241,7 @@
<div class="filter-controls"> <div class="filter-controls">
<h4 class="h6 mb-3 fw-bold"> <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> </h4>
<form method="GET" class="mb-0"> <form method="GET" class="mb-0">
@ -296,10 +296,10 @@
<div class="col-auto"> <div class="col-auto">
<label for="tier1_count" class="form-label small text-muted mb-1"> <label for="tier1_count" class="form-label small text-muted mb-1">
{% trans "Top N Candidates" %} {% trans "Top N applications" %}
</label> </label>
<input type="number" name="tier1_count" id="tier1_count" class="form-control form-control-sm" <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>
<div class="col-auto"> <div class="col-auto">
@ -312,14 +312,14 @@
</div> </div>
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);"> <h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
<i class="fas fa-users me-1"></i> {% trans "Candidate List" %} <i class="fas fa-users me-1"></i> {% trans "Application List" %}
<span class="badge bg-primary-theme ms-2">{{ candidates|length }} / {{ total_candidates }} Total</span> <span class="badge bg-primary-theme ms-2">{{ applications|length }} / {{ total_applications }} {% trans "Total" %}</span>
</h2> </h2>
<div class="kaauh-card p-3"> <div class="kaauh-card p-3">
{% if candidates %} {% if applications %}
<div class="bulk-action-bar p-3 bg-light border-bottom"> <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 %} {% csrf_token %}
{# MODIFIED: Using d-flex for horizontal alignment and align-items-end to align everything based on the baseline of the button/select #} {# 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" data-bs-toggle="modal"
hx-boost='true' hx-boost='true'
data-bs-target="#emailModal" data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug %}" hx-get="{% url 'compose_application_email' job.slug %}"
hx-target="#emailModalBody" hx-target="#emailModalBody"
hx-include="#candidate-form" hx-include="#application-form"
title="Email Participants"> title="Email Participants">
<i class="fas fa-envelope"></i> <i class="fas fa-envelope"></i>
</button> </button>
@ -361,13 +361,13 @@
{% endif %} {% endif %}
<div class="table-responsive"> <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 %} {% csrf_token %}
<table class="table candidate-table align-middle"> <table class="table application-table align-middle">
<thead> <thead>
<tr> <tr>
<th scope="col" style="width: 2%;"> <th scope="col" style="width: 2%;">
{% if candidates %} {% if applications %}
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" class="form-check-input" id="selectAllCheckbox"> type="checkbox" class="form-check-input" id="selectAllCheckbox">
@ -401,33 +401,33 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for candidate in candidates %} {% for application in applications %}
<tr> <tr>
<td> <td>
<div class="form-check"> <div class="form-check">
<input <input
name="candidate_ids" name="candidate_ids"
value="{{ candidate.id }}" value="{{ application.id }}"
type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}"> type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
</div> </div>
</td> </td>
<td> <td>
<a href="#" class="candidate-name text-primary-theme text-decoration-none"> <a href="#" class="application-name text-primary-theme text-decoration-none">
{{ candidate.name }} {{ application.name }}
</a> </a>
</td> </td>
<td> <td>
<div class="candidate-details"> <div class="application-details">
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br> <i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
<i class="fas fa-phone me-1"></i> {{ candidate.phone }} <i class="fas fa-phone me-1"></i> {{ application.phone }}
</div> </div>
</td> </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"> <td class="text-center">
{% if candidate.is_resume_parsed %} {% if application.is_resume_parsed %}
{% if candidate.match_score %} {% if application.match_score %}
<span class="badge ai-score-badge"> <span class="badge ai-score-badge">
{{ candidate.match_score|default:"0" }}% {{ application.match_score|default:"0" }}%
</span> </span>
{% endif %} {% endif %}
{% else %} {% else %}
@ -442,23 +442,23 @@
{% endif %} {% endif %}
</td> </td>
<td class="text-center"> <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"> <span class="badge ai-score-badge d-inline-block text-wrap">
{{ candidate.screening_stage_rating|default:"--" }} {{ application.screening_stage_rating|default:"--" }}
</span> </span>
{% endif %} {% endif %}
</td> </td>
<td class="text-center"> <td class="text-center">
{% if candidate.professional_category %} {% if application.professional_category %}
<span class="badge ai-score-badge"> <span class="badge ai-score-badge">
{{ candidate.professional_category }} {{ application.professional_category }}
</span> </span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if candidate.top_3_keywords %} {% if application.top_3_keywords %}
<div class="d-flex flex-wrap gap-2"> <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;"> <span class="badge ai-score-badge" style="font-size: smaller;">
{{ skill }} {{ skill }}
</span> </span>
@ -471,9 +471,9 @@
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="View Candidate Profile and Criteria"> title="View Application Profile and Criteria">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
</td> </td>
@ -481,10 +481,10 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if not candidates %} {% if not applications %}
<div class="alert alert-info text-center mt-3 mb-0" role="alert"> <div class="alert alert-info text-center mt-3 mb-0" role="alert">
<i class="fas fa-info-circle me-1"></i> <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> </div>
{% endif %} {% endif %}
</form> </form>
@ -498,14 +498,14 @@
<div class="modal-content kaauh-card"> <div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);"> <h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
{% trans "Candidate Criteria Review" %} {% trans "Application Criteria Review" %}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div id="candidateviewModalBody" class="modal-body"> <div id="candidateviewModalBody" class="modal-body">
<div class="text-center py-5 text-muted"> <div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br> <i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading candidate data..." %} {% trans "Loading application data..." %}
</div> </div>
</div> </div>
<div class="modal-footer" style="border-top: 1px solid var(--kaauh-border);"> <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"> <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2"> <div class="flex space-x-2">
{% if document.file %} {% 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" class="text-green-600 hover:text-green-800 mr-3"
title="Download document"> title="Download document">
<i class="fas fa-download"></i> <i class="fas fa-download"></i>

View File

@ -184,7 +184,7 @@
{% endif %} {% endif %}
</td> </td>
<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"> class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye me-1"></i> <i class="fas fa-eye me-1"></i>
{% trans "View Details" %} {% 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 */ /* Funnel Specific Styles */
#candidate_funnel_chart { #application_funnel_chart {
max-height: 400px; max-height: 400px;
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
@ -224,7 +224,7 @@
<div class="card-header"> <div class="card-header">
<h2> <h2>
<i class="fas fa-chart-area stat-icon"></i> <i class="fas fa-chart-area stat-icon"></i>
{% trans "Daily Candidate Applications Trend" %} {% trans "Daily Applications Trend" %}
</h2> </h2>
</div> </div>
<div class="chart-container"> <div class="chart-container">
@ -261,7 +261,7 @@
</h2> </h2>
</div> </div>
<div class="chart-container d-flex justify-content-center align-items-center"> <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> </div>
</div> </div>
@ -289,11 +289,11 @@
<script> <script>
// Pass context data safely to JavaScript // 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 jobTitles = JSON.parse('{{ job_titles|escapejs }}').slice(0, 5);
const jobAppCounts = JSON.parse('{{ job_app_counts|escapejs }}').slice(0, 5); const jobAppCounts = JSON.parse('{{ job_app_counts|escapejs }}').slice(0, 5);
const stages = JSON.parse('{{ candidate_stage|escapejs }}'); const stages = JSON.parse('{{ application_stage|escapejs }}');
const counts = JSON.parse('{{ candidates_count|safe }}'); const counts = JSON.parse('{{ application_count|safe }}');
// --- 1. BAR CHART configuration (Top 5 Applications) --- // --- 1. BAR CHART configuration (Top 5 Applications) ---
const ctxBar = document.getElementById('applicationsChart').getContext('2d'); const ctxBar = document.getElementById('applicationsChart').getContext('2d');
@ -358,7 +358,7 @@
// Slice and use the first N shades based on the number of stages // Slice and use the first N shades based on the number of stages
const stageColors = tealShades.slice(tealShades.length - stages.length); 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, { new Chart(ctxFunnel, {
type: 'bar', type: 'bar',
@ -377,7 +377,7 @@
}, },
// 2. VISIBLE CANDIDATE COUNT DATASET // 2. VISIBLE CANDIDATE COUNT DATASET
{ {
label: '{% trans "Candidate Count" %}', label: '{% trans "Application Count" %}',
data: counts, data: counts,
backgroundColor: stageColors, backgroundColor: stageColors,
barThickness: 50 barThickness: 50
@ -490,7 +490,7 @@
}, },
y: { y: {
beginAtZero: true, beginAtZero: true,
title: { display: true, text: '{% trans "New Candidates" %}' }, title: { display: true, text: '{% trans "New Applications" %}' },
ticks: { precision: 0 }, ticks: { precision: 0 },
grid: { color: '#e0e0e0' } grid: { color: '#e0e0e0' }
} }

View File

@ -59,7 +59,7 @@
<button type="button" class="btn btn-main-action" <button type="button" class="btn btn-main-action"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="{% trans 'View Details and Score Breakdown' %}"> title="{% trans 'View Details and Score Breakdown' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>

View File

@ -1,4 +1,5 @@
{% load i18n %} {% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% with request.resolver_match as resolved %} {% with request.resolver_match as resolved %}
{% if 'resume-template' in resolved.route and resolved.kwargs.slug == candidate.slug %} {% 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;"> <nav aria-label="breadcrumb" style="font-family: 'Inter', sans-serif; margin-bottom:1.5rem;">
@ -59,7 +60,7 @@
</li> </li>
<li style="display: flex; align-items: center;"> <li style="display: flex; align-items: center;">
<a <a
href="{% url 'candidate_list' %}" href="{% url 'application_list' %}"
style=" style="
color: #6c757d; /* text-secondary/gray */ color: #6c757d; /* text-secondary/gray */
text-decoration: none; text-decoration: none;
@ -81,7 +82,7 @@
</li> </li>
<li style="display: flex; align-items: center;"> <li style="display: flex; align-items: center;">
<a <a
href="{% url 'candidate_detail' candidate.slug %}" href="{% url 'application_detail' candidate.slug %}"
style=" style="
color: #6c757d; /* text-secondary/gray */ color: #6c757d; /* text-secondary/gray */
text-decoration: none; text-decoration: none;
@ -90,7 +91,11 @@
onmouseover="this.style.color='#000000';" onmouseover="this.style.color='#000000';"
onmouseout="this.style.color='#6c757d';" 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> </a>
</li> </li>

View File

@ -1,18 +1,18 @@
<td class="text-center" id="status-result-{{ candidate.pk}}"> <td class="text-center" id="status-result-{{ application.pk}}">
{% if candidate.exam_status %} {% if application.exam_status %}
<button type="button" class="btn btn-{% if candidate.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm" <button type="button" class="btn btn-{% if application.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
{{ candidate.exam_status }} {{ application.exam_status }}
</button> </button>
{% else %} {% else %}
-- --
{% endif %} {% endif %}
</td> </td>
<td id="exam-score-{{ candidate.pk}}" hx-swap-oob="true"> <td id="exam-score-{{ application.pk}}" hx-swap-oob="true">
{{candidate.exam_score|default:"--"}} {{application.exam_score|default:"--"}}
</td> </td>

View File

@ -1,22 +1,22 @@
<td class="text-center" id="interview-result-{{ candidate.pk}}"> <td class="text-center" id="interview-result-{{ application.pk}}">
{% if not candidate.interview_status %} {% if not application.interview_status %}
<button type="button" class="btn btn-warning btn-sm" <button type="button" class="btn btn-warning btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
{% else %} {% else %}
{% if candidate.interview_status %} {% if application.interview_status %}
<button type="button" class="btn btn-{% if candidate.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm" <button type="button" class="btn btn-{% if application.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
{{ candidate.interview_status }} {{ application.interview_status }}
</button> </button>
{% else %} {% else %}
-- --

View File

@ -1,22 +1,22 @@
<td class="text-center" id="status-result-{{ candidate.pk}}"> <td class="text-center" id="status-result-{{ application.pk}}">
{% if not candidate.offer_status %} {% if not application.offer_status %}
<button type="button" class="btn btn-warning btn-sm" <button type="button" class="btn btn-warning btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
{% else %} {% else %}
{% if candidate.offer_status %} {% if application.offer_status %}
<button type="button" class="btn btn-{% if candidate.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm" <button type="button" class="btn btn-{% if application.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" 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" hx-target="#candidateviewModalBody"
title="Pass Exam"> title="Pass Exam">
{{ candidate.offer_status }} {{ application.offer_status }}
</button> </button>
{% else %} {% else %}
-- --

View File

@ -1,5 +1,5 @@
<!-- Stage Update Form with Errors --> <!-- 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 %} {% csrf_token %}
<!-- Stage Selection --> <!-- Stage Selection -->

View File

@ -1,10 +1,11 @@
<!-- Stage Update Modal --> <!-- Stage Update Modal -->
{% load i18n%}
<div class="modal fade" id="stageUpdateModal" tabindex="-1" aria-labelledby="stageUpdateModalLabel" aria-hidden="true"> <div class="modal fade" id="stageUpdateModal" tabindex="-1" aria-labelledby="stageUpdateModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-top"> <div class="modal-dialog modal-dialog-top">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="stageUpdateModalLabel"> <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> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
@ -16,19 +17,19 @@
<div class="alert alert-info mb-4"> <div class="alert alert-info mb-4">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<i class="fas fa-info-circle me-2"></i> <i class="fas fa-info-circle me-2"></i>
<strong>Current Stage:</strong> <strong>{% trans "Current Stage:" %}</strong>
<span class="badge bg-primary ms-2" id="currentStageBadge">{{candidate.stage}}</span> <span class="badge bg-primary ms-2" id="currentStageBadge">{{application.stage}}</span>
</div> </div>
</div> </div>
<!-- Form Container --> <!-- Form Container -->
<div id="stageUpdateFormContainer"> <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 %} {% csrf_token %}
<!-- Stage Selection --> <!-- Stage Selection -->
<div class="mb-3"> <div class="mb-3">
<label for="id_stage" class="form-label"> <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> </label>
<select id="id_stage" name="stage" class="form-select"> <select id="id_stage" name="stage" class="form-select">
@ -42,19 +43,19 @@
<div class="alert alert-light border" id="availableStagesInfo"> <div class="alert alert-light border" id="availableStagesInfo">
<small class="text-muted"> <small class="text-muted">
<i class="fas fa-lightbulb me-1"></i> <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> </small>
</div> </div>
<!-- Form Actions --> <!-- Form Actions -->
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"> <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>
<button type="submit" class="btn btn-main-action" id="stageUpdateSubmit"> <button type="submit" class="btn btn-main-action" id="stageUpdateSubmit">
<i class="fas fa-save me-1"></i> <i class="fas fa-save me-1"></i>
<span class="ms-2">Update</span> <span class="ms-2">{% trans "Update" %}</span>
</button> </button>
</div> </div>
</form> </form>
@ -73,26 +74,26 @@ function updateStageInfo(selectedValue, currentStage) {
if (selectedValue && selectedValue !== currentStage) { if (selectedValue && selectedValue !== currentStage) {
const stageLabels = { const stageLabels = {
'Applied': 'Applied', 'Applied': '{% trans "Applied" %}',
'Exam': 'Exam', 'Exam': '{% trans "Exam" %}',
'Interview': 'Interview', 'Interview': '{% trans "Interview" %}',
'Offer': 'Offer' 'Offer': '{% trans "Offer" %}'
}; };
if (availableStagesText) { 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.remove('alert-warning', 'alert-light');
availableStagesText.parentElement.parentElement.classList.add('alert-success'); availableStagesText.parentElement.parentElement.classList.add('alert-success');
} }
} else if (selectedValue === currentStage) { } else if (selectedValue === currentStage) {
if (availableStagesText) { 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.remove('alert-success', 'alert-warning');
availableStagesText.parentElement.parentElement.classList.add('alert-info'); availableStagesText.parentElement.parentElement.classList.add('alert-info');
} }
} else { } else {
if (availableStagesText) { 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.remove('alert-success', 'alert-warning');
availableStagesText.parentElement.parentElement.classList.add('alert-light'); availableStagesText.parentElement.parentElement.classList.add('alert-light');
} }

View File

@ -25,9 +25,9 @@
{# SCOPED - 3. Total Candidates #} {# SCOPED - 3. Total Candidates #}
<div class="card"> <div class="card">
<div class="card-header"> <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>
<div class="stat-value">{{ total_candidates }}</div> <div class="stat-value">{{ total_applications }}</div>
<div class="stat-caption">{% trans "Total applications" %}</div> <div class="stat-caption">{% trans "Total applications" %}</div>
</div> </div>

View File

@ -10,20 +10,20 @@
<h1 class="h3 mb-1"> <h1 class="h3 mb-1">
<i class="fas fa-calendar-plus me-2"></i> <i class="fas fa-calendar-plus me-2"></i>
{% if has_future_meeting %} {% if has_future_meeting %}
{% trans "Update Interview" %} for {{ candidate.name }} {% trans "Update Interview" %} for {{ application.name }}
{% else %} {% else %}
{% trans "Schedule Interview" %} for {{ candidate.name }} {% trans "Schedule Interview" %} for {{ application.name }}
{% endif %} {% endif %}
</h1> </h1>
<p class="text-muted mb-0">{% trans "Job" %}: {{ job.title }}</p> <p class="text-muted mb-0">{% trans "Job" %}: {{ job.title }}</p>
{% if has_future_meeting %} {% if has_future_meeting %}
<div class="alert alert-info mt-2 mb-0" role="alert"> <div class="alert alert-info mt-2 mb-0" role="alert">
<i class="fas fa-info-circle me-1"></i> <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> </div>
{% endif %} {% endif %}
</div> </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" %} <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Candidates" %}
</a> </a>
</div> </div>
@ -85,7 +85,7 @@
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i> {% trans "Schedule Meeting" %} <i class="fas fa-save me-1"></i> {% trans "Schedule Meeting" %}
</button> </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" %} <i class="fas fa-times me-1"></i> {% trans "Cancel" %}
</a> </a>
</div> </div>

View File

@ -1,7 +1,7 @@
{% extends "base.html" %} {% 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 %} {% block customCSS %}
<style> <style>
@ -142,34 +142,34 @@
<!-- Page Header --> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div> <div>
<h1 class="page-header h3 mb-1">Assign Staff to Job</h1> <h1 class="page-header h3 mb-1">{% trans "Assign Staff to Job" %}</h1>
<p class="text-secondary mb-0">Job: {{ job.title }} ({{ job.internal_job_id }})</p> <p class="text-secondary mb-0">{% trans "Job:" %} {{ job.title }} ({{ job.internal_job_id }})</p>
</div> </div>
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary"> <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> </a>
</div> </div>
<!-- Job Information Card --> <!-- Job Information Card -->
<div class="kaauh-card mb-4"> <div class="kaauh-card mb-4">
<div class="job-header-card"> <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>
<div class="card-body"> <div class="card-body">
<div class="row mx-2 my-2"> <div class="row mx-2 my-2">
<div class="col-md-6"> <div class="col-md-6">
<p><strong>Department:</strong> {{ job.get_department_display|default:job.department }}</p> <p><strong>{% trans "Department:" %}</strong> {{ job.get_department_display|default:job.department }}</p>
<p><strong>Job Type:</strong> {{ job.get_job_type_display }}</p> <p><strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}</p>
<p><strong>Workplace Type:</strong> {{ job.get_workplace_type_display }}</p> <p><strong>{% trans "Workplace Type:" %}</strong> {{ job.get_workplace_type_display }}</p>
</div> </div>
<div class="col-md-6"> <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 %}"> <span class="badge {% if job.status == 'ACTIVE' %}bg-success{% elif job.status == 'CLOSED' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ job.get_status_display }} {{ job.get_status_display }}
</span> </span>
</p> </p>
<p><strong>Applications:</strong> {{ applications.count }}</p> <p><strong>{% trans "Applications:" %}</strong> {{ applications.count }}</p>
<p><strong>Created:</strong> {{ job.created_at|date:"M d, Y" }}</p> <p><strong>{% trans "Created:" %}</strong> {{ job.created_at|date:"M d, Y" }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -178,7 +178,7 @@
<!-- Staff Assignment Form --> <!-- Staff Assignment Form -->
<div class="kaauh-card"> <div class="kaauh-card">
<div class="card-header"> <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>
<div class="card-body"> <div class="card-body">
{% if messages %} {% if messages %}
@ -248,10 +248,10 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<button type="submit" class="btn btn-main-action"> <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> </button>
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary ms-2"> <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> </a>
</div> </div>
</div> </div>
@ -263,18 +263,18 @@
{% if job.staff_assignments.exists %} {% if job.staff_assignments.exists %}
<div class="kaauh-card mt-4"> <div class="kaauh-card mt-4">
<div class="card-header"> <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>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Staff Member</th> <th>{% trans "Staff Member" %}</th>
<th>Email</th> <th>{% trans "Email" %}</th>
<th>Assignment Date</th> <th>{% trans "Assignment Date" %}</th>
<th>Notes</th> <th>{% trans "Notes" %}</th>
<th>Status</th> <th>{% trans "Status" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -283,16 +283,16 @@
<td> <td>
<strong>{{ assignment.staff.get_full_name|default:assignment.staff.username }}</strong> <strong>{{ assignment.staff.get_full_name|default:assignment.staff.username }}</strong>
{% if assignment.staff.is_active %} {% 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 %} {% else %}
<span class="badge bg-danger ms-2">Inactive</span> <span class="badge bg-danger ms-2">{% trans "Inactive" %}</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ assignment.staff.email }}</td> <td>{{ assignment.staff.email }}</td>
<td>{{ assignment.assignment_date|date:"M d, Y" }}</td> <td>{{ assignment.assignment_date|date:"M d, Y" }}</td>
<td>{{ assignment.notes|default:"-" }}</td> <td>{{ assignment.notes|default:"-" }}</td>
<td> <td>
<span class="badge bg-primary">Assigned</span> <span class="badge bg-primary">{% trans "Assigned" %}</span>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -306,7 +306,7 @@
<!-- Available Staff Members --> <!-- Available Staff Members -->
<div class="kaauh-card mt-4"> <div class="kaauh-card mt-4">
<div class="card-header"> <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>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
@ -329,11 +329,11 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
{% if staff_user.is_active %} {% if staff_user.is_active %}
<span class="badge bg-success">Active</span> <span class="badge bg-success">{% trans "Active" %}</span>
{% else %} {% else %}
<span class="badge bg-danger">Inactive</span> <span class="badge bg-danger">{% trans "Inactive" %}</span>
{% endif %} {% endif %}
<span class="badge bg-primary ms-1">Staff</span> <span class="badge bg-primary ms-1">{% trans "Staff" %}</span>
</div> </div>
</div> </div>
</div> </div>
@ -342,7 +342,7 @@
<div class="col-12"> <div class="col-12">
<div class="alert alert-warning"> <div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i> <i class="fas fa-exclamation-triangle me-2"></i>
No staff members available. Please create staff accounts first. {% trans "No staff members available. Please create staff accounts first." %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@ -359,18 +359,18 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="staffAssignmentModalLabel"> <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> </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>
<div class="modal-body"> <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 id="selectedStaffInfo"></div>
</div> </div>
<div class="modal-footer"> <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"> <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> </button>
</div> </div>
</div> </div>
@ -398,16 +398,16 @@ document.addEventListener('DOMContentLoaded', function() {
// Show selected staff info in modal // Show selected staff info in modal
selectedStaffInfo.innerHTML = ` selectedStaffInfo.innerHTML = `
<div class="alert alert-info"> <div class="alert alert-info">
<strong>Selected Staff Member:</strong><br> <strong>{% trans "Selected Staff Member:" %}</strong><br>
Name: ${selectedOption.text}<br> {% trans "Name:" %} ${selectedOption.text}<br>
This assignment will take effect immediately. {% trans "This assignment will take effect immediately." %}
</div> </div>
`; `;
// Show modal // Show modal
modal.show(); modal.show();
} else { } else {
alert('Please select a staff member.'); alert('{% trans "Please select a staff member." %}');
} }
}); });
} }