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_EMAIL_VERIFICATION = 'none'
|
||||
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||
ACCOUNT_EMAIL_VERIFICATION = "optional"
|
||||
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ from django.utils import timezone
|
||||
from .models import (
|
||||
JobPosting, Application, TrainingMaterial,
|
||||
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
|
||||
SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,InterviewNote,
|
||||
SharedFormTemplate, Source, HiringAgency, IntegrationLog,BulkInterviewTemplate,JobPostingImage,Note,
|
||||
AgencyAccessLink, AgencyJobAssignment,Interview,ScheduledInterview
|
||||
)
|
||||
from django.contrib.auth import get_user_model
|
||||
@ -250,4 +250,4 @@ admin.site.register(ScheduledInterview)
|
||||
|
||||
|
||||
admin.site.register(JobPostingImage)
|
||||
admin.site.register(User)
|
||||
# admin.site.register(User)
|
||||
|
||||
@ -18,7 +18,7 @@ from .models import (
|
||||
BulkInterviewTemplate,
|
||||
BreakTime,
|
||||
JobPostingImage,
|
||||
InterviewNote,
|
||||
Note,
|
||||
ScheduledInterview,
|
||||
Source,
|
||||
HiringAgency,
|
||||
@ -720,94 +720,100 @@ class FormTemplateForm(forms.ModelForm):
|
||||
# BreakTimeFormSet = formset_factory(BreakTimeForm, extra=1, can_delete=True)
|
||||
|
||||
|
||||
# class BulkInterviewTemplateForm(forms.ModelForm):
|
||||
# applications = forms.ModelMultipleChoiceField(
|
||||
# queryset=Application.objects.none(),
|
||||
# widget=forms.CheckboxSelectMultiple,
|
||||
# required=True,
|
||||
# )
|
||||
# working_days = forms.MultipleChoiceField(
|
||||
# choices=[
|
||||
# (0, "Monday"),
|
||||
# (1, "Tuesday"),
|
||||
# (2, "Wednesday"),
|
||||
# (3, "Thursday"),
|
||||
# (4, "Friday"),
|
||||
# (5, "Saturday"),
|
||||
# (6, "Sunday"),
|
||||
# ],
|
||||
# widget=forms.CheckboxSelectMultiple,
|
||||
# required=True,
|
||||
# )
|
||||
class BulkInterviewTemplateForm(forms.ModelForm):
|
||||
applications = forms.ModelMultipleChoiceField(
|
||||
queryset=Application.objects.none(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=True,
|
||||
)
|
||||
working_days = forms.MultipleChoiceField(
|
||||
choices=[
|
||||
(0, "Monday"),
|
||||
(1, "Tuesday"),
|
||||
(2, "Wednesday"),
|
||||
(3, "Thursday"),
|
||||
(4, "Friday"),
|
||||
(5, "Saturday"),
|
||||
(6, "Sunday"),
|
||||
],
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=True,
|
||||
)
|
||||
|
||||
# class Meta:
|
||||
# model = BulkInterviewTemplate
|
||||
# fields = [
|
||||
# 'schedule_interview_type',
|
||||
# "applications",
|
||||
# "start_date",
|
||||
# "end_date",
|
||||
# "working_days",
|
||||
# "start_time",
|
||||
# "end_time",
|
||||
# "interview_duration",
|
||||
# "buffer_time",
|
||||
# "break_start_time",
|
||||
# "break_end_time",
|
||||
# ]
|
||||
# widgets = {
|
||||
# "start_date": forms.DateInput(
|
||||
# attrs={"type": "date", "class": "form-control"}
|
||||
# ),
|
||||
# "end_date": forms.DateInput(
|
||||
# attrs={"type": "date", "class": "form-control"}
|
||||
# ),
|
||||
# "start_time": forms.TimeInput(
|
||||
# attrs={"type": "time", "class": "form-control"}
|
||||
# ),
|
||||
# "end_time": forms.TimeInput(
|
||||
# attrs={"type": "time", "class": "form-control"}
|
||||
# ),
|
||||
# "interview_duration": forms.NumberInput(attrs={"class": "form-control"}),
|
||||
# "buffer_time": forms.NumberInput(attrs={"class": "form-control"}),
|
||||
# "break_start_time": forms.TimeInput(
|
||||
# attrs={"type": "time", "class": "form-control"}
|
||||
# ),
|
||||
# "break_end_time": forms.TimeInput(
|
||||
# attrs={"type": "time", "class": "form-control"}
|
||||
# ),
|
||||
# "schedule_interview_type":forms.RadioSelect()
|
||||
# }
|
||||
class Meta:
|
||||
model = BulkInterviewTemplate
|
||||
fields = [
|
||||
'schedule_interview_type',
|
||||
'topic',
|
||||
'physical_address',
|
||||
"applications",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"working_days",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"interview_duration",
|
||||
"buffer_time",
|
||||
"break_start_time",
|
||||
"break_end_time",
|
||||
]
|
||||
widgets = {
|
||||
"topic": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"start_date": forms.DateInput(
|
||||
attrs={"type": "date", "class": "form-control"}
|
||||
),
|
||||
"end_date": forms.DateInput(
|
||||
attrs={"type": "date", "class": "form-control"}
|
||||
),
|
||||
"start_time": forms.TimeInput(
|
||||
attrs={"type": "time", "class": "form-control"}
|
||||
),
|
||||
"end_time": forms.TimeInput(
|
||||
attrs={"type": "time", "class": "form-control"}
|
||||
),
|
||||
"interview_duration": forms.NumberInput(attrs={"class": "form-control"}),
|
||||
"buffer_time": forms.NumberInput(attrs={"class": "form-control"}),
|
||||
"break_start_time": forms.TimeInput(
|
||||
attrs={"type": "time", "class": "form-control"}
|
||||
),
|
||||
"break_end_time": forms.TimeInput(
|
||||
attrs={"type": "time", "class": "form-control"}
|
||||
),
|
||||
"schedule_interview_type":forms.RadioSelect(),
|
||||
"physical_address": forms.Textarea(
|
||||
attrs={"class": "form-control", "rows": 3, "placeholder": "Enter physical address if 'In-Person' is selected"}
|
||||
),
|
||||
}
|
||||
|
||||
# def __init__(self, slug, *args, **kwargs):
|
||||
# super().__init__(*args, **kwargs)
|
||||
# self.fields["applications"].queryset = Application.objects.filter(
|
||||
# job__slug=slug, stage="Interview"
|
||||
# )
|
||||
def __init__(self, slug, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["applications"].queryset = Application.objects.filter(
|
||||
job__slug=slug, stage="Interview"
|
||||
)
|
||||
|
||||
# def clean_working_days(self):
|
||||
# working_days = self.cleaned_data.get("working_days")
|
||||
# return [int(day) for day in working_days]
|
||||
def clean_working_days(self):
|
||||
working_days = self.cleaned_data.get("working_days")
|
||||
return [int(day) for day in working_days]
|
||||
|
||||
|
||||
# class InterviewNoteForm(forms.ModelForm):
|
||||
# """Form for creating and editing meeting comments"""
|
||||
class NoteForm(forms.ModelForm):
|
||||
"""Form for creating and editing meeting comments"""
|
||||
|
||||
# class Meta:
|
||||
# model = InterviewNote
|
||||
# fields = ["content"]
|
||||
# widgets = {
|
||||
# "content": CKEditor5Widget(
|
||||
# attrs={
|
||||
# "class": "form-control",
|
||||
# "placeholder": _("Enter your comment or note"),
|
||||
# },
|
||||
# config_name="extends",
|
||||
# ),
|
||||
# }
|
||||
# labels = {
|
||||
# "content": _("Comment"),
|
||||
# }
|
||||
class Meta:
|
||||
model = Note
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"content": CKEditor5Widget(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": _("Enter your comment or note"),
|
||||
},
|
||||
config_name="extends",
|
||||
),
|
||||
}
|
||||
labels = {
|
||||
"content": _("Note"),
|
||||
}
|
||||
|
||||
# def __init__(self, *args, **kwargs):
|
||||
# super().__init__(*args, **kwargs)
|
||||
@ -891,6 +897,15 @@ class JobPostingStatusForm(forms.ModelForm):
|
||||
widgets = {
|
||||
"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):
|
||||
@ -2090,23 +2105,19 @@ class CandidateEmailForm(forms.Form):
|
||||
|
||||
|
||||
|
||||
|
||||
from django.forms import HiddenInput
|
||||
class MessageForm(forms.ModelForm):
|
||||
"""Form for creating and editing messages between users"""
|
||||
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = ["recipient", "job", "subject", "content", "message_type"]
|
||||
fields = ["job","recipient", "subject", "content", "message_type"]
|
||||
widgets = {
|
||||
"recipient": forms.Select(
|
||||
attrs={"class": "form-select", "placeholder": "Select recipient","required": True,}
|
||||
),
|
||||
"job": forms.Select(
|
||||
attrs={"class": "form-select", "placeholder": "Select job",
|
||||
"hx-get": "/en/messages/create/",
|
||||
"hx-target": "#id_recipient",
|
||||
"hx-select": "#id_recipient",
|
||||
"hx-swap": "outerHTML",}
|
||||
attrs={"class": "form-select", "placeholder": "Select job"}
|
||||
),
|
||||
"subject": forms.TextInput(
|
||||
attrs={
|
||||
@ -2180,7 +2191,7 @@ class MessageForm(forms.ModelForm):
|
||||
self.fields["job"].queryset = JobPosting.objects.filter(
|
||||
id__in=job_ids
|
||||
).order_by("-created_at")
|
||||
|
||||
|
||||
print("Agency user job queryset:", self.fields["job"].queryset)
|
||||
elif self.user.user_type == "candidate":
|
||||
# Candidates can only see jobs they applied for
|
||||
@ -2210,6 +2221,8 @@ class MessageForm(forms.ModelForm):
|
||||
self.fields["recipient"].queryset = User.objects.filter(
|
||||
user_type="staff"
|
||||
).order_by("username")
|
||||
|
||||
|
||||
|
||||
def clean(self):
|
||||
"""Validate message form data"""
|
||||
|
||||
@ -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.validators
|
||||
@ -31,6 +31,18 @@ class Migration(migrations.Migration):
|
||||
('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(
|
||||
name='FormStage',
|
||||
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')),
|
||||
('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')),
|
||||
('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')),
|
||||
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
|
||||
('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')),
|
||||
('password', models.CharField(blank=True, max_length=20, 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)),
|
||||
('join_before_host', models.BooleanField(default=False)),
|
||||
('host_email', models.CharField(blank=True, max_length=255, null=True)),
|
||||
@ -278,24 +290,6 @@ class Migration(migrations.Migration):
|
||||
'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(
|
||||
name='JobPosting',
|
||||
fields=[
|
||||
@ -363,12 +357,15 @@ class Migration(migrations.Migration):
|
||||
('start_date', models.DateField(db_index=True, verbose_name='Start Date')),
|
||||
('end_date', models.DateField(db_index=True, verbose_name='End Date')),
|
||||
('working_days', models.JSONField(verbose_name='Working Days')),
|
||||
('topic', models.CharField(max_length=255, verbose_name='Interview Topic')),
|
||||
('start_time', models.TimeField(verbose_name='Start Time')),
|
||||
('end_time', models.TimeField(verbose_name='End Time')),
|
||||
('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')),
|
||||
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (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')),
|
||||
('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)')),
|
||||
@ -438,6 +435,25 @@ class Migration(migrations.Migration):
|
||||
'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(
|
||||
name='Notification',
|
||||
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')),
|
||||
('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')),
|
||||
('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={
|
||||
'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(
|
||||
verbose_name=_("Working Days")
|
||||
)
|
||||
topic = models.CharField(max_length=255, verbose_name=_("Interview Topic"))
|
||||
|
||||
start_time = models.TimeField(verbose_name=_("Start Time"))
|
||||
end_time = models.TimeField(verbose_name=_("End Time"))
|
||||
@ -1414,6 +1415,14 @@ class BulkInterviewTemplate(Base):
|
||||
buffer_time = models.PositiveIntegerField(
|
||||
verbose_name=_("Buffer Time (minutes)"), default=0
|
||||
)
|
||||
schedule_interview_type = models.CharField(
|
||||
max_length=10,
|
||||
choices=[('Remote', 'Remote (e.g., Zoom)'), ('Onsite', 'In-Person (Physical Location)')],
|
||||
default='Onsite',
|
||||
verbose_name=_("Interview Type"),
|
||||
)
|
||||
physical_address = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
created_by = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -1509,7 +1518,7 @@ class ScheduledInterview(Base):
|
||||
return self.interview_location
|
||||
# --- 3. Interview Notes Model (Fixed) ---
|
||||
|
||||
class InterviewNote(Base):
|
||||
class Note(Base):
|
||||
"""Model for storing notes, feedback, or comments related to a specific ScheduledInterview."""
|
||||
|
||||
class NoteType(models.TextChoices):
|
||||
@ -1517,13 +1526,24 @@ class InterviewNote(Base):
|
||||
LOGISTICS = 'Logistics', _('Logistical Note')
|
||||
GENERAL = 'General', _('General Comment')
|
||||
|
||||
1
|
||||
|
||||
application = models.ForeignKey(
|
||||
Application,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="notes",
|
||||
verbose_name=_("Application"),
|
||||
db_index=True,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
interview = models.ForeignKey(
|
||||
Interview,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="notes",
|
||||
verbose_name=_("Scheduled Interview"),
|
||||
db_index=True
|
||||
db_index=True,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
author = models.ForeignKey(
|
||||
@ -2692,3 +2712,5 @@ class Document(Base):
|
||||
if self.file:
|
||||
return self.file.name.split(".")[-1].upper()
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ from . linkedin_service import LinkedInService
|
||||
from django.shortcuts import get_object_or_404
|
||||
from . models import JobPosting
|
||||
from django.utils import timezone
|
||||
from . models import ScheduledInterview,Interview,Message
|
||||
from . models import BulkInterviewTemplate,Interview,Message,ScheduledInterview
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
# Add python-docx import for Word document processing
|
||||
@ -27,9 +27,9 @@ except ImportError:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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 = '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.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
@ -668,7 +669,7 @@ def handle_resume_parsing_and_scoring(pk: int):
|
||||
|
||||
from django.utils import timezone
|
||||
def create_interview_and_meeting(
|
||||
candidate_id,
|
||||
application_id,
|
||||
job_id,
|
||||
schedule_id,
|
||||
slot_date,
|
||||
@ -679,24 +680,13 @@ def create_interview_and_meeting(
|
||||
Synchronous task for a single interview slot, dispatched by django-q.
|
||||
"""
|
||||
try:
|
||||
application = Application.objects.get(pk=candidate_id)
|
||||
application = Application.objects.get(pk=application_id)
|
||||
job = JobPosting.objects.get(pk=job_id)
|
||||
schedule = ScheduledInterview.objects.get(pk=schedule_id)
|
||||
schedule = BulkInterviewTemplate.objects.get(pk=schedule_id)
|
||||
|
||||
interview_datetime = timezone.make_aware(datetime.combine(slot_date, slot_time))
|
||||
meeting_topic = f"Interview for {job.title} - {application.name}"
|
||||
meeting_topic = schedule.topic
|
||||
|
||||
# 1. External API Call (Slow)
|
||||
# "status": "success",
|
||||
# "message": "Meeting created successfully.",
|
||||
# "meeting_details": {
|
||||
# "join_url": meeting_data['join_url'],
|
||||
# "meeting_id": meeting_data['id'],
|
||||
# "password": meeting_data['password'],
|
||||
# "host_email": meeting_data['host_email']
|
||||
# },
|
||||
# "zoom_gateway_response": meeting_data
|
||||
# }
|
||||
result = create_zoom_meeting(meeting_topic, interview_datetime, duration)
|
||||
|
||||
if result["status"] == "success":
|
||||
@ -711,33 +701,19 @@ def create_interview_and_meeting(
|
||||
password=result["meeting_details"]["password"],
|
||||
location_type="Remote"
|
||||
)
|
||||
schedule = ScheduledInterview.objects.create(
|
||||
application=application,
|
||||
job=job,
|
||||
schedule=schedule,
|
||||
interview_date=slot_date,
|
||||
interview_time=slot_time,
|
||||
interview=interview
|
||||
)
|
||||
schedule.interview = interview
|
||||
schedule.status = "scheduled"
|
||||
|
||||
schedule.save()
|
||||
|
||||
# 2. Database Writes (Slow)
|
||||
# zoom_meeting = ZoomMeetingDetails.objects.create(
|
||||
# topic=meeting_topic,
|
||||
# start_time=interview_datetime,
|
||||
# duration=duration,
|
||||
# meeting_id=result["meeting_details"]["meeting_id"],
|
||||
# details_url=result["meeting_details"]["join_url"],
|
||||
# zoom_gateway_response=result["zoom_gateway_response"],
|
||||
# host_email=result["meeting_details"]["host_email"],
|
||||
# password=result["meeting_details"]["password"],
|
||||
# location_type="Remote"
|
||||
# )
|
||||
# ScheduledInterview.objects.create(
|
||||
# application=candidate,
|
||||
# job=job,
|
||||
# interview_location=zoom_meeting,
|
||||
# schedule=schedule,
|
||||
# interview_date=slot_date,
|
||||
# interview_time=slot_time
|
||||
# )
|
||||
|
||||
# Log success or use Django-Q result system for monitoring
|
||||
logger.info(f"Successfully scheduled interview for {Application.name}")
|
||||
return True # Task succeeded
|
||||
else:
|
||||
|
||||
@ -587,16 +587,16 @@ urlpatterns = [
|
||||
# path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'),
|
||||
|
||||
#interview and meeting related urls
|
||||
# path(
|
||||
# "jobs/<slug:slug>/schedule-interviews/",
|
||||
# views.schedule_interviews_view,
|
||||
# name="schedule_interviews",
|
||||
# ),
|
||||
# path(
|
||||
# "jobs/<slug:slug>/confirm-schedule-interviews/",
|
||||
# views.confirm_schedule_interviews_view,
|
||||
# name="confirm_schedule_interviews_view",
|
||||
# ),
|
||||
path(
|
||||
"jobs/<slug:slug>/schedule-interviews/",
|
||||
views.schedule_interviews_view,
|
||||
name="schedule_interviews",
|
||||
),
|
||||
path(
|
||||
"jobs/<slug:slug>/confirm-schedule-interviews/",
|
||||
views.confirm_schedule_interviews_view,
|
||||
name="confirm_schedule_interviews_view",
|
||||
),
|
||||
|
||||
# path(
|
||||
# "meetings/create-meeting/",
|
||||
@ -682,5 +682,6 @@ urlpatterns = [
|
||||
# Email invitation URLs
|
||||
# path("interviews/meetings/<slug:slug>/send-application-invitation/", views.send_application_invitation, name="send_application_invitation"),
|
||||
# path("interviews/meetings/<slug:slug>/send-participants-invitation/", views.send_participants_invitation, name="send_participants_invitation"),
|
||||
|
||||
path("note/<slug:slug>/application_add_note/", views.application_add_note, name="application_add_note"),
|
||||
path("note/<slug:slug>/interview_add_note/", views.interview_add_note, name="interview_add_note"),
|
||||
]
|
||||
|
||||
@ -33,7 +33,8 @@ from .forms import (
|
||||
PasswordResetForm,
|
||||
StaffAssignmentForm,
|
||||
RemoteInterviewForm,
|
||||
OnsiteInterviewForm
|
||||
OnsiteInterviewForm,
|
||||
BulkInterviewTemplateForm
|
||||
)
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
@ -132,7 +133,8 @@ from .models import (
|
||||
Source,
|
||||
Message,
|
||||
Document,
|
||||
Interview
|
||||
Interview,
|
||||
BulkInterviewTemplate
|
||||
)
|
||||
|
||||
|
||||
@ -517,6 +519,7 @@ def job_detail(request, slug):
|
||||
job_status = status_form.cleaned_data["status"]
|
||||
form_template = job.form_template
|
||||
if job_status == "ACTIVE":
|
||||
|
||||
form_template.is_active = True
|
||||
form_template.save(update_fields=["is_active"])
|
||||
else:
|
||||
@ -533,7 +536,9 @@ def job_detail(request, slug):
|
||||
|
||||
return redirect("job_detail", slug=slug)
|
||||
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) ---
|
||||
|
||||
@ -605,10 +610,13 @@ def job_detail(request, slug):
|
||||
if avg_t_in_exam_duration
|
||||
else 0
|
||||
)
|
||||
|
||||
|
||||
category_data = (
|
||||
applications.filter(ai_analysis_data__analysis_data_en__category__isnull=False)
|
||||
.values("ai_analysis_data__analysis_data_en__category")
|
||||
applications.filter(
|
||||
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(
|
||||
application_count=Count("id"),
|
||||
category=Cast(
|
||||
@ -617,6 +625,7 @@ def job_detail(request, slug):
|
||||
)
|
||||
.order_by("ai_analysis_data__analysis_data_en__category")
|
||||
)
|
||||
|
||||
# Prepare data for Chart.js
|
||||
categories = [item["category"] for item in category_data]
|
||||
applications_count = [item["application_count"] for item in category_data]
|
||||
@ -711,7 +720,7 @@ def request_cvs_download(request, slug):
|
||||
if not job.applications.exists():
|
||||
messages.warning(request, _("No applications found for this job. ZIP file generation skipped."))
|
||||
return redirect('job_detail', slug=slug)
|
||||
|
||||
|
||||
async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
|
||||
|
||||
# Provide user feedback and redirect
|
||||
@ -1463,323 +1472,293 @@ def form_submission_details(request, template_id, slug):
|
||||
)
|
||||
|
||||
|
||||
# def _handle_get_request(request, slug, job):
|
||||
# """
|
||||
# Handles GET requests, setting up forms and restoring candidate selections
|
||||
# from the session for persistence.
|
||||
# """
|
||||
# SESSION_KEY = f"schedule_candidate_ids_{slug}"
|
||||
def _handle_get_request(request, slug, job):
|
||||
"""
|
||||
Handles GET requests, setting up forms and restoring candidate selections
|
||||
from the session for persistence.
|
||||
"""
|
||||
SESSION_KEY = f"schedule_candidate_ids_{slug}"
|
||||
|
||||
# form = BulkInterviewTemplateForm(slug=slug)
|
||||
# # break_formset = BreakTimeFormSet(prefix='breaktime')
|
||||
form = BulkInterviewTemplateForm(slug=slug)
|
||||
# break_formset = BreakTimeFormSet(prefix='breaktime')
|
||||
|
||||
# selected_ids = []
|
||||
selected_ids = []
|
||||
|
||||
# # 1. Capture IDs from HTMX request and store in session (when first clicked)
|
||||
# if "HX-Request" in request.headers:
|
||||
# candidate_ids = request.GET.getlist("candidate_ids")
|
||||
# 1. Capture IDs from HTMX request and store in session (when first clicked)
|
||||
if "HX-Request" in request.headers:
|
||||
candidate_ids = request.GET.getlist("candidate_ids")
|
||||
|
||||
# if candidate_ids:
|
||||
# request.session[SESSION_KEY] = candidate_ids
|
||||
# selected_ids = candidate_ids
|
||||
if candidate_ids:
|
||||
request.session[SESSION_KEY] = candidate_ids
|
||||
selected_ids = candidate_ids
|
||||
|
||||
# # 2. Restore IDs from session (on refresh or navigation)
|
||||
# if not selected_ids:
|
||||
# selected_ids = request.session.get(SESSION_KEY, [])
|
||||
# 2. Restore IDs from session (on refresh or navigation)
|
||||
if not selected_ids:
|
||||
selected_ids = request.session.get(SESSION_KEY, [])
|
||||
|
||||
# # 3. Use the list of IDs to initialize the form
|
||||
# if selected_ids:
|
||||
# candidates_to_load = Application.objects.filter(pk__in=selected_ids)
|
||||
# print(candidates_to_load)
|
||||
# form.initial["applications"] = candidates_to_load
|
||||
# 3. Use the list of IDs to initialize the form
|
||||
if selected_ids:
|
||||
candidates_to_load = Application.objects.filter(pk__in=selected_ids)
|
||||
form.initial["applications"] = candidates_to_load
|
||||
|
||||
# return render(
|
||||
# request,
|
||||
# "interviews/schedule_interviews.html",
|
||||
# {"form": form, "job": job},
|
||||
# )
|
||||
|
||||
|
||||
#TODO:MAIN FUNCTIONS
|
||||
# def _handle_preview_submission(request, slug, job):
|
||||
# """
|
||||
# Handles the initial POST request (Preview Schedule).
|
||||
# Validates forms, calculates slots, saves data to session, and renders preview.
|
||||
# """
|
||||
# SESSION_DATA_KEY = "interview_schedule_data"
|
||||
# form = BulkInterviewTemplateForm(slug, request.POST)
|
||||
# # break_formset = BreakTimeFormSet(request.POST,prefix='breaktime')
|
||||
|
||||
# if form.is_valid():
|
||||
# # Get the form data
|
||||
# applications = form.cleaned_data["applications"]
|
||||
# start_date = form.cleaned_data["start_date"]
|
||||
# end_date = form.cleaned_data["end_date"]
|
||||
# working_days = form.cleaned_data["working_days"]
|
||||
# start_time = form.cleaned_data["start_time"]
|
||||
# end_time = form.cleaned_data["end_time"]
|
||||
# interview_duration = form.cleaned_data["interview_duration"]
|
||||
# buffer_time = form.cleaned_data["buffer_time"]
|
||||
# break_start_time = form.cleaned_data["break_start_time"]
|
||||
# break_end_time = form.cleaned_data["break_end_time"]
|
||||
# schedule_interview_type=form.cleaned_data["schedule_interview_type"]
|
||||
# # Process break times
|
||||
# # breaks = []
|
||||
# # for break_form in break_formset:
|
||||
# # print(break_form.cleaned_data)
|
||||
# # if break_form.cleaned_data and not break_form.cleaned_data.get("DELETE"):
|
||||
# # breaks.append(
|
||||
# # {
|
||||
# # "start_time": break_form.cleaned_data["start_time"].strftime("%H:%M:%S"),
|
||||
# # "end_time": break_form.cleaned_data["end_time"].strftime("%H:%M:%S"),
|
||||
# # }
|
||||
# # )
|
||||
|
||||
# # Create a temporary schedule object (not saved to DB)
|
||||
# temp_schedule = BulkInterviewTemplate(
|
||||
# job=job,
|
||||
# start_date=start_date,
|
||||
# end_date=end_date,
|
||||
# working_days=working_days,
|
||||
# start_time=start_time,
|
||||
# end_time=end_time,
|
||||
# interview_duration=interview_duration,
|
||||
# buffer_time=buffer_time or 5,
|
||||
# break_start_time=break_start_time or None,
|
||||
# break_end_time=break_end_time or None,
|
||||
# )
|
||||
|
||||
# # Get available slots (temp_breaks logic moved into get_available_time_slots if needed)
|
||||
# available_slots = get_available_time_slots(temp_schedule)
|
||||
|
||||
# if len(available_slots) < len(applications):
|
||||
# messages.error(
|
||||
# request,
|
||||
# f"Not enough available slots. Required: {len(applications)}, Available: {len(available_slots)}",
|
||||
# )
|
||||
# return render(
|
||||
# request,
|
||||
# "interviews/schedule_interviews.html",
|
||||
# {"form": form, "job": job},
|
||||
# )
|
||||
|
||||
# # Create a preview schedule
|
||||
# preview_schedule = []
|
||||
# for i, application in enumerate(applications):
|
||||
# slot = available_slots[i]
|
||||
# preview_schedule.append(
|
||||
# {"application": application, "date": slot["date"], "time": slot["time"]}
|
||||
# )
|
||||
|
||||
# # Save the form data to session for later use
|
||||
# schedule_data = {
|
||||
# "start_date": start_date.isoformat(),
|
||||
# "end_date": end_date.isoformat(),
|
||||
# "working_days": working_days,
|
||||
# "start_time": start_time.isoformat(),
|
||||
# "end_time": end_time.isoformat(),
|
||||
# "interview_duration": interview_duration,
|
||||
# "buffer_time": buffer_time,
|
||||
# "break_start_time": break_start_time.isoformat() if break_start_time else None,
|
||||
# "break_end_time": break_end_time.isoformat() if break_end_time else None,
|
||||
# "candidate_ids": [c.id for c in applications],
|
||||
# "schedule_interview_type":schedule_interview_type
|
||||
|
||||
# }
|
||||
# request.session[SESSION_DATA_KEY] = schedule_data
|
||||
|
||||
# # Render the preview page
|
||||
# return render(
|
||||
# request,
|
||||
# "interviews/preview_schedule.html",
|
||||
# {
|
||||
# "job": job,
|
||||
# "schedule": preview_schedule,
|
||||
# "start_date": start_date,
|
||||
# "end_date": end_date,
|
||||
# "working_days": working_days,
|
||||
# "start_time": start_time,
|
||||
# "end_time": end_time,
|
||||
# "break_start_time": break_start_time,
|
||||
# "break_end_time": break_end_time,
|
||||
# "interview_duration": interview_duration,
|
||||
# "buffer_time": buffer_time,
|
||||
# "schedule_interview_type":schedule_interview_type,
|
||||
# "form":OnsiteLocationForm()
|
||||
# },
|
||||
# )
|
||||
# else:
|
||||
# # Re-render the form if validation fails
|
||||
# return render(
|
||||
# request,
|
||||
# "interviews/schedule_interviews.html",
|
||||
# {"form": form, "job": job},
|
||||
# )
|
||||
|
||||
|
||||
# def _handle_confirm_schedule(request, slug, job):
|
||||
# """
|
||||
# Handles the final POST request (Confirm Schedule).
|
||||
# Creates the main schedule record and queues individual interviews asynchronously.
|
||||
# """
|
||||
|
||||
# SESSION_DATA_KEY = "interview_schedule_data"
|
||||
# SESSION_ID_KEY = f"schedule_candidate_ids_{slug}"
|
||||
|
||||
# # 1. Get schedule data from session
|
||||
# schedule_data = request.session.get(SESSION_DATA_KEY)
|
||||
|
||||
# if not schedule_data:
|
||||
# messages.error(request, "Session expired. Please try again.")
|
||||
# return redirect("schedule_interviews", slug=slug)
|
||||
|
||||
# # 2. Create the Interview Schedule (Parent Record)
|
||||
# try:
|
||||
# # Handle break times: If they exist, convert them; otherwise, pass None.
|
||||
# break_start = schedule_data.get("break_start_time")
|
||||
# break_end = schedule_data.get("break_end_time")
|
||||
|
||||
# schedule = BulkInterviewTemplate.objects.create(
|
||||
# job=job,
|
||||
# created_by=request.user,
|
||||
# start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
|
||||
# end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
|
||||
# working_days=schedule_data["working_days"],
|
||||
# start_time=time.fromisoformat(schedule_data["start_time"]),
|
||||
# end_time=time.fromisoformat(schedule_data["end_time"]),
|
||||
# interview_duration=schedule_data["interview_duration"],
|
||||
# buffer_time=schedule_data["buffer_time"],
|
||||
# # Convert time strings to time objects only if they exist and handle None gracefully
|
||||
# break_start_time=time.fromisoformat(break_start) if break_start else None,
|
||||
# break_end_time=time.fromisoformat(break_end) if break_end else None,
|
||||
# schedule_interview_type=schedule_data.get("schedule_interview_type")
|
||||
# )
|
||||
# except Exception as e:
|
||||
# # Clear data on failure to prevent stale data causing repeated errors
|
||||
# messages.error(request, f"Error creating schedule: {e}")
|
||||
# if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||
# if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||
# return redirect("schedule_interviews", slug=slug)
|
||||
|
||||
# # 3. Setup candidates and get slots
|
||||
# candidates = Application.objects.filter(id__in=schedule_data["candidate_ids"])
|
||||
# schedule.applications.set(candidates)
|
||||
# available_slots = get_available_time_slots(schedule)
|
||||
|
||||
# # 4. Handle Remote/Onsite logic
|
||||
# if schedule_data.get("schedule_interview_type") == 'Remote':
|
||||
# # ... (Remote logic remains unchanged)
|
||||
# queued_count = 0
|
||||
# for i, candidate in enumerate(candidates):
|
||||
# if i < len(available_slots):
|
||||
# slot = available_slots[i]
|
||||
|
||||
# async_task(
|
||||
# "recruitment.tasks.create_interview_and_meeting",
|
||||
# candidate.pk, job.pk, schedule.pk, slot["date"], slot["time"], schedule.interview_duration,
|
||||
# )
|
||||
# queued_count += 1
|
||||
|
||||
# messages.success(
|
||||
# request,
|
||||
# f"Schedule successfully created. Queued {queued_count} interviews to be booked asynchronously. Check back in a moment!",
|
||||
# )
|
||||
|
||||
# if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||
# if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||
|
||||
# return redirect("job_detail", slug=slug)
|
||||
|
||||
# elif schedule_data.get("schedule_interview_type") == 'Onsite':
|
||||
# print("inside...")
|
||||
|
||||
# if request.method == 'POST':
|
||||
# form = OnsiteLocationForm(request.POST)
|
||||
|
||||
# if form.is_valid():
|
||||
|
||||
# if not available_slots:
|
||||
# messages.error(request, "No available slots found for the selected schedule range.")
|
||||
# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job})
|
||||
|
||||
# # Extract common location data from the form
|
||||
# physical_address = form.cleaned_data['physical_address']
|
||||
# room_number = form.cleaned_data['room_number']
|
||||
# topic=form.cleaned_data['topic']
|
||||
|
||||
|
||||
# try:
|
||||
# # 1. Iterate over candidates and create a NEW Location object for EACH
|
||||
# for i, candidate in enumerate(candidates):
|
||||
# if i < len(available_slots):
|
||||
# slot = available_slots[i]
|
||||
|
||||
|
||||
# location_start_dt = datetime.combine(slot['date'], schedule.start_time)
|
||||
|
||||
# # --- CORE FIX: Create a NEW Location object inside the loop ---
|
||||
# onsite_location = OnsiteLocationDetails.objects.create(
|
||||
# start_time=location_start_dt,
|
||||
# duration=schedule.interview_duration,
|
||||
# physical_address=physical_address,
|
||||
# room_number=room_number,
|
||||
# location_type="Onsite",
|
||||
# topic=topic
|
||||
|
||||
# )
|
||||
|
||||
# # 2. Create the ScheduledInterview, linking the unique location
|
||||
# ScheduledInterview.objects.create(
|
||||
# application=candidate,
|
||||
# job=job,
|
||||
# schedule=schedule,
|
||||
# interview_date=slot['date'],
|
||||
# interview_time=slot['time'],
|
||||
# interview_location=onsite_location,
|
||||
# )
|
||||
|
||||
# messages.success(
|
||||
# request,
|
||||
# f"Onsite schedule interviews created successfully for {len(candidates)} candidates."
|
||||
# )
|
||||
|
||||
# # Clear session data keys upon successful completion
|
||||
# if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||
# if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||
|
||||
# return redirect('job_detail', slug=job.slug)
|
||||
|
||||
# except Exception as e:
|
||||
# messages.error(request, f"Error creating onsite location/interviews: {e}")
|
||||
# # On failure, re-render the form with the error and ensure 'job' is present
|
||||
# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job})
|
||||
|
||||
# else:
|
||||
# # Form is invalid, re-render with errors
|
||||
# # Ensure 'job' is passed to prevent NoReverseMatch
|
||||
# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job})
|
||||
|
||||
# else:
|
||||
# # For a GET request
|
||||
# form = OnsiteLocationForm()
|
||||
|
||||
# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job})
|
||||
return render(
|
||||
request,
|
||||
"interviews/schedule_interviews.html",
|
||||
{"form": form, "job": job},
|
||||
)
|
||||
|
||||
|
||||
|
||||
# def schedule_interviews_view(request, slug):
|
||||
# job = get_object_or_404(JobPosting, slug=slug)
|
||||
# if request.method == "POST":
|
||||
# # return _handle_confirm_schedule(request, slug, job)
|
||||
# return _handle_preview_submission(request, slug, job)
|
||||
# else:
|
||||
# return _handle_get_request(request, slug, job)
|
||||
def _handle_preview_submission(request, slug, job):
|
||||
"""
|
||||
Handles the initial POST request (Preview Schedule).
|
||||
Validates forms, calculates slots, saves data to session, and renders preview.
|
||||
"""
|
||||
SESSION_DATA_KEY = "interview_schedule_data"
|
||||
form = BulkInterviewTemplateForm(slug, request.POST)
|
||||
# break_formset = BreakTimeFormSet(request.POST,prefix='breaktime')
|
||||
|
||||
if form.is_valid():
|
||||
# Get the form data
|
||||
applications = form.cleaned_data["applications"]
|
||||
start_date = form.cleaned_data["start_date"]
|
||||
end_date = form.cleaned_data["end_date"]
|
||||
working_days = form.cleaned_data["working_days"]
|
||||
start_time = form.cleaned_data["start_time"]
|
||||
end_time = form.cleaned_data["end_time"]
|
||||
interview_duration = form.cleaned_data["interview_duration"]
|
||||
buffer_time = form.cleaned_data["buffer_time"]
|
||||
break_start_time = form.cleaned_data["break_start_time"]
|
||||
break_end_time = form.cleaned_data["break_end_time"]
|
||||
schedule_interview_type=form.cleaned_data["schedule_interview_type"]
|
||||
physical_address=form.cleaned_data["physical_address"]
|
||||
# Process break times
|
||||
# breaks = []
|
||||
# for break_form in break_formset:
|
||||
# print(break_form.cleaned_data)
|
||||
# if break_form.cleaned_data and not break_form.cleaned_data.get("DELETE"):
|
||||
# breaks.append(
|
||||
# {
|
||||
# "start_time": break_form.cleaned_data["start_time"].strftime("%H:%M:%S"),
|
||||
# "end_time": break_form.cleaned_data["end_time"].strftime("%H:%M:%S"),
|
||||
# }
|
||||
# )
|
||||
|
||||
# Create a temporary schedule object (not saved to DB)
|
||||
temp_schedule = BulkInterviewTemplate(
|
||||
job=job,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
working_days=working_days,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
interview_duration=interview_duration,
|
||||
buffer_time=buffer_time or 5,
|
||||
break_start_time=break_start_time or None,
|
||||
break_end_time=break_end_time or None,
|
||||
schedule_interview_type=schedule_interview_type,
|
||||
physical_address=physical_address
|
||||
)
|
||||
|
||||
# Get available slots (temp_breaks logic moved into get_available_time_slots if needed)
|
||||
available_slots = get_available_time_slots(temp_schedule)
|
||||
|
||||
if len(available_slots) < len(applications):
|
||||
messages.error(
|
||||
request,
|
||||
f"Not enough available slots. Required: {len(applications)}, Available: {len(available_slots)}",
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"interviews/schedule_interviews.html",
|
||||
{"form": form, "job": job},
|
||||
)
|
||||
|
||||
# Create a preview schedule
|
||||
preview_schedule = []
|
||||
for i, application in enumerate(applications):
|
||||
slot = available_slots[i]
|
||||
preview_schedule.append(
|
||||
{"application": application, "date": slot["date"], "time": slot["time"]}
|
||||
)
|
||||
|
||||
# Save the form data to session for later use
|
||||
schedule_data = {
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
"working_days": working_days,
|
||||
"start_time": start_time.isoformat(),
|
||||
"end_time": end_time.isoformat(),
|
||||
"interview_duration": interview_duration,
|
||||
"buffer_time": buffer_time,
|
||||
"break_start_time": break_start_time.isoformat() if break_start_time else None,
|
||||
"break_end_time": break_end_time.isoformat() if break_end_time else None,
|
||||
"candidate_ids": [c.id for c in applications],
|
||||
"schedule_interview_type":schedule_interview_type,
|
||||
"physical_address":physical_address,
|
||||
"topic":form.cleaned_data.get("topic"),
|
||||
|
||||
}
|
||||
request.session[SESSION_DATA_KEY] = schedule_data
|
||||
|
||||
# Render the preview page
|
||||
return render(
|
||||
request,
|
||||
"interviews/preview_schedule.html",
|
||||
{
|
||||
"job": job,
|
||||
"schedule": preview_schedule,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"working_days": working_days,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"break_start_time": break_start_time,
|
||||
"break_end_time": break_end_time,
|
||||
"interview_duration": interview_duration,
|
||||
"buffer_time": buffer_time,
|
||||
# "schedule_interview_type":schedule_interview_type,
|
||||
# "form":OnsiteLocationForm()
|
||||
},
|
||||
)
|
||||
else:
|
||||
# Re-render the form if validation fails
|
||||
return render(
|
||||
request,
|
||||
"interviews/schedule_interviews.html",
|
||||
{"form": form, "job": job},
|
||||
)
|
||||
|
||||
|
||||
# def confirm_schedule_interviews_view(request, slug):
|
||||
# job = get_object_or_404(JobPosting, slug=slug)
|
||||
# if request.method == "POST":
|
||||
# return _handle_confirm_schedule(request, slug, job)
|
||||
def _handle_confirm_schedule(request, slug, job):
|
||||
"""
|
||||
Handles the final POST request (Confirm Schedule).
|
||||
Creates the main schedule record and queues individual interviews asynchronously.
|
||||
"""
|
||||
|
||||
SESSION_DATA_KEY = "interview_schedule_data"
|
||||
SESSION_ID_KEY = f"schedule_candidate_ids_{slug}"
|
||||
|
||||
# 1. Get schedule data from session
|
||||
schedule_data = request.session.get(SESSION_DATA_KEY)
|
||||
|
||||
if not schedule_data:
|
||||
messages.error(request, "Session expired. Please try again.")
|
||||
return redirect("schedule_interviews", slug=slug)
|
||||
|
||||
# 2. Create the Interview Schedule (Parent Record)
|
||||
try:
|
||||
# Handle break times: If they exist, convert them; otherwise, pass None.
|
||||
break_start = schedule_data.get("break_start_time")
|
||||
break_end = schedule_data.get("break_end_time")
|
||||
|
||||
schedule = BulkInterviewTemplate.objects.create(
|
||||
job=job,
|
||||
created_by=request.user,
|
||||
start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
|
||||
end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
|
||||
working_days=schedule_data["working_days"],
|
||||
start_time=time.fromisoformat(schedule_data["start_time"]),
|
||||
end_time=time.fromisoformat(schedule_data["end_time"]),
|
||||
interview_duration=schedule_data["interview_duration"],
|
||||
buffer_time=schedule_data["buffer_time"],
|
||||
break_start_time=time.fromisoformat(break_start) if break_start else None,
|
||||
break_end_time=time.fromisoformat(break_end) if break_end else None,
|
||||
schedule_interview_type=schedule_data.get("schedule_interview_type"),
|
||||
physical_address=schedule_data.get("physical_address"),
|
||||
topic=schedule_data.get("topic"),
|
||||
)
|
||||
except Exception as e:
|
||||
# Clear data on failure to prevent stale data causing repeated errors
|
||||
messages.error(request, f"Error creating schedule: {e}")
|
||||
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||
if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||
return redirect("schedule_interviews", slug=slug)
|
||||
|
||||
applications = Application.objects.filter(id__in=schedule_data["candidate_ids"])
|
||||
schedule.applications.set(applications)
|
||||
available_slots = get_available_time_slots(schedule)
|
||||
|
||||
if schedule_data.get("schedule_interview_type") == 'Remote':
|
||||
queued_count = 0
|
||||
for i, application in enumerate(applications):
|
||||
if i < len(available_slots):
|
||||
slot = available_slots[i]
|
||||
async_task(
|
||||
"recruitment.tasks.create_interview_and_meeting",
|
||||
application.pk, job.pk, schedule.pk, slot["date"], slot["time"], schedule.interview_duration,
|
||||
)
|
||||
queued_count += 1
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Schedule successfully created. Queued {queued_count} interviews to be booked asynchronously. Check back in a moment!",
|
||||
)
|
||||
|
||||
if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||
|
||||
return redirect("job_detail", slug=slug)
|
||||
|
||||
elif schedule_data.get("schedule_interview_type") == 'Onsite':
|
||||
try:
|
||||
for i, application in enumerate(applications):
|
||||
if i < len(available_slots):
|
||||
slot = available_slots[i]
|
||||
|
||||
start_dt = datetime.combine(slot['date'], schedule.start_time)
|
||||
|
||||
interview = Interview.objects.create(
|
||||
topic=schedule.topic,
|
||||
start_time=start_dt,
|
||||
duration=schedule.interview_duration,
|
||||
location_type="Onsite",
|
||||
physical_address=schedule.physical_address,
|
||||
)
|
||||
|
||||
# 2. Create the ScheduledInterview, linking the unique location
|
||||
ScheduledInterview.objects.create(
|
||||
application=application,
|
||||
job=job,
|
||||
schedule=schedule,
|
||||
interview_date=slot['date'],
|
||||
interview_time=slot['time'],
|
||||
interview=interview
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"created successfully for {len(applications)} application."
|
||||
)
|
||||
|
||||
# Clear session data keys upon successful completion
|
||||
if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||
|
||||
return redirect('job_detail', slug=job.slug)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error creating onsite interviews: {e}")
|
||||
return redirect("schedule_interviews", slug=slug)
|
||||
|
||||
|
||||
def schedule_interviews_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
if request.method == "POST":
|
||||
# return _handle_confirm_schedule(request, slug, job)
|
||||
return _handle_preview_submission(request, slug, job)
|
||||
else:
|
||||
# if request.session.get("interview_schedule_data"):
|
||||
print(request.session.get("interview_schedule_data"))
|
||||
return _handle_get_request(request, slug, job)
|
||||
# return redirect("applications_interview_view", slug=slug)
|
||||
|
||||
|
||||
def confirm_schedule_interviews_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
if request.method == "POST":
|
||||
# print(request.session['interview_schedule_data'])
|
||||
return _handle_confirm_schedule(request, slug, job)
|
||||
|
||||
|
||||
@staff_user_required
|
||||
@ -4716,7 +4695,7 @@ def message_detail(request, message_id):
|
||||
@login_required
|
||||
def message_create(request):
|
||||
"""Create a new message"""
|
||||
from .email_service import EmailService
|
||||
from .email_service import EmailService
|
||||
if request.method == "POST":
|
||||
form = MessageForm(request.user, request.POST)
|
||||
|
||||
@ -4727,10 +4706,7 @@ def message_create(request):
|
||||
# Send email if message_type is 'email' and recipient has email
|
||||
|
||||
if message.recipient and message.recipient.email:
|
||||
|
||||
try:
|
||||
|
||||
|
||||
email_result = async_task('recruitment.tasks._task_send_individual_email',
|
||||
subject=message.subject,
|
||||
body_message=message.content,
|
||||
@ -4758,9 +4734,35 @@ def message_create(request):
|
||||
|
||||
messages.error(request, "Please correct the errors below.")
|
||||
else:
|
||||
|
||||
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 = {
|
||||
"form": form,
|
||||
}
|
||||
@ -4896,7 +4898,7 @@ def message_delete(request, message_id):
|
||||
Redirects to the message list on success (either via standard redirect
|
||||
or HTMX's hx-redirect header).
|
||||
"""
|
||||
|
||||
|
||||
# 1. Retrieve the message
|
||||
# Use select_related to fetch linked objects efficiently for checks/logging
|
||||
message = get_object_or_404(
|
||||
@ -4907,13 +4909,13 @@ def message_delete(request, message_id):
|
||||
# Only the sender or recipient can delete the message
|
||||
if message.sender != request.user and message.recipient != request.user:
|
||||
messages.error(request, "You don't have permission to delete this message.")
|
||||
|
||||
|
||||
# HTMX requests should handle redirection via client-side logic (hx-redirect)
|
||||
if "HX-Request" in request.headers:
|
||||
# Returning 403 or 400 is ideal, but 200 with an empty body is often accepted
|
||||
# by HTMX and the message is shown on the next page/refresh.
|
||||
return HttpResponse(status=403)
|
||||
|
||||
return HttpResponse(status=403)
|
||||
|
||||
# Standard navigation redirect
|
||||
return redirect("message_list")
|
||||
|
||||
@ -4927,7 +4929,7 @@ def message_delete(request, message_id):
|
||||
# 1. Set the HTMX response header for redirection
|
||||
response = HttpResponse(status=200)
|
||||
response["HX-Redirect"] = reverse("message_list") # <--- EXPLICIT HEADER
|
||||
return response
|
||||
return response
|
||||
|
||||
# Standard navigation fallback
|
||||
return redirect("message_list")
|
||||
@ -5097,7 +5099,8 @@ def document_upload(request, slug):
|
||||
if upload_target == 'person':
|
||||
return redirect("applicant_portal_dashboard")
|
||||
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
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
@ -5109,7 +5112,6 @@ def document_upload(request, slug):
|
||||
def document_delete(request, document_id):
|
||||
"""Delete a document"""
|
||||
document = get_object_or_404(Document, id=document_id)
|
||||
print(document)
|
||||
|
||||
# Initialize variables for redirection outside of the complex logic
|
||||
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":
|
||||
# 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.
|
||||
return HttpResponse(status=200)
|
||||
response = HttpResponse(status=200)
|
||||
response["HX-Refresh"] = "true" # Instruct HTMX to refresh the current view
|
||||
return response
|
||||
|
||||
# --- Standard Navigation Fallback ---
|
||||
else:
|
||||
@ -6583,3 +6587,57 @@ def interview_detail(request, slug):
|
||||
# messages.error(request, f"Failed to send invitation emails: {str(e)}")
|
||||
|
||||
# return redirect('meeting_details', slug=slug)
|
||||
|
||||
def application_add_note(request, slug):
|
||||
from .models import Note
|
||||
from .forms import NoteForm
|
||||
|
||||
application = get_object_or_404(Application, slug=slug)
|
||||
notes = Note.objects.filter(application=application).order_by('-created_at')
|
||||
|
||||
if request.method == 'POST':
|
||||
form = NoteForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
# messages.success(request, "Note added successfully.")
|
||||
else:
|
||||
messages.error(request, "Note content cannot be empty.")
|
||||
|
||||
return render(request, 'recruitment/partials/note_form.html', {'notes':notes})
|
||||
else:
|
||||
form = NoteForm()
|
||||
|
||||
form.initial['application'] = application
|
||||
form.fields['application'].widget = HiddenInput()
|
||||
form.fields['interview'].widget = HiddenInput()
|
||||
form.initial['author'] = request.user
|
||||
form.fields['author'].widget = HiddenInput()
|
||||
url = reverse('application_add_note', kwargs={'slug':slug})
|
||||
notes = Note.objects.filter(application=application).order_by('-created_at')
|
||||
return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':application,'notes':notes,'url':url})
|
||||
|
||||
def interview_add_note(request, slug):
|
||||
from .models import Note
|
||||
from .forms import NoteForm
|
||||
|
||||
interview = get_object_or_404(Interview, slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = NoteForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "Note added successfully.")
|
||||
else:
|
||||
messages.error(request, "Note content cannot be empty.")
|
||||
|
||||
return redirect('interview_detail', slug=slug)
|
||||
else:
|
||||
form = NoteForm()
|
||||
|
||||
form.initial['interview'] = interview
|
||||
form.fields['interview'].widget = HiddenInput()
|
||||
form.fields['application'].widget = HiddenInput()
|
||||
form.initial['author'] = request.user
|
||||
form.fields['author'].widget = HiddenInput()
|
||||
|
||||
return render(request, 'recruitment/partials/note_form.html', {'form': form,'instance':interview,'notes':interview.notes.all()})
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
|
||||
<div class="container-fluid max-width-1600">
|
||||
|
||||
|
||||
|
||||
|
||||
<a class="navbar-brand text-white d-none d-lg-block me-4 pe-4" href="{% url 'dashboard' %}" aria-label="Home">
|
||||
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 60px; height: 60px;">
|
||||
@ -206,7 +206,7 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
|
||||
<li class="d-lg-none"><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'message_list' %}"> <i class="fas fa-envelope fs-5 me-3"></i> <span>{% trans "Messages" %}</span></a></li>
|
||||
{% if request.user.is_authenticated %}
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'user_detail' request.user.pk %}"><i class="fas fa-user-circle me-3 fs-5"></i> <span>{% trans "My Profile" %}</span></a></li>
|
||||
@ -293,10 +293,10 @@
|
||||
</a>
|
||||
</li>
|
||||
<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">
|
||||
<i class="fas fa-calendar-check me-2"></i>
|
||||
{% trans "Meetings" %}
|
||||
{% trans "Meetings & interviews" %}
|
||||
</span>
|
||||
</a>
|
||||
</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();
|
||||
|
||||
try{
|
||||
document.addEventListener('htmx:afterSwap', form_loader);
|
||||
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||
remove_form_loader();
|
||||
});
|
||||
}catch(e){
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{% load static %}
|
||||
{% load file_filters %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<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>
|
||||
@ -25,12 +26,8 @@
|
||||
|
||||
<form
|
||||
method="post"
|
||||
action="{% url 'application_document_upload' application.slug %}"
|
||||
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 %}
|
||||
<div class="modal-body">
|
||||
@ -64,7 +61,7 @@
|
||||
id="documentDescription"
|
||||
rows="3"
|
||||
class="form-control"
|
||||
placeholder="{% trans "Optional description..." %}"
|
||||
placeholder='{% trans "Optional description..." %}'
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@ -101,22 +98,23 @@
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<a
|
||||
href="{% url 'document_download' document.id %}"
|
||||
class="btn btn-sm btn-outline-primary me-2"
|
||||
title="{% trans "Download" %}"
|
||||
>
|
||||
<i class="fas fa-download"></i>
|
||||
href="{% url 'document_download' document.id %}"
|
||||
class="btn btn-sm btn-outline-primary me-2"
|
||||
title='{% trans "Download" %}'
|
||||
>
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
|
||||
{% 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"
|
||||
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>
|
||||
</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -131,6 +129,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.hover-bg-light:hover {
|
||||
background-color: #f8f9fa;
|
||||
@ -139,7 +138,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function confirmDelete(documentId, fileName) {
|
||||
/*function confirmDelete(documentId, fileName) {
|
||||
var deletePrefix = "{% trans "Are you sure you want to delete" %}";
|
||||
if (confirm(deletePrefix + ' "' + fileName + '"?')) {
|
||||
htmx.ajax('POST', `{% url 'document_delete' 0 %}`.replace('0', documentId), {
|
||||
@ -147,5 +146,16 @@ function confirmDelete(documentId, fileName) {
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function closeUploadModal() {
|
||||
var modalElement = document.getElementById('documentUploadModal');
|
||||
if (modalElement) {
|
||||
var modal = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -170,11 +170,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if schedule_interview_type == "Onsite" %}
|
||||
<button type="submit" name="confirm_schedule" class="btn btn-teal-primary px-4" data-bs-toggle="modal" data-bs-target="#interviewDetailsModal" data-placement="top">
|
||||
<i class="fas fa-check me-2"></i> {% trans "Confirm Schedule" %}
|
||||
</button>
|
||||
{% else %}
|
||||
|
||||
<form method="post" action="{% url 'confirm_schedule_interviews_view' slug=job.slug %}" class="mt-4 d-flex justify-content-end gap-3">
|
||||
{% csrf_token %}
|
||||
<a href="{% url 'schedule_interviews' slug=job.slug %}" class="btn btn-secondary px-4">
|
||||
@ -184,7 +180,6 @@
|
||||
<i class="fas fa-check me-2"></i> {% trans "Confirm Schedule" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -196,20 +191,20 @@
|
||||
<h5 class="modal-title" id="interviewDetailsModalLabel">{% trans "Interview Details" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body"> <form method="post" action="{% url 'confirm_schedule_interviews_view' job.slug %}" enctype="multipart/form-data" id="onsite-form">
|
||||
{% comment %} <div class="modal-body"> <form method="post" action="{% url 'confirm_schedule_interviews_view' job.slug %}" enctype="multipart/form-data" id="onsite-form">
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
{# Renders the single 'location' field using the crispy filter #}
|
||||
{{ form|crispy }}
|
||||
|
||||
</form>
|
||||
{{ form|crispy }}
|
||||
|
||||
</form> {% endcomment %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
|
||||
<a href="{% url 'list_meetings' %}" class="btn btn-secondary me-2">
|
||||
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
|
||||
<a href="#" class="btn btn-secondary me-2">
|
||||
<i class="fas fa-times me-1"></i> Close
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" form="onsite-form">
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" form="onsite-form">
|
||||
<i class="fas fa-save me-1"></i> Save Location
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}Bulk Interview Scheduling - {{ job.title }} - ATS{% endblock %}
|
||||
|
||||
@ -125,7 +126,6 @@
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h5 class="section-header">{% trans "Select Candidates" %}</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.candidates.id_for_label }}">
|
||||
{% trans "Candidates to Schedule (Hold Ctrl/Cmd to select multiple)" %}
|
||||
@ -141,14 +141,19 @@
|
||||
<h5 class="section-header">{% trans "Schedule Details" %}</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ form.topic.id_for_label }}">{% trans "Topic" %}</label>
|
||||
{{ form.topic }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ form.schedule_interview_type.id_for_label }}">{% trans "Interview Type" %}</label>
|
||||
{{ form.schedule_interview_type }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@ -217,8 +222,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ form.physical_address.id_for_label }}">{% trans "Physical Address" %}</label>
|
||||
{{ form.physical_address }}
|
||||
{% if form.physical_address.errors %}
|
||||
<div class="text-danger small mt-1">{{ form.physical_address.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends "portal_base.html" %}
|
||||
{% load static %}
|
||||
{% load static crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% if form.instance.pk %}{% trans "Reply to Message" %}{% else %}{% trans "Compose Message" %}{% endif %}{% endblock %}
|
||||
|
||||
@ -11,23 +11,23 @@
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
{% if form.instance.pk %}
|
||||
<i class="fas fa-reply"></i> Reply to Message
|
||||
<i class="fas fa-reply"></i> {% trans "Reply to Message" %}
|
||||
{% else %}
|
||||
<i class="fas fa-envelope"></i> Compose Message
|
||||
<i class="fas fa-envelope"></i> {% trans "Compose Message" %}
|
||||
{% endif %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if form.instance.parent_message %}
|
||||
<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>
|
||||
<small class="text-muted">
|
||||
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 "From" %} {{ form.instance.parent_message.sender.get_full_name|default:form.instance.parent_message.sender.username }}
|
||||
{% trans "on" %} {{ form.instance.parent_message.created_at|date:"M d, Y H:i" }}
|
||||
</small>
|
||||
<div class="mt-2">
|
||||
<strong>Original message:</strong>
|
||||
<strong>{% trans "Original message:" %}</strong>
|
||||
<div class="border-start ps-3 mt-2">
|
||||
{{ form.instance.parent_message.content|linebreaks }}
|
||||
</div>
|
||||
@ -38,99 +38,17 @@
|
||||
<form method="post" id="messageForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<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>
|
||||
|
||||
{{form|crispy}}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'message_list' %}" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
<a href="{% url 'message_list' %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-times"></i> {% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-main-action">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
{% if form.instance.pk %}
|
||||
Send Reply
|
||||
{% trans "Send Reply" %}
|
||||
{% else %}
|
||||
Send Message
|
||||
{% trans "Send Message" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
@ -184,6 +102,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Character counter for subject
|
||||
const subjectField = document.getElementById('id_subject');
|
||||
const maxLength = 200;
|
||||
const charsLabel = "{% trans 'characters' %}";
|
||||
|
||||
if (subjectField) {
|
||||
// Add character counter display
|
||||
@ -194,7 +113,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
function updateCounter() {
|
||||
const remaining = maxLength - subjectField.value.length;
|
||||
counter.textContent = `${subjectField.value.length}/${maxLength} characters`;
|
||||
counter.textContent = `${subjectField.value.length}/${maxLength} ${charsLabel}`;
|
||||
if (remaining < 20) {
|
||||
counter.className = 'text-warning';
|
||||
} else {
|
||||
@ -216,19 +135,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (!recipient) {
|
||||
e.preventDefault();
|
||||
alert('Please select a recipient.');
|
||||
alert("{% trans 'Please select a recipient.' %}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!subject) {
|
||||
e.preventDefault();
|
||||
alert('Please enter a subject.');
|
||||
alert("{% trans 'Please enter a subject.' %}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
e.preventDefault();
|
||||
alert('Please enter a message.');
|
||||
alert("{% trans 'Please enter a message.' %}");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@ -480,7 +480,7 @@
|
||||
|
||||
{# TAB 5 CONTENT: PARSED SUMMARY #}
|
||||
{% if application.parsed_summary %}
|
||||
|
||||
|
||||
<div class="tab-pane fade" id="summary-pane" role="tabpanel" aria-labelledby="summary-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5>
|
||||
<div class="border-start border-primary ps-3 pt-1 pb-1">
|
||||
@ -663,7 +663,7 @@
|
||||
<i class="fas fa-eye me-1"></i>
|
||||
{% trans "View Actual Resume" %}
|
||||
</a> {% endcomment %}
|
||||
|
||||
|
||||
<a href="{{ application.resume.url }}" download class="btn btn-outline-primary">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% trans "Download Resume" %}
|
||||
|
||||
@ -278,7 +278,7 @@
|
||||
{% trans "To Offer" %}
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
||||
</button>
|
||||
</form>
|
||||
@ -286,7 +286,7 @@
|
||||
{# Separator (Vertical Rule) - Aligns automatically at the bottom with align-items-end #}
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
<button id="emailBotton" type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
@ -323,6 +323,9 @@
|
||||
<th scope="col" style="width: 28%;">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 10%;">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Notes" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 10%;">
|
||||
<i class="fas fa-cog me-1"></i> {% trans "Actions" %}
|
||||
</th>
|
||||
@ -394,6 +397,15 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td><button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#noteModal"
|
||||
hx-get="{% url 'application_add_note' application.slug %}"
|
||||
hx-swap="outerHTML"
|
||||
hx-target=".notemodal">
|
||||
<i class="fas fa-calendar-plus me-1"></i>
|
||||
Add note
|
||||
</button></td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
@ -462,51 +474,78 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "recruitment/partials/note_modal.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
const changeStageButton = document.getElementById('changeStage');
|
||||
const emailButton = document.getElementById('emailBotton');
|
||||
const updateStatus = document.getElementById('update_status');
|
||||
|
||||
if (selectAllCheckbox) {
|
||||
// Function to safely update header checkbox state
|
||||
function updateSelectAllState() {
|
||||
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
||||
const totalCount = rowCheckboxes.length;
|
||||
if (selectAllCheckbox) {
|
||||
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else if (checkedCount === totalCount) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
// Function to safely update the header checkbox state
|
||||
function updateSelectAllState() {
|
||||
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
||||
const totalCount = rowCheckboxes.length;
|
||||
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = true;
|
||||
emailButton.disabled = true;
|
||||
updateStatus.disabled = true;
|
||||
} else if (checkedCount === totalCount) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
} else {
|
||||
// Set to indeterminate state (partially checked)
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Logic for 'Select All' checkbox (Clicking it updates all rows)
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
const isChecked = selectAllCheckbox.checked;
|
||||
// 1. Logic for the 'Select All' checkbox (Clicking it updates all rows)
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
const isChecked = selectAllCheckbox.checked;
|
||||
|
||||
rowCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
// Temporarily disable the change listener on rows to prevent cascading events
|
||||
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
|
||||
|
||||
// Update all row checkboxes
|
||||
rowCheckboxes.forEach(function (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
|
||||
// Dispatch event for the framework (data-bind-selections)
|
||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
|
||||
// Re-attach the change listeners to the rows
|
||||
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
|
||||
|
||||
// Ensure the header state is correct after forcing all changes
|
||||
updateSelectAllState();
|
||||
});
|
||||
|
||||
// 2. Logic to update 'Select All' state based on row checkboxes
|
||||
// Attach the function to be called whenever a row checkbox changes
|
||||
rowCheckboxes.forEach(function (checkbox) {
|
||||
checkbox.addEventListener('change', updateSelectAllState);
|
||||
});
|
||||
|
||||
// Initial check to set the correct state on load (in case items are pre-checked)
|
||||
updateSelectAllState();
|
||||
});
|
||||
|
||||
// 2. Logic to update 'Select All' state based on row checkboxes
|
||||
rowCheckboxes.forEach(function (checkbox) {
|
||||
checkbox.addEventListener('change', updateSelectAllState);
|
||||
});
|
||||
|
||||
// Initial check to set correct state on load
|
||||
updateSelectAllState();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -225,11 +225,11 @@
|
||||
</div>
|
||||
|
||||
{# Button #}
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
<button id="emailBotton" type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
@ -261,9 +261,10 @@
|
||||
<th style="width: 15%;">{% trans "Name" %}</th>
|
||||
<th style="width: 15%;">{% trans "Contact Info" %}</th>
|
||||
<th style="width: 10%;" class="text-center">{% trans "AI Score" %}</th>
|
||||
<th style="width: 15%;">{% trans "Exam Date" %}</th>
|
||||
<th style="width: 15%;">{% trans "Exam Score" %}</th>
|
||||
<th style="width: 10%;">{% trans "Exam Date" %}</th>
|
||||
<th style="width: 10%;">{% trans "Exam Score" %}</th>
|
||||
<th style="width: 10%;" class="text-center">{% trans "Exam Results" %}</th>
|
||||
<th style="width: 10%"> {% trans "Notes"%}</th>
|
||||
<th style="width: 15%;">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -324,6 +325,15 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#noteModal"
|
||||
hx-get="{% url 'application_add_note' application.slug %}"
|
||||
hx-swap="outerHTML"
|
||||
hx-target=".notemodal">
|
||||
<i class="fas fa-calendar-plus me-1"></i>
|
||||
Add note
|
||||
</button></td>
|
||||
|
||||
<td >
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
@ -395,14 +405,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "recruitment/partials/note_modal.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
const changeStageButton = document.getElementById('changeStage');
|
||||
const emailButton = document.getElementById('emailBotton');
|
||||
const updateStatus = document.getElementById('update_status');
|
||||
|
||||
if (selectAllCheckbox) {
|
||||
|
||||
@ -414,13 +428,22 @@
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = true;
|
||||
emailButton.disabled = true;
|
||||
updateStatus.disabled = true;
|
||||
} else if (checkedCount === totalCount) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
} else {
|
||||
// Set to indeterminate state (partially checked)
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -218,7 +218,7 @@
|
||||
{% trans "To Exam" %}
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
||||
</button>
|
||||
</form>
|
||||
@ -227,23 +227,23 @@
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
|
||||
{# Form 2: Schedule Interviews #}
|
||||
<form hx-boost="true" hx-include="#application-form" action="#" method="get" class="action-group">
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
|
||||
<form hx-boost="true" hx-include="#application-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
|
||||
<button id="scheduleInterview" type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-calendar-plus me-1"></i> {% trans "Bulk Schedule Interviews" %}
|
||||
</button>
|
||||
</form>
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
|
||||
|
||||
<button type="button" class="btn btn-outline-info btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<button id="emailBotton" type="button" class="btn btn-outline-info btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -271,8 +271,9 @@
|
||||
<th style="width: 10%"><i class="fas fa-calendar me-1"></i> {% trans "Meeting Date" %}</th>
|
||||
<th style="width: 7%"><i class="fas fa-video me-1"></i> {% trans "Link" %}</th>
|
||||
<th style="width: 8%"><i class="fas fa-check-circle me-1"></i> {% trans "Meeting Status" %}</th> {% endcomment %}
|
||||
<th style="width: 15%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview Result"%}</th>
|
||||
<th style="width: 15%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview List"%}</th>
|
||||
<th style="width: 10%"><i class="fas fa-video me-1"></i> {% trans "Interviews"%}</th>
|
||||
<th style="width: 10%"><i class="fas fa-sticky-note me-1"></i> {% trans "Notes"%}</th>
|
||||
<th style="width: 5%"><i class="fas fa-check-circle me-1"></i> {% trans "Result"%}</th>
|
||||
<th style="width: 10%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -353,6 +354,26 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td> {% endcomment %}
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'get_interview_list' application.slug %}"
|
||||
hx-target="#candidateviewModalBody">
|
||||
View
|
||||
<i class="fas fa-list"></i>
|
||||
{{candidate.get_interviews}}
|
||||
</button>
|
||||
</td>
|
||||
<td><button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#noteModal"
|
||||
hx-get="{% url 'application_add_note' application.slug %}"
|
||||
hx-swap="outerHTML"
|
||||
hx-target=".notemodal">
|
||||
<i class="fas fa-calendar-plus me-1"></i>
|
||||
Add note
|
||||
</button></td>
|
||||
<td class="text-center" id="interview-result-{{ application.pk }}">
|
||||
{% if not application.interview_status %}
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
@ -379,18 +400,6 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'get_interview_list' application.slug %}"
|
||||
hx-target="#candidateviewModalBody">
|
||||
Interview List
|
||||
<i class="fas fa-list"></i>
|
||||
{{candidate.get_interviews}}
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
{% if application.get_latest_meeting %}
|
||||
{% if application.get_latest_meeting.location_type == 'Remote'%}
|
||||
|
||||
@ -445,7 +454,6 @@
|
||||
<i class="fas fa-calendar-plus me-1"></i>
|
||||
Schedule Interview
|
||||
</button>
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -504,6 +512,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% include "recruitment/partials/note_modal.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
@ -511,6 +522,10 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
const changeStageButton = document.getElementById('changeStage');
|
||||
const emailButton = document.getElementById('emailBotton');
|
||||
const updateStatus = document.getElementById('update_status');
|
||||
const scheduleInterviewButton = document.getElementById('scheduleInterview');
|
||||
|
||||
if (selectAllCheckbox) {
|
||||
|
||||
@ -522,13 +537,25 @@
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = true;
|
||||
emailButton.disabled = true;
|
||||
updateStatus.disabled = true;
|
||||
scheduleInterviewButton.disabled = true;
|
||||
} else if (checkedCount === totalCount) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
scheduleInterviewButton.disabled = false;
|
||||
} else {
|
||||
// Set to indeterminate state (partially checked)
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
scheduleInterviewButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -220,26 +220,23 @@
|
||||
</select>
|
||||
|
||||
{# Button #}
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
{# Separator (Vertical Rule) #}
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<button id="emailBotton" type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_application_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#application-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
@ -263,8 +260,9 @@
|
||||
<th style="width: 15%"><i class="fas fa-phone me-1"></i> {% trans "Contact" %}</th>
|
||||
<th class="text-center" style="width: 10%"><i class="fas fa-check-circle me-1"></i> {% trans "Offer" %}</th>
|
||||
<th scope="col" style="width: 30%;">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
|
||||
</th>
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Documents" %}
|
||||
</th>
|
||||
<th style="width: 10%"><i class="fas fa-phone me-1"></i> {% trans "Notes" %}</th>
|
||||
<th style="width: 5%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -356,6 +354,15 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td><button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#noteModal"
|
||||
hx-get="{% url 'application_add_note' application.slug %}"
|
||||
hx-swap="outerHTML"
|
||||
hx-target=".notemodal">
|
||||
<i class="fas fa-calendar-plus me-1"></i>
|
||||
Add note
|
||||
</button></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
@ -423,6 +430,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "recruitment/partials/note_modal.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
@ -430,6 +439,9 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
const changeStageButton = document.getElementById('changeStage');
|
||||
const emailButton = document.getElementById('emailBotton');
|
||||
const updateStatus = document.getElementById('update_status');
|
||||
|
||||
if (selectAllCheckbox) {
|
||||
|
||||
@ -441,13 +453,22 @@
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = true;
|
||||
emailButton.disabled = true;
|
||||
updateStatus.disabled = true;
|
||||
} else if (checkedCount === totalCount) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
} else {
|
||||
// Set to indeterminate state (partially checked)
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -340,11 +340,11 @@
|
||||
</div>
|
||||
|
||||
{# Button #}
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
|
||||
</button>
|
||||
{# email button#}
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
<button id="emailBotton" type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
@ -374,28 +374,31 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</th>
|
||||
<th scope="col" style="width: 8%;">
|
||||
<th scope="col" style="width: 13%;">
|
||||
<i class="fas fa-user me-1"></i> {% trans "Name" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 10%;">
|
||||
<th scope="col" style="width: 15%;">
|
||||
<i class="fas fa-phone me-1"></i> {% trans "Contact Info" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 5%;">
|
||||
<i class="fas fa-graduation-cap me-1"></i> {% trans "GPA" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 6%;" class="text-center">
|
||||
<th scope="col" style="width: 5%;" class="text-center">
|
||||
<i class="fas fa-robot me-1"></i> {% trans "AI Score" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 15%;" class="text-center">
|
||||
<th scope="col" style="width: 10%;" class="text-center">
|
||||
<i class="fas fa-robot me-1"></i> {% trans "Is Qualified?" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 10%;">
|
||||
<th scope="col" style="width: 20%;">
|
||||
<i class="fas fa-graduation-cap me-1"></i> {% trans "Professional Category" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 15%;">
|
||||
<i class="fas fa-graduation-cap me-1"></i> {% trans "Top 3 Skills" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 10%;" class="text-center">
|
||||
<i class="fas fa-cog me-1"></i> {% trans "Note" %}
|
||||
</th>
|
||||
<th scope="col" style="width: 5%;" class="text-center">
|
||||
<i class="fas fa-cog me-1"></i> {% trans "Actions" %}
|
||||
</th>
|
||||
</tr>
|
||||
@ -466,7 +469,15 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td><button type="button" class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#noteModal"
|
||||
hx-get="{% url 'application_add_note' application.slug %}"
|
||||
hx-swap="outerHTML"
|
||||
hx-target=".notemodal">
|
||||
<i class="fas fa-calendar-plus me-1"></i>
|
||||
Add note
|
||||
</button></td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
@ -536,6 +547,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "recruitment/partials/note_modal.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -544,8 +557,12 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
|
||||
const changeStageButton = document.getElementById('changeStage');
|
||||
const emailButton = document.getElementById('emailBotton');
|
||||
const updateStatus = document.getElementById('update_status');
|
||||
|
||||
if (selectAllCheckbox) {
|
||||
|
||||
// Function to safely update the header checkbox state
|
||||
function updateSelectAllState() {
|
||||
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
|
||||
@ -554,12 +571,22 @@
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = true;
|
||||
emailButton.disabled = true;
|
||||
updateStatus.disabled = true;
|
||||
} else if (checkedCount === totalCount) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
} else {
|
||||
// Set to indeterminate state (partially checked)
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
changeStageButton.disabled = false;
|
||||
emailButton.disabled = false;
|
||||
updateStatus.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -567,18 +594,26 @@
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
const isChecked = selectAllCheckbox.checked;
|
||||
|
||||
// Temporarily disable the change listener on rows to prevent cascading events
|
||||
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
|
||||
|
||||
// Update all row checkboxes
|
||||
rowCheckboxes.forEach(function (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
|
||||
// Dispatch event for the framework (data-bind-selections)
|
||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
|
||||
// Re-attach the change listeners to the rows
|
||||
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
|
||||
|
||||
// Ensure the header state is correct after forcing all changes
|
||||
updateSelectAllState();
|
||||
});
|
||||
|
||||
// 2. Logic to update 'Select All' state based on row checkboxes
|
||||
// Attach the function to be called whenever a row checkbox changes
|
||||
rowCheckboxes.forEach(function (checkbox) {
|
||||
checkbox.addEventListener('change', updateSelectAllState);
|
||||
});
|
||||
|
||||
52
templates/recruitment/partials/note_form.html
Normal file
52
templates/recruitment/partials/note_form.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% load i18n crispy_forms_tags %}
|
||||
<div class="p-3">
|
||||
<form hx-boost="true" id="noteform" action="{{url}}" method="post" hx-select=".note-table-body" hx-target=".note-table-body" hx-swap="outerHTML" hx-push-url="false">
|
||||
{% csrf_token %}
|
||||
{{form|crispy}}
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="notesubmit" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="submit" class="btn btn-main-action" id="saveNoteBtn">{% trans "Save Note" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="table-responsive mt-3">
|
||||
<table class="table table-sm" id="notesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Author" %}</th>
|
||||
<th scope="col" style="width: 60%;">{% trans "Note" %}</th>
|
||||
<th scope="col">{% trans "Created" %}</th>
|
||||
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="note-table-body">
|
||||
{% if notes %}
|
||||
{% for note in notes %}
|
||||
<tr id="note-{{ note.id }}">
|
||||
<td class="align-middle">
|
||||
{{ note.author.get_full_name|default:note.author.username }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{ note.content|linebreaksbr }}
|
||||
</td>
|
||||
<td class="align-middle text-nowrap">
|
||||
<span class="text-muted">
|
||||
{{ note.created_at|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="align-middle text-end">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger delete-note-btn"
|
||||
data-note-id="{{ note.id }}">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-3">{% trans "No notes yet." %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
13
templates/recruitment/partials/note_modal.html
Normal file
13
templates/recruitment/partials/note_modal.html
Normal file
@ -0,0 +1,13 @@
|
||||
<div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content kaauh-card">
|
||||
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body notemodal">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
x
Reference in New Issue
Block a user