This commit is contained in:
Faheed 2025-10-07 17:55:26 +03:00
commit 7a0bf3262d
24 changed files with 986 additions and 447 deletions

BIN
db.sqlite3 Normal file

Binary file not shown.

View File

@ -4,7 +4,7 @@ from crispy_forms.helper import FormHelper
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from crispy_forms.layout import Layout, Submit, HTML, Div, Field 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 CandidateForm(forms.ModelForm):
class Meta: class Meta:
@ -409,3 +409,47 @@ class FormTemplateForm(forms.ModelForm):
Field('is_active', css_class='form-check-input'), Field('is_active', css_class='form-check-input'),
Submit('submit', _('Create Template'), css_class='btn btn-primary mt-3') 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'
)

View File

@ -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,
},
),
]

View File

@ -714,3 +714,46 @@ class HiringAgency(Base):
verbose_name = _('Hiring Agency') verbose_name = _('Hiring Agency')
verbose_name_plural = _('Hiring Agencies') verbose_name_plural = _('Hiring Agencies')
ordering = ['name'] 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}"

View File

@ -22,8 +22,8 @@ def get_all_responses_flat(stage_responses):
""" """
all_responses = [] all_responses = []
if stage_responses: if stage_responses:
print(stage_responses.get(9).get("responses")[0].value)
for stage_id, responses in stage_responses.items(): for stage_id, responses in stage_responses.items():
if responses: # Check if responses list exists and is not empty
for response in responses: for response in responses:
# Check if response is an object or string # Check if response is an object or string
if hasattr(response, 'stage') and hasattr(response, 'field'): if hasattr(response, 'stage') and hasattr(response, 'field'):

View File

@ -19,6 +19,7 @@ urlpatterns = [
path('jobs/linkedin/login/', views.linkedin_login, name='linkedin_login'), path('jobs/linkedin/login/', views.linkedin_login, name='linkedin_login'),
path('jobs/linkedin/callback/', views.linkedin_callback, name='linkedin_callback'), 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 # Candidate URLs
path('candidates/', views_frontend.CandidateListView.as_view(), name='candidate_list'), path('candidates/', views_frontend.CandidateListView.as_view(), name='candidate_list'),
path('candidates/create/', views_frontend.CandidateCreateView.as_view(), name='candidate_create'), path('candidates/create/', views_frontend.CandidateCreateView.as_view(), name='candidate_create'),

View File

@ -4,7 +4,11 @@
# import requests # import requests
from recruitment import models from recruitment import models
from django.conf import settings 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") # nlp = spacy.load("en_core_web_sm")
# def extract_text_from_pdf(pdf_path): # def extract_text_from_pdf(pdf_path):
@ -382,3 +386,130 @@ def delete_zoom_meeting(meeting_id):
"status": "error", "status": "error",
"message": str(e) "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

View File

@ -9,17 +9,17 @@ from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm,InterviewScheduleForm
from rest_framework import viewsets from rest_framework import viewsets
from django.contrib import messages from django.contrib import messages
from django.core.paginator import Paginator from django.core.paginator import Paginator
from .linkedin_service import LinkedInService 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 .models import ZoomMeeting, Candidate, JobPosting
from .serializers import JobPostingSerializer, CandidateSerializer from .serializers import JobPostingSerializer, CandidateSerializer
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.views.generic import CreateView,UpdateView,DetailView,ListView 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 from django.views.decorators.csrf import ensure_csrf_cookie
import logging import logging
@ -843,3 +843,136 @@ def form_submission_details(request, form_id, submission_id):
'responses': responses, 'responses': responses,
'stage_responses': stage_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
})

View File

@ -1139,6 +1139,8 @@
fileSettings: document.getElementById('fileSettings'), fileSettings: document.getElementById('fileSettings'),
fileTypes: document.getElementById('fileTypes'), fileTypes: document.getElementById('fileTypes'),
maxFileSize: document.getElementById('maxFileSize'), maxFileSize: document.getElementById('maxFileSize'),
multipleFiles: document.getElementById('multipleFiles'),
maxFiles: document.getElementById('maxFiles'),
closeEditorBtn: document.getElementById('closeEditorBtn'), closeEditorBtn: document.getElementById('closeEditorBtn'),
// Form settings elements // Form settings elements
formTitle: document.getElementById('formTitle'), formTitle: document.getElementById('formTitle'),
@ -1151,7 +1153,6 @@
cancelFormSettingsBtn: document.getElementById('cancelFormSettingsBtn'), cancelFormSettingsBtn: document.getElementById('cancelFormSettingsBtn'),
saveFormSettingsBtn: document.getElementById('saveFormSettingsBtn') saveFormSettingsBtn: document.getElementById('saveFormSettingsBtn')
}; };
// Utility Functions // Utility Functions
function getFieldIcon(type) { function getFieldIcon(type) {
const icons = { const icons = {
@ -1443,7 +1444,6 @@
fileUpload.appendChild(uploadedFile); fileUpload.appendChild(uploadedFile);
}); });
} }
fieldContent.appendChild(fileUpload); fieldContent.appendChild(fileUpload);
} else if (field.type === 'select') { } else if (field.type === 'select') {
const select = document.createElement('select'); const select = document.createElement('select');
@ -1515,18 +1515,16 @@
}); });
}); });
// Make draggable // Make draggable for reordering
fieldDiv.draggable = true; fieldDiv.draggable = true;
fieldDiv.addEventListener('dragstart', (e) => { fieldDiv.addEventListener('dragstart', (e) => {
state.draggedFieldIndex = parseInt(fieldDiv.dataset.fieldIndex); state.draggedFieldIndex = parseInt(fieldDiv.dataset.fieldIndex);
e.dataTransfer.setData('text/plain', 'reorder'); e.dataTransfer.setData('text/plain', 'reorder');
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
}); });
fieldDiv.addEventListener('dragover', (e) => { fieldDiv.addEventListener('dragover', (e) => {
e.preventDefault(); e.preventDefault();
}); });
fieldDiv.addEventListener('drop', (e) => { fieldDiv.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
const targetIndex = parseInt(fieldDiv.dataset.fieldIndex); const targetIndex = parseInt(fieldDiv.dataset.fieldIndex);
@ -1536,21 +1534,30 @@
// Add file input event listener // Add file input event listener
const fileInput = fieldDiv.querySelector('.file-input'); const fileInput = fieldDiv.querySelector('.file-input');
if (fileInput) { 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); handleFileUpload(e, field);
}); });
// Make the file upload area clickable // Make the file upload area clickable
const fileUploadArea = fieldDiv.querySelector('.file-upload-area'); const fileUploadArea = fieldDiv.querySelector('.file-upload-area');
if (fileUploadArea) { if (fileUploadArea) {
fileUploadArea.addEventListener('click', () => { // Remove any existing listeners
fileInput.click(); const newFileUploadArea = fileUploadArea.cloneNode(true);
fileUploadArea.parentNode.replaceChild(newFileUploadArea, fileUploadArea);
newFileUploadArea.addEventListener('click', () => {
newFileInput.click();
}); });
} }
} }
return fieldDiv; return fieldDiv;
} }
function handleFileUpload(event, field) { function handleFileUpload(event, field) {
const files = Array.from(event.target.files); const files = Array.from(event.target.files);
if (files.length === 0) return; if (files.length === 0) return;
@ -1860,59 +1867,106 @@ function handleFileUpload(event, field) {
// Initialize Event Listeners // Initialize Event Listeners
function initEventListeners() { function initEventListeners() {
// Sidebar drag start // Wait for DOM to be fully loaded
document.querySelectorAll('.field-item').forEach(item => { 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) => { item.addEventListener('dragstart', (e) => {
const type = item.dataset.type; const type = item.dataset.type;
const label = item.dataset.label; const label = item.dataset.label;
startDrag(e, type, label); startDrag(e, type, label);
}); });
item.setAttribute('data-drag-initialized', 'true');
}
}); });
// Form stage drop zone // Form stage drop zone
if (elements.formStage) {
elements.formStage.addEventListener('drop', drop); elements.formStage.addEventListener('drop', drop);
elements.formStage.addEventListener('dragover', allowDrop); elements.formStage.addEventListener('dragover', allowDrop);
elements.formStage.addEventListener('dragenter', dragEnter); elements.formStage.addEventListener('dragenter', dragEnter);
elements.formStage.addEventListener('dragleave', dragLeave); elements.formStage.addEventListener('dragleave', dragLeave);
elements.formStage.addEventListener('click', clearSelection); elements.formStage.addEventListener('click', clearSelection);
}
// Button events // Button events
if (elements.addStageBtn) {
elements.addStageBtn.addEventListener('click', addStage); elements.addStageBtn.addEventListener('click', addStage);
}
if (elements.saveFormBtn) {
elements.saveFormBtn.addEventListener('click', saveForm); elements.saveFormBtn.addEventListener('click', saveForm);
}
if (elements.renameStageBtn) {
elements.renameStageBtn.addEventListener('click', () => { elements.renameStageBtn.addEventListener('click', () => {
if (state.stages[state.currentStage]) {
openRenameModal(state.stages[state.currentStage]); openRenameModal(state.stages[state.currentStage]);
}
}); });
}
// Form settings button // Form settings button
if (elements.formSettingsBtn) {
elements.formSettingsBtn.addEventListener('click', openFormSettingsModal); elements.formSettingsBtn.addEventListener('click', openFormSettingsModal);
// Modal events }
// Modal events - Stage Rename
if (elements.closeRenameModal) {
elements.closeRenameModal.addEventListener('click', closeRenameModal); elements.closeRenameModal.addEventListener('click', closeRenameModal);
}
if (elements.cancelRenameBtn) {
elements.cancelRenameBtn.addEventListener('click', closeRenameModal); elements.cancelRenameBtn.addEventListener('click', closeRenameModal);
}
if (elements.saveRenameBtn) {
elements.saveRenameBtn.addEventListener('click', saveStageName); elements.saveRenameBtn.addEventListener('click', saveStageName);
}
if (elements.renameModal) {
elements.renameModal.addEventListener('click', (e) => { elements.renameModal.addEventListener('click', (e) => {
if (e.target === elements.renameModal) { if (e.target === elements.renameModal) {
closeRenameModal(); closeRenameModal();
} }
}); });
}
// Form settings modal events // Form settings modal events
if (elements.closeFormSettingsModal) {
elements.closeFormSettingsModal.addEventListener('click', closeFormSettingsModal); elements.closeFormSettingsModal.addEventListener('click', closeFormSettingsModal);
}
if (elements.cancelFormSettingsBtn) {
elements.cancelFormSettingsBtn.addEventListener('click', closeFormSettingsModal); elements.cancelFormSettingsBtn.addEventListener('click', closeFormSettingsModal);
}
if (elements.saveFormSettingsBtn) {
elements.saveFormSettingsBtn.addEventListener('click', saveFormSettings); elements.saveFormSettingsBtn.addEventListener('click', saveFormSettings);
}
if (elements.formSettingsModal) {
elements.formSettingsModal.addEventListener('click', (e) => { elements.formSettingsModal.addEventListener('click', (e) => {
if (e.target === elements.formSettingsModal) { if (e.target === elements.formSettingsModal) {
closeFormSettingsModal(); closeFormSettingsModal();
} }
}); });
}
// Field editor events // Field editor events
if (elements.closeEditorBtn) {
elements.closeEditorBtn.addEventListener('click', clearSelection); elements.closeEditorBtn.addEventListener('click', clearSelection);
}
if (elements.fieldLabel) {
elements.fieldLabel.addEventListener('input', () => { elements.fieldLabel.addEventListener('input', () => {
if (state.selectedField) { if (state.selectedField) {
state.selectedField.label = elements.fieldLabel.value; state.selectedField.label = elements.fieldLabel.value;
renderCurrentStage(); renderCurrentStage();
} }
}); });
}
if (elements.fieldPlaceholder) {
elements.fieldPlaceholder.addEventListener('input', () => { elements.fieldPlaceholder.addEventListener('input', () => {
if (state.selectedField) { if (state.selectedField) {
state.selectedField.placeholder = elements.fieldPlaceholder.value; state.selectedField.placeholder = elements.fieldPlaceholder.value;
} }
}); });
}
if (elements.requiredField) {
elements.requiredField.addEventListener('change', () => { elements.requiredField.addEventListener('change', () => {
if (state.selectedField) { if (state.selectedField) {
state.selectedField.required = elements.requiredField.checked; state.selectedField.required = elements.requiredField.checked;
@ -1920,6 +1974,8 @@ function handleFileUpload(event, field) {
renderCurrentStage(); renderCurrentStage();
} }
}); });
}
if (elements.addOptionBtn) {
elements.addOptionBtn.addEventListener('click', () => { elements.addOptionBtn.addEventListener('click', () => {
if (state.selectedField && if (state.selectedField &&
(state.selectedField.type === 'select' || (state.selectedField.type === 'select' ||
@ -1929,42 +1985,83 @@ function handleFileUpload(event, field) {
renderOptionsEditor(state.selectedField); renderOptionsEditor(state.selectedField);
} }
}); });
}
if (elements.fileTypes) {
elements.fileTypes.addEventListener('input', () => { elements.fileTypes.addEventListener('input', () => {
if (state.selectedField && state.selectedField.type === 'file') { if (state.selectedField && state.selectedField.type === 'file') {
state.selectedField.fileTypes = elements.fileTypes.value; state.selectedField.fileTypes = elements.fileTypes.value;
} }
}); });
}
if (elements.maxFileSize) {
elements.maxFileSize.addEventListener('input', () => { elements.maxFileSize.addEventListener('input', () => {
if (state.selectedField && state.selectedField.type === 'file') { if (state.selectedField && state.selectedField.type === 'file') {
state.selectedField.maxFileSize = parseInt(elements.maxFileSize.value) || 5; 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 // Initialize Application
function init() { function init() {
// Initialize form title // Initialize form title
if (elements.formTitle) {
elements.formTitle.textContent = 'Loading...'; elements.formTitle.textContent = 'Loading...';
}
// Hide the form stage initially to prevent flickering // Hide the form stage initially to prevent flickering
if (elements.formStage) {
elements.formStage.style.display = 'none'; elements.formStage.style.display = 'none';
}
if (elements.emptyState) {
elements.emptyState.style.display = 'block'; elements.emptyState.style.display = 'block';
elements.emptyState.innerHTML = '<i class="fas fa-spinner fa-spin"></i><p>Loading form template...</p>'; 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 // Only render navigation if we have a template to load
if (djangoConfig.loadUrl) { if (djangoConfig.loadUrl) {
loadExistingTemplate(); loadExistingTemplate();
} else { } else {
// For new templates, show empty state // For new templates, show empty state
if (elements.formTitle) {
elements.formTitle.textContent = 'New Form Template'; elements.formTitle.textContent = 'New Form Template';
}
if (elements.formStage) {
elements.formStage.style.display = 'block'; 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(); renderStageNavigation();
renderCurrentStage(); renderCurrentStage();
} }
} }
// Start the application // Make sure the DOM is fully loaded before initializing
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -7,228 +7,57 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-3">
<div> <h2>Submission Details</h2>
<h1><i class="fas fa-file-alt"></i> Submission Details</h1> <a href="" class="btn btn-outline-secondary">
<p class="text-muted mb-0">{{ form.name }}</p> <i class="fas fa-arrow-left"></i> Back
</div>
<div>
<a href="" class="btn btn-outline-primary me-2">
<i class="fas fa-arrow-left"></i> Back to Submissions
</a> </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> </div>
</div> </div>
</div> </div>
<!-- Submission Summary --> <!-- Basic Information -->
<div class="row mb-4"> <div class="card mb-3">
<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>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-4">
<table class="table table-sm"> <strong>Submission ID:</strong> {{ submission.id }}
<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> </div>
<div class="col-md-6"> <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>
{% if submission.applicant_name or submission.applicant_email %}
<div class="row mt-2">
{% if submission.applicant_name %} {% if submission.applicant_name %}
<table class="table table-sm"> <div class="col-md-6">
<tr> <strong>Applicant Name:</strong> {{ submission.applicant_name }}
<td><strong>Applicant Name:</strong></td> </div>
<td>{{ submission.applicant_name }}</td>
</tr>
{% endif %} {% endif %}
{% if submission.applicant_email %} {% if submission.applicant_email %}
<tr> <div class="col-md-6">
<td><strong>Email:</strong></td> <strong>Email:</strong> {{ submission.applicant_email }}
<td>{{ submission.applicant_email }}</td> </div>
</tr>
{% endif %} {% endif %}
</table>
</div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
<!-- Form Responses by Stage --> <!-- Responses Table -->
<div class="card"> <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>
</div>
</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>
<div class="card-body"> <div class="card-body">
<h5 class="mb-3">Responses</h5>
{% get_all_responses_flat stage_responses as all_responses %} {% get_all_responses_flat stage_responses as all_responses %}
{% if all_responses %} {% if all_responses %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-striped">
<thead class="table-dark"> <thead>
<tr> <tr>
<th>Stage</th>
<th>Field Label</th> <th>Field Label</th>
<th>Field Type</th>
<th>Required</th>
<th>Response Value</th> <th>Response Value</th>
<th>File</th> <th>File</th>
</tr> </tr>
@ -236,35 +65,24 @@
<tbody> <tbody>
{% for response in all_responses %} {% for response in all_responses %}
<tr> <tr>
<td>{{ response.stage_name }}</td>
<td> <td>
{{ response.field_label }} <strong>{{ response.field_label }}</strong>
{% if response.required %} {% if response.required %}
<span class="text-danger">*</span> <span class="text-danger">*</span>
{% endif %} {% endif %}
</td> </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>
{% endif %}
</td>
<td> <td>
{% if response.uploaded_file %} {% 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 %} {% elif response.value %}
{% if response.field_type == 'checkbox' and response.value|length > 0 %} {% if response.field_type == 'checkbox' and response.value|length > 0 %}
<ul class="list-unstyled mb-0"> <div>
{% for val in response.value %} {% 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 %} {% endfor %}
</ul> </div>
{% elif response.field_type == 'radio' %} {% elif response.field_type == 'radio' or response.field_type == 'select' %}
<span class="badge bg-info">{{ response.value }}</span> <span class="badge bg-info">{{ response.value }}</span>
{% elif response.field_type == 'select' %}
<span class="badge bg-secondary">{{ response.value }}</span>
{% else %} {% else %}
<p class="mb-0">{{ response.value|linebreaksbr }}</p> <p class="mb-0">{{ response.value|linebreaksbr }}</p>
{% endif %} {% endif %}
@ -285,138 +103,28 @@
</table> </table>
</div> </div>
{% else %} {% else %}
<div class="text-center text-muted py-3"> <div class="text-center text-muted py-4">
<i class="fas fa-inbox fa-2x mb-2"></i>
<p>No responses found for this submission.</p> <p>No responses found for this submission.</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </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 %} {% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
.response-value { /* Minimal styling */
max-height: 200px; .table th {
overflow-y: auto; border-top: none;
}
.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;
font-weight: 600; font-weight: 600;
color: #333; color: #495057;
}
.table td {
vertical-align: top;
}
.response-value {
max-width: 300px;
} }
</style> </style>
{% endblock %} {% 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 %}

View File

@ -269,6 +269,7 @@
{# Action area - visually separated with pt-2 border-top #} {# Action area - visually separated with pt-2 border-top #}
<div class="mt-auto 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"> <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"> <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" %} <i class="fas fa-eye me-1"></i> {% trans "Preview" %}
</a> </a>

View 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>

View 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 }}

View 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 %}

View 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 %}