finish interview and validation
This commit is contained in:
parent
c4115efb52
commit
670ff55883
@ -199,7 +199,7 @@ ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]
|
|||||||
ACCOUNT_UNIQUE_EMAIL = True
|
ACCOUNT_UNIQUE_EMAIL = True
|
||||||
ACCOUNT_EMAIL_VERIFICATION = 'none'
|
ACCOUNT_EMAIL_VERIFICATION = 'none'
|
||||||
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||||
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
ACCOUNT_EMAIL_VERIFICATION = "optional"
|
||||||
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.utils import timezone
|
|||||||
from .models import (
|
from .models import (
|
||||||
JobPosting, Application, TrainingMaterial,
|
JobPosting, Application, TrainingMaterial,
|
||||||
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
|
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
|
||||||
SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,InterviewNote,
|
SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,Note,
|
||||||
AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview
|
AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview
|
||||||
)
|
)
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
@ -250,4 +250,4 @@ admin.site.register(ScheduledInterview)
|
|||||||
|
|
||||||
|
|
||||||
admin.site.register(JobPostingImage)
|
admin.site.register(JobPostingImage)
|
||||||
admin.site.register(User)
|
# admin.site.register(User)
|
||||||
|
|||||||
@ -18,7 +18,7 @@ from .models import (
|
|||||||
BulkInterviewTemplate,
|
BulkInterviewTemplate,
|
||||||
BreakTime,
|
BreakTime,
|
||||||
JobPostingImage,
|
JobPostingImage,
|
||||||
InterviewNote,
|
Note,
|
||||||
ScheduledInterview,
|
ScheduledInterview,
|
||||||
Source,
|
Source,
|
||||||
HiringAgency,
|
HiringAgency,
|
||||||
@ -720,94 +720,100 @@ class FormTemplateForm(forms.ModelForm):
|
|||||||
# BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True)
|
# BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True)
|
||||||
|
|
||||||
|
|
||||||
# class BulkInterviewTemplateForm(forms.ModelForm):
|
class BulkInterviewTemplateForm(forms.ModelForm):
|
||||||
# applications = forms.ModelMultipleChoiceField(
|
applications = forms.ModelMultipleChoiceField(
|
||||||
# queryset=Application.objects.none(),
|
queryset=Application.objects.none(),
|
||||||
# widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
# required=True,
|
required=True,
|
||||||
# )
|
)
|
||||||
# working_days = forms.MultipleChoiceField(
|
working_days = forms.MultipleChoiceField(
|
||||||
# choices=[
|
choices=[
|
||||||
# (0, "Monday"),
|
(0, "Monday"),
|
||||||
# (1, "Tuesday"),
|
(1, "Tuesday"),
|
||||||
# (2, "Wednesday"),
|
(2, "Wednesday"),
|
||||||
# (3, "Thursday"),
|
(3, "Thursday"),
|
||||||
# (4, "Friday"),
|
(4, "Friday"),
|
||||||
# (5, "Saturday"),
|
(5, "Saturday"),
|
||||||
# (6, "Sunday"),
|
(6, "Sunday"),
|
||||||
# ],
|
],
|
||||||
# widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
# required=True,
|
required=True,
|
||||||
# )
|
)
|
||||||
|
|
||||||
# class Meta:
|
class Meta:
|
||||||
# model = BulkInterviewTemplate
|
model = BulkInterviewTemplate
|
||||||
# fields = [
|
fields = [
|
||||||
# 'schedule_interview_type',
|
'schedule_interview_type',
|
||||||
# "applications",
|
'topic',
|
||||||
# "start_date",
|
'physical_address',
|
||||||
# "end_date",
|
"applications",
|
||||||
# "working_days",
|
"start_date",
|
||||||
# "start_time",
|
"end_date",
|
||||||
# "end_time",
|
"working_days",
|
||||||
# "interview_duration",
|
"start_time",
|
||||||
# "buffer_time",
|
"end_time",
|
||||||
# "break_start_time",
|
"interview_duration",
|
||||||
# "break_end_time",
|
"buffer_time",
|
||||||
# ]
|
"break_start_time",
|
||||||
# widgets = {
|
"break_end_time",
|
||||||
# "start_date": forms.DateInput(
|
]
|
||||||
# attrs={"type": "date", "class": "form-control"}
|
widgets = {
|
||||||
# ),
|
"topic": forms.TextInput(attrs={"class": "form-control"}),
|
||||||
# "end_date": forms.DateInput(
|
"start_date": forms.DateInput(
|
||||||
# attrs={"type": "date", "class": "form-control"}
|
attrs={"type": "date", "class": "form-control"}
|
||||||
# ),
|
),
|
||||||
# "start_time": forms.TimeInput(
|
"end_date": forms.DateInput(
|
||||||
# attrs={"type": "time", "class": "form-control"}
|
attrs={"type": "date", "class": "form-control"}
|
||||||
# ),
|
),
|
||||||
# "end_time": forms.TimeInput(
|
"start_time": forms.TimeInput(
|
||||||
# attrs={"type": "time", "class": "form-control"}
|
attrs={"type": "time", "class": "form-control"}
|
||||||
# ),
|
),
|
||||||
# "interview_duration": forms.NumberInput(attrs={"class": "form-control"}),
|
"end_time": forms.TimeInput(
|
||||||
# "buffer_time": forms.NumberInput(attrs={"class": "form-control"}),
|
attrs={"type": "time", "class": "form-control"}
|
||||||
# "break_start_time": forms.TimeInput(
|
),
|
||||||
# attrs={"type": "time", "class": "form-control"}
|
"interview_duration": forms.NumberInput(attrs={"class": "form-control"}),
|
||||||
# ),
|
"buffer_time": forms.NumberInput(attrs={"class": "form-control"}),
|
||||||
# "break_end_time": forms.TimeInput(
|
"break_start_time": forms.TimeInput(
|
||||||
# attrs={"type": "time", "class": "form-control"}
|
attrs={"type": "time", "class": "form-control"}
|
||||||
# ),
|
),
|
||||||
# "schedule_interview_type":forms.RadioSelect()
|
"break_end_time": forms.TimeInput(
|
||||||
# }
|
attrs={"type": "time", "class": "form-control"}
|
||||||
|
),
|
||||||
|
"schedule_interview_type":forms.RadioSelect(),
|
||||||
|
"physical_address": forms.Textarea(
|
||||||
|
attrs={"class": "form-control", "rows": 3, "placeholder": "Enter physical address if 'In-Person' is selected"}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
# def __init__(self, slug, *args, **kwargs):
|
def __init__(self, slug, *args, **kwargs):
|
||||||
# super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# self.fields["applications"].queryset = Application.objects.filter(
|
self.fields["applications"].queryset = Application.objects.filter(
|
||||||
# job__slug=slug, stage="Interview"
|
job__slug=slug, stage="Interview"
|
||||||
# )
|
)
|
||||||
|
|
||||||
# def clean_working_days(self):
|
def clean_working_days(self):
|
||||||
# working_days = self.cleaned_data.get("working_days")
|
working_days = self.cleaned_data.get("working_days")
|
||||||
# return [int(day) for day in working_days]
|
return [int(day) for day in working_days]
|
||||||
|
|
||||||
|
|
||||||
# class InterviewNoteForm(forms.ModelForm):
|
class NoteForm(forms.ModelForm):
|
||||||
# """Form for creating and editing meeting comments"""
|
"""Form for creating and editing meeting comments"""
|
||||||
|
|
||||||
# class Meta:
|
class Meta:
|
||||||
# model = InterviewNote
|
model = Note
|
||||||
# fields = ["content"]
|
fields = "__all__"
|
||||||
# widgets = {
|
widgets = {
|
||||||
# "content": CKEditor5Widget(
|
"content": CKEditor5Widget(
|
||||||
# attrs={
|
attrs={
|
||||||
# "class": "form-control",
|
"class": "form-control",
|
||||||
# "placeholder": _("Enter your comment or note"),
|
"placeholder": _("Enter your comment or note"),
|
||||||
# },
|
},
|
||||||
# config_name="extends",
|
config_name="extends",
|
||||||
# ),
|
),
|
||||||
# }
|
}
|
||||||
# labels = {
|
labels = {
|
||||||
# "content": _("Comment"),
|
"content": _("Comment"),
|
||||||
# }
|
}
|
||||||
|
|
||||||
# def __init__(self, *args, **kwargs):
|
# def __init__(self, *args, **kwargs):
|
||||||
# super().__init__(*args, **kwargs)
|
# super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-12-01 12:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bulkinterviewtemplate',
|
||||||
|
name='schedule_interview_type',
|
||||||
|
field=models.CharField(choices=[('Remote', 'Remote (e.g., Zoom)'), ('Onsite', 'In-Person (Physical Location)')], default='Onsite', max_length=10, verbose_name='Interview Type'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-12-01 13:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0002_bulkinterviewtemplate_schedule_interview_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bulkinterviewtemplate',
|
||||||
|
name='physical_address',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
19
recruitment/migrations/0004_bulkinterviewtemplate_topic.py
Normal file
19
recruitment/migrations/0004_bulkinterviewtemplate_topic.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-12-01 14:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0003_bulkinterviewtemplate_physical_address'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bulkinterviewtemplate',
|
||||||
|
name='topic',
|
||||||
|
field=models.CharField(default='', max_length=255, verbose_name='Interview Topic'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1397,6 +1397,7 @@ class BulkInterviewTemplate(Base):
|
|||||||
working_days = models.JSONField(
|
working_days = models.JSONField(
|
||||||
verbose_name=_("Working Days")
|
verbose_name=_("Working Days")
|
||||||
)
|
)
|
||||||
|
topic = models.CharField(max_length=255, verbose_name=_("Interview Topic"))
|
||||||
|
|
||||||
start_time = models.TimeField(verbose_name=_("Start Time"))
|
start_time = models.TimeField(verbose_name=_("Start Time"))
|
||||||
end_time = models.TimeField(verbose_name=_("End Time"))
|
end_time = models.TimeField(verbose_name=_("End Time"))
|
||||||
@ -1414,6 +1415,14 @@ class BulkInterviewTemplate(Base):
|
|||||||
buffer_time = models.PositiveIntegerField(
|
buffer_time = models.PositiveIntegerField(
|
||||||
verbose_name=_("Buffer Time (minutes)"), default=0
|
verbose_name=_("Buffer Time (minutes)"), default=0
|
||||||
)
|
)
|
||||||
|
schedule_interview_type = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('Remote', 'Remote (e.g., Zoom)'), ('Onsite', 'In-Person (Physical Location)')],
|
||||||
|
default='Onsite',
|
||||||
|
verbose_name=_("Interview Type"),
|
||||||
|
)
|
||||||
|
physical_address = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
|
||||||
created_by = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
User, on_delete=models.CASCADE, db_index=True
|
User, on_delete=models.CASCADE, db_index=True
|
||||||
)
|
)
|
||||||
@ -1509,7 +1518,7 @@ class ScheduledInterview(Base):
|
|||||||
return self.interview_location
|
return self.interview_location
|
||||||
# --- 3. Interview Notes Model (Fixed) ---
|
# --- 3. Interview Notes Model (Fixed) ---
|
||||||
|
|
||||||
class InterviewNote(Base):
|
class Note(Base):
|
||||||
"""Model for storing notes, feedback, or comments related to a specific ScheduledInterview."""
|
"""Model for storing notes, feedback, or comments related to a specific ScheduledInterview."""
|
||||||
|
|
||||||
class NoteType(models.TextChoices):
|
class NoteType(models.TextChoices):
|
||||||
@ -1517,13 +1526,24 @@ class InterviewNote(Base):
|
|||||||
LOGISTICS = 'Logistics', _('Logistical Note')
|
LOGISTICS = 'Logistics', _('Logistical Note')
|
||||||
GENERAL = 'General', _('General Comment')
|
GENERAL = 'General', _('General Comment')
|
||||||
|
|
||||||
1
|
|
||||||
|
application = models.ForeignKey(
|
||||||
|
Application,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="notes",
|
||||||
|
verbose_name=_("Application"),
|
||||||
|
db_index=True,
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
interview = models.ForeignKey(
|
interview = models.ForeignKey(
|
||||||
Interview,
|
Interview,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="notes",
|
related_name="notes",
|
||||||
verbose_name=_("Scheduled Interview"),
|
verbose_name=_("Scheduled Interview"),
|
||||||
db_index=True
|
db_index=True,
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
author = models.ForeignKey(
|
author = models.ForeignKey(
|
||||||
@ -2692,3 +2712,5 @@ class Document(Base):
|
|||||||
if self.file:
|
if self.file:
|
||||||
return self.file.name.split(".")[-1].upper()
|
return self.file.name.split(".")[-1].upper()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from . linkedin_service import LinkedInService
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from . models import JobPosting
|
from . models import JobPosting
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from . models import ScheduledInterview,Interview,Message
|
from . models import BulkInterviewTemplate,Interview,Message,ScheduledInterview
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
# Add python-docx import for Word document processing
|
# Add python-docx import for Word document processing
|
||||||
@ -668,7 +668,7 @@ def handle_resume_parsing_and_scoring(pk: int):
|
|||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
def create_interview_and_meeting(
|
def create_interview_and_meeting(
|
||||||
candidate_id,
|
application_id,
|
||||||
job_id,
|
job_id,
|
||||||
schedule_id,
|
schedule_id,
|
||||||
slot_date,
|
slot_date,
|
||||||
@ -679,24 +679,13 @@ def create_interview_and_meeting(
|
|||||||
Synchronous task for a single interview slot, dispatched by django-q.
|
Synchronous task for a single interview slot, dispatched by django-q.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
application = Application.objects.get(pk=candidate_id)
|
application = Application.objects.get(pk=application_id)
|
||||||
job = JobPosting.objects.get(pk=job_id)
|
job = JobPosting.objects.get(pk=job_id)
|
||||||
schedule = ScheduledInterview.objects.get(pk=schedule_id)
|
schedule = BulkInterviewTemplate.objects.get(pk=schedule_id)
|
||||||
|
|
||||||
interview_datetime = timezone.make_aware(datetime.combine(slot_date, slot_time))
|
interview_datetime = timezone.make_aware(datetime.combine(slot_date, slot_time))
|
||||||
meeting_topic = f"Interview for {job.title} - {application.name}"
|
meeting_topic = schedule.topic
|
||||||
|
|
||||||
# 1. External API Call (Slow)
|
|
||||||
# "status": "success",
|
|
||||||
# "message": "Meeting created successfully.",
|
|
||||||
# "meeting_details": {
|
|
||||||
# "join_url": meeting_data['join_url'],
|
|
||||||
# "meeting_id": meeting_data['id'],
|
|
||||||
# "password": meeting_data['password'],
|
|
||||||
# "host_email": meeting_data['host_email']
|
|
||||||
# },
|
|
||||||
# "zoom_gateway_response": meeting_data
|
|
||||||
# }
|
|
||||||
result = create_zoom_meeting(meeting_topic, interview_datetime, duration)
|
result = create_zoom_meeting(meeting_topic, interview_datetime, duration)
|
||||||
|
|
||||||
if result["status"] == "success":
|
if result["status"] == "success":
|
||||||
@ -711,33 +700,19 @@ def create_interview_and_meeting(
|
|||||||
password=result["meeting_details"]["password"],
|
password=result["meeting_details"]["password"],
|
||||||
location_type="Remote"
|
location_type="Remote"
|
||||||
)
|
)
|
||||||
|
schedule = ScheduledInterview.objects.create(
|
||||||
|
application=application,
|
||||||
|
job=job,
|
||||||
|
schedule=schedule,
|
||||||
|
interview_date=slot_date,
|
||||||
|
interview_time=slot_time,
|
||||||
|
interview=interview
|
||||||
|
)
|
||||||
schedule.interview = interview
|
schedule.interview = interview
|
||||||
schedule.status = "scheduled"
|
schedule.status = "scheduled"
|
||||||
|
|
||||||
schedule.save()
|
schedule.save()
|
||||||
|
|
||||||
# 2. Database Writes (Slow)
|
|
||||||
# zoom_meeting = ZoomMeetingDetails.objects.create(
|
|
||||||
# topic=meeting_topic,
|
|
||||||
# start_time=interview_datetime,
|
|
||||||
# duration=duration,
|
|
||||||
# meeting_id=result["meeting_details"]["meeting_id"],
|
|
||||||
# details_url=result["meeting_details"]["join_url"],
|
|
||||||
# zoom_gateway_response=result["zoom_gateway_response"],
|
|
||||||
# host_email=result["meeting_details"]["host_email"],
|
|
||||||
# password=result["meeting_details"]["password"],
|
|
||||||
# location_type="Remote"
|
|
||||||
# )
|
|
||||||
# ScheduledInterview.objects.create(
|
|
||||||
# application=candidate,
|
|
||||||
# job=job,
|
|
||||||
# interview_location=zoom_meeting,
|
|
||||||
# schedule=schedule,
|
|
||||||
# interview_date=slot_date,
|
|
||||||
# interview_time=slot_time
|
|
||||||
# )
|
|
||||||
|
|
||||||
# Log success or use Django-Q result system for monitoring
|
|
||||||
logger.info(f"Successfully scheduled interview for {Application.name}")
|
logger.info(f"Successfully scheduled interview for {Application.name}")
|
||||||
return True # Task succeeded
|
return True # Task succeeded
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -587,16 +587,16 @@ urlpatterns = [
|
|||||||
# path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'),
|
# path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'),
|
||||||
|
|
||||||
#interview and meeting related urls
|
#interview and meeting related urls
|
||||||
# path(
|
path(
|
||||||
# "jobs/<slug:slug>/schedule-interviews/",
|
"jobs/<slug:slug>/schedule-interviews/",
|
||||||
# views.schedule_interviews_view,
|
views.schedule_interviews_view,
|
||||||
# name="schedule_interviews",
|
name="schedule_interviews",
|
||||||
# ),
|
),
|
||||||
# path(
|
path(
|
||||||
# "jobs/<slug:slug>/confirm-schedule-interviews/",
|
"jobs/<slug:slug>/confirm-schedule-interviews/",
|
||||||
# views.confirm_schedule_interviews_view,
|
views.confirm_schedule_interviews_view,
|
||||||
# name="confirm_schedule_interviews_view",
|
name="confirm_schedule_interviews_view",
|
||||||
# ),
|
),
|
||||||
|
|
||||||
# path(
|
# path(
|
||||||
# "meetings/create-meeting/",
|
# "meetings/create-meeting/",
|
||||||
@ -682,5 +682,6 @@ urlpatterns = [
|
|||||||
# Email invitation URLs
|
# Email invitation URLs
|
||||||
# path("interviews/meetings/<slug:slug>/send-application-invitation/", views.send_application_invitation, name="send_application_invitation"),
|
# path("interviews/meetings/<slug:slug>/send-application-invitation/", views.send_application_invitation, name="send_application_invitation"),
|
||||||
# path("interviews/meetings/<slug:slug>/send-participants-invitation/", views.send_participants_invitation, name="send_participants_invitation"),
|
# path("interviews/meetings/<slug:slug>/send-participants-invitation/", views.send_participants_invitation, name="send_participants_invitation"),
|
||||||
|
path("note/<slug:slug>/application_add_note/", views.application_add_note, name="application_add_note"),
|
||||||
|
path("note/<slug:slug>/interview_add_note/", views.interview_add_note, name="interview_add_note"),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -33,7 +33,8 @@ from .forms import (
|
|||||||
PasswordResetForm,
|
PasswordResetForm,
|
||||||
StaffAssignmentForm,
|
StaffAssignmentForm,
|
||||||
RemoteInterviewForm,
|
RemoteInterviewForm,
|
||||||
OnsiteInterviewForm
|
OnsiteInterviewForm,
|
||||||
|
BulkInterviewTemplateForm
|
||||||
)
|
)
|
||||||
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
|
||||||
@ -132,7 +133,8 @@ from .models import (
|
|||||||
Source,
|
Source,
|
||||||
Message,
|
Message,
|
||||||
Document,
|
Document,
|
||||||
Interview
|
Interview,
|
||||||
|
BulkInterviewTemplate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1463,323 +1465,293 @@ def form_submission_details(request, template_id, slug):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# def _handle_get_request(request, slug, job):
|
def _handle_get_request(request, slug, job):
|
||||||
# """
|
"""
|
||||||
# Handles GET requests, setting up forms and restoring candidate selections
|
Handles GET requests, setting up forms and restoring candidate selections
|
||||||
# from the session for persistence.
|
from the session for persistence.
|
||||||
# """
|
"""
|
||||||
# SESSION_KEY = f"schedule_candidate_ids_{slug}"
|
SESSION_KEY = f"schedule_candidate_ids_{slug}"
|
||||||
|
|
||||||
# form = BulkInterviewTemplateForm(slug=slug)
|
form = BulkInterviewTemplateForm(slug=slug)
|
||||||
# # break_formset = BreakTimeFormSet(prefix='breaktime')
|
# break_formset = BreakTimeFormSet(prefix='breaktime')
|
||||||
|
|
||||||
# selected_ids = []
|
selected_ids = []
|
||||||
|
|
||||||
# # 1. Capture IDs from HTMX request and store in session (when first clicked)
|
# 1. Capture IDs from HTMX request and store in session (when first clicked)
|
||||||
# if "HX-Request" in request.headers:
|
if "HX-Request" in request.headers:
|
||||||
# candidate_ids = request.GET.getlist("candidate_ids")
|
candidate_ids = request.GET.getlist("candidate_ids")
|
||||||
|
|
||||||
# if candidate_ids:
|
if candidate_ids:
|
||||||
# request.session[SESSION_KEY] = candidate_ids
|
request.session[SESSION_KEY] = candidate_ids
|
||||||
# selected_ids = candidate_ids
|
selected_ids = candidate_ids
|
||||||
|
|
||||||
# # 2. Restore IDs from session (on refresh or navigation)
|
# 2. Restore IDs from session (on refresh or navigation)
|
||||||
# if not selected_ids:
|
if not selected_ids:
|
||||||
# selected_ids = request.session.get(SESSION_KEY, [])
|
selected_ids = request.session.get(SESSION_KEY, [])
|
||||||
|
|
||||||
# # 3. Use the list of IDs to initialize the form
|
# 3. Use the list of IDs to initialize the form
|
||||||
# if selected_ids:
|
if selected_ids:
|
||||||
# candidates_to_load = Application.objects.filter(pk__in=selected_ids)
|
candidates_to_load = Application.objects.filter(pk__in=selected_ids)
|
||||||
# print(candidates_to_load)
|
form.initial["applications"] = candidates_to_load
|
||||||
# form.initial["applications"] = candidates_to_load
|
|
||||||
|
|
||||||
# return render(
|
return render(
|
||||||
# request,
|
request,
|
||||||
# "interviews/schedule_interviews.html",
|
"interviews/schedule_interviews.html",
|
||||||
# {"form": form, "job": job},
|
{"form": form, "job": job},
|
||||||
# )
|
)
|
||||||
|
|
||||||
|
|
||||||
#TODO:MAIN FUNCTIONS
|
|
||||||
# def _handle_preview_submission(request, slug, job):
|
|
||||||
# """
|
|
||||||
# Handles the initial POST request (Preview Schedule).
|
|
||||||
# Validates forms, calculates slots, saves data to session, and renders preview.
|
|
||||||
# """
|
|
||||||
# SESSION_DATA_KEY = "interview_schedule_data"
|
|
||||||
# form = BulkInterviewTemplateForm(slug, request.POST)
|
|
||||||
# # break_formset = BreakTimeFormSet(request.POST,prefix='breaktime')
|
|
||||||
|
|
||||||
# if form.is_valid():
|
|
||||||
# # Get the form data
|
|
||||||
# applications = form.cleaned_data["applications"]
|
|
||||||
# start_date = form.cleaned_data["start_date"]
|
|
||||||
# end_date = form.cleaned_data["end_date"]
|
|
||||||
# working_days = form.cleaned_data["working_days"]
|
|
||||||
# start_time = form.cleaned_data["start_time"]
|
|
||||||
# end_time = form.cleaned_data["end_time"]
|
|
||||||
# interview_duration = form.cleaned_data["interview_duration"]
|
|
||||||
# buffer_time = form.cleaned_data["buffer_time"]
|
|
||||||
# break_start_time = form.cleaned_data["break_start_time"]
|
|
||||||
# break_end_time = form.cleaned_data["break_end_time"]
|
|
||||||
# schedule_interview_type=form.cleaned_data["schedule_interview_type"]
|
|
||||||
# # Process break times
|
|
||||||
# # breaks = []
|
|
||||||
# # for break_form in break_formset:
|
|
||||||
# # print(break_form.cleaned_data)
|
|
||||||
# # if break_form.cleaned_data and not break_form.cleaned_data.get("DELETE"):
|
|
||||||
# # breaks.append(
|
|
||||||
# # {
|
|
||||||
# # "start_time": break_form.cleaned_data["start_time"].strftime("%H:%M:%S"),
|
|
||||||
# # "end_time": break_form.cleaned_data["end_time"].strftime("%H:%M:%S"),
|
|
||||||
# # }
|
|
||||||
# # )
|
|
||||||
|
|
||||||
# # Create a temporary schedule object (not saved to DB)
|
|
||||||
# temp_schedule = BulkInterviewTemplate(
|
|
||||||
# job=job,
|
|
||||||
# start_date=start_date,
|
|
||||||
# end_date=end_date,
|
|
||||||
# working_days=working_days,
|
|
||||||
# start_time=start_time,
|
|
||||||
# end_time=end_time,
|
|
||||||
# interview_duration=interview_duration,
|
|
||||||
# buffer_time=buffer_time or 5,
|
|
||||||
# break_start_time=break_start_time or None,
|
|
||||||
# break_end_time=break_end_time or None,
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # Get available slots (temp_breaks logic moved into get_available_time_slots if needed)
|
|
||||||
# available_slots = get_available_time_slots(temp_schedule)
|
|
||||||
|
|
||||||
# if len(available_slots) < len(applications):
|
|
||||||
# messages.error(
|
|
||||||
# request,
|
|
||||||
# f"Not enough available slots. Required: {len(applications)}, Available: {len(available_slots)}",
|
|
||||||
# )
|
|
||||||
# return render(
|
|
||||||
# request,
|
|
||||||
# "interviews/schedule_interviews.html",
|
|
||||||
# {"form": form, "job": job},
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # Create a preview schedule
|
|
||||||
# preview_schedule = []
|
|
||||||
# for i, application in enumerate(applications):
|
|
||||||
# slot = available_slots[i]
|
|
||||||
# preview_schedule.append(
|
|
||||||
# {"application": application, "date": slot["date"], "time": slot["time"]}
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # Save the form data to session for later use
|
|
||||||
# schedule_data = {
|
|
||||||
# "start_date": start_date.isoformat(),
|
|
||||||
# "end_date": end_date.isoformat(),
|
|
||||||
# "working_days": working_days,
|
|
||||||
# "start_time": start_time.isoformat(),
|
|
||||||
# "end_time": end_time.isoformat(),
|
|
||||||
# "interview_duration": interview_duration,
|
|
||||||
# "buffer_time": buffer_time,
|
|
||||||
# "break_start_time": break_start_time.isoformat() if break_start_time else None,
|
|
||||||
# "break_end_time": break_end_time.isoformat() if break_end_time else None,
|
|
||||||
# "candidate_ids": [c.id for c in applications],
|
|
||||||
# "schedule_interview_type":schedule_interview_type
|
|
||||||
|
|
||||||
# }
|
|
||||||
# request.session[SESSION_DATA_KEY] = schedule_data
|
|
||||||
|
|
||||||
# # Render the preview page
|
|
||||||
# return render(
|
|
||||||
# request,
|
|
||||||
# "interviews/preview_schedule.html",
|
|
||||||
# {
|
|
||||||
# "job": job,
|
|
||||||
# "schedule": preview_schedule,
|
|
||||||
# "start_date": start_date,
|
|
||||||
# "end_date": end_date,
|
|
||||||
# "working_days": working_days,
|
|
||||||
# "start_time": start_time,
|
|
||||||
# "end_time": end_time,
|
|
||||||
# "break_start_time": break_start_time,
|
|
||||||
# "break_end_time": break_end_time,
|
|
||||||
# "interview_duration": interview_duration,
|
|
||||||
# "buffer_time": buffer_time,
|
|
||||||
# "schedule_interview_type":schedule_interview_type,
|
|
||||||
# "form":OnsiteLocationForm()
|
|
||||||
# },
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# # Re-render the form if validation fails
|
|
||||||
# return render(
|
|
||||||
# request,
|
|
||||||
# "interviews/schedule_interviews.html",
|
|
||||||
# {"form": form, "job": job},
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
# def _handle_confirm_schedule(request, slug, job):
|
|
||||||
# """
|
|
||||||
# Handles the final POST request (Confirm Schedule).
|
|
||||||
# Creates the main schedule record and queues individual interviews asynchronously.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# SESSION_DATA_KEY = "interview_schedule_data"
|
|
||||||
# SESSION_ID_KEY = f"schedule_candidate_ids_{slug}"
|
|
||||||
|
|
||||||
# # 1. Get schedule data from session
|
|
||||||
# schedule_data = request.session.get(SESSION_DATA_KEY)
|
|
||||||
|
|
||||||
# if not schedule_data:
|
|
||||||
# messages.error(request, "Session expired. Please try again.")
|
|
||||||
# return redirect("schedule_interviews", slug=slug)
|
|
||||||
|
|
||||||
# # 2. Create the Interview Schedule (Parent Record)
|
|
||||||
# try:
|
|
||||||
# # Handle break times: If they exist, convert them; otherwise, pass None.
|
|
||||||
# break_start = schedule_data.get("break_start_time")
|
|
||||||
# break_end = schedule_data.get("break_end_time")
|
|
||||||
|
|
||||||
# schedule = BulkInterviewTemplate.objects.create(
|
|
||||||
# job=job,
|
|
||||||
# created_by=request.user,
|
|
||||||
# start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
|
|
||||||
# end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
|
|
||||||
# working_days=schedule_data["working_days"],
|
|
||||||
# start_time=time.fromisoformat(schedule_data["start_time"]),
|
|
||||||
# end_time=time.fromisoformat(schedule_data["end_time"]),
|
|
||||||
# interview_duration=schedule_data["interview_duration"],
|
|
||||||
# buffer_time=schedule_data["buffer_time"],
|
|
||||||
# # Convert time strings to time objects only if they exist and handle None gracefully
|
|
||||||
# break_start_time=time.fromisoformat(break_start) if break_start else None,
|
|
||||||
# break_end_time=time.fromisoformat(break_end) if break_end else None,
|
|
||||||
# schedule_interview_type=schedule_data.get("schedule_interview_type")
|
|
||||||
# )
|
|
||||||
# except Exception as e:
|
|
||||||
# # Clear data on failure to prevent stale data causing repeated errors
|
|
||||||
# messages.error(request, f"Error creating schedule: {e}")
|
|
||||||
# if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
|
||||||
# if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
|
||||||
# return redirect("schedule_interviews", slug=slug)
|
|
||||||
|
|
||||||
# # 3. Setup candidates and get slots
|
|
||||||
# candidates = Application.objects.filter(id__in=schedule_data["candidate_ids"])
|
|
||||||
# schedule.applications.set(candidates)
|
|
||||||
# available_slots = get_available_time_slots(schedule)
|
|
||||||
|
|
||||||
# # 4. Handle Remote/Onsite logic
|
|
||||||
# if schedule_data.get("schedule_interview_type") == 'Remote':
|
|
||||||
# # ... (Remote logic remains unchanged)
|
|
||||||
# queued_count = 0
|
|
||||||
# for i, candidate in enumerate(candidates):
|
|
||||||
# if i < len(available_slots):
|
|
||||||
# slot = available_slots[i]
|
|
||||||
|
|
||||||
# async_task(
|
|
||||||
# "recruitment.tasks.create_interview_and_meeting",
|
|
||||||
# candidate.pk, job.pk, schedule.pk, slot["date"], slot["time"], schedule.interview_duration,
|
|
||||||
# )
|
|
||||||
# queued_count += 1
|
|
||||||
|
|
||||||
# messages.success(
|
|
||||||
# request,
|
|
||||||
# f"Schedule successfully created. Queued {queued_count} interviews to be booked asynchronously. Check back in a moment!",
|
|
||||||
# )
|
|
||||||
|
|
||||||
# if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
|
||||||
# if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
|
||||||
|
|
||||||
# return redirect("job_detail", slug=slug)
|
|
||||||
|
|
||||||
# elif schedule_data.get("schedule_interview_type") == 'Onsite':
|
|
||||||
# print("inside...")
|
|
||||||
|
|
||||||
# if request.method == 'POST':
|
|
||||||
# form = OnsiteLocationForm(request.POST)
|
|
||||||
|
|
||||||
# if form.is_valid():
|
|
||||||
|
|
||||||
# if not available_slots:
|
|
||||||
# messages.error(request, "No available slots found for the selected schedule range.")
|
|
||||||
# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job})
|
|
||||||
|
|
||||||
# # Extract common location data from the form
|
|
||||||
# physical_address = form.cleaned_data['physical_address']
|
|
||||||
# room_number = form.cleaned_data['room_number']
|
|
||||||
# topic=form.cleaned_data['topic']
|
|
||||||
|
|
||||||
|
|
||||||
# try:
|
|
||||||
# # 1. Iterate over candidates and create a NEW Location object for EACH
|
|
||||||
# for i, candidate in enumerate(candidates):
|
|
||||||
# if i < len(available_slots):
|
|
||||||
# slot = available_slots[i]
|
|
||||||
|
|
||||||
|
|
||||||
# location_start_dt = datetime.combine(slot['date'], schedule.start_time)
|
|
||||||
|
|
||||||
# # --- CORE FIX: Create a NEW Location object inside the loop ---
|
|
||||||
# onsite_location = OnsiteLocationDetails.objects.create(
|
|
||||||
# start_time=location_start_dt,
|
|
||||||
# duration=schedule.interview_duration,
|
|
||||||
# physical_address=physical_address,
|
|
||||||
# room_number=room_number,
|
|
||||||
# location_type="Onsite",
|
|
||||||
# topic=topic
|
|
||||||
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # 2. Create the ScheduledInterview, linking the unique location
|
|
||||||
# ScheduledInterview.objects.create(
|
|
||||||
# application=candidate,
|
|
||||||
# job=job,
|
|
||||||
# schedule=schedule,
|
|
||||||
# interview_date=slot['date'],
|
|
||||||
# interview_time=slot['time'],
|
|
||||||
# interview_location=onsite_location,
|
|
||||||
# )
|
|
||||||
|
|
||||||
# messages.success(
|
|
||||||
# request,
|
|
||||||
# f"Onsite schedule interviews created successfully for {len(candidates)} candidates."
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # Clear session data keys upon successful completion
|
|
||||||
# if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
|
||||||
# if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
|
||||||
|
|
||||||
# return redirect('job_detail', slug=job.slug)
|
|
||||||
|
|
||||||
# except Exception as e:
|
|
||||||
# messages.error(request, f"Error creating onsite location/interviews: {e}")
|
|
||||||
# # On failure, re-render the form with the error and ensure 'job' is present
|
|
||||||
# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job})
|
|
||||||
|
|
||||||
# else:
|
|
||||||
# # Form is invalid, re-render with errors
|
|
||||||
# # Ensure 'job' is passed to prevent NoReverseMatch
|
|
||||||
# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job})
|
|
||||||
|
|
||||||
# else:
|
|
||||||
# # For a GET request
|
|
||||||
# form = OnsiteLocationForm()
|
|
||||||
|
|
||||||
# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# def schedule_interviews_view(request, slug):
|
def _handle_preview_submission(request, slug, job):
|
||||||
# job = get_object_or_404(JobPosting, slug=slug)
|
"""
|
||||||
# if request.method == "POST":
|
Handles the initial POST request (Preview Schedule).
|
||||||
# # return _handle_confirm_schedule(request, slug, job)
|
Validates forms, calculates slots, saves data to session, and renders preview.
|
||||||
# return _handle_preview_submission(request, slug, job)
|
"""
|
||||||
# else:
|
SESSION_DATA_KEY = "interview_schedule_data"
|
||||||
# return _handle_get_request(request, slug, job)
|
form = BulkInterviewTemplateForm(slug, request.POST)
|
||||||
|
# break_formset = BreakTimeFormSet(request.POST,prefix='breaktime')
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
# Get the form data
|
||||||
|
applications = form.cleaned_data["applications"]
|
||||||
|
start_date = form.cleaned_data["start_date"]
|
||||||
|
end_date = form.cleaned_data["end_date"]
|
||||||
|
working_days = form.cleaned_data["working_days"]
|
||||||
|
start_time = form.cleaned_data["start_time"]
|
||||||
|
end_time = form.cleaned_data["end_time"]
|
||||||
|
interview_duration = form.cleaned_data["interview_duration"]
|
||||||
|
buffer_time = form.cleaned_data["buffer_time"]
|
||||||
|
break_start_time = form.cleaned_data["break_start_time"]
|
||||||
|
break_end_time = form.cleaned_data["break_end_time"]
|
||||||
|
schedule_interview_type=form.cleaned_data["schedule_interview_type"]
|
||||||
|
physical_address=form.cleaned_data["physical_address"]
|
||||||
|
# Process break times
|
||||||
|
# breaks = []
|
||||||
|
# for break_form in break_formset:
|
||||||
|
# print(break_form.cleaned_data)
|
||||||
|
# if break_form.cleaned_data and not break_form.cleaned_data.get("DELETE"):
|
||||||
|
# breaks.append(
|
||||||
|
# {
|
||||||
|
# "start_time": break_form.cleaned_data["start_time"].strftime("%H:%M:%S"),
|
||||||
|
# "end_time": break_form.cleaned_data["end_time"].strftime("%H:%M:%S"),
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
|
||||||
|
# Create a temporary schedule object (not saved to DB)
|
||||||
|
temp_schedule = BulkInterviewTemplate(
|
||||||
|
job=job,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
working_days=working_days,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
interview_duration=interview_duration,
|
||||||
|
buffer_time=buffer_time or 5,
|
||||||
|
break_start_time=break_start_time or None,
|
||||||
|
break_end_time=break_end_time or None,
|
||||||
|
schedule_interview_type=schedule_interview_type,
|
||||||
|
physical_address=physical_address
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get available slots (temp_breaks logic moved into get_available_time_slots if needed)
|
||||||
|
available_slots = get_available_time_slots(temp_schedule)
|
||||||
|
|
||||||
|
if len(available_slots) < len(applications):
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
f"Not enough available slots. Required: {len(applications)}, Available: {len(available_slots)}",
|
||||||
|
)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"interviews/schedule_interviews.html",
|
||||||
|
{"form": form, "job": job},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a preview schedule
|
||||||
|
preview_schedule = []
|
||||||
|
for i, application in enumerate(applications):
|
||||||
|
slot = available_slots[i]
|
||||||
|
preview_schedule.append(
|
||||||
|
{"application": application, "date": slot["date"], "time": slot["time"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save the form data to session for later use
|
||||||
|
schedule_data = {
|
||||||
|
"start_date": start_date.isoformat(),
|
||||||
|
"end_date": end_date.isoformat(),
|
||||||
|
"working_days": working_days,
|
||||||
|
"start_time": start_time.isoformat(),
|
||||||
|
"end_time": end_time.isoformat(),
|
||||||
|
"interview_duration": interview_duration,
|
||||||
|
"buffer_time": buffer_time,
|
||||||
|
"break_start_time": break_start_time.isoformat() if break_start_time else None,
|
||||||
|
"break_end_time": break_end_time.isoformat() if break_end_time else None,
|
||||||
|
"candidate_ids": [c.id for c in applications],
|
||||||
|
"schedule_interview_type":schedule_interview_type,
|
||||||
|
"physical_address":physical_address,
|
||||||
|
"topic":form.cleaned_data.get("topic"),
|
||||||
|
|
||||||
|
}
|
||||||
|
request.session[SESSION_DATA_KEY] = schedule_data
|
||||||
|
|
||||||
|
# Render the preview page
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"interviews/preview_schedule.html",
|
||||||
|
{
|
||||||
|
"job": job,
|
||||||
|
"schedule": preview_schedule,
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date,
|
||||||
|
"working_days": working_days,
|
||||||
|
"start_time": start_time,
|
||||||
|
"end_time": end_time,
|
||||||
|
"break_start_time": break_start_time,
|
||||||
|
"break_end_time": break_end_time,
|
||||||
|
"interview_duration": interview_duration,
|
||||||
|
"buffer_time": buffer_time,
|
||||||
|
# "schedule_interview_type":schedule_interview_type,
|
||||||
|
# "form":OnsiteLocationForm()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Re-render the form if validation fails
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"interviews/schedule_interviews.html",
|
||||||
|
{"form": form, "job": job},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# def confirm_schedule_interviews_view(request, slug):
|
def _handle_confirm_schedule(request, slug, job):
|
||||||
# job = get_object_or_404(JobPosting, slug=slug)
|
"""
|
||||||
# if request.method == "POST":
|
Handles the final POST request (Confirm Schedule).
|
||||||
# return _handle_confirm_schedule(request, slug, job)
|
Creates the main schedule record and queues individual interviews asynchronously.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SESSION_DATA_KEY = "interview_schedule_data"
|
||||||
|
SESSION_ID_KEY = f"schedule_candidate_ids_{slug}"
|
||||||
|
|
||||||
|
# 1. Get schedule data from session
|
||||||
|
schedule_data = request.session.get(SESSION_DATA_KEY)
|
||||||
|
|
||||||
|
if not schedule_data:
|
||||||
|
messages.error(request, "Session expired. Please try again.")
|
||||||
|
return redirect("schedule_interviews", slug=slug)
|
||||||
|
|
||||||
|
# 2. Create the Interview Schedule (Parent Record)
|
||||||
|
try:
|
||||||
|
# Handle break times: If they exist, convert them; otherwise, pass None.
|
||||||
|
break_start = schedule_data.get("break_start_time")
|
||||||
|
break_end = schedule_data.get("break_end_time")
|
||||||
|
|
||||||
|
schedule = BulkInterviewTemplate.objects.create(
|
||||||
|
job=job,
|
||||||
|
created_by=request.user,
|
||||||
|
start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
|
||||||
|
end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
|
||||||
|
working_days=schedule_data["working_days"],
|
||||||
|
start_time=time.fromisoformat(schedule_data["start_time"]),
|
||||||
|
end_time=time.fromisoformat(schedule_data["end_time"]),
|
||||||
|
interview_duration=schedule_data["interview_duration"],
|
||||||
|
buffer_time=schedule_data["buffer_time"],
|
||||||
|
break_start_time=time.fromisoformat(break_start) if break_start else None,
|
||||||
|
break_end_time=time.fromisoformat(break_end) if break_end else None,
|
||||||
|
schedule_interview_type=schedule_data.get("schedule_interview_type"),
|
||||||
|
physical_address=schedule_data.get("physical_address"),
|
||||||
|
topic=schedule_data.get("topic"),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Clear data on failure to prevent stale data causing repeated errors
|
||||||
|
messages.error(request, f"Error creating schedule: {e}")
|
||||||
|
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||||
|
if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||||
|
return redirect("schedule_interviews", slug=slug)
|
||||||
|
|
||||||
|
applications = Application.objects.filter(id__in=schedule_data["candidate_ids"])
|
||||||
|
schedule.applications.set(applications)
|
||||||
|
available_slots = get_available_time_slots(schedule)
|
||||||
|
|
||||||
|
if schedule_data.get("schedule_interview_type") == 'Remote':
|
||||||
|
queued_count = 0
|
||||||
|
for i, application in enumerate(applications):
|
||||||
|
if i < len(available_slots):
|
||||||
|
slot = available_slots[i]
|
||||||
|
async_task(
|
||||||
|
"recruitment.tasks.create_interview_and_meeting",
|
||||||
|
application.pk, job.pk, schedule.pk, slot["date"], slot["time"], schedule.interview_duration,
|
||||||
|
)
|
||||||
|
queued_count += 1
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"Schedule successfully created. Queued {queued_count} interviews to be booked asynchronously. Check back in a moment!",
|
||||||
|
)
|
||||||
|
|
||||||
|
if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||||
|
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||||
|
|
||||||
|
return redirect("job_detail", slug=slug)
|
||||||
|
|
||||||
|
elif schedule_data.get("schedule_interview_type") == 'Onsite':
|
||||||
|
try:
|
||||||
|
for i, application in enumerate(applications):
|
||||||
|
if i < len(available_slots):
|
||||||
|
slot = available_slots[i]
|
||||||
|
|
||||||
|
start_dt = datetime.combine(slot['date'], schedule.start_time)
|
||||||
|
|
||||||
|
interview = Interview.objects.create(
|
||||||
|
topic=schedule.topic,
|
||||||
|
start_time=start_dt,
|
||||||
|
duration=schedule.interview_duration,
|
||||||
|
location_type="Onsite",
|
||||||
|
physical_address=schedule.physical_address,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Create the ScheduledInterview, linking the unique location
|
||||||
|
ScheduledInterview.objects.create(
|
||||||
|
application=application,
|
||||||
|
job=job,
|
||||||
|
schedule=schedule,
|
||||||
|
interview_date=slot['date'],
|
||||||
|
interview_time=slot['time'],
|
||||||
|
interview=interview
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"created successfully for {len(applications)} application."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clear session data keys upon successful completion
|
||||||
|
if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||||
|
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||||
|
|
||||||
|
return redirect('job_detail', slug=job.slug)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f"Error creating onsite interviews: {e}")
|
||||||
|
return redirect("schedule_interviews", slug=slug)
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_interviews_view(request, slug):
|
||||||
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
|
if request.method == "POST":
|
||||||
|
# return _handle_confirm_schedule(request, slug, job)
|
||||||
|
return _handle_preview_submission(request, slug, job)
|
||||||
|
else:
|
||||||
|
# if request.session.get("interview_schedule_data"):
|
||||||
|
print(request.session.get("interview_schedule_data"))
|
||||||
|
return _handle_get_request(request, slug, job)
|
||||||
|
# return redirect("applications_interview_view", slug=slug)
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_schedule_interviews_view(request, slug):
|
||||||
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
|
if request.method == "POST":
|
||||||
|
# print(request.session['interview_schedule_data'])
|
||||||
|
return _handle_confirm_schedule(request, slug, job)
|
||||||
|
|
||||||
|
|
||||||
@staff_user_required
|
@staff_user_required
|
||||||
@ -6583,3 +6555,57 @@ def interview_detail(request, slug):
|
|||||||
# messages.error(request, f"Failed to send invitation emails: {str(e)}")
|
# messages.error(request, f"Failed to send invitation emails: {str(e)}")
|
||||||
|
|
||||||
# return redirect('meeting_details', slug=slug)
|
# return redirect('meeting_details', slug=slug)
|
||||||
|
|
||||||
|
def application_add_note(request, slug):
|
||||||
|
from .models import Note
|
||||||
|
from .forms import NoteForm
|
||||||
|
|
||||||
|
application = get_object_or_404(Application, slug=slug)
|
||||||
|
notes = Note.objects.filter(application=application).order_by('-created_at')
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = NoteForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
# messages.success(request, "Note added successfully.")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Note content cannot be empty.")
|
||||||
|
|
||||||
|
return render(request, 'recruitment/partials/note_form.html', {'notes':notes})
|
||||||
|
else:
|
||||||
|
form = NoteForm()
|
||||||
|
|
||||||
|
form.initial['application'] = application
|
||||||
|
form.fields['application'].widget = HiddenInput()
|
||||||
|
form.fields['interview'].widget = HiddenInput()
|
||||||
|
form.initial['author'] = request.user
|
||||||
|
form.fields['author'].widget = HiddenInput()
|
||||||
|
url = reverse('application_add_note', kwargs={'slug':slug})
|
||||||
|
notes = Note.objects.filter(application=application).order_by('-created_at')
|
||||||
|
return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':application,'notes':notes,'url':url})
|
||||||
|
|
||||||
|
def interview_add_note(request, slug):
|
||||||
|
from .models import Note
|
||||||
|
from .forms import NoteForm
|
||||||
|
|
||||||
|
interview = get_object_or_404(Interview, slug=slug)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = NoteForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, "Note added successfully.")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Note content cannot be empty.")
|
||||||
|
|
||||||
|
return redirect('interview_detail', slug=slug)
|
||||||
|
else:
|
||||||
|
form = NoteForm()
|
||||||
|
|
||||||
|
form.initial['interview'] = interview
|
||||||
|
form.fields['interview'].widget = HiddenInput()
|
||||||
|
form.fields['application'].widget = HiddenInput()
|
||||||
|
form.initial['author'] = request.user
|
||||||
|
form.fields['author'].widget = HiddenInput()
|
||||||
|
|
||||||
|
return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':interview,'notes':interview.notes.all()})
|
||||||
|
|||||||
@ -444,11 +444,25 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function remove_form_loader(){
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
forms.forEach(form => {
|
||||||
|
form.addEventListener('htmx:afterRequest', function(evt) {
|
||||||
|
const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.classList.remove('loading');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//form_loader();
|
//form_loader();
|
||||||
|
|
||||||
try{
|
try{
|
||||||
document.addEventListener('htmx:afterSwap', form_loader);
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||||
|
remove_form_loader();
|
||||||
|
});
|
||||||
}catch(e){
|
}catch(e){
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -170,11 +170,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if schedule_interview_type == "Onsite" %}
|
|
||||||
<button type="submit" name="confirm_schedule" class="btn btn-teal-primary px-4" data-bs-toggle="modal" data-bs-target="#interviewDetailsModal" data-placement="top">
|
|
||||||
<i class="fas fa-check me-2"></i> {% trans "Confirm Schedule" %}
|
|
||||||
</button>
|
|
||||||
{% else %}
|
|
||||||
<form method="post" action="{% url 'confirm_schedule_interviews_view' slug=job.slug %}" class="mt-4 d-flex justify-content-end gap-3">
|
<form method="post" action="{% url 'confirm_schedule_interviews_view' slug=job.slug %}" class="mt-4 d-flex justify-content-end gap-3">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<a href="{% url 'schedule_interviews' slug=job.slug %}" class="btn btn-secondary px-4">
|
<a href="{% url 'schedule_interviews' slug=job.slug %}" class="btn btn-secondary px-4">
|
||||||
@ -184,7 +180,6 @@
|
|||||||
<i class="fas fa-check me-2"></i> {% trans "Confirm Schedule" %}
|
<i class="fas fa-check me-2"></i> {% trans "Confirm Schedule" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -196,20 +191,20 @@
|
|||||||
<h5 class="modal-title" id="interviewDetailsModalLabel">{% trans "Interview Details" %}</h5>
|
<h5 class="modal-title" id="interviewDetailsModalLabel">{% trans "Interview Details" %}</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body"> <form method="post" action="{% url 'confirm_schedule_interviews_view' job.slug %}" enctype="multipart/form-data" id="onsite-form">
|
{% comment %} <div class="modal-body"> <form method="post" action="{% url 'confirm_schedule_interviews_view' job.slug %}" enctype="multipart/form-data" id="onsite-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{# Renders the single 'location' field using the crispy filter #}
|
{# Renders the single 'location' field using the crispy filter #}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
|
|
||||||
</form>
|
</form> {% endcomment %}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
|
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
|
||||||
<a href="{% url 'list_meetings' %}" class="btn btn-secondary me-2">
|
<a href="#" class="btn btn-secondary me-2">
|
||||||
<i class="fas fa-times me-1"></i> Close
|
<i class="fas fa-times me-1"></i> Close
|
||||||
</a>
|
</a>
|
||||||
<button type="submit" class="btn btn-primary" form="onsite-form">
|
<button type="submit" class="btn btn-primary" form="onsite-form">
|
||||||
<i class="fas fa-save me-1"></i> Save Location
|
<i class="fas fa-save me-1"></i> Save Location
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static i18n %}
|
{% load static i18n %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
{% block title %}Bulk Interview Scheduling - {{ job.title }} - ATS{% endblock %}
|
{% block title %}Bulk Interview Scheduling - {{ job.title }} - ATS{% endblock %}
|
||||||
|
|
||||||
@ -125,7 +126,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h5 class="section-header">{% trans "Select Candidates" %}</h5>
|
<h5 class="section-header">{% trans "Select Candidates" %}</h5>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.candidates.id_for_label }}">
|
<label for="{{ form.candidates.id_for_label }}">
|
||||||
{% trans "Candidates to Schedule (Hold Ctrl/Cmd to select multiple)" %}
|
{% trans "Candidates to Schedule (Hold Ctrl/Cmd to select multiple)" %}
|
||||||
@ -141,14 +141,19 @@
|
|||||||
<h5 class="section-header">{% trans "Schedule Details" %}</h5>
|
<h5 class="section-header">{% trans "Schedule Details" %}</h5>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label for="{{ form.topic.id_for_label }}">{% trans "Topic" %}</label>
|
||||||
|
{{ form.topic }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<label for="{{ form.schedule_interview_type.id_for_label }}">{% trans "Interview Type" %}</label>
|
<label for="{{ form.schedule_interview_type.id_for_label }}">{% trans "Interview Type" %}</label>
|
||||||
{{ form.schedule_interview_type }}
|
{{ form.schedule_interview_type }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -217,8 +222,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label for="{{ form.physical_address.id_for_label }}">{% trans "Physical Address" %}</label>
|
||||||
|
{{ form.physical_address }}
|
||||||
|
{% if form.physical_address.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.physical_address.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -278,7 +278,7 @@
|
|||||||
{% trans "To Offer" %}
|
{% trans "To Offer" %}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="btn btn-main-action btn-sm">
|
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
|
||||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -286,7 +286,7 @@
|
|||||||
{# Separator (Vertical Rule) - Aligns automatically at the bottom with align-items-end #}
|
{# Separator (Vertical Rule) - Aligns automatically at the bottom with align-items-end #}
|
||||||
<div class="vr" style="height: 28px;"></div>
|
<div class="vr" style="height: 28px;"></div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
<button id="emailBotton" type="button" class="btn btn-outline-primary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
hx-boost='true'
|
hx-boost='true'
|
||||||
data-bs-target="#emailModal"
|
data-bs-target="#emailModal"
|
||||||
@ -323,6 +323,9 @@
|
|||||||
<th scope="col" style="width: 28%;">
|
<th scope="col" style="width: 28%;">
|
||||||
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
|
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
|
||||||
</th>
|
</th>
|
||||||
|
<th scope="col" style="width: 10%;">
|
||||||
|
<i class="fas fa-file-alt me-1"></i> {% trans "Notes" %}
|
||||||
|
</th>
|
||||||
<th scope="col" style="width: 10%;">
|
<th scope="col" style="width: 10%;">
|
||||||
<i class="fas fa-cog me-1"></i> {% trans "Actions" %}
|
<i class="fas fa-cog me-1"></i> {% trans "Actions" %}
|
||||||
</th>
|
</th>
|
||||||
@ -394,6 +397,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</td>
|
</td>
|
||||||
|
<td><button type="button" class="btn btn-outline-primary btn-sm"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#noteModal"
|
||||||
|
hx-get="{% url 'application_add_note' application.slug %}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target=".notemodal">
|
||||||
|
<i class="fas fa-calendar-plus me-1"></i>
|
||||||
|
Add note
|
||||||
|
</button></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -462,51 +474,78 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include "recruitment/partials/note_modal.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||||
|
const changeStageButton = document.getElementById('changeStage');
|
||||||
|
const emailButton = document.getElementById('emailBotton');
|
||||||
|
const updateStatus = document.getElementById('update_status');
|
||||||
|
|
||||||
if (selectAllCheckbox) {
|
if (selectAllCheckbox) {
|
||||||
// Function to safely update header checkbox state
|
|
||||||
function updateSelectAllState() {
|
|
||||||
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
|
||||||
const totalCount = rowCheckboxes.length;
|
|
||||||
|
|
||||||
if (checkedCount === 0) {
|
// Function to safely update the header checkbox state
|
||||||
selectAllCheckbox.checked = false;
|
function updateSelectAllState() {
|
||||||
selectAllCheckbox.indeterminate = false;
|
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
||||||
} else if (checkedCount === totalCount) {
|
const totalCount = rowCheckboxes.length;
|
||||||
selectAllCheckbox.checked = true;
|
|
||||||
selectAllCheckbox.indeterminate = false;
|
if (checkedCount === 0) {
|
||||||
} else {
|
selectAllCheckbox.checked = false;
|
||||||
selectAllCheckbox.checked = false;
|
selectAllCheckbox.indeterminate = false;
|
||||||
selectAllCheckbox.indeterminate = true;
|
changeStageButton.disabled = true;
|
||||||
|
emailButton.disabled = true;
|
||||||
|
updateStatus.disabled = true;
|
||||||
|
} else if (checkedCount === totalCount) {
|
||||||
|
selectAllCheckbox.checked = true;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
changeStageButton.disabled = false;
|
||||||
|
emailButton.disabled = false;
|
||||||
|
updateStatus.disabled = false;
|
||||||
|
} else {
|
||||||
|
// Set to indeterminate state (partially checked)
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = true;
|
||||||
|
changeStageButton.disabled = false;
|
||||||
|
emailButton.disabled = false;
|
||||||
|
updateStatus.disabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Logic for 'Select All' checkbox (Clicking it updates all rows)
|
// 1. Logic for the 'Select All' checkbox (Clicking it updates all rows)
|
||||||
selectAllCheckbox.addEventListener('change', function () {
|
selectAllCheckbox.addEventListener('change', function () {
|
||||||
const isChecked = selectAllCheckbox.checked;
|
const isChecked = selectAllCheckbox.checked;
|
||||||
|
|
||||||
rowCheckboxes.forEach(checkbox => {
|
// Temporarily disable the change listener on rows to prevent cascading events
|
||||||
checkbox.checked = isChecked;
|
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
|
||||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
|
// Update all row checkboxes
|
||||||
|
rowCheckboxes.forEach(function (checkbox) {
|
||||||
|
checkbox.checked = isChecked;
|
||||||
|
|
||||||
|
// Dispatch event for the framework (data-bind-selections)
|
||||||
|
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();
|
updateSelectAllState();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
// 2. Logic to update 'Select All' state based on row checkboxes
|
|
||||||
rowCheckboxes.forEach(function (checkbox) {
|
|
||||||
checkbox.addEventListener('change', updateSelectAllState);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial check to set correct state on load
|
|
||||||
updateSelectAllState();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -225,11 +225,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Button #}
|
{# Button #}
|
||||||
<button type="submit" class="btn btn-main-action btn-sm">
|
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
|
||||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
<button id="emailBotton" type="button" class="btn btn-outline-primary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
hx-boost='true'
|
hx-boost='true'
|
||||||
data-bs-target="#emailModal"
|
data-bs-target="#emailModal"
|
||||||
@ -261,9 +261,10 @@
|
|||||||
<th style="width: 15%;">{% trans "Name" %}</th>
|
<th style="width: 15%;">{% trans "Name" %}</th>
|
||||||
<th style="width: 15%;">{% trans "Contact Info" %}</th>
|
<th style="width: 15%;">{% trans "Contact Info" %}</th>
|
||||||
<th style="width: 10%;" class="text-center">{% trans "AI Score" %}</th>
|
<th style="width: 10%;" class="text-center">{% trans "AI Score" %}</th>
|
||||||
<th style="width: 15%;">{% trans "Exam Date" %}</th>
|
<th style="width: 10%;">{% trans "Exam Date" %}</th>
|
||||||
<th style="width: 15%;">{% trans "Exam Score" %}</th>
|
<th style="width: 10%;">{% trans "Exam Score" %}</th>
|
||||||
<th style="width: 10%;" class="text-center">{% trans "Exam Results" %}</th>
|
<th style="width: 10%;" class="text-center">{% trans "Exam Results" %}</th>
|
||||||
|
<th style="width: 10%"> {% trans "Notes"%}</th>
|
||||||
<th style="width: 15%;">{% trans "Actions" %}</th>
|
<th style="width: 15%;">{% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -324,6 +325,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td><button type="button" class="btn btn-outline-primary btn-sm"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#noteModal"
|
||||||
|
hx-get="{% url 'application_add_note' application.slug %}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target=".notemodal">
|
||||||
|
<i class="fas fa-calendar-plus me-1"></i>
|
||||||
|
Add note
|
||||||
|
</button></td>
|
||||||
|
|
||||||
<td >
|
<td >
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
@ -395,14 +405,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include "recruitment/partials/note_modal.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||||
|
const changeStageButton = document.getElementById('changeStage');
|
||||||
|
const emailButton = document.getElementById('emailBotton');
|
||||||
|
const updateStatus = document.getElementById('update_status');
|
||||||
|
|
||||||
if (selectAllCheckbox) {
|
if (selectAllCheckbox) {
|
||||||
|
|
||||||
@ -414,13 +428,22 @@
|
|||||||
if (checkedCount === 0) {
|
if (checkedCount === 0) {
|
||||||
selectAllCheckbox.checked = false;
|
selectAllCheckbox.checked = false;
|
||||||
selectAllCheckbox.indeterminate = false;
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
changeStageButton.disabled = true;
|
||||||
|
emailButton.disabled = true;
|
||||||
|
updateStatus.disabled = true;
|
||||||
} else if (checkedCount === totalCount) {
|
} else if (checkedCount === totalCount) {
|
||||||
selectAllCheckbox.checked = true;
|
selectAllCheckbox.checked = true;
|
||||||
selectAllCheckbox.indeterminate = false;
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
changeStageButton.disabled = false;
|
||||||
|
emailButton.disabled = false;
|
||||||
|
updateStatus.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
// Set to indeterminate state (partially checked)
|
// Set to indeterminate state (partially checked)
|
||||||
selectAllCheckbox.checked = false;
|
selectAllCheckbox.checked = false;
|
||||||
selectAllCheckbox.indeterminate = true;
|
selectAllCheckbox.indeterminate = true;
|
||||||
|
changeStageButton.disabled = false;
|
||||||
|
emailButton.disabled = false;
|
||||||
|
updateStatus.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -218,7 +218,7 @@
|
|||||||
{% trans "To Exam" %}
|
{% trans "To Exam" %}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="btn btn-main-action btn-sm">
|
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
|
||||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -227,23 +227,23 @@
|
|||||||
<div class="vr" style="height: 28px;"></div>
|
<div class="vr" style="height: 28px;"></div>
|
||||||
|
|
||||||
{# Form 2: Schedule Interviews #}
|
{# Form 2: Schedule Interviews #}
|
||||||
<form hx-boost="true" hx-include="#application-form" action="#" method="get" class="action-group">
|
<form hx-boost="true" hx-include="#application-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
|
||||||
<button type="submit" class="btn btn-main-action btn-sm">
|
<button id="scheduleInterview" type="submit" class="btn btn-main-action btn-sm">
|
||||||
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
|
<i class="fas fa-calendar-plus me-1"></i> {% trans "Bulk Schedule Interviews" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="vr" style="height: 28px;"></div>
|
<div class="vr" style="height: 28px;"></div>
|
||||||
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-info btn-sm"
|
<button id="emailBotton" type="button" class="btn btn-outline-info btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
hx-boost='true'
|
hx-boost='true'
|
||||||
data-bs-target="#emailModal"
|
data-bs-target="#emailModal"
|
||||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||||
hx-target="#emailModalBody"
|
hx-target="#emailModalBody"
|
||||||
hx-include="#application-form"
|
hx-include="#application-form"
|
||||||
title="Email Participants">
|
title="Email Participants">
|
||||||
<i class="fas fa-envelope"></i>
|
<i class="fas fa-envelope"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -271,8 +271,9 @@
|
|||||||
<th style="width: 10%"><i class="fas fa-calendar me-1"></i> {% trans "Meeting Date" %}</th>
|
<th style="width: 10%"><i class="fas fa-calendar me-1"></i> {% trans "Meeting Date" %}</th>
|
||||||
<th style="width: 7%"><i class="fas fa-video me-1"></i> {% trans "Link" %}</th>
|
<th style="width: 7%"><i class="fas fa-video me-1"></i> {% trans "Link" %}</th>
|
||||||
<th style="width: 8%"><i class="fas fa-check-circle me-1"></i> {% trans "Meeting Status" %}</th> {% endcomment %}
|
<th style="width: 8%"><i class="fas fa-check-circle me-1"></i> {% trans "Meeting Status" %}</th> {% endcomment %}
|
||||||
<th style="width: 15%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview Result"%}</th>
|
<th style="width: 10%"><i class="fas fa-video me-1"></i> {% trans "Interviews"%}</th>
|
||||||
<th style="width: 15%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview List"%}</th>
|
<th style="width: 10%"><i class="fas fa-sticky-note me-1"></i> {% trans "Notes"%}</th>
|
||||||
|
<th style="width: 5%"><i class="fas fa-check-circle me-1"></i> {% trans "Result"%}</th>
|
||||||
<th style="width: 10%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
|
<th style="width: 10%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -353,6 +354,26 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</td> {% endcomment %}
|
</td> {% endcomment %}
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#candidateviewModal"
|
||||||
|
hx-get="{% url 'get_interview_list' application.slug %}"
|
||||||
|
hx-target="#candidateviewModalBody">
|
||||||
|
View
|
||||||
|
<i class="fas fa-list"></i>
|
||||||
|
{{candidate.get_interviews}}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td><button type="button" class="btn btn-outline-primary btn-sm"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#noteModal"
|
||||||
|
hx-get="{% url 'application_add_note' application.slug %}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target=".notemodal">
|
||||||
|
<i class="fas fa-calendar-plus me-1"></i>
|
||||||
|
Add note
|
||||||
|
</button></td>
|
||||||
<td class="text-center" id="interview-result-{{ application.pk }}">
|
<td class="text-center" id="interview-result-{{ application.pk }}">
|
||||||
{% if not application.interview_status %}
|
{% if not application.interview_status %}
|
||||||
<button type="button" class="btn btn-warning btn-sm"
|
<button type="button" class="btn btn-warning btn-sm"
|
||||||
@ -379,18 +400,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#candidateviewModal"
|
|
||||||
hx-get="{% url 'get_interview_list' application.slug %}"
|
|
||||||
hx-target="#candidateviewModalBody">
|
|
||||||
Interview List
|
|
||||||
<i class="fas fa-list"></i>
|
|
||||||
{{candidate.get_interviews}}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
{% if application.get_latest_meeting %}
|
{% if application.get_latest_meeting %}
|
||||||
{% if application.get_latest_meeting.location_type == 'Remote'%}
|
{% if application.get_latest_meeting.location_type == 'Remote'%}
|
||||||
|
|
||||||
@ -445,7 +454,6 @@
|
|||||||
<i class="fas fa-calendar-plus me-1"></i>
|
<i class="fas fa-calendar-plus me-1"></i>
|
||||||
Schedule Interview
|
Schedule Interview
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -504,6 +512,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% include "recruitment/partials/note_modal.html" %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
@ -511,6 +522,10 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||||
|
const changeStageButton = document.getElementById('changeStage');
|
||||||
|
const emailButton = document.getElementById('emailBotton');
|
||||||
|
const updateStatus = document.getElementById('update_status');
|
||||||
|
const scheduleInterviewButton = document.getElementById('scheduleInterview');
|
||||||
|
|
||||||
if (selectAllCheckbox) {
|
if (selectAllCheckbox) {
|
||||||
|
|
||||||
@ -522,13 +537,25 @@
|
|||||||
if (checkedCount === 0) {
|
if (checkedCount === 0) {
|
||||||
selectAllCheckbox.checked = false;
|
selectAllCheckbox.checked = false;
|
||||||
selectAllCheckbox.indeterminate = false;
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
changeStageButton.disabled = true;
|
||||||
|
emailButton.disabled = true;
|
||||||
|
updateStatus.disabled = true;
|
||||||
|
scheduleInterviewButton.disabled = true;
|
||||||
} else if (checkedCount === totalCount) {
|
} else if (checkedCount === totalCount) {
|
||||||
selectAllCheckbox.checked = true;
|
selectAllCheckbox.checked = true;
|
||||||
selectAllCheckbox.indeterminate = false;
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
changeStageButton.disabled = false;
|
||||||
|
emailButton.disabled = false;
|
||||||
|
updateStatus.disabled = false;
|
||||||
|
scheduleInterviewButton.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
// Set to indeterminate state (partially checked)
|
// Set to indeterminate state (partially checked)
|
||||||
selectAllCheckbox.checked = false;
|
selectAllCheckbox.checked = false;
|
||||||
selectAllCheckbox.indeterminate = true;
|
selectAllCheckbox.indeterminate = true;
|
||||||
|
changeStageButton.disabled = false;
|
||||||
|
emailButton.disabled = false;
|
||||||
|
updateStatus.disabled = false;
|
||||||
|
scheduleInterviewButton.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -220,26 +220,23 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
{# Button #}
|
{# Button #}
|
||||||
<button type="submit" class="btn btn-main-action btn-sm">
|
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
|
||||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
{# Separator (Vertical Rule) #}
|
{# Separator (Vertical Rule) #}
|
||||||
<div class="vr" style="height: 28px;"></div>
|
<div class="vr" style="height: 28px;"></div>
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
<button id="emailBotton" type="button" class="btn btn-outline-primary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
hx-boost='true'
|
hx-boost='true'
|
||||||
data-bs-target="#emailModal"
|
data-bs-target="#emailModal"
|
||||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||||
hx-target="#emailModalBody"
|
hx-target="#emailModalBody"
|
||||||
hx-include="#application-form"
|
hx-include="#application-form"
|
||||||
title="Email Participants">
|
title="Email Participants">
|
||||||
<i class="fas fa-envelope"></i>
|
<i class="fas fa-envelope"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -263,8 +260,9 @@
|
|||||||
<th style="width: 15%"><i class="fas fa-phone me-1"></i> {% trans "Contact" %}</th>
|
<th style="width: 15%"><i class="fas fa-phone me-1"></i> {% trans "Contact" %}</th>
|
||||||
<th class="text-center" style="width: 10%"><i class="fas fa-check-circle me-1"></i> {% trans "Offer" %}</th>
|
<th class="text-center" style="width: 10%"><i class="fas fa-check-circle me-1"></i> {% trans "Offer" %}</th>
|
||||||
<th scope="col" style="width: 30%;">
|
<th scope="col" style="width: 30%;">
|
||||||
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
|
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
|
||||||
</th>
|
</th>
|
||||||
|
<th style="width: 10%"><i class="fas fa-phone me-1"></i> {% trans "Notes" %}</th>
|
||||||
<th style="width: 5%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
|
<th style="width: 5%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -356,6 +354,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</td>
|
</td>
|
||||||
|
<td><button type="button" class="btn btn-outline-primary btn-sm"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#noteModal"
|
||||||
|
hx-get="{% url 'application_add_note' application.slug %}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target=".notemodal">
|
||||||
|
<i class="fas fa-calendar-plus me-1"></i>
|
||||||
|
Add note
|
||||||
|
</button></td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -423,6 +430,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include "recruitment/partials/note_modal.html" %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
@ -430,6 +439,9 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||||
|
const changeStageButton = document.getElementById('changeStage');
|
||||||
|
const emailButton = document.getElementById('emailBotton');
|
||||||
|
const updateStatus = document.getElementById('update_status');
|
||||||
|
|
||||||
if (selectAllCheckbox) {
|
if (selectAllCheckbox) {
|
||||||
|
|
||||||
@ -441,13 +453,22 @@
|
|||||||
if (checkedCount === 0) {
|
if (checkedCount === 0) {
|
||||||
selectAllCheckbox.checked = false;
|
selectAllCheckbox.checked = false;
|
||||||
selectAllCheckbox.indeterminate = false;
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
changeStageButton.disabled = true;
|
||||||
|
emailButton.disabled = true;
|
||||||
|
updateStatus.disabled = true;
|
||||||
} else if (checkedCount === totalCount) {
|
} else if (checkedCount === totalCount) {
|
||||||
selectAllCheckbox.checked = true;
|
selectAllCheckbox.checked = true;
|
||||||
selectAllCheckbox.indeterminate = false;
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
changeStageButton.disabled = false;
|
||||||
|
emailButton.disabled = false;
|
||||||
|
updateStatus.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
// Set to indeterminate state (partially checked)
|
// Set to indeterminate state (partially checked)
|
||||||
selectAllCheckbox.checked = false;
|
selectAllCheckbox.checked = false;
|
||||||
selectAllCheckbox.indeterminate = true;
|
selectAllCheckbox.indeterminate = true;
|
||||||
|
changeStageButton.disabled = false;
|
||||||
|
emailButton.disabled = false;
|
||||||
|
updateStatus.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -340,11 +340,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Button #}
|
{# Button #}
|
||||||
<button type="submit" class="btn btn-main-action btn-sm">
|
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
|
||||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
||||||
</button>
|
</button>
|
||||||
{# email button#}
|
{# email button#}
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
<button id="emailBotton" type="button" class="btn btn-outline-primary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
hx-boost='true'
|
hx-boost='true'
|
||||||
data-bs-target="#emailModal"
|
data-bs-target="#emailModal"
|
||||||
@ -374,28 +374,31 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 8%;">
|
<th scope="col" style="width: 13%;">
|
||||||
<i class="fas fa-user me-1"></i> {% trans "Name" %}
|
<i class="fas fa-user me-1"></i> {% trans "Name" %}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 10%;">
|
<th scope="col" style="width: 15%;">
|
||||||
<i class="fas fa-phone me-1"></i> {% trans "Contact Info" %}
|
<i class="fas fa-phone me-1"></i> {% trans "Contact Info" %}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 5%;">
|
<th scope="col" style="width: 5%;">
|
||||||
<i class="fas fa-graduation-cap me-1"></i> {% trans "GPA" %}
|
<i class="fas fa-graduation-cap me-1"></i> {% trans "GPA" %}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 6%;" class="text-center">
|
<th scope="col" style="width: 5%;" class="text-center">
|
||||||
<i class="fas fa-robot me-1"></i> {% trans "AI Score" %}
|
<i class="fas fa-robot me-1"></i> {% trans "AI Score" %}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 15%;" class="text-center">
|
<th scope="col" style="width: 10%;" class="text-center">
|
||||||
<i class="fas fa-robot me-1"></i> {% trans "Is Qualified?" %}
|
<i class="fas fa-robot me-1"></i> {% trans "Is Qualified?" %}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 10%;">
|
<th scope="col" style="width: 20%;">
|
||||||
<i class="fas fa-graduation-cap me-1"></i> {% trans "Professional Category" %}
|
<i class="fas fa-graduation-cap me-1"></i> {% trans "Professional Category" %}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 15%;">
|
<th scope="col" style="width: 15%;">
|
||||||
<i class="fas fa-graduation-cap me-1"></i> {% trans "Top 3 Skills" %}
|
<i class="fas fa-graduation-cap me-1"></i> {% trans "Top 3 Skills" %}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 10%;" class="text-center">
|
<th scope="col" style="width: 10%;" class="text-center">
|
||||||
|
<i class="fas fa-cog me-1"></i> {% trans "Note" %}
|
||||||
|
</th>
|
||||||
|
<th scope="col" style="width: 5%;" class="text-center">
|
||||||
<i class="fas fa-cog me-1"></i> {% trans "Actions" %}
|
<i class="fas fa-cog me-1"></i> {% trans "Actions" %}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -466,7 +469,15 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td><button type="button" class="btn btn-outline-primary btn-sm"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#noteModal"
|
||||||
|
hx-get="{% url 'application_add_note' application.slug %}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target=".notemodal">
|
||||||
|
<i class="fas fa-calendar-plus me-1"></i>
|
||||||
|
Add note
|
||||||
|
</button></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -536,6 +547,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include "recruitment/partials/note_modal.html" %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
@ -544,8 +557,12 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||||
|
const changeStageButton = document.getElementById('changeStage');
|
||||||
|
const emailButton = document.getElementById('emailBotton');
|
||||||
|
const updateStatus = document.getElementById('update_status');
|
||||||
|
|
||||||
if (selectAllCheckbox) {
|
if (selectAllCheckbox) {
|
||||||
|
|
||||||
// Function to safely update the header checkbox state
|
// Function to safely update the header checkbox state
|
||||||
function updateSelectAllState() {
|
function updateSelectAllState() {
|
||||||
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
||||||
@ -554,12 +571,22 @@
|
|||||||
if (checkedCount === 0) {
|
if (checkedCount === 0) {
|
||||||
selectAllCheckbox.checked = false;
|
selectAllCheckbox.checked = false;
|
||||||
selectAllCheckbox.indeterminate = false;
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
changeStageButton.disabled = true;
|
||||||
|
emailButton.disabled = true;
|
||||||
|
updateStatus.disabled = true;
|
||||||
} else if (checkedCount === totalCount) {
|
} else if (checkedCount === totalCount) {
|
||||||
selectAllCheckbox.checked = true;
|
selectAllCheckbox.checked = true;
|
||||||
selectAllCheckbox.indeterminate = false;
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
changeStageButton.disabled = false;
|
||||||
|
emailButton.disabled = false;
|
||||||
|
updateStatus.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
|
// Set to indeterminate state (partially checked)
|
||||||
selectAllCheckbox.checked = false;
|
selectAllCheckbox.checked = false;
|
||||||
selectAllCheckbox.indeterminate = true;
|
selectAllCheckbox.indeterminate = true;
|
||||||
|
changeStageButton.disabled = false;
|
||||||
|
emailButton.disabled = false;
|
||||||
|
updateStatus.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,18 +594,26 @@
|
|||||||
selectAllCheckbox.addEventListener('change', function () {
|
selectAllCheckbox.addEventListener('change', function () {
|
||||||
const isChecked = selectAllCheckbox.checked;
|
const isChecked = selectAllCheckbox.checked;
|
||||||
|
|
||||||
|
// Temporarily disable the change listener on rows to prevent cascading events
|
||||||
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
|
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
|
||||||
|
|
||||||
|
// Update all row checkboxes
|
||||||
rowCheckboxes.forEach(function (checkbox) {
|
rowCheckboxes.forEach(function (checkbox) {
|
||||||
checkbox.checked = isChecked;
|
checkbox.checked = isChecked;
|
||||||
|
|
||||||
|
// Dispatch event for the framework (data-bind-selections)
|
||||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Re-attach the change listeners to the rows
|
||||||
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
|
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
|
||||||
|
|
||||||
|
// Ensure the header state is correct after forcing all changes
|
||||||
updateSelectAllState();
|
updateSelectAllState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Logic to update 'Select All' state based on row checkboxes
|
// 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) {
|
rowCheckboxes.forEach(function (checkbox) {
|
||||||
checkbox.addEventListener('change', updateSelectAllState);
|
checkbox.addEventListener('change', updateSelectAllState);
|
||||||
});
|
});
|
||||||
|
|||||||
52
templates/recruitment/partials/note_form.html
Normal file
52
templates/recruitment/partials/note_form.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{% load i18n crispy_forms_tags %}
|
||||||
|
<div class="p-3">
|
||||||
|
<form hx-boost="true" id="noteform" action="{{url}}" method="post" hx-select=".note-table-body" hx-target=".note-table-body" hx-swap="outerHTML" hx-push-url="false">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form|crispy}}
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" id="notesubmit" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<button type="submit" class="btn btn-main-action" id="saveNoteBtn">{% trans "Save Note" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="table-responsive mt-3">
|
||||||
|
<table class="table table-sm" id="notesTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans "Author" %}</th>
|
||||||
|
<th scope="col" style="width: 60%;">{% trans "Note" %}</th>
|
||||||
|
<th scope="col">{% trans "Created" %}</th>
|
||||||
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="note-table-body">
|
||||||
|
{% if notes %}
|
||||||
|
{% for note in notes %}
|
||||||
|
<tr id="note-{{ note.id }}">
|
||||||
|
<td class="align-middle">
|
||||||
|
{{ note.author.get_full_name|default:note.author.username }}
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
{{ note.content|linebreaksbr }}
|
||||||
|
</td>
|
||||||
|
<td class="align-middle text-nowrap">
|
||||||
|
<span class="text-muted">
|
||||||
|
{{ note.created_at|date:"SHORT_DATETIME_FORMAT" }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle text-end">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger delete-note-btn"
|
||||||
|
data-note-id="{{ note.id }}">
|
||||||
|
{% trans "Delete" %}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center text-muted py-3">{% trans "No notes yet." %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
13
templates/recruitment/partials/note_modal.html
Normal file
13
templates/recruitment/partials/note_modal.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<div class="modal-content kaauh-card">
|
||||||
|
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body notemodal">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
x
Reference in New Issue
Block a user