diff --git a/db.sqlite3 b/db.sqlite3 index 68bc01e..5a07bdf 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index 6768e08..1d29710 100644 Binary files a/recruitment/__pycache__/forms.cpython-313.pyc and b/recruitment/__pycache__/forms.cpython-313.pyc differ diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index 3c54891..8a2f7c5 100644 Binary files a/recruitment/__pycache__/models.cpython-313.pyc and b/recruitment/__pycache__/models.cpython-313.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index 08329cb..2f2412a 100644 Binary files a/recruitment/__pycache__/urls.cpython-313.pyc and b/recruitment/__pycache__/urls.cpython-313.pyc differ diff --git a/recruitment/__pycache__/views.cpython-313.pyc b/recruitment/__pycache__/views.cpython-313.pyc index 2f50f81..254399c 100644 Binary files a/recruitment/__pycache__/views.cpython-313.pyc and b/recruitment/__pycache__/views.cpython-313.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index d4d3532..78e4cb3 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -234,12 +234,12 @@ class JobPostingForm(forms.ModelForm): 'value': 'United States' }), - + 'salary_range': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': '$60,000 - $80,000' }), - + # Application Information # 'application_url': forms.URLInput(attrs={ @@ -251,7 +251,7 @@ class JobPostingForm(forms.ModelForm): 'class': 'form-control', 'type': 'date' }), - + 'open_positions': forms.NumberInput(attrs={ 'class': 'form-control', 'min': 1, @@ -479,4 +479,12 @@ class JobPostingStatusForm(forms.ModelForm): class FormTemplateIsActiveForm(forms.ModelForm): class Meta: model = FormTemplate - fields = ['is_active'] \ No newline at end of file + fields = ['is_active'] + +class CandidateExamDateForm(forms.ModelForm): + class Meta: + model = Candidate + fields = ['exam_date'] + widgets = { + 'exam_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}), + } \ No newline at end of file diff --git a/recruitment/migrations/0002_candidate_is_potential_candidate_and_more.py b/recruitment/migrations/0002_candidate_is_potential_candidate_and_more.py new file mode 100644 index 0000000..66870d2 --- /dev/null +++ b/recruitment/migrations/0002_candidate_is_potential_candidate_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.6 on 2025-10-12 12:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='candidate', + name='is_potential_candidate', + field=models.BooleanField(default=False, verbose_name='Potential Candidate'), + ), + migrations.AlterField( + model_name='jobposting', + name='status', + field=models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20), + ), + ] diff --git a/recruitment/migrations/0003_alter_candidate_exam_date.py b/recruitment/migrations/0003_alter_candidate_exam_date.py new file mode 100644 index 0000000..8cada1f --- /dev/null +++ b/recruitment/migrations/0003_alter_candidate_exam_date.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.6 on 2025-10-12 15:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0002_candidate_is_potential_candidate_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='candidate', + name='exam_date', + field=models.DateTimeField(blank=True, null=True, verbose_name='Exam Date'), + ), + ] diff --git a/recruitment/migrations/0004_alter_candidate_interview_date.py b/recruitment/migrations/0004_alter_candidate_interview_date.py new file mode 100644 index 0000000..ec8feb1 --- /dev/null +++ b/recruitment/migrations/0004_alter_candidate_interview_date.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.6 on 2025-10-12 15:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0003_alter_candidate_exam_date'), + ] + + operations = [ + migrations.AlterField( + model_name='candidate', + name='interview_date', + field=models.DateTimeField(blank=True, null=True, verbose_name='Interview Date'), + ), + ] diff --git a/recruitment/models.py b/recruitment/models.py index a038f86..404c8e2 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -294,6 +294,9 @@ class Candidate(Base): is_resume_parsed = models.BooleanField( default=False, verbose_name=_("Resume Parsed") ) + is_potential_candidate = models.BooleanField( + default=False, verbose_name=_("Potential Candidate") + ) parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary")) applied = models.BooleanField(default=False, verbose_name=_("Applied")) stage = models.CharField( @@ -310,7 +313,7 @@ class Candidate(Base): blank=True, verbose_name=_("Applicant Status"), ) - exam_date = models.DateField(null=True, blank=True, verbose_name=_("Exam Date")) + exam_date = models.DateTimeField(null=True, blank=True, verbose_name=_("Exam Date")) exam_status = models.CharField( choices=ExamStatus.choices, max_length=100, @@ -318,7 +321,7 @@ class Candidate(Base): blank=True, verbose_name=_("Exam Status"), ) - interview_date = models.DateField( + interview_date = models.DateTimeField( null=True, blank=True, verbose_name=_("Interview Date") ) interview_status = models.CharField( diff --git a/recruitment/urls.py b/recruitment/urls.py index 023a178..024da8f 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -64,8 +64,15 @@ urlpatterns = [ path('forms/builder//', views.form_builder, name='form_builder'), path('forms/', views.form_templates_list, name='form_templates_list'), path('forms/create-template/', views.create_form_template, name='create_form_template'), + path('jobs//candidate-tiers/', views.candidate_tier_management_view, name='candidate_tier_management'), + path('jobs//candidate_exam_view/', views.candidate_exam_view, name='candidate_exam_view'), + path('jobs//update_candidate_exam_status/', views.update_candidate_exam_status, name='update_candidate_exam_status'), + path('jobs//bulk_update_candidate_exam_status/', views.bulk_update_candidate_exam_status, name='bulk_update_candidate_exam_status'), + path('htmx//candidate_criteria_view/', views.candidate_criteria_view_htmx, name='candidate_criteria_view_htmx'), + path('htmx//candidate_set_exam_date/', views.candidate_set_exam_date, name='candidate_set_exam_date'), + path('htmx/bulk_candidate_move_to_exam/', views.bulk_candidate_move_to_exam, name='bulk_candidate_move_to_exam'), # path('forms/form//submit/', views.submit_form, name='submit_form'), # path('forms/form//', views.form_wizard_view, name='form_wizard'), diff --git a/recruitment/views.py b/recruitment/views.py index a4da922..c740262 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -12,6 +12,7 @@ from django.urls import reverse from django.conf import settings from django.utils import timezone from .forms import ( + CandidateExamDateForm, ZoomMeetingForm, JobPostingForm, FormTemplateForm, @@ -216,7 +217,7 @@ def create_job(request): job.created_by = request.POST.get("created_by", "").strip() if not job.created_by: job.created_by = "University Administrator" - + job.save() job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug}) job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative) @@ -273,7 +274,7 @@ def edit_job(request, slug): def job_detail(request, slug): """View details of a specific job""" job = get_object_or_404(JobPosting, slug=slug) - + # Get all candidates for this job, ordered by most recent candidates = job.candidates.all().order_by("-created_at") @@ -289,22 +290,22 @@ def job_detail(request, slug): # 2. Check for POST request (Status Update Submission) if request.method == 'POST': - + status_form = JobPostingStatusForm(request.POST, instance=job) - + if status_form.is_valid(): status_form.save() - + # Add a success message messages.success(request, f"Status for '{job.title}' updated to '{job.get_status_display()}' successfully!") - - + + return redirect('job_detail', slug=slug) else: - + messages.error(request, "Failed to update status due to validation errors.") - - + + context = { "job": job, @@ -325,15 +326,15 @@ def job_image_upload(request, slug): image_upload_form=JobPostingImageForm(request.POST,request.FILES) if image_upload_form.is_valid(): image_upload_form = image_upload_form.save(commit=False) - + image_upload_form.job = job image_upload_form.save() messages.success(request, f"Image uploaded successfully for {job.title}.") return redirect('job_detail', slug=job.slug) else: - + messages.error(request, "Image upload failed: Please ensure a valid image file was selected.") - + return redirect('job_detail', slug=job.slug) return redirect('job_detail', slug=job.slug) @@ -1226,102 +1227,102 @@ def candidate_tier_management_view(request, slug): job = get_object_or_404(JobPosting, slug=slug) # Get all candidates for this job, ordered by match score (descending) - candidates = job.candidates.all().order_by("-match_score") + candidates = job.candidates.filter(stage="Applied").order_by("-match_score") # Get tier categorization parameters - tier1_count = int(request.GET.get("tier1_count", 100)) + # tier1_count = int(request.GET.get("tier1_count", 100)) - # Categorize candidates into tiers - tier1_candidates = candidates[:tier1_count] if tier1_count > 0 else [] - remaining_candidates = candidates[tier1_count:] if tier1_count > 0 else [] + # # Categorize candidates into tiers + # tier1_candidates = candidates[:tier1_count] if tier1_count > 0 else [] + # remaining_candidates = candidates[tier1_count:] if tier1_count > 0 else [] - if len(remaining_candidates) > 0: - # Tier 2: Next 50% of remaining candidates - tier2_count = max(1, len(remaining_candidates) // 2) - tier2_candidates = remaining_candidates[:tier2_count] - tier3_candidates = remaining_candidates[tier2_count:] - else: - tier2_candidates = [] - tier3_candidates = [] + # if len(remaining_candidates) > 0: + # # Tier 2: Next 50% of remaining candidates + # tier2_count = max(1, len(remaining_candidates) // 2) + # tier2_candidates = remaining_candidates[:tier2_count] + # tier3_candidates = remaining_candidates[tier2_count:] + # else: + # tier2_candidates = [] + # tier3_candidates = [] - # Handle form submissions - if request.method == "POST": - # Update tier categorization - if "update_tiers" in request.POST: - tier1_count = int(request.POST.get("tier1_count", 100)) - messages.success(request, f"Tier categorization updated. Tier 1: {tier1_count} candidates") - return redirect("candidate_tier_management", slug=slug) + # # Handle form submissions + # if request.method == "POST": + # # Update tier categorization + # if "update_tiers" in request.POST: + # tier1_count = int(request.POST.get("tier1_count", 100)) + # messages.success(request, f"Tier categorization updated. Tier 1: {tier1_count} candidates") + # return redirect("candidate_tier_management", slug=slug) - # Update individual candidate stages - elif "update_stage" in request.POST: - candidate_id = request.POST.get("candidate_id") - new_stage = request.POST.get("new_stage") - candidate = get_object_or_404(Candidate, id=candidate_id, job=job) + # # Update individual candidate stages + # elif "update_stage" in request.POST: + # candidate_id = request.POST.get("candidate_id") + # new_stage = request.POST.get("new_stage") + # candidate = get_object_or_404(Candidate, id=candidate_id, job=job) - if candidate.can_transition_to(new_stage): - old_stage = candidate.stage - candidate.stage = new_stage - candidate.save() - messages.success(request, f"Updated {candidate.name} from {old_stage} to {new_stage}") - else: - messages.error(request, f"Cannot transition {candidate.name} from {candidate.stage} to {new_stage}") + # if candidate.can_transition_to(new_stage): + # old_stage = candidate.stage + # candidate.stage = new_stage + # candidate.save() + # messages.success(request, f"Updated {candidate.name} from {old_stage} to {new_stage}") + # else: + # messages.error(request, f"Cannot transition {candidate.name} from {candidate.stage} to {new_stage}") - # Update exam status - elif "update_exam_status" in request.POST: - candidate_id = request.POST.get("candidate_id") - exam_status = request.POST.get("exam_status") - exam_date = request.POST.get("exam_date") - candidate = get_object_or_404(Candidate, id=candidate_id, job=job) + # # Update exam status + # elif "update_exam_status" in request.POST: + # candidate_id = request.POST.get("candidate_id") + # exam_status = request.POST.get("exam_status") + # exam_date = request.POST.get("exam_date") + # candidate = get_object_or_404(Candidate, id=candidate_id, job=job) - if candidate.stage == "Exam": - candidate.exam_status = exam_status - if exam_date: - candidate.exam_date = exam_date - candidate.save() - messages.success(request, f"Updated exam status for {candidate.name}") - else: - messages.error(request, f"Can only update exam status for candidates in Exam stage") + # if candidate.stage == "Exam": + # candidate.exam_status = exam_status + # if exam_date: + # candidate.exam_date = exam_date + # candidate.save() + # messages.success(request, f"Updated exam status for {candidate.name}") + # else: + # messages.error(request, f"Can only update exam status for candidates in Exam stage") - # Bulk stage update - elif "bulk_update_stage" in request.POST: - selected_candidates = request.POST.getlist("selected_candidates") - new_stage = request.POST.get("bulk_new_stage") - updated_count = 0 + # # Bulk stage update + # elif "bulk_update_stage" in request.POST: + # selected_candidates = request.POST.getlist("selected_candidates") + # new_stage = request.POST.get("bulk_new_stage") + # updated_count = 0 - for candidate_id in selected_candidates: - candidate = get_object_or_404(Candidate, id=candidate_id, job=job) - if candidate.can_transition_to(new_stage): - candidate.stage = new_stage - candidate.save() - updated_count += 1 + # for candidate_id in selected_candidates: + # candidate = get_object_or_404(Candidate, id=candidate_id, job=job) + # if candidate.can_transition_to(new_stage): + # candidate.stage = new_stage + # candidate.save() + # updated_count += 1 - messages.success(request, f"Updated {updated_count} candidates to {new_stage} stage") + # messages.success(request, f"Updated {updated_count} candidates to {new_stage} stage") - # Mark individual candidate as Candidate - elif "mark_as_candidate" in request.POST: - candidate_id = request.POST.get("candidate_id") - candidate = get_object_or_404(Candidate, id=candidate_id, job=job) + # # Mark individual candidate as Candidate + # elif "mark_as_candidate" in request.POST: + # candidate_id = request.POST.get("candidate_id") + # candidate = get_object_or_404(Candidate, id=candidate_id, job=job) - if candidate.applicant_status == "Applicant": - candidate.applicant_status = "Candidate" - candidate.save() - messages.success(request, f"Marked {candidate.name} as Candidate") - else: - messages.info(request, f"{candidate.name} is already marked as Candidate") + # if candidate.applicant_status == "Applicant": + # candidate.applicant_status = "Candidate" + # candidate.save() + # messages.success(request, f"Marked {candidate.name} as Candidate") + # else: + # messages.info(request, f"{candidate.name} is already marked as Candidate") - # Mark all Tier 1 candidates as Candidates - elif "mark_as_candidates" in request.POST: - updated_count = 0 - for candidate in tier1_candidates: - if candidate.applicant_status == "Applicant": - candidate.applicant_status = "Candidate" - candidate.save() - updated_count += 1 + # # Mark all Tier 1 candidates as Candidates + # elif "mark_as_candidates" in request.POST: + # updated_count = 0 + # for candidate in tier1_candidates: + # if candidate.applicant_status == "Applicant": + # candidate.applicant_status = "Candidate" + # candidate.save() + # updated_count += 1 - if updated_count > 0: - messages.success(request, f"Marked {updated_count} Tier 1 candidates as Candidates") - else: - messages.info(request, "All Tier 1 candidates are already marked as Candidates") + # if updated_count > 0: + # messages.success(request, f"Marked {updated_count} Tier 1 candidates as Candidates") + # else: + # messages.info(request, "All Tier 1 candidates are already marked as Candidates") # Group candidates by current stage for display stage_groups = { @@ -1333,17 +1334,75 @@ def candidate_tier_management_view(request, slug): context = { "job": job, - "tier1_candidates": tier1_candidates, - "tier2_candidates": tier2_candidates, - "tier3_candidates": tier3_candidates, - "stage_groups": stage_groups, - "tier1_count": tier1_count, - "total_candidates": candidates.count(), + "candidates": candidates, + # "stage_groups": stage_groups, + # "tier1_count": tier1_count, + # "total_candidates": candidates.count(), } return render(request, "recruitment/candidate_tier_management.html", context) + +def candidate_exam_view(request, slug): + """ + Manage candidate tiers and stage transitions + """ + job = get_object_or_404(JobPosting, slug=slug) + candidates = job.candidates.filter(stage="Exam").order_by("-match_score") + + return render(request, "recruitment/candidate_exam_view.html", {"job": job, "candidates": candidates}) + +def update_candidate_exam_status(request, slug): + candidate = get_object_or_404(Candidate, slug=slug) + if request.method == "POST": + form = CandidateExamDateForm(request.POST, instance=candidate) + if form.is_valid(): + form.save() + return redirect("candidate_exam_view", slug=candidate.job.slug) + else: + form = CandidateExamDateForm(request.POST, instance=candidate) + return render(request, "includes/candidate_exam_status_form.html", {"candidate": candidate,"form": form}) +def bulk_update_candidate_exam_status(request,slug): + job = get_object_or_404(JobPosting, slug=slug) + print(request.headers) + status = request.headers.get('status') + print(status) + if status: + for c in request.POST.items(): + try: + candidate = Candidate.objects.get(pk=c[0]) + candidate.exam_status = "Passed" if status == "pass" else "Failed" + candidate.stage = "Interview" + candidate.save() + except Exception as e: + print(e) + messages.success(request, f"Updated exam status selected candidates") + return redirect("candidate_exam_view", slug=job.slug) + def candidate_criteria_view_htmx(request, pk): candidate = get_object_or_404(Candidate, pk=pk) - print(candidate) - return render(request, "includes/candidate_modal_body.html", {"candidate": candidate}) \ No newline at end of file + return render(request, "includes/candidate_modal_body.html", {"candidate": candidate}) + + +def candidate_set_exam_date(request, slug): + candidate = get_object_or_404(Candidate, slug=slug) + candidate.exam_date = timezone.now() + candidate.save() + messages.success(request, f"Set exam date for {candidate.name} to {candidate.exam_date}") + return redirect("candidate_tier_management", slug=candidate.job.slug) +def bulk_candidate_move_to_exam(request): + for c in request.POST.items(): + try: + candidate = Candidate.objects.get(pk=c[0]) + candidate.stage = "Exam" + candidate.applicant_status = "Candidate" + candidate.exam_date = timezone.now() + candidate.save() + except Exception as e: + print(e) + messages.success(request, f"Moved {candidate.name} to Exam stage") + return redirect("candidate_tier_management", slug=candidate.job.slug) + # def response(): + # yield SSE.patch_elements("","") + # yield SSE.execute_script("console.log('hello world');") + # return DatastarResponse(response()) \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 010b2ab..7053ff1 100644 --- a/templates/base.html +++ b/templates/base.html @@ -21,7 +21,7 @@ --kaauh-light-bg: #f9fbfd; --kaauh-border: #eaeff3; } - + /* === Top Bar === */ .top-bar { @@ -232,7 +232,7 @@ {% block customCSS %}{% endblock %} - +
@@ -327,7 +327,7 @@ {% trans "Form Templates" %} - + - + +
  • @@ -543,7 +543,8 @@ }); }); - + + {% block customJS %}{% endblock %} diff --git a/templates/includes/candidate_exam_status_form.html b/templates/includes/candidate_exam_status_form.html new file mode 100644 index 0000000..0a4fdf1 --- /dev/null +++ b/templates/includes/candidate_exam_status_form.html @@ -0,0 +1,7 @@ +{% load i18n %} +{% url 'update_candidate_exam_status' slug=candidate.slug as url %} + + {% csrf_token %} + {{ form.as_p }} + + \ No newline at end of file diff --git a/templates/includes/candidate_modal_body.html b/templates/includes/candidate_modal_body.html index 6b32d24..139555b 100644 --- a/templates/includes/candidate_modal_body.html +++ b/templates/includes/candidate_modal_body.html @@ -14,7 +14,11 @@ {% for key, value in candidate.criteria_checklist.items %}
  • {{ key }} - {{ value|yesno:"Yes,No" }} + {% if value == 'Met' %} + Yes + {% else %} + Not Mentioned + {% endif %}
  • {% endfor %} diff --git a/templates/recruitment/candidate_exam_view.html b/templates/recruitment/candidate_exam_view.html new file mode 100644 index 0000000..048b73b --- /dev/null +++ b/templates/recruitment/candidate_exam_view.html @@ -0,0 +1,363 @@ +{% extends 'base.html' %} +{% load static i18n %} + +{% block title %}Candidate Tier Management - {{ job.title }} - ATS{% endblock %} + +{% block customCSS %} + +{% endblock %} + +{% block content %} +
    +
    +
    +

    + + {% trans "Exam" %} - {{ job.title }} +

    +

    + Total Candidates: {{ total_candidates }} +

    +
    + + {% trans "Back to Job" %} + +
    + + +
    +
    + {% csrf_token %} +
    +
    + + +
    +
    + +
    +
    +
    +
    + + +

    {% trans "Candidate Tiers" %}

    +
    + {% url "bulk_update_candidate_exam_status" job.slug as bulk_update_candidate_exam_status_url %} + {% if candidates %} + + + {% endif %} +
    + + + + + + + + + + + + + + {% for candidate in candidates %} + + + + + + + + + + {% endfor %} + +
    + {% if candidates %} +
    + +
    + {% endif %} +
    {% trans "Name" %}{% trans "Contact" %}{% trans "AI Score" %}{% trans "Exam Status" %}{% trans "Exam Date" %}{% trans "Actions" %}
    +
    + +
    +
    +
    {{ candidate.name }}
    +
    +
    + Email: {{ candidate.email }}
    + Phone: {{ candidate.phone }}
    +
    +
    + {{ candidate.match_score|default:"0" }} + + {% if candidate.exam_status == "Passed" %} + {{ candidate.exam_status }} + {% elif candidate.exam_status == "Failed" %} + {{ candidate.exam_status }} + {% endif %} + {{candidate.exam_date|date:"M d, Y h:i A"}} + + + {% if candidate.stage != "Exam" %} + + {% endif %} +
    +
    +
    + + + +{% endblock %} \ No newline at end of file diff --git a/templates/recruitment/candidate_tier_management.html b/templates/recruitment/candidate_tier_management.html index 42d10f5..9ec3196 100644 --- a/templates/recruitment/candidate_tier_management.html +++ b/templates/recruitment/candidate_tier_management.html @@ -232,466 +232,106 @@
    - -
    -

    {% trans "Bulk Stage Update" %}

    -
    - {% csrf_token %} -
    -
    - - -
    -
    - -
    -
    -
    -
    - - - {% comment %}
    - {% for stage_name, stage_candidates in stage_groups.items %} -
    -
    - {{ stage_name }} - {{ stage_candidates.count }} -
    -
    - {% for candidate in stage_candidates %} -
    -
    - - -
    -
    - {% empty %} -

    {% trans "No candidates in this stage" %}

    - {% endfor %} -
    -
    - {% endfor %} -
    {% endcomment %} - -

    {% trans "Candidate Tiers" %}

    - - - - - -
    - -
    - {% if tier1_candidates %} -
    - -
    -
    - - - - - - - - - - - - - {% for candidate in tier1_candidates %} - - - - - - - - - {% endfor %} - -
    {% trans "Name" %}{% trans "Contact" %}{% trans "AI Score" %}{% trans "Status" %}{% trans "Stage" %}{% trans "Actions" %}
    -
    {{ candidate.name }}
    -
    -
    - Email: {{ candidate.email }}
    - Phone: {{ candidate.phone }}
    -
    -
    - {{ candidate.match_score|default:"0" }} - - - {{ candidate.get_applicant_status_display }} - - - - {{ candidate.get_stage_display }} - - {% if candidate.stage == "Exam" and candidate.exam_status %} -
    - {{ candidate.get_exam_status_display }} - {% endif %} -
    - -
    -
    - {% else %} -

    {% trans "No candidates in Tier 1" %}

    +

    {% trans "Candidate Tiers" %}

    +
    + {% url "bulk_candidate_move_to_exam" as move_to_exam_url %} + {% if candidates %} + {% endif %} -
    +
    + + + + + + + + + + + + + + {% for candidate in candidates %} + + + + {% endfor %} + +
    + {% if candidates %} +
    + +
    + {% endif %} +
    {% trans "Name" %}{% trans "Contact" %}{% trans "AI Score" %}{% trans "Status" %}{% trans "Stage" %}{% trans "Actions" %}
    +
    + -
    - {% if tier2_candidates %} -
    - - - - - - - - - - - - - {% for candidate in tier2_candidates %} - - - - - - - + + + + + + - - {% endfor %} - -
    {% trans "Name" %}{% trans "Contact" %}{% trans "AI Score" %}{% trans "Status" %}{% trans "Stage" %}{% trans "Actions" %}
    -
    {{ candidate.name }}
    -
    -
    - Email: {{ candidate.email }}
    - Phone: {{ candidate.phone }}
    -
    -
    - {{ candidate.match_score|default:"0" }} - - - {{ candidate.get_applicant_status_display }} - - - - {{ candidate.get_stage_display }} - - {% if candidate.stage == "Exam" and candidate.exam_status %} -
    - {{ candidate.get_exam_status_display }} - {% endif %} -
    - -
    - {% if candidate.applicant_status == 'Applicant' %} - + type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}"> +
    +
    +
    {{ candidate.name }}
    +
    +
    + Email: {{ candidate.email }}
    + Phone: {{ candidate.phone }}
    +
    +
    + {{ candidate.match_score|default:"0" }} + + + {{ candidate.get_applicant_status_display }} + + + + {{ candidate.get_stage_display }} + + {% if candidate.stage == "Exam" and candidate.exam_status %} +
    + {{ candidate.get_exam_status_display }} {% endif %} - {% for next_stage in candidate.get_available_stages %} - - {% endfor %} - {% if candidate.stage == "Exam" %} - - {% endif %} - - -
    + + {% if candidate.stage != "Exam" %} + {% endif %} - - -
    -
    - {% else %} -

    {% trans "No candidates in Tier 2" %}

    - {% endif %} -
    - - -
    - {% if tier3_candidates %} -
    - - - - - - - - - - - - - {% for candidate in tier3_candidates %} - - - - - - - - - {% endfor %} - -
    {% trans "Name" %}{% trans "Contact" %}{% trans "AI Score" %}{% trans "Status" %}{% trans "Stage" %}{% trans "Actions" %}
    -
    {{ candidate.name }}
    -
    -
    - Email: {{ candidate.email }}
    - Phone: {{ candidate.phone }}
    -
    -
    - {{ candidate.match_score|default:"0" }} - - - {{ candidate.get_applicant_status_display }} - - - - {{ candidate.get_stage_display }} - - {% if candidate.stage == "Exam" and candidate.exam_status %} -
    - {{ candidate.get_exam_status_display }} - {% endif %} -
    - -
    - {% if candidate.applicant_status == 'Applicant' %} - - {% endif %} - {% for next_stage in candidate.get_available_stages %} - - {% endfor %} - {% if candidate.stage == "Exam" %} - - {% endif %} -
    - - -
    -
    - {% else %} -

    {% trans "No candidates in Tier 3" %}

    - {% endif %} -
    -
    - - - -{% for candidate in tier1_candidates|add:tier2_candidates|add:tier3_candidates %} -{% if candidate.stage == "Exam" %} -
    -
    -
    -
    -{% endif %} -{% endfor %} + -{% endblock %} - -{% block customJS %} - - -{% endblock customJS %} +{% endblock %} \ No newline at end of file