Compare commits
3 Commits
4caf57a3e0
...
c532ca97b6
| Author | SHA1 | Date | |
|---|---|---|---|
| c532ca97b6 | |||
| 97d9e034cd | |||
| c13450ae83 |
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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={
|
||||
@ -255,7 +255,7 @@ class JobPostingForm(forms.ModelForm):
|
||||
'class': 'form-control',
|
||||
'type': 'date'
|
||||
}),
|
||||
|
||||
|
||||
'open_positions': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': 1,
|
||||
@ -483,4 +483,12 @@ class JobPostingStatusForm(forms.ModelForm):
|
||||
class FormTemplateIsActiveForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FormTemplate
|
||||
fields = ['is_active']
|
||||
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'}),
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
18
recruitment/migrations/0003_alter_candidate_exam_date.py
Normal file
18
recruitment/migrations/0003_alter_candidate_exam_date.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -229,7 +229,7 @@ class JobPosting(Base):
|
||||
parts.append(self.location_city)
|
||||
if self.location_state:
|
||||
parts.append(self.location_state)
|
||||
if self.location_country and self.location_country != "United States":
|
||||
if self.location_country:
|
||||
parts.append(self.location_country)
|
||||
return ", ".join(parts) if parts else "Not specified"
|
||||
|
||||
@ -295,6 +295,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(
|
||||
@ -311,7 +314,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,
|
||||
@ -319,7 +322,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(
|
||||
|
||||
@ -65,8 +65,15 @@ urlpatterns = [
|
||||
path('forms/builder/<int:template_id>/', 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/<slug:slug>/candidate-tiers/', views.candidate_tier_management_view, name='candidate_tier_management'),
|
||||
path('jobs/<slug:slug>/candidate_exam_view/', views.candidate_exam_view, name='candidate_exam_view'),
|
||||
path('jobs/<slug:slug>/update_candidate_exam_status/', views.update_candidate_exam_status, name='update_candidate_exam_status'),
|
||||
path('jobs/<slug:slug>/bulk_update_candidate_exam_status/', views.bulk_update_candidate_exam_status, name='bulk_update_candidate_exam_status'),
|
||||
|
||||
path('htmx/<int:pk>/candidate_criteria_view/', views.candidate_criteria_view_htmx, name='candidate_criteria_view_htmx'),
|
||||
path('htmx/<slug:slug>/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/<int:template_id>/submit/', views.submit_form, name='submit_form'),
|
||||
# path('forms/form/<int:template_id>/', views.form_wizard_view, name='form_wizard'),
|
||||
|
||||
@ -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
|
||||
applicants = job.candidates.all().order_by("-created_at")
|
||||
@ -291,22 +292,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,
|
||||
@ -327,15 +328,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)
|
||||
|
||||
@ -963,6 +964,8 @@ def submit_form(request, template_id):
|
||||
resume=resume.get_file if resume.is_file else None,
|
||||
job=submission.template.job,
|
||||
)
|
||||
return redirect('application_success')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Candidate creation failed,{e}")
|
||||
pass
|
||||
@ -1238,102 +1241,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 = {
|
||||
@ -1345,17 +1348,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})
|
||||
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())
|
||||
@ -245,7 +245,7 @@
|
||||
</style>
|
||||
{% block customCSS %}{% endblock %}
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<body class="d-flex flex-column min-vh-100" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
|
||||
<div class="top-bar d-none d-md-block">
|
||||
{# Changed container to container-fluid and added max-width-1600 to inner div #}
|
||||
@ -587,7 +587,8 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js"></script>
|
||||
|
||||
{% block customJS %}{% endblock %}
|
||||
|
||||
|
||||
@ -5,7 +5,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ATS Form Builder - Vanilla JS</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
/* Updated CSS styles with a new Teal/Aqua theme */
|
||||
:root {
|
||||
@ -771,8 +773,22 @@
|
||||
</div>
|
||||
<!-- Main Content Area -->
|
||||
<div class="main-content">
|
||||
<nav>
|
||||
<div class="bread" style="color: #6c757d; font-size: 0.9rem;">
|
||||
<span class="me-2">
|
||||
<a href="{% url 'dashboard' %}" style="color: #6c757d !important; text-decoration: none !important;">Home</a>
|
||||
/
|
||||
</span>
|
||||
<span class="me-2">
|
||||
<a href="{% url 'job_list' %}" style="color: #6c757d !important; text-decoration: none !important;">Jobs</a>
|
||||
/
|
||||
</span>
|
||||
<span style="color: #6c757d; font-weight: 600;">Form Builder</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="toolbar">
|
||||
<h1 id="formTitle">Resume Application Form</h1>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-outline" id="formSettingsBtn">
|
||||
<i class="fas fa-cog"></i> Settings
|
||||
@ -961,7 +977,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
// Application State
|
||||
const state = {
|
||||
draggedStageIndex: null,
|
||||
|
||||
7
templates/includes/candidate_exam_status_form.html
Normal file
7
templates/includes/candidate_exam_status_form.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% url 'update_candidate_exam_status' slug=candidate.slug as url %}
|
||||
<form data-on-submit="@post('{{url}}', {contentType: 'form', headers: {'X-CSRFToken': '{{ csrf_token }}'}})">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">{% trans "Update" %}</button>
|
||||
</form>
|
||||
@ -14,7 +14,11 @@
|
||||
{% for key, value in candidate.criteria_checklist.items %}
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<span>{{ key }}</span>
|
||||
<span class="badge bg-{{ value|yesno:"success,danger" }}">{{ value|yesno:"Yes,No" }}</span>
|
||||
{% if value == 'Met' %}
|
||||
<span class="badge bg-success">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Not Mentioned</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@ -393,7 +393,7 @@
|
||||
id="copyJobLinkButton"
|
||||
data-url="{{ job.application_url }}">
|
||||
<i class="fas fa-link me-1"></i>
|
||||
{% trans "Copy Public Link" %}
|
||||
{% trans "Copy and Share Public Link" %}
|
||||
</button>
|
||||
|
||||
<span id="copyFeedback" class="text-success ms-2 small" style="display:none;">
|
||||
|
||||
@ -13,6 +13,10 @@
|
||||
--kaauh-primary-text: #343a40;
|
||||
}
|
||||
|
||||
/* Primary Color Overrides */
|
||||
.text-primary-theme { color: var(--kaauh-teal) !important; }
|
||||
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
|
||||
|
||||
/* Enhanced Card Styling */
|
||||
.card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
@ -20,7 +24,7 @@
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.card:hover {
|
||||
.card:not(.no-hover):hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
@ -32,6 +36,9 @@
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-main-action:hover {
|
||||
@ -51,17 +58,6 @@
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* Job Card Specifics */
|
||||
.job-card .card-title {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 600;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
.job-card .card-text i {
|
||||
color: var(--kaauh-teal);
|
||||
width: 1.25rem; /* Align icons vertically */
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
font-size: 0.8rem;
|
||||
@ -71,41 +67,100 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.7px;
|
||||
}
|
||||
/* Mapped color classes to theme */
|
||||
.bg-draft { background-color: #6c757d !important; } /* secondary */
|
||||
.bg-active { background-color: var(--kaauh-teal) !important; } /* primary teal */
|
||||
.bg-closed { background-color: #dc3545 !important; } /* danger */
|
||||
.bg-archived { background-color: #343a40 !important; } /* dark */
|
||||
.bg-DRAFT { background-color: #6c757d !important; }
|
||||
.bg-ACTIVE { background-color: var(--kaauh-teal) !important; }
|
||||
.bg-CLOSED { background-color: #dc3545 !important; }
|
||||
.bg-ARCHIVED { background-color: #343a40 !important; }
|
||||
.bg-info { background-color: #17a2b8 !important; }
|
||||
|
||||
.bg-info { background-color: #17a2b8 !important; } /* LinkedIn badge */
|
||||
/* --- TABLE ALIGNMENT AND SIZING FIXES --- */
|
||||
.table {
|
||||
table-layout: fixed; /* Ensures column widths are respected */
|
||||
width: 100%;
|
||||
}
|
||||
.table thead th {
|
||||
color: var(--kaauh-primary-text);
|
||||
font-weight: 500; /* Lighter weight for smaller font */
|
||||
font-size: 0.85rem; /* Smaller font size for header text */
|
||||
vertical-align: middle;
|
||||
border-bottom: 2px solid var(--kaauh-border);
|
||||
padding: 0.5rem 0.25rem; /* Reduced vertical and horizontal padding */
|
||||
}
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #f3f7f9;
|
||||
}
|
||||
|
||||
/* Optimized Main Table Column Widths (Total must be 100%) */
|
||||
.table th:nth-child(1) { width: 22%; } /* Job ID (Tight) */
|
||||
|
||||
.table th:nth-child(2) { width: 12%; } /* Source (Tight) */
|
||||
.table th:nth-child(3) { width: 8%; } /* Actions (Tight, icon buttons) */
|
||||
.table th:nth-child(4) { width: 8%; } /* Form (Tight, icon buttons) */
|
||||
.table th:nth-child(5) { width: 50%; } /* Candidate Metrics (colspan=7) */
|
||||
|
||||
/* Pagination Link Styling */
|
||||
.pagination .page-item .page-link {
|
||||
/* NESTED TABLE STYLING FOR CANDIDATE MANAGEMENT HEADER */
|
||||
.nested-header-table {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed; /* CRITICAL for 1:1 data alignment */
|
||||
}
|
||||
.nested-header-table thead th {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0.3rem 0 0.1rem 0; /* Reduced padding here too */
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
font-size: 0.75rem; /* Even smaller font for nested headers */
|
||||
width: calc(100% / 7);
|
||||
}
|
||||
/* Explicit widths are technically defined by the 1/7 rule, but keeping them for clarity/safety */
|
||||
.nested-header-table thead th:nth-child(1),
|
||||
.nested-header-table thead th:nth-child(2),
|
||||
.nested-header-table thead th:nth-child(5) {
|
||||
width: calc(100% / 7);
|
||||
}
|
||||
.nested-header-table thead th:nth-child(3),
|
||||
.nested-header-table thead th:nth-child(4) {
|
||||
width: calc(100% / 7 * 2);
|
||||
}
|
||||
|
||||
/* Inner Nested Table (P/F) */
|
||||
.nested-stage-metrics {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.nested-stage-metrics thead th {
|
||||
padding: 0.1rem 0; /* Very minimal padding */
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-border);
|
||||
}
|
||||
.pagination .page-item.active .page-link {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
}
|
||||
.pagination .page-item:hover .page-link:not(.active) {
|
||||
background-color: #e9ecef;
|
||||
font-size: 0.7rem; /* Smallest font size */
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/* Filter & Search Layout Adjustments */
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem; /* Space between filter and clear buttons */
|
||||
/* Main TH for Candidate Management Header */
|
||||
.candidate-management-header {
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
border-left: 2px solid var(--kaauh-teal);
|
||||
border-right: 1px solid var(--kaauh-border) !important;
|
||||
}
|
||||
.form-control-search {
|
||||
/* Optional: Add slight shadow/focus effect to search bar */
|
||||
box-shadow: none;
|
||||
border-color: var(--kaauh-border);
|
||||
|
||||
/* Candidate Management Data Cells (7 columns total now) */
|
||||
.candidate-data-cell {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem; /* Keep data readable */
|
||||
border-left: 1px solid var(--kaauh-border);
|
||||
}
|
||||
.form-control-search:focus {
|
||||
border-color: var(--kaauh-teal);
|
||||
box-shadow: 0 0 0 0.1rem rgba(0, 99, 110, 0.25);
|
||||
.candidate-data-cell a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 0.4rem 0; /* Minimized vertical padding */
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -114,10 +169,10 @@
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-briefcase me-2"></i> Job Postings
|
||||
<i class="fas fa-briefcase me-2"></i> {% trans "Job Postings" %}
|
||||
</h1>
|
||||
<a href="{% url 'job_create' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus me-1"></i> Create New Job
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Create New Job" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -125,7 +180,7 @@
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label for="search" class="form-label small text-muted">Search by Title or Department</label>
|
||||
<label for="search" class="form-label small text-muted">{% trans "Search by Title or Department" %}</label>
|
||||
<div class="input-group input-group-lg mb-3">
|
||||
<form method="get" action="" class="w-100">
|
||||
{% include 'includes/search_form.html' %}
|
||||
@ -133,117 +188,123 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% url 'job_list' as job_list_url %}
|
||||
|
||||
<form method="GET" class="row g-3 align-items-end" >
|
||||
|
||||
<div class="col-md-3">
|
||||
<label for="status" class="form-label small text-muted">Filter by Status</label>
|
||||
<select name="status" id="status" class="form-select form-select-sm">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="DRAFT" {% if status_filter == 'DRAFT' %}selected{% endif %}>Draft</option>
|
||||
<option value="PUBLISHED" {% if status_filter == 'PUBLISHED' %}selected{% endif %}>Published</option>
|
||||
<option value="CLOSED" {% if status_filter == 'CLOSED' %}selected{% endif %}>Closed</option>
|
||||
<option value="ARCHIVED" {% if status_filter == 'ARCHIVED' %}selected{% endif %}>Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<div class="filter-buttons">
|
||||
<button type="submit" class="btn btn-main-action btn-lg">
|
||||
<i class="fas fa-filter me-1"></i> Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<form method="GET" class="row g-3 align-items-end" >
|
||||
<div class="col-md-3">
|
||||
<label for="status" class="form-label small text-muted">{% trans "Filter by Status" %}</label>
|
||||
<select name="status" id="status" class="form-select form-select-sm">
|
||||
<option value="">{% trans "All Statuses" %}</option>
|
||||
<option value="DRAFT" {% if status_filter == 'DRAFT' %}selected{% endif %}>{% trans "Draft" %}</option>
|
||||
<option value="ACTIVE" {% if status_filter == 'ACTIVE' %}selected{% endif %}>{% trans "Active" %}</option>
|
||||
<option value="CLOSED" {% if status_filter == 'CLOSED' %}selected{% endif %}>{% trans "Closed" %}</option>
|
||||
<option value="ARCHIVED" {% if status_filter == 'ARCHIVED' %}selected{% endif %}>{% trans "Archived" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="filter-buttons">
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-filter me-1"></i> {% trans "Apply Filters" %}
|
||||
</button>
|
||||
<a href="{% url 'job_list' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Clear" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if page_obj %}
|
||||
<div id="job-list">
|
||||
{# View Switcher #}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="job-list" %}
|
||||
{% comment %} --- START OF TABLE VIEW (Data relied upon context variable 'jobs') --- {% endcomment %}
|
||||
<div id="job-list">
|
||||
{% comment %} Placeholder for View Switcher {% endcomment %}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="job-list" %}
|
||||
|
||||
{# Card View (Default) #}
|
||||
<div class="card-view active row">
|
||||
{% for job in page_obj %}
|
||||
<div class="col-md-6 col-lg-4 mb-4 ">
|
||||
<div class="card job-card h-100">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="card-title flex-grow-1 me-3">{{ job.title }}</h5>
|
||||
<span class="badge bg-{{ job.status|lower|striptags|yesno:'active,draft,closed,archived' }} status-badge">
|
||||
{{ job.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="card-text text-muted small">
|
||||
<i class="fas fa-building"></i> {{ job.department|default:"No Department" }}<br>
|
||||
<i class="fas fa-map-marker-alt"></i> {{ job.get_location_display }}<br>
|
||||
<i class="fas fa-clock"></i> {{ job.get_job_type_display }}<br>
|
||||
<i class="fas fa-briefcase"></i> {{ job.get_source }}
|
||||
</p>
|
||||
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
{% if job.posted_to_linkedin %}
|
||||
<span class="badge bg-info mb-2">
|
||||
<i class="fab fa-linkedin me-1"></i> Posted to LinkedIn
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-sm btn-main-action">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Table View #}
|
||||
<div class="table-view">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<div class="table-view active">
|
||||
<div class="card shadow-sm">
|
||||
<div class="table-responsive ">
|
||||
<table class="table table-hover align-middle mb-0 table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Job Title" %}</th>
|
||||
<th scope="col">{% trans "Department" %}</th>
|
||||
<th scope="col">{% trans "Location" %}</th>
|
||||
<th scope="col">{% trans "Job Type" %}</th>
|
||||
<th scope="col">{% trans "Status" %}</th>
|
||||
<th scope="col">{% trans "Job ID" %}</th>
|
||||
{% comment %} <th scope="col">{% trans "Job Title" %}</th>
|
||||
<th scope="col">{% trans "Status" %}</th> {% endcomment %}
|
||||
<th scope="col">{% trans "Source" %}</th>
|
||||
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||
<th scope="col">{% trans "Actions" %}</th>
|
||||
<th scop="col" class="text-center">{% trans "Manage Forms" %}</th>
|
||||
|
||||
<th scope="col" colspan="7" class="candidate-management-header">
|
||||
{% trans "Applicants Metrics" %}
|
||||
<table class="nested-header-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 14.28%;">{% trans "Applied" %}</th>
|
||||
<th style="width: 14.28%;">{% trans "Screened" %}</th>
|
||||
|
||||
<th colspan="2">{% trans "Exam" %}
|
||||
<table class="nested-stage-metrics">
|
||||
<thead>
|
||||
<th>P</th>
|
||||
<th>F</th>
|
||||
</thead>
|
||||
</table>
|
||||
</th>
|
||||
|
||||
<th colspan="2">{% trans "Interview" %}
|
||||
<table class="nested-stage-metrics">
|
||||
<thead>
|
||||
<th>P</th>
|
||||
<th>F</th>
|
||||
</thead>
|
||||
</table>
|
||||
</th>
|
||||
<th style="width: 14.28%;">{% trans "Offer" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job in page_obj %}
|
||||
{% comment %} This loop relies on the 'jobs' variable passed from the Django view {% endcomment %}
|
||||
{% for job in jobs %}
|
||||
<tr>
|
||||
<td class="fw-medium">{{ job.title }}</td>
|
||||
<td>{{ job.department|default:"N/A" }}</td>
|
||||
<td>{{ job.get_location_display }}</td>
|
||||
<td>{{ job.get_job_type_display }}</td>
|
||||
<td><span class="badge bg-{{ job.status|lower|striptags|yesno:'active,draft,closed,archived' }} status-badge">{{ job.get_status_display }}</span></td>
|
||||
<td class="fw-medium text-primary-theme">{{ job }}</td>
|
||||
{% comment %} <td class="fw-medium text-primary-theme">{{ job.title }}</td>
|
||||
<td><span class="badge bg-{{ job.status }} status-badge">{{ job.status }}</span></td> {% endcomment %}
|
||||
<td>{{ job.get_source }}</td>
|
||||
<td class="text-end">
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-primary" title="View">
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-outline-secondary" title="Edit">
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'form_wizard' job.form_template.id %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_builder' job.form_template.id %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_template_submissions_list' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Submissions' %}">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{# CANDIDATE MANAGEMENT DATA - 7 SEPARATE COLUMNS CORRESPONDING TO THE HEADER #}
|
||||
<td class="candidate-data-cell text-primary-theme"><a href="#" class="text-primary-theme">{% if job.metrics.applied %}{{ job.metrics.applied }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-info"><a href="#" class="text-info">{% if job.metrics.screening %}{{ job.metrics.screening }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="#" class="text-success">{% if job.metrics.exam_p %}{{ job.metrics.exam_p }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-danger"><a href="#" class="text-danger">{% if job.metrics.exam_f %}{{ job.metrics.exam_f }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="#" class="text-success">{% if job.metrics.interview_p %}{{ job.metrics.interview_p }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-danger"><a href="#" class="text-danger">{% if job.metrics.interview_f %}{{ job.metrics.interview_f }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="#" class="text-success">{% if job.metrics.offer %}{{ job.metrics.offer }}{% else %}-{% endif %}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -251,45 +312,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if page_obj.has_other_pages %}
|
||||
<nav aria-label="Job pagination" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">Last</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
</div>
|
||||
|
||||
{% comment %} Fallback/Empty State {% endcomment %}
|
||||
{% if not jobs and not job_list_data and not page_obj %}
|
||||
<div class="text-center py-5 card shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-briefcase fa-3x mb-3" style="color: var(--kaauh-teal-dark);"></i>
|
||||
<h3>No job postings found</h3>
|
||||
<p class="text-muted">Create your first job posting to get started or adjust your filters.</p>
|
||||
<h3>{% trans "No job postings found" %}</h3>
|
||||
<p class="text-muted">{% trans "Create your first job posting to get started or adjust your filters." %}</p>
|
||||
<a href="{% url 'job_create' %}" class="btn btn-main-action mt-3">
|
||||
<i class="fas fa-plus me-1"></i> Create Job
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Create Job" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
363
templates/recruitment/candidate_exam_view.html
Normal file
363
templates/recruitment/candidate_exam_view.html
Normal file
@ -0,0 +1,363 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Candidate Tier Management - {{ job.title }} - ATS{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* Minimal Tier Management Styles */
|
||||
.tier-controls {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.tier-controls .form-row {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.tier-controls .form-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.bulk-update-controls {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.stage-groups {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.stage-group {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.stage-group .stage-header {
|
||||
background-color: #495057;
|
||||
color: white;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.stage-group .stage-body {
|
||||
padding: 0.75rem;
|
||||
min-height: 80px;
|
||||
}
|
||||
.stage-candidate {
|
||||
padding: 0.375rem;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
}
|
||||
.stage-candidate:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.match-score {
|
||||
font-weight: 600;
|
||||
color: #0056b3;
|
||||
}
|
||||
.btn-sm {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
|
||||
/* Tab Styles for Tiers */
|
||||
.nav-tabs {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.nav-tabs .nav-link {
|
||||
border: none;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.nav-tabs .nav-link:hover {
|
||||
border: none;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
border: none;
|
||||
border-bottom: 2px solid #007bff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tier-1 .nav-link {
|
||||
color: #155724;
|
||||
}
|
||||
.tier-1 .nav-link.active {
|
||||
border-bottom-color: #28a745;
|
||||
}
|
||||
.tier-2 .nav-link {
|
||||
color: #856404;
|
||||
}
|
||||
.tier-2 .nav-link.active {
|
||||
border-bottom-color: #ffc107;
|
||||
}
|
||||
.tier-3 .nav-link {
|
||||
color: #721c24;
|
||||
}
|
||||
.tier-3 .nav-link.active {
|
||||
border-bottom-color: #dc3545;
|
||||
}
|
||||
|
||||
/* Candidate Table Styles */
|
||||
.candidate-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background-color: white;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
.candidate-table thead {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.candidate-table th {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #495057;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
.candidate-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.candidate-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.candidate-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.candidate-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
.candidate-table-responsive {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.stage-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
margin-left: 0.375rem;
|
||||
}
|
||||
.stage-Applied {
|
||||
background-color: #e9ecef;
|
||||
color: #495057;
|
||||
}
|
||||
.stage-Exam {
|
||||
background-color: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
.stage-Interview {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
.stage-Offer {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.exam-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
.exam-controls select,
|
||||
.exam-controls input {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.25rem;
|
||||
}
|
||||
.tier-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
color: #495057;
|
||||
margin-left: 0.375rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="fas fa-layer-group me-2"></i>
|
||||
{% trans "Exam" %} - {{ job.title }}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
Total Candidates: {{ total_candidates }}
|
||||
</p>
|
||||
</div>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Job" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tier Controls -->
|
||||
<div class="tier-controls">
|
||||
<form method="post" class="mb-0">
|
||||
{% csrf_token %}
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="tier1_count">{% trans "Number of candidates in Tier 1 (Top N)" %}</label>
|
||||
<input type="number" name="tier1_count" id="tier1_count" class="form-control"
|
||||
value="{{ tier1_count }}" min="1" max="{{ total_candidates }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" name="update_tiers" class="btn btn-primary">
|
||||
<i class="fas fa-sync-alt me-1"></i> {% trans "Update Tiers" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Tier Display -->
|
||||
<h2 class="h4 mb-3 mt-5">{% trans "Candidate Tiers" %}</h2>
|
||||
<div class="candidate-table-responsive" data-signals__ifmissing="{_fetching: false, selections: Array({{ candidates|length }}).fill(false)}">
|
||||
{% url "bulk_update_candidate_exam_status" job.slug as bulk_update_candidate_exam_status_url %}
|
||||
{% if candidates %}
|
||||
<button class="btn btn-primary"
|
||||
data-attr="{disabled: !$selections.filter(Boolean).length}"
|
||||
data-on-click="@post('{{bulk_update_candidate_exam_status_url}}',{
|
||||
contentType: 'form',
|
||||
selector: '#myform',
|
||||
headers: {'X-CSRFToken': '{{ csrf_token }}','status': 'pass'}
|
||||
})"
|
||||
>Mark as Pass and move to Interview</button>
|
||||
<button class="btn btn-danger"
|
||||
data-attr="{disabled: !$selections.filter(Boolean).length}"
|
||||
data-on-click="@post('{{bulk_update_candidate_exam_status_url}}',{
|
||||
contentType: 'form',
|
||||
selector: '#myform',
|
||||
headers: {'X-CSRFToken': '{{ csrf_token }}','status': 'fail'}
|
||||
})"
|
||||
>Mark as Failed</button>
|
||||
{% endif %}
|
||||
<form id="myform" action="{{move_to_exam_url}}" method="post">
|
||||
<table class="candidate-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{% if candidates %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
data-bind-_all
|
||||
data-on-change="$selections = Array({{ candidates|length }}).fill($_all)"
|
||||
data-effect="$selections; $_all = $selections.every(Boolean)"
|
||||
data-attr-disabled="$_fetching"
|
||||
type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Contact" %}</th>
|
||||
<th>{% trans "AI Score" %}</th>
|
||||
<th>{% trans "Exam Status" %}</th>
|
||||
<th>{% trans "Exam Date" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input
|
||||
data-bind-selections
|
||||
data-attr-disabled="$_fetching"
|
||||
name="{{ candidate.id }}"
|
||||
|
||||
type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-name">{{ candidate.name }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
Email: {{ candidate.email }}<br>
|
||||
Phone: {{ candidate.phone }}<br>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-success">{{ candidate.match_score|default:"0" }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if candidate.exam_status == "Passed" %}
|
||||
<span class="badge bg-success">{{ candidate.exam_status }}</span>
|
||||
{% elif candidate.exam_status == "Failed" %}
|
||||
<span class="badge bg-danger">{{ candidate.exam_status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{candidate.exam_date|date:"M d, Y h:i A"}}</td>
|
||||
<td>
|
||||
<button class="btn btn-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
>
|
||||
{% include "icons/view.html" %}
|
||||
{% trans "View" %}</button>
|
||||
<button class="btn btn-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'update_candidate_exam_status' candidate.slug %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
>
|
||||
{% include "icons/view.html" %}
|
||||
{% trans "Set Exam Date" %}</button>
|
||||
{% if candidate.stage != "Exam" %}
|
||||
<button hx-post="{% url 'candidate_set_exam_date' candidate.slug %}"
|
||||
hx-target=".candidate-table"
|
||||
hx-select=".candidate-table"
|
||||
hx-swap="outerHTML"
|
||||
class="btn btn-primary"> {% trans "Move to Exam" %} {% include "icons/right.html" %}</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Tab Content -->
|
||||
|
||||
<div class="modal fade modal-lg" id="candidateviewModal" tabindex="-1" aria-labelledby="candidateviewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="candidateviewModalLabel">Form Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div id="candidateviewModalBody" class="modal-body">
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -174,59 +174,132 @@
|
||||
{% trans "Number of Potential Candidates (Top N)" %}
|
||||
</label>
|
||||
<input type="number" name="tier1_count" id="tier1_count" class="form-control"
|
||||
value="{{ tier1_count }}" min="1" max="{{ total_candidates }}"
|
||||
placeholder="Enter N for top candidates">
|
||||
value="{{ tier1_count }}" min="1" max="{{ total_candidates }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-auto">
|
||||
<button type="submit" name="update_tiers" class="btn btn-main-action">
|
||||
<i class="fas fa-filter me-1"></i> {% trans "Apply Filter" %}
|
||||
<div class="form-group">
|
||||
<button type="submit" name="update_tiers" class="btn btn-primary">
|
||||
<i class="fas fa-sync-alt me-1"></i> {% trans "Update Tiers" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="small text-muted mt-2 mb-0">
|
||||
{% trans "The candidates list is filtered by either the minimum AI Score OR the Top N candidates, whichever criteria results in a smaller set." %}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
<!-- Tier Display -->
|
||||
<h2 class="h4 mb-3 mt-5">{% trans "Candidate Tiers" %}</h2>
|
||||
<div class="candidate-table-responsive" data-signals__ifmissing="{_fetching: false, selections: Array({{ candidates|length }}).fill(false)}">
|
||||
{% url "bulk_candidate_move_to_exam" as move_to_exam_url %}
|
||||
{% if candidates %}
|
||||
<button class="btn btn-primary"
|
||||
data-attr="{disabled: !$selections.filter(Boolean).length}"
|
||||
data-on-click="@post('{{move_to_exam_url}}',{
|
||||
contentType: 'form',
|
||||
selector: '#myform',
|
||||
headers: {'X-CSRFToken': '{{ csrf_token }}'}})"
|
||||
>Bulk Move To Exam</button>
|
||||
{% endif %}
|
||||
<form id="myform" action="{{move_to_exam_url}}" method="post">
|
||||
<table class="candidate-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{% if candidates %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
data-bind-_all
|
||||
data-on-change="$selections = Array({{ candidates|length }}).fill($_all)"
|
||||
data-effect="$selections; $_all = $selections.every(Boolean)"
|
||||
data-attr-disabled="$_fetching"
|
||||
type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Contact" %}</th>
|
||||
<th>{% trans "AI Score" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Stage" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input
|
||||
data-bind-selections
|
||||
data-attr-disabled="$_fetching"
|
||||
name="{{ candidate.id }}"
|
||||
|
||||
<ul class="nav nav-pills mb-3 justify-content-center" id="candidateViewTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="all-applicants-tab" data-bs-toggle="tab" data-bs-target="#all-applicants" type="button"
|
||||
role="tab" aria-controls="all-applicants" aria-selected="true">
|
||||
<i class="fas fa-users me-1"></i> {% trans "All Applicants" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="potential-candidates-tab" data-bs-toggle="tab" data-bs-target="#potential-candidates" type="button"
|
||||
role="tab" aria-controls="potential-candidates" aria-selected="false">
|
||||
<i class="fas fa-trophy me-1"></i> {% trans "Potential Candidates" %}
|
||||
<span class="candidate-indicator-badge">{{ tier1_candidates|length }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-name">{{ candidate.name }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
Email: {{ candidate.email }}<br>
|
||||
Phone: {{ candidate.phone }}<br>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-success">{{ candidate.match_score|default:"0" }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {% if candidate.applicant_status == 'Candidate' %}bg-success{% else %}bg-secondary{% endif %}">
|
||||
{{ candidate.get_applicant_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="stage-badge stage-{{ candidate.stage }}">
|
||||
{{ candidate.get_stage_display }}
|
||||
</span>
|
||||
{% if candidate.stage == "Exam" and candidate.exam_status %}
|
||||
<br>
|
||||
<span class="badge bg-info">{{ candidate.get_exam_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
>
|
||||
{% include "icons/view.html" %}
|
||||
{% trans "View" %}</button>
|
||||
{% if candidate.stage != "Exam" %}
|
||||
<button hx-post="{% url 'candidate_move_to_exam' candidate.pk %}"
|
||||
hx-target=".candidate-table"
|
||||
hx-select=".candidate-table"
|
||||
hx-swap="outerHTML"
|
||||
class="btn btn-primary"> {% trans "Move to Exam" %} {% include "icons/right.html" %}</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Tab Content -->
|
||||
|
||||
<div class="tab-content kaauh-card p-3 shadow-sm" id="candidateViewTabContent">
|
||||
<div class="modal fade modal-lg" id="candidateviewModal" tabindex="-1" aria-labelledby="candidateviewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="candidateviewModalLabel">Form Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div id="candidateviewModalBody" class="modal-body">
|
||||
|
||||
<div class="tab-pane fade show active" id="all-applicants" role="tabpanel" aria-labelledby="all-applicants-tab">
|
||||
<h2 class="h5 mb-3" style="color: var(--kaauh-teal-dark);">{% trans "Complete List of All Submissions" %}</h2>
|
||||
{% include "recruitment/partials/_candidate_table.html" with candidates=all_applicants_list is_potential_view=False only %}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="potential-candidates" role="tabpanel" aria-labelledby="potential-candidates-tab">
|
||||
<h2 class="h5 mb-3" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Top" %} {{ tier1_candidates|length }} {% trans "Candidates by AI Score" %}
|
||||
</h2>
|
||||
{% include "recruitment/partials/_candidate_table.html" with candidates=tier1_candidates is_potential_view=True only %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% comment %}
|
||||
Modals for candidate view and exam status update should be included here,
|
||||
e.g., {% include "includes/_modals_for_candidate_management.html" %}
|
||||
{% endcomment %}
|
||||
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user