update the zoom meeting
This commit is contained in:
parent
ce15603802
commit
ffae8b2e64
1
ZoomMeetingAPISpec.json
Normal file
1
ZoomMeetingAPISpec.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-12 21:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0004_alter_candidate_interview_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='interviewschedule',
|
||||||
|
name='breaks',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interviewschedule',
|
||||||
|
name='breaks',
|
||||||
|
field=models.JSONField(blank=True, default=list, verbose_name='Break Times'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
recruitment/migrations/0006_zoommeeting_meeting_status.py
Normal file
18
recruitment/migrations/0006_zoommeeting_meeting_status.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-13 12:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0005_remove_interviewschedule_breaks_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='zoommeeting',
|
||||||
|
name='meeting_status',
|
||||||
|
field=models.CharField(choices=[('scheduled', 'Scheduled'), ('started', 'Started'), ('ended', 'Ended')], default='scheduled', max_length=20, verbose_name='Meeting Status'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-13 12:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0006_zoommeeting_meeting_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='zoommeeting',
|
||||||
|
name='meeting_status',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='zoommeeting',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
recruitment/migrations/0008_zoommeeting_password.py
Normal file
18
recruitment/migrations/0008_zoommeeting_password.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-13 12:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0007_remove_zoommeeting_meeting_status_zoommeeting_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='zoommeeting',
|
||||||
|
name='password',
|
||||||
|
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Password'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -421,7 +421,6 @@ class Candidate(Base):
|
|||||||
return self.STAGE_SEQUENCE.get(old_stage, [])
|
return self.STAGE_SEQUENCE.get(old_stage, [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
||||||
def submission(self):
|
def submission(self):
|
||||||
return FormSubmission.objects.filter(template__job=self.job).first()
|
return FormSubmission.objects.filter(template__job=self.job).first()
|
||||||
@property
|
@property
|
||||||
@ -432,6 +431,16 @@ class Candidate(Base):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.full_name
|
return self.full_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_meetings(self):
|
||||||
|
return self.scheduled_interviews.all()
|
||||||
|
@property
|
||||||
|
def get_latest_meeting(self):
|
||||||
|
schedule = self.scheduled_interviews.order_by('-created_at').first()
|
||||||
|
if schedule:
|
||||||
|
return schedule.zoom_meeting
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class TrainingMaterial(Base):
|
class TrainingMaterial(Base):
|
||||||
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
||||||
@ -453,6 +462,10 @@ class TrainingMaterial(Base):
|
|||||||
|
|
||||||
|
|
||||||
class ZoomMeeting(Base):
|
class ZoomMeeting(Base):
|
||||||
|
class MeetingStatus(models.TextChoices):
|
||||||
|
SCHEDULED = "scheduled", _("Scheduled")
|
||||||
|
STARTED = "started", _("Started")
|
||||||
|
ENDED = "ended", _("Ended")
|
||||||
# Basic meeting details
|
# Basic meeting details
|
||||||
topic = models.CharField(max_length=255, verbose_name=_("Topic"))
|
topic = models.CharField(max_length=255, verbose_name=_("Topic"))
|
||||||
meeting_id = models.CharField(
|
meeting_id = models.CharField(
|
||||||
@ -469,6 +482,9 @@ class ZoomMeeting(Base):
|
|||||||
participant_video = models.BooleanField(
|
participant_video = models.BooleanField(
|
||||||
default=True, verbose_name=_("Participant Video")
|
default=True, verbose_name=_("Participant Video")
|
||||||
)
|
)
|
||||||
|
password = models.CharField(
|
||||||
|
max_length=20, blank=True, null=True, verbose_name=_("Password")
|
||||||
|
)
|
||||||
join_before_host = models.BooleanField(
|
join_before_host = models.BooleanField(
|
||||||
default=False, verbose_name=_("Join Before Host")
|
default=False, verbose_name=_("Join Before Host")
|
||||||
)
|
)
|
||||||
@ -480,6 +496,12 @@ class ZoomMeeting(Base):
|
|||||||
zoom_gateway_response = models.JSONField(
|
zoom_gateway_response = models.JSONField(
|
||||||
blank=True, null=True, verbose_name=_("Zoom Gateway Response")
|
blank=True, null=True, verbose_name=_("Zoom Gateway Response")
|
||||||
)
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Status"),
|
||||||
|
)
|
||||||
# Timestamps
|
# Timestamps
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -952,7 +974,9 @@ class InterviewSchedule(Base):
|
|||||||
) # Store days of week as [0,1,2,3,4] for Mon-Fri
|
) # Store days of week as [0,1,2,3,4] for Mon-Fri
|
||||||
start_time = models.TimeField(verbose_name=_("Start Time"))
|
start_time = models.TimeField(verbose_name=_("Start Time"))
|
||||||
end_time = models.TimeField(verbose_name=_("End Time"))
|
end_time = models.TimeField(verbose_name=_("End Time"))
|
||||||
breaks = models.ManyToManyField(BreakTime, blank=True, related_name="schedules")
|
|
||||||
|
breaks = models.JSONField(default=list, blank=True, verbose_name=_('Break Times'))
|
||||||
|
|
||||||
interview_duration = models.PositiveIntegerField(
|
interview_duration = models.PositiveIntegerField(
|
||||||
verbose_name=_("Interview Duration (minutes)")
|
verbose_name=_("Interview Duration (minutes)")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -66,8 +66,10 @@ urlpatterns = [
|
|||||||
path('forms/', views.form_templates_list, name='form_templates_list'),
|
path('forms/', views.form_templates_list, name='form_templates_list'),
|
||||||
path('forms/create-template/', views.create_form_template, name='create_form_template'),
|
path('forms/create-template/', views.create_form_template, name='create_form_template'),
|
||||||
|
|
||||||
path('jobs/<slug:slug>/candidate-tiers/', views.candidate_tier_management_view, name='candidate_tier_management'),
|
path('jobs/<slug:slug>/candidate_screening_view/', views.candidate_screening_view, name='candidate_screening_view'),
|
||||||
path('jobs/<slug:slug>/candidate_exam_view/', views.candidate_exam_view, name='candidate_exam_view'),
|
path('jobs/<slug:slug>/candidate_exam_view/', views.candidate_exam_view, name='candidate_exam_view'),
|
||||||
|
path('jobs/<slug:slug>/candidate_interview_view/', views.candidate_interview_view, name='candidate_interview_view'),
|
||||||
|
|
||||||
path('jobs/<slug:slug>/update_candidate_exam_status/', views.update_candidate_exam_status, name='update_candidate_exam_status'),
|
path('jobs/<slug:slug>/update_candidate_exam_status/', views.update_candidate_exam_status, name='update_candidate_exam_status'),
|
||||||
path('jobs/<slug:slug>/bulk_update_candidate_exam_status/', views.bulk_update_candidate_exam_status, name='bulk_update_candidate_exam_status'),
|
path('jobs/<slug:slug>/bulk_update_candidate_exam_status/', views.bulk_update_candidate_exam_status, name='bulk_update_candidate_exam_status'),
|
||||||
|
|
||||||
@ -89,5 +91,6 @@ urlpatterns = [
|
|||||||
# path('api/forms/save/', views.save_form_builder, name='save_form_builder'),
|
# path('api/forms/save/', views.save_form_builder, name='save_form_builder'),
|
||||||
# path('api/forms/<int:form_id>/load/', views.load_form, name='load_form'),
|
# path('api/forms/<int:form_id>/load/', views.load_form, name='load_form'),
|
||||||
# path('api/forms/<int:form_id>/update/', views.update_form_builder, name='update_form_builder'),
|
# path('api/forms/<int:form_id>/update/', views.update_form_builder, name='update_form_builder'),
|
||||||
|
path('jobs/<slug:slug>/calendar/', views.interview_calendar_view, name='interview_calendar'),
|
||||||
|
path('jobs/<slug:slug>/calendar/interview/<int:interview_id>/', views.interview_detail_view, name='interview_detail'),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -161,7 +161,7 @@ def create_zoom_meeting(topic, start_time, duration):
|
|||||||
meeting_details = {
|
meeting_details = {
|
||||||
"topic": topic,
|
"topic": topic,
|
||||||
"type": 2,
|
"type": 2,
|
||||||
"start_time": start_time,
|
"start_time": start_time.isoformat() + "Z",
|
||||||
"duration": duration,
|
"duration": duration,
|
||||||
"timezone": "UTC",
|
"timezone": "UTC",
|
||||||
"settings": {
|
"settings": {
|
||||||
@ -273,6 +273,7 @@ def get_zoom_meeting_details(meeting_id):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing the meeting details or an error message.
|
dict: A dictionary containing the meeting details or an error message.
|
||||||
|
The 'start_time' in 'meeting_details' will be a Python datetime object.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
access_token = get_access_token()
|
access_token = get_access_token()
|
||||||
@ -288,6 +289,19 @@ def get_zoom_meeting_details(meeting_id):
|
|||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
meeting_data = response.json()
|
meeting_data = response.json()
|
||||||
|
if 'start_time' in meeting_data and meeting_data['start_time']:
|
||||||
|
try:
|
||||||
|
# Convert ISO 8601 string (with 'Z' for UTC) to datetime object
|
||||||
|
meeting_data['start_time'] = str(datetime.fromisoformat(
|
||||||
|
meeting_data['start_time'].replace('Z', '+00:00')
|
||||||
|
))
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to parse start_time '{meeting_data['start_time']}' for meeting {meeting_id}: {e}"
|
||||||
|
)
|
||||||
|
meeting_data['start_time'] = None # Ensure it's None on failure
|
||||||
|
else:
|
||||||
|
meeting_data['start_time'] = None # Explicitly set to None if not present
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": "Meeting details retrieved successfully.",
|
"message": "Meeting details retrieved successfully.",
|
||||||
@ -325,11 +339,13 @@ def update_zoom_meeting(meeting_id, updated_data):
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
response = requests.patch(
|
response = requests.patch(
|
||||||
f"https://api.zoom.us/v2/meetings/{meeting_id}",
|
f"https://api.zoom.us/v2/meetings/{meeting_id}/",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json=updated_data
|
json=updated_data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print(response.status_code)
|
||||||
|
|
||||||
if response.status_code == 204:
|
if response.status_code == 204:
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@ -465,7 +481,7 @@ def send_interview_email(scheduled_interview):
|
|||||||
fail_silently=False,
|
fail_silently=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_available_time_slots(schedule, breaks=None):
|
def get_available_time_slots(schedule):
|
||||||
"""
|
"""
|
||||||
Generate a list of available time slots based on the schedule criteria.
|
Generate a list of available time slots based on the schedule criteria.
|
||||||
Returns a list of dictionaries with 'date' and 'time' keys.
|
Returns a list of dictionaries with 'date' and 'time' keys.
|
||||||
@ -475,7 +491,6 @@ def get_available_time_slots(schedule, breaks=None):
|
|||||||
end_date = schedule.end_date
|
end_date = schedule.end_date
|
||||||
|
|
||||||
# Convert working days to a set for quick lookup
|
# Convert working days to a set for quick lookup
|
||||||
# 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)
|
working_days_set = set(int(day) for day in schedule.working_days)
|
||||||
|
|
||||||
# Parse times
|
# Parse times
|
||||||
@ -485,17 +500,12 @@ def get_available_time_slots(schedule, breaks=None):
|
|||||||
# Calculate slot duration (interview duration + buffer time)
|
# Calculate slot duration (interview duration + buffer time)
|
||||||
slot_duration = timedelta(minutes=schedule.interview_duration + schedule.buffer_time)
|
slot_duration = timedelta(minutes=schedule.interview_duration + schedule.buffer_time)
|
||||||
|
|
||||||
# Debug output - remove in production
|
# Get breaks from the schedule
|
||||||
print(f"Working days: {working_days_set}")
|
breaks = schedule.breaks if hasattr(schedule, 'breaks') and schedule.breaks else []
|
||||||
print(f"Date range: {current_date} to {end_date}")
|
|
||||||
print(f"Time range: {start_time} to {end_time}")
|
|
||||||
print(f"Slot duration: {slot_duration}")
|
|
||||||
print(f"Breaks: {breaks}")
|
|
||||||
|
|
||||||
while current_date <= end_date:
|
while current_date <= end_date:
|
||||||
# Check if current day is a working day
|
# Check if current day is a working day
|
||||||
weekday = current_date.weekday() # Monday is 0, Sunday is 6
|
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:
|
if weekday in working_days_set:
|
||||||
# Generate slots for this day
|
# Generate slots for this day
|
||||||
@ -511,13 +521,18 @@ def get_available_time_slots(schedule, breaks=None):
|
|||||||
|
|
||||||
# Check if slot conflicts with any break time
|
# Check if slot conflicts with any break time
|
||||||
conflict_with_break = False
|
conflict_with_break = False
|
||||||
if breaks:
|
for break_data in breaks:
|
||||||
for break_time in breaks:
|
# Parse break times
|
||||||
|
try:
|
||||||
|
break_start = datetime.strptime(break_data['start_time'], '%H:%M:%S').time()
|
||||||
|
break_end = datetime.strptime(break_data['end_time'], '%H:%M:%S').time()
|
||||||
|
|
||||||
# Check if the slot overlaps with this break time
|
# Check if the slot overlaps with this break time
|
||||||
if not (current_time >= break_time.end_time or slot_end_time <= break_time.start_time):
|
if not (current_time >= break_end or slot_end_time <= break_start):
|
||||||
conflict_with_break = True
|
conflict_with_break = True
|
||||||
print(f"Slot {current_time}-{slot_end_time} conflicts with break {break_time.start_time}-{break_time.end_time}")
|
|
||||||
break
|
break
|
||||||
|
except (ValueError, KeyError) as e:
|
||||||
|
continue
|
||||||
|
|
||||||
if not conflict_with_break:
|
if not conflict_with_break:
|
||||||
# Add this slot to available slots
|
# Add this slot to available slots
|
||||||
@ -525,7 +540,6 @@ def get_available_time_slots(schedule, breaks=None):
|
|||||||
'date': current_date,
|
'date': current_date,
|
||||||
'time': current_time
|
'time': current_time
|
||||||
})
|
})
|
||||||
print(f"Added slot: {current_date} {current_time}")
|
|
||||||
|
|
||||||
# Move to next slot
|
# Move to next slot
|
||||||
current_datetime = datetime.combine(current_date, current_time) + slot_duration
|
current_datetime = datetime.combine(current_date, current_time) + slot_duration
|
||||||
@ -534,11 +548,9 @@ def get_available_time_slots(schedule, breaks=None):
|
|||||||
# Move to next day
|
# Move to next day
|
||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
print(f"Total slots generated: {len(slots)}")
|
|
||||||
return slots
|
return slots
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def json_to_markdown_table(data_list):
|
def json_to_markdown_table(data_list):
|
||||||
if not data_list:
|
if not data_list:
|
||||||
return ""
|
return ""
|
||||||
@ -550,4 +562,4 @@ def json_to_markdown_table(data_list):
|
|||||||
for row in data_list:
|
for row in data_list:
|
||||||
values = [str(row.get(header, "")) for header in headers]
|
values = [str(row.get(header, "")) for header in headers]
|
||||||
markdown += "| " + " | ".join(values) + " |\n"
|
markdown += "| " + " | ".join(values) + " |\n"
|
||||||
return markdown
|
return markdown
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from rich import print
|
from rich import print
|
||||||
|
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from datetime import datetime
|
from datetime import datetime,time,timedelta
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -31,6 +32,7 @@ from .utils import (
|
|||||||
create_zoom_meeting,
|
create_zoom_meeting,
|
||||||
delete_zoom_meeting,
|
delete_zoom_meeting,
|
||||||
update_zoom_meeting,
|
update_zoom_meeting,
|
||||||
|
get_zoom_meeting_details,
|
||||||
schedule_interviews,
|
schedule_interviews,
|
||||||
get_available_time_slots,
|
get_available_time_slots,
|
||||||
)
|
)
|
||||||
@ -47,6 +49,7 @@ from .models import (
|
|||||||
ZoomMeeting,
|
ZoomMeeting,
|
||||||
Candidate,
|
Candidate,
|
||||||
JobPosting,
|
JobPosting,
|
||||||
|
ScheduledInterview
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
from datastar_py.django import (
|
from datastar_py.django import (
|
||||||
@ -81,15 +84,18 @@ class ZoomMeetingCreateView(CreateView):
|
|||||||
if instance.start_time < timezone.now():
|
if instance.start_time < timezone.now():
|
||||||
messages.error(self.request, "Start time must be in the future.")
|
messages.error(self.request, "Start time must be in the future.")
|
||||||
return redirect("/create-meeting/", status=400)
|
return redirect("/create-meeting/", status=400)
|
||||||
start_time = instance.start_time.isoformat() + "Z"
|
start_time = instance.start_time
|
||||||
|
# start_time = instance.start_time.isoformat() + "Z"
|
||||||
duration = instance.duration
|
duration = instance.duration
|
||||||
|
|
||||||
result = create_zoom_meeting(topic, start_time, duration)
|
result = create_zoom_meeting(topic, start_time, duration)
|
||||||
|
print(result)
|
||||||
if result["status"] == "success":
|
if result["status"] == "success":
|
||||||
instance.meeting_id = result["meeting_details"]["meeting_id"]
|
instance.meeting_id = result["meeting_details"]["meeting_id"]
|
||||||
instance.join_url = result["meeting_details"]["join_url"]
|
instance.join_url = result["meeting_details"]["join_url"]
|
||||||
instance.host_email = result["meeting_details"]["host_email"]
|
instance.host_email = result["meeting_details"]["host_email"]
|
||||||
|
instance.password = result["meeting_details"]["password"]
|
||||||
|
instance.status = result["zoom_gateway_response"]["status"]
|
||||||
instance.zoom_gateway_response = result["zoom_gateway_response"]
|
instance.zoom_gateway_response = result["zoom_gateway_response"]
|
||||||
instance.save()
|
instance.save()
|
||||||
messages.success(self.request, result["message"])
|
messages.success(self.request, result["message"])
|
||||||
@ -139,6 +145,21 @@ class ZoomMeetingUpdateView(UpdateView):
|
|||||||
template_name = "meetings/update_meeting.html"
|
template_name = "meetings/update_meeting.html"
|
||||||
success_url = "/"
|
success_url = "/"
|
||||||
|
|
||||||
|
# def get_form_kwargs(self):
|
||||||
|
# kwargs = super().get_form_kwargs()
|
||||||
|
# # Ensure the form is initialized with the instance's current values
|
||||||
|
# if self.object:
|
||||||
|
# kwargs['initial'] = getattr(kwargs, 'initial', {})
|
||||||
|
# initial_start_time = ""
|
||||||
|
# if self.object.start_time:
|
||||||
|
# try:
|
||||||
|
# initial_start_time = self.object.start_time.strftime('%m-%d-%Y,T%H:%M')
|
||||||
|
# except AttributeError:
|
||||||
|
# print(f"Warning: start_time {self.object.start_time} is not a datetime object.")
|
||||||
|
# initial_start_time = ""
|
||||||
|
# kwargs['initial']['start_time'] = initial_start_time
|
||||||
|
# return kwargs
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
instance = form.save(commit=False)
|
instance = form.save(commit=False)
|
||||||
updated_data = {
|
updated_data = {
|
||||||
@ -149,15 +170,39 @@ class ZoomMeetingUpdateView(UpdateView):
|
|||||||
if instance.start_time < timezone.now():
|
if instance.start_time < timezone.now():
|
||||||
messages.error(self.request, "Start time must be in the future.")
|
messages.error(self.request, "Start time must be in the future.")
|
||||||
return redirect(f"/update-meeting/{instance.pk}/", status=400)
|
return redirect(f"/update-meeting/{instance.pk}/", status=400)
|
||||||
|
|
||||||
result = update_zoom_meeting(instance.meeting_id, updated_data)
|
result = update_zoom_meeting(instance.meeting_id, updated_data)
|
||||||
|
|
||||||
if result["status"] == "success":
|
if result["status"] == "success":
|
||||||
instance.save()
|
# Fetch the latest details from Zoom after successful update
|
||||||
messages.success(self.request, result["message"])
|
details_result = get_zoom_meeting_details(instance.meeting_id)
|
||||||
return redirect(reverse("meeting_details", kwargs={"pk": instance.pk}))
|
|
||||||
|
if details_result["status"] == "success":
|
||||||
|
zoom_details = details_result["meeting_details"]
|
||||||
|
# Update instance with fetched details
|
||||||
|
|
||||||
|
instance.topic = zoom_details.get("topic", instance.topic)
|
||||||
|
|
||||||
|
instance.duration = zoom_details.get("duration", instance.duration)
|
||||||
|
instance.join_url = zoom_details.get("join_url", instance.join_url)
|
||||||
|
instance.password = zoom_details.get("password", instance.password)
|
||||||
|
# Corrected status assignment: instance.status, not instance.password
|
||||||
|
instance.status = zoom_details.get("status")
|
||||||
|
|
||||||
|
instance.zoom_gateway_response = details_result.get("meeting_details") # Store full response
|
||||||
|
instance.save()
|
||||||
|
messages.success(self.request, result["message"] + " Local data updated from Zoom.")
|
||||||
|
else:
|
||||||
|
# If fetching details fails, save with form data and log a warning
|
||||||
|
logger.warning(
|
||||||
|
f"Successfully updated Zoom meeting {instance.meeting_id}, but failed to fetch updated details. "
|
||||||
|
f"Error: {details_result.get('message', 'Unknown error')}"
|
||||||
|
)
|
||||||
|
instance.save() # Save with data from the form
|
||||||
|
messages.success(self.request, result["message"] + " (Note: Could not refresh local data from Zoom.)")
|
||||||
|
return redirect(reverse("meeting_details", kwargs={"slug": instance.slug}))
|
||||||
else:
|
else:
|
||||||
messages.error(self.request, result["message"])
|
messages.error(self.request, result["message"])
|
||||||
return redirect(reverse("meeting_details", kwargs={"pk": instance.pk}))
|
return redirect(reverse("meeting_details", kwargs={"slug": instance.slug}))
|
||||||
|
|
||||||
|
|
||||||
def ZoomMeetingDeleteView(request, pk):
|
def ZoomMeetingDeleteView(request, pk):
|
||||||
@ -1065,12 +1110,11 @@ def form_submission_details(request, template_id, slug):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def schedule_interviews_view(request, slug):
|
||||||
def schedule_interviews_view(request, job_id):
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
job = get_object_or_404(JobPosting, id=job_id)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = InterviewScheduleForm(job_id, request.POST)
|
form = InterviewScheduleForm(slug, request.POST)
|
||||||
break_formset = BreakTimeFormSet(request.POST)
|
break_formset = BreakTimeFormSet(request.POST)
|
||||||
|
|
||||||
# Check if this is a confirmation request
|
# Check if this is a confirmation request
|
||||||
@ -1079,21 +1123,31 @@ def schedule_interviews_view(request, job_id):
|
|||||||
schedule_data = request.session.get("interview_schedule_data")
|
schedule_data = request.session.get("interview_schedule_data")
|
||||||
if not schedule_data:
|
if not schedule_data:
|
||||||
messages.error(request, "Session expired. Please try again.")
|
messages.error(request, "Session expired. Please try again.")
|
||||||
return redirect("schedule_interviews", job_id=job_id)
|
return redirect("schedule_interviews", slug=slug)
|
||||||
|
|
||||||
# Create the interview schedule
|
# Create the interview schedule
|
||||||
schedule = InterviewSchedule.objects.create(
|
schedule = InterviewSchedule.objects.create(
|
||||||
job=job, created_by=request.user, **schedule_data
|
job=job,
|
||||||
|
created_by=request.user,
|
||||||
|
start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
|
||||||
|
end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
|
||||||
|
working_days=schedule_data["working_days"],
|
||||||
|
start_time=time.fromisoformat(schedule_data["start_time"]),
|
||||||
|
end_time=time.fromisoformat(schedule_data["end_time"]),
|
||||||
|
interview_duration=schedule_data["interview_duration"],
|
||||||
|
buffer_time=schedule_data["buffer_time"],
|
||||||
|
breaks=schedule_data["breaks"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add candidates to the schedule
|
# Add candidates to the schedule
|
||||||
candidates = Candidate.objects.filter(id__in=schedule_data["candidate_ids"])
|
candidates = Candidate.objects.filter(id__in=schedule_data["candidate_ids"])
|
||||||
schedule.candidates.set(candidates)
|
schedule.candidates.set(candidates)
|
||||||
|
|
||||||
# Add break times to the schedule
|
# Create temporary break time objects for slot calculation
|
||||||
if "breaks" in schedule_data and schedule_data["breaks"]:
|
temp_breaks = []
|
||||||
for break_data in schedule_data["breaks"]:
|
for break_data in schedule_data["breaks"]:
|
||||||
break_time = BreakTime.objects.create(
|
temp_breaks.append(
|
||||||
|
BreakTime(
|
||||||
start_time=datetime.strptime(
|
start_time=datetime.strptime(
|
||||||
break_data["start_time"], "%H:%M:%S"
|
break_data["start_time"], "%H:%M:%S"
|
||||||
).time(),
|
).time(),
|
||||||
@ -1101,21 +1155,71 @@ def schedule_interviews_view(request, job_id):
|
|||||||
break_data["end_time"], "%H:%M:%S"
|
break_data["end_time"], "%H:%M:%S"
|
||||||
).time(),
|
).time(),
|
||||||
)
|
)
|
||||||
schedule.breaks.add(break_time)
|
|
||||||
|
|
||||||
# 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:
|
# Get available slots
|
||||||
del request.session["interview_schedule_data"]
|
available_slots = get_available_time_slots(schedule)
|
||||||
return redirect("job_detail", pk=job_id)
|
|
||||||
except Exception as e:
|
# Create scheduled interviews
|
||||||
messages.error(request, f"Error scheduling interviews: {str(e)}")
|
scheduled_count = 0
|
||||||
return redirect("schedule_interviews", job_id=job_id)
|
for i, candidate in enumerate(candidates):
|
||||||
|
if i < len(available_slots):
|
||||||
|
slot = available_slots[i]
|
||||||
|
interview_datetime = datetime.combine(slot['date'], slot['time'])
|
||||||
|
|
||||||
|
# Create Zoom meeting
|
||||||
|
meeting_topic = f"Interview for {job.title} - {candidate.name}"
|
||||||
|
|
||||||
|
start_time = interview_datetime.isoformat() + "Z"
|
||||||
|
|
||||||
|
zoom_meeting = create_zoom_meeting(
|
||||||
|
topic=meeting_topic,
|
||||||
|
start_time=start_time,
|
||||||
|
duration=schedule.interview_duration
|
||||||
|
)
|
||||||
|
|
||||||
|
result = create_zoom_meeting(meeting_topic, start_time, schedule.interview_duration)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
zoom_meeting = ZoomMeeting.objects.create(
|
||||||
|
topic=meeting_topic,
|
||||||
|
start_time=interview_datetime,
|
||||||
|
duration=schedule.interview_duration,
|
||||||
|
meeting_id=result["meeting_details"]["meeting_id"],
|
||||||
|
join_url=result["meeting_details"]["join_url"],
|
||||||
|
zoom_gateway_response=result["zoom_gateway_response"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create scheduled interview record
|
||||||
|
scheduled_interview = ScheduledInterview.objects.create(
|
||||||
|
candidate=candidate,
|
||||||
|
job=job,
|
||||||
|
zoom_meeting=zoom_meeting,
|
||||||
|
schedule=schedule,
|
||||||
|
interview_date=slot['date'],
|
||||||
|
interview_time=slot['time']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send email to candidate
|
||||||
|
# try:
|
||||||
|
# send_interview_email(scheduled_interview)
|
||||||
|
# except Exception as e:
|
||||||
|
# messages.warning(
|
||||||
|
# request,
|
||||||
|
# f"Interview scheduled for {candidate.name}, but failed to send email: {str(e)}"
|
||||||
|
# )
|
||||||
|
|
||||||
|
scheduled_count += 1
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
# This is the initial form submission
|
# This is the initial form submission
|
||||||
if form.is_valid() and break_formset.is_valid():
|
if form.is_valid() and break_formset.is_valid():
|
||||||
@ -1139,8 +1243,8 @@ def schedule_interviews_view(request, job_id):
|
|||||||
{
|
{
|
||||||
"start_time": break_form.cleaned_data[
|
"start_time": break_form.cleaned_data[
|
||||||
"start_time"
|
"start_time"
|
||||||
].isoformat(),
|
].strftime("%H:%M:%S"),
|
||||||
"end_time": break_form.cleaned_data["end_time"].isoformat(),
|
"end_time": break_form.cleaned_data["end_time"].strftime("%H:%M:%S"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1154,6 +1258,7 @@ def schedule_interviews_view(request, job_id):
|
|||||||
end_time=end_time,
|
end_time=end_time,
|
||||||
interview_duration=interview_duration,
|
interview_duration=interview_duration,
|
||||||
buffer_time=buffer_time,
|
buffer_time=buffer_time,
|
||||||
|
breaks=breaks,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create temporary break time objects
|
# Create temporary break time objects
|
||||||
@ -1171,7 +1276,7 @@ def schedule_interviews_view(request, job_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Get available slots
|
# Get available slots
|
||||||
available_slots = get_available_time_slots(temp_schedule, temp_breaks)
|
available_slots = get_available_time_slots(temp_schedule)
|
||||||
|
|
||||||
if len(available_slots) < len(candidates):
|
if len(available_slots) < len(candidates):
|
||||||
messages.error(
|
messages.error(
|
||||||
@ -1224,7 +1329,7 @@ def schedule_interviews_view(request, job_id):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
form = InterviewScheduleForm(job_id=job_id)
|
form = InterviewScheduleForm(slug=slug)
|
||||||
break_formset = BreakTimeFormSet()
|
break_formset = BreakTimeFormSet()
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
@ -1232,9 +1337,157 @@ def schedule_interviews_view(request, job_id):
|
|||||||
"interviews/schedule_interviews.html",
|
"interviews/schedule_interviews.html",
|
||||||
{"form": form, "break_formset": break_formset, "job": job},
|
{"form": form, "break_formset": break_formset, "job": job},
|
||||||
)
|
)
|
||||||
|
# def schedule_interviews_view(request, slug):
|
||||||
|
# job = get_object_or_404(JobPosting, slug=slug)
|
||||||
|
|
||||||
|
# if request.method == "POST":
|
||||||
|
# form = InterviewScheduleForm(slug, request.POST)
|
||||||
|
# break_formset = BreakTimeFormSet(request.POST)
|
||||||
|
|
||||||
def candidate_tier_management_view(request, slug):
|
# # 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,
|
||||||
|
# start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
|
||||||
|
# end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
|
||||||
|
# working_days=schedule_data["working_days"],
|
||||||
|
# start_time=time.fromisoformat(schedule_data["start_time"]),
|
||||||
|
# end_time=time.fromisoformat(schedule_data["end_time"]),
|
||||||
|
# interview_duration=schedule_data["interview_duration"],
|
||||||
|
# buffer_time=schedule_data["buffer_time"],
|
||||||
|
# breaks=schedule_data["breaks"], # Direct assignment for JSON field
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # 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() and break_formset.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"]
|
||||||
|
# interview_duration = form.cleaned_data["interview_duration"]
|
||||||
|
# buffer_time = form.cleaned_data["buffer_time"]
|
||||||
|
|
||||||
|
# # Process break times
|
||||||
|
# breaks = []
|
||||||
|
# for break_form in break_formset:
|
||||||
|
# if break_form.cleaned_data and not break_form.cleaned_data.get(
|
||||||
|
# "DELETE"
|
||||||
|
# ):
|
||||||
|
# breaks.append(
|
||||||
|
# {
|
||||||
|
# "start_time": break_form.cleaned_data[
|
||||||
|
# "start_time"
|
||||||
|
# ].strftime("%H:%M:%S"),
|
||||||
|
# "end_time": break_form.cleaned_data["end_time"].strftime("%H:%M:%S"),
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # Create a temporary schedule object (not saved to DB)
|
||||||
|
# temp_schedule = InterviewSchedule(
|
||||||
|
# job=job,
|
||||||
|
# start_date=start_date,
|
||||||
|
# end_date=end_date,
|
||||||
|
# working_days=working_days,
|
||||||
|
# start_time=start_time,
|
||||||
|
# end_time=end_time,
|
||||||
|
# interview_duration=interview_duration,
|
||||||
|
# buffer_time=buffer_time,
|
||||||
|
# breaks=breaks, # Direct assignment for JSON field
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # 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, "break_formset": break_formset, "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(),
|
||||||
|
# "interview_duration": interview_duration,
|
||||||
|
# "buffer_time": buffer_time,
|
||||||
|
# "candidate_ids": [c.id for c in candidates],
|
||||||
|
# "breaks": breaks,
|
||||||
|
# }
|
||||||
|
# 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,
|
||||||
|
# "breaks": breaks,
|
||||||
|
# "interview_duration": interview_duration,
|
||||||
|
# "buffer_time": buffer_time,
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# form = InterviewScheduleForm(slug=slug)
|
||||||
|
# break_formset = BreakTimeFormSet()
|
||||||
|
|
||||||
|
# return render(
|
||||||
|
# request,
|
||||||
|
# "interviews/schedule_interviews.html",
|
||||||
|
# {"form": form, "break_formset": break_formset, "job": job},
|
||||||
|
# )
|
||||||
|
|
||||||
|
def candidate_screening_view(request, slug):
|
||||||
"""
|
"""
|
||||||
Manage candidate tiers and stage transitions
|
Manage candidate tiers and stage transitions
|
||||||
"""
|
"""
|
||||||
@ -1265,7 +1518,7 @@ def candidate_tier_management_view(request, slug):
|
|||||||
# if "update_tiers" in request.POST:
|
# if "update_tiers" in request.POST:
|
||||||
# tier1_count = int(request.POST.get("tier1_count", 100))
|
# tier1_count = int(request.POST.get("tier1_count", 100))
|
||||||
# messages.success(request, f"Tier categorization updated. Tier 1: {tier1_count} candidates")
|
# messages.success(request, f"Tier categorization updated. Tier 1: {tier1_count} candidates")
|
||||||
# return redirect("candidate_tier_management", slug=slug)
|
# return redirect("candidate_screening_view", slug=slug)
|
||||||
|
|
||||||
# # Update individual candidate stages
|
# # Update individual candidate stages
|
||||||
# elif "update_stage" in request.POST:
|
# elif "update_stage" in request.POST:
|
||||||
@ -1354,9 +1607,16 @@ def candidate_tier_management_view(request, slug):
|
|||||||
# "total_candidates": candidates.count(),
|
# "total_candidates": candidates.count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "recruitment/candidate_tier_management.html", context)
|
return render(request, "recruitment/candidate_screening_view.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def get_candidates_from_request(request):
|
||||||
|
for c in request.POST.items():
|
||||||
|
try:
|
||||||
|
yield Candidate.objects.get(pk=c[0])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
yield None
|
||||||
def candidate_exam_view(request, slug):
|
def candidate_exam_view(request, slug):
|
||||||
"""
|
"""
|
||||||
Manage candidate tiers and stage transitions
|
Manage candidate tiers and stage transitions
|
||||||
@ -1378,15 +1638,16 @@ def update_candidate_exam_status(request, slug):
|
|||||||
return render(request, "includes/candidate_exam_status_form.html", {"candidate": candidate,"form": form})
|
return render(request, "includes/candidate_exam_status_form.html", {"candidate": candidate,"form": form})
|
||||||
def bulk_update_candidate_exam_status(request,slug):
|
def bulk_update_candidate_exam_status(request,slug):
|
||||||
job = get_object_or_404(JobPosting, slug=slug)
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
print(request.headers)
|
|
||||||
status = request.headers.get('status')
|
status = request.headers.get('status')
|
||||||
print(status)
|
|
||||||
if status:
|
if status:
|
||||||
for c in request.POST.items():
|
for candidate in get_candidates_from_request(request):
|
||||||
try:
|
try:
|
||||||
candidate = Candidate.objects.get(pk=c[0])
|
if status == "pass":
|
||||||
candidate.exam_status = "Passed" if status == "pass" else "Failed"
|
candidate.exam_status = "Passed"
|
||||||
candidate.stage = "Interview"
|
candidate.stage = "Interview"
|
||||||
|
else:
|
||||||
|
candidate.exam_status = "Failed"
|
||||||
candidate.save()
|
candidate.save()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@ -1403,20 +1664,97 @@ def candidate_set_exam_date(request, slug):
|
|||||||
candidate.exam_date = timezone.now()
|
candidate.exam_date = timezone.now()
|
||||||
candidate.save()
|
candidate.save()
|
||||||
messages.success(request, f"Set exam date for {candidate.name} to {candidate.exam_date}")
|
messages.success(request, f"Set exam date for {candidate.name} to {candidate.exam_date}")
|
||||||
return redirect("candidate_tier_management", slug=candidate.job.slug)
|
return redirect("candidate_screening_view", slug=candidate.job.slug)
|
||||||
|
|
||||||
def bulk_candidate_move_to_exam(request):
|
def bulk_candidate_move_to_exam(request):
|
||||||
for c in request.POST.items():
|
for candidate in get_candidates_from_request(request):
|
||||||
try:
|
candidate.stage = "Exam"
|
||||||
candidate = Candidate.objects.get(pk=c[0])
|
candidate.applicant_status = "Candidate"
|
||||||
candidate.stage = "Exam"
|
candidate.exam_date = timezone.now()
|
||||||
candidate.applicant_status = "Candidate"
|
candidate.save()
|
||||||
candidate.exam_date = timezone.now()
|
|
||||||
candidate.save()
|
messages.success(request, f"Candidates Moved to Exam stage")
|
||||||
except Exception as e:
|
return redirect("candidate_screening_view", slug=candidate.job.slug)
|
||||||
print(e)
|
|
||||||
messages.success(request, f"Moved {candidate.name} to Exam stage")
|
|
||||||
return redirect("candidate_tier_management", slug=candidate.job.slug)
|
|
||||||
# def response():
|
# def response():
|
||||||
# yield SSE.patch_elements("","")
|
# yield SSE.patch_elements("","")
|
||||||
# yield SSE.execute_script("console.log('hello world');")
|
# yield SSE.execute_script("console.log('hello world');")
|
||||||
# return DatastarResponse(response())
|
# return DatastarResponse(response())
|
||||||
|
|
||||||
|
def candidate_interview_view(request,slug):
|
||||||
|
job = get_object_or_404(JobPosting,slug=slug)
|
||||||
|
if "Datastar-Request" in request.headers:
|
||||||
|
for candidate in get_candidates_from_request(request):
|
||||||
|
print(candidate)
|
||||||
|
context = {"job":job,"candidates":job.candidates.all()}
|
||||||
|
return render(request,"recruitment/candidate_interview_view.html",context)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def interview_calendar_view(request, slug):
|
||||||
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
|
|
||||||
|
# Get all scheduled interviews for this job
|
||||||
|
scheduled_interviews = ScheduledInterview.objects.filter(
|
||||||
|
job=job
|
||||||
|
).select_related('candidate', 'zoom_meeting')
|
||||||
|
print(scheduled_interviews)
|
||||||
|
# Convert interviews to calendar events
|
||||||
|
events = []
|
||||||
|
for interview in scheduled_interviews:
|
||||||
|
# Create start datetime
|
||||||
|
start_datetime = datetime.combine(
|
||||||
|
interview.interview_date,
|
||||||
|
interview.interview_time
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate end datetime based on interview duration
|
||||||
|
duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60
|
||||||
|
end_datetime = start_datetime + timedelta(minutes=duration)
|
||||||
|
|
||||||
|
# Determine event color based on status
|
||||||
|
color = '#00636e' # Default color
|
||||||
|
if interview.status == 'confirmed':
|
||||||
|
color = '#00a86b' # Green for confirmed
|
||||||
|
elif interview.status == 'cancelled':
|
||||||
|
color = '#e74c3c' # Red for cancelled
|
||||||
|
elif interview.status == 'completed':
|
||||||
|
color = '#95a5a6' # Gray for completed
|
||||||
|
|
||||||
|
events.append({
|
||||||
|
'title': f"Interview: {interview.candidate.name}",
|
||||||
|
'start': start_datetime.isoformat(),
|
||||||
|
'end': end_datetime.isoformat(),
|
||||||
|
'url': f"{request.path}interview/{interview.id}/",
|
||||||
|
'color': color,
|
||||||
|
'extendedProps': {
|
||||||
|
'candidate': interview.candidate.name,
|
||||||
|
'email': interview.candidate.email,
|
||||||
|
'status': interview.status,
|
||||||
|
'meeting_id': interview.zoom_meeting.meeting_id if interview.zoom_meeting else None,
|
||||||
|
'join_url': interview.zoom_meeting.join_url if interview.zoom_meeting else None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'job': job,
|
||||||
|
'events': events,
|
||||||
|
'calendar_color': '#00636e',
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'recruitment/interview_calendar.html', context)
|
||||||
|
|
||||||
|
def interview_detail_view(request, slug, interview_id):
|
||||||
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
|
interview = get_object_or_404(
|
||||||
|
ScheduledInterview,
|
||||||
|
id=interview_id,
|
||||||
|
job=job
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'job': job,
|
||||||
|
'interview': interview,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'recruitment/interview_detail.html', context)
|
||||||
|
|||||||
@ -204,7 +204,7 @@ class CandidateDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
|||||||
# def job_detail(request, slug):
|
# def job_detail(request, slug):
|
||||||
# job = get_object_or_404(models.JobPosting, slug=slug, status='Published')
|
# job = get_object_or_404(models.JobPosting, slug=slug, status='Published')
|
||||||
# form = forms.CandidateForm()
|
# form = forms.CandidateForm()
|
||||||
|
|
||||||
# return render(request, 'jobs/job_detail.html', {'job': job, 'form': form})
|
# return render(request, 'jobs/job_detail.html', {'job': job, 'form': form})
|
||||||
|
|
||||||
|
|
||||||
@ -278,6 +278,8 @@ def candidate_update_stage(request, slug):
|
|||||||
'new_stage_display': candidate.get_stage_display(),
|
'new_stage_display': candidate.get_stage_display(),
|
||||||
'candidate': candidate
|
'candidate': candidate
|
||||||
}
|
}
|
||||||
|
messages.success(request,"Candidate Stage Updated")
|
||||||
|
return redirect("candidate_detail",slug=candidate.slug)
|
||||||
def response():
|
def response():
|
||||||
stage_form = forms.CandidateStageForm(candidate=candidate)
|
stage_form = forms.CandidateStageForm(candidate=candidate)
|
||||||
context['stage_form'] = stage_form
|
context['stage_form'] = stage_form
|
||||||
|
|||||||
3
templates/icons/link.html
Normal file
3
templates/icons/link.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 heroicon">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 340 B |
@ -80,7 +80,7 @@
|
|||||||
<button type="submit" name="confirm_schedule" class="btn btn-success">
|
<button type="submit" name="confirm_schedule" class="btn btn-success">
|
||||||
<i class="fas fa-check"></i> Confirm Schedule
|
<i class="fas fa-check"></i> Confirm Schedule
|
||||||
</button>
|
</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
|
<i class="fas fa-arrow-left"></i> Back to Edit
|
||||||
</a>
|
</a>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="submit" class="btn btn-primary">Preview Schedule</button>
|
<button type="submit" class="btn btn-primary">Preview Schedule</button>
|
||||||
<a href="{% url 'job_detail' pk=job.id %}" class="btn btn-secondary">Cancel</a>
|
<a href="{% url 'job_detail' slug=job.slug %}" class="btn btn-secondary">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
.text-info { color: var(--stage-exam) !important; }
|
.text-info { color: var(--stage-exam) !important; }
|
||||||
.text-success { color: var(--stage-offer) !important; }
|
.text-success { color: var(--stage-offer) !important; }
|
||||||
.text-secondary { color: var(--stage-inactive) !important; }
|
.text-secondary { color: var(--stage-inactive) !important; }
|
||||||
.bg-success { background-color: var(--kaauh-teal) !important; }
|
.bg-success { background-color: var(--kaauh-teal) !important; }
|
||||||
.bg-warning { background-color: #ffc107 !important; }
|
.bg-warning { background-color: #ffc107 !important; }
|
||||||
.bg-secondary { background-color: #6c757d !important; }
|
.bg-secondary { background-color: #6c757d !important; }
|
||||||
.bg-danger { background-color: #dc3545 !important; }
|
.bg-danger { background-color: #dc3545 !important; }
|
||||||
@ -117,7 +117,7 @@
|
|||||||
border-right-color: transparent;
|
border-right-color: transparent;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main Action Button Style */
|
/* Main Action Button Style */
|
||||||
.btn-main-action {
|
.btn-main-action {
|
||||||
background-color: var(--kaauh-teal);
|
background-color: var(--kaauh-teal);
|
||||||
@ -238,7 +238,7 @@
|
|||||||
|
|
||||||
/* Active State (Applies glow/scale to current stage) */
|
/* Active State (Applies glow/scale to current stage) */
|
||||||
.stage-item.active .stage-icon {
|
.stage-item.active .stage-icon {
|
||||||
box-shadow: 0 0 0 4px rgba(0, 99, 110, 0.4);
|
box-shadow: 0 0 0 4px rgba(0, 99, 110, 0.4);
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
.stage-item.active .stage-count {
|
.stage-item.active .stage-count {
|
||||||
@ -257,7 +257,7 @@
|
|||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
margin: 0 0.5rem;
|
margin: 0 0.5rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -18px;
|
top: -18px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
@ -283,7 +283,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
@ -296,7 +296,7 @@
|
|||||||
<li class="breadcrumb-item"><a href="{% url 'job_list' %}" class="text-secondary">Jobs</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'job_list' %}" class="text-secondary">Jobs</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page" class="text-secondary">Job Detail</li>
|
<li class="breadcrumb-item active" aria-current="page" class="text-secondary">Job Detail</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
|
|
||||||
{# LEFT COLUMN: JOB DETAILS WITH TABS #}
|
{# LEFT COLUMN: JOB DETAILS WITH TABS #}
|
||||||
@ -387,12 +387,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-secondary"
|
class="btn btn-outline-secondary"
|
||||||
id="copyJobLinkButton"
|
id="copyJobLinkButton"
|
||||||
data-url="{{ job.application_url }}">
|
data-url="{{ job.application_url }}">
|
||||||
<i class="fas fa-link me-1"></i>
|
<i class="fas fa-link me-1"></i>
|
||||||
{% trans "Copy and Share Public Link" %}
|
{% trans "Copy and Share Public Link" %}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -699,10 +699,12 @@
|
|||||||
<i class="fas fa-list-alt me-1"></i> {% trans "View All Existing Forms" %}
|
<i class="fas fa-list-alt me-1"></i> {% trans "View All Existing Forms" %}
|
||||||
</a> {% endcomment %}
|
</a> {% endcomment %}
|
||||||
|
|
||||||
{% comment %} <a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||||
<i class="fas fa-user-plus"></i> {% trans "Create Applicant" %}
|
<i class="fas fa-user-plus"></i> {% trans "Create Candidate" %}
|
||||||
</a> {% endcomment %}
|
</a>
|
||||||
|
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action">
|
||||||
|
<i class="fas fa-layer-group"></i> {% trans "Manage Tiers" %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -775,19 +777,19 @@
|
|||||||
document.getElementById('copyJobLinkButton').addEventListener('click', function() {
|
document.getElementById('copyJobLinkButton').addEventListener('click', function() {
|
||||||
// 1. Get the URL from the data attribute
|
// 1. Get the URL from the data attribute
|
||||||
const urlToCopy = this.getAttribute('data-url');
|
const urlToCopy = this.getAttribute('data-url');
|
||||||
|
|
||||||
// 2. Use the modern Clipboard API
|
// 2. Use the modern Clipboard API
|
||||||
navigator.clipboard.writeText(urlToCopy).then(() => {
|
navigator.clipboard.writeText(urlToCopy).then(() => {
|
||||||
|
|
||||||
// 3. Show feedback message
|
// 3. Show feedback message
|
||||||
const feedback = document.getElementById('copyFeedback');
|
const feedback = document.getElementById('copyFeedback');
|
||||||
feedback.style.display = 'inline';
|
feedback.style.display = 'inline';
|
||||||
|
|
||||||
// 4. Hide feedback after 2 seconds
|
// 4. Hide feedback after 2 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
feedback.style.display = 'none';
|
feedback.style.display = 'none';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
// Fallback for older browsers or security issues
|
// Fallback for older browsers or security issues
|
||||||
console.error('Could not copy text: ', err);
|
console.error('Could not copy text: ', err);
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
--kaauh-teal-dark: #004a53;
|
--kaauh-teal-dark: #004a53;
|
||||||
--kaauh-border: #eaeff3;
|
--kaauh-border: #eaeff3;
|
||||||
--kaauh-primary-text: #343a40;
|
--kaauh-primary-text: #343a40;
|
||||||
--kaauh-gray-light: #f8f9fa;
|
--kaauh-gray-light: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhanced Card Styling (Consistent) */
|
/* Enhanced Card Styling (Consistent) */
|
||||||
@ -114,7 +114,7 @@
|
|||||||
.table-view .table tbody tr:hover {
|
.table-view .table tbody tr:hover {
|
||||||
background-color: var(--kaauh-gray-light);
|
background-color: var(--kaauh-gray-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pagination Link Styling (Consistent) */
|
/* Pagination Link Styling (Consistent) */
|
||||||
.pagination .page-item .page-link {
|
.pagination .page-item .page-link {
|
||||||
color: var(--kaauh-teal-dark);
|
color: var(--kaauh-teal-dark);
|
||||||
@ -134,7 +134,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icon color for empty state */
|
/* Icon color for empty state */
|
||||||
.text-muted.fa-3x {
|
.text-muted.fa-3x {
|
||||||
color: var(--kaauh-teal-dark) !important;
|
color: var(--kaauh-teal-dark) !important;
|
||||||
@ -177,7 +177,7 @@
|
|||||||
<option value="ended" {% if status_filter == 'ended' %}selected{% endif %}>{% trans "Ended" %}</option>
|
<option value="ended" {% if status_filter == 'ended' %}selected{% endif %}>{% trans "Ended" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<div class="filter-buttons">
|
<div class="filter-buttons">
|
||||||
<button type="submit" class="btn btn-main-action btn-lg">
|
<button type="submit" class="btn btn-main-action btn-lg">
|
||||||
@ -221,7 +221,7 @@
|
|||||||
|
|
||||||
<div class="mt-auto pt-2 border-top">
|
<div class="mt-auto pt-2 border-top">
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<a href="{% url 'meeting_details' meeting.pk %}" class="btn btn-sm btn-outline-primary">
|
<a href="{% url 'meeting_details' meeting.slug %}" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="fas fa-eye"></i> {% trans "View" %}
|
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@ -231,12 +231,12 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a href="{% url 'update_meeting' meeting.pk %}" class="btn btn-sm btn-outline-secondary">
|
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||||
data-delete-url="{% url 'delete_meeting' meeting.pk %}"
|
data-delete-url="{% url 'delete_meeting' meeting.slug %}"
|
||||||
data-item-name="{{ meeting.topic }}">
|
data-item-name="{{ meeting.topic }}">
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -281,15 +281,15 @@
|
|||||||
<i class="fas fa-sign-in-alt"></i>
|
<i class="fas fa-sign-in-alt"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'meeting_details' meeting.pk %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
<a href="{% url 'meeting_details' meeting.slug %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'update_meeting' meeting.pk %}" class="btn btn-outline-secondary" title="{% trans 'Update' %}">
|
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-outline-secondary" title="{% trans 'Update' %}">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||||
data-delete-url="{% url 'delete_meeting' meeting.pk %}"
|
data-delete-url="{% url 'delete_meeting' meeting.slug %}"
|
||||||
data-item-name="{{ meeting.topic }}">
|
data-item-name="{{ meeting.topic }}">
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -3,61 +3,256 @@
|
|||||||
|
|
||||||
{% block title %}{% trans "Meeting Details" %} - {{ block.super }}{% endblock %}
|
{% block title %}{% trans "Meeting Details" %} - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block customCSS %}
|
||||||
|
<style>
|
||||||
|
/* UI Variables for the KAAT-S Theme (Consistent with list_meetings.html) */
|
||||||
|
:root {
|
||||||
|
--kaauh-teal: #00636e;
|
||||||
|
--kaauh-teal-dark: #004a53;
|
||||||
|
--kaauh-border: #eaeff3;
|
||||||
|
--kaauh-primary-text: #343a40;
|
||||||
|
--kaauh-gray-light: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Container and Card Styling */
|
||||||
|
.container {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
border: 1px solid var(--kaauh-border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
background-color: white;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.card:not(.no-hover):hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.card.no-hover:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Header Card Styling */
|
||||||
|
.card-header {
|
||||||
|
background-color: var(--kaauh-gray-light);
|
||||||
|
border-bottom: 1px solid var(--kaauh-border);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 0.75rem 0.75rem 0 0; /* Match top border radius of card */
|
||||||
|
}
|
||||||
|
.card-header h1 {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 0.5rem 0; /* Space below title */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.card-header .heroicon {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
color: var(--kaauh-teal);
|
||||||
|
}
|
||||||
|
.card-header .btn-secondary {
|
||||||
|
background-color: var(--kaauh-teal);
|
||||||
|
border-color: var(--kaauh-teal);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.card-header .btn-secondary:hover {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
border-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badge Styling (from list_meetings.html) */
|
||||||
|
.status-badge {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.4em 0.8em;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.7px;
|
||||||
|
}
|
||||||
|
.bg-waiting { background-color: #ffc107 !important; color: var(--kaauh-primary-text) !important;}
|
||||||
|
.bg-started { background-color: var(--kaauh-teal) !important; color: white !important;}
|
||||||
|
.bg-ended { background-color: #dc3545 !important; color: white !important;}
|
||||||
|
|
||||||
|
|
||||||
|
/* Detail Row Styling */
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--kaauh-border);
|
||||||
|
}
|
||||||
|
.detail-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.detail-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
min-width: 40%; /* Ensure labels align */
|
||||||
|
}
|
||||||
|
.detail-value {
|
||||||
|
text-align: right;
|
||||||
|
color: var(--kaauh-primary-text);
|
||||||
|
word-wrap: break-word; /* Long URLs or text wrap */
|
||||||
|
max-width: 60%; /* Ensure values align */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Title Styling within content cards */
|
||||||
|
.card h2 {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1.5rem 1.5rem 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
border-bottom: 1px solid var(--kaauh-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Join URL Styling */
|
||||||
|
.join-url-display {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid var(--kaauh-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
word-break: break-all; /* Force long URLs to break */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions Styling */
|
||||||
|
.actions {
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem; /* Space between buttons */
|
||||||
|
justify-content: flex-start; /* Align buttons to the left */
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--kaauh-teal);
|
||||||
|
border-color: var(--kaauh-teal);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
border-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
border-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
border-color: #bd2130;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
border-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
border-color: #545b62;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* API Response Styling */
|
||||||
|
#gateway-response {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
#gateway-response .card {
|
||||||
|
border-left: 4px solid var(--kaauh-teal); /* Indicate it's different content */
|
||||||
|
}
|
||||||
|
#gateway-response pre {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid var(--kaauh-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
overflow-x: auto; /* For long JSON lines */
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--kaauh-primary-text);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card">
|
<div class="container py-4">
|
||||||
<div class="card-header">
|
<div class="card no-hover">
|
||||||
<h1>
|
<div class="card-header">
|
||||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<h1>
|
||||||
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
</svg>
|
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
{{ meeting.topic }}
|
</svg>
|
||||||
</h1>
|
{{ meeting.topic }}
|
||||||
<span class="status-badge status-{{ meeting.status }}">
|
</h1>
|
||||||
{{ meeting.status|title }}
|
<span class="status-badge bg-{{ meeting.status }}">
|
||||||
</span>
|
{{ meeting.status|title }}
|
||||||
<a href="{% url 'list_meetings' %}" class="btn btn-secondary">{% trans "Back to Meetings" %}</a>
|
</span>
|
||||||
|
<a href="{% url 'list_meetings' %}" class="btn btn-secondary mt-3">{% trans "Back to Meetings" %}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card no-hover">
|
||||||
<h2>{% trans "Meeting Information" %}</h2>
|
<h2>{% trans "Meeting Information" %}</h2>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">{% trans "Meeting ID" %}:</div>
|
<div class="detail-label">{% trans "Meeting ID" %}:</div>
|
||||||
<div class="detail-value">{{ meeting.meeting_id }}</div>
|
<div class="detail-value">{{ meeting.meeting_id }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">{% trans "Topic" %}:</div>
|
||||||
|
<div class="detail-value">{{ meeting.topic }}</div>
|
||||||
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">{% trans "Start Time" %}:</div>
|
<div class="detail-label">{% trans "Start Time" %}:</div>
|
||||||
<div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div>
|
<div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">{% trans "Duration" %}:</div>
|
<div class="detail-label">{% trans "Duration" %}:</div>
|
||||||
<div class="detail-value">{{ meeting.duration }} minutes</div>
|
<div class="detail-value">{{ meeting.duration }} {% trans "minutes" %}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">{% trans "Timezone" %}:</div>
|
||||||
|
<div class="detail-value">{{ meeting.timezone|default:"UTC" }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">{% trans "Host Email" %}:</div>
|
<div class="detail-label">{% trans "Host Email" %}:</div>
|
||||||
<div class="detail-value">{{ meeting.host_email }}</div>
|
<div class="detail-value">{{ meeting.host_email|default:"N/A" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if meeting.join_url %}
|
{% if meeting.join_url %}
|
||||||
<div class="card">
|
<div class="card no-hover">
|
||||||
<h2>{% trans "Join Information" %}</h2>
|
<h2>{% trans "Join Information" %}</h2>
|
||||||
<a href="{{ meeting.join_url }}" class="btn btn-primary" target="_blank">{% trans "Join Meeting" %}</a>
|
<a href="{{ meeting.join_url }}" class="btn btn-primary" target="_blank">{% trans "Join Meeting" %}</a>
|
||||||
|
{% comment %} <div class="join-url-display">
|
||||||
|
<strong>{% trans "Join URL:" %}</strong> {{ meeting.join_url }}
|
||||||
|
</div> {% endcomment %}
|
||||||
{% if meeting.password %}
|
{% if meeting.password %}
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">{% trans "Password" %}:</div>
|
<div class="detail-label">{% trans "Password" %}: {{ meeting.password }}</div>
|
||||||
<div class="detail-value">{{ meeting.password }}</div>
|
<div class="detail-value"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card no-hover">
|
||||||
<h2>{% trans "Settings" %}</h2>
|
<h2>{% trans "Settings" %}</h2>
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">{% trans "Host Video" %}:</div>
|
|
||||||
<div class="detail-value">{{ meeting.host_video|yesno:"Yes,No" }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">{% trans "Participant Video" %}:</div>
|
<div class="detail-label">{% trans "Participant Video" %}:</div>
|
||||||
<div class="detail-value">{{ meeting.participant_video|yesno:"Yes,No" }}</div>
|
<div class="detail-value">{{ meeting.participant_video|yesno:"Yes,No" }}</div>
|
||||||
@ -76,27 +271,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="card no-hover">
|
||||||
{% if meeting.zoom_gateway_response %}
|
<div class="actions">
|
||||||
<a href="#" class="btn btn-secondary" onclick="toggleGateway()">{% trans "View API Response" %}</a>
|
{% if meeting.zoom_gateway_response %}
|
||||||
{% endif %}
|
<a href="#" class="btn btn-secondary" onclick="toggleGateway()">{% trans "View API Response" %}</a>
|
||||||
<a href="{% url 'update_meeting' meeting.pk %}" class="btn btn-primary">{% trans "Update Meeting" %}</a>
|
{% endif %}
|
||||||
<form method="post" action="{% url 'delete_meeting' meeting.pk %}" style="display: inline;">
|
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary">{% trans "Update Meeting" %}</a>
|
||||||
{% csrf_token %}
|
<form method="post" action="{% url 'delete_meeting' meeting.pk %}" style="display: inline;">
|
||||||
<button type="submit" class="btn btn-danger" onclick="return confirm('{% trans "Are you sure?" %}')">{% trans "Delete Meeting" %}</button>
|
{% csrf_token %}
|
||||||
</form>
|
<button type="submit" class="btn btn-danger" onclick="return confirm('{% trans "Are you sure?" %}')">{% trans "Delete Meeting" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if meeting.zoom_gateway_response %}
|
{% if meeting.zoom_gateway_response %}
|
||||||
<div id="gateway-response" style="display: none; margin-top: 2rem;">
|
{% comment %} <div id="gateway-response">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>{% trans "Zoom API Response" %}</h3>
|
<h3>{% trans "Zoom API Response" %}</h3>
|
||||||
<pre>{{ meeting.zoom_gateway_response|safe }}</pre>
|
<pre>{{ meeting.zoom_gateway_response|safe }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> {% endcomment %}
|
||||||
<script>
|
<script>
|
||||||
function toggleGateway() {
|
function toggleGateway() {
|
||||||
document.getElementById('gateway-response').style.display = document.getElementById('gateway-response').style.display === 'none' ? 'block' : 'none';
|
const element = document.getElementById('gateway-response');
|
||||||
|
if (element.style.display === 'none' || !element.style.display) {
|
||||||
|
element.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
element.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
color: var(--kaauh-gray);
|
color: var(--kaauh-gray);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CARD TITLE STYLING */
|
/* CARD TITLE STYLING */
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
@ -140,9 +140,9 @@
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Primary Action Button (Update) */
|
/* Primary Action Button (Update) */
|
||||||
.btn-main-action {
|
.btn-main-action {
|
||||||
background-color: var(--kaauh-teal);
|
background-color: var(--kaauh-teal);
|
||||||
border-color: var(--kaauh-teal);
|
border-color: var(--kaauh-teal);
|
||||||
color: white;
|
color: white;
|
||||||
@ -190,24 +190,20 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="card-title">{% trans "Meeting Information" %}</h2>
|
<h2 class="card-title">{% trans "Meeting Information" %}</h2>
|
||||||
|
|
||||||
<form method="post" action="{% url 'update_meeting' meeting.pk %}">
|
<form method="post" action="{% url 'update_meeting' meeting.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="topic" class="form-label">{% trans "Topic:" %}</label>
|
<label class="form-label" for="{{ form.topic.id_for_label }}">{% trans "Topic" %}</label>
|
||||||
<input type="text" id="topic" name="topic" class="form-input" value="{{ meeting.topic }}" required>
|
{{ form.topic }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="start_time" class="form-label">{% trans "Start Time (ISO 8601):" %}</label>
|
<label class="form-label" for="{{ form.start_time.id_for_label }}">{% trans "Start Time" %}</label>
|
||||||
{# Note: Django template filter for datetime-local needs to be precise Y-m-d\TH:i #}
|
{{ form.start_time }}
|
||||||
<input type="datetime-local" id="start_time" name="start_time" class="form-input"
|
|
||||||
value="{{ meeting.start_time|slice:'0:16' }}" required>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="duration" class="form-label">{% trans "Duration (minutes):" %}</label>
|
<label class="form-label" for="{{ form.duration.id_for_label }}">{% trans "Duration (minutes)" %}</label>
|
||||||
<input type="number" id="duration" name="duration" class="form-input" value="{{ meeting.duration }}" required>
|
{{ form.duration }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
|||||||
318
templates/recruitment/candidate_interview_view.html
Normal file
318
templates/recruitment/candidate_interview_view.html
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}Candidate Tier Management - {{ job.title }} - ATS{% endblock %}
|
||||||
|
|
||||||
|
{% block customCSS %}
|
||||||
|
<style>
|
||||||
|
/* Minimal Tier Management Styles */
|
||||||
|
.tier-controls {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.tier-controls .form-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.tier-controls .form-group {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.bulk-update-controls {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.stage-groups {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.stage-group {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.stage-group .stage-header {
|
||||||
|
background-color: #495057;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.stage-group .stage-body {
|
||||||
|
padding: 0.75rem;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
.stage-candidate {
|
||||||
|
padding: 0.375rem;
|
||||||
|
border-bottom: 1px solid #f1f3f4;
|
||||||
|
}
|
||||||
|
.stage-candidate:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.match-score {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0056b3;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Styles for Tiers */
|
||||||
|
.nav-tabs {
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
border: none;
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.nav-tabs .nav-link:hover {
|
||||||
|
border: none;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.nav-tabs .nav-link.active {
|
||||||
|
color: #495057;
|
||||||
|
background-color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.tier-1 .nav-link {
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.tier-1 .nav-link.active {
|
||||||
|
border-bottom-color: #28a745;
|
||||||
|
}
|
||||||
|
.tier-2 .nav-link {
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
.tier-2 .nav-link.active {
|
||||||
|
border-bottom-color: #ffc107;
|
||||||
|
}
|
||||||
|
.tier-3 .nav-link {
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.tier-3 .nav-link.active {
|
||||||
|
border-bottom-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Candidate Table Styles */
|
||||||
|
.candidate-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.candidate-table thead {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.candidate-table th {
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #495057;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.candidate-table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-bottom: 1px solid #f1f3f4;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.candidate-table tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.candidate-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.candidate-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.candidate-details {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.candidate-table-responsive {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.stage-badge {
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 0.375rem;
|
||||||
|
}
|
||||||
|
.stage-Applied {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
.stage-Exam {
|
||||||
|
background-color: #cce5ff;
|
||||||
|
color: #004085;
|
||||||
|
}
|
||||||
|
.stage-Interview {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
.stage-Offer {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.exam-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
margin-top: 0.375rem;
|
||||||
|
}
|
||||||
|
.exam-controls select,
|
||||||
|
.exam-controls input {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
}
|
||||||
|
.tier-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background-color: rgba(0,0,0,0.1);
|
||||||
|
color: #495057;
|
||||||
|
margin-left: 0.375rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-1">
|
||||||
|
<i class="fas fa-layer-group me-2"></i>
|
||||||
|
{% trans "Interview" %} - {{ job.title }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Job" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tier Display -->
|
||||||
|
<h2 class="h4 mb-3 mt-5">{% trans "Candidate Tiers" %}</h2>
|
||||||
|
<div class="candidate-table-responsive" data-signals__ifmissing="{_fetching: false, selections: Array({{ candidates|length }}).fill(false)}">
|
||||||
|
{% url "candidate_interview_view" job.slug as bulk_update_candidate_exam_status_url %}
|
||||||
|
{% if candidates %}
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
data-attr="{disabled: !$selections.filter(Boolean).length}"
|
||||||
|
data-on-click="@post('{{bulk_update_candidate_exam_status_url}}',{
|
||||||
|
contentType: 'form',
|
||||||
|
selector: '#myform',
|
||||||
|
headers: {'X-CSRFToken': '{{ csrf_token }}','status': 'pass'}
|
||||||
|
})"
|
||||||
|
>Mark as Pass and move to Interview</button>
|
||||||
|
<button class="btn btn-danger"
|
||||||
|
data-attr="{disabled: !$selections.filter(Boolean).length}"
|
||||||
|
data-on-click="@post('{{bulk_update_candidate_exam_status_url}}',{
|
||||||
|
contentType: 'form',
|
||||||
|
selector: '#myform',
|
||||||
|
headers: {'X-CSRFToken': '{{ csrf_token }}','status': 'fail'}
|
||||||
|
})"
|
||||||
|
>Mark as Failed</button>
|
||||||
|
{% endif %}
|
||||||
|
<form id="myform" action="{{move_to_exam_url}}" method="post">
|
||||||
|
<table class="candidate-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{% if candidates %}
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
data-bind-_all
|
||||||
|
data-on-change="$selections = Array({{ candidates|length }}).fill($_all)"
|
||||||
|
data-effect="$selections; $_all = $selections.every(Boolean)"
|
||||||
|
data-attr-disabled="$_fetching"
|
||||||
|
type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
<th>{% trans "Name" %}</th>
|
||||||
|
<th>{% trans "Contact" %}</th>
|
||||||
|
<th>{% trans "Interview Date" %}</th>
|
||||||
|
<th>{% trans "Interview Link" %}</th>
|
||||||
|
<th>{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for candidate in candidates %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
data-bind-selections
|
||||||
|
data-attr-disabled="$_fetching"
|
||||||
|
name="{{ candidate.id }}"
|
||||||
|
|
||||||
|
type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="candidate-name">{{ candidate.name }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="candidate-details">
|
||||||
|
Email: {{ candidate.email }}<br>
|
||||||
|
Phone: {{ candidate.phone }}<br>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{{candidate.get_latest_meeting.start_time|date:"m-d-Y h:i A"}}</td>
|
||||||
|
<td><a href="{{candidate.get_latest_meeting.join_url}}">{% include "icons/link.html" %}</a></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary btn-sm"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#candidateviewModal"
|
||||||
|
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||||
|
hx-target="#candidateviewModalBody"
|
||||||
|
>
|
||||||
|
{% include "icons/view.html" %}
|
||||||
|
{% trans "View" %}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!-- Tab Content -->
|
||||||
|
|
||||||
|
<div class="modal fade modal-lg" id="candidateviewModal" tabindex="-1" aria-labelledby="candidateviewModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="candidateviewModalLabel">Form Settings</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div id="candidateviewModalBody" class="modal-body">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
251
templates/recruitment/interview_calendar.html
Normal file
251
templates/recruitment/interview_calendar.html
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
<!-- templates/recruitment/interview_calendar.html -->
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block customCSS %}
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--calendar-color: #00636e;
|
||||||
|
--calendar-light: rgba(0, 99, 110, 0.1);
|
||||||
|
--calendar-hover: rgba(0, 99, 110, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
background-color: var(--calendar-color);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-container {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-toolbar-title {
|
||||||
|
color: var(--calendar-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-button-primary {
|
||||||
|
background-color: var(--calendar-color) !important;
|
||||||
|
border-color: var(--calendar-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-button-primary:hover {
|
||||||
|
background-color: #004d56 !important;
|
||||||
|
border-color: #004d56 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-button-primary:not(:disabled):active, .fc-button-primary:not(:disabled).fc-button-active {
|
||||||
|
background-color: #003a40 !important;
|
||||||
|
border-color: #003a40 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day.fc-day-today {
|
||||||
|
background-color: var(--calendar-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-event-title {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-scheduled {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #0d47a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-confirmed {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #1b5e20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #b71c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-completed {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interview-details {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interview-details .card {
|
||||||
|
border-left: 4px solid var(--calendar-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="calendar-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h1 class="h3 mb-0">Interview Calendar</h1>
|
||||||
|
<div>
|
||||||
|
<span class="h5">{{ job.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calendar-container">
|
||||||
|
<div id="calendar"></div>
|
||||||
|
|
||||||
|
<div class="calendar-legend">
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-color" style="background-color: #00636e;"></div>
|
||||||
|
<span>Scheduled</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-color" style="background-color: #00a86b;"></div>
|
||||||
|
<span>Confirmed</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-color" style="background-color: #e74c3c;"></div>
|
||||||
|
<span>Cancelled</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-color" style="background-color: #95a5a6;"></div>
|
||||||
|
<span>Completed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="interview-details" id="interview-details" style="display: none;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Interview Details</h5>
|
||||||
|
<button type="button" class="btn-close" id="close-details"></button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="interview-info">
|
||||||
|
<!-- Interview details will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Include FullCalendar JS -->
|
||||||
|
<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 events = {{ events|safe }};
|
||||||
|
|
||||||
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||||
|
},
|
||||||
|
events: events,
|
||||||
|
eventClick: function(info) {
|
||||||
|
// Prevent default browser behavior
|
||||||
|
info.jsEvent.preventDefault();
|
||||||
|
|
||||||
|
// Show interview details
|
||||||
|
showInterviewDetails(info.event);
|
||||||
|
},
|
||||||
|
eventMouseEnter: function(info) {
|
||||||
|
// Change cursor to pointer on hover
|
||||||
|
document.body.style.cursor = 'pointer';
|
||||||
|
},
|
||||||
|
eventMouseLeave: function() {
|
||||||
|
// Reset cursor
|
||||||
|
document.body.style.cursor = 'default';
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
aspectRatio: 2,
|
||||||
|
eventDisplay: 'block',
|
||||||
|
displayEventTime: true,
|
||||||
|
displayEventEnd: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
|
|
||||||
|
// Function to show interview details
|
||||||
|
function showInterviewDetails(event) {
|
||||||
|
const detailsContainer = document.getElementById('interview-details');
|
||||||
|
const infoContainer = document.getElementById('interview-info');
|
||||||
|
|
||||||
|
const statusClass = `status-${event.extendedProps.status}`;
|
||||||
|
const statusText = event.extendedProps.status.charAt(0).toUpperCase() + event.extendedProps.status.slice(1);
|
||||||
|
|
||||||
|
let meetingInfo = '';
|
||||||
|
if (event.extendedProps.meeting_id) {
|
||||||
|
meetingInfo = `
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6>Meeting Information</h6>
|
||||||
|
<p><strong>Meeting ID:</strong> ${event.extendedProps.meeting_id}</p>
|
||||||
|
<p><strong>Join URL:</strong> <a href="${event.extendedProps.join_url}" target="_blank">${event.extendedProps.join_url}</a></p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
infoContainer.innerHTML = `
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Candidate Information</h6>
|
||||||
|
<p><strong>Name:</strong> ${event.extendedProps.candidate}</p>
|
||||||
|
<p><strong>Email:</strong> ${event.extendedProps.email}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Interview Details</h6>
|
||||||
|
<p><strong>Date:</strong> ${event.start.toLocaleDateString()}</p>
|
||||||
|
<p><strong>Time:</strong> ${event.start.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} - ${event.end.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</p>
|
||||||
|
<p><strong>Status:</strong> <span class="status-badge ${statusClass}">${statusText}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${meetingInfo}
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="${event.url}" class="btn btn-primary btn-sm">View Full Details</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
detailsContainer.style.display = 'block';
|
||||||
|
|
||||||
|
// Scroll to details
|
||||||
|
detailsContainer.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close details button
|
||||||
|
document.getElementById('close-details').addEventListener('click', function() {
|
||||||
|
document.getElementById('interview-details').style.display = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
149
templates/recruitment/interview_detail.html
Normal file
149
templates/recruitment/interview_detail.html
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<!-- templates/recruitment/interview_detail.html -->
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--calendar-color: #00636e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
background-color: var(--calendar-color);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
border-left: 4px solid var(--calendar-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-scheduled {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #0d47a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-confirmed {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #1b5e20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #b71c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-completed {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #424242;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="detail-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h1 class="h3 mb-0">Interview Details</h1>
|
||||||
|
<div>
|
||||||
|
<span class="h5">{{ job.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card detail-card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>Candidate Information</h5>
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td><strong>Name:</strong></td>
|
||||||
|
<td>{{ interview.candidate.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Email:</strong></td>
|
||||||
|
<td>{{ interview.candidate.email }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Phone:</strong></td>
|
||||||
|
<td>{{ interview.candidate.phone|default:"Not provided" }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>Interview Details</h5>
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td><strong>Date:</strong></td>
|
||||||
|
<td>{{ interview.interview_date|date:"l, F j, Y" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Time:</strong></td>
|
||||||
|
<td>{{ interview.interview_time|time:"g:i A" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Status:</strong></td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-{{ interview.status }}">
|
||||||
|
{{ interview.status|title }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if interview.zoom_meeting %}
|
||||||
|
<div class="mt-4">
|
||||||
|
<h5>Meeting Information</h5>
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td><strong>Meeting ID:</strong></td>
|
||||||
|
<td>{{ interview.zoom_meeting.meeting_id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Topic:</strong></td>
|
||||||
|
<td>{{ interview.zoom_meeting.topic }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Duration:</strong></td>
|
||||||
|
<td>{{ interview.zoom_meeting.duration }} minutes</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Join URL:</strong></td>
|
||||||
|
<td><a href="{{ interview.zoom_meeting.join_url }}" target="_blank">{{ interview.zoom_meeting.join_url }}</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'interview_calendar' slug=job.slug %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-calendar"></i> Back to Calendar
|
||||||
|
</a>
|
||||||
|
{% if interview.status == 'scheduled' %}
|
||||||
|
<button class="btn btn-success">
|
||||||
|
<i class="fas fa-check"></i> Confirm Interview
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if interview.status != 'cancelled' and interview.status != 'completed' %}
|
||||||
|
<button class="btn btn-danger">
|
||||||
|
<i class="fas fa-times"></i> Cancel Interview
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Loading…
x
Reference in New Issue
Block a user