meetings list
This commit is contained in:
parent
517ee1d54d
commit
4148c7eb16
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -584,11 +584,12 @@ class InterviewScheduleForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = InterviewSchedule
|
||||
fields = [
|
||||
'candidates', 'start_date', 'end_date', 'working_days',
|
||||
'candidates', 'interview_type', 'start_date', 'end_date', 'working_days',
|
||||
'start_time', 'end_time', 'interview_duration', 'buffer_time',
|
||||
'break_start_time', 'break_end_time'
|
||||
]
|
||||
widgets = {
|
||||
'interview_type': forms.Select(attrs={'class': 'form-control'}),
|
||||
'start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
|
||||
@ -1326,7 +1327,8 @@ class CandidateEmailForm(forms.Form):
|
||||
"""Generate initial message with candidate and meeting information"""
|
||||
candidate=self.candidates.first()
|
||||
message_parts=[]
|
||||
if candidate.stage == 'Applied':
|
||||
|
||||
if candidate and candidate.stage == 'Applied':
|
||||
message_parts = [
|
||||
f"Than you, for your interest in the {self.job.title} role.",
|
||||
f"We regret to inform you that you were not selected to move forward to the exam round at this time.",
|
||||
@ -1335,7 +1337,7 @@ class CandidateEmailForm(forms.Form):
|
||||
f"Wishing you the best in your job search,",
|
||||
f"The KAAUH Hiring team"
|
||||
]
|
||||
elif candidate.stage == 'Exam':
|
||||
elif candidate and candidate.stage == 'Exam':
|
||||
message_parts = [
|
||||
f"Than you,for your interest in the {self.job.title} role.",
|
||||
f"We're pleased to inform you that your initial screening was successful!",
|
||||
@ -1346,7 +1348,7 @@ class CandidateEmailForm(forms.Form):
|
||||
f"Best regards, The KAAUH Hiring team"
|
||||
]
|
||||
|
||||
elif candidate.stage == 'Interview':
|
||||
elif candidate and candidate.stage == 'Interview':
|
||||
message_parts = [
|
||||
f"Than you, for your interest in the {self.job.title} role.",
|
||||
f"We're pleased to inform you that your initial screening was successful!",
|
||||
@ -1357,7 +1359,7 @@ class CandidateEmailForm(forms.Form):
|
||||
f"Best regards, The KAAUH Hiring team"
|
||||
]
|
||||
|
||||
elif candidate.stage == 'Offer':
|
||||
elif candidate and candidate.stage == 'Offer':
|
||||
message_parts = [
|
||||
f"Congratulations, ! We are delighted to inform you that we are extending a formal offer of employment for the {self.job.title} role.",
|
||||
f"This is an exciting moment, and we look forward to having you join the KAAUH team.",
|
||||
@ -1366,7 +1368,7 @@ class CandidateEmailForm(forms.Form):
|
||||
f"Welcome to the team!",
|
||||
f"Best regards, The KAAUH Hiring team"
|
||||
]
|
||||
elif candidate.stage == 'Hired':
|
||||
elif candidate and candidate.stage == 'Hired':
|
||||
message_parts = [
|
||||
f"Welcome aboard,!",
|
||||
f"We are thrilled to officially confirm your employment as our new {self.job.title}.",
|
||||
@ -1589,6 +1591,17 @@ KAAUH HIRING TEAM
|
||||
self.initial['message_for_agency'] = agency_message.strip()
|
||||
self.initial['message_for_participants'] = participants_message.strip()
|
||||
|
||||
|
||||
|
||||
|
||||
class InterviewScheduleLocationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model=InterviewSchedule
|
||||
fields=['location']
|
||||
widgets={
|
||||
'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-09 11:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0004_remove_jobposting_participants_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='scheduledinterview',
|
||||
name='meeting_type',
|
||||
field=models.CharField(choices=[('Remote', 'Remote Interview'), ('Onsite', 'In-Person Interview')], default='Remote', max_length=10, verbose_name='Interview Meeting Type'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-09 11:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0005_scheduledinterview_meeting_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='scheduledinterview',
|
||||
name='meeting_type',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interviewschedule',
|
||||
name='meeting_type',
|
||||
field=models.CharField(choices=[('Remote', 'Remote Interview'), ('Onsite', 'In-Person Interview')], default='Remote', max_length=10, verbose_name='Interview Meeting Type'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-09 11:30
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0006_remove_scheduledinterview_meeting_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='interviewschedule',
|
||||
old_name='meeting_type',
|
||||
new_name='interview_type',
|
||||
),
|
||||
]
|
||||
18
recruitment/migrations/0008_interviewschedule_location.py
Normal file
18
recruitment/migrations/0008_interviewschedule_location.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-09 12:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0007_rename_meeting_type_interviewschedule_interview_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interviewschedule',
|
||||
name='location',
|
||||
field=models.CharField(blank=True, default='Remote', null=True),
|
||||
),
|
||||
]
|
||||
18
recruitment/migrations/0009_alter_zoommeeting_meeting_id.py
Normal file
18
recruitment/migrations/0009_alter_zoommeeting_meeting_id.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-09 13:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0008_interviewschedule_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='zoommeeting',
|
||||
name='meeting_id',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=20, null=True, unique=True, verbose_name='Meeting ID'),
|
||||
),
|
||||
]
|
||||
19
recruitment/migrations/0010_alter_zoommeeting_meeting_id.py
Normal file
19
recruitment/migrations/0010_alter_zoommeeting_meeting_id.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-09 13:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0009_alter_zoommeeting_meeting_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='zoommeeting',
|
||||
name='meeting_id',
|
||||
field=models.CharField(db_index=True, default=1, max_length=20, unique=True, verbose_name='Meeting ID'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-09 13:50
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0010_alter_zoommeeting_meeting_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='scheduledinterview',
|
||||
name='zoom_meeting',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting'),
|
||||
),
|
||||
]
|
||||
@ -742,6 +742,7 @@ class ZoomMeeting(Base):
|
||||
topic = models.CharField(max_length=255, verbose_name=_("Topic"))
|
||||
meeting_id = models.CharField(
|
||||
db_index=True, max_length=20, unique=True, verbose_name=_("Meeting ID") # Added index
|
||||
|
||||
) # Unique identifier for the meeting
|
||||
start_time = models.DateTimeField(db_index=True, verbose_name=_("Start Time")) # Added index
|
||||
duration = models.PositiveIntegerField(
|
||||
@ -1599,6 +1600,20 @@ class BreakTime(models.Model):
|
||||
|
||||
class InterviewSchedule(Base):
|
||||
"""Stores the scheduling criteria for interviews"""
|
||||
"""Stores individual scheduled interviews"""
|
||||
|
||||
class InterviewType(models.TextChoices):
|
||||
REMOTE = 'Remote', 'Remote Interview'
|
||||
ONSITE = 'Onsite', 'In-Person Interview'
|
||||
|
||||
interview_type = models.CharField(
|
||||
max_length=10,
|
||||
choices=InterviewType.choices,
|
||||
default=InterviewType.REMOTE,
|
||||
verbose_name="Interview Meeting Type"
|
||||
)
|
||||
|
||||
location=models.CharField(null=True,blank=True,default='Remote')
|
||||
|
||||
job = models.ForeignKey(
|
||||
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules", db_index=True
|
||||
@ -1636,14 +1651,16 @@ class InterviewSchedule(Base):
|
||||
|
||||
|
||||
class ScheduledInterview(Base):
|
||||
"""Stores individual scheduled interviews"""
|
||||
|
||||
|
||||
#for one candidate
|
||||
candidate = models.ForeignKey(
|
||||
Candidate,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="scheduled_interviews",
|
||||
db_index=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
participants = models.ManyToManyField('Participants', blank=True)
|
||||
system_users=models.ManyToManyField(User,blank=True)
|
||||
@ -1653,7 +1670,8 @@ class ScheduledInterview(Base):
|
||||
"JobPosting", on_delete=models.CASCADE, related_name="scheduled_interviews", db_index=True
|
||||
)
|
||||
zoom_meeting = models.OneToOneField(
|
||||
ZoomMeeting, on_delete=models.CASCADE, related_name="interview", db_index=True
|
||||
ZoomMeeting, on_delete=models.CASCADE, related_name="interview", db_index=True,
|
||||
null=True, blank=True
|
||||
)
|
||||
schedule = models.ForeignKey(
|
||||
InterviewSchedule, on_delete=models.CASCADE, related_name="interviews",null=True,blank=True, db_index=True
|
||||
|
||||
@ -461,6 +461,7 @@ def create_interview_and_meeting(
|
||||
meeting_topic = f"Interview for {job.title} - {candidate.name}"
|
||||
|
||||
# 1. External API Call (Slow)
|
||||
|
||||
result = create_zoom_meeting(meeting_topic, interview_datetime, duration)
|
||||
|
||||
if result["status"] == "success":
|
||||
|
||||
@ -234,5 +234,8 @@ urlpatterns = [
|
||||
path('jobs/<slug:job_slug>/candidates/compose_email/', views.compose_candidate_email, name='compose_candidate_email'),
|
||||
path('interview/partcipants/<slug:slug>/',views.create_interview_participants,name='create_interview_participants'),
|
||||
path('interview/email/<slug:slug>/',views.send_interview_email,name='send_interview_email'),
|
||||
path('interview/schedule/location/<slug:slug>/',views.schedule_interview_location_form,name='schedule_interview_location_form'),
|
||||
path('interview/list',views.InterviewListView,name='interview_list')
|
||||
|
||||
|
||||
]
|
||||
|
||||
@ -43,7 +43,8 @@ from .forms import (
|
||||
LinkedPostContentForm,
|
||||
CandidateEmailForm,
|
||||
SourceForm,
|
||||
InterviewEmailForm
|
||||
InterviewEmailForm,
|
||||
InterviewScheduleLocationForm
|
||||
)
|
||||
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
|
||||
from rest_framework import viewsets
|
||||
@ -192,6 +193,43 @@ class ZoomMeetingListView(LoginRequiredMixin, ListView):
|
||||
context["status_filter"] = self.request.GET.get("status", "")
|
||||
context["candidate_name_filter"] = self.request.GET.get("candidate_name", "")
|
||||
return context
|
||||
|
||||
@login_required
|
||||
def InterviewListView(request):
|
||||
interview_type=request.GET.get('interview_type','Remote')
|
||||
print(interview_type)
|
||||
if interview_type=='Onsite':
|
||||
meetings=ScheduledInterview.objects.filter(schedule__interview_type=interview_type)
|
||||
else:
|
||||
meetings=ZoomMeeting.objects.all()
|
||||
print(meetings)
|
||||
return render(request, "meetings/list_meetings.html",{
|
||||
'meetings':meetings,
|
||||
'current_interview_type':interview_type
|
||||
})
|
||||
|
||||
# search_query = request.GET.get("q", "") # Renamed from 'search' to 'q' for consistency
|
||||
# if search_query:
|
||||
# interviews = interviews.filter(
|
||||
# Q(topic__icontains=search_query) | Q(meeting_id__icontains=search_query)
|
||||
# )
|
||||
|
||||
# # Handle filter by status
|
||||
# status_filter = request.GET.get("status", "")
|
||||
# if status_filter:
|
||||
# queryset = queryset.filter(status=status_filter)
|
||||
|
||||
# # Handle search by candidate name
|
||||
# candidate_name = request.GET.get("candidate_name", "")
|
||||
# if candidate_name:
|
||||
# # Filter based on the name of the candidate associated with the meeting's interview
|
||||
# queryset = queryset.filter(
|
||||
# Q(interview__candidate__first_name__icontains=candidate_name) |
|
||||
# Q(interview__candidate__last_name__icontains=candidate_name)
|
||||
# )
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ZoomMeetingDetailsView(LoginRequiredMixin, DetailView):
|
||||
@ -1126,6 +1164,7 @@ def _handle_get_request(request, slug, job):
|
||||
|
||||
|
||||
def _handle_preview_submission(request, slug, job):
|
||||
|
||||
"""
|
||||
Handles the initial POST request (Preview Schedule).
|
||||
Validates forms, calculates slots, saves data to session, and renders preview.
|
||||
@ -1137,6 +1176,7 @@ def _handle_preview_submission(request, slug, job):
|
||||
if form.is_valid():
|
||||
# Get the form data
|
||||
candidates = form.cleaned_data["candidates"]
|
||||
interview_type=form.cleaned_data["interview_type"]
|
||||
start_date = form.cleaned_data["start_date"]
|
||||
end_date = form.cleaned_data["end_date"]
|
||||
working_days = form.cleaned_data["working_days"]
|
||||
@ -1197,6 +1237,7 @@ def _handle_preview_submission(request, slug, job):
|
||||
|
||||
# Save the form data to session for later use
|
||||
schedule_data = {
|
||||
"interview_type":interview_type,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
"working_days": working_days,
|
||||
@ -1217,6 +1258,7 @@ def _handle_preview_submission(request, slug, job):
|
||||
{
|
||||
"job": job,
|
||||
"schedule": preview_schedule,
|
||||
"interview_type":interview_type,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"working_days": working_days,
|
||||
@ -1260,6 +1302,7 @@ def _handle_confirm_schedule(request, slug, job):
|
||||
schedule = InterviewSchedule.objects.create(
|
||||
job=job,
|
||||
created_by=request.user,
|
||||
interview_type=schedule_data["interview_type"],
|
||||
start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
|
||||
end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
|
||||
working_days=schedule_data["working_days"],
|
||||
@ -1286,34 +1329,59 @@ def _handle_confirm_schedule(request, slug, job):
|
||||
available_slots = get_available_time_slots(schedule) # This should still be synchronous and fast
|
||||
|
||||
# 4. Queue scheduled interviews asynchronously (FAST RESPONSE)
|
||||
queued_count = 0
|
||||
for i, candidate in enumerate(candidates):
|
||||
if i < len(available_slots):
|
||||
slot = available_slots[i]
|
||||
if schedule.interview_type=='Remote':
|
||||
queued_count = 0
|
||||
for i, candidate in enumerate(candidates):
|
||||
if i < len(available_slots):
|
||||
slot = available_slots[i]
|
||||
|
||||
# Dispatch the individual creation task to the background queue
|
||||
async_task(
|
||||
"recruitment.tasks.create_interview_and_meeting",
|
||||
candidate.pk,
|
||||
job.pk,
|
||||
schedule.pk,
|
||||
slot['date'],
|
||||
slot['time'],
|
||||
schedule.interview_duration,
|
||||
)
|
||||
queued_count += 1
|
||||
# Dispatch the individual creation task to the background queue
|
||||
|
||||
async_task(
|
||||
"recruitment.tasks.create_interview_and_meeting",
|
||||
candidate.pk,
|
||||
job.pk,
|
||||
schedule.pk,
|
||||
slot['date'],
|
||||
slot['time'],
|
||||
schedule.interview_duration,
|
||||
)
|
||||
queued_count += 1
|
||||
|
||||
# 5. Success and Cleanup (IMMEDIATE RESPONSE)
|
||||
messages.success(
|
||||
request,
|
||||
f"Schedule successfully created. Queued {queued_count} interviews to be booked asynchronously. Check back in a moment!"
|
||||
)
|
||||
# 5. Success and Cleanup (IMMEDIATE RESPONSE)
|
||||
messages.success(
|
||||
request,
|
||||
f"Schedule successfully created. Queued {queued_count} interviews to be booked asynchronously. Check back in a moment!"
|
||||
)
|
||||
|
||||
# Clear both session data keys upon successful completion
|
||||
if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||
# Clear both session data keys upon successful completion
|
||||
if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||
|
||||
return redirect("job_detail", slug=slug)
|
||||
else:
|
||||
for i, candidate in enumerate(candidates):
|
||||
if i < len(available_slots):
|
||||
slot = available_slots[i]
|
||||
ScheduledInterview.objects.create(
|
||||
candidate=candidate,
|
||||
job=job,
|
||||
# zoom_meeting=None,
|
||||
schedule=schedule,
|
||||
interview_date=slot['date'],
|
||||
interview_time= slot['time']
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Onsite schedule Interview Create succesfully"
|
||||
)
|
||||
|
||||
# Clear both session data keys upon successful completion
|
||||
if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
|
||||
if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
|
||||
return redirect('schedule_interview_location_form',slug=schedule.slug)
|
||||
|
||||
return redirect("job_detail", slug=slug)
|
||||
|
||||
def schedule_interviews_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
@ -1322,6 +1390,7 @@ def schedule_interviews_view(request, slug):
|
||||
return _handle_preview_submission(request, slug, job)
|
||||
else:
|
||||
return _handle_get_request(request, slug, job)
|
||||
|
||||
def confirm_schedule_interviews_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
if request.method == "POST":
|
||||
@ -4062,4 +4131,18 @@ def send_interview_email(request, slug):
|
||||
|
||||
|
||||
|
||||
def schedule_interview_location_form(request,slug):
|
||||
schedule=get_object_or_404(InterviewSchedule,slug=slug)
|
||||
if request.method=='POST':
|
||||
form=InterviewScheduleLocationForm(request.POST,instance=schedule)
|
||||
form.save()
|
||||
return redirect('list_meetings')
|
||||
else:
|
||||
form=InterviewScheduleLocationForm(instance=schedule)
|
||||
return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule})
|
||||
|
||||
|
||||
|
||||
def onsite_interview_list_view(request):
|
||||
onsite_interviews=ScheduledInterview.objects.filter(schedule__interview_type='Onsite')
|
||||
return render(request,'interviews/onsite_interview_list.html',{'onsite_interviews':onsite_interviews})
|
||||
|
||||
@ -255,6 +255,16 @@
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item me-lg-4">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'interview_list' %}active{% endif %}" href="{% url 'interview_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||||
</svg>
|
||||
{% trans "Onsite Interviews" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item me-lg-4">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'participants_list' %}active{% endif %}" href="{% url 'participants_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
|
||||
432
templates/interviews/onsite_interview_list.html
Normal file
432
templates/interviews/onsite_interview_list.html
Normal file
@ -0,0 +1,432 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "Zoom Meetings" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* UI Variables for the KAAT-S Theme (Consistent with Reference) */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
--kaauh-gray-light: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Enhanced Card Styling (Consistent) */
|
||||
.card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
background-color: white;
|
||||
}
|
||||
.card:not(.no-hover):hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
.card.no-hover:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* Main Action Button Style (Teal Theme) */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Secondary Button Style (For Edit/Outline - Consistent) */
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
/* Primary Outline for View/Join */
|
||||
.btn-outline-primary {
|
||||
color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
.btn-outline-primary:hover {
|
||||
background-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Meeting Card Specifics (Adapted to Standard Card View) */
|
||||
.meeting-card .card-title {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 600;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
.meeting-card .card-text i {
|
||||
color: var(--kaauh-teal);
|
||||
width: 1.25rem;
|
||||
}
|
||||
/* Primary Color Overrides */
|
||||
.text-primary-theme { color: var(--kaauh-teal) !important; }
|
||||
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
|
||||
.text-success { color: var(--kaauh-success) !important; }
|
||||
.text-danger { color: var(--kaauh-danger) !important; }
|
||||
.text-info { color: #17a2b8 !important; }
|
||||
|
||||
/* Status Badges (Standardized) */
|
||||
.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;
|
||||
}
|
||||
/* Status Badge Mapping */
|
||||
.bg-waiting { background-color: #ffc107 !important; color: var(--kaauh-primary-text) !important;}
|
||||
.bg-scheduled { 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;}
|
||||
|
||||
/* Table Styling (Consistent with Reference) */
|
||||
.table-view .table thead th {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border-color: var(--kaauh-border);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 1rem;
|
||||
}
|
||||
.table-view .table tbody td {
|
||||
vertical-align: middle;
|
||||
padding: 1rem;
|
||||
border-color: var(--kaauh-border);
|
||||
}
|
||||
.table-view .table tbody tr:hover {
|
||||
background-color: var(--kaauh-gray-light);
|
||||
}
|
||||
|
||||
/* Pagination Link Styling (Consistent) */
|
||||
.pagination .page-item .page-link {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-border);
|
||||
}
|
||||
.pagination .page-item.active .page-link {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
}
|
||||
.pagination .page-item:hover .page-link:not(.active) {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
/* Filter & Search Layout Adjustments */
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Icon color for empty state */
|
||||
.text-muted.fa-3x {
|
||||
color: var(--kaauh-teal-dark) !important;
|
||||
}
|
||||
@keyframes svg-pulse {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply the animation to the custom class */
|
||||
.svg-pulse {
|
||||
animation: svg-pulse 2s infinite ease-in-out;
|
||||
transform-origin: center; /* Ensure scaling is centered */
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-video me-2"></i> {% trans "Zoom Meetings" %}
|
||||
</h1>
|
||||
<a href="{% url 'create_meeting' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Create Meeting" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4 shadow-sm no-hover">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="search" class="form-label small text-muted">{% trans "Search by Topic" %}</label>
|
||||
<div class="input-group input-group-lg mb-3">
|
||||
<form method="get" action="" class="w-100">
|
||||
{% include "includes/search_form.html" with search_query=search_query %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<form method="GET" class="row g-3 align-items-end" >
|
||||
{% if search_query %}<input class="form-control form-control-sm" type="hidden" name="q" value="{{ search_query }}">{% endif %}
|
||||
{% if status_filter %}<input class="form-control form-control-sm" type="hidden" name="status" value="{{ status_filter }}">{% endif %}
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="status" class="form-label small text-muted">{% trans "Filter by Status" %}</label>
|
||||
<select name="status" id="status" class="form-select form-select-sm">
|
||||
<option value="">{% trans "All Statuses" %}</option>
|
||||
<option value="waiting" {% if status_filter == 'waiting' %}selected{% endif %}>{% trans "Waiting" %}</option>
|
||||
<option value="started" {% if status_filter == 'started' %}selected{% endif %}>{% trans "Started" %}</option>
|
||||
<option value="ended" {% if status_filter == 'ended' %}selected{% endif %}>{% trans "Ended" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="candidate_name" class="form-label small text-muted">{% trans "Candidate Name" %}</label>
|
||||
<input type="text" class="form-control form-control-sm" id="candidate_name" name="candidate_name" placeholder="{% trans 'Search by candidate...' %}" value="{{ candidate_name_filter }}">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="filter-buttons">
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-filter me-1"></i> {% trans "Apply Filters" %}
|
||||
</button>
|
||||
{% if status_filter or search_query or candidate_name_filter %}
|
||||
<a href="{% url 'list_meetings' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Clear" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if meetings %}
|
||||
<div id="meetings-list">
|
||||
{# View Switcher #}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="meetings-list" %}
|
||||
|
||||
{# Card View #}
|
||||
<div class="card-view active row">
|
||||
{% for meeting in meetings %}
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card meeting-card h-100 shadow-sm">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="card-title flex-grow-1 me-3"><a href="{% url 'meeting_details' meeting.slug %}" class="text-decoration-none text-primary-theme">{{ meeting.topic }}</a></h5>
|
||||
<span class="status-badge bg-{{ meeting.status }}">
|
||||
{{ meeting.status|title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="card-text text-muted small mb-3">
|
||||
<i class="fas fa-user"></i> {% trans "Candidate" %}: {% if meeting.interview %}{{ meeting.interview.candidate.name }}{% else %}
|
||||
<button data-bs-toggle="modal"
|
||||
data-bs-target="#meetingModal"
|
||||
hx-get="{% url 'set_meeting_candidate' meeting.slug %}"
|
||||
hx-target="#meetingModalBody"
|
||||
hx-swap="outerHTML"
|
||||
class="btn text-primary-theme btn-link btn-sm">Set Candidate</button>
|
||||
{% endif %}<br>
|
||||
<i class="fas fa-briefcase"></i> {% trans "Job" %}: {% if meeting.interview %}{{ meeting.interview.job.title }}{% else %}
|
||||
<button data-bs-toggle="modal"
|
||||
data-bs-target="#meetingModal"
|
||||
hx-get="{% url 'set_meeting_candidate' meeting.slug %}"
|
||||
hx-target="#meetingModalBody"
|
||||
hx-swap="outerHTML"
|
||||
class="btn text-primary-theme btn-link btn-sm">Set Job</button>
|
||||
{% endif %}<br>
|
||||
<i class="fas fa-hashtag"></i> {% trans "ID" %}: {{ meeting.meeting_id|default:meeting.id }}<br>
|
||||
<i class="fas fa-clock"></i> {% trans "Start" %}: {{ meeting.start_time|date:"M d, Y H:i" }}<br>
|
||||
<i class="fas fa-stopwatch"></i> {% trans "Duration" %}: {{ meeting.duration }} minutes{% if meeting.password %}<br><i class="fas fa-lock"></i> {% trans "Password" %}: Yes{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'meeting_details' meeting.slug %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||
</a>
|
||||
|
||||
{% if meeting.join_url %}
|
||||
<a href="{{ meeting.join_url }}" target="_blank" class="btn btn-sm btn-main-action">
|
||||
<i class="fas fa-link"></i> {% trans "Join" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
hx-post="{% url 'delete_meeting' meeting.slug %}"
|
||||
hx-target="#deleteModalBody"
|
||||
hx-swap="outerHTML"
|
||||
data-item-name="{{ meeting.topic }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Table View #}
|
||||
<div class="table-view">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Topic" %}</th>
|
||||
<th scope="col">{% trans "Candidate" %}</th>
|
||||
<th scope="col">{% trans "Job" %}</th>
|
||||
<th scope="col">{% trans "ID" %}</th>
|
||||
<th scope="col">{% trans "Start Time" %}</th>
|
||||
<th scope="col">{% trans "Duration" %}</th>
|
||||
<th scope="col">{% trans "Status" %}</th>
|
||||
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for meeting in meetings %}
|
||||
<tr>
|
||||
<td><strong class="text-primary"><a href="{% url 'meeting_details' meeting.slug %}" class="text-decoration-none text-secondary">{{ meeting.topic }}<a></strong></td>
|
||||
<td>
|
||||
{% if meeting.interview %}
|
||||
<a class="text-primary text-decoration-none" href="{% url 'candidate_detail' meeting.interview.candidate.slug %}">{{ meeting.interview.candidate.name }} <i class="fas fa-link"></i></a>
|
||||
{% else %}
|
||||
<button data-bs-toggle="modal"
|
||||
data-bs-target="#meetingModal"
|
||||
hx-get="{% url 'set_meeting_candidate' meeting.slug %}"
|
||||
hx-target="#meetingModalBody"
|
||||
hx-swap="outerHTML"
|
||||
class="btn btn-outline-primary btn-sm">Set Candidate</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if meeting.interview %}
|
||||
<a class="text-primary text-decoration-none" href="{% url 'job_detail' meeting.interview.job.slug %}">{{ meeting.interview.job.title }} <i class="fas fa-link"></i></a>
|
||||
{% else %}
|
||||
<button data-bs-toggle="modal"
|
||||
data-bs-target="#meetingModal"
|
||||
hx-get="{% url 'set_meeting_candidate' meeting.slug %}"
|
||||
hx-target="#meetingModalBody"
|
||||
hx-swap="outerHTML"
|
||||
class="btn btn-outline-primary btn-sm">Set Job</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ meeting.meeting_id|default:meeting.id }}</td>
|
||||
<td>{{ meeting.start_time|date:"M d, Y H:i" }}</td>
|
||||
<td>{{ meeting.duration }} min</td>
|
||||
<td>
|
||||
{% if meeting %}
|
||||
<span class="badge {% if meeting.status == 'waiting' %}bg-warning{% elif meeting.status == 'started' %}bg-success{% elif meeting.status == 'ended' %}bg-danger{% endif %}">
|
||||
{% if meeting.status == 'started' %}
|
||||
<i class="fas fa-circle me-1 text-success"></i>
|
||||
{% endif %}
|
||||
{{ meeting.status|title }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-muted">--</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
{% if meeting.join_url %}
|
||||
<a href="{{ meeting.join_url }}" target="_blank" class="btn btn-main-action" title="{% trans 'Join' %}">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'meeting_details' meeting.slug %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-outline-secondary" title="{% trans 'Update' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#meetingModal"
|
||||
hx-post="{% url 'delete_meeting' meeting.slug %}"
|
||||
hx-target="#meetingModalBody"
|
||||
hx-swap="outerHTML"
|
||||
data-item-name="{{ meeting.topic }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Pagination (Standardized) #}
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if search_query %}&q={{ search_query }}{% endif %}">Last</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center py-5 card shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-video fa-3x mb-3" style="color: var(--kaauh-teal-dark);"></i>
|
||||
<h3>{% trans "No Zoom meetings found" %}</h3>
|
||||
<p class="text-muted">{% trans "Create your first meeting or adjust your filters." %}</p>
|
||||
<a href="{% url 'create_meeting' %}" class="btn btn-main-action mt-3">
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Create Your First Meeting" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -119,6 +119,7 @@
|
||||
{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p class="mb-2"><strong><i class="fas fa-calendar-day me-2 text-primary-theme"></i> Interview Type:</strong> {{interview_type}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
34
templates/interviews/schedule_interview_location_form.html
Normal file
34
templates/interviews/schedule_interview_location_form.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6 col-md-8">
|
||||
<div class="card shadow-lg border-0 rounded-lg mt-5">
|
||||
<div class="card-header bg-main-action text-white">
|
||||
<h3 class="text-center font-weight-light my-4">Set Interview Location</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'schedule_interview_location_form' schedule.slug %}" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Renders the single 'location' field using the crispy filter #}
|
||||
{{ form|crispy }}
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
|
||||
|
||||
<a href="{% url 'list_meetings' %}" class="btn btn-secondary me-2">
|
||||
<i class="fas fa-times me-1"></i> Close
|
||||
</a>
|
||||
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i> Save Location
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@ -139,7 +139,17 @@
|
||||
|
||||
<div class="col-md-8">
|
||||
<h5 class="section-header">{% trans "Schedule Details" %}</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group mb-3">
|
||||
<label for="{{ form.start_date.id_for_label }}">{% trans "Interview Type" %}</label>
|
||||
{{ form.interview_type }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user