update the bulk button to select
This commit is contained in:
parent
d0db3d1323
commit
302aa8d0bf
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.
14
recruitment/migrations/0012_merge_20251014_1403.py
Normal file
14
recruitment/migrations/0012_merge_20251014_1403.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-14 11:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0010_alter_scheduledinterview_schedule'),
|
||||||
|
('recruitment', '0011_alter_jobpostingimage_job_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
21
recruitment/migrations/0013_alter_formtemplate_created_by.py
Normal file
21
recruitment/migrations/0013_alter_formtemplate_created_by.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-14 11:24
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0012_merge_20251014_1403'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='formtemplate',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -273,12 +273,12 @@ class Candidate(Base):
|
|||||||
CANDIDATE = "Candidate", _("Candidate")
|
CANDIDATE = "Candidate", _("Candidate")
|
||||||
|
|
||||||
# Stage transition validation constants
|
# Stage transition validation constants
|
||||||
STAGE_SEQUENCE = {
|
# STAGE_SEQUENCE = {
|
||||||
"Applied": ["Exam", "Interview", "Offer"],
|
# "Applied": ["Exam", "Interview", "Offer"],
|
||||||
"Exam": ["Interview", "Offer"],
|
# "Exam": ["Interview", "Offer"],
|
||||||
"Interview": ["Offer"],
|
# "Interview": ["Offer"],
|
||||||
"Offer": [], # Final stage - no further transitions
|
# "Offer": [], # Final stage - no further transitions
|
||||||
}
|
# }
|
||||||
|
|
||||||
job = models.ForeignKey(
|
job = models.ForeignKey(
|
||||||
JobPosting,
|
JobPosting,
|
||||||
@ -375,50 +375,50 @@ class Candidate(Base):
|
|||||||
return self.resume.size
|
return self.resume.size
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def clean(self):
|
# def clean(self):
|
||||||
"""Validate stage transitions"""
|
# """Validate stage transitions"""
|
||||||
# Only validate if this is an existing record (not being created)
|
# # Only validate if this is an existing record (not being created)
|
||||||
if self.pk and self.stage != self.__class__.objects.get(pk=self.pk).stage:
|
# if self.pk and self.stage != self.__class__.objects.get(pk=self.pk).stage:
|
||||||
old_stage = self.__class__.objects.get(pk=self.pk).stage
|
# old_stage = self.__class__.objects.get(pk=self.pk).stage
|
||||||
allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, [])
|
# allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, [])
|
||||||
|
|
||||||
if self.stage not in allowed_next_stages:
|
# if self.stage not in allowed_next_stages:
|
||||||
raise ValidationError(
|
# raise ValidationError(
|
||||||
{
|
# {
|
||||||
"stage": f'Cannot transition from "{old_stage}" to "{self.stage}". '
|
# "stage": f'Cannot transition from "{old_stage}" to "{self.stage}". '
|
||||||
f"Allowed transitions: {', '.join(allowed_next_stages) or 'None (final stage)'}"
|
# f"Allowed transitions: {', '.join(allowed_next_stages) or 'None (final stage)'}"
|
||||||
}
|
# }
|
||||||
)
|
# )
|
||||||
|
|
||||||
# Validate that the stage is a valid choice
|
# # Validate that the stage is a valid choice
|
||||||
if self.stage not in [choice[0] for choice in self.Stage.choices]:
|
# if self.stage not in [choice[0] for choice in self.Stage.choices]:
|
||||||
raise ValidationError(
|
# raise ValidationError(
|
||||||
{
|
# {
|
||||||
"stage": f"Invalid stage. Must be one of: {', '.join(choice[0] for choice in self.Stage.choices)}"
|
# "stage": f"Invalid stage. Must be one of: {', '.join(choice[0] for choice in self.Stage.choices)}"
|
||||||
}
|
# }
|
||||||
)
|
# )
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Override save to ensure validation is called"""
|
"""Override save to ensure validation is called"""
|
||||||
self.clean() # Call validation before saving
|
self.clean() # Call validation before saving
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def can_transition_to(self, new_stage):
|
# def can_transition_to(self, new_stage):
|
||||||
"""Check if a stage transition is allowed"""
|
# """Check if a stage transition is allowed"""
|
||||||
if not self.pk: # New record - can be in Applied stage
|
# if not self.pk: # New record - can be in Applied stage
|
||||||
return new_stage == "Applied"
|
# return new_stage == "Applied"
|
||||||
|
|
||||||
old_stage = self.__class__.objects.get(pk=self.pk).stage
|
# old_stage = self.__class__.objects.get(pk=self.pk).stage
|
||||||
allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, [])
|
# allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, [])
|
||||||
return new_stage in allowed_next_stages
|
# return new_stage in allowed_next_stages
|
||||||
|
|
||||||
def get_available_stages(self):
|
# def get_available_stages(self):
|
||||||
"""Get list of stages this candidate can transition to"""
|
# """Get list of stages this candidate can transition to"""
|
||||||
if not self.pk: # New record
|
# if not self.pk: # New record
|
||||||
return ["Applied"]
|
# return ["Applied"]
|
||||||
|
|
||||||
old_stage = self.__class__.objects.get(pk=self.pk).stage
|
# old_stage = self.__class__.objects.get(pk=self.pk).stage
|
||||||
return self.STAGE_SEQUENCE.get(old_stage, [])
|
# return self.STAGE_SEQUENCE.get(old_stage, [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def submission(self):
|
def submission(self):
|
||||||
@ -544,7 +544,7 @@ class FormTemplate(Base):
|
|||||||
blank=True, help_text="Description of the form template"
|
blank=True, help_text="Description of the form template"
|
||||||
)
|
)
|
||||||
created_by = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
User, on_delete=models.CASCADE, related_name="form_templates"
|
User, on_delete=models.CASCADE, related_name="form_templates",null=True,blank=True
|
||||||
)
|
)
|
||||||
is_active = models.BooleanField(
|
is_active = models.BooleanField(
|
||||||
default=False, help_text="Whether this template is active"
|
default=False, help_text="Whether this template is active"
|
||||||
|
|||||||
@ -7,10 +7,10 @@ from .models import FormField,FormStage,FormTemplate,Candidate,JobPosting
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@receiver(post_save, sender=JobPosting)
|
# @receiver(post_save, sender=JobPosting)
|
||||||
def create_form_for_job(sender, instance, created, **kwargs):
|
# def create_form_for_job(sender, instance, created, **kwargs):
|
||||||
if created:
|
# if created:
|
||||||
FormTemplate.objects.create(job=instance, is_active=True, name=instance.title)
|
# FormTemplate.objects.create(job=instance, is_active=True, name=instance.title)
|
||||||
@receiver(post_save, sender=Candidate)
|
@receiver(post_save, sender=Candidate)
|
||||||
def score_candidate_resume(sender, instance, created, **kwargs):
|
def score_candidate_resume(sender, instance, created, **kwargs):
|
||||||
if not instance.is_resume_parsed:
|
if not instance.is_resume_parsed:
|
||||||
|
|||||||
@ -17,7 +17,6 @@ urlpatterns = [
|
|||||||
path('jobs/<slug:slug>/candidate/application/success', views.application_success, name='application_success'),
|
path('jobs/<slug:slug>/candidate/application/success', views.application_success, name='application_success'),
|
||||||
path('careers/',views.kaauh_career,name='kaauh_career'),
|
path('careers/',views.kaauh_career,name='kaauh_career'),
|
||||||
|
|
||||||
|
|
||||||
# LinkedIn Integration URLs
|
# LinkedIn Integration URLs
|
||||||
path('jobs/<slug:slug>/post-to-linkedin/', views.post_to_linkedin, name='post_to_linkedin'),
|
path('jobs/<slug:slug>/post-to-linkedin/', views.post_to_linkedin, name='post_to_linkedin'),
|
||||||
path('jobs/linkedin/login/', views.linkedin_login, name='linkedin_login'),
|
path('jobs/linkedin/login/', views.linkedin_login, name='linkedin_login'),
|
||||||
@ -34,7 +33,6 @@ urlpatterns = [
|
|||||||
path('candidate/<slug:slug>/view/', views_frontend.candidate_detail, name='candidate_detail'),
|
path('candidate/<slug:slug>/view/', views_frontend.candidate_detail, name='candidate_detail'),
|
||||||
path('candidate/<slug:slug>/update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'),
|
path('candidate/<slug:slug>/update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'),
|
||||||
|
|
||||||
|
|
||||||
# Training URLs
|
# Training URLs
|
||||||
path('training/', views_frontend.TrainingListView.as_view(), name='training_list'),
|
path('training/', views_frontend.TrainingListView.as_view(), name='training_list'),
|
||||||
path('training/create/', views_frontend.TrainingCreateView.as_view(), name='training_create'),
|
path('training/create/', views_frontend.TrainingCreateView.as_view(), name='training_create'),
|
||||||
@ -75,7 +73,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('htmx/<int:pk>/candidate_criteria_view/', views.candidate_criteria_view_htmx, name='candidate_criteria_view_htmx'),
|
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/<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('htmx/<slug:slug>/candidate_update_status/', views.candidate_update_status, name='candidate_update_status'),
|
||||||
|
|
||||||
path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'),
|
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'),
|
path('forms/form/<int:template_id>/', views.form_wizard_view, name='form_wizard'),
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from rich import print
|
|||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.http import JsonResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
from datetime import datetime,time,timedelta
|
from datetime import datetime,time,timedelta
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -262,12 +262,13 @@ def create_job(request):
|
|||||||
else:
|
else:
|
||||||
job.created_by = request.POST.get("created_by", "").strip()
|
job.created_by = request.POST.get("created_by", "").strip()
|
||||||
if not job.created_by:
|
if not job.created_by:
|
||||||
job.created_by = "University Administrator"
|
job.created_by = request.user.username
|
||||||
|
|
||||||
job.save()
|
job.save()
|
||||||
job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug})
|
job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug})
|
||||||
job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative)
|
job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative)
|
||||||
job.application_url=job_apply_url_absolute
|
job.application_url=job_apply_url_absolute
|
||||||
|
FormTemplate.objects.create(job=job, is_active=True, name=job.title,created_by=request.user)
|
||||||
job.save()
|
job.save()
|
||||||
messages.success(request, f'Job "{job.title}" created successfully!')
|
messages.success(request, f'Job "{job.title}" created successfully!')
|
||||||
return redirect("job_list")
|
return redirect("job_list")
|
||||||
@ -328,15 +329,15 @@ def job_detail(request, slug):
|
|||||||
|
|
||||||
# Count candidates by stage for summary statistics
|
# Count candidates by stage for summary statistics
|
||||||
total_applicant = applicants.count()
|
total_applicant = applicants.count()
|
||||||
|
|
||||||
applied_count = applicants.filter(stage="Applied").count()
|
applied_count = applicants.filter(stage="Applied").count()
|
||||||
|
|
||||||
exam_count=applicants.filter(stage="Exam").count
|
exam_count=applicants.filter(stage="Exam").count
|
||||||
|
|
||||||
interview_count = applicants.filter(stage="Interview").count()
|
interview_count = applicants.filter(stage="Interview").count()
|
||||||
|
|
||||||
offer_count = applicants.filter(stage="Offer").count()
|
offer_count = applicants.filter(stage="Offer").count()
|
||||||
|
|
||||||
|
|
||||||
status_form = JobPostingStatusForm(instance=job)
|
status_form = JobPostingStatusForm(instance=job)
|
||||||
image_upload_form=JobPostingImageForm(instance=job)
|
image_upload_form=JobPostingImageForm(instance=job)
|
||||||
@ -1521,7 +1522,7 @@ def candidate_screening_view(request, slug):
|
|||||||
offer_count=job.candidates.filter(stage='Offer').count()
|
offer_count=job.candidates.filter(stage='Offer').count()
|
||||||
# Get all candidates for this job, ordered by match score (descending)
|
# Get all candidates for this job, ordered by match score (descending)
|
||||||
candidates = job.candidates.filter(stage="Applied").order_by("-match_score")
|
candidates = job.candidates.filter(stage="Applied").order_by("-match_score")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Get tier categorization parameters
|
# Get tier categorization parameters
|
||||||
@ -1629,33 +1630,31 @@ def candidate_screening_view(request, slug):
|
|||||||
|
|
||||||
min_ai_score_str = request.GET.get('min_ai_score')
|
min_ai_score_str = request.GET.get('min_ai_score')
|
||||||
tier1_count_str = request.GET.get('tier1_count')
|
tier1_count_str = request.GET.get('tier1_count')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if the string value exists and is not an empty string before conversion
|
# Check if the string value exists and is not an empty string before conversion
|
||||||
if min_ai_score_str:
|
if min_ai_score_str:
|
||||||
min_ai_score = int(min_ai_score_str)
|
min_ai_score = int(min_ai_score_str)
|
||||||
else:
|
else:
|
||||||
min_ai_score = 0
|
min_ai_score = 0
|
||||||
|
|
||||||
if tier1_count_str:
|
if tier1_count_str:
|
||||||
tier1_count = int(tier1_count_str)
|
tier1_count = int(tier1_count_str)
|
||||||
else:
|
else:
|
||||||
tier1_count = 0
|
tier1_count = 0
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# This catches if the user enters non-numeric text (e.g., "abc")
|
# This catches if the user enters non-numeric text (e.g., "abc")
|
||||||
min_ai_score = 0
|
min_ai_score = 0
|
||||||
tier1_count = 0
|
tier1_count = 0
|
||||||
print(min_ai_score)
|
|
||||||
print(tier1_count)
|
|
||||||
# You can now safely use min_ai_score and tier1_count as integers (0 or greater)
|
# You can now safely use min_ai_score and tier1_count as integers (0 or greater)
|
||||||
if min_ai_score > 0:
|
if min_ai_score > 0:
|
||||||
candidates = candidates.filter(match_score__gte=min_ai_score)
|
candidates = candidates.filter(match_score__gte=min_ai_score)
|
||||||
print(candidates)
|
|
||||||
|
|
||||||
if tier1_count > 0:
|
if tier1_count > 0:
|
||||||
candidates = candidates[:tier1_count]
|
candidates = candidates[:tier1_count]
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"job": job,
|
"job": job,
|
||||||
"candidates": candidates,
|
"candidates": candidates,
|
||||||
@ -1697,7 +1696,6 @@ def update_candidate_exam_status(request, slug):
|
|||||||
def bulk_update_candidate_exam_status(request,slug):
|
def bulk_update_candidate_exam_status(request,slug):
|
||||||
job = get_object_or_404(JobPosting, slug=slug)
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
status = request.headers.get('status')
|
status = request.headers.get('status')
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
for candidate in get_candidates_from_request(request):
|
for candidate in get_candidates_from_request(request):
|
||||||
try:
|
try:
|
||||||
@ -1724,19 +1722,18 @@ def candidate_set_exam_date(request, slug):
|
|||||||
messages.success(request, f"Set exam date for {candidate.name} to {candidate.exam_date}")
|
messages.success(request, f"Set exam date for {candidate.name} to {candidate.exam_date}")
|
||||||
return redirect("candidate_screening_view", slug=candidate.job.slug)
|
return redirect("candidate_screening_view", slug=candidate.job.slug)
|
||||||
|
|
||||||
def bulk_candidate_move_to_exam(request):
|
def candidate_update_status(request, slug):
|
||||||
for candidate in get_candidates_from_request(request):
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
candidate.stage = "Exam"
|
mark_as = request.POST.get('mark_as')
|
||||||
candidate.applicant_status = "Candidate"
|
candidate_ids = request.POST.getlist("candidate_ids")
|
||||||
candidate.exam_date = timezone.now()
|
|
||||||
candidate.save()
|
|
||||||
|
|
||||||
messages.success(request, f"Candidates Moved to Exam stage")
|
if c := Candidate.objects.filter(pk__in = candidate_ids):
|
||||||
return redirect("candidate_screening_view", slug=candidate.job.slug)
|
c.update(stage=mark_as,exam_date=timezone.now(),applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
|
||||||
# def response():
|
|
||||||
# yield SSE.patch_elements("","")
|
messages.success(request, f"Candidates Updated")
|
||||||
# yield SSE.execute_script("console.log('hello world');")
|
response = HttpResponse(redirect("candidate_screening_view", slug=job.slug))
|
||||||
# return DatastarResponse(response())
|
response.headers["HX-Refresh"] = "true"
|
||||||
|
return response
|
||||||
|
|
||||||
def candidate_interview_view(request,slug):
|
def candidate_interview_view(request,slug):
|
||||||
job = get_object_or_404(JobPosting,slug=slug)
|
job = get_object_or_404(JobPosting,slug=slug)
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
padding-right: var(--bs-gutter-x, 0.75rem); /* Add Bootstrap padding for responsiveness */
|
padding-right: var(--bs-gutter-x, 0.75rem); /* Add Bootstrap padding for responsiveness */
|
||||||
padding-left: var(--bs-gutter-x, 0.75rem);
|
padding-left: var(--bs-gutter-x, 0.75rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Top Bar === */
|
/* === Top Bar === */
|
||||||
.top-bar {
|
.top-bar {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
@ -77,7 +77,7 @@
|
|||||||
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
|
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
|
||||||
}
|
}
|
||||||
/* Change the outer navbar container to fluid, rely on inner max-width */
|
/* Change the outer navbar container to fluid, rely on inner max-width */
|
||||||
.navbar-dark > .container {
|
.navbar-dark > .container {
|
||||||
max-width: 100%; /* Override default container width */
|
max-width: 100%; /* Override default container width */
|
||||||
}
|
}
|
||||||
.nav-link {
|
.nav-link {
|
||||||
@ -276,7 +276,7 @@
|
|||||||
<div class="en small">Princess Nourah bint Abdulrahman University</div>
|
<div class="en small">Princess Nourah bint Abdulrahman University</div>
|
||||||
<div class="en small">King Abdullah bin Abdulaziz University Hospital</div>
|
<div class="en small">King Abdullah bin Abdulaziz University Hospital</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<img src="{% static 'image/kaauh.png' %}" alt="KAAUH Logo" style="max-height: 100px;max-width:100px;">
|
<img src="{% static 'image/kaauh.png' %}" alt="KAAUH Logo" style="max-height: 100px;max-width:100px;">
|
||||||
</div>
|
</div>
|
||||||
@ -325,7 +325,7 @@
|
|||||||
|
|
||||||
{% trans "Form Templates" %}
|
{% trans "Form Templates" %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
</li> {% endcomment %}
|
</li> {% endcomment %}
|
||||||
|
|
||||||
@ -349,8 +349,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
<li class="nav-item me-4">
|
<li class="nav-item me-4">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'training_list' %}active{% endif %}" href="{% url 'training_list' %}">
|
<a class="nav-link {% if request.resolver_match.url_name == 'training_list' %}active{% endif %}" href="{% url 'training_list' %}">
|
||||||
<span class="d-flex align-items-center gap-2">
|
<span class="d-flex align-items-center gap-2">
|
||||||
@ -362,7 +362,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item dropdown ms-2">
|
<li class="nav-item dropdown ms-2">
|
||||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
||||||
data-bs-offset="0, 8" data-bs-auto-close="outside">
|
data-bs-offset="0, 8" data-bs-auto-close="outside">
|
||||||
@ -390,9 +390,9 @@
|
|||||||
<span class="d-none d-lg-inline">{{ LANGUAGE_CODE|upper }}</span>
|
<span class="d-none d-lg-inline">{{ LANGUAGE_CODE|upper }}</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end" data-bs-popper="static">
|
<ul class="dropdown-menu dropdown-menu-end" data-bs-popper="static">
|
||||||
|
|
||||||
{% get_current_language as LANGUAGE_CODE %}
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
@ -401,7 +401,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||||
@ -476,13 +476,13 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="fab fa-linkedin text-primary me-1"></i>
|
<i class="fab fa-linkedin text-primary me-1"></i>
|
||||||
<span class="text-primary d-none d-lg-inline ms-auto me-3">
|
<span class="text-primary d-none d-lg-inline ms-auto me-3">
|
||||||
{% trans "LinkedIn Connected" %}
|
{% trans "LinkedIn Connected" %}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a></li>
|
</a></li>
|
||||||
|
|
||||||
<li><hr class="dropdown-divider my-1"></li>
|
<li><hr class="dropdown-divider my-1"></li>
|
||||||
<li>
|
<li>
|
||||||
<form method="post" action="" class="d-inline">
|
<form method="post" action="" class="d-inline">
|
||||||
@ -518,13 +518,13 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="mt-auto">
|
<footer class="mt-auto">
|
||||||
<div class="footer-bottom py-3 small text-muted" style="background-color: #00363a;">
|
<div class="footer-bottom py-3 small text-muted" style="background-color: #00363a;">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="d-flex justify-content-between align-items-center flex-wrap max-width-1600">
|
<div class="d-flex justify-content-between align-items-center flex-wrap max-width-1600">
|
||||||
<p class="mb-0 text-white-50">
|
<p class="mb-0 text-white-50">
|
||||||
© {% now "Y" %} {% trans "King Abdullah Academic University Hospital (KAAUH)." %}
|
© {% now "Y" %} {% trans "King Abdullah Academic University Hospital (KAAUH)." %}
|
||||||
{% trans "All rights reserved." %}
|
{% trans "All rights reserved." %}
|
||||||
</p>
|
</p>
|
||||||
<a class="text-decoration-none" href="https://tenhal.sa/" target='_blank'>
|
<a class="text-decoration-none" href="https://tenhal.sa/" target='_blank'>
|
||||||
@ -588,7 +588,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.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>
|
{% comment %} <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js"></script> {% endcomment %}
|
||||||
|
|
||||||
{% block customJS %}{% endblock %}
|
{% block customJS %}{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
--kaauh-teal-dark: #004a53;
|
--kaauh-teal-dark: #004a53;
|
||||||
--kaauh-border: #eaeff3;
|
--kaauh-border: #eaeff3;
|
||||||
--kaauh-primary-text: #343a40;
|
--kaauh-primary-text: #343a40;
|
||||||
--kaauh-success: #28a745;
|
--kaauh-success: #28a745;
|
||||||
--kaauh-info: #17a2b8; /* Used for Exam stages (Pending status) */
|
--kaauh-info: #17a2b8; /* Used for Exam stages (Pending status) */
|
||||||
--kaauh-danger: #dc3545;
|
--kaauh-danger: #dc3545;
|
||||||
--kaauh-warning: #ffc107;
|
--kaauh-warning: #ffc107;
|
||||||
@ -28,7 +28,7 @@
|
|||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dedicated style for the tier control block (consistent with .filter-controls) */
|
/* Dedicated style for the tier control block (consistent with .filter-controls) */
|
||||||
.tier-controls {
|
.tier-controls {
|
||||||
background-color: var(--kaauh-border); /* Light background for control sections */
|
background-color: var(--kaauh-border); /* Light background for control sections */
|
||||||
@ -96,14 +96,14 @@
|
|||||||
.form-control-sm,
|
.form-control-sm,
|
||||||
.btn-sm {
|
.btn-sm {
|
||||||
/* Reduce vertical padding even more than default Bootstrap 'sm' */
|
/* Reduce vertical padding even more than default Bootstrap 'sm' */
|
||||||
padding-top: 0.2rem !important;
|
padding-top: 0.2rem !important;
|
||||||
padding-bottom: 0.2rem !important;
|
padding-bottom: 0.2rem !important;
|
||||||
/* Ensure a consistent, small height for inputs and buttons */
|
/* Ensure a consistent, small height for inputs and buttons */
|
||||||
height: 28px !important;
|
height: 28px !important;
|
||||||
font-size: 0.8rem !important;
|
font-size: 0.8rem !important;
|
||||||
}
|
}
|
||||||
.btn-main-action.btn-sm { font-weight: 600 !important; }
|
.btn-main-action.btn-sm { font-weight: 600 !important; }
|
||||||
|
|
||||||
/* Container for the timeline include */
|
/* Container for the timeline include */
|
||||||
.applicant-tracking-timeline {
|
.applicant-tracking-timeline {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
/* 4. Candidate Table Styling (KAAT-S Look) */
|
/* 4. Candidate Table Styling (KAAT-S Look) */
|
||||||
.candidate-table {
|
.candidate-table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
@ -172,7 +172,7 @@
|
|||||||
.bg-success { background-color: var(--kaauh-success) !important; color: white; }
|
.bg-success { background-color: var(--kaauh-success) !important; color: white; }
|
||||||
.bg-danger { background-color: var(--kaauh-danger) !important; color: white; }
|
.bg-danger { background-color: var(--kaauh-danger) !important; color: white; }
|
||||||
.bg-info-pending { background-color: var(--kaauh-info) !important; color: white; }
|
.bg-info-pending { background-color: var(--kaauh-info) !important; color: white; }
|
||||||
|
|
||||||
.tier-badge { /* Used for Tier labels */
|
.tier-badge { /* Used for Tier labels */
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 0.125rem 0.5rem;
|
padding: 0.125rem 0.5rem;
|
||||||
@ -191,7 +191,7 @@
|
|||||||
.candidate-table th:nth-child(5) { width: 12%; } /* Exam Status */
|
.candidate-table th:nth-child(5) { width: 12%; } /* Exam Status */
|
||||||
.candidate-table th:nth-child(6) { width: 15%; } /* Exam Date */
|
.candidate-table th:nth-child(6) { width: 15%; } /* Exam Date */
|
||||||
.candidate-table th:nth-child(7) { width: 220px; } /* Actions */
|
.candidate-table th:nth-child(7) { width: 220px; } /* Actions */
|
||||||
|
|
||||||
.cd_exam{
|
.cd_exam{
|
||||||
color: #00636e;
|
color: #00636e;
|
||||||
}
|
}
|
||||||
@ -253,32 +253,23 @@
|
|||||||
|
|
||||||
<div class="kaauh-card shadow-sm p-3">
|
<div class="kaauh-card shadow-sm p-3">
|
||||||
<div class="candidate-table-responsive" data-signals__ifmissing="{_fetching: false, selections: Array({{ candidates|length }}).fill(false)}">
|
<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 %}
|
<div class="col-md-3 col-sm-6 mb-3 d-flex gap-2">
|
||||||
|
|
||||||
<div class="mb-3 d-flex gap-2">
|
|
||||||
{% if candidates %}
|
{% if candidates %}
|
||||||
<button class="btn btn-bulk-pass btn-sm"
|
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post">
|
||||||
data-attr="{disabled: !$selections.filter(Boolean).length}"
|
<div class="d-flex align-items-center">
|
||||||
data-on-click="@post('{{bulk_update_candidate_exam_status_url}}',{
|
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="height: 3rem;">
|
||||||
contentType: 'form',
|
<option value="Applied">
|
||||||
selector: '#candidate-form',
|
<i class="fas fa-arrow-left me-1"></i> {% trans "Apply" %}
|
||||||
headers: {'X-CSRFToken': '{{ csrf_token }}','status': 'Passed'}
|
</option>
|
||||||
})"
|
<option value="Interview">
|
||||||
>
|
<i class="fas fa-arrow-right me-1"></i> {% trans "Interview" %}
|
||||||
<i class="fas fa-check-circle me-1"></i>
|
</option>
|
||||||
{% trans "Bulk Mark Passed (-> Interview)" %}
|
</select>
|
||||||
</button>
|
<button type="submit" class="btn btn-main-action btn-mds ms-2">
|
||||||
<button class="btn btn-bulk-fail btn-sm"
|
<i class="fas fa-arrow-right me-1"></i> {% trans "Update" %}
|
||||||
data-attr="{disabled: !$selections.filter(Boolean).length}"
|
</button>
|
||||||
data-on-click="@post('{{bulk_update_candidate_exam_status_url}}',{
|
</div>
|
||||||
contentType: 'form',
|
</form>
|
||||||
selector: '#candidate-form',
|
|
||||||
headers: {'X-CSRFToken': '{{ csrf_token }}','status': 'Failed'}
|
|
||||||
})"
|
|
||||||
>
|
|
||||||
<i class="fas fa-times-circle me-1"></i>
|
|
||||||
{% trans "Bulk Mark Failed" %}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -291,11 +282,7 @@
|
|||||||
{% if candidates %}
|
{% if candidates %}
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
data-bind-_all
|
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||||
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="checkAll">
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
@ -312,13 +299,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
data-bind-selections
|
name="candidate_ids"
|
||||||
data-attr-disabled="$_fetching"
|
value="{{ candidate.id }}"
|
||||||
name="candidate_ids"
|
type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||||
value="{{ candidate.id }}"
|
</div>
|
||||||
type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}">
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="candidate-name">
|
<div class="candidate-name">
|
||||||
@ -378,14 +363,14 @@
|
|||||||
</table>
|
</table>
|
||||||
{% if not candidates %}
|
{% if not candidates %}
|
||||||
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
|
||||||
<i class="fas fa-info-circle me-1"></i>
|
<i class="fas fa-info-circle me-1"></i>
|
||||||
{% trans "No candidates are currently in the Exam stage for this job." %}
|
{% trans "No candidates are currently in the Exam stage for this job." %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade modal-lg" id="candidateviewModal" tabindex="-1" aria-labelledby="candidateviewModalLabel" aria-hidden="true">
|
<div class="modal fade modal-lg" id="candidateviewModal" tabindex="-1" aria-labelledby="candidateviewModalLabel" aria-hidden="true">
|
||||||
@ -411,4 +396,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block customJS %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
|
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||||
|
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
|
||||||
|
// Function to safely update the header checkbox state
|
||||||
|
function updateSelectAllState() {
|
||||||
|
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
||||||
|
const totalCount = rowCheckboxes.length;
|
||||||
|
|
||||||
|
if (checkedCount === 0) {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
} else if (checkedCount === totalCount) {
|
||||||
|
selectAllCheckbox.checked = true;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
} else {
|
||||||
|
// Set to indeterminate state (partially checked)
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: We do NOT fire a change event here to prevent the infinite loop.
|
||||||
|
// Your existing data-bind-_all logic should handle the bulk action status.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Logic for the 'Select All' checkbox (Clicking it updates all rows)
|
||||||
|
selectAllCheckbox.addEventListener('change', function () {
|
||||||
|
const isChecked = selectAllCheckbox.checked;
|
||||||
|
|
||||||
|
// Temporarily disable the change listener on rows to prevent cascading events
|
||||||
|
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
|
||||||
|
|
||||||
|
// Update all row checkboxes
|
||||||
|
rowCheckboxes.forEach(function (checkbox) {
|
||||||
|
checkbox.checked = isChecked;
|
||||||
|
|
||||||
|
// You must still dispatch the event here so your framework's data-bind-selections
|
||||||
|
// picks up the change on individual elements. This should NOT trigger the updateSelectAllState.
|
||||||
|
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-attach the change listeners to the rows
|
||||||
|
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
|
||||||
|
|
||||||
|
// Ensure the header state is correct after forcing all changes
|
||||||
|
updateSelectAllState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Logic to update 'Select All' state based on row checkboxes
|
||||||
|
// Attach the function to be called whenever a row checkbox changes
|
||||||
|
rowCheckboxes.forEach(function (checkbox) {
|
||||||
|
checkbox.addEventListener('change', updateSelectAllState);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial check to set the correct state on load (in case items are pre-checked)
|
||||||
|
updateSelectAllState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -194,48 +194,40 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="filter-controls shadow-sm">
|
<div class="filter-controls shadow-sm">
|
||||||
<h4 class="h6 mb-3 fw-bold" style="color: var(--kaauh-primary-text);">
|
<h4 class="h6 mb-3 fw-bold" style="color: var(--kaauh-primary-text);">
|
||||||
<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 Candidate Filter" %}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<form method="GET" class="mb-0">
|
<form method="GET" class="mb-0 pb-3">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row g-3 align-items-end">
|
<div class="d-flex flex-nowrap g-3 align-items-end" style="overflow-x: auto;">
|
||||||
|
|
||||||
<div class="col-md-3 col-sm-6">
|
<div class="p-2">
|
||||||
<label for="min_ai_score" class="form-label small text-muted">
|
<label for="min_ai_score" class="form-label small text-muted mb-1">
|
||||||
{% trans "Minimum AI Score" %}
|
{% trans "Min AI Score" %}
|
||||||
|
</label>
|
||||||
|
<input type="number" name="min_ai_score" id="min_ai_score" class="form-control form-control-sm"
|
||||||
|
value="{{ min_ai_score}}" min="0" max="100" step="1"
|
||||||
|
placeholder="e.g., 75" style="min-width: 120px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 col-sm-6">
|
<div class="p-2">
|
||||||
<label for="min_ai_score" class="form-label small text-muted mb-1">
|
<label for="tier1_count" class="form-label small text-muted mb-1">
|
||||||
{% trans "Min AI Score" %}
|
{% trans "Top N" %}
|
||||||
</label>
|
</label>
|
||||||
<input type="number" name="min_ai_score" id="min_ai_score" class="form-control form-control-sm"
|
<input type="number" name="tier1_count" id="tier1_count" class="form-control form-control-sm"
|
||||||
value="{{ min_ai_score}}" min="0" max="100" step="1"
|
value="{{ tier1_count }}" min="1" max="{{ total_candidates }}" style="min-width: 100px;">
|
||||||
placeholder="e.g., 75">
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2 col-sm-6">
|
<div class="p-2">
|
||||||
<label for="tier1_count" class="form-label small text-muted mb-1">
|
<label class="form-label small text-muted mb-1 d-block"> </label>
|
||||||
{% trans "Top N" %}
|
<button type="submit" name="update_tiers" class="btn btn-main-action btn-sm w-100" style="min-width: 150px;">
|
||||||
</label>
|
<i class="fas fa-sync-alt me-1"></i> {% trans "Update Filters" %}
|
||||||
<input type="number" name="tier1_count" id="tier1_count" class="form-control form-control-sm"
|
</button>
|
||||||
value="{{ tier1_count }}" min="1" max="{{ total_candidates }}">
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
<div class="col-md-3 col-sm-6">
|
|
||||||
<button type="submit" name="update_tiers" class="btn btn-main-action btn-sm w-100">
|
|
||||||
<i class="fas fa-sync-alt me-1"></i> {% trans "Update Filters" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% comment %} Empty col for spacing (2 + 2 + 3 + 5 = 12) {% endcomment %}
|
|
||||||
<div class="col-md-5 d-none d-md-block"></div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
|
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
|
||||||
@ -244,22 +236,23 @@
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="kaauh-card shadow-sm p-3">
|
<div class="kaauh-card shadow-sm p-3">
|
||||||
{% url "bulk_candidate_move_to_exam" as move_to_exam_url %}
|
|
||||||
|
|
||||||
{% if candidates %}
|
{% if candidates %}
|
||||||
<button class="btn btn-bulk-action btn-sm mb-3"
|
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post">
|
||||||
data-attr="{disabled: !$selections.filter(Boolean).length}"
|
<div class="d-flex align-items-center">
|
||||||
data-on-click="@post('{{move_to_exam_url}}',{
|
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="height: 3rem;">
|
||||||
contentType: 'form',
|
<option value="Exam">
|
||||||
selector: '#candidate-form',
|
<i class="fas fa-arrow-right me-1"></i> {% trans "Exam" %}
|
||||||
headers: {'X-CSRFToken': '{{ csrf_token }}'}})"
|
</option>
|
||||||
>
|
</select>
|
||||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Bulk Move to Exam" %}
|
<button type="submit" class="btn btn-main-action btn-mds ms-2">
|
||||||
</button>
|
<i class="fas fa-arrow-right me-1"></i> {% trans "Update" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<form id="candidate-form" action="{{move_to_exam_url}}" method="post">
|
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<table class="table candidate-table align-middle">
|
<table class="table candidate-table align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
@ -268,11 +261,7 @@
|
|||||||
{% if candidates %}
|
{% if candidates %}
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
data-bind-_all
|
type="checkbox" class="form-check-input" id="selectAllCheckbox">
|
||||||
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="checkAll">
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
@ -290,11 +279,9 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
data-bind-selections
|
|
||||||
data-attr-disabled="$_fetching"
|
|
||||||
name="candidate_ids"
|
name="candidate_ids"
|
||||||
value="{{ candidate.id }}"
|
value="{{ candidate.id }}"
|
||||||
type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}">
|
type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@ -378,4 +365,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block customJS %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
|
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||||
|
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
|
||||||
|
// Function to safely update the header checkbox state
|
||||||
|
function updateSelectAllState() {
|
||||||
|
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
||||||
|
const totalCount = rowCheckboxes.length;
|
||||||
|
|
||||||
|
if (checkedCount === 0) {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
} else if (checkedCount === totalCount) {
|
||||||
|
selectAllCheckbox.checked = true;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
} else {
|
||||||
|
// Set to indeterminate state (partially checked)
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: We do NOT fire a change event here to prevent the infinite loop.
|
||||||
|
// Your existing data-bind-_all logic should handle the bulk action status.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Logic for the 'Select All' checkbox (Clicking it updates all rows)
|
||||||
|
selectAllCheckbox.addEventListener('change', function () {
|
||||||
|
const isChecked = selectAllCheckbox.checked;
|
||||||
|
|
||||||
|
// Temporarily disable the change listener on rows to prevent cascading events
|
||||||
|
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
|
||||||
|
|
||||||
|
// Update all row checkboxes
|
||||||
|
rowCheckboxes.forEach(function (checkbox) {
|
||||||
|
checkbox.checked = isChecked;
|
||||||
|
|
||||||
|
// You must still dispatch the event here so your framework's data-bind-selections
|
||||||
|
// picks up the change on individual elements. This should NOT trigger the updateSelectAllState.
|
||||||
|
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-attach the change listeners to the rows
|
||||||
|
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
|
||||||
|
|
||||||
|
// Ensure the header state is correct after forcing all changes
|
||||||
|
updateSelectAllState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Logic to update 'Select All' state based on row checkboxes
|
||||||
|
// Attach the function to be called whenever a row checkbox changes
|
||||||
|
rowCheckboxes.forEach(function (checkbox) {
|
||||||
|
checkbox.addEventListener('change', updateSelectAllState);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial check to set the correct state on load (in case items are pre-checked)
|
||||||
|
updateSelectAllState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Loading…
x
Reference in New Issue
Block a user