This commit is contained in:
Faheed 2025-10-07 18:35:03 +03:00
commit ef952ab596
11 changed files with 81 additions and 49 deletions

View File

@ -5,7 +5,7 @@ from django.utils import timezone
from .models import (
JobPosting, Candidate, TrainingMaterial, ZoomMeeting,
FormTemplate, FormStage, FormField, FormSubmission, FieldResponse,
SharedFormTemplate, Source, HiringAgency, IntegrationLog
SharedFormTemplate, Source, HiringAgency, IntegrationLog,InterviewSchedule
)
class FormFieldInline(admin.TabularInline):
@ -260,5 +260,5 @@ class FormSubmissionAdmin(admin.ModelAdmin):
admin.site.register(FormStage)
admin.site.register(FormField)
admin.site.register(FieldResponse)
admin.site.register(SharedFormTemplate)
admin.site.register(InterviewSchedule)
# admin.site.register(HiringAgency)

View File

@ -413,7 +413,7 @@ class FormTemplateForm(forms.ModelForm):
class InterviewScheduleForm(forms.ModelForm):
candidates = forms.ModelMultipleChoiceField(
queryset=Candidate.objects.none(),
widget=forms.CheckboxSelectMultiple,
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}),
required=True
)
working_days = forms.MultipleChoiceField(
@ -426,9 +426,9 @@ class InterviewScheduleForm(forms.ModelForm):
(5, 'Saturday'),
(6, 'Sunday'),
],
widget=forms.CheckboxSelectMultiple,
widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check'}),
required=True
)
)
class Meta:
model = InterviewSchedule
@ -438,18 +438,27 @@ class InterviewScheduleForm(forms.ModelForm):
'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'}),
'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'break_start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'break_end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
}
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,
job__slug=slug,
stage='Interview'
)
)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-md-3'
self.helper.field_class = 'col-md-9'
def clean_working_days(self):
working_days = self.cleaned_data.get('working_days')
# Convert string values to integers
return [int(day) for day in working_days]

View File

@ -393,12 +393,13 @@ def schedule_interviews(schedule):
Returns the number of interviews successfully scheduled.
"""
candidates = list(schedule.candidates.all())
print(candidates)
if not candidates:
return 0
# Calculate available time slots
available_slots = get_available_time_slots(schedule)
print(available_slots)
if len(available_slots) < len(candidates):
raise ValueError(f"Not enough available slots. Required: {len(candidates)}, Available: {len(available_slots)}")
@ -474,7 +475,8 @@ def get_available_time_slots(schedule):
end_date = schedule.end_date
# Convert working days to a set for quick lookup
working_days_set = set(schedule.working_days)
# working_days should be a list of integers where 0=Monday, 1=Tuesday, etc.
working_days_set = set(int(day) for day in schedule.working_days)
# Parse times
start_time = schedule.start_time
@ -485,31 +487,51 @@ def get_available_time_slots(schedule):
# Calculate slot duration (interview duration + buffer time)
slot_duration = timedelta(minutes=schedule.interview_duration + schedule.buffer_time)
# Debug output - remove in production
print(f"Working days: {working_days_set}")
print(f"Date range: {current_date} to {end_date}")
print(f"Time range: {start_time} to {end_time}")
print(f"Slot duration: {slot_duration}")
while current_date <= end_date:
# Check if current day is a working day
if current_date.weekday() in working_days_set:
weekday = current_date.weekday() # Monday is 0, Sunday is 6
print(f"Checking {current_date}, weekday: {weekday}, in working days: {weekday in working_days_set}")
if 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
while True:
# Calculate the end time of this slot
slot_end_time = (datetime.combine(current_date, current_time) + slot_duration).time()
# Add this slot to available slots
slots.append({
'date': current_date,
'time': current_time
})
# Check if the slot fits within the working hours
if slot_end_time > end_time:
break
# Check if slot conflicts with break time
conflict_with_break = False
if break_start and break_end:
# Check if the slot overlaps with break time
if not (current_time >= break_end or slot_end_time <= break_start):
conflict_with_break = True
print(f"Slot {current_time}-{slot_end_time} conflicts with break {break_start}-{break_end}")
if not conflict_with_break:
# Add this slot to available slots
slots.append({
'date': current_date,
'time': current_time
})
print(f"Added slot: {current_date} {current_time}")
# Move to next slot
current_datetime = datetime.combine(current_date, current_time)
current_datetime += slot_duration
current_datetime = datetime.combine(current_date, current_time) + slot_duration
current_time = current_datetime.time()
# Move to next day
current_date += timedelta(days=1)
print(f"Total slots generated: {len(slots)}")
return slots

View File

@ -1,5 +1,6 @@
import json
import requests
from rich import print
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.http import JsonResponse

View File

@ -75,7 +75,7 @@
<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">
<a href="{% url 'schedule_interviews' slug=job.slug %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Edit
</a>
</form>

View File

@ -88,7 +88,7 @@
<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>
<a href="{% url 'job_detail' slug=job.slug %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>

View File

@ -39,7 +39,7 @@
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Outlined Button Styles */
.btn-outline-primary {
color: var(--kaauh-teal);
@ -58,7 +58,7 @@
color: white;
border-color: var(--kaauh-teal-dark);
}
.btn-outline-info {
.btn-outline-info {
color: #17a2b8;
border-color: #17a2b8;
}
@ -75,7 +75,7 @@
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Candidate Header Card (The teal header) */
.candidate-header-card {
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
@ -95,7 +95,7 @@
border-radius: 0.4rem;
font-weight: 700;
}
/* Left Column Tabs (Main Content Tabs) */
.main-tabs {
border-bottom: 1px solid var(--kaauh-border);
@ -130,11 +130,11 @@
}
.right-column-card .nav-link {
padding: 0.9rem 1rem;
font-size: 0.95rem;
font-size: 0.95rem;
font-weight: 600;
color: var(--kaauh-primary-text);
border-right: 1px solid var(--kaauh-border);
border-bottom: 1px solid var(--kaauh-border);
border-bottom: 1px solid var(--kaauh-border);
background-color: #f8f9fa;
}
.right-column-card .nav-item:last-child .nav-link {
@ -144,14 +144,14 @@
background-color: white;
color: var(--kaauh-teal-dark);
border-bottom: 3px solid var(--kaauh-teal);
border-right-color: transparent;
border-right-color: transparent;
margin-bottom: -1px;
}
.right-column-card .tab-content {
padding: 1.5rem 1.25rem;
background-color: white;
}
/* ==================================== */
/* NEW: PARSED DATA GRID OPTIMIZATION */
/* ==================================== */
@ -171,11 +171,11 @@
{% block content %}
<div class="container-fluid py-4">
<div class="row g-4">
{# LEFT COLUMN: MAIN CANDIDATE DETAILS AND TABS #}
<div class="col-lg-8">
<div class="card shadow-sm no-hover">
{# HEADER SECTION (The original Candidate Header Card content, redesigned) #}
<div class="candidate-header-card">
<div class="d-flex justify-content-between align-items-start flex-wrap">
@ -185,11 +185,11 @@
<span class="badge {% if candidate.applied %}bg-success{% else %}bg-warning{% endif %}">
{{ candidate.applied|yesno:"Applied,Pending" }}
</span>
<span id="stageDisplay" class="badge"
<span id="stageDisplay" class="badge"
data-class="{'bg-primary': $stage == 'Applied', 'bg-info': $stage == 'Exam', 'bg-warning': $stage == 'Interview', 'bg-success': $stage == 'Offer'}"
data-signals-stage="'{{ candidate.stage }}'">
{% trans "Stage:" %}
<span data-text="'{{ candidate.stage }}'">{{ candidate.stage }}</span>
{% trans "Stage:" %}
<span data-text="$stage"></span>
</span>
</div>
<small class="text-white opacity-75">
@ -204,7 +204,7 @@
{% endif %}
</div>
</div>
{# LEFT TABS NAVIGATION #}
<ul class="nav nav-tabs main-tabs" id="candidateTabs" role="tablist">
<li class="nav-item" role="presentation">
@ -230,7 +230,7 @@
<div class="card-body">
<div class="tab-content" id="candidateTabsContent">
{# TAB 1 CONTENT: CONTACT & DATES (Original Contact Card) #}
<div class="tab-pane fade show active" id="contact-pane" role="tabpanel" aria-labelledby="contact-tab">
<h5 class="text-primary mb-4">{% trans "Core Details" %}</h5>
@ -297,7 +297,7 @@
</div>
</div>
{% endif %}
</div>
</div>
</div>
@ -305,7 +305,7 @@
{# RIGHT COLUMN: ACTIONS AND PARSED DATA TABS #}
<div class="col-lg-4">
{# ACTIONS CARD (The new consolidated action card) #}
{% if user.is_staff %}
<div class="card shadow-sm mb-4 p-3">
@ -345,7 +345,7 @@
</ul>
<div class="tab-content" id="parsedDataTabsContent">
{# TAB 1: PARSED DATA - UPDATED TO 2-COLUMN GRID #}
<div class="tab-pane fade show active" id="data-pane" role="tabpanel" aria-labelledby="data-tab">
<h6 class="text-muted small text-uppercase mb-3">{% trans "Structured Resume Data" %}</h6>