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")
|
||||||
@ -1646,12 +1647,10 @@ def candidate_screening_view(request, slug):
|
|||||||
# 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]
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
@ -412,3 +397,69 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 %}
|
||||||
@ -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>
|
||||||
@ -379,3 +366,69 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 %}
|
||||||
Loading…
x
Reference in New Issue
Block a user