done with the refractoring 2 in the branch ai_overview for candidate to pplication still lot left
This commit is contained in:
parent
1ad85b82e1
commit
8b51416e10
@ -35,7 +35,7 @@ class CandidateSyncService:
|
||||
}
|
||||
|
||||
# Get all hired candidates for this job
|
||||
hired_candidates = list(job.hired_candidates.select_related('job'))
|
||||
hired_candidates = list(job.hired_applications.select_related('job'))
|
||||
|
||||
results['total_candidates'] = len(hired_candidates)
|
||||
|
||||
|
||||
@ -132,7 +132,7 @@ class JobPosting(Base):
|
||||
# Application Information ---job detail apply link for the candidates
|
||||
application_url = models.URLField(
|
||||
validators=[URLValidator()],
|
||||
help_text="URL where candidates apply",
|
||||
help_text="URL where applicants apply",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
@ -222,7 +222,7 @@ class JobPosting(Base):
|
||||
related_name="jobs",
|
||||
verbose_name=_("Hiring Agency"),
|
||||
help_text=_(
|
||||
"External agency responsible for sourcing candidates for this role"
|
||||
"External agency responsible for sourcing applicants for this role"
|
||||
),
|
||||
)
|
||||
cancel_reason = models.TextField(
|
||||
@ -365,7 +365,7 @@ class JobPosting(Base):
|
||||
|
||||
@property
|
||||
def current_applications_count(self):
|
||||
"""Returns the current number of candidates associated with this job."""
|
||||
"""Returns the current number of applications associated with this job."""
|
||||
return self.applications.count()
|
||||
|
||||
@property
|
||||
@ -377,7 +377,7 @@ class JobPosting(Base):
|
||||
return self.current_applications_count >= self.max_applications
|
||||
|
||||
@property
|
||||
def all_candidates(self):
|
||||
def all_applications(self):
|
||||
# 1. Define the safe JSON extraction and conversion expression
|
||||
safe_score_expression = Cast(
|
||||
Coalesce(
|
||||
@ -397,68 +397,70 @@ class JobPosting(Base):
|
||||
).order_by("-sortable_score")
|
||||
|
||||
@property
|
||||
def screening_candidates(self):
|
||||
return self.all_candidates.filter(stage="Applied")
|
||||
def screening_applications(self):
|
||||
return self.all_applications.filter(stage="Applied")
|
||||
|
||||
@property
|
||||
def exam_candidates(self):
|
||||
return self.all_candidates.filter(stage="Exam")
|
||||
def exam_applications(self):
|
||||
return self.all_applications.filter(stage="Exam")
|
||||
|
||||
@property
|
||||
def interview_candidates(self):
|
||||
return self.all_candidates.filter(stage="Interview")
|
||||
def interview_applications(self):
|
||||
return self.all_applications.filter(stage="Interview")
|
||||
|
||||
@property
|
||||
def document_review_candidates(self):
|
||||
return self.all_candidates.filter(stage="Document Review")
|
||||
def document_review_applications(self):
|
||||
return self.all_applications.filter(stage="Document Review")
|
||||
|
||||
@property
|
||||
def offer_candidates(self):
|
||||
return self.all_candidates.filter(stage="Offer")
|
||||
def offer_applications(self):
|
||||
return self.all_applications.filter(stage="Offer")
|
||||
|
||||
@property
|
||||
def accepted_candidates(self):
|
||||
return self.all_candidates.filter(offer_status="Accepted")
|
||||
def accepted_applications(self):
|
||||
return self.all_applications.filter(offer_status="Accepted")
|
||||
|
||||
@property
|
||||
def hired_candidates(self):
|
||||
return self.all_candidates.filter(stage="Hired")
|
||||
def hired_applications(self):
|
||||
return self.all_applications.filter(stage="Hired")
|
||||
|
||||
# counts
|
||||
@property
|
||||
def all_candidates_count(self):
|
||||
return self.all_candidates.count()
|
||||
def all_applications_count(self):
|
||||
return self.all_applications.count()
|
||||
|
||||
|
||||
@property
|
||||
def screening_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Applied").count() or 0
|
||||
def screening_applications_count(self):
|
||||
return self.all_applications.filter(stage="Applied").count() or 0
|
||||
|
||||
@property
|
||||
def exam_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Exam").count() or 0
|
||||
def exam_applications_count(self):
|
||||
return self.all_applications.filter(stage="Exam").count() or 0
|
||||
|
||||
@property
|
||||
def interview_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Interview").count() or 0
|
||||
def interview_applications_count(self):
|
||||
return self.all_applications.filter(stage="Interview").count() or 0
|
||||
|
||||
@property
|
||||
def document_review_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Document Review").count() or 0
|
||||
def document_review_applications_count(self):
|
||||
return self.all_applications.filter(stage="Document Review").count() or 0
|
||||
|
||||
@property
|
||||
def offer_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Offer").count() or 0
|
||||
def offer_applications_count(self):
|
||||
return self.all_applications.filter(stage="Offer").count() or 0
|
||||
|
||||
@property
|
||||
def hired_candidates_count(self):
|
||||
return self.all_candidates.filter(stage="Hired").count() or 0
|
||||
def hired_applications_count(self):
|
||||
return self.all_applications.filter(stage="Hired").count() or 0
|
||||
|
||||
@property
|
||||
def vacancy_fill_rate(self):
|
||||
total_positions = self.open_positions
|
||||
print(total_positions)
|
||||
|
||||
no_of_positions_filled = self.applications.filter(stage__in=["HIRED"]).count()
|
||||
no_of_positions_filled = self.applications.filter(stage__in=["Hired"]).count()
|
||||
print(no_of_positions_filled)
|
||||
|
||||
if total_positions > 0:
|
||||
vacancy_fill_rate = no_of_positions_filled / total_positions
|
||||
|
||||
@ -866,7 +866,7 @@ def sync_hired_candidates_task(job_slug):
|
||||
# action=IntegrationLog.ActionChoices.SYNC,
|
||||
# endpoint="multi_source_sync",
|
||||
# method="BACKGROUND_TASK",
|
||||
# request_data={"job_slug": job_slug, "candidate_count": job.accepted_candidates.count()},
|
||||
# request_data={"job_slug": job_slug, "candidate_count": job.accepted_applications.count()},
|
||||
# response_data=results,
|
||||
# status_code="SUCCESS" if results.get('summary', {}).get('failed', 0) == 0 else "PARTIAL",
|
||||
# ip_address="127.0.0.1", # Background task
|
||||
|
||||
@ -452,13 +452,13 @@ def edit_job(request, slug):
|
||||
if form.is_valid():
|
||||
try:
|
||||
form.save()
|
||||
messages.success(request, f'Job "{job.title}" updated successfully!')
|
||||
messages.success(request, _('Job "%(title)s" updated successfully!') % {'title': job.title})
|
||||
return redirect("job_list")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating job: {e}")
|
||||
messages.error(request, f"Error updating job: {e}")
|
||||
messages.error(request, _('Error updating job: %(error)s') % {'error': e})
|
||||
else:
|
||||
messages.error(request, "Please correct the errors below.")
|
||||
messages.error(request, _("Please correct the errors below."))
|
||||
else:
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
form = JobPostingForm(instance=job)
|
||||
@ -478,18 +478,18 @@ def job_detail(request, slug):
|
||||
"""View details of a specific job"""
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
# Get all applications for this job, ordered by most recent
|
||||
applicants = job.applications.all().order_by("-created_at")
|
||||
applications = job.applications.all().order_by("-created_at")
|
||||
|
||||
# Count applications by stage for summary statistics
|
||||
total_applicant = applicants.count()
|
||||
total_applications = applications.count()
|
||||
|
||||
applied_count = applicants.filter(stage="Applied").count()
|
||||
applied_count = applications.filter(stage="Applied").count()
|
||||
|
||||
exam_count = applicants.filter(stage="Exam").count()
|
||||
exam_count = applications.filter(stage="Exam").count()
|
||||
|
||||
interview_count = applicants.filter(stage="Interview").count()
|
||||
interview_count = applications.filter(stage="Interview").count()
|
||||
|
||||
offer_count = applicants.filter(stage="Offer").count()
|
||||
offer_count = applications.filter(stage="Offer").count()
|
||||
|
||||
status_form = JobPostingStatusForm(instance=job)
|
||||
linkedin_content_form = LinkedPostContentForm(instance=job)
|
||||
@ -528,8 +528,8 @@ def job_detail(request, slug):
|
||||
|
||||
# --- 2. Quality Metrics (JSON Aggregation) ---
|
||||
|
||||
candidates_with_score = applicants.filter(is_resume_parsed=True)
|
||||
total_candidates = candidates_with_score.count() # For context
|
||||
applications_with_score = applications.filter(is_resume_parsed=True)
|
||||
total_applications_ = applications_with_score.count() # For context
|
||||
|
||||
# Define the queryset for applications that have been parsed
|
||||
score_expression = Cast(
|
||||
@ -544,34 +544,34 @@ def job_detail(request, slug):
|
||||
)
|
||||
|
||||
# 2. ANNOTATE the queryset with the new field
|
||||
candidates_with_score = candidates_with_score.annotate(
|
||||
applications_with_score = applications_with_score.annotate(
|
||||
annotated_match_score=score_expression
|
||||
)
|
||||
|
||||
avg_match_score_result = candidates_with_score.aggregate(
|
||||
avg_match_score_result = applications_with_score.aggregate(
|
||||
avg_score=Avg('annotated_match_score')
|
||||
)
|
||||
avg_match_score = avg_match_score_result.get("avg_score") or 0.0
|
||||
|
||||
high_potential_count = candidates_with_score.filter(
|
||||
high_potential_count = applications_with_score.filter(
|
||||
annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD
|
||||
).count()
|
||||
|
||||
high_potential_ratio = (
|
||||
round((high_potential_count / total_candidates) * 100, 1)
|
||||
if total_candidates > 0
|
||||
round((high_potential_count / total_applications_) * 100, 1)
|
||||
if total_applications_ > 0
|
||||
else 0
|
||||
)
|
||||
|
||||
# --- 3. Time Metrics (Duration Aggregation) ---
|
||||
|
||||
# Metric: Average Time from Applied to Interview (T2I)
|
||||
t2i_candidates = applicants.filter(interview_date__isnull=False).annotate(
|
||||
t2i_applications = applications.filter(interview_date__isnull=False).annotate(
|
||||
time_to_interview=ExpressionWrapper(
|
||||
F("interview_date") - F("created_at"), output_field=DurationField()
|
||||
)
|
||||
)
|
||||
avg_t2i_duration = t2i_candidates.aggregate(avg_t2i=Avg("time_to_interview"))[
|
||||
avg_t2i_duration = t2i_applications.aggregate(avg_t2i=Avg("time_to_interview"))[
|
||||
"avg_t2i"
|
||||
]
|
||||
|
||||
@ -583,14 +583,14 @@ def job_detail(request, slug):
|
||||
)
|
||||
|
||||
# Metric: Average Time in Exam Stage
|
||||
t_in_exam_candidates = applicants.filter(
|
||||
t_in_exam_applications = applications.filter(
|
||||
exam_date__isnull=False, interview_date__isnull=False
|
||||
).annotate(
|
||||
time_in_exam=ExpressionWrapper(
|
||||
F("interview_date") - F("exam_date"), output_field=DurationField()
|
||||
)
|
||||
)
|
||||
avg_t_in_exam_duration = t_in_exam_candidates.aggregate(
|
||||
avg_t_in_exam_duration = t_in_exam_applications.aggregate(
|
||||
avg_t_in_exam=Avg("time_in_exam")
|
||||
)["avg_t_in_exam"]
|
||||
|
||||
@ -602,26 +602,27 @@ def job_detail(request, slug):
|
||||
)
|
||||
|
||||
category_data = (
|
||||
applicants.filter(ai_analysis_data__analysis_data__category__isnull=False)
|
||||
.values("ai_analysis_data__analysis_data__category")
|
||||
applications.filter(ai_analysis_data__analysis_data_en__category__isnull=False)
|
||||
.values("ai_analysis_data__analysis_data_en__category")
|
||||
.annotate(
|
||||
candidate_count=Count("id"),
|
||||
application_count=Count("id"),
|
||||
category=Cast(
|
||||
"ai_analysis_data__analysis_data__category", output_field=CharField()
|
||||
"ai_analysis_data__analysis_data_en__category", output_field=CharField()
|
||||
),
|
||||
)
|
||||
.order_by("ai_analysis_data__analysis_data__category")
|
||||
.order_by("ai_analysis_data__analysis_data_en__category")
|
||||
)
|
||||
# Prepare data for Chart.js
|
||||
print(category_data)
|
||||
|
||||
categories = [item["category"] for item in category_data]
|
||||
candidate_counts = [item["candidate_count"] for item in category_data]
|
||||
|
||||
applications_count = [item["application_count"] for item in category_data]
|
||||
# avg_scores = [round(item['avg_match_score'], 2) if item['avg_match_score'] is not None else 0 for item in category_data]
|
||||
|
||||
context = {
|
||||
"job": job,
|
||||
"applicants": applicants,
|
||||
"total_applicants": total_applicant, # This was total_candidates in the prompt, using total_applicant for consistency
|
||||
"applications": applications,
|
||||
"total_applications": total_applications, # This was total_candidates in the prompt, using total_applicant for consistency
|
||||
"applied_count": applied_count,
|
||||
"exam_count": exam_count,
|
||||
"interview_count": interview_count,
|
||||
@ -629,7 +630,7 @@ def job_detail(request, slug):
|
||||
"status_form": status_form,
|
||||
"image_upload_form": image_upload_form,
|
||||
"categories": categories,
|
||||
"candidate_counts": candidate_counts,
|
||||
"applications_count": applications_count,
|
||||
# 'avg_scores': avg_scores,
|
||||
# New statistics
|
||||
"avg_match_score": avg_match_score,
|
||||
@ -1765,7 +1766,7 @@ def applications_screening_view(request, slug):
|
||||
Manage candidate tiers and stage transitions
|
||||
"""
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
candidates = job.screening_candidates
|
||||
applications = job.screening_applications
|
||||
|
||||
# Get filter parameters
|
||||
min_ai_score_str = request.GET.get("min_ai_score")
|
||||
@ -1805,31 +1806,31 @@ def applications_screening_view(request, slug):
|
||||
|
||||
# Apply filters
|
||||
if min_ai_score > 0:
|
||||
candidates = candidates.filter(
|
||||
ai_analysis_data__analysis_data__match_score__gte=min_ai_score
|
||||
applications = applications.filter(
|
||||
ai_analysis_data__analysis_data_en__match_score__gte=min_ai_score
|
||||
)
|
||||
|
||||
if min_experience > 0:
|
||||
candidates = candidates.filter(
|
||||
ai_analysis_data__analysis_data__years_of_experience__gte=min_experience
|
||||
applications = applications.filter(
|
||||
ai_analysis_data__analysis_data_en__years_of_experience__gte=min_experience
|
||||
)
|
||||
|
||||
if screening_rating:
|
||||
candidates = candidates.filter(
|
||||
ai_analysis_data__analysis_data__screening_stage_rating=screening_rating
|
||||
applications = applications.filter(
|
||||
ai_analysis_data__analysis_data_en__screening_stage_rating=screening_rating
|
||||
)
|
||||
if gpa:
|
||||
candidates = candidates.filter(
|
||||
applications = applications.filter(
|
||||
person__gpa__gt= gpa
|
||||
)
|
||||
print(candidates)
|
||||
print(applications)
|
||||
|
||||
if tier1_count > 0:
|
||||
candidates = candidates[:tier1_count]
|
||||
applications = applications[:tier1_count]
|
||||
|
||||
context = {
|
||||
"job": job,
|
||||
"candidates": candidates,
|
||||
"applications": applications,
|
||||
"min_ai_score": min_ai_score,
|
||||
"min_experience": min_experience,
|
||||
"screening_rating": screening_rating,
|
||||
@ -1838,7 +1839,7 @@ def applications_screening_view(request, slug):
|
||||
"current_stage": "Applied",
|
||||
}
|
||||
|
||||
return render(request, "recruitment/candidate_screening_view.html", context)
|
||||
return render(request, "recruitment/applications_screening_view.html", context)
|
||||
|
||||
|
||||
@staff_user_required
|
||||
@ -1847,8 +1848,8 @@ def applications_exam_view(request, slug):
|
||||
Manage candidate tiers and stage transitions
|
||||
"""
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
context = {"job": job, "candidates": job.exam_candidates, "current_stage": "Exam"}
|
||||
return render(request, "recruitment/candidate_exam_view.html", context)
|
||||
context = {"job": job, "applications": job.exam_applications, "current_stage": "Exam"}
|
||||
return render(request, "recruitment/applications_exam_view.html", context)
|
||||
|
||||
|
||||
@staff_user_required
|
||||
@ -1888,9 +1889,9 @@ def bulk_update_application_exam_status(request, slug):
|
||||
|
||||
|
||||
def application_criteria_view_htmx(request, pk):
|
||||
candidate = get_object_or_404(Application, pk=pk)
|
||||
application = get_object_or_404(Application, pk=pk)
|
||||
return render(
|
||||
request, "includes/candidate_modal_body.html", {"candidate": candidate}
|
||||
request, "includes/application_modal_body.html", {"application": application}
|
||||
)
|
||||
|
||||
|
||||
@ -1989,11 +1990,11 @@ def applications_interview_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
context = {
|
||||
"job": job,
|
||||
"candidates": job.interview_candidates,
|
||||
"applications": job.interview_applications,
|
||||
"current_stage": "Interview",
|
||||
|
||||
}
|
||||
return render(request, "recruitment/candidate_interview_view.html", context)
|
||||
return render(request, "recruitment/applications_interview_view.html", context)
|
||||
|
||||
|
||||
@staff_user_required
|
||||
@ -2004,12 +2005,12 @@ def applications_document_review_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
|
||||
# Get candidates from Interview stage who need document review
|
||||
candidates = job.document_review_candidates.select_related('person')
|
||||
print(candidates)
|
||||
applications = job.document_review_applications.select_related('person')
|
||||
|
||||
# Get search query for filtering
|
||||
search_query = request.GET.get('q', '')
|
||||
if search_query:
|
||||
candidates = candidates.filter(
|
||||
applications = applications.filter(
|
||||
Q(person__first_name__icontains=search_query) |
|
||||
Q(person__last_name__icontains=search_query) |
|
||||
Q(person__email__icontains=search_query)
|
||||
@ -2017,11 +2018,11 @@ def applications_document_review_view(request, slug):
|
||||
|
||||
context = {
|
||||
"job": job,
|
||||
"candidates": candidates,
|
||||
"applications": applications,
|
||||
"current_stage": "Document Review",
|
||||
"search_query": search_query,
|
||||
}
|
||||
return render(request, "recruitment/candidate_document_review_view.html", context)
|
||||
return render(request, "recruitment/applications_document_review_view.html", context)
|
||||
|
||||
|
||||
@staff_user_required
|
||||
@ -2066,7 +2067,7 @@ def reschedule_meeting_for_application(request, slug, candidate_id, meeting_id):
|
||||
@staff_user_required
|
||||
def schedule_meeting_for_application(request, slug, candidate_pk, meeting_id):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
candidate = get_object_or_404(Application, pk=candidate_pk)
|
||||
application = get_object_or_404(Application, pk=candidate_pk)
|
||||
meeting = get_object_or_404(ZoomMeetingDetails, pk=meeting_id)
|
||||
if request.method == "POST":
|
||||
result = delete_zoom_meeting(meeting.meeting_id)
|
||||
@ -2082,7 +2083,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk, meeting_id):
|
||||
|
||||
context = {
|
||||
"job": job,
|
||||
"candidate": candidate,
|
||||
"application": application,
|
||||
"meeting": meeting,
|
||||
"delete_url": reverse(
|
||||
"schedule_meeting_for_application",
|
||||
@ -2767,7 +2768,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk):
|
||||
Handles POST to process the form, create a meeting, and redirect back.
|
||||
"""
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
candidate = get_object_or_404(Application, pk=candidate_pk, job=job)
|
||||
application = get_object_or_404(Application, pk=candidate_pk, job=job)
|
||||
|
||||
if request.method == "POST":
|
||||
form = ZoomMeetingForm(request.POST)
|
||||
@ -2778,7 +2779,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk):
|
||||
|
||||
# Use a default topic if not provided
|
||||
if not topic_val:
|
||||
topic_val = f"Interview: {job.title} with {candidate.name}"
|
||||
topic_val = f"Interview: {job.title} with {application.name}"
|
||||
|
||||
# Ensure start_time is in the future
|
||||
if start_time_val <= timezone.now():
|
||||
@ -2788,7 +2789,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk):
|
||||
# return render(request, "recruitment/schedule_meeting_form.html", {
|
||||
# 'form': form,
|
||||
# 'job': job,
|
||||
# 'candidate': candidate,
|
||||
# 'application': application,
|
||||
# 'initial_topic': topic_val,
|
||||
# 'initial_start_time': start_time_val.strftime('%Y-%m-%dT%H:%M') if start_time_val else '',
|
||||
# 'initial_duration': duration_val
|
||||
@ -2821,14 +2822,14 @@ def schedule_meeting_for_application(request, slug, candidate_pk):
|
||||
)
|
||||
# Create a ScheduledInterview record
|
||||
ScheduledInterview.objects.create(
|
||||
application=candidate,
|
||||
application=application,
|
||||
job=job,
|
||||
interview_location=zoom_meeting_instance,
|
||||
interview_date=start_time_val.date(),
|
||||
interview_time=start_time_val.time(),
|
||||
status="scheduled",
|
||||
)
|
||||
messages.success(request, f"Meeting scheduled with {candidate.name}.")
|
||||
messages.success(request, f"Meeting scheduled with {application.name}.")
|
||||
return redirect("applications_interview_view", slug=job.slug)
|
||||
else:
|
||||
messages.error(
|
||||
@ -2842,7 +2843,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk):
|
||||
{
|
||||
"form": form,
|
||||
"job": job,
|
||||
"candidate": candidate,
|
||||
"application": application,
|
||||
"initial_topic": topic_val,
|
||||
"initial_start_time": start_time_val.strftime("%Y-%m-%dT%H:%M")
|
||||
if start_time_val
|
||||
@ -2858,9 +2859,9 @@ def schedule_meeting_for_application(request, slug, candidate_pk):
|
||||
{
|
||||
"form": form,
|
||||
"job": job,
|
||||
"candidate": candidate,
|
||||
"application": application,
|
||||
"initial_topic": request.POST.get(
|
||||
"topic", f"Interview: {job.title} with {candidate.name}"
|
||||
"topic", f"Interview: {job.title} with {application.name}"
|
||||
),
|
||||
"initial_start_time": request.POST.get("start_time", ""),
|
||||
"initial_duration": request.POST.get("duration", 60),
|
||||
@ -2868,7 +2869,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk):
|
||||
)
|
||||
else: # GET request
|
||||
initial_data = {
|
||||
"topic": f"Interview: {job.title} with {candidate.name}",
|
||||
"topic": f"Interview: {job.title} with {application.name}",
|
||||
"start_time": (timezone.now() + timedelta(hours=1)).strftime(
|
||||
"%Y-%m-%dT%H:%M"
|
||||
), # Default to 1 hour from now
|
||||
@ -2878,7 +2879,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk):
|
||||
return render(
|
||||
request,
|
||||
"meetings/schedule_meeting_form.html",
|
||||
{"form": form, "job": job, "candidate": candidate},
|
||||
{"form": form, "job": job, "application": application},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
import json
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Add imports for user type restrictions
|
||||
from recruitment.decorators import StaffRequiredMixin, staff_user_required
|
||||
@ -91,7 +92,7 @@ class JobUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin,
|
||||
form_class = forms.JobPostingForm
|
||||
template_name = 'jobs/edit_job.html'
|
||||
success_url = reverse_lazy('job_list')
|
||||
success_message = 'Job updated successfully.'
|
||||
success_message = _('Job updated successfully.')
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
|
||||
@ -99,12 +100,12 @@ class JobDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin,
|
||||
model = models.JobPosting
|
||||
template_name = 'jobs/partials/delete_modal.html'
|
||||
success_url = reverse_lazy('job_list')
|
||||
success_message = 'Job deleted successfully.'
|
||||
success_message = _('Job deleted successfully.')
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
class JobApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
model = models.Application
|
||||
template_name = 'jobs/job_candidates_list.html'
|
||||
template_name = 'jobs/job_applications_list.html'
|
||||
context_object_name = 'applications'
|
||||
paginate_by = 10
|
||||
|
||||
@ -146,7 +147,7 @@ class JobApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
|
||||
class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
model = models.Application
|
||||
template_name = 'recruitment/candidate_list.html'
|
||||
template_name = 'recruitment/applications_list.html'
|
||||
context_object_name = 'applications'
|
||||
paginate_by = 100
|
||||
|
||||
@ -186,9 +187,9 @@ class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = models.Application
|
||||
form_class = forms.ApplicationForm
|
||||
template_name = 'recruitment/candidate_create.html'
|
||||
template_name = 'recruitment/application_create.html'
|
||||
success_url = reverse_lazy('application_list')
|
||||
success_message = 'Candidate created successfully.'
|
||||
success_message = _('Application created successfully.')
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
@ -215,17 +216,17 @@ class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessa
|
||||
class ApplicationUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = models.Application
|
||||
form_class = forms.ApplicationForm
|
||||
template_name = 'recruitment/candidate_update.html'
|
||||
template_name = 'recruitment/application_update.html'
|
||||
success_url = reverse_lazy('application_list')
|
||||
success_message = 'Candidate updated successfully.'
|
||||
success_message = _('Application updated successfully.')
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
|
||||
class ApplicationDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
model = models.Application
|
||||
template_name = 'recruitment/candidate_delete.html'
|
||||
template_name = 'recruitment/application_delete.html'
|
||||
success_url = reverse_lazy('application_list')
|
||||
success_message = 'Candidate deleted successfully.'
|
||||
success_message = _('Application deleted successfully.')
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
|
||||
@ -255,9 +256,9 @@ def training_list(request):
|
||||
@staff_user_required
|
||||
def application_detail(request, slug):
|
||||
from rich.json import JSON
|
||||
candidate = get_object_or_404(models.Application, slug=slug)
|
||||
application = get_object_or_404(models.Application, slug=slug)
|
||||
try:
|
||||
parsed = ast.literal_eval(candidate.parsed_summary)
|
||||
parsed = ast.literal_eval(application.parsed_summary)
|
||||
except:
|
||||
parsed = {}
|
||||
|
||||
@ -271,7 +272,7 @@ def application_detail(request, slug):
|
||||
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False)
|
||||
# parsed = json_to_markdown_table([parsed])
|
||||
return render(request, 'recruitment/application_detail.html', {
|
||||
'candidate': candidate,
|
||||
'application': application,
|
||||
'parsed': parsed,
|
||||
'stage_form': stage_form,
|
||||
})
|
||||
@ -287,7 +288,7 @@ def application_resume_template_view(request, slug):
|
||||
messages.error(request, _("You don't have permission to view this page."))
|
||||
return redirect('application_list')
|
||||
|
||||
return render(request, 'recruitment/candidate_resume_template.html', {
|
||||
return render(request, 'recruitment/application_resume_template.html', {
|
||||
'application': application
|
||||
})
|
||||
|
||||
@ -301,7 +302,7 @@ def application_update_stage(request, slug):
|
||||
stage_value = form.cleaned_data['stage']
|
||||
application.stage = stage_value
|
||||
application.save(update_fields=['stage'])
|
||||
messages.success(request,"application Stage Updated")
|
||||
messages.success(request,_("application Stage Updated"))
|
||||
return redirect("application_detail",slug=application.slug)
|
||||
|
||||
class TrainingListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
|
||||
@ -386,7 +387,7 @@ def dashboard_view(request):
|
||||
# --- 1. BASE QUERYSETS & GLOBAL METRICS (UNFILTERED) ---
|
||||
|
||||
all_jobs_queryset = models.JobPosting.objects.all().order_by('-created_at')
|
||||
all_candidates_queryset = models.Application.objects.all()
|
||||
all_applications_queryset = models.Application.objects.all()
|
||||
|
||||
# Global KPI Card Metrics
|
||||
total_jobs_global = all_jobs_queryset.count()
|
||||
@ -400,7 +401,7 @@ def dashboard_view(request):
|
||||
# --- 2. TIME SERIES: GLOBAL DAILY APPLICANTS ---
|
||||
|
||||
# Group ALL candidates by creation date
|
||||
global_daily_applications_qs = all_candidates_queryset.annotate(
|
||||
global_daily_applications_qs = all_applications_queryset.annotate(
|
||||
date=TruncDate('created_at')
|
||||
).values('date').annotate(
|
||||
count=Count('pk')
|
||||
@ -412,14 +413,14 @@ def dashboard_view(request):
|
||||
|
||||
# --- 3. FILTERING LOGIC: Determine the scope for scoped metrics ---
|
||||
|
||||
candidate_queryset = all_candidates_queryset
|
||||
application_queryset = all_applications_queryset
|
||||
job_scope_queryset = all_jobs_queryset
|
||||
interview_queryset = models.ScheduledInterview.objects.all()
|
||||
|
||||
current_job = None
|
||||
if selected_job_pk:
|
||||
# Filter all base querysets
|
||||
candidate_queryset = candidate_queryset.filter(job__pk=selected_job_pk)
|
||||
application_queryset = application_queryset.filter(job__pk=selected_job_pk)
|
||||
interview_queryset = interview_queryset.filter(job__pk=selected_job_pk)
|
||||
|
||||
try:
|
||||
@ -434,7 +435,7 @@ def dashboard_view(request):
|
||||
scoped_dates = []
|
||||
scoped_counts = []
|
||||
if selected_job_pk:
|
||||
scoped_daily_applications_qs = candidate_queryset.annotate(
|
||||
scoped_daily_applications_qs = application_queryset.annotate(
|
||||
date=TruncDate('created_at')
|
||||
).values('date').annotate(
|
||||
count=Count('pk')
|
||||
@ -446,7 +447,7 @@ def dashboard_view(request):
|
||||
|
||||
# --- 5. SCOPED CORE AGGREGATIONS (FILTERED OR ALL) ---
|
||||
|
||||
total_candidates = candidate_queryset.count()
|
||||
total_applications = application_queryset.count()
|
||||
|
||||
|
||||
score_expression = Cast(
|
||||
@ -461,7 +462,7 @@ def dashboard_view(request):
|
||||
)
|
||||
|
||||
# 2. ANNOTATE the queryset with the new field
|
||||
candidates_with_score_query = candidate_queryset.annotate(
|
||||
applications_with_score_query = application_queryset.annotate(
|
||||
annotated_match_score=score_expression
|
||||
)
|
||||
|
||||
@ -493,24 +494,24 @@ def dashboard_view(request):
|
||||
# A. Pipeline & Volume Metrics (Scoped)
|
||||
total_active_jobs = job_scope_queryset.filter(status="ACTIVE").count()
|
||||
last_week = timezone.now() - timedelta(days=7)
|
||||
new_candidates_7days = candidate_queryset.filter(created_at__gte=last_week).count()
|
||||
new_applications_7days = application_queryset.filter(created_at__gte=last_week).count()
|
||||
|
||||
open_positions_agg = job_scope_queryset.filter(status="ACTIVE").aggregate(total_open=Sum('open_positions'))
|
||||
total_open_positions = open_positions_agg['total_open'] or 0
|
||||
average_applications_result = job_scope_queryset.annotate(
|
||||
candidate_count=Count('applications', distinct=True)
|
||||
).aggregate(avg_apps=Avg('candidate_count'))['avg_apps']
|
||||
applications_count=Count('applications', distinct=True)
|
||||
).aggregate(avg_apps=Avg('applications_count'))['avg_apps']
|
||||
average_applications = round(average_applications_result or 0, 2)
|
||||
|
||||
|
||||
# B. Efficiency & Conversion Metrics (Scoped)
|
||||
hired_candidates = candidate_queryset.filter(
|
||||
hired_applications = application_queryset.filter(
|
||||
stage='Hired'
|
||||
)
|
||||
|
||||
lst=[c.time_to_hire_days for c in hired_candidates]
|
||||
lst=[c.time_to_hire_days for c in hired_applications]
|
||||
|
||||
time_to_hire_query = hired_candidates.annotate(
|
||||
time_to_hire_query = hired_applications.annotate(
|
||||
time_diff=ExpressionWrapper(
|
||||
F('join_date') - F('created_at__date'),
|
||||
output_field=fields.DurationField()
|
||||
@ -527,11 +528,11 @@ def dashboard_view(request):
|
||||
)
|
||||
print(avg_time_to_hire_days)
|
||||
|
||||
applied_count = candidate_queryset.filter(stage='Applied').count()
|
||||
advanced_count = candidate_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count()
|
||||
applied_count = application_queryset.filter(stage='Applied').count()
|
||||
advanced_count = application_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count()
|
||||
screening_pass_rate = round( (advanced_count / applied_count) * 100, 1 ) if applied_count > 0 else 0
|
||||
offers_extended_count = candidate_queryset.filter(stage='Offer').count()
|
||||
offers_accepted_count = candidate_queryset.filter(offer_status='Accepted').count()
|
||||
offers_extended_count = application_queryset.filter(stage='Offer').count()
|
||||
offers_accepted_count = application_queryset.filter(offer_status='Accepted').count()
|
||||
offers_accepted_rate = round( (offers_accepted_count / offers_extended_count) * 100, 1 ) if offers_extended_count > 0 else 0
|
||||
filled_positions = offers_accepted_count
|
||||
vacancy_fill_rate = round( (filled_positions / total_open_positions) * 100, 1 ) if total_open_positions > 0 else 0
|
||||
@ -542,21 +543,21 @@ def dashboard_view(request):
|
||||
meetings_scheduled_this_week = interview_queryset.filter(
|
||||
interview_date__week=current_week, interview_date__year=current_year
|
||||
).count()
|
||||
avg_match_score_result = candidates_with_score_query.aggregate(avg_score=Avg('annotated_match_score'))['avg_score']
|
||||
avg_match_score_result = applications_with_score_query.aggregate(avg_score=Avg('annotated_match_score'))['avg_score']
|
||||
avg_match_score = round(avg_match_score_result or 0, 1)
|
||||
high_potential_count = candidates_with_score_query.filter(annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD).count()
|
||||
high_potential_ratio = round( (high_potential_count / total_candidates) * 100, 1 ) if total_candidates > 0 else 0
|
||||
total_scored_candidates = candidates_with_score_query.count()
|
||||
scored_ratio = round( (total_scored_candidates / total_candidates) * 100, 1 ) if total_candidates > 0 else 0
|
||||
high_potential_count = applications_with_score_query.filter(annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD).count()
|
||||
high_potential_ratio = round( (high_potential_count / total_applications) * 100, 1 ) if total_applications > 0 else 0
|
||||
total_scored_candidates = applications_with_score_query.count()
|
||||
scored_ratio = round( (total_scored_candidates / total_applications) * 100, 1 ) if total_applications > 0 else 0
|
||||
|
||||
|
||||
# --- 6. CHART DATA PREPARATION ---
|
||||
|
||||
# A. Pipeline Funnel (Scoped)
|
||||
stage_counts = candidate_queryset.values('stage').annotate(count=Count('stage'))
|
||||
stage_counts = application_queryset.values('stage').annotate(count=Count('stage'))
|
||||
stage_map = {item['stage']: item['count'] for item in stage_counts}
|
||||
candidate_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired']
|
||||
candidates_count = [
|
||||
application_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired']
|
||||
application_count = [
|
||||
stage_map.get('Applied', 0), stage_map.get('Exam', 0), stage_map.get('Interview', 0),
|
||||
stage_map.get('Offer', 0), stage_map.get('Hired',0)
|
||||
]
|
||||
@ -570,9 +571,9 @@ def dashboard_view(request):
|
||||
rotation_degrees_final = round(min(rotation_degrees, 180), 1) # Ensure max 180 degrees
|
||||
|
||||
#
|
||||
hiring_source_counts = candidate_queryset.values('hiring_source').annotate(count=Count('stage'))
|
||||
hiring_source_counts = application_queryset.values('hiring_source').annotate(count=Count('stage'))
|
||||
source_map= {item['hiring_source']: item['count'] for item in hiring_source_counts}
|
||||
candidates_count_in_each_source = [
|
||||
applications_count_in_each_source = [
|
||||
source_map.get('Public', 0), source_map.get('Internal', 0), source_map.get('Agency', 0),
|
||||
|
||||
]
|
||||
@ -589,8 +590,8 @@ def dashboard_view(request):
|
||||
|
||||
# Scoped KPIs
|
||||
'total_active_jobs': total_active_jobs,
|
||||
'total_candidates': total_candidates,
|
||||
'new_candidates_7days': new_candidates_7days,
|
||||
'total_applications': total_applications,
|
||||
'new_applications_7days': new_applications_7days,
|
||||
'total_open_positions': total_open_positions,
|
||||
'average_applications': average_applications,
|
||||
'avg_time_to_hire_days': avg_time_to_hire_days,
|
||||
@ -604,8 +605,8 @@ def dashboard_view(request):
|
||||
'scored_ratio': scored_ratio,
|
||||
|
||||
# Chart Data
|
||||
'candidate_stage': json.dumps(candidate_stage),
|
||||
'candidates_count': json.dumps(candidates_count),
|
||||
'application_stage': json.dumps(application_stage),
|
||||
'application_count': json.dumps(application_count),
|
||||
'job_titles': json.dumps(job_titles),
|
||||
'job_app_counts': json.dumps(job_app_counts),
|
||||
# 'source_volume_chart_data' is intentionally REMOVED
|
||||
@ -628,7 +629,7 @@ def dashboard_view(request):
|
||||
'current_job': current_job,
|
||||
|
||||
|
||||
'candidates_count_in_each_source': json.dumps(candidates_count_in_each_source),
|
||||
'applications_count_in_each_source': json.dumps(applications_count_in_each_source),
|
||||
'all_hiring_sources': json.dumps(all_hiring_sources),
|
||||
}
|
||||
|
||||
@ -642,95 +643,95 @@ def applications_offer_view(request, slug):
|
||||
job = get_object_or_404(models.JobPosting, slug=slug)
|
||||
|
||||
# Filter candidates for this specific job and stage
|
||||
candidates = job.offer_candidates
|
||||
applications = job.offer_applications
|
||||
|
||||
# Handle search
|
||||
search_query = request.GET.get('search', '')
|
||||
if search_query:
|
||||
candidates = candidates.filter(
|
||||
applications = applications.filter(
|
||||
Q(first_name__icontains=search_query) |
|
||||
Q(last_name__icontains=search_query) |
|
||||
Q(email__icontains=search_query) |
|
||||
Q(phone__icontains=search_query)
|
||||
)
|
||||
|
||||
candidates = candidates.order_by('-created_at')
|
||||
applications = applications.order_by('-created_at')
|
||||
|
||||
context = {
|
||||
'job': job,
|
||||
'candidates': candidates,
|
||||
'applications': applications,
|
||||
'search_query': search_query,
|
||||
'current_stage': 'Offer',
|
||||
}
|
||||
return render(request, 'recruitment/candidate_offer_view.html', context)
|
||||
return render(request, 'recruitment/applications_offer_view.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def applications_hired_view(request, slug):
|
||||
"""View for hired candidates"""
|
||||
"""View for hired applications"""
|
||||
job = get_object_or_404(models.JobPosting, slug=slug)
|
||||
|
||||
# Filter candidates with offer_status = 'Accepted'
|
||||
candidates = job.hired_candidates
|
||||
# Filter applications with offer_status = 'Accepted'
|
||||
applications = job.hired_applications
|
||||
|
||||
# Handle search
|
||||
search_query = request.GET.get('search', '')
|
||||
if search_query:
|
||||
candidates = candidates.filter(
|
||||
applications = applications.filter(
|
||||
Q(first_name__icontains=search_query) |
|
||||
Q(last_name__icontains=search_query) |
|
||||
Q(email__icontains=search_query) |
|
||||
Q(phone__icontains=search_query)
|
||||
)
|
||||
|
||||
candidates = candidates.order_by('-created_at')
|
||||
applications = applications.order_by('-created_at')
|
||||
|
||||
context = {
|
||||
'job': job,
|
||||
'candidates': candidates,
|
||||
'applications': applications,
|
||||
'search_query': search_query,
|
||||
'current_stage': 'Hired',
|
||||
}
|
||||
return render(request, 'recruitment/candidate_hired_view.html', context)
|
||||
return render(request, 'recruitment/applications_hired_view.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_user_required
|
||||
def update_application_status(request, job_slug, candidate_slug, stage_type, status):
|
||||
def update_application_status(request, job_slug, application_slug, stage_type, status):
|
||||
"""Handle exam/interview/offer status updates"""
|
||||
from django.utils import timezone
|
||||
|
||||
job = get_object_or_404(models.JobPosting, slug=job_slug)
|
||||
candidate = get_object_or_404(models.Application, slug=candidate_slug, job=job)
|
||||
application = get_object_or_404(models.Application, slug=application_slug, job=job)
|
||||
|
||||
if request.method == "POST":
|
||||
if stage_type == 'exam':
|
||||
status = request.POST.get("exam_status")
|
||||
score = request.POST.get("exam_score")
|
||||
candidate.exam_status = status
|
||||
candidate.exam_score = score
|
||||
candidate.exam_date = timezone.now()
|
||||
candidate.save(update_fields=['exam_status','exam_score', 'exam_date'])
|
||||
return render(request,'recruitment/partials/exam-results.html',{'candidate':candidate,'job':job})
|
||||
application.exam_status = status
|
||||
application.exam_score = score
|
||||
application.exam_date = timezone.now()
|
||||
application.save(update_fields=['exam_status','exam_score', 'exam_date'])
|
||||
return render(request,'recruitment/partials/exam-results.html',{'application':application,'job':job})
|
||||
elif stage_type == 'interview':
|
||||
candidate.interview_status = status
|
||||
candidate.interview_date = timezone.now()
|
||||
candidate.save(update_fields=['interview_status', 'interview_date'])
|
||||
return render(request,'recruitment/partials/interview-results.html',{'candidate':candidate,'job':job})
|
||||
application.interview_status = status
|
||||
application.interview_date = timezone.now()
|
||||
application.save(update_fields=['interview_status', 'interview_date'])
|
||||
return render(request,'recruitment/partials/interview-results.html',{'application':application,'job':job})
|
||||
elif stage_type == 'offer':
|
||||
candidate.offer_status = status
|
||||
candidate.offer_date = timezone.now()
|
||||
candidate.save(update_fields=['offer_status', 'offer_date'])
|
||||
return render(request,'recruitment/partials/offer-results.html',{'candidate':candidate,'job':job})
|
||||
return redirect('application_detail', candidate.slug)
|
||||
application.offer_status = status
|
||||
application.offer_date = timezone.now()
|
||||
application.save(update_fields=['offer_status', 'offer_date'])
|
||||
return render(request,'recruitment/partials/offer-results.html',{'application':application,'job':job})
|
||||
return redirect('application_detail', application.slug)
|
||||
else:
|
||||
if stage_type == 'exam':
|
||||
return render(request,"includes/candidate_update_exam_form.html",{'candidate':candidate,'job':job})
|
||||
return render(request,"includes/applications_update_exam_form.html",{'application':application,'job':job})
|
||||
elif stage_type == 'interview':
|
||||
return render(request,"includes/candidate_update_interview_form.html",{'candidate':candidate,'job':job})
|
||||
return render(request,"includes/applications_update_interview_form.html",{'application':application,'job':job})
|
||||
elif stage_type == 'offer':
|
||||
return render(request,"includes/candidate_update_offer_form.html",{'candidate':candidate,'job':job})
|
||||
return render(request,"includes/applications_update_offer_form.html",{'application':application,'job':job})
|
||||
|
||||
|
||||
# Stage configuration for CSV export
|
||||
@ -766,7 +767,7 @@ STAGE_CONFIG = {
|
||||
@login_required
|
||||
@staff_user_required
|
||||
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)
|
||||
|
||||
# Validate stage
|
||||
@ -776,23 +777,23 @@ def export_applications_csv(request, job_slug, stage):
|
||||
|
||||
config = STAGE_CONFIG[stage]
|
||||
|
||||
# Filter candidates based on stage
|
||||
# Filter applications based on stage
|
||||
if stage == 'hired':
|
||||
candidates = job.applications.filter(**config['filter'])
|
||||
applications = job.applications.filter(**config['filter'])
|
||||
else:
|
||||
candidates = job.applications.filter(**config['filter'])
|
||||
applications = job.applications.filter(**config['filter'])
|
||||
|
||||
# Handle search if provided
|
||||
search_query = request.GET.get('search', '')
|
||||
if search_query:
|
||||
candidates = candidates.filter(
|
||||
applications = applications.filter(
|
||||
Q(first_name__icontains=search_query) |
|
||||
Q(last_name__icontains=search_query) |
|
||||
Q(email__icontains=search_query) |
|
||||
Q(phone__icontains=search_query)
|
||||
)
|
||||
|
||||
candidates = candidates.order_by('-created_at')
|
||||
applications = applications.order_by('-created_at')
|
||||
|
||||
# Create CSV response
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
@ -809,87 +810,87 @@ def export_applications_csv(request, job_slug, stage):
|
||||
headers.extend(['Job Title', 'Department'])
|
||||
writer.writerow(headers)
|
||||
|
||||
# Write candidate data
|
||||
for candidate in candidates:
|
||||
# Write application data
|
||||
for application in applications:
|
||||
row = []
|
||||
|
||||
# Extract data based on stage configuration
|
||||
for field in config['fields']:
|
||||
if field == 'name':
|
||||
row.append(candidate.name)
|
||||
row.append(application.name)
|
||||
elif field == 'email':
|
||||
row.append(candidate.email)
|
||||
row.append(application.email)
|
||||
elif field == 'phone':
|
||||
row.append(candidate.phone)
|
||||
row.append(application.phone)
|
||||
elif field == 'created_at':
|
||||
row.append(candidate.created_at.strftime('%Y-%m-%d %H:%M') if candidate.created_at else '')
|
||||
row.append(application.created_at.strftime('%Y-%m-%d %H:%M') if application.created_at else '')
|
||||
elif field == 'stage':
|
||||
row.append(candidate.stage or '')
|
||||
row.append(application.stage or '')
|
||||
elif field == 'exam_status':
|
||||
row.append(candidate.exam_status or '')
|
||||
row.append(application.exam_status or '')
|
||||
elif field == 'exam_date':
|
||||
row.append(candidate.exam_date.strftime('%Y-%m-%d %H:%M') if candidate.exam_date else '')
|
||||
row.append(application.exam_date.strftime('%Y-%m-%d %H:%M') if application.exam_date else '')
|
||||
elif field == 'interview_status':
|
||||
row.append(candidate.interview_status or '')
|
||||
row.append(application.interview_status or '')
|
||||
elif field == 'interview_date':
|
||||
row.append(candidate.interview_date.strftime('%Y-%m-%d %H:%M') if candidate.interview_date else '')
|
||||
row.append(application.interview_date.strftime('%Y-%m-%d %H:%M') if application.interview_date else '')
|
||||
elif field == 'offer_status':
|
||||
row.append(candidate.offer_status or '')
|
||||
row.append(application.offer_status or '')
|
||||
elif field == 'offer_date':
|
||||
row.append(candidate.offer_date.strftime('%Y-%m-%d %H:%M') if candidate.offer_date else '')
|
||||
row.append(application.offer_date.strftime('%Y-%m-%d %H:%M') if application.offer_date else '')
|
||||
elif field == 'ai_score':
|
||||
# Extract AI score using model property
|
||||
try:
|
||||
score = candidate.match_score
|
||||
score = application.match_score
|
||||
row.append(f"{score}%" if score else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'years_experience':
|
||||
# Extract years of experience using model property
|
||||
try:
|
||||
years = candidate.years_of_experience
|
||||
years = application.years_of_experience
|
||||
row.append(f"{years}" if years else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'screening_rating':
|
||||
# Extract screening rating using model property
|
||||
try:
|
||||
rating = candidate.screening_stage_rating
|
||||
rating = application.screening_stage_rating
|
||||
row.append(rating if rating else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'professional_category':
|
||||
# Extract professional category using model property
|
||||
try:
|
||||
category = candidate.professional_category
|
||||
category = application.professional_category
|
||||
row.append(category if category else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'top_skills':
|
||||
# Extract top 3 skills using model property
|
||||
try:
|
||||
skills = candidate.top_3_keywords
|
||||
skills = application.top_3_keywords
|
||||
row.append(', '.join(skills) if skills else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'strengths':
|
||||
# Extract strengths using model property
|
||||
try:
|
||||
strengths = candidate.strengths
|
||||
strengths = application.strengths
|
||||
row.append(strengths if strengths else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'weaknesses':
|
||||
# Extract weaknesses using model property
|
||||
try:
|
||||
weaknesses = candidate.weaknesses
|
||||
weaknesses = application.weaknesses
|
||||
row.append(weaknesses if weaknesses else '')
|
||||
except:
|
||||
row.append('')
|
||||
elif field == 'join_date':
|
||||
row.append(candidate.join_date.strftime('%Y-%m-%d') if candidate.join_date else '')
|
||||
row.append(application.join_date.strftime('%Y-%m-%d') if application.join_date else '')
|
||||
else:
|
||||
row.append(getattr(candidate, field, ''))
|
||||
row.append(getattr(application, field, ''))
|
||||
|
||||
# Add job information
|
||||
row.extend([job.title, job.department or ''])
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
{% if LANGUAGE_CODE == 'en' %}
|
||||
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ candidate.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ candidate.professional_category }} </span></h5>
|
||||
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ application.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ application.professional_category }} </span></h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
@ -10,7 +10,7 @@
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i>
|
||||
<small class="text-muted">{% trans "Job Fit" %}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ candidate.job_fit_narrative }}</p>
|
||||
<p class="mb-1">{{ application.job_fit_narrative }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
@ -18,7 +18,7 @@
|
||||
<small class="text-muted">{% trans "Top Keywords" %}</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for keyword in candidate.top_3_keywords %}
|
||||
{% for keyword in application.top_3_keywords %}
|
||||
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -31,18 +31,18 @@
|
||||
<i class="fas fa-clock me-2 text-info"></i>
|
||||
<small class="text-muted">{% trans "Experience" %}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>{{ candidate.years_of_experience }}</strong> {% trans "years" %}</p>
|
||||
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ candidate.most_recent_job_title }}</p>
|
||||
<p class="mb-1"><strong>{{ application.years_of_experience }}</strong> {% trans "years" %}</p>
|
||||
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ application.most_recent_job_title }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-chart-line me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Skills" %}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ candidate.soft_skills_score }}%</p>
|
||||
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ application.soft_skills_score }}%</p>
|
||||
<p class="mb-0"><strong>{% trans "Industry Match:" %}</strong>
|
||||
<span class="badge {% if candidate.industry_match_score >= 70 %}bg-success{% elif candidate.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||
{{ candidate.industry_match_score }}%
|
||||
<span class="badge {% if application.industry_match_score >= 70 %}bg-success{% elif application.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||
{{ application.industry_match_score }}%
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -50,17 +50,17 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-comment me-1 text-info"></i> {% trans "Recommendation" %}</label>
|
||||
<textarea class="form-control" rows="6" readonly>{{ candidate.recommendation }}</textarea>
|
||||
<textarea class="form-control" rows="6" readonly>{{ application.recommendation }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-up me-1 text-success"></i> {% trans "Strengths" %}</label>
|
||||
<textarea class="form-control" rows="4" readonly>{{ candidate.strengths }}</textarea>
|
||||
<textarea class="form-control" rows="4" readonly>{{ application.strengths }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-down me-1 text-danger"></i> {% trans "Weaknesses" %}</label>
|
||||
<textarea class="form-control" rows="4" readonly>{{ candidate.weaknesses }}</textarea>
|
||||
<textarea class="form-control" rows="4" readonly>{{ application.weaknesses }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@ -74,7 +74,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for criterion, status in candidate.criteria_checklist.items %}
|
||||
{% for criterion, status in application.criteria_checklist.items %}
|
||||
<tr>
|
||||
<td>{{ criterion }}</td>
|
||||
<td>
|
||||
@ -99,7 +99,7 @@
|
||||
<i class="fas fa-check-circle me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Minimum Requirements" %}</small>
|
||||
</div>
|
||||
{% if candidate.min_requirements_met %}
|
||||
{% if application.min_requirements_met %}
|
||||
<span class="badge bg-success">{% trans "Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Not Met" %}</span>
|
||||
@ -110,15 +110,15 @@
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Screening Rating" %}</small>
|
||||
</div>
|
||||
<span class="badge bg-secondary">{{ candidate.screening_stage_rating }}</span>
|
||||
<span class="badge bg-secondary">{{ application.screening_stage_rating }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if candidate.language_fluency %}
|
||||
{% if application.language_fluency %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-language me-1 text-info"></i> {% trans "Language Fluency" %}</label>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for language in candidate.language_fluency %}
|
||||
{% for language in application.language_fluency %}
|
||||
<span class="badge bg-light text-dark">{{ language }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -126,7 +126,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ candidate.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ candidate.professional_category_ar }} </span></h5>
|
||||
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ application.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ application.professional_category_ar }} </span></h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
@ -134,7 +134,7 @@
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i>
|
||||
<small class="text-muted">{% trans "Job Fit" %}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ candidate.job_fit_narrative_ar }}</p>
|
||||
<p class="mb-1">{{ application.job_fit_narrative_ar }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
@ -142,7 +142,7 @@
|
||||
<small class="text-muted">{% trans "Top Keywords" %}</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for keyword in candidate.top_3_keywords_ar %}
|
||||
{% for keyword in application.top_3_keywords_ar %}
|
||||
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -155,18 +155,18 @@
|
||||
<i class="fas fa-clock me-2 text-info"></i>
|
||||
<small class="text-muted">{% trans "Experience" %}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>{{ candidate.years_of_experience }}</strong> {% trans "years" %}</p>
|
||||
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ candidate.most_recent_job_title_ar }}</p>
|
||||
<p class="mb-1"><strong>{{ application.years_of_experience }}</strong> {% trans "years" %}</p>
|
||||
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ application.most_recent_job_title_ar }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-chart-line me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Skills" %}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ candidate.soft_skills_score }}%</p>
|
||||
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ application.soft_skills_score }}%</p>
|
||||
<p class="mb-0"><strong>{% trans "Industry Match:" %}</strong>
|
||||
<span class="badge {% if candidate.industry_match_score >= 70 %}bg-success{% elif candidate.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||
{{ candidate.industry_match_score }}%
|
||||
<span class="badge {% if application.industry_match_score >= 70 %}bg-success{% elif application.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||
{{ application.industry_match_score }}%
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -174,17 +174,17 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-comment me-1 text-info"></i> {% trans "Recommendation" %}</label>
|
||||
<textarea class="form-control" rows="6" readonly>{{ candidate.recommendation_ar }}</textarea>
|
||||
<textarea class="form-control" rows="6" readonly>{{ application.recommendation_ar }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-up me-1 text-success"></i> {% trans "Strengths" %}</label>
|
||||
<textarea class="form-control" rows="4" readonly>{{ candidate.strengths_ar }}</textarea>
|
||||
<textarea class="form-control" rows="4" readonly>{{ application.strengths_ar }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-down me-1 text-danger"></i> {% trans "Weaknesses" %}</label>
|
||||
<textarea class="form-control" rows="4" readonly>{{ candidate.weaknesses_ar }}</textarea>
|
||||
<textarea class="form-control" rows="4" readonly>{{ application.weaknesses_ar }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@ -198,7 +198,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for criterion, status in candidate.criteria_checklist_ar.items %}
|
||||
{% for criterion, status in application.criteria_checklist_ar.items %}
|
||||
<tr>
|
||||
<td>{{ criterion }}</td>
|
||||
<td>
|
||||
@ -223,7 +223,7 @@
|
||||
<i class="fas fa-check-circle me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Minimum Requirements" %}</small>
|
||||
</div>
|
||||
{% if candidate.min_requirements_met_ar %}
|
||||
{% if application.min_requirements_met_ar %}
|
||||
<span class="badge bg-success">{% trans "Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Not Met" %}</span>
|
||||
@ -234,15 +234,15 @@
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Screening Rating" %}</small>
|
||||
</div>
|
||||
<span class="badge bg-secondary">{{ candidate.screening_stage_rating_ar }}</span>
|
||||
<span class="badge bg-secondary">{{ application.screening_stage_rating_ar }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if candidate.language_fluency_ar %}
|
||||
{% if application.language_fluency_ar %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-language me-1 text-info"></i> {% trans "Language Fluency" %}</label>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for language in candidate.language_fluency_ar %}
|
||||
{% for language in application.language_fluency_ar %}
|
||||
<span class="badge bg-light text-dark">{{ language }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -1,15 +1,15 @@
|
||||
{% load i18n %}
|
||||
<form id="exam-update-form" hx-post="{% url 'update_application_status' job.slug candidate.slug 'exam' 'Failed' %}" hx-swap='outerHTML' hx-target="#status-result-{{ candidate.pk }}"
|
||||
<form id="exam-update-form" hx-post="{% url 'update_application_status' job.slug application.slug 'exam' 'Failed' %}" hx-swap='outerHTML' hx-target="#status-result-{{ application.pk }}"
|
||||
hx-on::after-request="const modal = bootstrap.Modal.getInstance(document.getElementById('candidateviewModal')); if (modal) { modal.hide(); }">
|
||||
<div class="d-flex justify-content-center align-items-center gap-2">
|
||||
<div class="form-check d-flex align-items-center gap-2">
|
||||
<input class="form-check-input" type="radio" name="exam_status" id="exam_passed" value="Passed" {% if candidate.exam_status == 'Passed' %}checked{% endif %}>
|
||||
<input class="form-check-input" type="radio" name="exam_status" id="exam_passed" value="Passed" {% if application.exam_status == 'Passed' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="exam_passed">
|
||||
<i class="fas fa-check me-1"></i> {% trans "Passed" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check d-flex align-items-center gap-2">
|
||||
<input class="form-check-input" type="radio" name="exam_status" id="exam_failed" value="Failed" {% if candidate.exam_status == 'Failed' %}checked{% endif %}>
|
||||
<input class="form-check-input" type="radio" name="exam_status" id="exam_failed" value="Failed" {% if application.exam_status == 'Failed' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="exam_failed">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Failed" %}
|
||||
</label>
|
||||
@ -20,7 +20,7 @@
|
||||
<label for="exam_score" class="form-label small text-muted">{% trans "Exam Score" %}</label>
|
||||
</div>
|
||||
<div class="w-25">
|
||||
<input type="number" class="form-control form-control-sm" id="exam_score" name="exam_score" min="0" max="100" required value="{{ candidate.exam_score }}">
|
||||
<input type="number" class="form-control form-control-sm" id="exam_score" name="exam_score" min="0" max="100" required value="{{ application.exam_score }}">
|
||||
</div>
|
||||
<div class="w-25 text-start ps-none">
|
||||
</div>
|
||||
@ -1,10 +1,10 @@
|
||||
{% load i18n %}
|
||||
<div class="d-flex justify-content-center align-items-center gap-2" hx-swap='outerHTML' hx-target="#interview-result-{{ candidate.pk }}"
|
||||
<div class="d-flex justify-content-center align-items-center gap-2" hx-swap='outerHTML' hx-target="#interview-result-{{ application.pk }}"
|
||||
hx-on::after-request="const modal = bootstrap.Modal.getInstance(document.getElementById('candidateviewModal')); if (modal) { modal.hide(); }">
|
||||
<a hx-post="{% url 'update_application_status' job.slug candidate.slug 'interview' 'Passed' %}" class="btn btn-outline-secondary">
|
||||
<a hx-post="{% url 'update_application_status' job.slug application.slug 'interview' 'Passed' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-check me-1"></i> {% trans "Passed" %}
|
||||
</a>
|
||||
<a hx-post="{% url 'update_application_status' job.slug candidate.slug 'interview' 'Failed' %}" class="btn btn-danger">
|
||||
<a hx-post="{% url 'update_application_status' job.slug application.slug 'interview' 'Failed' %}" class="btn btn-danger">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Failed" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -1,10 +1,10 @@
|
||||
{% load i18n %}
|
||||
<div class="d-flex justify-content-center align-items-center gap-2" hx-swap='outerHTML' hx-target="#status-result-{{ candidate.pk }}"
|
||||
<div class="d-flex justify-content-center align-items-center gap-2" hx-swap='outerHTML' hx-target="#status-result-{{ application.pk }}"
|
||||
hx-on::after-request="const modal = bootstrap.Modal.getInstance(document.getElementById('candidateviewModal')); if (modal) { modal.hide(); }">
|
||||
<a hx-post="{% url 'update_application_status' job.slug candidate.slug 'offer' 'Accepted' %}" class="btn btn-outline-secondary">
|
||||
<a hx-post="{% url 'update_application_status' job.slug application.slug 'offer' 'Accepted' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-check me-1"></i> {% trans "Accepted" %}
|
||||
</a>
|
||||
<a hx-post="{% url 'update_application_status' job.slug candidate.slug 'offer' 'Rejected' %}" class="btn btn-danger">
|
||||
<a hx-post="{% url 'update_application_status' job.slug application.slug 'offer' 'Rejected' %}" class="btn btn-danger">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Rejected" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -26,7 +26,7 @@
|
||||
<form
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
hx-post="{% url 'document_upload' candidate.id %}"
|
||||
hx-post="{% url 'document_upload' application.id %}"
|
||||
hx-target="#documents-pane"
|
||||
hx-select="#documents-pane"
|
||||
hx-swap="outerHTML"
|
||||
@ -108,7 +108,7 @@
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
|
||||
{% if user.is_superuser or candidate.job.assigned_to == user %}
|
||||
{% if user.is_superuser or application.job.assigned_to == user %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
@ -125,7 +125,7 @@
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-file-alt fa-3x mb-3"></i>
|
||||
<p class="mb-2">{% trans "No documents uploaded yet." %}</p>
|
||||
<p class="small">{% trans "Click \"Upload Document\" to add files for this candidate." %}</p>
|
||||
<p class="small">{% trans "Click \"Upload Document\" to add files for this application." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Create New Job Post - {{ block.super }}{% endblock %}
|
||||
{% block title %}{% trans "Create New Job Post" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
|
||||
|
||||
@ -150,12 +150,12 @@
|
||||
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-decoration-none text-secondary">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_list' %}" class="text-decoration-none text-secondary">Jobs</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-decoration-none text-secondary">{% trans "Home" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_list' %}" class="text-decoration-none text-secondary">{% trans "Jobs" %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;
|
||||
">Job Detail</li>
|
||||
">{% trans "Job Detail" %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row g-4">
|
||||
@ -218,19 +218,19 @@
|
||||
<div class="card-body">
|
||||
|
||||
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action btn-sm"><li class="fa fa-edit"></li>{% trans "Edit JOb" %}</a>
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action btn-sm"><li class="fa fa-edit"></li>{% trans "Edit Job" %}</a>
|
||||
<div class="float-end">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Assigned to :" %} </strong> {{ job.assigned_to|default:"N/A" }}
|
||||
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Assigned to :" %} </strong> {{ job.assigned_to|default:"" }}
|
||||
</div>
|
||||
</div>
|
||||
</h5>
|
||||
<div class="row g-3 mb-4 border-bottom pb-3 small text-secondary">
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-building me-2 text-primary"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
|
||||
<i class="fas fa-building me-2 text-primary"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-hashtag me-2 text-primary"></i> <strong>{% trans "Position No:" %}</strong> {{ job.position_number|default:"N/A" }}
|
||||
<i class="fas fa-hashtag me-2 text-primary"></i> <strong>{% trans "Position No:" %}</strong> {{ job.position_number|default:"" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
|
||||
@ -242,16 +242,16 @@
|
||||
<i class="fas fa-globe me-2 text-primary"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fa-solid fa-money-bill me-2 text-primary"></i> <strong>{% trans "Salary:" %}</strong> {{ job.salary_range |default:"N/A" }}
|
||||
<i class="fa-solid fa-money-bill me-2 text-primary"></i> <strong>{% trans "Salary:" %}</strong> {{ job.salary_range |default:"" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Created By:" %}</strong> {{ job.created_by|default:"N/A" }}
|
||||
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Created By:" %}</strong> {{ job.created_by|default:"" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-plus me-2 text-primary"></i> <strong>{% trans "Created At:" %}</strong> {{ job.created_at|default:"N/A" }}
|
||||
<i class="fas fa-plus me-2 text-primary"></i> <strong>{% trans "Created At:" %}</strong> {{ job.created_at|default:"" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-edit me-2 text-primary"></i> <strong>{% trans "Updated At:" %}</strong> {{ job.updated_at|default:"N/A" }}
|
||||
<i class="fas fa-edit me-2 text-primary"></i> <strong>{% trans "Updated At:" %}</strong> {{ job.updated_at|default:"" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -293,7 +293,7 @@
|
||||
<ul class="nav nav-tabs" id="rightJobTabs" role="tablist">
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link active" id="applicants-tab" data-bs-toggle="tab" data-bs-target="#applicants-pane" type="button" role="tab" aria-controls="applicants-pane" aria-selected="true">
|
||||
<i class="fas fa-users me-1"></i> {% trans "Applicants" %}
|
||||
<i class="fas fa-users me-1"></i> {% trans "Applications" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
@ -322,14 +322,14 @@
|
||||
|
||||
{# TAB 1: APPLICANTS CONTENT #}
|
||||
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab">
|
||||
<h5 class="mb-3">{% trans "Total Applicants" %} (<span id="total_candidates">{{ total_applicants }}</span>)</h5>
|
||||
<h5 class="mb-3">{% trans "Total Applications" %} (<span id="total_candidates">{{ total_applications }}</span>)</h5>
|
||||
|
||||
<div class="d-grid gap-3">
|
||||
<a href="{% url '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 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 href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action">
|
||||
@ -345,7 +345,7 @@
|
||||
|
||||
{# TAB 2: TRACKING CONTENT #}
|
||||
<div class="tab-pane fade" id="tracking-pane" role="tabpanel" aria-labelledby="tracking-tab">
|
||||
<h5 class="mb-3"><i class="fas fa-project-diagram me-2 text-primary"></i>{% trans "Applicant Stages" %}</h5>
|
||||
<h5 class="mb-3"><i class="fas fa-project-diagram me-2 text-primary"></i>{% trans "Applications Stages" %}</h5>
|
||||
{% include 'jobs/partials/applicant_tracking.html' %}
|
||||
<p class="text-muted small">
|
||||
{% trans "The applicant tracking flow is defined by the attached Form Template. View the Form Template tab to manage stages and fields." %}
|
||||
@ -491,12 +491,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Card 2: Candidate Category Chart #}
|
||||
{# Card 2: Application Category Chart #}
|
||||
<div class="card shadow-sm no-hover mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-chart-pie me-2 text-primary"></i>
|
||||
{% trans "Candidate Categories & Scores" %}
|
||||
{% trans "Application Categories & Scores" %}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
@ -535,7 +535,7 @@
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
<div class="h4 mb-0 text-success fw-bold">{{ high_potential_count }}</div>
|
||||
<small class="text-muted d-block">{% trans "High Potential" %}</small>
|
||||
<small class="text-muted d-block">{% trans "High Potential (score>=75)" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -616,14 +616,24 @@
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{# Translated JS strings used by the inline scripts below. Using `as` + `escapejs` ensures safe JS embedding. #}
|
||||
{% trans "Copy failed. Please copy the URL manually:" as copy_failed_msg %}
|
||||
{% trans "Number of Applications" as chart_label_applications %}
|
||||
{% trans " application(s)" as chart_tooltip_suffix %}
|
||||
{% trans "No application category data available for this job." as no_chart_data_msg %}
|
||||
|
||||
{% block customJS%}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
const COPY_FAILED_MSG = "{{ copy_failed_msg|escapejs }}";
|
||||
const CHART_LABEL_NUMBER_OF_APPLICATIONS = "{{ chart_label_applications|escapejs }}";
|
||||
const CHART_TOOLTIP_SUFFIX = "{{ chart_tooltip_suffix|escapejs }}";
|
||||
const NO_CHART_DATA_MSG = "{{ no_chart_data_msg|escapejs }}";
|
||||
|
||||
// Pass data from Django to JavaScript safely
|
||||
window.jobChartData = {
|
||||
categories: {{ categories|safe|default:"[]" }},
|
||||
candidate_counts: {{ candidate_counts|safe|default:"[]" }},
|
||||
applications_count: {{ applications_count|safe|default:"[]" }},
|
||||
avg_scores: {{ avg_scores|safe|default:"[]" }}
|
||||
};
|
||||
|
||||
@ -646,7 +656,7 @@
|
||||
}).catch(err => {
|
||||
// Fallback for older browsers or security issues
|
||||
console.error('Could not copy text: ', err);
|
||||
alert("Copy failed. Please copy the URL manually: " + urlToCopy);
|
||||
alert(COPY_FAILED_MSG + ' ' + urlToCopy);
|
||||
});
|
||||
});
|
||||
|
||||
@ -661,10 +671,10 @@
|
||||
|
||||
// Safely get job_category_data from Django context
|
||||
// Using window.jobChartData to avoid template parsing issues
|
||||
const jobChartData = window.jobChartData || { categories: [], candidate_counts: [], avg_scores: []};
|
||||
const jobChartData = window.jobChartData || { categories: [], applications_count: [], avg_scores: []};
|
||||
|
||||
const categories = jobChartData.categories || [];
|
||||
const candidateCounts = jobChartData.candidate_counts || [];
|
||||
const candidateCounts = jobChartData.applications_count || [];
|
||||
const avgScores = jobChartData.avg_scores || [];
|
||||
|
||||
if (categories.length > 0) { // Only render if there's data
|
||||
@ -674,7 +684,7 @@
|
||||
labels: categories,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Number of Candidates',
|
||||
label: CHART_LABEL_NUMBER_OF_APPLICATIONS,
|
||||
data: candidateCounts,
|
||||
backgroundColor: [
|
||||
'rgba(0, 99, 110, 0.7)', // --kaauh-teal
|
||||
@ -715,7 +725,7 @@
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += context.parsed + ' candidate(s)';
|
||||
label += context.parsed + CHART_TOOLTIP_SUFFIX;
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@ -724,8 +734,8 @@
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Display a message if no data is available
|
||||
chartCtx.canvas.parentNode.innerHTML = '<p class="text-center text-muted mt-4">No candidate category data available for this job.</p>';
|
||||
// Display a message if no data is available (translated)
|
||||
chartCtx.canvas.parentNode.innerHTML = '<p class="text-center text-muted mt-4">' + NO_CHART_DATA_MSG + '</p>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -330,12 +330,12 @@
|
||||
</td>
|
||||
|
||||
{# CANDIDATE MANAGEMENT DATA #}
|
||||
<td class="candidate-data-cell text-primary-theme"><a href="{% url 'applications_screening_view' job.slug %}" class="text-primary-theme">{% if job.all_candidates.count %}{{ job.all_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-info"><a href="{% url 'applications_screening_view' job.slug %}" class="text-info">{% if job.screening_candidates.count %}{{ job.screening_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'applications_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_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_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_offer_view' job.slug %}" class="text-success">{% if job.offer_candidates.count %}{{ job.offer_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-primary-theme"><a href="{% url 'applications_screening_view' job.slug %}" class="text-primary-theme">{% if job.all_applications.count %}{{ job.all_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-info"><a href="{% url 'applications_screening_view' job.slug %}" class="text-info">{% if job.screening_applications.count %}{{ job.screening_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'applications_exam_view' job.slug %}" class="text-success">{% if job.exam_applications.count %}{{ job.exam_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'applications_interview_view' job.slug %}" class="text-success">{% if job.interview_applications.count %}{{ job.interview_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'applications_document_review_view' job.slug %}" class="text-success">{% if job.document_review_applications.count %}{{ job.document_review_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'applications_offer_view' job.slug %}" class="text-success">{% if job.offer_applications.count %}{{ job.offer_applications.count }}{% else %}-{% endif %}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -360,8 +360,8 @@
|
||||
|
||||
<ul class="list-unstyled small mb-3">
|
||||
<li><i class="fas fa-calendar-alt text-primary-theme me-2"></i>{% trans "Deadline" %}: {{ job.application_deadline|date:"d-m-Y" }}</li>
|
||||
<li><i class="fas fa-users text-primary-theme me-2"></i>{% trans "Total Applicants" %}: {{ job.all_candidates.count|default:"0" }}</li>
|
||||
<li><i class="fas fa-clipboard-check text-success me-2"></i> {% trans "Offers Made" %}: {{ job.offer_candidates.count|default:"0" }}</li>
|
||||
<li><i class="fas fa-users text-primary-theme me-2"></i>{% trans "Total Applicants" %}: {{ job.all_applications.count|default:"0" }}</li>
|
||||
<li><i class="fas fa-clipboard-check text-success me-2"></i> {% trans "Offers Made" %}: {{ job.offer_applications.count|default:"0" }}</li>
|
||||
<li><i class="fas fa-file-alt text-info me-2"></i> {% trans "Form" %}:
|
||||
{% if job.form_template %}
|
||||
<a href="{% url 'form_template_submissions_list' job.form_template.slug %}" class="text-info">{{ job.form_template.name }} ({{ job.form_template.submissions.count }} submissions)</a>
|
||||
|
||||
@ -109,7 +109,7 @@
|
||||
<i class="fas fa-file-signature cd_screening"></i>
|
||||
</div>
|
||||
<div class="stage-label cd_screening">{% trans "Screened" %}</div>
|
||||
<div class="stage-count">{{ job.screening_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count">{{ job.screening_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 1 -> 2 {% endcomment %}
|
||||
@ -123,7 +123,7 @@
|
||||
<i class="fas fa-clipboard-check cd_exam"></i>
|
||||
</div>
|
||||
<div class="stage-label cd_exam">{% trans "Exam" %}</div>
|
||||
<div class="stage-count ">{{ job.exam_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count ">{{ job.exam_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 2 -> 3 {% endcomment %}
|
||||
@ -137,7 +137,7 @@
|
||||
<i class="fas fa-comments cd_interview"></i>
|
||||
</div>
|
||||
<div class="stage-label cd_interview">{% trans "Interview" %}</div>
|
||||
<div class="stage-count">{{ job.interview_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count">{{ job.interview_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 3 -> 4 {% endcomment %}
|
||||
@ -151,7 +151,7 @@
|
||||
<i class="fas fa-file-alt"></i>
|
||||
</div>
|
||||
<div class="stage-label">{% trans "Document Review" %}</div>
|
||||
<div class="stage-count">{{ job.document_review_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count">{{ job.document_review_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 4 -> 5 {% endcomment %}
|
||||
@ -165,7 +165,7 @@
|
||||
<i class="fas fa-handshake"></i>
|
||||
</div>
|
||||
<div class="stage-label">{% trans "Offer" %}</div>
|
||||
<div class="stage-count">{{ job.offer_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count">{{ job.offer_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 5 -> 6 {% endcomment %}
|
||||
@ -179,7 +179,7 @@
|
||||
<i class="fas fa-trophy"></i>
|
||||
</div>
|
||||
<div class="stage-label">{% trans "Hired" %}</div>
|
||||
<div class="stage-count">{{ job.hired_candidates.count|default:"0" }}</div>
|
||||
<div class="stage-count">{{ job.hired_applications.count|default:"0" }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
35
templates/recruitment/application_delete.html
Normal file
35
templates/recruitment/application_delete.html
Normal file
@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "Delete Application" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1>
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
{% trans "Delete Application" %}: {{ object.candidate.full_name }}
|
||||
</h1>
|
||||
<a href="{% url 'applications_list' %}" class="btn btn-secondary">{% trans "Back to List" %}</a>
|
||||
</div>
|
||||
<p>{% trans "Are you sure you want to delete this application for" %} "{{ object.candidate.full_name }}" {% trans "for the job" %} "{{ object.job.title }}"? {% trans "This action cannot be undone." %}</p>
|
||||
{% if object.job %}
|
||||
<p><strong>{% trans "Job:" %}</strong> {{ object.job.title }}</p>
|
||||
{% endif %}
|
||||
{% if object.candidate %}
|
||||
<p><strong>{% trans "Candidate:" %}</strong> {{ object.candidate.full_name }}</p>
|
||||
{% endif %}
|
||||
<p><strong>{% trans "Application Date:" %}</strong> {{ object.created_at|date:"M d, Y" }}</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
{% trans "Yes, Delete Application" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static humanize i18n %}
|
||||
{% load static i18n humanize%}
|
||||
|
||||
{% block title %}{{ candidate.name }} - {{ block.super }}{% endblock %}
|
||||
{% block title %}{{ application.name }} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
@ -269,7 +269,7 @@
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-secondary text-decoration-none">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-secondary text-decoration-none">Job:({{candidate.job.title}})</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_detail' application.job.slug %}" class="text-secondary text-decoration-none">Job:({{application.job.title}})</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" class="text-secondary" style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;
|
||||
@ -277,7 +277,7 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{# LEFT COLUMN: MAIN CANDIDATE DETAILS AND TABS #}
|
||||
{# LEFT COLUMN: MAIN application DETAILS AND TABS #}
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm no-hover">
|
||||
|
||||
@ -285,16 +285,16 @@
|
||||
<div class="candidate-header-card">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap">
|
||||
<div>
|
||||
<h1 class="h3 mb-2">{{ candidate.name }}</h1>
|
||||
<h1 class="h3 mb-2">{{ application.name }}</h1>
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
|
||||
<span id="stageDisplay" class="badge">
|
||||
{% trans "Stage:" %}
|
||||
<span>{{ candidate.stage }}</span>
|
||||
<span>{{ application.stage }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<small class="text-white opacity-75">
|
||||
{% trans "Applied for:" %} <strong>{{ candidate.job.title }}</strong>
|
||||
{% trans "Applied for:" %} <strong>{{ application.job.title }}</strong>
|
||||
</small>
|
||||
</div>
|
||||
{# Change Stage button #}
|
||||
@ -341,7 +341,7 @@
|
||||
<i class="fas fa-envelope fa-2x text-muted me-3"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">{% trans "Email" %}</small>
|
||||
<strong>{{ candidate.email }}</strong>
|
||||
<strong>{{ application.email }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -350,7 +350,7 @@
|
||||
<i class="fas fa-briefcase fa-2x text-muted me-3"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">{% trans "Position Applied" %}</small>
|
||||
<strong>{{ candidate.job.title }}</strong>
|
||||
<strong>{{ application.job.title }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -360,10 +360,10 @@
|
||||
<div>
|
||||
<small class="text-muted d-block">{% trans "Applied Date" %}</small>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<strong>{{ candidate.created_at|date:"M d, Y H:i" }}</strong>
|
||||
<strong>{{ application.created_at|date:"M d, Y H:i" }}</strong>
|
||||
<span class="badge bg-light text-dark">
|
||||
<i class="far fa-clock me-1"></i>
|
||||
{{ candidate.created_at|naturaltime }}
|
||||
{{ application.created_at|naturaltime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -380,15 +380,15 @@
|
||||
{# ENHANCED: CANDIDATE JOURNEY TIMELINE CARD #}
|
||||
<div class="card shadow-sm timeline-card">
|
||||
<div class="card-header bg-white border-bottom py-3">
|
||||
<h5 class="mb-0 text-muted"><i class="fas fa-route me-2"></i>{% trans "Candidate Journey" %}</h5>
|
||||
<h5 class="mb-0 text-muted"><i class="fas fa-route me-2"></i>{% trans "Application Journey" %}</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
|
||||
<h6 class="text-uppercase text-secondary mb-3">{% trans "Current Stage" %}</h6>
|
||||
<div class="p-3 mb-4 rounded current-stage">
|
||||
<p class="mb-0 fw-bold fs-5 text-primary">{{ candidate.stage }}</p>
|
||||
<p class="mb-0 fw-bold fs-5 text-primary">{{ application.stage }}</p>
|
||||
<small class="text-muted d-block mt-1">
|
||||
{% trans "Latest status update:" %} {{ candidate.updated_at|date:"M d, Y" }}
|
||||
{% trans "Latest status update:" %} {{ application.updated_at|date:"M d, Y" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@ -402,50 +402,50 @@
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Application Submitted" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.created_at|date:"M d, Y" }}
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ application.created_at|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.created_at|date:"h:i A" }}
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ application.created_at|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% if candidate.exam_date %}
|
||||
{% if application.exam_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fa-clipboard-check"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Exam" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.exam_date|date:"M d, Y" }}
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ application.exam_date|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.exam_date|date:"h:i A" }}
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ application.exam_date|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.get_interview_date %}
|
||||
{% if application.get_interview_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.get_interview_date}}
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ application.get_interview_date}}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.get_interview_time}}
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ application.get_interview_time}}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.offer_date %}
|
||||
{% if application.offer_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.offer_date|date:"M d, Y" }}
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ application.offer_date|date:"M d, Y" }}
|
||||
|
||||
</small>
|
||||
</div>
|
||||
@ -453,13 +453,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.hired_date %}
|
||||
{% if application.hired_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.hired_date|date:"M d, Y" }}
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ application.hired_date|date:"M d, Y" }}
|
||||
|
||||
</small>
|
||||
</div>
|
||||
@ -473,28 +473,28 @@
|
||||
|
||||
{# TAB 4 CONTENT: DOCUMENTS #}
|
||||
<div class="tab-pane fade" id="documents-pane" role="tabpanel" aria-labelledby="documents-tab">
|
||||
{% with documents=candidate.documents %}
|
||||
{% with documents=application.documents %}
|
||||
{% include 'includes/document_list.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{# TAB 5 CONTENT: PARSED SUMMARY #}
|
||||
{% if candidate.parsed_summary %}
|
||||
{% if application.parsed_summary %}
|
||||
|
||||
<div class="tab-pane fade" id="summary-pane" role="tabpanel" aria-labelledby="summary-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5>
|
||||
<div class="border-start border-primary ps-3 pt-1 pb-1">
|
||||
{% include 'includes/candidate_modal_body.html' %}
|
||||
{% include 'includes/application_modal_body.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# TAB 5 CONTENT: AI ANALYSIS #}
|
||||
{% if candidate.is_resume_parsed %}
|
||||
{% if application.is_resume_parsed %}
|
||||
<div class="tab-pane fade" id="analysis-pane" role="tabpanel" aria-labelledby="analysis-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "AI Analysis Report" %}</h5>
|
||||
<div class="border-start border-primary ps-3 pt-1 pb-1">
|
||||
{% with analysis=candidate.ai_analysis_data %}
|
||||
{% with analysis=application.ai_analysis_data %}
|
||||
{# Match Score Card #}
|
||||
<div class="mb-4 p-3 rounded" style="background-color: {% if analysis.match_score >= 70 %}rgba(40, 167, 69, 0.1){% elif analysis.match_score >= 40 %}rgba(255, 193, 7, 0.1){% else %}rgba(220, 53, 69, 0.1){% endif %}">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
@ -626,7 +626,7 @@
|
||||
<span class="visually-hidden">{% trans "Loading..." %}</span>
|
||||
</div>
|
||||
<h5 class="text-primary">{% trans "Resume is being parsed" %}</h5>
|
||||
<p class="text-muted">{% trans "Our AI is analyzing the candidate's resume to generate insights. This may take a few moments." %}</p>
|
||||
<p class="text-muted">{% trans "Our AI is analyzing the application's resume to generate insights. This may take a few moments." %}</p>
|
||||
<div class="progress mt-3" style="height: 6px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 75%"></div>
|
||||
@ -648,28 +648,28 @@
|
||||
<div class="card shadow-sm mb-2 p-2">
|
||||
<h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
|
||||
<div class="d-grid gap-2">
|
||||
{% comment %} <a href="{% url 'application_update' candidate.slug %}" class="btn btn-outline-primary">
|
||||
{% comment %} <a href="{% url 'application_update' application.slug %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-edit"></i> {% trans "Edit Details" %}
|
||||
</a> {% endcomment %}
|
||||
{% comment %} <a href="{% url 'application_delete' candidate.slug %}" class="btn btn-outline-danger" onclick="return confirm('{% trans "Are you sure you want to delete this candidate?" %}')">
|
||||
{% comment %} <a href="{% url 'application_delete' application.slug %}" class="btn btn-outline-danger" onclick="return confirm('{% trans "Are you sure you want to delete this candidate?" %}')">
|
||||
<i class="fas fa-trash-alt"></i> {% trans "Delete Candidate" %}
|
||||
</a> {% endcomment %}
|
||||
<a href="{% url 'application_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
|
||||
</a>
|
||||
{% if candidate.resume %}
|
||||
{% if application.resume %}
|
||||
|
||||
{% comment %} <a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
||||
{% comment %} <a href="{{ application.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="fas fa-eye me-1"></i>
|
||||
{% trans "View Actual Resume" %}
|
||||
</a> {% endcomment %}
|
||||
|
||||
<a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary">
|
||||
<a href="{{ application.resume.url }}" download class="btn btn-outline-primary">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% trans "Download Resume" %}
|
||||
</a>
|
||||
|
||||
{% comment %} <a href="{% url 'application_resume_template' candidate.slug %}" class="btn btn-outline-info">
|
||||
{% comment %} <a href="{% url 'application_resume_template' application.slug %}" class="btn btn-outline-info">
|
||||
<i class="fas fa-file-alt me-1"></i>
|
||||
{% trans "View Resume AI Overview" %}
|
||||
</a> {% endcomment %}
|
||||
@ -681,7 +681,7 @@
|
||||
<div class="card shadow-sm mb-4 p-2">
|
||||
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire:" %}
|
||||
|
||||
{% with days=candidate.time_to_hire_days %}
|
||||
{% with days=application.time_to_hire_days %}
|
||||
{% if days > 0 %}
|
||||
{{ days }} day{{ days|pluralize }}
|
||||
{% else %}
|
||||
@ -699,10 +699,10 @@
|
||||
</div>
|
||||
|
||||
<div class="resume-parsed-section">
|
||||
{% if candidate.is_resume_parsed %}
|
||||
{% include 'recruitment/candidate_resume_template.html' %}
|
||||
{% if application.is_resume_parsed %}
|
||||
{% include 'recruitment/application_resume_template.html' %}
|
||||
{% else %}
|
||||
{% if candidate.scoring_timeout %}
|
||||
{% if application.scoring_timeout %}
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
||||
<div class="ai-loading-container">
|
||||
<i class="fas fa-robot ai-robot-icon"></i>
|
||||
@ -711,7 +711,7 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
||||
<button type="submit" class="btn btn-sm btn-main-action" hx-get="{% url 'application_retry_scoring' candidate.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume , Please Wait.. <i class='fa-solid fa-spinner fa-spin'></i>`">
|
||||
<button type="submit" class="btn btn-sm btn-main-action" hx-get="{% url 'application_retry_scoring' application.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume , Please Wait.. <i class='fa-solid fa-spinner fa-spin'></i>`">
|
||||
<i class="fas fa-redo-alt me-1"></i>
|
||||
{% trans "Unable to Parse Resume , click to retry" %}
|
||||
</button>
|
||||
@ -724,6 +724,6 @@
|
||||
|
||||
|
||||
{% if user.is_staff %}
|
||||
{% include "recruitment/partials/stage_update_modal.html" with candidate=candidate form=stage_form %}
|
||||
{% include "recruitment/partials/stage_update_modal.html" with application=application form=stage_form %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -7,9 +7,9 @@
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
{% if LANGUAGE_CODE == 'ar' %}
|
||||
<title>{{ candidate.resume_data_ar.full_name|default:"Candidate" }} - Candidate Profile</title>
|
||||
<title>{{ application.resume_data_ar.full_name|default:"Application" }} - Application Profile</title>
|
||||
{% else %}
|
||||
<title>{{ candidate.resume_data_en.full_name|default:"Candidate" }} - Candidate Profile</title>
|
||||
<title>{{ application.resume_data_en.full_name|default:"Application" }} - Application Profile</title>
|
||||
{% endif %}
|
||||
<!-- Use a modern icon set -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
@ -532,40 +532,40 @@
|
||||
<!-- Header Section -->
|
||||
<header class="header-box">
|
||||
<div class="header-info">
|
||||
<h1>{{ candidate.resume_data.full_name|default:"Candidate Name" }}</h1>
|
||||
<p>{{ candidate.resume_data.current_title|default:"Professional Title" }}</p>
|
||||
<h1>{{ application.resume_data.full_name|default:"Application Name" }}</h1>
|
||||
<p>{{ application.resume_data.current_title|default:"Professional Title" }}</p>
|
||||
<div class="contact-details">
|
||||
<div class="contact-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span>{{ candidate.resume_data.location|default:"Location" }}</span>
|
||||
<span>{{ application.resume_data.location|default:"Location" }}</span>
|
||||
</div>
|
||||
<!-- Displaying the raw contact string (which contains both phone and email in the example) -->
|
||||
<div class="contact-item">
|
||||
<i class="fas fa-id-card"></i>
|
||||
<span title="Contact Information: Phone and Email">{{ candidate.resume_data.contact|default:"Contact Information" }}</span>
|
||||
<span title="Contact Information: Phone and Email">{{ application.resume_data.contact|default:"Contact Information" }}</span>
|
||||
</div>
|
||||
<!-- GitHub and LinkedIn links for quick access (null in example but included for completeness) -->
|
||||
{% if candidate.resume_data.linkedin %}
|
||||
{% if application.resume_data.linkedin %}
|
||||
<div class="contact-item">
|
||||
|
||||
<a href="{{ candidate.resume_data.linkedin }}" target="_blank"><i class="fab fa-linkedin text-white"></i></a>
|
||||
<a href="{{ application.resume_data.linkedin }}" target="_blank"><i class="fab fa-linkedin text-white"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if candidate.resume_data.github %}
|
||||
{% if application.resume_data.github %}
|
||||
<div class="contact-item">
|
||||
|
||||
<a href="{{ candidate.resume_data.github }}" target="_blank"><i class="fab fa-github text-white"></i></a>
|
||||
<a href="{{ application.resume_data.github }}" target="_blank"><i class="fab fa-github text-white"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-box">
|
||||
<div class="score-value">{{ candidate.analysis_data.match_score|default:0 }}%</div>
|
||||
<div class="score-value">{{ application.analysis_data.match_score|default:0 }}%</div>
|
||||
<div class="score-text">Match Score</div>
|
||||
<div class="assessment-rating
|
||||
{% if candidate.analysis_data.match_score|default:0 < 50 %}score-red{% elif candidate.analysis_data.match_score|default:0 < 75 %}score-yellow{% else %}score-green{% endif %}">
|
||||
{% if application.analysis_data.match_score|default:0 < 50 %}score-red{% elif application.analysis_data.match_score|default:0 < 75 %}score-yellow{% else %}score-green{% endif %}">
|
||||
<!-- scoring_data.screening_stage_rating -->
|
||||
{{ candidate.analysis_data.screening_stage_rating|default:"Assessment" }}
|
||||
{{ application.analysis_data.screening_stage_rating|default:"Assessment" }}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -582,8 +582,8 @@
|
||||
Summary
|
||||
</h2>
|
||||
<p class="summary-text">
|
||||
<!-- candidate.resume_data.summary, falling back to scoring_data.job_fit_narrative -->
|
||||
{{ candidate.resume_data.summary|default:"Professional summary not available." }}
|
||||
<!-- application.resume_data.summary, falling back to scoring_data.job_fit_narrative -->
|
||||
{{ application.resume_data.summary|default:"Professional summary not available." }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@ -593,7 +593,7 @@
|
||||
<i class="fas fa-briefcase"></i>
|
||||
Experience
|
||||
</h2>
|
||||
{% for experience in candidate.resume_data.experience %}
|
||||
{% for experience in application.resume_data.experience %}
|
||||
<div class="experience-item">
|
||||
<div class="experience-header">
|
||||
<div>
|
||||
@ -608,7 +608,7 @@
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
{% if experience.start_date %}{{ experience.start_date }}{% endif %} -
|
||||
{% if experience.end_date and experience.end_date != "Present" %}{{ experience.end_date }}{% else %}Present{% endif %}
|
||||
<!-- candidate.resume_data.experience[].location -->
|
||||
<!-- application.resume_data.experience[].location -->
|
||||
{% if experience.location %}<span style="margin-left: 1rem;"><i class="fas fa-map-pin"></i>{{ experience.location }}</span>{% endif %}
|
||||
</p>
|
||||
{% if experience.key_achievements %}
|
||||
@ -628,7 +628,7 @@
|
||||
<i class="fas fa-graduation-cap"></i>
|
||||
Education
|
||||
</h2>
|
||||
{% for education in candidate.resume_data.education %}
|
||||
{% for education in application.resume_data.education %}
|
||||
<div class="education-item">
|
||||
<div class="icon-badge">
|
||||
<i class="fas fa-certificate"></i>
|
||||
@ -642,7 +642,7 @@
|
||||
{% if education.gpa %}
|
||||
<p class="meta"><i class="fas fa-award"></i> GPA: {{ education.gpa }}</p>
|
||||
{% endif %}
|
||||
<!-- candidate.resume_data.education[].relevant_courses -->
|
||||
<!-- application.resume_data.education[].relevant_courses -->
|
||||
{% if education.relevant_courses %}
|
||||
<p class="meta" style="margin-top: 0.25rem;">Courses: {{ education.relevant_courses|join:", " }}</p>
|
||||
{% endif %}
|
||||
@ -657,7 +657,7 @@
|
||||
<i class="fas fa-project-diagram"></i>
|
||||
Projects
|
||||
</h2>
|
||||
{% for project in candidate.resume_data.projects %}
|
||||
{% for project in application.resume_data.projects %}
|
||||
<div class="project-item">
|
||||
<h3>{{ project.name }}</h3>
|
||||
<p class="description">{{ project.brief_description }}</p>
|
||||
@ -670,37 +670,37 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not candidate.resume_data.projects %}
|
||||
{% if not application.resume_data.projects %}
|
||||
<p style="color: var(--color-gray-500); font-size: 0.875rem;">No projects detailed in the resume.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Keywords Section (NOW IN THE LEFT COLUMN) -->
|
||||
{% if candidate.analysis_data.top_3_keywords or candidate.analysis_data.cultural_fit_keywords %}
|
||||
{% if application.analysis_data.top_3_keywords or application.analysis_data.cultural_fit_keywords %}
|
||||
<section class="card-section" style="margin-top: 0;">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-tags"></i>
|
||||
Keywords
|
||||
</h2>
|
||||
|
||||
{% if candidate.analysis_data.top_3_keywords %}
|
||||
{% if application.analysis_data.top_3_keywords %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<h3 class="keyword-subheader"><i class="fas fa-key"></i>Top Keywords (Job Match)</h3>
|
||||
<div class="tag-list">
|
||||
<!-- scoring_data.top_3_keywords -->
|
||||
{% for keyword in candidate.analysis_data.top_3_keywords %}
|
||||
{% for keyword in application.analysis_data.top_3_keywords %}
|
||||
<span class="keyword-tag">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.cultural_fit_keywords %}
|
||||
{% if application.analysis_data.cultural_fit_keywords %}
|
||||
<div>
|
||||
<h3 class="keyword-subheader"><i class="fas fa-users"></i>Cultural Fit Keywords</h3>
|
||||
<div class="tag-list">
|
||||
<!-- scoring_data.cultural_fit_keywords -->
|
||||
{% for keyword in candidate.analysis_data.cultural_fit_keywords %}
|
||||
{% for keyword in application.analysis_data.cultural_fit_keywords %}
|
||||
<span class="cultural-tag">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -719,57 +719,57 @@
|
||||
Analysis
|
||||
</h2>
|
||||
|
||||
{% if candidate.analysis_data.category %}
|
||||
{% if application.analysis_data.category %}
|
||||
<div class="analysis-metric" style="margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-gray-100);">
|
||||
<span class="metric-title">Target Role Category:</span>
|
||||
<!-- scoring_data.category -->
|
||||
<span class="metric-value" style="color: var(--kaauh-teal);">{{ candidate.analysis_data.category }}</span>
|
||||
<span class="metric-value" style="color: var(--kaauh-teal);">{{ application.analysis_data.category }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if candidate.analysis_data.red_flags %}
|
||||
{% if application.analysis_data.red_flags %}
|
||||
<div class="narrative-box">
|
||||
<h3 class="flag-title red"><i class="fas fa-flag"></i>Red Flags</h3>
|
||||
<!-- scoring_data.red_flags -->
|
||||
<p class="narrative-text">{{ candidate.analysis_data.red_flags|join:". "|default:"None." }}</p>
|
||||
<p class="narrative-text">{{ application.analysis_data.red_flags|join:". "|default:"None." }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.strengths %}
|
||||
{% if application.analysis_data.strengths %}
|
||||
<div class="narrative-box strength-box">
|
||||
<h3 class="flag-title green"><i class="fas fa-circle-check"></i>Strengths</h3>
|
||||
<!-- scoring_data.strengths -->
|
||||
<p class="narrative-text">{{ candidate.analysis_data.strengths }}</p>
|
||||
<p class="narrative-text">{{ application.analysis_data.strengths }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.weaknesses %}
|
||||
{% if application.analysis_data.weaknesses %}
|
||||
<div class="narrative-box" style="margin-bottom: 1rem;">
|
||||
<h3 class="flag-title red"><i class="fas fa-triangle-exclamation"></i>Weaknesses</h3>
|
||||
<!-- scoring_data.weaknesses -->
|
||||
<p class="narrative-text">{{ candidate.analysis_data.weaknesses }}</p>
|
||||
<p class="narrative-text">{{ application.analysis_data.weaknesses }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.recommendation %}
|
||||
{% if application.analysis_data.recommendation %}
|
||||
<div class="analysis-summary">
|
||||
<h3 style="font-size: 0.875rem;">Recommendation</h3>
|
||||
<!-- scoring_data.recommendation -->
|
||||
<p style="font-size: 0.875rem;">{{ candidate.analysis_data.recommendation }}</p>
|
||||
<p style="font-size: 0.875rem;">{{ application.analysis_data.recommendation }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Criteria Checklist Section -->
|
||||
{% if candidate.analysis_data.criteria_checklist %}
|
||||
{% if application.analysis_data.criteria_checklist %}
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-list-check"></i>
|
||||
Required Criteria Check
|
||||
</h2>
|
||||
<div style="margin-top: 0.75rem;">
|
||||
{% for criterion, status in candidate.analysis_data.criteria_checklist.items %}
|
||||
{% for criterion, status in application.analysis_data.criteria_checklist.items %}
|
||||
<div class="criteria-item">
|
||||
<span class="text-gray-700">{{ criterion }}</span>
|
||||
<span class="metric-value" style="font-size: 0.875rem;">
|
||||
@ -790,10 +790,10 @@
|
||||
<i class="fas fa-tools"></i>
|
||||
Skills
|
||||
</h2>
|
||||
{% if candidate.resume_data.skills %}
|
||||
{% for category, skills in candidate.resume_data.skills.items %}
|
||||
{% if application.resume_data.skills %}
|
||||
{% for category, skills in application.resume_data.skills.items %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<!-- candidate.resume_data.skills -->
|
||||
<!-- application.resume_data.skills -->
|
||||
<h3 class="keyword-subheader" style="color: var(--color-gray-700);"><i class="fas fa-list-alt" style="color: transparent;"></i>{{ category|cut:"_"|title }}</h3>
|
||||
<div class="tag-list">
|
||||
{% for skill in skills %}
|
||||
@ -813,8 +813,8 @@
|
||||
<i class="fas fa-language"></i>
|
||||
Languages
|
||||
</h2>
|
||||
{% if candidate.analysis_data.language_fluency %}
|
||||
{% for language in candidate.analysis_data.language_fluency %}
|
||||
{% if application.analysis_data.language_fluency %}
|
||||
{% for language in application.analysis_data.language_fluency %}
|
||||
<div style="margin-bottom: 0.75rem;">
|
||||
<div class="analysis-metric" style="margin-bottom: 0.25rem; border-bottom: none;">
|
||||
<!-- scoring_data.language_fluency -->
|
||||
@ -842,69 +842,69 @@
|
||||
Key Metrics
|
||||
</h2>
|
||||
<div style="margin-top: 0.75rem;">
|
||||
{% if candidate.analysis_data.min_req_met_bool is not none %}
|
||||
{% if application.analysis_data.min_req_met_bool is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-shield-halved"></i>Min Requirements Met:</span>
|
||||
<!-- scoring_data.min_req_met_bool -->
|
||||
<span class="metric-value {% if candidate.analysis_data.min_req_met_bool %}text-green-check{% else %}text-red-x{% endif %}">
|
||||
{% if candidate.analysis_data.min_req_met_bool %}<i class="fas fa-check-circle"></i> Yes{% else %}<i class="fas fa-times-circle"></i> No{% endif %}
|
||||
<span class="metric-value {% if application.analysis_data.min_req_met_bool %}text-green-check{% else %}text-red-x{% endif %}">
|
||||
{% if application.analysis_data.min_req_met_bool %}<i class="fas fa-check-circle"></i> Yes{% else %}<i class="fas fa-times-circle"></i> No{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.years_of_experience is not none %}
|
||||
{% if application.analysis_data.years_of_experience is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-clock"></i>Total Experience:</span>
|
||||
<!-- scoring_data.years_of_experience -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.years_of_experience|floatformat:1 }} years</span>
|
||||
<span class="metric-value">{{ application.analysis_data.years_of_experience|floatformat:1 }} years</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.most_recent_job_title %}
|
||||
{% if application.analysis_data.most_recent_job_title %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-id-badge"></i>Most Recent Title (Scoring):</span>
|
||||
<!-- scoring_data.most_recent_job_title (explicitly added) -->
|
||||
<span class="metric-value max-w-50-percent" style="text-align: right;">{{ candidate.analysis_data.most_recent_job_title }}</span>
|
||||
<span class="metric-value max-w-50-percent" style="text-align: right;">{{ application.analysis_data.most_recent_job_title }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.seniority_level_match is not none %}
|
||||
{% if application.analysis_data.seniority_level_match is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-user-tie"></i>Seniority Match:</span>
|
||||
<!-- scoring_data.seniority_level_match -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.seniority_level_match|default:0 }}/100</span>
|
||||
<span class="metric-value">{{ application.analysis_data.seniority_level_match|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.soft_skills_score is not none %}
|
||||
{% if application.analysis_data.soft_skills_score is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-handshake"></i>Soft Skills Score:</span>
|
||||
<!-- scoring_data.soft_skills_score -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.soft_skills_score|default:0 }}/100</span>
|
||||
<span class="metric-value">{{ application.analysis_data.soft_skills_score|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.employment_stability_score is not none %}
|
||||
{% if application.analysis_data.employment_stability_score is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-anchor"></i>Stability Score:</span>
|
||||
<!-- scoring_data.employment_stability_score -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.employment_stability_score|default:0 }}/100</span>
|
||||
<span class="metric-value">{{ application.analysis_data.employment_stability_score|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.experience_industry_match is not none %}
|
||||
{% if application.analysis_data.experience_industry_match is not none %}
|
||||
<div class="analysis-metric" style="border-bottom: none; padding-bottom: 0;">
|
||||
<span class="metric-label"><i class="fas fa-industry"></i>Industry Match:</span>
|
||||
<!-- scoring_data.experience_industry_match -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.experience_industry_match|default:0 }}/100</span>
|
||||
<span class="metric-value">{{ application.analysis_data.experience_industry_match|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if candidate.analysis_data.transferable_skills_narrative %}
|
||||
{% if application.analysis_data.transferable_skills_narrative %}
|
||||
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-gray-100); font-size: 0.875rem; color: var(--color-gray-500);">
|
||||
<i class="fas fa-puzzle-piece" style="margin-right: 0.25rem;"></i> Transferable Skills:
|
||||
<!-- scoring_data.transferable_skills_narrative -->
|
||||
{{ candidate.analysis_data.transferable_skills_narrative }}
|
||||
{{ application.analysis_data.transferable_skills_narrative }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
@ -914,7 +914,7 @@
|
||||
|
||||
|
||||
{% if LANGUAGE_CODE == 'ar' %}
|
||||
{% with data_source=candidate.resume_data_ar analysis_source=candidate.analysis_data_ar %}
|
||||
{% with data_source=application.resume_data_ar analysis_source=application.analysis_data_ar %}
|
||||
<div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto; direction: rtl; text-align: right;">
|
||||
|
||||
{% include 'recruitment/partials/ai_overview_breadcromb.html' %}
|
||||
@ -1263,13 +1263,13 @@
|
||||
{% endwith %}
|
||||
|
||||
{% else %}
|
||||
{% with data_source=candidate.resume_data_en analysis_source=candidate.analysis_data_en %}
|
||||
{% with data_source=application.resume_data_en analysis_source=application.analysis_data_en %}
|
||||
<div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
|
||||
|
||||
{% include 'recruitment/partials/ai_overview_breadcromb.html' %}
|
||||
<header class="header-box">
|
||||
<div class="header-info">
|
||||
<h1>{{ data_source.full_name|default:"Candidate Name" }}</h1>
|
||||
<h1>{{ data_source.full_name|default:"Application Name" }}</h1>
|
||||
<p>{{ data_source.current_title|default:"Professional Title" }}</p>
|
||||
<div class="contact-details">
|
||||
<div class="contact-item">
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Document Review - {{ job.title }} - University ATS{% endblock %}
|
||||
{% block title %}{% blocktrans %} Document Review - {{ job.title }} - University ATS{%endblocktrans %}{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
@ -60,8 +60,8 @@
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -70,10 +70,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -81,24 +81,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -118,7 +118,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -213,7 +213,7 @@
|
||||
<div class="d-flex gap-2">
|
||||
{% comment %} <a href="{% url 'export_applications_csv' job.slug 'document_review' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export document review candidates to CSV' %}">
|
||||
title="{% trans 'Export document review applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a> {% endcomment %}
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -229,7 +229,7 @@
|
||||
<!-- Search and Filter Controls -->
|
||||
<div class="filter-controls">
|
||||
<h4 class="h6 mb-3 fw-bold">
|
||||
<i class="fas fa-search me-1"></i> {% trans "Search Candidates" %}
|
||||
<i class="fas fa-search me-1"></i> {% trans "Search Applications" %}
|
||||
</h4>
|
||||
<form method="GET" class="mb-0">
|
||||
<div class="row g-3 align-items-end">
|
||||
@ -251,18 +251,18 @@
|
||||
</div>
|
||||
|
||||
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
|
||||
<i class="fas fa-users me-1"></i> {% trans "Candidates Ready for Document Review" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ candidates|length }}</span>
|
||||
<i class="fas fa-users me-1"></i> {% trans "Applications Ready for Document Review" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ applications|length }}</span>
|
||||
</h2>
|
||||
|
||||
<div class="kaauh-card p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
{# Use d-flex to align the entire contents (two forms and the separator) horizontally #}
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
|
||||
{# Form 1: Status Update #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
||||
@ -292,7 +292,7 @@
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -300,12 +300,12 @@
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
<table class="table candidate-table align-middle">
|
||||
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 2%;">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
</div>
|
||||
@ -329,21 +329,21 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
<input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-name">
|
||||
{{ candidate.person.first_name }} {{ candidate.person.last_name }}
|
||||
<div class="application-name">
|
||||
{{ application.person.first_name }} {{ application.person.last_name }}
|
||||
</div>
|
||||
</td>
|
||||
<td><div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.person.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.person.phone|default:"--" }}
|
||||
<td><div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.person.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.person.phone|default:"--" }}
|
||||
</div></td>
|
||||
<td>
|
||||
<span class="stage-badge stage-Interview">
|
||||
@ -351,7 +351,7 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% with documents=candidate.documents.all %}
|
||||
{% with documents=application.documents.all %}
|
||||
{% if documents %}
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
@ -398,7 +398,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'application_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
<i class="fas fa-eye ms-1"></i>
|
||||
@ -412,7 +412,7 @@
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates are currently ready for document review." %}
|
||||
{% trans "No applications are currently ready for document review." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
@ -425,7 +425,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
|
||||
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Candidate Details / Bulk Action Form" %}
|
||||
{% trans "Application Details / Bulk Action Form" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Candidate Tier Management - {{ job.title }} - ATS{% endblock %}
|
||||
{% block title %}{% blocktrans %}Application Tier Management - {{ job.title }} - ATS {% endblocktrans %}{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
@ -71,8 +71,8 @@
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -81,10 +81,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -92,24 +92,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -129,7 +129,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -174,13 +174,13 @@
|
||||
{% trans "Exam Management" %} - {{ job.title }}
|
||||
</h1>
|
||||
<h2 class="h5 text-muted mb-0">
|
||||
{% trans "Candidates in Exam Stage:" %} <span class="fw-bold">{{ total_candidates }}</span>
|
||||
{% trans "Applications in Exam Stage:" %} <span class="fw-bold">{{ total_candidates }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'export_applications_csv' job.slug 'exam' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export exam candidates to CSV' %}">
|
||||
title="{% trans 'Export exam applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -194,15 +194,15 @@
|
||||
</div>
|
||||
|
||||
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
|
||||
{% trans "Candidate List" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ candidates|length }} / {{ total_candidates }} Total</span>
|
||||
{% trans "Application List" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ applications|length }} / {{ total_candidates }} Total</span>
|
||||
<small class="text-muted fw-normal ms-2">({% trans "Sorted by AI Score" %})</small>
|
||||
</h2>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Using d-flex for horizontal alignment and align-items-end to align items to the bottom baseline #}
|
||||
@ -235,7 +235,7 @@
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -245,13 +245,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" method="post">
|
||||
<form id="application-form" method="post">
|
||||
{% csrf_token %}
|
||||
<table class="table candidate-table align-middle">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2%;">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
@ -268,56 +268,56 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input
|
||||
name="candidate_ids"
|
||||
value="{{ candidate.id }}"
|
||||
type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
value="{{ application.id }}"
|
||||
type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-name">
|
||||
{{ candidate.name }}
|
||||
<div class="application-name">
|
||||
{{ application.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
|
||||
<div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge ai-score-badge">{{ candidate.match_score|default:"0" }}%</span>
|
||||
<span class="badge ai-score-badge">{{ application.match_score|default:"0" }}%</span>
|
||||
</td>
|
||||
<td>
|
||||
{{candidate.exam_date|date:"d-m-Y h:i A"|default:"--"}}
|
||||
{{application.exam_date|date:"d-m-Y h:i A"|default:"--"}}
|
||||
</td>
|
||||
<td id="exam-score-{{ candidate.pk}}">
|
||||
{{candidate.exam_score|default:"--"}}
|
||||
<td id="exam-score-{{ application.pk}}">
|
||||
{{application.exam_score|default:"--"}}
|
||||
</td>
|
||||
|
||||
<td class="text-center" id="status-result-{{ candidate.pk}}">
|
||||
{% if not candidate.exam_status %}
|
||||
<td class="text-center" id="status-result-{{ application.pk}}">
|
||||
{% if not application.exam_status %}
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'exam' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'exam' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if candidate.exam_status %}
|
||||
<button type="button" class="btn btn-{% if candidate.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
{% if application.exam_status %}
|
||||
<button type="button" class="btn btn-{% if application.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'exam' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'exam' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
{{ candidate.exam_status }}
|
||||
{{ application.exam_status }}
|
||||
</button>
|
||||
{% else %}
|
||||
--
|
||||
@ -329,7 +329,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'application_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
<i class="fas fa-eye"></i>
|
||||
@ -339,10 +339,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not candidates %}
|
||||
{% if not applications %}
|
||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates are currently in the Exam stage for this job." %}
|
||||
{% trans "No applications are currently in the Exam stage for this job." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
@ -355,14 +355,14 @@
|
||||
<div class="modal-content kaauh-card">
|
||||
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
|
||||
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Candidate Details & Exam Update" %}
|
||||
{% trans "Application Details & Exam Update" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div id="candidateviewModalBody" class="modal-body">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||
{% trans "Loading candidate data..." %}
|
||||
{% trans "Loading application data..." %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="border-top: 1px solid var(--kaauh-border);">
|
||||
@ -60,8 +60,8 @@
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -70,10 +70,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -81,24 +81,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -118,7 +118,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -189,22 +189,22 @@
|
||||
<div>
|
||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-trophy me-2"></i>
|
||||
{% trans "Hired Candidates" %} - {{ job.title }}
|
||||
{% trans "Hired Applications" %} - {{ job.title }}
|
||||
</h1>
|
||||
<h2 class="h5 text-muted mb-0">
|
||||
{% trans "Successfully Hired:" %} <span class="fw-bold">{{ candidates|length }}</span>
|
||||
{% trans "Successfully Hired:" %} <span class="fw-bold">{{ applications|length }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button"
|
||||
class="btn btn-main-action"
|
||||
onclick="syncHiredCandidates()"
|
||||
title="{% trans 'Sync hired candidates to external sources' %}">
|
||||
title="{% trans 'Sync hired applications to external sources' %}">
|
||||
<i class="fas fa-sync me-1"></i> {% trans "Sync to Sources" %}
|
||||
</button>
|
||||
<a href="{% url 'export_applications_csv' job.slug 'hired' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export hired candidates to CSV' %}">
|
||||
title="{% trans 'Export hired applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -217,7 +217,7 @@
|
||||
<div class="success-header">
|
||||
<i class="fas fa-check-circle fa-3x mb-3"></i>
|
||||
<h3 class="mb-2">{% trans "Congratulations!" %}</h3>
|
||||
<p class="mb-0">{% trans "These candidates have successfully completed the hiring process and joined your team." %}</p>
|
||||
<p class="mb-0">{% trans "These applications have successfully completed the hiring process and joined your team." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="applicant-tracking-timeline">
|
||||
@ -225,9 +225,9 @@
|
||||
</div>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# MODIFIED: Using d-flex for horizontal alignment and align-items-end to align everything based on the baseline of the button/select #}
|
||||
@ -258,7 +258,7 @@
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -269,13 +269,13 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
{% csrf_token %}
|
||||
<table class="table candidate-table align-middle">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2%">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
@ -291,36 +291,36 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
<input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-name">
|
||||
{{ candidate.name }}
|
||||
<div class="application-name">
|
||||
{{ application.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
|
||||
<div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<div class="application-details">
|
||||
<strong>{{ job.title }}</strong><br>
|
||||
<small class="text-muted">{{ job.department }}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="hired-date">
|
||||
{% if candidate.offer_date %}
|
||||
{% if application.offer_date %}
|
||||
<i class="fas fa-calendar me-1"></i>
|
||||
{{ candidate.offer_date|date:"M d, Y" }}
|
||||
{{ application.offer_date|date:"M d, Y" }}
|
||||
{% else %}
|
||||
<span class="text-muted">--</span>
|
||||
{% endif %}
|
||||
@ -337,12 +337,12 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'application_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<a href="{% url 'application_resume_template' candidate.slug %}"
|
||||
<a href="{% url 'application_resume_template' application.slug %}"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="View Resume Template">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
@ -356,10 +356,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
{% if not candidates %}
|
||||
{% if not applications %}
|
||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates have been hired for this position yet." %}
|
||||
{% trans "No applications have been hired for this position yet." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -372,7 +372,7 @@
|
||||
<div class="modal-content kaauh-card">
|
||||
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
|
||||
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Hired Candidate Details" %}
|
||||
{% trans "Hired Application Details" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -399,7 +399,7 @@
|
||||
<div id="syncResultsModalBody" class="modal-body">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||
{% trans "Syncing candidates..." %}
|
||||
{% trans "Syncing applications..." %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -491,8 +491,8 @@
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5>{% trans "Syncing hired candidates..." %}</h5>
|
||||
<p class="text-muted">{% trans "Please wait while we sync candidates to external sources." %}</p>
|
||||
<h5>{% trans "Syncing hired applications..." %}</h5>
|
||||
<p class="text-muted">{% trans "Please wait while we sync applications to external sources." %}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -553,7 +553,7 @@
|
||||
<strong>{% trans "Failed:" %}</strong> <span class="text-danger">${results.failed_syncs}</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>{% trans "Candidates Synced:" %}</strong> ${results.total_candidates}
|
||||
<strong>{% trans "Applications Synced:" %}</strong> ${results.total_candidates}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -579,7 +579,7 @@
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">{% trans "Candidates Processed:" %}</small>
|
||||
<small class="text-muted">{% trans "Applications Processed:" %}</small>
|
||||
<div class="fw-bold">${source.candidates_processed}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}- {{ job.title }} - ATS{% endblock %}
|
||||
{% block title %}{% blocktrans %}Application Tier Management - {{ job.title }} - ATS {% endblocktrans %}{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
@ -71,8 +71,8 @@
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -81,10 +81,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -92,24 +92,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -129,7 +129,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -177,13 +177,13 @@
|
||||
{% trans "Interview Management" %} - {{ job.title }}
|
||||
</h1>
|
||||
<h2 class="h5 text-muted mb-0">
|
||||
{% trans "Candidates in Interview Stage:" %} <span class="fw-bold">{{ candidates|length }}</span>
|
||||
{% trans "Applications in Interview Stage:" %} <span class="fw-bold">{{ applications|length }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'export_applications_csv' job.slug 'interview' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export interview candidates to CSV' %}">
|
||||
title="{% trans 'Export interview applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -196,13 +196,13 @@
|
||||
</div>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
{# Use d-flex to align the entire contents (two forms and the separator) horizontally #}
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
|
||||
{# Form 1: Status Update #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
||||
@ -227,7 +227,7 @@
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
|
||||
{# Form 2: Schedule Interviews #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
|
||||
</button>
|
||||
@ -241,7 +241,7 @@
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -251,13 +251,13 @@
|
||||
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
{% csrf_token %}
|
||||
<table class="table candidate-table align-middle">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2%">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
@ -276,11 +276,11 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
<input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
|
||||
</td>
|
||||
@ -288,35 +288,35 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'application_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
{{ candidate.name }}
|
||||
{{ application.name }}
|
||||
</button>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
|
||||
<div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="candidate-details text-muted">
|
||||
{% if candidate.get_latest_meeting %}
|
||||
{{ candidate.get_latest_meeting }}
|
||||
<td class="application-details text-muted">
|
||||
{% if application.get_latest_meeting %}
|
||||
{{ application.get_latest_meeting }}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="candidate-details text-muted"><div class="d-block">
|
||||
{% if candidate.get_latest_meeting.duration %}
|
||||
{{ candidate.get_latest_meeting.duration }} {% trans _("Minutes") %}
|
||||
<td class="application-details text-muted"><div class="d-block">
|
||||
{% if application.get_latest_meeting.duration %}
|
||||
{{ application.get_latest_meeting.duration }} {% trans _("Minutes") %}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
</div></td>
|
||||
<td class="candidate-details text-muted">
|
||||
{% with latest_meeting=candidate.get_latest_meeting %}
|
||||
<td class="application-details text-muted">
|
||||
{% with latest_meeting=application.get_latest_meeting %}
|
||||
{% if latest_meeting %}
|
||||
{{ latest_meeting.start_time|date:"d-m-Y h:i A" }}
|
||||
{% else %}
|
||||
@ -325,7 +325,7 @@
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td>
|
||||
{% with latest_meeting=candidate.get_latest_meeting %}
|
||||
{% with latest_meeting=application.get_latest_meeting %}
|
||||
{% if latest_meeting and latest_meeting.details_url %}
|
||||
<a href="{{ latest_meeting.details_url }}" target="_blank" class="btn btn-sm bg-primary-theme text-white" title="Join Interview"
|
||||
{% if latest_meeting.status == 'ended' %}disabled{% endif %}>
|
||||
@ -339,7 +339,7 @@
|
||||
</td>
|
||||
<td>
|
||||
{{ latest_meeting.status }}
|
||||
{% with latest_meeting=candidate.get_latest_meeting %}
|
||||
{% with latest_meeting=application.get_latest_meeting %}
|
||||
{% if latest_meeting %}
|
||||
<span class="badge {% if latest_meeting.status == 'waiting' %}bg-warning{% elif latest_meeting.status == 'started' %}bg-success{% elif latest_meeting.status == 'ended' %}bg-danger{% endif %}">
|
||||
{% if latest_meeting.status == 'started' %}
|
||||
@ -352,25 +352,25 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="text-center" id="interview-result-{{ candidate.pk }}">
|
||||
{% if not candidate.interview_status %}
|
||||
<td class="text-center" id="interview-result-{{ application.pk }}">
|
||||
{% if not application.interview_status %}
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'interview' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if candidate.interview_status %}
|
||||
<button type="button" class="btn btn-{% if candidate.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
{% if application.interview_status %}
|
||||
<button type="button" class="btn btn-{% if application.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'interview' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
{{ candidate.interview_status }}
|
||||
{{ application.interview_status }}
|
||||
</button>
|
||||
{% else %}
|
||||
--
|
||||
@ -379,13 +379,13 @@
|
||||
</td>
|
||||
<td>
|
||||
|
||||
{% if candidate.get_latest_meeting %}
|
||||
{% if candidate.get_latest_meeting.location_type == 'Remote'%}
|
||||
{% if application.get_latest_meeting %}
|
||||
{% if application.get_latest_meeting.location_type == 'Remote'%}
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'reschedule_meeting_for_application' job.slug candidate.pk candidate.get_latest_meeting.pk %}"
|
||||
hx-get="{% url 'reschedule_meeting_for_application' job.slug application.pk application.get_latest_meeting.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Reschedule">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
@ -394,7 +394,7 @@
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'schedule_meeting_for_application' job.slug candidate.pk candidate.get_latest_meeting.pk %}"
|
||||
hx-get="{% url 'schedule_meeting_for_application' job.slug application.pk application.get_latest_meeting.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Delete Meeting">
|
||||
<i class="fas fa-trash"></i>
|
||||
@ -404,7 +404,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'reschedule_onsite_meeting' job.slug candidate.pk candidate.get_latest_meeting.pk %}"
|
||||
hx-get="{% url 'reschedule_onsite_meeting' job.slug application.pk application.get_latest_meeting.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Reschedule">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
@ -413,7 +413,7 @@
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'delete_onsite_meeting_for_application' job.slug candidate.pk candidate.get_latest_meeting.pk %}"
|
||||
hx-get="{% url 'delete_onsite_meeting_for_application' job.slug application.pk application.get_latest_meeting.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Delete Meeting">
|
||||
<i class="fas fa-trash"></i>
|
||||
@ -425,7 +425,7 @@
|
||||
<button type="button" class="btn btn-main-action btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'schedule_meeting_for_application' job.slug candidate.pk %}"
|
||||
hx-get="{% url 'schedule_meeting_for_application' job.slug application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
data-modal-title="{% trans 'Schedule Interview' %}"
|
||||
title="Schedule Interview">
|
||||
@ -435,7 +435,7 @@
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
{# UPDATED: Points to the specific Onsite scheduling URL #}
|
||||
hx-get="{% url 'schedule_onsite_meeting_for_application' job.slug candidate.pk %}"
|
||||
hx-get="{% url 'schedule_onsite_meeting_for_application' job.slug application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
data-modal-title="{% trans 'Schedule Onsite Interview' %}"
|
||||
title="Schedule Onsite Interview">
|
||||
@ -448,10 +448,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not candidates %}
|
||||
{% if not applications %}
|
||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates are currently in the Interview stage for this job." %}
|
||||
{% trans "No applications are currently in the Interview stage for this job." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
@ -463,7 +463,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
|
||||
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Candidate Details / Bulk Action Form" %}
|
||||
{% trans "Application Details / Bulk Action Form" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -598,7 +598,7 @@
|
||||
</div>
|
||||
`;
|
||||
// Reset the modal title to its default state for next use
|
||||
const defaultTitle = "{% trans "Candidate Details / Bulk Action Form" %}";
|
||||
const defaultTitle = "{% trans "Application Details / Bulk Action Form" %}";
|
||||
candidateviewModalLabel.textContent = defaultTitle;
|
||||
}
|
||||
});
|
||||
@ -1,7 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}- {{ job.title }} - ATS{% endblock %}
|
||||
{% block title %}{% blocktrans %}Application Tier Management - {{ job.title }} - ATS {% endblocktrans %}{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
@ -71,8 +70,8 @@
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -81,10 +80,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -92,24 +91,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -129,7 +128,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -176,13 +175,13 @@
|
||||
{% trans "Offer Management" %} - {{ job.title }}
|
||||
</h1>
|
||||
<h2 class="h5 text-muted mb-0">
|
||||
{% trans "Candidates in Offer Stage:" %} <span class="fw-bold">{{ candidates|length }}</span>
|
||||
{% trans "Applications in Offer Stage:" %} <span class="fw-bold">{{ applications|length }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'export_applications_csv' job.slug 'offer' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export offer candidates to CSV' %}">
|
||||
title="{% trans 'Export offer applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -195,13 +194,13 @@
|
||||
</div>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
{# Use d-flex and align-items-end on the container to align the form and the separator #}
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
|
||||
{# Form: Hired/Rejected Status Update #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Select element #}
|
||||
@ -238,7 +237,7 @@
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -247,13 +246,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get">
|
||||
{% csrf_token %}
|
||||
<table class="table candidate-table align-middle">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2%">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
@ -270,43 +269,43 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
<input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-name">
|
||||
{{ candidate.name }}
|
||||
<div class="application-name">
|
||||
{{ application.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
|
||||
<div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center" id="status-result-{{ candidate.pk}}">
|
||||
{% if not candidate.offer_status %}
|
||||
<td class="text-center" id="status-result-{{ application.pk}}">
|
||||
{% if not application.offer_status %}
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'offer' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'offer' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if candidate.offer_status %}
|
||||
<button type="button" class="btn btn-{% if candidate.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm"
|
||||
{% if application.offer_status %}
|
||||
<button type="button" class="btn btn-{% if application.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'offer' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'offer' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
{{ candidate.offer_status }}
|
||||
{{ application.offer_status }}
|
||||
</button>
|
||||
{% else %}
|
||||
--
|
||||
@ -314,7 +313,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% with documents=candidate.documents.all %}
|
||||
{% with documents=application.documents.all %}
|
||||
{% if documents %}
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
@ -361,7 +360,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'application_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
<i class="fas fa-eye"></i>
|
||||
@ -371,10 +370,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not candidates %}
|
||||
{% if not applications %}
|
||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates are currently in the Offer stage for this job." %}
|
||||
{% trans "No applications are currently in the Offer stage for this job." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
@ -387,7 +386,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
|
||||
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Candidate Details / Bulk Action Form" %}
|
||||
{% trans "Application Details / Bulk Action Form" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Candidate Management - {{ job.title }} - University ATS{% endblock %}
|
||||
{% block title %}Application Management - {{ job.title }} - University ATS{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
@ -71,8 +71,8 @@
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
/* 3. Application Table Styling (Aligned with KAAT-S) */
|
||||
.application-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
@ -81,10 +81,10 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
.application-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
.application-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -92,24 +92,24 @@
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
.application-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
.application-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
.application-table thead th:nth-child(1) { width: 40px; }
|
||||
.application-table thead th:nth-child(4) { width: 10%; }
|
||||
.application-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
.application-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
.application-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
@ -129,7 +129,7 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
.bg-application { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
@ -216,7 +216,7 @@
|
||||
<div>
|
||||
<h1 class="h3 mb-1 page-header">
|
||||
<i class="fas fa-layer-group me-2"></i>
|
||||
{% trans "Applicant Screening" %}
|
||||
{% trans "Application Screening" %}
|
||||
</h1>
|
||||
<h2 class="h5 text-muted mb-0">
|
||||
{% trans "Job:" %} {{ job.title }}
|
||||
@ -226,7 +226,7 @@
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'export_applications_csv' job.slug 'screening' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans 'Export screening candidates to CSV' %}">
|
||||
title="{% trans 'Export screening applications to CSV' %}">
|
||||
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
|
||||
</a>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
@ -241,7 +241,7 @@
|
||||
|
||||
<div class="filter-controls">
|
||||
<h4 class="h6 mb-3 fw-bold">
|
||||
<i class="fas fa-sort-numeric-up me-1"></i> {% trans "AI Scoring & Top Candidate Filter" %}
|
||||
<i class="fas fa-sort-numeric-up me-1"></i> {% trans "AI Scoring & Top Application Filter" %}
|
||||
</h4>
|
||||
|
||||
<form method="GET" class="mb-0">
|
||||
@ -296,10 +296,10 @@
|
||||
|
||||
<div class="col-auto">
|
||||
<label for="tier1_count" class="form-label small text-muted mb-1">
|
||||
{% trans "Top N Candidates" %}
|
||||
{% trans "Top N applications" %}
|
||||
</label>
|
||||
<input type="number" name="tier1_count" id="tier1_count" class="form-control form-control-sm"
|
||||
value="{{ tier1_count }}" min="1" max="{{ total_candidates }}" style="width: 120px;">
|
||||
value="{{ tier1_count }}" min="1" max="{{ total_applications }}" style="width: 120px;">
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
@ -312,14 +312,14 @@
|
||||
</div>
|
||||
|
||||
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
|
||||
<i class="fas fa-users me-1"></i> {% trans "Candidate List" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ candidates|length }} / {{ total_candidates }} Total</span>
|
||||
<i class="fas fa-users me-1"></i> {% trans "Application List" %}
|
||||
<span class="badge bg-primary-theme ms-2">{{ applications|length }} / {{ total_applications }} {% trans "Total" %}</span>
|
||||
</h2>
|
||||
|
||||
<div class="kaauh-card p-3">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# MODIFIED: Using d-flex for horizontal alignment and align-items-end to align everything based on the baseline of the button/select #}
|
||||
@ -350,7 +350,7 @@
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
@ -361,13 +361,13 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" action="{% url 'application_update_status' job.slug %}" method="post">
|
||||
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<table class="table candidate-table align-middle">
|
||||
<table class="table application-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 2%;">
|
||||
{% if candidates %}
|
||||
{% if applications %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||
@ -401,33 +401,33 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input
|
||||
name="candidate_ids"
|
||||
value="{{ candidate.id }}"
|
||||
type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
value="{{ application.id }}"
|
||||
type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="candidate-name text-primary-theme text-decoration-none">
|
||||
{{ candidate.name }}
|
||||
<a href="#" class="application-name text-primary-theme text-decoration-none">
|
||||
{{ application.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone }}
|
||||
<div class="application-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ application.email }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">{{candidate.person.gpa|default:"0"}}</td>
|
||||
<td class="text-center">{{application.person.gpa|default:"0"}}</td>
|
||||
<td class="text-center">
|
||||
{% if candidate.is_resume_parsed %}
|
||||
{% if candidate.match_score %}
|
||||
{% if application.is_resume_parsed %}
|
||||
{% if application.match_score %}
|
||||
<span class="badge ai-score-badge">
|
||||
{{ candidate.match_score|default:"0" }}%
|
||||
{{ application.match_score|default:"0" }}%
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
@ -442,23 +442,23 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if candidate.screening_stage_rating %}
|
||||
{% if application.screening_stage_rating %}
|
||||
<span class="badge ai-score-badge d-inline-block text-wrap">
|
||||
{{ candidate.screening_stage_rating|default:"--" }}
|
||||
{{ application.screening_stage_rating|default:"--" }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if candidate.professional_category %}
|
||||
{% if application.professional_category %}
|
||||
<span class="badge ai-score-badge">
|
||||
{{ candidate.professional_category }}
|
||||
{{ application.professional_category }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if candidate.top_3_keywords %}
|
||||
{% if application.top_3_keywords %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for skill in candidate.top_3_keywords %}
|
||||
{% for skill in application.top_3_keywords %}
|
||||
<span class="badge ai-score-badge" style="font-size: smaller;">
|
||||
{{ skill }}
|
||||
</span>
|
||||
@ -471,9 +471,9 @@
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'application_criteria_view_htmx' candidate.pk %}"
|
||||
hx-get="{% url 'application_criteria_view_htmx' application.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Candidate Profile and Criteria">
|
||||
title="View Application Profile and Criteria">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
@ -481,10 +481,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not candidates %}
|
||||
{% if not applications %}
|
||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "No candidates match the current stage and filter criteria." %}
|
||||
{% trans "No applications match the current stage and filter criteria." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
@ -498,14 +498,14 @@
|
||||
<div class="modal-content kaauh-card">
|
||||
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
|
||||
<h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Candidate Criteria Review" %}
|
||||
{% trans "Application Criteria Review" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div id="candidateviewModalBody" class="modal-body">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||
{% trans "Loading candidate data..." %}
|
||||
{% trans "Loading application data..." %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="border-top: 1px solid var(--kaauh-border);">
|
||||
@ -1,11 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete Candidate - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1>
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2
|
||||
@ -167,7 +167,7 @@
|
||||
}
|
||||
|
||||
/* Funnel Specific Styles */
|
||||
#candidate_funnel_chart {
|
||||
#application_funnel_chart {
|
||||
max-height: 400px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
@ -224,7 +224,7 @@
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
<i class="fas fa-chart-area stat-icon"></i>
|
||||
{% trans "Daily Candidate Applications Trend" %}
|
||||
{% trans "Daily Applications Trend" %}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
@ -261,7 +261,7 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="chart-container d-flex justify-content-center align-items-center">
|
||||
<canvas id="candidate_funnel_chart"></canvas>
|
||||
<canvas id="application_funnel_chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -289,11 +289,11 @@
|
||||
|
||||
<script>
|
||||
// Pass context data safely to JavaScript
|
||||
const totalCandidatesScoped = parseInt('{{ total_candidates|default:0 }}');
|
||||
const totalCandidatesScoped = parseInt('{{ total_applications|default:0 }}');
|
||||
const jobTitles = JSON.parse('{{ job_titles|escapejs }}').slice(0, 5);
|
||||
const jobAppCounts = JSON.parse('{{ job_app_counts|escapejs }}').slice(0, 5);
|
||||
const stages = JSON.parse('{{ candidate_stage|escapejs }}');
|
||||
const counts = JSON.parse('{{ candidates_count|safe }}');
|
||||
const stages = JSON.parse('{{ application_stage|escapejs }}');
|
||||
const counts = JSON.parse('{{ application_count|safe }}');
|
||||
|
||||
// --- 1. BAR CHART configuration (Top 5 Applications) ---
|
||||
const ctxBar = document.getElementById('applicationsChart').getContext('2d');
|
||||
@ -358,7 +358,7 @@
|
||||
// Slice and use the first N shades based on the number of stages
|
||||
const stageColors = tealShades.slice(tealShades.length - stages.length);
|
||||
|
||||
const ctxFunnel = document.getElementById('candidate_funnel_chart').getContext('2d');
|
||||
const ctxFunnel = document.getElementById('application_funnel_chart').getContext('2d');
|
||||
|
||||
new Chart(ctxFunnel, {
|
||||
type: 'bar',
|
||||
@ -377,7 +377,7 @@
|
||||
},
|
||||
// 2. VISIBLE CANDIDATE COUNT DATASET
|
||||
{
|
||||
label: '{% trans "Candidate Count" %}',
|
||||
label: '{% trans "Application Count" %}',
|
||||
data: counts,
|
||||
backgroundColor: stageColors,
|
||||
barThickness: 50
|
||||
@ -490,7 +490,7 @@
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: '{% trans "New Candidates" %}' },
|
||||
title: { display: true, text: '{% trans "New Applications" %}' },
|
||||
ticks: { precision: 0 },
|
||||
grid: { color: '#e0e0e0' }
|
||||
}
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
<td class="text-center" id="status-result-{{ candidate.pk}}">
|
||||
{% if candidate.exam_status %}
|
||||
<button type="button" class="btn btn-{% if candidate.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
<td class="text-center" id="status-result-{{ application.pk}}">
|
||||
{% if application.exam_status %}
|
||||
<button type="button" class="btn btn-{% if application.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'exam' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'exam' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
{{ candidate.exam_status }}
|
||||
{{ application.exam_status }}
|
||||
</button>
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td id="exam-score-{{ candidate.pk}}" hx-swap-oob="true">
|
||||
{{candidate.exam_score|default:"--"}}
|
||||
<td id="exam-score-{{ application.pk}}" hx-swap-oob="true">
|
||||
{{application.exam_score|default:"--"}}
|
||||
</td>
|
||||
@ -1,22 +1,22 @@
|
||||
<td class="text-center" id="interview-result-{{ candidate.pk}}">
|
||||
{% if not candidate.interview_status %}
|
||||
<td class="text-center" id="interview-result-{{ application.pk}}">
|
||||
{% if not application.interview_status %}
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'interview' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if candidate.interview_status %}
|
||||
<button type="button" class="btn btn-{% if candidate.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
{% if application.interview_status %}
|
||||
<button type="button" class="btn btn-{% if application.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'interview' 'passed' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
{{ candidate.interview_status }}
|
||||
{{ application.interview_status }}
|
||||
</button>
|
||||
{% else %}
|
||||
--
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
<td class="text-center" id="status-result-{{ candidate.pk}}">
|
||||
{% if not candidate.offer_status %}
|
||||
<td class="text-center" id="status-result-{{ application.pk}}">
|
||||
{% if not application.offer_status %}
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'offer' 'Accepted' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'offer' 'Accepted' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if candidate.offer_status %}
|
||||
<button type="button" class="btn btn-{% if candidate.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm"
|
||||
{% if application.offer_status %}
|
||||
<button type="button" class="btn btn-{% if application.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_application_status' job.slug candidate.slug 'offer' 'Rejected' %}"
|
||||
hx-get="{% url 'update_application_status' job.slug application.slug 'offer' 'Rejected' %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="Pass Exam">
|
||||
{{ candidate.offer_status }}
|
||||
{{ application.offer_status }}
|
||||
</button>
|
||||
{% else %}
|
||||
--
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
<!-- Stage Update Modal -->
|
||||
{% load i18n%}
|
||||
<div class="modal fade" id="stageUpdateModal" tabindex="-1" aria-labelledby="stageUpdateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-top">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="stageUpdateModalLabel">
|
||||
<i class="fas fa-exchange-alt me-2"></i>Update Application Stage
|
||||
<i class="fas fa-exchange-alt me-2"></i>{% trans "Update Application Stage" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -16,14 +17,14 @@
|
||||
<div class="alert alert-info mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Current Stage:</strong>
|
||||
<span class="badge bg-primary ms-2" id="currentStageBadge">{{candidate.stage}}</span>
|
||||
<strong>{% trans "Current Stage:" %}</strong>
|
||||
<span class="badge bg-primary ms-2" id="currentStageBadge">{{application.stage}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Container -->
|
||||
<div id="stageUpdateFormContainer">
|
||||
<form id="stageUpdateForm" action="{% url 'application_update_stage' candidate.slug %}" method="post">
|
||||
<form id="stageUpdateForm" action="{% url 'application_update_stage' application.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<!-- Stage Selection -->
|
||||
<div class="mb-3">
|
||||
@ -73,26 +74,26 @@ function updateStageInfo(selectedValue, currentStage) {
|
||||
|
||||
if (selectedValue && selectedValue !== currentStage) {
|
||||
const stageLabels = {
|
||||
'Applied': 'Applied',
|
||||
'Exam': 'Exam',
|
||||
'Interview': 'Interview',
|
||||
'Offer': 'Offer'
|
||||
'Applied': '{% trans "Applied" %}',
|
||||
'Exam': '{% trans "Exam" %}',
|
||||
'Interview': '{% trans "Interview" %}',
|
||||
'Offer': '{% trans "Offer" %}'
|
||||
};
|
||||
|
||||
if (availableStagesText) {
|
||||
availableStagesText.textContent = `Moving from "${currentStage}" to "${stageLabels[selectedValue]}". This transition is allowed.`;
|
||||
availableStagesText.textContent = `{% trans "Moving from" %} "${currentStage}" {% trans "to" %} "${stageLabels[selectedValue]}". {% trans "This transition is allowed." %}`;
|
||||
availableStagesText.parentElement.parentElement.classList.remove('alert-warning', 'alert-light');
|
||||
availableStagesText.parentElement.parentElement.classList.add('alert-success');
|
||||
}
|
||||
} else if (selectedValue === currentStage) {
|
||||
if (availableStagesText) {
|
||||
availableStagesText.textContent = 'No stage change selected. The candidate is already in this stage.';
|
||||
availableStagesText.textContent = '{% trans "No stage change selected. The candidate is already in this stage." %}';
|
||||
availableStagesText.parentElement.parentElement.classList.remove('alert-success', 'alert-warning');
|
||||
availableStagesText.parentElement.parentElement.classList.add('alert-info');
|
||||
}
|
||||
} else {
|
||||
if (availableStagesText) {
|
||||
availableStagesText.textContent = 'Select a stage above to see transition information.';
|
||||
availableStagesText.textContent = '{% trans "Select a stage above to see transition information." %}';
|
||||
availableStagesText.parentElement.parentElement.classList.remove('alert-success', 'alert-warning');
|
||||
availableStagesText.parentElement.parentElement.classList.add('alert-light');
|
||||
}
|
||||
|
||||
@ -25,9 +25,9 @@
|
||||
{# SCOPED - 3. Total Candidates #}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users stat-icon"></i> {% trans "Total Candidates" %}</h3>
|
||||
<h3><i class="fas fa-users stat-icon"></i> {% trans "Total Applications" %}</h3>
|
||||
</div>
|
||||
<div class="stat-value">{{ total_candidates }}</div>
|
||||
<div class="stat-value">{{ total_applications }}</div>
|
||||
<div class="stat-caption">{% trans "Total applications" %}</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -10,16 +10,16 @@
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="fas fa-calendar-plus me-2"></i>
|
||||
{% if has_future_meeting %}
|
||||
{% trans "Update Interview" %} for {{ candidate.name }}
|
||||
{% trans "Update Interview" %} for {{ application.name }}
|
||||
{% else %}
|
||||
{% trans "Schedule Interview" %} for {{ candidate.name }}
|
||||
{% trans "Schedule Interview" %} for {{ application.name }}
|
||||
{% endif %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">{% trans "Job" %}: {{ job.title }}</p>
|
||||
{% if has_future_meeting %}
|
||||
<div class="alert alert-info mt-2 mb-0" role="alert">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans "This candidate has upcoming interviews. You are updating an existing schedule." %}
|
||||
{% trans "This application has upcoming interviews. You are updating an existing schedule." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Assign Staff to {{ job.title }}{% endblock %}
|
||||
{% block title %}{% trans "Assign Staff to" %} {{ job.title }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
@ -142,34 +142,34 @@
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="page-header h3 mb-1">Assign Staff to Job</h1>
|
||||
<p class="text-secondary mb-0">Job: {{ job.title }} ({{ job.internal_job_id }})</p>
|
||||
<h1 class="page-header h3 mb-1">{% trans "Assign Staff to Job" %}</h1>
|
||||
<p class="text-secondary mb-0">{% trans "Job:" %} {{ job.title }} ({{ job.internal_job_id }})</p>
|
||||
</div>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Back to Job Details
|
||||
<i class="fas fa-arrow-left me-2"></i>{% trans "Back to Job Details" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Job Information Card -->
|
||||
<div class="kaauh-card mb-4">
|
||||
<div class="job-header-card">
|
||||
<h5 class="mb-0"><i class="fas fa-briefcase me-2"></i>Job Information</h5>
|
||||
<h5 class="mb-0"><i class="fas fa-briefcase me-2"></i>{% trans "Job Information" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mx-2 my-2">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Department:</strong> {{ job.get_department_display|default:job.department }}</p>
|
||||
<p><strong>Job Type:</strong> {{ job.get_job_type_display }}</p>
|
||||
<p><strong>Workplace Type:</strong> {{ job.get_workplace_type_display }}</p>
|
||||
<p><strong>{% trans "Department:" %}</strong> {{ job.get_department_display|default:job.department }}</p>
|
||||
<p><strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}</p>
|
||||
<p><strong>{% trans "Workplace Type:" %}</strong> {{ job.get_workplace_type_display }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Status:</strong>
|
||||
<p><strong>{% trans "Status:" %}</strong>
|
||||
<span class="badge {% if job.status == 'ACTIVE' %}bg-success{% elif job.status == 'CLOSED' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||
{{ job.get_status_display }}
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>Applications:</strong> {{ applications.count }}</p>
|
||||
<p><strong>Created:</strong> {{ job.created_at|date:"M d, Y" }}</p>
|
||||
<p><strong>{% trans "Applications:" %}</strong> {{ applications.count }}</p>
|
||||
<p><strong>{% trans "Created:" %}</strong> {{ job.created_at|date:"M d, Y" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -178,7 +178,7 @@
|
||||
<!-- Staff Assignment Form -->
|
||||
<div class="kaauh-card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-user-tie me-2"></i>Staff Assignment</h5>
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-user-tie me-2"></i>{% trans "Staff Assignment" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
@ -248,10 +248,10 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus me-2"></i>Assign Staff to Job
|
||||
<i class="fas fa-user-plus me-2"></i>{% trans "Assign Staff to Job" %}
|
||||
</button>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary ms-2">
|
||||
<i class="fas fa-times me-2"></i>Cancel
|
||||
<i class="fas fa-times me-2"></i>{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -263,18 +263,18 @@
|
||||
{% if job.staff_assignments.exists %}
|
||||
<div class="kaauh-card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-users me-2"></i>Current Staff Assignments</h5>
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-users me-2"></i>{% trans "Current Staff Assignments" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Staff Member</th>
|
||||
<th>Email</th>
|
||||
<th>Assignment Date</th>
|
||||
<th>Notes</th>
|
||||
<th>Status</th>
|
||||
<th>{% trans "Staff Member" %}</th>
|
||||
<th>{% trans "Email" %}</th>
|
||||
<th>{% trans "Assignment Date" %}</th>
|
||||
<th>{% trans "Notes" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -283,16 +283,16 @@
|
||||
<td>
|
||||
<strong>{{ assignment.staff.get_full_name|default:assignment.staff.username }}</strong>
|
||||
{% if assignment.staff.is_active %}
|
||||
<span class="badge bg-success ms-2">Active</span>
|
||||
<span class="badge bg-success ms-2">{% trans "Active" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger ms-2">Inactive</span>
|
||||
<span class="badge bg-danger ms-2">{% trans "Inactive" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ assignment.staff.email }}</td>
|
||||
<td>{{ assignment.assignment_date|date:"M d, Y" }}</td>
|
||||
<td>{{ assignment.notes|default:"-" }}</td>
|
||||
<td>
|
||||
<span class="badge bg-primary">Assigned</span>
|
||||
<span class="badge bg-primary">{% trans "Assigned" %}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -306,7 +306,7 @@
|
||||
<!-- Available Staff Members -->
|
||||
<div class="kaauh-card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-users me-2"></i>Available Staff Members</h5>
|
||||
<h5 class="mb-0 text-primary"><i class="fas fa-users me-2"></i>{% trans "Available Staff Members" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
@ -329,11 +329,11 @@
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{% if staff_user.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
<span class="badge bg-success">{% trans "Active" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
<span class="badge bg-danger">{% trans "Inactive" %}</span>
|
||||
{% endif %}
|
||||
<span class="badge bg-primary ms-1">Staff</span>
|
||||
<span class="badge bg-primary ms-1">{% trans "Staff" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -342,7 +342,7 @@
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
No staff members available. Please create staff accounts first.
|
||||
{% trans "No staff members available. Please create staff accounts first." %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@ -359,18 +359,18 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="staffAssignmentModalLabel">
|
||||
<i class="fas fa-user-plus me-2"></i>Confirm Staff Assignment
|
||||
<i class="fas fa-user-plus me-2"></i>{% trans "Confirm Staff Assignment" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to assign this staff member to the job <strong>{{ job.title }}</strong>?</p>
|
||||
<p>{% trans "Are you sure you want to assign this staff member to the job" %} <strong>{{ job.title }}</strong>?</p>
|
||||
<div id="selectedStaffInfo"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="button" class="btn btn-main-action" id="confirmAssignmentBtn">
|
||||
<i class="fas fa-check me-2"></i>Confirm Assignment
|
||||
<i class="fas fa-check me-2"></i>{% trans "Confirm Assignment" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -398,16 +398,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Show selected staff info in modal
|
||||
selectedStaffInfo.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<strong>Selected Staff Member:</strong><br>
|
||||
Name: ${selectedOption.text}<br>
|
||||
This assignment will take effect immediately.
|
||||
<strong>{% trans "Selected Staff Member:" %}</strong><br>
|
||||
{% trans "Name:" %} ${selectedOption.text}<br>
|
||||
{% trans "This assignment will take effect immediately." %}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show modal
|
||||
modal.show();
|
||||
} else {
|
||||
alert('Please select a staff member.');
|
||||
alert('{% trans "Please select a staff member." %}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user