Compare commits
6 Commits
bfa36091c1
...
760a28db67
| Author | SHA1 | Date | |
|---|---|---|---|
| 760a28db67 | |||
| aa62abc73b | |||
| edd2d52015 | |||
| eca1705ff8 | |||
| 670ff55883 | |||
| c4115efb52 |
@ -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": _("Note"),
|
||||||
# }
|
}
|
||||||
|
|
||||||
# def __init__(self, *args, **kwargs):
|
# def __init__(self, *args, **kwargs):
|
||||||
# super().__init__(*args, **kwargs)
|
# super().__init__(*args, **kwargs)
|
||||||
@ -892,6 +898,15 @@ class JobPostingStatusForm(forms.ModelForm):
|
|||||||
"status": forms.Select(attrs={"class": "form-select"}),
|
"status": forms.Select(attrs={"class": "form-select"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def clean_status(self):
|
||||||
|
status = self.cleaned_data.get("status")
|
||||||
|
if status == "ACTIVE":
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
print(self.instance.assigned_to)
|
||||||
|
if not self.instance.assigned_to:
|
||||||
|
raise ValidationError("Please assign the job posting before setting it to Active.")
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
class LinkedPostContentForm(forms.ModelForm):
|
class LinkedPostContentForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -2090,23 +2105,19 @@ class CandidateEmailForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from django.forms import HiddenInput
|
||||||
class MessageForm(forms.ModelForm):
|
class MessageForm(forms.ModelForm):
|
||||||
"""Form for creating and editing messages between users"""
|
"""Form for creating and editing messages between users"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Message
|
model = Message
|
||||||
fields = ["recipient", "job", "subject", "content", "message_type"]
|
fields = ["job","recipient", "subject", "content", "message_type"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"recipient": forms.Select(
|
"recipient": forms.Select(
|
||||||
attrs={"class": "form-select", "placeholder": "Select recipient","required": True,}
|
attrs={"class": "form-select", "placeholder": "Select recipient","required": True,}
|
||||||
),
|
),
|
||||||
"job": forms.Select(
|
"job": forms.Select(
|
||||||
attrs={"class": "form-select", "placeholder": "Select job",
|
attrs={"class": "form-select", "placeholder": "Select job"}
|
||||||
"hx-get": "/en/messages/create/",
|
|
||||||
"hx-target": "#id_recipient",
|
|
||||||
"hx-select": "#id_recipient",
|
|
||||||
"hx-swap": "outerHTML",}
|
|
||||||
),
|
),
|
||||||
"subject": forms.TextInput(
|
"subject": forms.TextInput(
|
||||||
attrs={
|
attrs={
|
||||||
@ -2211,6 +2222,8 @@ class MessageForm(forms.ModelForm):
|
|||||||
user_type="staff"
|
user_type="staff"
|
||||||
).order_by("username")
|
).order_by("username")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Validate message form data"""
|
"""Validate message form data"""
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-11-27 15:36
|
# Generated by Django 5.2.6 on 2025-12-02 10:27
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
@ -31,6 +31,18 @@ class Migration(migrations.Migration):
|
|||||||
('end_time', models.TimeField(verbose_name='End Time')),
|
('end_time', models.TimeField(verbose_name='End Time')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EmailContent',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('subject', models.CharField(max_length=255, verbose_name='Subject')),
|
||||||
|
('message', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Message Body')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Email Content',
|
||||||
|
'verbose_name_plural': 'Email Contents',
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='FormStage',
|
name='FormStage',
|
||||||
fields=[
|
fields=[
|
||||||
@ -57,7 +69,6 @@ class Migration(migrations.Migration):
|
|||||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||||
('location_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], db_index=True, max_length=10, verbose_name='Location Type')),
|
('location_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom, Google Meet)'), ('Onsite', 'In-Person (Physical Location)')], db_index=True, max_length=10, verbose_name='Location Type')),
|
||||||
('topic', models.CharField(blank=True, help_text="e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'", max_length=255, verbose_name='Meeting/Location Topic')),
|
('topic', models.CharField(blank=True, help_text="e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'", max_length=255, verbose_name='Meeting/Location Topic')),
|
||||||
('details_url', models.URLField(blank=True, max_length=2048, null=True, verbose_name='Meeting/Location URL')),
|
|
||||||
('timezone', models.CharField(default='UTC', max_length=50, verbose_name='Timezone')),
|
('timezone', models.CharField(default='UTC', max_length=50, verbose_name='Timezone')),
|
||||||
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
|
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
|
||||||
('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')),
|
('duration', models.PositiveIntegerField(verbose_name='Duration (minutes)')),
|
||||||
@ -65,6 +76,7 @@ class Migration(migrations.Migration):
|
|||||||
('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')),
|
('meeting_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='External Meeting ID')),
|
||||||
('password', models.CharField(blank=True, max_length=20, null=True)),
|
('password', models.CharField(blank=True, max_length=20, null=True)),
|
||||||
('zoom_gateway_response', models.JSONField(blank=True, null=True)),
|
('zoom_gateway_response', models.JSONField(blank=True, null=True)),
|
||||||
|
('details_url', models.JSONField(blank=True, null=True)),
|
||||||
('participant_video', models.BooleanField(default=True)),
|
('participant_video', models.BooleanField(default=True)),
|
||||||
('join_before_host', models.BooleanField(default=False)),
|
('join_before_host', models.BooleanField(default=False)),
|
||||||
('host_email', models.CharField(blank=True, max_length=255, null=True)),
|
('host_email', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
@ -278,24 +290,6 @@ class Migration(migrations.Migration):
|
|||||||
'verbose_name_plural': 'Applications',
|
'verbose_name_plural': 'Applications',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
|
||||||
name='InterviewNote',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
|
||||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
|
||||||
('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')),
|
|
||||||
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content/Feedback')),
|
|
||||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_notes', to=settings.AUTH_USER_MODEL, verbose_name='Author')),
|
|
||||||
('interview', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.interview', verbose_name='Scheduled Interview')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Interview Note',
|
|
||||||
'verbose_name_plural': 'Interview Notes',
|
|
||||||
'ordering': ['created_at'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='JobPosting',
|
name='JobPosting',
|
||||||
fields=[
|
fields=[
|
||||||
@ -363,12 +357,15 @@ class Migration(migrations.Migration):
|
|||||||
('start_date', models.DateField(db_index=True, verbose_name='Start Date')),
|
('start_date', models.DateField(db_index=True, verbose_name='Start Date')),
|
||||||
('end_date', models.DateField(db_index=True, verbose_name='End Date')),
|
('end_date', models.DateField(db_index=True, verbose_name='End Date')),
|
||||||
('working_days', models.JSONField(verbose_name='Working Days')),
|
('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')),
|
('start_time', models.TimeField(verbose_name='Start Time')),
|
||||||
('end_time', models.TimeField(verbose_name='End Time')),
|
('end_time', models.TimeField(verbose_name='End Time')),
|
||||||
('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')),
|
('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')),
|
||||||
('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')),
|
('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')),
|
||||||
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')),
|
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')),
|
||||||
('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')),
|
('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')),
|
||||||
|
('schedule_interview_type', models.CharField(choices=[('Remote', 'Remote (e.g., Zoom)'), ('Onsite', 'In-Person (Physical Location)')], default='Onsite', max_length=10, verbose_name='Interview Type')),
|
||||||
|
('physical_address', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
('applications', models.ManyToManyField(blank=True, related_name='interview_schedules', to='recruitment.application')),
|
('applications', models.ManyToManyField(blank=True, related_name='interview_schedules', to='recruitment.application')),
|
||||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interview', verbose_name='Location Template (Zoom/Onsite)')),
|
('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interview', verbose_name='Location Template (Zoom/Onsite)')),
|
||||||
@ -438,6 +435,25 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ['-created_at'],
|
'ordering': ['-created_at'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Note',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||||
|
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||||
|
('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')),
|
||||||
|
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content/Feedback')),
|
||||||
|
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.application', verbose_name='Application')),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_notes', to=settings.AUTH_USER_MODEL, verbose_name='Author')),
|
||||||
|
('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.interview', verbose_name='Scheduled Interview')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Interview Note',
|
||||||
|
'verbose_name_plural': 'Interview Notes',
|
||||||
|
'ordering': ['created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Notification',
|
name='Notification',
|
||||||
fields=[
|
fields=[
|
||||||
@ -478,7 +494,7 @@ class Migration(migrations.Migration):
|
|||||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
|
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size], verbose_name='Profile Image')),
|
||||||
('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')),
|
('linkedin_profile', models.URLField(blank=True, null=True, verbose_name='LinkedIn Profile URL')),
|
||||||
('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')),
|
('agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='recruitment.hiringagency', verbose_name='Hiring Agency')),
|
||||||
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')),
|
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Person',
|
'verbose_name': 'Person',
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-11-28 10:24
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('recruitment', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='person',
|
|
||||||
name='user',
|
|
||||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-02 10:28
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_ckeditor_5.fields
|
||||||
|
import django_extensions.db.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0005_merge_20251202_1308'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EmailContent',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('subject', models.CharField(max_length=255, verbose_name='Subject')),
|
||||||
|
('message', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Message Body')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Email Content',
|
||||||
|
'verbose_name_plural': 'Email Contents',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interview',
|
||||||
|
name='details_url',
|
||||||
|
field=models.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Note',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||||
|
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||||
|
('note_type', models.CharField(choices=[('Feedback', 'Candidate Feedback'), ('Logistics', 'Logistical Note'), ('General', 'General Comment')], default='Feedback', max_length=50, verbose_name='Note Type')),
|
||||||
|
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content/Feedback')),
|
||||||
|
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.application', verbose_name='Application')),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_notes', to=settings.AUTH_USER_MODEL, verbose_name='Author')),
|
||||||
|
('interview', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='recruitment.interview', verbose_name='Scheduled Interview')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Interview Note',
|
||||||
|
'verbose_name_plural': 'Interview Notes',
|
||||||
|
'ordering': ['created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='InterviewNote',
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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
|
||||||
@ -27,9 +27,9 @@ except ImportError:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
OPENROUTER_API_KEY ='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'
|
OPENROUTER_API_KEY ='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'
|
||||||
# OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
|
OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct'
|
||||||
|
|
||||||
OPENROUTER_MODEL = 'openai/gpt-oss-20b'
|
# OPENROUTER_MODEL = 'qwen/qwen-2.5-7b-instruct'
|
||||||
# OPENROUTER_MODEL = 'openai/gpt-oss-20b'
|
# OPENROUTER_MODEL = 'openai/gpt-oss-20b'
|
||||||
# OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free'
|
# OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free'
|
||||||
|
|
||||||
@ -623,7 +623,8 @@ def handle_resume_parsing_and_scoring(pk: int):
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
If a top-level key or its required fields are missing, set the field to null, an empty list, or an empty object as appropriate.
|
If a top-level key or its required fields are missing, set the field to null, an empty list, or an empty object as appropriate.
|
||||||
|
Be Clear and Direct Avoid overly indirect politeness which can add confusion.
|
||||||
|
Be strict,objective and concise and critical in your responses, and don't give inflated scores to weak candidates.
|
||||||
Output only valid JSON—no markdown, no extra text.
|
Output only valid JSON—no markdown, no extra text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -668,7 +669,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 +680,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 +701,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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -517,6 +519,7 @@ def job_detail(request, slug):
|
|||||||
job_status = status_form.cleaned_data["status"]
|
job_status = status_form.cleaned_data["status"]
|
||||||
form_template = job.form_template
|
form_template = job.form_template
|
||||||
if job_status == "ACTIVE":
|
if job_status == "ACTIVE":
|
||||||
|
|
||||||
form_template.is_active = True
|
form_template.is_active = True
|
||||||
form_template.save(update_fields=["is_active"])
|
form_template.save(update_fields=["is_active"])
|
||||||
else:
|
else:
|
||||||
@ -533,7 +536,9 @@ def job_detail(request, slug):
|
|||||||
|
|
||||||
return redirect("job_detail", slug=slug)
|
return redirect("job_detail", slug=slug)
|
||||||
else:
|
else:
|
||||||
messages.error(request, "Failed to update status due to validation errors.")
|
error_messages = status_form.errors.get('status', [])
|
||||||
|
formatted_errors = "<br>".join(error_messages)
|
||||||
|
messages.error(request, f"{formatted_errors}")
|
||||||
|
|
||||||
# --- 2. Quality Metrics (JSON Aggregation) ---
|
# --- 2. Quality Metrics (JSON Aggregation) ---
|
||||||
|
|
||||||
@ -607,8 +612,11 @@ def job_detail(request, slug):
|
|||||||
)
|
)
|
||||||
|
|
||||||
category_data = (
|
category_data = (
|
||||||
applications.filter(ai_analysis_data__analysis_data_en__category__isnull=False)
|
applications.filter(
|
||||||
.values("ai_analysis_data__analysis_data_en__category")
|
ai_analysis_data__analysis_data_en__category__isnull=False
|
||||||
|
).exclude(
|
||||||
|
ai_analysis_data__analysis_data_en__category__exact=None
|
||||||
|
).values("ai_analysis_data__analysis_data_en__category")
|
||||||
.annotate(
|
.annotate(
|
||||||
application_count=Count("id"),
|
application_count=Count("id"),
|
||||||
category=Cast(
|
category=Cast(
|
||||||
@ -617,6 +625,7 @@ def job_detail(request, slug):
|
|||||||
)
|
)
|
||||||
.order_by("ai_analysis_data__analysis_data_en__category")
|
.order_by("ai_analysis_data__analysis_data_en__category")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prepare data for Chart.js
|
# Prepare data for Chart.js
|
||||||
categories = [item["category"] for item in category_data]
|
categories = [item["category"] for item in category_data]
|
||||||
applications_count = [item["application_count"] for item in category_data]
|
applications_count = [item["application_count"] for item in category_data]
|
||||||
@ -1463,323 +1472,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
|
||||||
@ -4727,10 +4706,7 @@ def message_create(request):
|
|||||||
# Send email if message_type is 'email' and recipient has email
|
# Send email if message_type is 'email' and recipient has email
|
||||||
|
|
||||||
if message.recipient and message.recipient.email:
|
if message.recipient and message.recipient.email:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
|
||||||
email_result = async_task('recruitment.tasks._task_send_individual_email',
|
email_result = async_task('recruitment.tasks._task_send_individual_email',
|
||||||
subject=message.subject,
|
subject=message.subject,
|
||||||
body_message=message.content,
|
body_message=message.content,
|
||||||
@ -4758,9 +4734,35 @@ def message_create(request):
|
|||||||
|
|
||||||
messages.error(request, "Please correct the errors below.")
|
messages.error(request, "Please correct the errors below.")
|
||||||
else:
|
else:
|
||||||
|
|
||||||
form = MessageForm(request.user)
|
form = MessageForm(request.user)
|
||||||
|
|
||||||
|
form.fields["job"].widget.attrs.update({"hx-get": "/en/messages/create/",
|
||||||
|
"hx-target": "#id_recipient",
|
||||||
|
"hx-select": "#id_recipient",
|
||||||
|
"hx-swap": "outerHTML",})
|
||||||
|
if request.user.user_type == "staff":
|
||||||
|
job_id = request.GET.get("job")
|
||||||
|
if job_id:
|
||||||
|
job = get_object_or_404(JobPosting, id=job_id)
|
||||||
|
applications=job.applications.all()
|
||||||
|
applicant_users = User.objects.filter(person_profile__in=applications.values_list('person', flat=True))
|
||||||
|
agency_users = User.objects.filter(id__in=AgencyJobAssignment.objects.filter(job=job).values_list('agency__user', flat=True))
|
||||||
|
form.fields["recipient"].queryset = applicant_users | agency_users
|
||||||
|
|
||||||
|
|
||||||
|
# form.fields["recipient"].queryset = User.objects.filter(person_profile__)
|
||||||
|
else:
|
||||||
|
|
||||||
|
form.fields['recipient'].widget = HiddenInput()
|
||||||
|
if request.method == "GET" and "HX-Request" in request.headers and request.user.user_type in ["candidate","agency"]:
|
||||||
|
print()
|
||||||
|
job_id = request.GET.get("job")
|
||||||
|
if job_id:
|
||||||
|
job = get_object_or_404(JobPosting, id=job_id)
|
||||||
|
form.fields["recipient"].queryset = User.objects.filter(id=job.assigned_to.id)
|
||||||
|
form.fields["recipient"].initial = job.assigned_to
|
||||||
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"form": form,
|
"form": form,
|
||||||
}
|
}
|
||||||
@ -5097,7 +5099,8 @@ def document_upload(request, slug):
|
|||||||
if upload_target == 'person':
|
if upload_target == 'person':
|
||||||
return redirect("applicant_portal_dashboard")
|
return redirect("applicant_portal_dashboard")
|
||||||
else:
|
else:
|
||||||
return redirect("applicant_application_detail", slug=application.slug)
|
return render(request, 'recruitment/application_detail.html', {'application': application})
|
||||||
|
# return redirect("application_detail", slug=application.slug)
|
||||||
|
|
||||||
# Handle GET request for AJAX
|
# Handle GET request for AJAX
|
||||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||||
@ -5109,7 +5112,6 @@ def document_upload(request, slug):
|
|||||||
def document_delete(request, document_id):
|
def document_delete(request, document_id):
|
||||||
"""Delete a document"""
|
"""Delete a document"""
|
||||||
document = get_object_or_404(Document, id=document_id)
|
document = get_object_or_404(Document, id=document_id)
|
||||||
print(document)
|
|
||||||
|
|
||||||
# Initialize variables for redirection outside of the complex logic
|
# Initialize variables for redirection outside of the complex logic
|
||||||
is_htmx = "HX-Request" in request.headers
|
is_htmx = "HX-Request" in request.headers
|
||||||
@ -5172,7 +5174,9 @@ def document_delete(request, document_id):
|
|||||||
if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||||
# For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML'
|
# For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML'
|
||||||
# to remove the element, or hx-redirect to navigate.
|
# to remove the element, or hx-redirect to navigate.
|
||||||
return HttpResponse(status=200)
|
response = HttpResponse(status=200)
|
||||||
|
response["HX-Refresh"] = "true" # Instruct HTMX to refresh the current view
|
||||||
|
return response
|
||||||
|
|
||||||
# --- Standard Navigation Fallback ---
|
# --- Standard Navigation Fallback ---
|
||||||
else:
|
else:
|
||||||
@ -6583,3 +6587,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()})
|
||||||
|
|||||||
@ -293,10 +293,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item me-lg-4">
|
<li class="nav-item me-lg-4">
|
||||||
<a class="nav-link {% if request.resolver_match.url_name == 'list_meetings' %}active{% endif %}" href="{% url 'interview_list' %}">
|
<a class="nav-link {% if request.resolver_match.url_name == 'interview_list' %}active{% endif %}" href="{% url 'interview_list' %}">
|
||||||
<span class="d-flex align-items-center gap-2">
|
<span class="d-flex align-items-center gap-2">
|
||||||
<i class="fas fa-calendar-check me-2"></i>
|
<i class="fas fa-calendar-check me-2"></i>
|
||||||
{% trans "Meetings" %}
|
{% trans "Meetings & interviews" %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load file_filters %}
|
{% load file_filters %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-white border-bottom d-flex justify-content-between align-items-center">
|
<div class="card-header bg-white border-bottom d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-0 text-primary">{% trans "Documents" %}</h5>
|
<h5 class="card-title mb-0 text-primary">{% trans "Documents" %}</h5>
|
||||||
@ -25,12 +26,8 @@
|
|||||||
|
|
||||||
<form
|
<form
|
||||||
method="post"
|
method="post"
|
||||||
|
action="{% url 'application_document_upload' application.slug %}"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
hx-post="{% url 'application_document_upload' application.slug %}"
|
|
||||||
hx-target="#documents-pane"
|
|
||||||
hx-select="#documents-pane"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
hx-on::after-request="bootstrap.Modal.getInstance(document.getElementById('documentUploadModal')).hide()"
|
|
||||||
>
|
>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -64,7 +61,7 @@
|
|||||||
id="documentDescription"
|
id="documentDescription"
|
||||||
rows="3"
|
rows="3"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="{% trans "Optional description..." %}"
|
placeholder='{% trans "Optional description..." %}'
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -101,22 +98,23 @@
|
|||||||
|
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<a
|
<a
|
||||||
href="{% url 'document_download' document.id %}"
|
href="{% url 'document_download' document.id %}"
|
||||||
class="btn btn-sm btn-outline-primary me-2"
|
class="btn btn-sm btn-outline-primary me-2"
|
||||||
title="{% trans "Download" %}"
|
title='{% trans "Download" %}'
|
||||||
>
|
>
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if user.is_superuser or application.job.assigned_to == user %}
|
{% if user.is_superuser or application.job.assigned_to == user %}
|
||||||
<button
|
<a
|
||||||
|
hx-post="{% url 'document_delete' document.id %}"
|
||||||
|
hx-confirm='{% trans "Are you sure you want to delete" %}'
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-outline-danger"
|
class="btn btn-sm btn-outline-danger"
|
||||||
onclick="confirmDelete({{ document.id }}, '{{ document.file.name|filename|default:"Document" }}')"
|
title='{% trans "Delete" %}'
|
||||||
title="{% trans "Delete" %}"
|
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -131,6 +129,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.hover-bg-light:hover {
|
.hover-bg-light:hover {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
@ -139,7 +138,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function confirmDelete(documentId, fileName) {
|
/*function confirmDelete(documentId, fileName) {
|
||||||
var deletePrefix = "{% trans "Are you sure you want to delete" %}";
|
var deletePrefix = "{% trans "Are you sure you want to delete" %}";
|
||||||
if (confirm(deletePrefix + ' "' + fileName + '"?')) {
|
if (confirm(deletePrefix + ' "' + fileName + '"?')) {
|
||||||
htmx.ajax('POST', `{% url 'document_delete' 0 %}`.replace('0', documentId), {
|
htmx.ajax('POST', `{% url 'document_delete' 0 %}`.replace('0', documentId), {
|
||||||
@ -147,5 +146,16 @@ function confirmDelete(documentId, fileName) {
|
|||||||
swap: 'innerHTML'
|
swap: 'innerHTML'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function closeUploadModal() {
|
||||||
|
var modalElement = document.getElementById('documentUploadModal');
|
||||||
|
if (modalElement) {
|
||||||
|
var modal = bootstrap.Modal.getInstance(modalElement);
|
||||||
|
if (modal) {
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -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,17 +191,17 @@
|
|||||||
<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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "portal_base.html" %}
|
{% extends "portal_base.html" %}
|
||||||
{% load static %}
|
{% load static crispy_forms_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}{% if form.instance.pk %}{% trans "Reply to Message" %}{% else %}{% trans "Compose Message" %}{% endif %}{% endblock %}
|
{% block title %}{% if form.instance.pk %}{% trans "Reply to Message" %}{% else %}{% trans "Compose Message" %}{% endif %}{% endblock %}
|
||||||
|
|
||||||
@ -11,23 +11,23 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
{% if form.instance.pk %}
|
{% if form.instance.pk %}
|
||||||
<i class="fas fa-reply"></i> Reply to Message
|
<i class="fas fa-reply"></i> {% trans "Reply to Message" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="fas fa-envelope"></i> Compose Message
|
<i class="fas fa-envelope"></i> {% trans "Compose Message" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if form.instance.parent_message %}
|
{% if form.instance.parent_message %}
|
||||||
<div class="alert alert-info mb-4">
|
<div class="alert alert-info mb-4">
|
||||||
<strong>Replying to:</strong> {{ form.instance.parent_message.subject }}
|
<strong>{% trans "Replying to:" %}</strong> {{ form.instance.parent_message.subject }}
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
From {{ form.instance.parent_message.sender.get_full_name|default:form.instance.parent_message.sender.username }}
|
{% trans "From" %} {{ form.instance.parent_message.sender.get_full_name|default:form.instance.parent_message.sender.username }}
|
||||||
on {{ form.instance.parent_message.created_at|date:"M d, Y H:i" }}
|
{% trans "on" %} {{ form.instance.parent_message.created_at|date:"M d, Y H:i" }}
|
||||||
</small>
|
</small>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<strong>Original message:</strong>
|
<strong>{% trans "Original message:" %}</strong>
|
||||||
<div class="border-start ps-3 mt-2">
|
<div class="border-start ps-3 mt-2">
|
||||||
{{ form.instance.parent_message.content|linebreaks }}
|
{{ form.instance.parent_message.content|linebreaks }}
|
||||||
</div>
|
</div>
|
||||||
@ -38,99 +38,17 @@
|
|||||||
<form method="post" id="messageForm">
|
<form method="post" id="messageForm">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="row">
|
{{form|crispy}}
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.job.id_for_label }}" class="form-label">
|
|
||||||
Related Job <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
{{ form.job }}
|
|
||||||
{% if form.job.errors %}
|
|
||||||
<div class="text-danger small mt-1">
|
|
||||||
{{ form.job.errors.0 }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="form-text">
|
|
||||||
Select a job if this message is related to a specific position
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.recipient.id_for_label }}" class="form-label">
|
|
||||||
Recipient <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
{{ form.recipient }}
|
|
||||||
|
|
||||||
{% if form.recipient.errors %}
|
|
||||||
<div class="text-danger small mt-1">
|
|
||||||
{{ form.recipient.errors.0 }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="form-text">
|
|
||||||
Select the user who will receive this message
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.message_type.id_for_label }}" class="form-label">
|
|
||||||
Message Type <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
{{ form.message_type }}
|
|
||||||
{% if form.message_type.errors %}
|
|
||||||
<div class="text-danger small mt-1">
|
|
||||||
{{ form.message_type.errors.0 }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="form-text">
|
|
||||||
Select the type of message you're sending
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.subject.id_for_label }}" class="form-label">
|
|
||||||
Subject <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
{{ form.subject }}
|
|
||||||
{% if form.subject.errors %}
|
|
||||||
<div class="text-danger small mt-1">
|
|
||||||
{{ form.subject.errors.0 }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.content.id_for_label }}" class="form-label">
|
|
||||||
Message <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
{{ form.content }}
|
|
||||||
{% if form.content.errors %}
|
|
||||||
<div class="text-danger small mt-1">
|
|
||||||
{{ form.content.errors.0 }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="form-text">
|
|
||||||
Write your message here. You can use line breaks and basic formatting.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<a href="{% url 'message_list' %}" class="btn btn-secondary">
|
<a href="{% url 'message_list' %}" class="btn btn-outline-primary">
|
||||||
<i class="fas fa-times"></i> Cancel
|
<i class="fas fa-times"></i> {% trans "Cancel" %}
|
||||||
</a>
|
</a>
|
||||||
<button type="submit" class="btn btn-main-action">
|
<button type="submit" class="btn btn-main-action">
|
||||||
<i class="fas fa-paper-plane"></i>
|
<i class="fas fa-paper-plane"></i>
|
||||||
{% if form.instance.pk %}
|
{% if form.instance.pk %}
|
||||||
Send Reply
|
{% trans "Send Reply" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
Send Message
|
{% trans "Send Message" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -184,6 +102,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Character counter for subject
|
// Character counter for subject
|
||||||
const subjectField = document.getElementById('id_subject');
|
const subjectField = document.getElementById('id_subject');
|
||||||
const maxLength = 200;
|
const maxLength = 200;
|
||||||
|
const charsLabel = "{% trans 'characters' %}";
|
||||||
|
|
||||||
if (subjectField) {
|
if (subjectField) {
|
||||||
// Add character counter display
|
// Add character counter display
|
||||||
@ -194,7 +113,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
function updateCounter() {
|
function updateCounter() {
|
||||||
const remaining = maxLength - subjectField.value.length;
|
const remaining = maxLength - subjectField.value.length;
|
||||||
counter.textContent = `${subjectField.value.length}/${maxLength} characters`;
|
counter.textContent = `${subjectField.value.length}/${maxLength} ${charsLabel}`;
|
||||||
if (remaining < 20) {
|
if (remaining < 20) {
|
||||||
counter.className = 'text-warning';
|
counter.className = 'text-warning';
|
||||||
} else {
|
} else {
|
||||||
@ -216,19 +135,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('Please select a recipient.');
|
alert("{% trans 'Please select a recipient.' %}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!subject) {
|
if (!subject) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('Please enter a subject.');
|
alert("{% trans 'Please enter a subject.' %}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('Please enter a message.');
|
alert("{% trans 'Please enter a message.' %}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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