add scheduler
This commit is contained in:
parent
48f61f173f
commit
d26c18fefd
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
|
||||
@ -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]
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user