...
This commit is contained in:
commit
7a0bf3262d
Binary file not shown.
BIN
db.sqlite3
Normal file
BIN
db.sqlite3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -4,7 +4,7 @@ from crispy_forms.helper import FormHelper
|
||||
from django.core.validators import URLValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from crispy_forms.layout import Layout, Submit, HTML, Div, Field
|
||||
from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting,FormTemplate
|
||||
from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting,FormTemplate,InterviewSchedule
|
||||
|
||||
class CandidateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@ -408,4 +408,48 @@ class FormTemplateForm(forms.ModelForm):
|
||||
Field('description', css_class='form-control'),
|
||||
Field('is_active', css_class='form-check-input'),
|
||||
Submit('submit', _('Create Template'), css_class='btn btn-primary mt-3')
|
||||
)
|
||||
|
||||
class InterviewScheduleForm(forms.ModelForm):
|
||||
candidates = forms.ModelMultipleChoiceField(
|
||||
queryset=Candidate.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 = InterviewSchedule
|
||||
fields = [
|
||||
'candidates', 'start_date', 'end_date', 'working_days',
|
||||
'start_time', 'end_time', 'break_start_time', 'break_end_time',
|
||||
'interview_duration', 'buffer_time'
|
||||
]
|
||||
widgets = {
|
||||
'start_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'end_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'start_time': forms.TimeInput(attrs={'type': 'time'}),
|
||||
'end_time': forms.TimeInput(attrs={'type': 'time'}),
|
||||
'break_start_time': forms.TimeInput(attrs={'type': 'time'}),
|
||||
'break_end_time': forms.TimeInput(attrs={'type': 'time'}),
|
||||
}
|
||||
|
||||
def __init__(self, slug, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Filter candidates based on the selected job
|
||||
self.fields['candidates'].queryset = Candidate.objects.filter(
|
||||
job_slug=slug,
|
||||
stage='Interview'
|
||||
)
|
||||
@ -0,0 +1,60 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-07 14:12
|
||||
|
||||
import django.db.models.deletion
|
||||
import django_extensions.db.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0025_formfield_max_files_formfield_multiple_files'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InterviewSchedule',
|
||||
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')),
|
||||
('start_date', models.DateField(verbose_name='Start Date')),
|
||||
('end_date', models.DateField(verbose_name='End Date')),
|
||||
('working_days', models.JSONField(verbose_name='Working Days')),
|
||||
('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)')),
|
||||
('candidates', models.ManyToManyField(related_name='interview_schedules', to='recruitment.candidate')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScheduledInterview',
|
||||
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')),
|
||||
('interview_date', models.DateField(verbose_name='Interview Date')),
|
||||
('interview_time', models.TimeField(verbose_name='Interview Time')),
|
||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)),
|
||||
('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.candidate')),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')),
|
||||
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
|
||||
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -714,3 +714,46 @@ class HiringAgency(Base):
|
||||
verbose_name = _('Hiring Agency')
|
||||
verbose_name_plural = _('Hiring Agencies')
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
|
||||
class InterviewSchedule(Base):
|
||||
"""Stores the scheduling criteria for interviews"""
|
||||
job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name='interview_schedules')
|
||||
candidates = models.ManyToManyField(Candidate, related_name='interview_schedules')
|
||||
start_date = models.DateField(verbose_name=_('Start Date'))
|
||||
end_date = models.DateField(verbose_name=_('End Date'))
|
||||
working_days = models.JSONField(verbose_name=_('Working Days')) # Store days of week as [0,1,2,3,4] for Mon-Fri
|
||||
start_time = models.TimeField(verbose_name=_('Start Time'))
|
||||
end_time = models.TimeField(verbose_name=_('End Time'))
|
||||
break_start_time = models.TimeField(verbose_name=_('Break Start Time'), null=True, blank=True)
|
||||
break_end_time = models.TimeField(verbose_name=_('Break End Time'), null=True, blank=True)
|
||||
interview_duration = models.PositiveIntegerField(verbose_name=_('Interview Duration (minutes)'))
|
||||
buffer_time = models.PositiveIntegerField(verbose_name=_('Buffer Time (minutes)'), default=0)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return f"Interview Schedule for {self.job.title}"
|
||||
|
||||
class ScheduledInterview(Base):
|
||||
"""Stores individual scheduled interviews"""
|
||||
candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE, related_name='scheduled_interviews')
|
||||
job = models.ForeignKey(JobPosting, on_delete=models.CASCADE, related_name='scheduled_interviews')
|
||||
zoom_meeting = models.OneToOneField(ZoomMeeting, on_delete=models.CASCADE, related_name='interview')
|
||||
schedule = models.ForeignKey(InterviewSchedule, on_delete=models.CASCADE, related_name='interviews')
|
||||
interview_date = models.DateField(verbose_name=_('Interview Date'))
|
||||
interview_time = models.TimeField(verbose_name=_('Interview Time'))
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('scheduled', _('Scheduled')),
|
||||
('confirmed', _('Confirmed')),
|
||||
('cancelled', _('Cancelled')),
|
||||
('completed', _('Completed')),
|
||||
],
|
||||
default='scheduled'
|
||||
)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"Interview with {self.candidate.name} for {self.job.title}"
|
||||
Binary file not shown.
@ -22,31 +22,31 @@ def get_all_responses_flat(stage_responses):
|
||||
"""
|
||||
all_responses = []
|
||||
if stage_responses:
|
||||
print(stage_responses.get(9).get("responses")[0].value)
|
||||
for stage_id, responses in stage_responses.items():
|
||||
for response in responses:
|
||||
# Check if response is an object or string
|
||||
if hasattr(response, 'stage') and hasattr(response, 'field'):
|
||||
stage_name = response.stage.name if hasattr(response.stage, 'name') else f"Stage {stage_id}"
|
||||
field_label = response.field.label if hasattr(response.field, 'label') else "Unknown Field"
|
||||
field_type = response.field.get_field_type_display() if hasattr(response.field, 'get_field_type_display') else "Unknown Type"
|
||||
required = response.field.required if hasattr(response.field, 'required') else False
|
||||
value = response.value if hasattr(response, 'value') else response
|
||||
uploaded_file = response.uploaded_file if hasattr(response, 'uploaded_file') else None
|
||||
else:
|
||||
stage_name = f"Stage {stage_id}"
|
||||
field_label = "Unknown Field"
|
||||
field_type = "Text"
|
||||
required = False
|
||||
value = response
|
||||
uploaded_file = None
|
||||
if responses: # Check if responses list exists and is not empty
|
||||
for response in responses:
|
||||
# Check if response is an object or string
|
||||
if hasattr(response, 'stage') and hasattr(response, 'field'):
|
||||
stage_name = response.stage.name if hasattr(response.stage, 'name') else f"Stage {stage_id}"
|
||||
field_label = response.field.label if hasattr(response.field, 'label') else "Unknown Field"
|
||||
field_type = response.field.get_field_type_display() if hasattr(response.field, 'get_field_type_display') else "Unknown Type"
|
||||
required = response.field.required if hasattr(response.field, 'required') else False
|
||||
value = response.value if hasattr(response, 'value') else response
|
||||
uploaded_file = response.uploaded_file if hasattr(response, 'uploaded_file') else None
|
||||
else:
|
||||
stage_name = f"Stage {stage_id}"
|
||||
field_label = "Unknown Field"
|
||||
field_type = "Text"
|
||||
required = False
|
||||
value = response
|
||||
uploaded_file = None
|
||||
|
||||
all_responses.append({
|
||||
'stage_name': stage_name,
|
||||
'field_label': field_label,
|
||||
'field_type': field_type,
|
||||
'required': required,
|
||||
'value': value,
|
||||
'uploaded_file': uploaded_file
|
||||
})
|
||||
all_responses.append({
|
||||
'stage_name': stage_name,
|
||||
'field_label': field_label,
|
||||
'field_type': field_type,
|
||||
'required': required,
|
||||
'value': value,
|
||||
'uploaded_file': uploaded_file
|
||||
})
|
||||
return all_responses
|
||||
|
||||
@ -19,6 +19,7 @@ urlpatterns = [
|
||||
path('jobs/linkedin/login/', views.linkedin_login, name='linkedin_login'),
|
||||
path('jobs/linkedin/callback/', views.linkedin_callback, name='linkedin_callback'),
|
||||
|
||||
path('jobs/<slug:slug>/schedule-interviews/', views.schedule_interviews_view, name='schedule_interviews'),
|
||||
# Candidate URLs
|
||||
path('candidates/', views_frontend.CandidateListView.as_view(), name='candidate_list'),
|
||||
path('candidates/create/', views_frontend.CandidateCreateView.as_view(), name='candidate_create'),
|
||||
|
||||
@ -4,7 +4,11 @@
|
||||
# import requests
|
||||
from recruitment import models
|
||||
from django.conf import settings
|
||||
|
||||
from datetime import datetime, timedelta, time, date
|
||||
from django.utils import timezone
|
||||
from .models import ScheduledInterview
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.mail import send_mail
|
||||
# nlp = spacy.load("en_core_web_sm")
|
||||
|
||||
# def extract_text_from_pdf(pdf_path):
|
||||
@ -53,7 +57,7 @@ def extract_text_from_pdf(file_path):
|
||||
raise
|
||||
return text.strip()
|
||||
|
||||
def score_resume_with_openrouter(prompt):
|
||||
def score_resume_with_openrouter(prompt):
|
||||
print("model call")
|
||||
response = requests.post(
|
||||
url="https://openrouter.ai/api/v1/chat/completions",
|
||||
@ -63,7 +67,7 @@ def score_resume_with_openrouter(prompt):
|
||||
},
|
||||
data=json.dumps({
|
||||
"model": OPENROUTER_MODEL,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
},
|
||||
)
|
||||
)
|
||||
@ -75,15 +79,15 @@ def score_resume_with_openrouter(prompt):
|
||||
res = response.json()
|
||||
content = res["choices"][0]['message']['content']
|
||||
try:
|
||||
|
||||
|
||||
content = content.replace("```json","").replace("```","")
|
||||
|
||||
|
||||
res = json.loads(content)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# res = raw_output["choices"][0]["message"]["content"]
|
||||
# res = raw_output["choices"][0]["message"]["content"]
|
||||
else:
|
||||
print("error response")
|
||||
return res
|
||||
@ -381,4 +385,131 @@ def delete_zoom_meeting(meeting_id):
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
}
|
||||
|
||||
def schedule_interviews(schedule):
|
||||
"""
|
||||
Schedule interviews for all candidates in the schedule based on the criteria.
|
||||
Returns the number of interviews successfully scheduled.
|
||||
"""
|
||||
candidates = list(schedule.candidates.all())
|
||||
if not candidates:
|
||||
return 0
|
||||
|
||||
# Calculate available time slots
|
||||
available_slots = get_available_time_slots(schedule)
|
||||
|
||||
if len(available_slots) < len(candidates):
|
||||
raise ValueError(f"Not enough available slots. Required: {len(candidates)}, Available: {len(available_slots)}")
|
||||
|
||||
# Schedule interviews
|
||||
scheduled_count = 0
|
||||
for i, candidate in enumerate(candidates):
|
||||
slot = available_slots[i]
|
||||
interview_datetime = datetime.combine(slot['date'], slot['time'])
|
||||
|
||||
# Create Zoom meeting
|
||||
meeting_topic = f"Interview for {schedule.job.title} - {candidate.name}"
|
||||
meeting = create_zoom_meeting(
|
||||
topic=meeting_topic,
|
||||
start_time=interview_datetime,
|
||||
duration=schedule.interview_duration,
|
||||
timezone=timezone.get_current_timezone_name()
|
||||
)
|
||||
|
||||
# Create scheduled interview record
|
||||
scheduled_interview = ScheduledInterview.objects.create(
|
||||
candidate=candidate,
|
||||
job=schedule.job,
|
||||
zoom_meeting=meeting,
|
||||
schedule=schedule,
|
||||
interview_date=slot['date'],
|
||||
interview_time=slot['time']
|
||||
)
|
||||
|
||||
# Send email to candidate
|
||||
send_interview_email(scheduled_interview)
|
||||
|
||||
scheduled_count += 1
|
||||
|
||||
return scheduled_count
|
||||
|
||||
def send_interview_email(scheduled_interview):
|
||||
"""
|
||||
Send an interview invitation email to the candidate.
|
||||
"""
|
||||
subject = f"Interview Invitation for {scheduled_interview.job.title}"
|
||||
|
||||
context = {
|
||||
'candidate_name': scheduled_interview.candidate.name,
|
||||
'job_title': scheduled_interview.job.title,
|
||||
'company_name': scheduled_interview.job.company.name,
|
||||
'interview_date': scheduled_interview.interview_date,
|
||||
'interview_time': scheduled_interview.interview_time,
|
||||
'join_url': scheduled_interview.zoom_meeting.join_url,
|
||||
'meeting_id': scheduled_interview.zoom_meeting.meeting_id,
|
||||
}
|
||||
|
||||
# Render email templates
|
||||
text_message = render_to_string('interviews/email/interview_invitation.txt', context)
|
||||
html_message = render_to_string('interviews/email/interview_invitation.html', context)
|
||||
|
||||
# Send email
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=text_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[scheduled_interview.candidate.email],
|
||||
html_message=html_message,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
def get_available_time_slots(schedule):
|
||||
"""
|
||||
Generate a list of available time slots based on the schedule criteria.
|
||||
Returns a list of dictionaries with 'date' and 'time' keys.
|
||||
"""
|
||||
slots = []
|
||||
current_date = schedule.start_date
|
||||
end_date = schedule.end_date
|
||||
|
||||
# Convert working days to a set for quick lookup
|
||||
working_days_set = set(schedule.working_days)
|
||||
|
||||
# Parse times
|
||||
start_time = schedule.start_time
|
||||
end_time = schedule.end_time
|
||||
break_start = schedule.break_start_time
|
||||
break_end = schedule.break_end_time
|
||||
|
||||
# Calculate slot duration (interview duration + buffer time)
|
||||
slot_duration = timedelta(minutes=schedule.interview_duration + schedule.buffer_time)
|
||||
|
||||
while current_date <= end_date:
|
||||
# Check if current day is a working day
|
||||
if current_date.weekday() in working_days_set:
|
||||
# Generate slots for this day
|
||||
current_time = start_time
|
||||
|
||||
while current_time + slot_duration <= end_time:
|
||||
# Check if slot is during break time
|
||||
if break_start and break_end:
|
||||
if current_time >= break_start and current_time < break_end:
|
||||
current_time = break_end
|
||||
continue
|
||||
|
||||
# Add this slot to available slots
|
||||
slots.append({
|
||||
'date': current_date,
|
||||
'time': current_time
|
||||
})
|
||||
|
||||
# Move to next slot
|
||||
current_datetime = datetime.combine(current_date, current_time)
|
||||
current_datetime += slot_duration
|
||||
current_time = current_datetime.time()
|
||||
|
||||
# Move to next day
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return slots
|
||||
@ -9,17 +9,17 @@ from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm
|
||||
from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm,InterviewScheduleForm
|
||||
from rest_framework import viewsets
|
||||
from django.contrib import messages
|
||||
from django.core.paginator import Paginator
|
||||
from .linkedin_service import LinkedInService
|
||||
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission
|
||||
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission,InterviewSchedule
|
||||
from .models import ZoomMeeting, Candidate, JobPosting
|
||||
from .serializers import JobPostingSerializer, CandidateSerializer
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.views.generic import CreateView,UpdateView,DetailView,ListView
|
||||
from .utils import create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting
|
||||
from .utils import create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting,schedule_interviews,get_available_time_slots
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
import logging
|
||||
|
||||
@ -843,3 +843,136 @@ def form_submission_details(request, form_id, submission_id):
|
||||
'responses': responses,
|
||||
'stage_responses': stage_responses
|
||||
})
|
||||
|
||||
|
||||
|
||||
def schedule_interviews_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = InterviewScheduleForm(slug, request.POST)
|
||||
|
||||
# Check if this is a confirmation request
|
||||
if 'confirm_schedule' in request.POST:
|
||||
# Get the schedule data from session
|
||||
schedule_data = request.session.get('interview_schedule_data')
|
||||
if not schedule_data:
|
||||
messages.error(request, "Session expired. Please try again.")
|
||||
return redirect('schedule_interviews', slug=slug)
|
||||
|
||||
# Create the interview schedule
|
||||
schedule = InterviewSchedule.objects.create(
|
||||
job=job,
|
||||
created_by=request.user,
|
||||
**schedule_data
|
||||
)
|
||||
|
||||
# Add candidates to the schedule
|
||||
candidates = Candidate.objects.filter(id__in=schedule_data['candidate_ids'])
|
||||
schedule.candidates.set(candidates)
|
||||
|
||||
# Schedule the interviews
|
||||
try:
|
||||
scheduled_count = schedule_interviews(schedule)
|
||||
messages.success(
|
||||
request,
|
||||
f"Successfully scheduled {scheduled_count} interviews."
|
||||
)
|
||||
# Clear the session data
|
||||
if 'interview_schedule_data' in request.session:
|
||||
del request.session['interview_schedule_data']
|
||||
return redirect('job_detail', slug=slug)
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
f"Error scheduling interviews: {str(e)}"
|
||||
)
|
||||
return redirect('schedule_interviews', slug=slug)
|
||||
|
||||
# This is the initial form submission
|
||||
if form.is_valid():
|
||||
# Get the form data
|
||||
candidates = form.cleaned_data['candidates']
|
||||
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']
|
||||
break_start_time = form.cleaned_data['break_start_time']
|
||||
break_end_time = form.cleaned_data['break_end_time']
|
||||
interview_duration = form.cleaned_data['interview_duration']
|
||||
buffer_time = form.cleaned_data['buffer_time']
|
||||
|
||||
# Create a temporary schedule object (not saved to DB)
|
||||
temp_schedule = InterviewSchedule(
|
||||
job=job,
|
||||
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
|
||||
)
|
||||
|
||||
# Get available slots
|
||||
available_slots = get_available_time_slots(temp_schedule)
|
||||
|
||||
if len(available_slots) < len(candidates):
|
||||
messages.error(
|
||||
request,
|
||||
f"Not enough available slots. Required: {len(candidates)}, Available: {len(available_slots)}"
|
||||
)
|
||||
return render(request, 'interviews/schedule_interviews.html', {
|
||||
'form': form,
|
||||
'job': job
|
||||
})
|
||||
|
||||
# Create a preview schedule
|
||||
preview_schedule = []
|
||||
for i, candidate in enumerate(candidates):
|
||||
slot = available_slots[i]
|
||||
preview_schedule.append({
|
||||
'candidate': candidate,
|
||||
'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(),
|
||||
'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,
|
||||
'interview_duration': interview_duration,
|
||||
'buffer_time': buffer_time,
|
||||
'candidate_ids': [c.id for c in candidates]
|
||||
}
|
||||
request.session['interview_schedule_data'] = 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
|
||||
})
|
||||
else:
|
||||
form = InterviewScheduleForm(slug=slug)
|
||||
|
||||
return render(request, 'interviews/schedule_interviews.html', {
|
||||
'form': form,
|
||||
'job': job
|
||||
})
|
||||
@ -1112,46 +1112,47 @@
|
||||
state.draggedStageIndex = null;
|
||||
}
|
||||
// DOM Elements
|
||||
const elements = {
|
||||
stageNav: document.getElementById('stageNav'),
|
||||
formStage: document.getElementById('formStage'),
|
||||
emptyState: document.getElementById('emptyState'),
|
||||
fieldEditor: document.getElementById('fieldEditor'),
|
||||
currentStageTitle: document.getElementById('currentStageTitle'),
|
||||
stageNameDisplay: document.getElementById('stageNameDisplay'),
|
||||
stageRequiredIndicator: document.getElementById('stageRequiredIndicator'),
|
||||
stagePredefinedBadge: document.getElementById('stagePredefinedBadge'),
|
||||
addStageBtn: document.getElementById('addStageBtn'),
|
||||
saveFormBtn: document.getElementById('saveFormBtn'),
|
||||
renameStageBtn: document.getElementById('renameStageBtn'),
|
||||
renameModal: document.getElementById('renameModal'),
|
||||
stageName: document.getElementById('stageName'),
|
||||
closeRenameModal: document.getElementById('closeRenameModal'),
|
||||
cancelRenameBtn: document.getElementById('cancelRenameBtn'),
|
||||
saveRenameBtn: document.getElementById('saveRenameBtn'),
|
||||
fieldLabel: document.getElementById('fieldLabel'),
|
||||
fieldPlaceholder: document.getElementById('fieldPlaceholder'),
|
||||
placeholderGroup: document.getElementById('placeholderGroup'),
|
||||
requiredField: document.getElementById('requiredField'),
|
||||
optionsEditor: document.getElementById('optionsEditor'),
|
||||
optionsList: document.getElementById('optionsList'),
|
||||
addOptionBtn: document.getElementById('addOptionBtn'),
|
||||
fileSettings: document.getElementById('fileSettings'),
|
||||
fileTypes: document.getElementById('fileTypes'),
|
||||
maxFileSize: document.getElementById('maxFileSize'),
|
||||
closeEditorBtn: document.getElementById('closeEditorBtn'),
|
||||
// Form settings elements
|
||||
formTitle: document.getElementById('formTitle'),
|
||||
formSettingsBtn: document.getElementById('formSettingsBtn'),
|
||||
formSettingsModal: document.getElementById('formSettingsModal'),
|
||||
formName: document.getElementById('formName'),
|
||||
formDescription: document.getElementById('formDescription'),
|
||||
formActive: document.getElementById('formActive'),
|
||||
closeFormSettingsModal: document.getElementById('closeFormSettingsModal'),
|
||||
cancelFormSettingsBtn: document.getElementById('cancelFormSettingsBtn'),
|
||||
saveFormSettingsBtn: document.getElementById('saveFormSettingsBtn')
|
||||
};
|
||||
|
||||
const elements = {
|
||||
stageNav: document.getElementById('stageNav'),
|
||||
formStage: document.getElementById('formStage'),
|
||||
emptyState: document.getElementById('emptyState'),
|
||||
fieldEditor: document.getElementById('fieldEditor'),
|
||||
currentStageTitle: document.getElementById('currentStageTitle'),
|
||||
stageNameDisplay: document.getElementById('stageNameDisplay'),
|
||||
stageRequiredIndicator: document.getElementById('stageRequiredIndicator'),
|
||||
stagePredefinedBadge: document.getElementById('stagePredefinedBadge'),
|
||||
addStageBtn: document.getElementById('addStageBtn'),
|
||||
saveFormBtn: document.getElementById('saveFormBtn'),
|
||||
renameStageBtn: document.getElementById('renameStageBtn'),
|
||||
renameModal: document.getElementById('renameModal'),
|
||||
stageName: document.getElementById('stageName'),
|
||||
closeRenameModal: document.getElementById('closeRenameModal'),
|
||||
cancelRenameBtn: document.getElementById('cancelRenameBtn'),
|
||||
saveRenameBtn: document.getElementById('saveRenameBtn'),
|
||||
fieldLabel: document.getElementById('fieldLabel'),
|
||||
fieldPlaceholder: document.getElementById('fieldPlaceholder'),
|
||||
placeholderGroup: document.getElementById('placeholderGroup'),
|
||||
requiredField: document.getElementById('requiredField'),
|
||||
optionsEditor: document.getElementById('optionsEditor'),
|
||||
optionsList: document.getElementById('optionsList'),
|
||||
addOptionBtn: document.getElementById('addOptionBtn'),
|
||||
fileSettings: document.getElementById('fileSettings'),
|
||||
fileTypes: document.getElementById('fileTypes'),
|
||||
maxFileSize: document.getElementById('maxFileSize'),
|
||||
multipleFiles: document.getElementById('multipleFiles'),
|
||||
maxFiles: document.getElementById('maxFiles'),
|
||||
closeEditorBtn: document.getElementById('closeEditorBtn'),
|
||||
// Form settings elements
|
||||
formTitle: document.getElementById('formTitle'),
|
||||
formSettingsBtn: document.getElementById('formSettingsBtn'),
|
||||
formSettingsModal: document.getElementById('formSettingsModal'),
|
||||
formName: document.getElementById('formName'),
|
||||
formDescription: document.getElementById('formDescription'),
|
||||
formActive: document.getElementById('formActive'),
|
||||
closeFormSettingsModal: document.getElementById('closeFormSettingsModal'),
|
||||
cancelFormSettingsBtn: document.getElementById('cancelFormSettingsBtn'),
|
||||
saveFormSettingsBtn: document.getElementById('saveFormSettingsBtn')
|
||||
};
|
||||
// Utility Functions
|
||||
function getFieldIcon(type) {
|
||||
const icons = {
|
||||
@ -1443,7 +1444,6 @@
|
||||
fileUpload.appendChild(uploadedFile);
|
||||
});
|
||||
}
|
||||
|
||||
fieldContent.appendChild(fileUpload);
|
||||
} else if (field.type === 'select') {
|
||||
const select = document.createElement('select');
|
||||
@ -1515,18 +1515,16 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Make draggable
|
||||
// Make draggable for reordering
|
||||
fieldDiv.draggable = true;
|
||||
fieldDiv.addEventListener('dragstart', (e) => {
|
||||
state.draggedFieldIndex = parseInt(fieldDiv.dataset.fieldIndex);
|
||||
e.dataTransfer.setData('text/plain', 'reorder');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
|
||||
fieldDiv.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
fieldDiv.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
const targetIndex = parseInt(fieldDiv.dataset.fieldIndex);
|
||||
@ -1536,21 +1534,30 @@
|
||||
// Add file input event listener
|
||||
const fileInput = fieldDiv.querySelector('.file-input');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
// Remove any existing listeners to prevent duplicates
|
||||
const newFileInput = fileInput.cloneNode(true);
|
||||
fileInput.parentNode.replaceChild(newFileInput, fileInput);
|
||||
|
||||
newFileInput.addEventListener('change', (e) => {
|
||||
handleFileUpload(e, field);
|
||||
});
|
||||
|
||||
// Make the file upload area clickable
|
||||
const fileUploadArea = fieldDiv.querySelector('.file-upload-area');
|
||||
if (fileUploadArea) {
|
||||
fileUploadArea.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
// Remove any existing listeners
|
||||
const newFileUploadArea = fileUploadArea.cloneNode(true);
|
||||
fileUploadArea.parentNode.replaceChild(newFileUploadArea, fileUploadArea);
|
||||
|
||||
newFileUploadArea.addEventListener('click', () => {
|
||||
newFileInput.click();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fieldDiv;
|
||||
}
|
||||
|
||||
function handleFileUpload(event, field) {
|
||||
const files = Array.from(event.target.files);
|
||||
if (files.length === 0) return;
|
||||
@ -1860,59 +1867,106 @@ function handleFileUpload(event, field) {
|
||||
|
||||
// Initialize Event Listeners
|
||||
function initEventListeners() {
|
||||
// Sidebar drag start
|
||||
document.querySelectorAll('.field-item').forEach(item => {
|
||||
// Wait for DOM to be fully loaded
|
||||
setTimeout(() => {
|
||||
// Sidebar drag start - ensure elements exist
|
||||
const fieldItems = document.querySelectorAll('.field-item');
|
||||
fieldItems.forEach(item => {
|
||||
if (!item.hasAttribute('data-drag-initialized')) {
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
const type = item.dataset.type;
|
||||
const label = item.dataset.label;
|
||||
startDrag(e, type, label);
|
||||
});
|
||||
});
|
||||
// Form stage drop zone
|
||||
item.setAttribute('data-drag-initialized', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
// Form stage drop zone
|
||||
if (elements.formStage) {
|
||||
elements.formStage.addEventListener('drop', drop);
|
||||
elements.formStage.addEventListener('dragover', allowDrop);
|
||||
elements.formStage.addEventListener('dragenter', dragEnter);
|
||||
elements.formStage.addEventListener('dragleave', dragLeave);
|
||||
elements.formStage.addEventListener('click', clearSelection);
|
||||
// Button events
|
||||
}
|
||||
|
||||
// Button events
|
||||
if (elements.addStageBtn) {
|
||||
elements.addStageBtn.addEventListener('click', addStage);
|
||||
}
|
||||
if (elements.saveFormBtn) {
|
||||
elements.saveFormBtn.addEventListener('click', saveForm);
|
||||
}
|
||||
if (elements.renameStageBtn) {
|
||||
elements.renameStageBtn.addEventListener('click', () => {
|
||||
openRenameModal(state.stages[state.currentStage]);
|
||||
if (state.stages[state.currentStage]) {
|
||||
openRenameModal(state.stages[state.currentStage]);
|
||||
}
|
||||
});
|
||||
// Form settings button
|
||||
}
|
||||
|
||||
// Form settings button
|
||||
if (elements.formSettingsBtn) {
|
||||
elements.formSettingsBtn.addEventListener('click', openFormSettingsModal);
|
||||
// Modal events
|
||||
}
|
||||
|
||||
// Modal events - Stage Rename
|
||||
if (elements.closeRenameModal) {
|
||||
elements.closeRenameModal.addEventListener('click', closeRenameModal);
|
||||
}
|
||||
if (elements.cancelRenameBtn) {
|
||||
elements.cancelRenameBtn.addEventListener('click', closeRenameModal);
|
||||
}
|
||||
if (elements.saveRenameBtn) {
|
||||
elements.saveRenameBtn.addEventListener('click', saveStageName);
|
||||
}
|
||||
if (elements.renameModal) {
|
||||
elements.renameModal.addEventListener('click', (e) => {
|
||||
if (e.target === elements.renameModal) {
|
||||
closeRenameModal();
|
||||
}
|
||||
});
|
||||
// Form settings modal events
|
||||
}
|
||||
|
||||
// Form settings modal events
|
||||
if (elements.closeFormSettingsModal) {
|
||||
elements.closeFormSettingsModal.addEventListener('click', closeFormSettingsModal);
|
||||
}
|
||||
if (elements.cancelFormSettingsBtn) {
|
||||
elements.cancelFormSettingsBtn.addEventListener('click', closeFormSettingsModal);
|
||||
}
|
||||
if (elements.saveFormSettingsBtn) {
|
||||
elements.saveFormSettingsBtn.addEventListener('click', saveFormSettings);
|
||||
}
|
||||
if (elements.formSettingsModal) {
|
||||
elements.formSettingsModal.addEventListener('click', (e) => {
|
||||
if (e.target === elements.formSettingsModal) {
|
||||
closeFormSettingsModal();
|
||||
}
|
||||
});
|
||||
// Field editor events
|
||||
}
|
||||
|
||||
// Field editor events
|
||||
if (elements.closeEditorBtn) {
|
||||
elements.closeEditorBtn.addEventListener('click', clearSelection);
|
||||
}
|
||||
if (elements.fieldLabel) {
|
||||
elements.fieldLabel.addEventListener('input', () => {
|
||||
if (state.selectedField) {
|
||||
state.selectedField.label = elements.fieldLabel.value;
|
||||
renderCurrentStage();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (elements.fieldPlaceholder) {
|
||||
elements.fieldPlaceholder.addEventListener('input', () => {
|
||||
if (state.selectedField) {
|
||||
state.selectedField.placeholder = elements.fieldPlaceholder.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (elements.requiredField) {
|
||||
elements.requiredField.addEventListener('change', () => {
|
||||
if (state.selectedField) {
|
||||
state.selectedField.required = elements.requiredField.checked;
|
||||
@ -1920,6 +1974,8 @@ function handleFileUpload(event, field) {
|
||||
renderCurrentStage();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (elements.addOptionBtn) {
|
||||
elements.addOptionBtn.addEventListener('click', () => {
|
||||
if (state.selectedField &&
|
||||
(state.selectedField.type === 'select' ||
|
||||
@ -1929,42 +1985,83 @@ function handleFileUpload(event, field) {
|
||||
renderOptionsEditor(state.selectedField);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (elements.fileTypes) {
|
||||
elements.fileTypes.addEventListener('input', () => {
|
||||
if (state.selectedField && state.selectedField.type === 'file') {
|
||||
state.selectedField.fileTypes = elements.fileTypes.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (elements.maxFileSize) {
|
||||
elements.maxFileSize.addEventListener('input', () => {
|
||||
if (state.selectedField && state.selectedField.type === 'file') {
|
||||
state.selectedField.maxFileSize = parseInt(elements.maxFileSize.value) || 5;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (elements.multipleFiles) {
|
||||
elements.multipleFiles.addEventListener('change', function() {
|
||||
if (elements.maxFiles) {
|
||||
elements.maxFiles.disabled = !this.checked;
|
||||
if (!this.checked) {
|
||||
elements.maxFiles.value = 1;
|
||||
if (state.selectedField) {
|
||||
state.selectedField.maxFiles = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100); // Small delay to ensure DOM is ready
|
||||
}
|
||||
|
||||
|
||||
// Initialize Application
|
||||
function init() {
|
||||
// Initialize form title
|
||||
elements.formTitle.textContent = 'Loading...';
|
||||
// Initialize form title
|
||||
if (elements.formTitle) {
|
||||
elements.formTitle.textContent = 'Loading...';
|
||||
}
|
||||
|
||||
// Hide the form stage initially to prevent flickering
|
||||
elements.formStage.style.display = 'none';
|
||||
elements.emptyState.style.display = 'block';
|
||||
elements.emptyState.innerHTML = '<i class="fas fa-spinner fa-spin"></i><p>Loading form template...</p>';
|
||||
if (elements.formStage) {
|
||||
elements.formStage.style.display = 'none';
|
||||
}
|
||||
if (elements.emptyState) {
|
||||
elements.emptyState.style.display = 'block';
|
||||
elements.emptyState.innerHTML = '<i class="fas fa-spinner fa-spin"></i><p>Loading form template...</p>';
|
||||
}
|
||||
|
||||
// Initialize event listeners first
|
||||
initEventListeners();
|
||||
|
||||
// Only render navigation if we have a template to load
|
||||
if (djangoConfig.loadUrl) {
|
||||
loadExistingTemplate();
|
||||
} else {
|
||||
// For new templates, show empty state
|
||||
elements.formTitle.textContent = 'New Form Template';
|
||||
elements.formStage.style.display = 'block';
|
||||
if (elements.formTitle) {
|
||||
elements.formTitle.textContent = 'New Form Template';
|
||||
}
|
||||
if (elements.formStage) {
|
||||
elements.formStage.style.display = 'block';
|
||||
}
|
||||
if (elements.emptyState) {
|
||||
elements.emptyState.style.display = 'block';
|
||||
elements.emptyState.innerHTML = '<i class="fas fa-cloud-upload-alt"></i><p>Drag form elements here to build your stage</p>';
|
||||
}
|
||||
renderStageNavigation();
|
||||
renderCurrentStage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the application
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
// Make sure the DOM is fully loaded before initializing
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -7,228 +7,57 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1><i class="fas fa-file-alt"></i> Submission Details</h1>
|
||||
<p class="text-muted mb-0">{{ form.name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="" class="btn btn-outline-primary me-2">
|
||||
<i class="fas fa-arrow-left"></i> Back to Submissions
|
||||
</a>
|
||||
<a href="" class="btn btn-outline-primary me-2" target="_blank">
|
||||
<i class="fas fa-eye"></i> Preview Form
|
||||
</a>
|
||||
<button class="btn btn-success" onclick="exportSubmission()">
|
||||
<i class="fas fa-download"></i> Export
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>Submission Details</h2>
|
||||
<a href="" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submission Summary -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 class="mb-0">{{ submission.id }}</h4>
|
||||
<small>Submission ID</small>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-hashtag fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 class="mb-0">{{ submission.submitted_at|date:"M d, Y" }}</h4>
|
||||
<small>Submitted</small>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-calendar-check fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-info text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 class="mb-0">{{ responses|length }}</h4>
|
||||
<small>Fields Completed</small>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-tasks fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submission Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Submission Information</h5>
|
||||
</div>
|
||||
<!-- Basic Information -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>Form:</strong></td>
|
||||
<td>{{ form.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Submitted:</strong></td>
|
||||
<td>{{ submission.submitted_at|date:"F d, Y H:i" }}</td>
|
||||
</tr>
|
||||
{% if submission.submitted_by %}
|
||||
<tr>
|
||||
<td><strong>Submitted By:</strong></td>
|
||||
<td>{{ submission.submitted_by.get_full_name|default:submission.submitted_by.username }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<div class="col-md-4">
|
||||
<strong>Submission ID:</strong> {{ submission.id }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if submission.applicant_name %}
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>Applicant Name:</strong></td>
|
||||
<td>{{ submission.applicant_name }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if submission.applicant_email %}
|
||||
<tr>
|
||||
<td><strong>Email:</strong></td>
|
||||
<td>{{ submission.applicant_email }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<div class="col-md-4">
|
||||
<strong>Submitted:</strong> {{ submission.submitted_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<strong>Form:</strong> {{ form.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Responses by Stage -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="fas fa-clipboard-list"></i> Submitted Responses</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for stage in stages %}
|
||||
<div class="mb-4">
|
||||
<div class="card border-primary">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-layer-group"></i> {{ stage.name }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% get_stage_responses stage_responses stage.id as stage_data %}
|
||||
{% if stage_data %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Field Label</th>
|
||||
<th>Field Type</th>
|
||||
<th>Response Value</th>
|
||||
<th>File</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for response in stage_data %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ response.field.label }}
|
||||
{% if response.field.required %}
|
||||
<span class="text-danger">*</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ response.field.get_field_type_display }}</td>
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<span class="text-primary">File: {{ response.uploaded_file.name }}</span>
|
||||
{% elif response.value %}
|
||||
{% if response.field.field_type == 'checkbox' and response.value|length > 0 %}
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for val in response.value %}
|
||||
<li><i class="fas fa-check text-success"></i> {{ val }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif response.field.field_type == 'radio' %}
|
||||
<span class="badge bg-info">{{ response.value }}</span>
|
||||
{% elif response.field.field_type == 'select' %}
|
||||
<span class="badge bg-secondary">{{ response.value }}</span>
|
||||
{% else %}
|
||||
<p class="mb-0">{{ response.value|linebreaksbr }}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">Not provided</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fas fa-inbox fa-2x mb-2"></i>
|
||||
<p>No responses submitted for this stage.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if submission.applicant_name or submission.applicant_email %}
|
||||
<div class="row mt-2">
|
||||
{% if submission.applicant_name %}
|
||||
<div class="col-md-6">
|
||||
<strong>Applicant Name:</strong> {{ submission.applicant_name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if submission.applicant_email %}
|
||||
<div class="col-md-6">
|
||||
<strong>Email:</strong> {{ submission.applicant_email }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not forloop.last %}
|
||||
<hr>
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-inbox fa-3x mb-3"></i>
|
||||
<h4>No stages found</h4>
|
||||
<p>This form doesn't have any stages defined.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw Data Table -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="fas fa-table"></i> All Responses (Raw Data)</h5>
|
||||
</div>
|
||||
<!-- Responses Table -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-3">Responses</h5>
|
||||
{% get_all_responses_flat stage_responses as all_responses %}
|
||||
{% if all_responses %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Stage</th>
|
||||
<th>Field Label</th>
|
||||
<th>Field Type</th>
|
||||
<th>Required</th>
|
||||
<th>Response Value</th>
|
||||
<th>File</th>
|
||||
</tr>
|
||||
@ -236,35 +65,24 @@
|
||||
<tbody>
|
||||
{% for response in all_responses %}
|
||||
<tr>
|
||||
<td>{{ response.stage_name }}</td>
|
||||
<td>
|
||||
{{ response.field_label }}
|
||||
<strong>{{ response.field_label }}</strong>
|
||||
{% if response.required %}
|
||||
<span class="text-danger">*</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ response.field_type }}</td>
|
||||
<td>
|
||||
{% if response.required %}
|
||||
<span class="badge bg-danger">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">No</span>
|
||||
<span class="text-danger">*</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<span class="text-primary">File: {{ response.uploaded_file.name }}</span>
|
||||
<span class="text-primary">File: {{ response.uploaded_file.name }}</span>
|
||||
{% elif response.value %}
|
||||
{% if response.field_type == 'checkbox' and response.value|length > 0 %}
|
||||
<ul class="list-unstyled mb-0">
|
||||
<div>
|
||||
{% for val in response.value %}
|
||||
<li><i class="fas fa-check text-success"></i> {{ val }}</li>
|
||||
<span class="badge bg-secondary me-1">{{ val }}</span>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif response.field_type == 'radio' %}
|
||||
</div>
|
||||
{% elif response.field_type == 'radio' or response.field_type == 'select' %}
|
||||
<span class="badge bg-info">{{ response.value }}</span>
|
||||
{% elif response.field_type == 'select' %}
|
||||
<span class="badge bg-secondary">{{ response.value }}</span>
|
||||
{% else %}
|
||||
<p class="mb-0">{{ response.value|linebreaksbr }}</p>
|
||||
{% endif %}
|
||||
@ -274,9 +92,9 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</a>
|
||||
<a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -285,138 +103,28 @@
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fas fa-inbox fa-2x mb-2"></i>
|
||||
<div class="text-center text-muted py-4">
|
||||
<p>No responses found for this submission.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">
|
||||
<i class="fas fa-exclamation-triangle text-danger"></i> Confirm Delete
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete this submission? This action cannot be undone.</p>
|
||||
<div class="alert alert-warning">
|
||||
<strong>Submission ID:</strong> {{ submission.id }}<br>
|
||||
<strong>Submitted:</strong> {{ submission.submitted_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
<i class="fas fa-trash"></i> Delete Submission
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.response-value {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.response-value ul {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid #e9ecef;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.card-title {
|
||||
font-size: 0.9rem;
|
||||
/* Minimal styling */
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: #495057;
|
||||
}
|
||||
.table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
.response-value {
|
||||
max-width: 300px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function exportSubmission() {
|
||||
// Create export options modal or direct download
|
||||
const format = prompt('Export format (csv, json, pdf):', 'csv');
|
||||
if (format) {
|
||||
window.open(`/recruitment/api/forms/{{ form.id }}/submissions/{{ submission.id }}/export/?format=${format}`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle delete confirmation
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const deleteBtn = document.querySelector('a[href*="delete"]');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle confirm delete
|
||||
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
||||
if (confirmDeleteBtn) {
|
||||
confirmDeleteBtn.addEventListener('click', function() {
|
||||
fetch(`/recruitment/api/forms/{{ form.id }}/submissions/{{ submission.id }}/delete/`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrfToken(),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Submission deleted successfully!');
|
||||
|
||||
} else {
|
||||
alert('Error deleting submission: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error deleting submission');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function getCsrfToken() {
|
||||
const cookie = document.cookie.split(';').find(c => c.trim().startsWith('csrftoken='));
|
||||
return cookie ? cookie.split('=')[1] : '';
|
||||
}
|
||||
|
||||
// Print functionality
|
||||
function printSubmission() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
// Add print button to header
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const headerActions = document.querySelector('.d-flex.justify-content-between.align-items-center.mb-4');
|
||||
if (headerActions) {
|
||||
const printBtn = document.createElement('button');
|
||||
printBtn.className = 'btn btn-outline-secondary me-2';
|
||||
printBtn.innerHTML = '<i class="fas fa-print"></i> Print';
|
||||
printBtn.onclick = printSubmission;
|
||||
headerActions.insertBefore(printBtn, headerActions.firstChild);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -269,6 +269,7 @@
|
||||
{# Action area - visually separated with pt-2 border-top #}
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
|
||||
<a href="{% url 'form_wizard' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "Preview" %}
|
||||
</a>
|
||||
|
||||
76
templates/interviews/email/interview_invitation.html
Normal file
76
templates/interviews/email/interview_invitation.html
Normal file
@ -0,0 +1,76 @@
|
||||
<!-- templates/interviews/email/interview_invitation.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Interview Invitation</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.footer {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Interview Invitation</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Dear {{ candidate_name }},</p>
|
||||
<p>We are pleased to invite you for an interview for the position of <strong>{{ job_title }}</strong> at <strong>{{ company_name }}</strong>.</p>
|
||||
|
||||
<h3>Interview Details:</h3>
|
||||
<ul>
|
||||
<li><strong>Date:</strong> {{ interview_date|date:"l, F j, Y" }}</li>
|
||||
<li><strong>Time:</strong> {{ interview_time|time:"g:i A" }}</li>
|
||||
<li><strong>Platform:</strong> Zoom Video Conference</li>
|
||||
<li><strong>Meeting ID:</strong> {{ meeting_id }}</li>
|
||||
</ul>
|
||||
|
||||
<p>Please join the interview using the link below:</p>
|
||||
<a href="{{ join_url }}" class="button">Join Interview</a>
|
||||
|
||||
<p>If you have any questions or need to reschedule, please contact us at your earliest convenience.</p>
|
||||
|
||||
<p>We look forward to speaking with you!</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The Hiring Team<br>
|
||||
{{ company_name }}</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This is an automated message. Please do not reply to this email.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
23
templates/interviews/email/interview_invitation.txt
Normal file
23
templates/interviews/email/interview_invitation.txt
Normal file
@ -0,0 +1,23 @@
|
||||
<!-- templates/interviews/email/interview_invitation.txt -->
|
||||
Interview Invitation
|
||||
|
||||
Dear {{ candidate_name }},
|
||||
|
||||
We are pleased to invite you for an interview for the position of {{ job_title }} at {{ company_name }}.
|
||||
|
||||
Interview Details:
|
||||
- Date: {{ interview_date|date:"l, F j, Y" }}
|
||||
- Time: {{ interview_time|time:"g:i A" }}
|
||||
- Platform: Zoom Video Conference
|
||||
- Meeting ID: {{ meeting_id }}
|
||||
|
||||
Please join the interview using the link below:
|
||||
{{ join_url }}
|
||||
|
||||
If you have any questions or need to reschedule, please contact us at your earliest convenience.
|
||||
|
||||
We look forward to speaking with you!
|
||||
|
||||
Best regards,
|
||||
The Hiring Team
|
||||
{{ company_name }}
|
||||
125
templates/interviews/preview_schedule.html
Normal file
125
templates/interviews/preview_schedule.html
Normal file
@ -0,0 +1,125 @@
|
||||
<!-- templates/interviews/preview_schedule.html -->
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>Interview Schedule Preview for {{ job.title }}</h1>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h5>Schedule Details</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Period:</strong> {{ start_date|date:"F j, Y" }} to {{ end_date|date:"F j, Y" }}</p>
|
||||
<p><strong>Working Days:</strong>
|
||||
{% for day_id in working_days %}
|
||||
{% if day_id == 0 %}Monday{% endif %}
|
||||
{% if day_id == 1 %}Tuesday{% endif %}
|
||||
{% if day_id == 2 %}Wednesday{% endif %}
|
||||
{% if day_id == 3 %}Thursday{% endif %}
|
||||
{% if day_id == 4 %}Friday{% endif %}
|
||||
{% if day_id == 5 %}Saturday{% endif %}
|
||||
{% if day_id == 6 %}Sunday{% endif %}
|
||||
{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p><strong>Working Hours:</strong> {{ start_time|time:"g:i A" }} to {{ end_time|time:"g:i A" }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if break_start_time and break_end_time %}
|
||||
<p><strong>Break Time:</strong> {{ break_start_time|time:"g:i A" }} to {{ break_end_time|time:"g:i A" }}</p>
|
||||
{% endif %}
|
||||
<p><strong>Interview Duration:</strong> {{ interview_duration }} minutes</p>
|
||||
<p><strong>Buffer Time:</strong> {{ buffer_time }} minutes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h5>Scheduled Interviews</h5>
|
||||
|
||||
<!-- Calendar View -->
|
||||
<div id="calendar-container">
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<div class="table-responsive mt-4">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Candidate</th>
|
||||
<th>Email</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in schedule %}
|
||||
<tr>
|
||||
<td>{{ item.date|date:"F j, Y" }}</td>
|
||||
<td>{{ item.time|time:"g:i A" }}</td>
|
||||
<td>{{ item.candidate.name }}</td>
|
||||
<td>{{ item.candidate.email }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<form method="post" class="mt-4">
|
||||
{% csrf_token %}
|
||||
<button type="submit" name="confirm_schedule" class="btn btn-success">
|
||||
<i class="fas fa-check"></i> Confirm Schedule
|
||||
</button>
|
||||
<a href="{% url 'schedule_interviews' job_id=job.id %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Edit
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include FullCalendar CSS and JS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek'
|
||||
},
|
||||
events: [
|
||||
{% for item in schedule %}
|
||||
{
|
||||
title: '{{ item.candidate.name }}',
|
||||
start: '{{ item.date|date:"Y-m-d" }}T{{ item.time|time:"H:i:s" }}',
|
||||
url: '#',
|
||||
extendedProps: {
|
||||
email: '{{ item.candidate.email }}',
|
||||
time: '{{ item.time|time:"g:i A" }}'
|
||||
}
|
||||
},
|
||||
{% endfor %}
|
||||
],
|
||||
eventClick: function(info) {
|
||||
// Show candidate details in a modal or alert
|
||||
alert('Candidate: ' + info.event.title +
|
||||
'\nDate: ' + info.event.start.toLocaleDateString() +
|
||||
'\nTime: ' + info.event.extendedProps.time +
|
||||
'\nEmail: ' + info.event.extendedProps.email);
|
||||
info.jsEvent.preventDefault();
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
97
templates/interviews/schedule_interviews.html
Normal file
97
templates/interviews/schedule_interviews.html
Normal file
@ -0,0 +1,97 @@
|
||||
<!-- templates/interviews/schedule_interviews.html -->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>Schedule Interviews for {{ job.title }}</h1>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Select Candidates</h5>
|
||||
<div class="form-group">
|
||||
{{ form.candidates }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Schedule Details</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.start_date.id_for_label }}">Start Date</label>
|
||||
{{ form.start_date }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.end_date.id_for_label }}">End Date</label>
|
||||
{{ form.end_date }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Working Days</label>
|
||||
{{ form.working_days }}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.start_time.id_for_label }}">Start Time</label>
|
||||
{{ form.start_time }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.end_time.id_for_label }}">End Time</label>
|
||||
{{ form.end_time }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.break_start_time.id_for_label }}">Break Start Time</label>
|
||||
{{ form.break_start_time }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.break_end_time.id_for_label }}">Break End Time</label>
|
||||
{{ form.break_end_time }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.interview_duration.id_for_label }}">Interview Duration (minutes)</label>
|
||||
{{ form.interview_duration }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.buffer_time.id_for_label }}">Buffer Time (minutes)</label>
|
||||
{{ form.buffer_time }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">Schedule Interviews</button>
|
||||
<a href="{% url 'job_detail' pk=job.slug %}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user