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_EMAIL_VERIFICATION = 'none'
|
||||
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||
ACCOUNT_EMAIL_VERIFICATION = "optional"
|
||||
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ from django.utils import timezone
|
||||
from .models import (
|
||||
JobPosting, Application, TrainingMaterial,
|
||||
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
|
||||
SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,InterviewNote,
|
||||
SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,Note,
|
||||
AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview
|
||||
)
|
||||
from django.contrib.auth import get_user_model
|
||||
@ -250,4 +250,4 @@ admin.site.register(ScheduledInterview)
|
||||
|
||||
|
||||
admin.site.register(JobPostingImage)
|
||||
admin.site.register(User)
|
||||
# admin.site.register(User)
|
||||
|
||||
@ -18,7 +18,7 @@ from .models import (
|
||||
BulkInterviewTemplate,
|
||||
BreakTime,
|
||||
JobPostingImage,
|
||||
InterviewNote,
|
||||
Note,
|
||||
ScheduledInterview,
|
||||
Source,
|
||||
HiringAgency,
|
||||
@ -720,94 +720,100 @@ class FormTemplateForm(forms.ModelForm):
|
||||
# BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True)
|
||||
|
||||
|
||||
# class BulkInterviewTemplateForm(forms.ModelForm):
|
||||
# applications = forms.ModelMultipleChoiceField(
|
||||
# queryset=Application.objects.none(),
|
||||
# widget=forms.CheckboxSelectMultiple,
|
||||
# required=True,
|
||||
# )
|
||||
# working_days = forms.MultipleChoiceField(
|
||||
# choices=[
|
||||
# (0, "Monday"),
|
||||
# (1, "Tuesday"),
|
||||
# (2, "Wednesday"),
|
||||
# (3, "Thursday"),
|
||||
# (4, "Friday"),
|
||||
# (5, "Saturday"),
|
||||
# (6, "Sunday"),
|
||||
# ],
|
||||
# widget=forms.CheckboxSelectMultiple,
|
||||
# required=True,
|
||||
# )
|
||||
class BulkInterviewTemplateForm(forms.ModelForm):
|
||||
applications = forms.ModelMultipleChoiceField(
|
||||
queryset=Application.objects.none(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=True,
|
||||
)
|
||||
working_days = forms.MultipleChoiceField(
|
||||
choices=[
|
||||
(0, "Monday"),
|
||||
(1, "Tuesday"),
|
||||
(2, "Wednesday"),
|
||||
(3, "Thursday"),
|
||||
(4, "Friday"),
|
||||
(5, "Saturday"),
|
||||
(6, "Sunday"),
|
||||
],
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=True,
|
||||
)
|
||||
|
||||
# class Meta:
|
||||
# model = BulkInterviewTemplate
|
||||
# fields = [
|
||||
# 'schedule_interview_type',
|
||||
# "applications",
|
||||
# "start_date",
|
||||
# "end_date",
|
||||
# "working_days",
|
||||
# "start_time",
|
||||
# "end_time",
|
||||
# "interview_duration",
|
||||
# "buffer_time",
|
||||
# "break_start_time",
|
||||
# "break_end_time",
|
||||
# ]
|
||||
# widgets = {
|
||||
# "start_date": forms.DateInput(
|
||||
# attrs={"type": "date", "class": "form-control"}
|
||||
# ),
|
||||
# "end_date": forms.DateInput(
|
||||
# attrs={"type": "date", "class": "form-control"}
|
||||
# ),
|
||||
# "start_time": forms.TimeInput(
|
||||
# attrs={"type": "time", "class": "form-control"}
|
||||
# ),
|
||||
# "end_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_start_time": forms.TimeInput(
|
||||
# attrs={"type": "time", "class": "form-control"}
|
||||
# ),
|
||||
# "break_end_time": forms.TimeInput(
|
||||
# attrs={"type": "time", "class": "form-control"}
|
||||
# ),
|
||||
# "schedule_interview_type":forms.RadioSelect()
|
||||
# }
|
||||
class Meta:
|
||||
model = BulkInterviewTemplate
|
||||
fields = [
|
||||
'schedule_interview_type',
|
||||
'topic',
|
||||
'physical_address',
|
||||
"applications",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"working_days",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"interview_duration",
|
||||
"buffer_time",
|
||||
"break_start_time",
|
||||
"break_end_time",
|
||||
]
|
||||
widgets = {
|
||||
"topic": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"start_date": forms.DateInput(
|
||||
attrs={"type": "date", "class": "form-control"}
|
||||
),
|
||||
"end_date": forms.DateInput(
|
||||
attrs={"type": "date", "class": "form-control"}
|
||||
),
|
||||
"start_time": forms.TimeInput(
|
||||
attrs={"type": "time", "class": "form-control"}
|
||||
),
|
||||
"end_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_start_time": forms.TimeInput(
|
||||
attrs={"type": "time", "class": "form-control"}
|
||||
),
|
||||
"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):
|
||||
# super().__init__(*args, **kwargs)
|
||||
# self.fields["applications"].queryset = Application.objects.filter(
|
||||
# job__slug=slug, stage="Interview"
|
||||
# )
|
||||
def __init__(self, slug, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["applications"].queryset = Application.objects.filter(
|
||||
job__slug=slug, stage="Interview"
|
||||
)
|
||||
|
||||
# def clean_working_days(self):
|
||||
# working_days = self.cleaned_data.get("working_days")
|
||||
# return [int(day) for day in working_days]
|
||||
def clean_working_days(self):
|
||||
working_days = self.cleaned_data.get("working_days")
|
||||
return [int(day) for day in working_days]
|
||||
|
||||
|
||||
# class InterviewNoteForm(forms.ModelForm):
|
||||
# """Form for creating and editing meeting comments"""
|
||||
class NoteForm(forms.ModelForm):
|
||||
"""Form for creating and editing meeting comments"""
|
||||
|
||||
# class Meta:
|
||||
# model = InterviewNote
|
||||
# fields = ["content"]
|
||||
# widgets = {
|
||||
# "content": CKEditor5Widget(
|
||||
# attrs={
|
||||
# "class": "form-control",
|
||||
# "placeholder": _("Enter your comment or note"),
|
||||
# },
|
||||
# config_name="extends",
|
||||
# ),
|
||||
# }
|
||||
# labels = {
|
||||
# "content": _("Comment"),
|
||||
# }
|
||||
class Meta:
|
||||
model = Note
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"content": CKEditor5Widget(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": _("Enter your comment or note"),
|
||||
},
|
||||
config_name="extends",
|
||||
),
|
||||
}
|
||||
labels = {
|
||||
"content": _("Comment"),
|
||||
}
|
||||
|
||||
# def __init__(self, *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(
|
||||
verbose_name=_("Working Days")
|
||||
)
|
||||
topic = models.CharField(max_length=255, verbose_name=_("Interview Topic"))
|
||||
|
||||
start_time = models.TimeField(verbose_name=_("Start Time"))
|
||||
end_time = models.TimeField(verbose_name=_("End Time"))
|
||||
@ -1414,6 +1415,14 @@ class BulkInterviewTemplate(Base):
|
||||
buffer_time = models.PositiveIntegerField(
|
||||
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(
|
||||
User, on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -1509,7 +1518,7 @@ class ScheduledInterview(Base):
|
||||
return self.interview_location
|
||||
# --- 3. Interview Notes Model (Fixed) ---
|
||||
|
||||
class InterviewNote(Base):
|
||||
class Note(Base):
|
||||
"""Model for storing notes, feedback, or comments related to a specific ScheduledInterview."""
|
||||
|
||||
class NoteType(models.TextChoices):
|
||||
@ -1517,13 +1526,24 @@ class InterviewNote(Base):
|
||||
LOGISTICS = 'Logistics', _('Logistical Note')
|
||||
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,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="notes",
|
||||
verbose_name=_("Scheduled Interview"),
|
||||
db_index=True
|
||||
db_index=True,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
author = models.ForeignKey(
|
||||
@ -2692,3 +2712,5 @@ class Document(Base):
|
||||
if self.file:
|
||||
return self.file.name.split(".")[-1].upper()
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ from . linkedin_service import LinkedInService
|
||||
from django.shortcuts import get_object_or_404
|
||||
from . models import JobPosting
|
||||
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
|
||||
User = get_user_model()
|
||||
# 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
|
||||
def create_interview_and_meeting(
|
||||
candidate_id,
|
||||
application_id,
|
||||
job_id,
|
||||
schedule_id,
|
||||
slot_date,
|
||||
@ -679,24 +679,13 @@ def create_interview_and_meeting(
|
||||
Synchronous task for a single interview slot, dispatched by django-q.
|
||||
"""
|
||||
try:
|
||||
application = Application.objects.get(pk=candidate_id)
|
||||
application = Application.objects.get(pk=application_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))
|
||||
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)
|
||||
|
||||
if result["status"] == "success":
|
||||
@ -711,33 +700,19 @@ def create_interview_and_meeting(
|
||||
password=result["meeting_details"]["password"],
|
||||
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.status = "scheduled"
|
||||
|
||||
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}")
|
||||
return True # Task succeeded
|
||||
else:
|
||||
|
||||
@ -587,16 +587,16 @@ urlpatterns = [
|
||||
# path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'),
|
||||
|
||||
#interview and meeting related urls
|
||||
# path(
|
||||
# "jobs/<slug:slug>/schedule-interviews/",
|
||||
# views.schedule_interviews_view,
|
||||
# name="schedule_interviews",
|
||||
# ),
|
||||
# path(
|
||||
# "jobs/<slug:slug>/confirm-schedule-interviews/",
|
||||
# views.confirm_schedule_interviews_view,
|
||||
# name="confirm_schedule_interviews_view",
|
||||
# ),
|
||||
path(
|
||||
"jobs/<slug:slug>/schedule-interviews/",
|
||||
views.schedule_interviews_view,
|
||||
name="schedule_interviews",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/confirm-schedule-interviews/",
|
||||
views.confirm_schedule_interviews_view,
|
||||
name="confirm_schedule_interviews_view",
|
||||
),
|
||||
|
||||
# path(
|
||||
# "meetings/create-meeting/",
|
||||
@ -682,5 +682,6 @@ urlpatterns = [
|
||||
# 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-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,
|
||||
StaffAssignmentForm,
|
||||
RemoteInterviewForm,
|
||||
OnsiteInterviewForm
|
||||
OnsiteInterviewForm,
|
||||
BulkInterviewTemplateForm
|
||||
)
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
@ -132,7 +133,8 @@ from .models import (
|
||||
Source,
|
||||
Message,
|
||||
Document,
|
||||
Interview
|
||||
Interview,
|
||||
BulkInterviewTemplate
|
||||
)
|
||||
|
||||
|
||||
@ -1463,323 +1465,293 @@ def form_submission_details(request, template_id, slug):
|
||||
)
|
||||
|
||||
|
||||
# def _handle_get_request(request, slug, job):
|
||||
# """
|
||||
# Handles GET requests, setting up forms and restoring candidate selections
|
||||
# from the session for persistence.
|
||||
# """
|
||||
# SESSION_KEY = f"schedule_candidate_ids_{slug}"
|
||||
def _handle_get_request(request, slug, job):
|
||||
"""
|
||||
Handles GET requests, setting up forms and restoring candidate selections
|
||||
from the session for persistence.
|
||||
"""
|
||||
SESSION_KEY = f"schedule_candidate_ids_{slug}"
|
||||
|
||||
# form = BulkInterviewTemplateForm(slug=slug)
|
||||
# # break_formset = BreakTimeFormSet(prefix='breaktime')
|
||||
form = BulkInterviewTemplateForm(slug=slug)
|
||||
# break_formset = BreakTimeFormSet(prefix='breaktime')
|
||||
|
||||
# selected_ids = []
|
||||
selected_ids = []
|
||||
|
||||
# # 1. Capture IDs from HTMX request and store in session (when first clicked)
|
||||
# if "HX-Request" in request.headers:
|
||||
# candidate_ids = request.GET.getlist("candidate_ids")
|
||||
# 1. Capture IDs from HTMX request and store in session (when first clicked)
|
||||
if "HX-Request" in request.headers:
|
||||
candidate_ids = request.GET.getlist("candidate_ids")
|
||||
|
||||
# if candidate_ids:
|
||||
# request.session[SESSION_KEY] = candidate_ids
|
||||
# selected_ids = candidate_ids
|
||||
if candidate_ids:
|
||||
request.session[SESSION_KEY] = candidate_ids
|
||||
selected_ids = candidate_ids
|
||||
|
||||
# # 2. Restore IDs from session (on refresh or navigation)
|
||||
# if not selected_ids:
|
||||
# selected_ids = request.session.get(SESSION_KEY, [])
|
||||
# 2. Restore IDs from session (on refresh or navigation)
|
||||
if not selected_ids:
|
||||
selected_ids = request.session.get(SESSION_KEY, [])
|
||||
|
||||
# # 3. Use the list of IDs to initialize the form
|
||||
# if selected_ids:
|
||||
# candidates_to_load = Application.objects.filter(pk__in=selected_ids)
|
||||
# print(candidates_to_load)
|
||||
# form.initial["applications"] = candidates_to_load
|
||||
# 3. Use the list of IDs to initialize the form
|
||||
if selected_ids:
|
||||
candidates_to_load = Application.objects.filter(pk__in=selected_ids)
|
||||
form.initial["applications"] = candidates_to_load
|
||||
|
||||
# return render(
|
||||
# request,
|
||||
# "interviews/schedule_interviews.html",
|
||||
# {"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})
|
||||
return render(
|
||||
request,
|
||||
"interviews/schedule_interviews.html",
|
||||
{"form": form, "job": job},
|
||||
)
|
||||
|
||||
|
||||
|
||||
# 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:
|
||||
# return _handle_get_request(request, slug, job)
|
||||
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"]
|
||||
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):
|
||||
# job = get_object_or_404(JobPosting, slug=slug)
|
||||
# if request.method == "POST":
|
||||
# return _handle_confirm_schedule(request, slug, 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"],
|
||||
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
|
||||
@ -6583,3 +6555,57 @@ def interview_detail(request, slug):
|
||||
# messages.error(request, f"Failed to send invitation emails: {str(e)}")
|
||||
|
||||
# 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();
|
||||
|
||||
try{
|
||||
document.addEventListener('htmx:afterSwap', form_loader);
|
||||
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||
remove_form_loader();
|
||||
});
|
||||
}catch(e){
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
@ -170,11 +170,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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">
|
||||
{% csrf_token %}
|
||||
<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" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -196,20 +191,20 @@
|
||||
<h5 class="modal-title" id="interviewDetailsModalLabel">{% trans "Interview Details" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</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 %}
|
||||
|
||||
|
||||
{# Renders the single 'location' field using the crispy filter #}
|
||||
{{ form|crispy }}
|
||||
|
||||
</form>
|
||||
{{ form|crispy }}
|
||||
|
||||
</form> {% endcomment %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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">
|
||||
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
|
||||
<a href="#" class="btn btn-secondary me-2">
|
||||
<i class="fas fa-times me-1"></i> Close
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" form="onsite-form">
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" form="onsite-form">
|
||||
<i class="fas fa-save me-1"></i> Save Location
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}Bulk Interview Scheduling - {{ job.title }} - ATS{% endblock %}
|
||||
|
||||
@ -125,7 +126,6 @@
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h5 class="section-header">{% trans "Select Candidates" %}</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.candidates.id_for_label }}">
|
||||
{% trans "Candidates to Schedule (Hold Ctrl/Cmd to select multiple)" %}
|
||||
@ -141,14 +141,19 @@
|
||||
<h5 class="section-header">{% trans "Schedule Details" %}</h5>
|
||||
<div class="row">
|
||||
<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">
|
||||
<label for="{{ form.schedule_interview_type.id_for_label }}">{% trans "Interview Type" %}</label>
|
||||
{{ form.schedule_interview_type }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@ -217,8 +222,17 @@
|
||||
</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>
|
||||
|
||||
@ -278,7 +278,7 @@
|
||||
{% trans "To Offer" %}
|
||||
</option>
|
||||
</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" %}
|
||||
</button>
|
||||
</form>
|
||||
@ -286,7 +286,7 @@
|
||||
{# Separator (Vertical Rule) - Aligns automatically at the bottom with align-items-end #}
|
||||
<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"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
@ -323,6 +323,9 @@
|
||||
<th scope="col" style="width: 28%;">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
|
||||
</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%;">
|
||||
<i class="fas fa-cog me-1"></i> {% trans "Actions" %}
|
||||
</th>
|
||||
@ -394,6 +397,15 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</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">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
@ -462,51 +474,78 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "recruitment/partials/note_modal.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
const changeStageButton = document.getElementById('changeStage');
|
||||
const emailButton = document.getElementById('emailBotton');
|
||||
const updateStatus = document.getElementById('update_status');
|
||||
|
||||
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 (selectAllCheckbox) {
|
||||
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else if (checkedCount === totalCount) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
// Function to safely update the header checkbox state
|
||||
function updateSelectAllState() {
|
||||
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
||||
const totalCount = rowCheckboxes.length;
|
||||
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
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)
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
const isChecked = selectAllCheckbox.checked;
|
||||
// 1. Logic for the 'Select All' checkbox (Clicking it updates all rows)
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
const isChecked = selectAllCheckbox.checked;
|
||||
|
||||
rowCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
// Temporarily disable the change listener on rows to prevent cascading events
|
||||
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
|
||||
|
||||
// Update all row checkboxes
|
||||
rowCheckboxes.forEach(function (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
// 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>
|
||||
{% endblock %}
|
||||
|
||||
@ -225,11 +225,11 @@
|
||||
</div>
|
||||
|
||||
{# 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" %}
|
||||
</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"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
@ -261,9 +261,10 @@
|
||||
<th style="width: 15%;">{% trans "Name" %}</th>
|
||||
<th style="width: 15%;">{% trans "Contact Info" %}</th>
|
||||
<th style="width: 10%;" class="text-center">{% trans "AI Score" %}</th>
|
||||
<th style="width: 15%;">{% trans "Exam Date" %}</th>
|
||||
<th style="width: 15%;">{% trans "Exam Score" %}</th>
|
||||
<th style="width: 10%;">{% trans "Exam Date" %}</th>
|
||||
<th style="width: 10%;">{% trans "Exam Score" %}</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>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -324,6 +325,15 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</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 >
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
@ -395,14 +405,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "recruitment/partials/note_modal.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
const changeStageButton = document.getElementById('changeStage');
|
||||
const emailButton = document.getElementById('emailBotton');
|
||||
const updateStatus = document.getElementById('update_status');
|
||||
|
||||
if (selectAllCheckbox) {
|
||||
|
||||
@ -414,13 +428,22 @@
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -218,7 +218,7 @@
|
||||
{% trans "To Exam" %}
|
||||
</option>
|
||||
</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" %}
|
||||
</button>
|
||||
</form>
|
||||
@ -227,23 +227,23 @@
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
|
||||
{# Form 2: Schedule Interviews #}
|
||||
<form hx-boost="true" hx-include="#application-form" action="#" method="get" class="action-group">
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
|
||||
<button id="scheduleInterview" type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-calendar-plus me-1"></i> {% trans "Bulk Schedule Interviews" %}
|
||||
</button>
|
||||
</form>
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
|
||||
|
||||
<button type="button" class="btn btn-outline-info btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<button id="emailBotton" type="button" class="btn btn-outline-info btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
</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: 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: 15%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview Result"%}</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-video me-1"></i> {% trans "Interviews"%}</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>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -353,6 +354,26 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</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 }}">
|
||||
{% if not application.interview_status %}
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
@ -379,18 +400,6 @@
|
||||
{% endif %}
|
||||
</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.location_type == 'Remote'%}
|
||||
|
||||
@ -445,7 +454,6 @@
|
||||
<i class="fas fa-calendar-plus me-1"></i>
|
||||
Schedule Interview
|
||||
</button>
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -504,6 +512,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% include "recruitment/partials/note_modal.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
@ -511,6 +522,10 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
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) {
|
||||
|
||||
@ -522,13 +537,25 @@
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = true;
|
||||
emailButton.disabled = true;
|
||||
updateStatus.disabled = true;
|
||||
scheduleInterviewButton.disabled = true;
|
||||
} else if (checkedCount === totalCount) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
scheduleInterviewButton.disabled = false;
|
||||
} else {
|
||||
// Set to indeterminate state (partially checked)
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
scheduleInterviewButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -220,26 +220,23 @@
|
||||
</select>
|
||||
|
||||
{# 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" %}
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
{# Separator (Vertical Rule) #}
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<button id="emailBotton" type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
@ -263,8 +260,9 @@
|
||||
<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 scope="col" style="width: 30%;">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
|
||||
</th>
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -356,6 +354,15 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</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>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
@ -423,6 +430,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "recruitment/partials/note_modal.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
@ -430,6 +439,9 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
const changeStageButton = document.getElementById('changeStage');
|
||||
const emailButton = document.getElementById('emailBotton');
|
||||
const updateStatus = document.getElementById('update_status');
|
||||
|
||||
if (selectAllCheckbox) {
|
||||
|
||||
@ -441,13 +453,22 @@
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -340,11 +340,11 @@
|
||||
</div>
|
||||
|
||||
{# 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" %}
|
||||
</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"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
@ -374,28 +374,31 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</th>
|
||||
<th scope="col" style="width: 8%;">
|
||||
<th scope="col" style="width: 13%;">
|
||||
<i class="fas fa-user me-1"></i> {% trans "Name" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 10%;">
|
||||
<th scope="col" style="width: 15%;">
|
||||
<i class="fas fa-phone me-1"></i> {% trans "Contact Info" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 5%;">
|
||||
<i class="fas fa-graduation-cap me-1"></i> {% trans "GPA" %}
|
||||
</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" %}
|
||||
</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?" %}
|
||||
</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" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 15%;">
|
||||
<i class="fas fa-graduation-cap me-1"></i> {% trans "Top 3 Skills" %}
|
||||
</th>
|
||||
<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" %}
|
||||
</th>
|
||||
</tr>
|
||||
@ -466,7 +469,15 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</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">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
@ -536,6 +547,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "recruitment/partials/note_modal.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -544,8 +557,12 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
const changeStageButton = document.getElementById('changeStage');
|
||||
const emailButton = document.getElementById('emailBotton');
|
||||
const updateStatus = document.getElementById('update_status');
|
||||
|
||||
if (selectAllCheckbox) {
|
||||
|
||||
// Function to safely update the header checkbox state
|
||||
function updateSelectAllState() {
|
||||
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
||||
@ -554,12 +571,22 @@
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -567,18 +594,26 @@
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
const isChecked = selectAllCheckbox.checked;
|
||||
|
||||
// Temporarily disable the change listener on rows to prevent cascading events
|
||||
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
|
||||
|
||||
// Update all row checkboxes
|
||||
rowCheckboxes.forEach(function (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
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