career page logic

This commit is contained in:
Faheed 2025-11-13 14:05:00 +03:00
parent 4148c7eb16
commit 0c3f942161
28 changed files with 1719 additions and 672 deletions

View File

@ -11,7 +11,7 @@ from .models import (
ZoomMeeting, Candidate,TrainingMaterial,JobPosting,
FormTemplate,InterviewSchedule,BreakTime,JobPostingImage,
Profile,MeetingComment,ScheduledInterview,Source,HiringAgency,
AgencyJobAssignment, AgencyAccessLink,Participants
AgencyJobAssignment, AgencyAccessLink,Participants,OnsiteMeeting
)
# from django_summernote.widgets import SummernoteWidget
from django_ckeditor_5.widgets import CKEditor5Widget
@ -1594,24 +1594,29 @@ KAAUH HIRING TEAM
class InterviewScheduleLocationForm(forms.ModelForm):
class Meta:
model=InterviewSchedule
fields=['location']
widgets={
'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}),
}
# class OnsiteLocationForm(forms.ModelForm):
# class Meta:
# model=
# fields=['location']
# widgets={
# 'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}),
# }
class OnsiteMeetingForm(forms.ModelForm):
class Meta:
model = OnsiteMeeting
fields = ['topic', 'start_time', 'duration', 'timezone', 'location', 'status']
widgets = {
'topic': forms.TextInput(attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'}),
'start_time': forms.DateTimeInput(
attrs={'type': 'datetime-local', 'class': 'form-control'}
),
'duration': forms.NumberInput(
attrs={'min': 15, 'placeholder': 'Duration in minutes', 'class': 'form-control'}
),
'location': forms.TextInput(attrs={'placeholder': 'Physical location', 'class': 'form-control'}),
'timezone': forms.TextInput(attrs={'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-control'}),
}

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-10 09:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0011_alter_scheduledinterview_zoom_meeting'),
]
operations = [
migrations.AddField(
model_name='interviewschedule',
name='interview_topic',
field=models.CharField(blank=True, null=True),
),
]

View File

@ -0,0 +1,45 @@
# Generated by Django 5.2.7 on 2025-11-10 13:00
import django.db.models.deletion
import django_extensions.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0012_interviewschedule_interview_topic'),
]
operations = [
migrations.CreateModel(
name='OnsiteMeeting',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('topic', models.CharField(max_length=255, verbose_name='Topic')),
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
('duration', models.PositiveIntegerField(verbose_name='Duration')),
('timezone', models.CharField(max_length=50, verbose_name='Timezone')),
('location', models.CharField(blank=True, null=True)),
],
options={
'abstract': False,
},
),
migrations.RemoveField(
model_name='interviewschedule',
name='interview_topic',
),
migrations.RemoveField(
model_name='interviewschedule',
name='location',
),
migrations.AddField(
model_name='scheduledinterview',
name='onsite_meeting',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.onsitemeeting'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-10 13:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0013_onsitemeeting_and_more'),
]
operations = [
migrations.AddField(
model_name='onsitemeeting',
name='status',
field=models.CharField(blank=True, db_index=True, default='waiting', max_length=20, null=True, verbose_name='Status'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2025-11-10 13:55
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0014_onsitemeeting_status'),
]
operations = [
migrations.AlterField(
model_name='scheduledinterview',
name='onsite_meeting',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='onsite_interview', to='recruitment.onsitemeeting'),
),
]

View File

@ -731,6 +731,27 @@ class TrainingMaterial(Base):
def __str__(self):
return self.title
class OnsiteMeeting(Base):
class MeetingStatus(models.TextChoices):
WAITING = "waiting", _("Waiting")
STARTED = "started", _("Started")
ENDED = "ended", _("Ended")
CANCELLED = "cancelled",_("Cancelled")
# Basic meeting details
topic = models.CharField(max_length=255, verbose_name=_("Topic"))
start_time = models.DateTimeField(db_index=True, verbose_name=_("Start Time")) # Added index
duration = models.PositiveIntegerField(
verbose_name=_("Duration")
) # Duration in minutes
timezone = models.CharField(max_length=50, verbose_name=_("Timezone"))
location=models.CharField(null=True,blank=True)
status = models.CharField(
db_index=True, max_length=20, # Added index
null=True,
blank=True,
verbose_name=_("Status"),
default=MeetingStatus.WAITING,
)
class ZoomMeeting(Base):
class MeetingStatus(models.TextChoices):
@ -1613,7 +1634,6 @@ class InterviewSchedule(Base):
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
@ -1673,6 +1693,11 @@ class ScheduledInterview(Base):
ZoomMeeting, on_delete=models.CASCADE, related_name="interview", db_index=True,
null=True, blank=True
)
onsite_meeting= models.OneToOneField(
OnsiteMeeting, on_delete=models.CASCADE, related_name="onsite_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
)

View File

@ -0,0 +1,19 @@
from django import template
register = template.Library()
@register.simple_tag
def add_get_params(request_get, *args):
"""
Constructs a GET query string by preserving all current
parameters EXCEPT 'page', which is handled separately.
"""
params = request_get.copy()
# Remove the page parameter to prevent it from duplicating or interfering
if 'page' in params:
del params['page']
# Return the URL-encoded string (e.g., department=IT&employment_type=FULL_TIME)
# The template prepends the '&' and the 'page=X'
return params.urlencode()

View File

@ -14,6 +14,7 @@ urlpatterns = [
path('jobs/<slug:slug>/update/', views.edit_job, name='job_update'),
# path('jobs/<slug:slug>/delete/', views., name='job_delete'),
path('jobs/<slug:slug>/', views.job_detail, name='job_detail'),
path('jobs/<slug:slug>/download/cvs/', views.job_cvs_download, name='job_cvs_download'),
path('careers/',views.kaauh_career,name='kaauh_career'),
@ -234,8 +235,14 @@ 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')
# # --- SCHEDULED INTERVIEW URLS (New Centralized Management) ---
# path('interview/list/', views.InterviewListView.as_view(), name='interview_list'),
# path('interviews/<slug:slug>/', views.ScheduledInterviewDetailView.as_view(), name='scheduled_interview_detail'),
# path('interviews/<slug:slug>/update/', views.ScheduledInterviewUpdateView.as_view(), name='update_scheduled_interview'),
# path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'),
]

View File

@ -1,5 +1,8 @@
import json
import io
import zipfile
from django.core.paginator import Paginator
from django.utils.translation import gettext as _
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
@ -21,6 +24,7 @@ from django.db.models import F, IntegerField, Count, Avg, Sum, Q, ExpressionWrap
from django.db.models.functions import Cast, Coalesce, TruncDate
from django.db.models.fields.json import KeyTextTransform
from django.db.models.expressions import ExpressionWrapper
from django.urls import reverse_lazy
from django.db.models import Count, Avg, F,Q
from .forms import (
CandidateExamDateForm,
@ -44,7 +48,7 @@ from .forms import (
CandidateEmailForm,
SourceForm,
InterviewEmailForm,
InterviewScheduleLocationForm
)
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
from rest_framework import viewsets
@ -53,7 +57,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from .linkedin_service import LinkedInService
from .serializers import JobPostingSerializer, CandidateSerializer
from django.shortcuts import get_object_or_404, render, redirect
from django.views.generic import CreateView, UpdateView, DetailView, ListView
from django.views.generic import CreateView, UpdateView, DetailView, ListView,DeleteView
from .utils import (
create_zoom_meeting,
delete_zoom_meeting,
@ -194,20 +198,17 @@ class ZoomMeetingListView(LoginRequiredMixin, ListView):
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
})
# @login_required
# def InterviewListView(request):
# # interview_type=request.GET.get('interview_type','Remote')
# # print(interview_type)
# interview_type='Onsite'
# meetings=ScheduledInterview.objects.filter(schedule__interview_type=interview_type)
# return render(request, "meetings/list_meetings.html",{
# 'meetings':meetings,
# })
# search_query = request.GET.get("q", "") # Renamed from 'search' to 'q' for consistency
# if search_query:
# interviews = interviews.filter(
@ -239,7 +240,10 @@ class ZoomMeetingDetailsView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context=super().get_context_data(**kwargs)
meeting = self.object
interview=meeting.interview
try:
interview=meeting.interview
except Exception as e:
print(e)
candidate = interview.candidate
job=meeting.get_job
@ -402,10 +406,13 @@ def edit_job(request, slug):
SCORE_PATH = 'ai_analysis_data__analysis_data__match_score'
HIGH_POTENTIAL_THRESHOLD=75
from django.contrib.sites.shortcuts import get_current_site
@login_required
def job_detail(request, slug):
"""View details of a specific job"""
job = get_object_or_404(JobPosting, slug=slug)
current_site=get_current_site(request)
print(current_site)
# Get all candidates for this job, ordered by most recent
applicants = job.candidates.all().order_by("-created_at")
@ -556,6 +563,60 @@ def job_detail(request, slug):
}
return render(request, "jobs/job_detail.html", context)
ALLOWED_EXTENSIONS = ('.pdf', '.docx')
def job_cvs_download(request,slug):
job = get_object_or_404(JobPosting,slug=slug)
entries=Candidate.objects.filter(job=job)
# 2. Create an in-memory byte stream (BytesIO)
zip_buffer = io.BytesIO()
# 3. Create the ZIP archive
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
for entry in entries:
# Check if the file field has a file
if not entry.resume:
continue
# Get the file name and check extension (case-insensitive)
file_name = entry.resume.name.split('/')[-1]
file_name_lower = file_name.lower()
if file_name_lower.endswith(ALLOWED_EXTENSIONS):
try:
# Open the file object (rb is read binary)
file_obj = entry.resume.open('rb')
# *** ROBUST METHOD: Read the content and write it to the ZIP ***
file_content = file_obj.read()
# Write the file content directly to the ZIP archive
zf.writestr(file_name, file_content)
file_obj.close()
except Exception as e:
# Log the error but continue with the rest of the files
print(f"Error processing file {file_name}: {e}")
continue
# 4. Prepare the response
zip_buffer.seek(0)
# 5. Create the HTTP response
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
# Set the header for the browser to download the file
response['Content-Disposition'] = 'attachment; filename=f"all_cvs_for_{job.title}.zip"'
return response
@login_required
def job_image_upload(request, slug):
#only for handling the post request
@ -612,6 +673,20 @@ def edit_linkedin_post_content(request,slug):
JOB_TYPES = [
("FULL_TIME", "Full-time"),
("PART_TIME", "Part-time"),
("CONTRACT", "Contract"),
("INTERNSHIP", "Internship"),
("FACULTY", "Faculty"),
("TEMPORARY", "Temporary"),
]
WORKPLACE_TYPES = [
("ON_SITE", "On-site"),
("REMOTE", "Remote"),
("HYBRID", "Hybrid"),
]
def kaauh_career(request):
@ -621,8 +696,48 @@ def kaauh_career(request):
status='ACTIVE',
form_template__is_active=True
)
selected_department=request.GET.get('department','')
department_type_keys=active_jobs.exclude(
department__isnull=True
).exclude(department__exact=''
).values_list(
'department',
flat=True
).distinct().order_by('department')
if selected_department and selected_department in department_type_keys:
active_jobs=active_jobs.filter(department=selected_department)
selected_workplace_type=request.GET.get('workplace_type','')
print(selected_workplace_type)
selected_job_type = request.GET.get('employment_type', '')
job_type_keys = active_jobs.values_list('job_type', flat=True).distinct()
workplace_type_keys=active_jobs.values_list('workplace_type',flat=True).distinct()
if selected_job_type and selected_job_type in job_type_keys:
active_jobs=active_jobs.filter(job_type=selected_job_type)
if selected_workplace_type and selected_workplace_type in workplace_type_keys:
active_jobs=active_jobs.filter(workplace_type=selected_workplace_type)
return render(request,'applicant/career.html',{'active_jobs':active_jobs})
JOBS_PER_PAGE=10
paginator = Paginator(active_jobs, JOBS_PER_PAGE)
page_number = request.GET.get('page', 1)
try:
page_obj = paginator.get_page(page_number)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
total_open_roles=active_jobs.all().count()
return render(request,'applicant/career.html',{'active_jobs': page_obj.object_list,
'job_type_keys':job_type_keys,
'selected_job_type':selected_job_type,
'workplace_type_keys':workplace_type_keys,
'selected_workplace_type':selected_workplace_type,
'selected_department':selected_department,
'department_type_keys':department_type_keys,
'total_open_roles': total_open_roles,'page_obj': page_obj})
# job detail facing the candidate:
def application_detail(request, slug):
@ -4131,18 +4246,19 @@ 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 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})

View File

@ -1,172 +1,216 @@
{% extends 'applicant/partials/candidate_facing_base.html'%}
{% extends 'applicant/partials/candidate_facing_base.html' %}
{% load static i18n %}
{% block content %}
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: var(--kaauh-teal); z-index: 1030;">
<div class="container-fluid">
<span class="navbar-text text-white fw-bold">{% trans "Job Overview" %}</span>
{# ------------------------------------------------ #}
{# 🚀 TOP NAV BAR (Sticky and Themed) #}
{# ------------------------------------------------ #}
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top border-bottom"
style="background-color: var(--kaauh-teal); z-index: 1030; height: 50px;">
<div class="container-fluid container-lg">
<span class="navbar-text text-white fw-bold fs-6">{% trans "Job Overview" %}</span>
</div>
</nav>
{# ------------------------------------------------ #}
{# 🔔 DJANGO MESSAGES (Refined placement and styling) #}
{# ------------------------------------------------ #}
{# ================================================= #}
{# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #}
{# ================================================= #}
{% if messages %}
<div class="container-fluid message-container">
<div class="row">
{# Using responsive columns to center the message content, similar to your form structure #}
<div class="col-lg-8 offset-lg-2 col-md-10 offset-md-1 col-12">
{% for message in messages %}
{# Use 'alert-{{ message.tags }}' to apply Bootstrap styling based on Django's tag (success, error/danger, info, warning) #}
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{# ================================================= #}
<div class="container">
<div class="row mb-5 mt-3 main-content-area">
{# ------------------------------------------------ #}
{# 💻 MAIN CONTENT CONTAINER #}
{# ------------------------------------------------ #}
<div class="container mt-4 mb-5">
<div class="row g-4 main-content-area">
<div class="col-lg-4 order-lg-2 order-1 d-none d-lg-block">
<div class="card shadow-sm sticky-top">
<div class="card-header bg-kaauh-teal-dark bg-white p-3">
<h5 class="mb-0 fw-bold"><i class="fas fa-file-signature me-2" style="color: var(--kaauh-teal);"></i>{% trans "Ready to Apply?" %}</h5>
{# 📌 RIGHT COLUMN: Sticky Apply Card (Desktop Only) #}
<div class="col-lg-4 order-lg-2 d-none d-lg-block">
<div class="card shadow-lg border-0" style="position: sticky; top: 70px;">
<div class="card-header bg-white border-bottom p-3">
<h5 class="mb-0 fw-bold text-kaauh-teal">
<i class="fas fa-file-signature me-2"></i>{% trans "Ready to Apply?" %}
</h5>
</div>
<div class="card-body text-center">
<p class="text-muted">{% trans "Review the job details, then apply below." %}</p>
<div class="card-body text-center p-4">
<p class="text-muted small mb-3">{% trans "Review the full job details below before submitting your application." %}</p>
{% if job.form_template %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100 shadow-sm">
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a>
{% elif not job.is_expired %}
<p class="text-danger fw-bold">{% trans "Application form is unavailable." %}</p>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-8 order-lg-1 order-2">
<div class="card shadow-sm">
{# 📝 LEFT COLUMN: Job Details #}
<div class="col-lg-8 order-lg-1">
<article class="card shadow-lg border-0">
<div class="card-header bg-white border-bottom p-4">
<h1 class="h3 mb-0 fw-bold" style="color: var(--kaauh-teal);">{{ job.title }}</h1>
</div>
{# Job Title Header #}
<header class="card-header bg-white border-bottom p-4">
<h1 class="h2 mb-0 fw-bolder text-kaauh-teal">{{ job.title }}</h1>
</header>
<div class="card-body p-4">
<h4 class="mb-3 fw-bold" style="color: var(--kaauh-teal-dark);">{% trans "Job Overview" %}</h4>
<div class="row row-cols-1 row-cols-md-2 g-3 mb-5 small text-secondary border p-3 rounded">
<h4 class="mb-4 fw-bold text-muted border-bottom pb-2">{% trans "Summary" %}</h4>
{# Job Metadata/Overview Grid #}
<section class="row row-cols-1 row-cols-md-2 g-3 mb-5 small text-secondary p-3 rounded bg-light-subtle border">
{# SALARY #}
{% if job.salary_range %}
<div class="col">
<i class="fas fa-money-bill-wave text-success me-2"></i>
<i class="fas fa-money-bill-wave text-success me-2 fa-fw"></i>
<strong>{% trans "Salary:" %}</strong>
<span class="fw-bold text-success">{{ job.salary_range }}</span>
</div>
{% endif %}
{# DEADLINE #}
<div class="col">
<i class="fas fa-calendar-alt text-muted me-2"></i>
<i class="fas fa-calendar-alt text-muted me-2 fa-fw"></i>
<strong>{% trans "Deadline:" %}</strong>
{% if job.application_deadline %}
{{ job.application_deadline|date:"M d, Y" }}
<time datetime="{{ job.application_deadline|date:'Y-m-d' }}">
{{ job.application_deadline|date:"M d, Y" }}
</time>
{% if job.is_expired %}
<span class="badge bg-danger ms-2">{% trans "EXPIRED" %}</span>
{% endif %}
{% else %}
<span class="text-muted">{% trans "Not specified" %}</span>
<span class="text-muted">{% trans "Ongoing" %}</span>
{% endif %}
</div>
<div class="col"> <i class="fas fa-briefcase text-muted me-2"></i> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }} </div>
<div class="col"> <i class="fas fa-map-marker-alt text-muted me-2"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }} </div>
<div class="col"> <i class="fas fa-building text-muted me-2"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }} </div>
<div class="col"> <i class="fas fa-hashtag text-muted me-2"></i> <strong>{% trans "JOB ID:" %}</strong> {{ job.internal_job_id|default:"N/A" }} </div>
<div class="col"> <i class="fas fa-desktop text-muted me-2"></i> <strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }} </div>
</div>
<div class="accordion" id="jobDetailAccordion">
{# JOB TYPE #}
<div class="col">
<i class="fas fa-briefcase text-muted me-2 fa-fw"></i>
<strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
</div>
{# LOCATION #}
<div class="col">
<i class="fas fa-map-marker-alt text-muted me-2 fa-fw"></i>
<strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
</div>
{# DEPARTMENT #}
<div class="col">
<i class="fas fa-building text-muted me-2 fa-fw"></i>
<strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
</div>
{# JOB ID #}
<div class="col">
<i class="fas fa-hashtag text-muted me-2 fa-fw"></i>
<strong>{% trans "JOB ID:" %}</strong> {{ job.internal_job_id|default:"N/A" }}
</div>
{# WORKPLACE TYPE #}
<div class="col">
<i class="fas fa-laptop-house text-muted me-2 fa-fw"></i>
<strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
</div>
</section>
{# Detailed Accordion Section #}
<div class="accordion accordion-flush" id="jobDetailAccordion">
{% with active_collapse="collapseOne" %}
{# JOB DESCRIPTION #}
{% if job.has_description_content %}
<div class="accordion-item">
<div class="accordion-item border-top border-bottom">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<i class="fas fa-info-circle me-3"></i> {% trans "Job Description" %}
<button class="accordion-button fw-bold fs-5 text-kaauh-teal-dark" type="button"
data-bs-toggle="collapse" data-bs-target="#{{ active_collapse }}" aria-expanded="true"
aria-controls="{{ active_collapse }}">
<i class="fas fa-info-circle me-3 fa-fw"></i> {% trans "Job Description" %}
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#jobDetailAccordion">
<div id="{{ active_collapse }}" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4">
{{ job.description|safe }}
<div class="wysiwyg-content">{{ job.description|safe }}</div>
</div>
</div>
</div>
{% endif %}
{# QUALIFICATIONS #}
{% if job.has_qualifications_content %}
<div class="accordion-item">
<div class="accordion-item border-bottom">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<i class="fas fa-graduation-cap me-3"></i> {% trans "Qualifications" %}
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<i class="fas fa-graduation-cap me-3 fa-fw"></i> {% trans "Qualifications" %}
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4">
{{ job.qualifications|safe }}
<div class="wysiwyg-content">{{ job.qualifications|safe }}</div>
</div>
</div>
</div>
{% endif %}
{# BENEFITS #}
{% if job.has_benefits_content %}
<div class="accordion-item">
<div class="accordion-item border-bottom">
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button collapsed fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<i class="fas fa-hand-holding-usd me-3"></i> {% trans "Benefits" %}
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<i class="fas fa-hand-holding-usd me-3 fa-fw"></i> {% trans "Benefits" %}
</button>
</h2>
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4">
{{ job.benefits|safe }}
<div class="wysiwyg-content">{{ job.benefits|safe }}</div>
</div>
</div>
</div>
{% endif %}
{# APPLICATION INSTRUCTIONS #}
{% if job.has_application_instructions_content %}
<div class="accordion-item">
<div class="accordion-item border-bottom">
<h2 class="accordion-header" id="headingFour">
<button class="accordion-button collapsed fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
<i class="fas fa-file-alt me-3"></i> {% trans "Application Instructions" %}
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
<i class="fas fa-file-alt me-3 fa-fw"></i> {% trans "Application Instructions" %}
</button>
</h2>
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4">
{{ job.application_instructions|safe }}
<div class="wysiwyg-content">{{ job.application_instructions|safe }}</div>
</div>
</div>
</div>
{% endif %}
{% endwith %}
</div>
</div>
</div>
</div>
</article>
</div>
</div>
</div>
<div class="mobile-fixed-apply-bar d-lg-none text-center">
{% if job.form_template %}
{# 📱 MOBILE FIXED APPLY BAR (Replaced inline style with utility classes) #}
{% if job.form_template %}
<footer class="fixed-bottom d-lg-none bg-white border-top shadow-lg p-3">
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a>
{% endif %}
</div>
</footer>
{% endif %}
{% endblock content%}

View File

@ -1,27 +1,37 @@
{% extends 'applicant/partials/candidate_facing_base.html'%}
{% extends 'applicant/partials/candidate_facing_base.html' %}
{% load static i18n %}
{% block title %}{% trans "My Profile" %} - {{ block.super }}{% endblock %}
{# Use a dynamic title for better SEO and user context #}
{% block title %}{% trans "Career Opportunities" %} | KAAUH - {{ block.super }}{% endblock %}
{% block content %}
{% block content %}
<div class="main-content-area">
<header class="hero-section">
<div class="container">
<div class="row">
<div class="col-lg-12 col-xl-10">
<h1 class="hero-title mb-4">
{% translate "Your Career in Health & Academia starts here." %}
{# ------------------------------------------------ #}
{# 🌟 HERO SECTION (High Visual Impact) #}
{# ------------------------------------------------ #}
<header class="hero-section text-white py-5 py-lg-6" style=" background-size: cover; background-position: center;">
{# Overlay for readability, assuming custom CSS defines .hero-overlay #}
<div class="hero-overlay"></div>
<div class="container position-relative" style="z-index: 2;">
<div class="row justify-content-center">
<div class="col-lg-12 col-xl-10 text-center">
{# Use a large, commanding font size #}
<h1 class="display-4 fw-bolder mb-4 animate__animated animate__fadeInDown">
{% trans "Your Career in Health & Academia Starts Here." %}
</h1>
<p class="lead mb-5">
{% translate "Join KAAUH, a national leader in patient care, research, and education. We are building the future of healthcare." %}
<p class="lead mb-5 fs-5 animate__animated animate__fadeInUp">
{% trans "Join KAAUH, a national leader in patient care, research, and education. We are building the future of healthcare." %}
</p>
<a href="#filterSidebar" class="btn btn-hero-action me-3 mb-3 mb-lg-0">
<i class="fas fa-compass me-2"></i> {% translate "Find Your Path" %}
{# Primary Action to scroll to listings/filters #}
<a href="#job-list-start" class="btn btn-hero-action btn-lg rounded-pill px-5 me-3 mb-3 mb-lg-0 shadow-lg animate__animated animate__zoomIn">
<i class="fas fa-compass me-2"></i> {% trans "Find Your Path" %}
</a>
<a href="https://kaauh.edu.sa/about-us" class="btn btn-outline-light rounded-pill px-4 btn-lg">
{% translate "About US" %}
{# Secondary Action #}
<a href="https://kaauh.edu.sa/about-us" class="btn btn-outline-light rounded-pill px-5 btn-lg animate__animated animate__zoomIn">
{% trans "About US" %}
</a>
</div>
</div>
@ -29,128 +39,236 @@
</header>
<section class="py-5 job-listing-section">
{# ------------------------------------------------ #}
{# 💻 JOB LISTING SECTION #}
{# ------------------------------------------------ #}
<section class="py-5 job-listing-section" id="job-list-start">
<div class="container">
<div class="row g-5">
<div class="col-lg-3 col-xl-3">
{# 📌 LEFT COLUMN: FILTERS (Smaller on larger screens) #}
<div class="col-lg-4">
<button class="btn btn-outline-dark filter-toggle-button d-lg-none" type="button"
{# Mobile Filter Toggle (Used aria-controls for better accessibility) #}
<button class="btn btn-outline-dark filter-toggle-button d-lg-none w-100 mb-3" type="button"
data-bs-toggle="collapse" data-bs-target="#filterSidebar" aria-expanded="false" aria-controls="filterSidebar">
<i class="fas fa-filter me-2"></i> {% translate "Filter Jobs" %}
<i class="fas fa-filter me-2"></i> {% trans "Filter Jobs" %}
</button>
<div class="collapse d-lg-block filter-sidebar-collapse" id="filterSidebar">
<div class="card sticky-top-filters p-4 bg-white">
<h4 class="fw-bold mb-4 text-primary-theme">
{% translate "Refine Your Search" %}
</h4>
{# Sticky top ensures filters remain visible while scrolling results #}
<div class="card border-0 shadow-sm sticky-top-filters p-4 bg-light-subtle" style="top: 20px;">
<div class="d-grid gap-3">
<select class="form-select" aria-label="Department filter">
<option selected>{% translate "Department (Faculty/Admin)" %}</option>
<option value="1">{% translate "Clinical Services" %}</option>
<option value="2">{% translate "Research Labs" %}</option>
<option value="3">{% translate "Training & Education" %}</option>
</select>
<h4 class="fw-bold mb-4 text-dark border-bottom pb-2">
<i class="fas fa-search me-2 text-primary-theme"></i>{% trans "Refine Your Search" %}
</h4>
<form method="GET" action="{% url 'kaauh_career'%}" class="d-grid gap-3">
{# NOTE: Replace select with Django form fields for real functionality #}
<select class="form-select" aria-label="Employment Type filter">
<option selected>{% translate "Employment Type" %}</option>
<option value="1">{% translate "Full-Time" %}</option>
<option value="2">{% translate "Part-Time" %}</option>
<select class="form-select form-select-lg" name="employment_type" aria-label="Employment Type filter" >
<option value="" {% if not selected_job_type %}selected{% endif %}>{% trans "Employment Type" %}</option>
{% for key in job_type_keys %}
<option value="{{ key }}" {% if key == selected_job_type %}selected{% endif %}>
<!-- Hard-coded mapping using IF statements -->
{% if key == 'FULL_TIME' %}{% trans "Full-time" %}{% endif %}
{% if key == 'PART_TIME' %}{% trans "Part-time" %}{% endif %}
{% if key == 'CONTRACT' %}{% trans "Contract" %}{% endif %}
{% if key == 'INTERNSHIP' %}{% trans "Internship" %}{% endif %}
{% if key == 'FACULTY' %}{% trans "Faculty" %}{% endif %}
{% if key == 'TEMPORARY' %}{% trans "Temporary" %}{% endif %}
</option>
{% endfor %}
</select>
<select class="form-select" aria-label="Specialty filter">
<option selected>{% translate "Specialty / Focus" %}</option>
<option value="1">{% translate "Women's Health" %}</option>
<option value="2">{% translate "Child Growth & Dev" %}</option>
<select class="form-select form-select-lg" name="workplace_type" aria-label="Workplace Type filter" >
<option value="" {% if not selected_workplace_type %}selected{% endif %}>{% trans "Workplace Type" %}</option>
{% for key in workplace_type_keys %}
<option value="{{ key }}" {% if key == selected_workplace_type %}selected{% endif %}>
<!-- Hard-coded mapping using IF statements -->
{% if key == 'ON_SITE' %}{% trans "On-site" %}{% endif %}
{% if key == 'REMOTE' %}{% trans "Remote" %}{% endif %}
{% if key == 'HYBRID' %}{% trans "Hybrid" %}{% endif %}
</option>
{% endfor %}
</select>
<button class="btn btn-main-action rounded-pill mt-3">{% translate "Apply Filters" %}</button>
</div>
<select class="form-select form-select-lg" name="department" aria-label="Department Type filter" >
<option value="" {% if not selected_department %}selected{% endif %}>{% trans "Departments" %}</option>
{% for key in department_type_keys %}
<option value="{{ key }}" {% if key == selected_department %}selected{% endif %}>
<!-- Hard-coded mapping using IF statements -->
{{key}}
</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-main-action btn-lg rounded-pill mt-3 shadow-sm">
{% trans "Apply Filters" %}
</button>
<a href="." class="btn btn-outline-secondary btn-sm">{% trans "Clear Filters" %}</a>
</form>
</div>
</div>
</div>
<div class="col-lg-9 col-xl-9">
{# 📜 RIGHT COLUMN: JOB LISTINGS (Wider on larger screens) #}
<div class="col-lg-8">
<div class="sticky-filter-bar">
{# Sticky Filter Bar (Summary of results and active filters) #}
<div class="sticky-filter-bar bg-white p-3 border-bottom mb-4 shadow-sm">
<div class="d-flex flex-wrap justify-content-between align-items-center">
{# Dynamic Count #}
<h3 class="fw-bold mb-0 text-dark fs-5">
{% comment %}
Assuming a 'job_count' context variable exists, otherwise
this remains static as in the original template.
{% endcomment %}
{% translate "Showing 37 Open Roles" %}
<span class="text-primary-theme">{{ total_open_roles|default:"0" }}</span> {% trans "Open Roles" %}
</h3>
<div class="d-flex flex-wrap gap-2">
<span class="filter-chip">
{% translate "Specialty: Women's Health" %}
<i class="fas fa-times text-xs ms-2 cursor-pointer" role="button" aria-label="Remove filter: Women's Health"></i>
</span>
</div>
{# Active Filter Chips (Use a dedicated class for styling) #}
<div class="d-flex flex-wrap gap-2 pt-2 pt-md-0">
{# --- Active Employment Type Filter Chip --- #}
{% if selected_job_type %}
<span class="filter-chip badge bg-primary-theme-subtle text-primary-theme fw-normal p-2 active-filter-chip">
{% trans "Type" %}:
{# Map the key back to its human-readable translation #}
<strong class="mx-1">
{% if selected_job_type == 'FULL_TIME' %}{% trans "Full-time" %}
{% elif selected_job_type == 'PART_TIME' %}{% trans "Part-time" %}
{% elif selected_job_type == 'CONTRACT' %}{% trans "Contract" %}
{% elif selected_job_type == 'INTERNSHIP' %}{% trans "Internship" %}
{% elif selected_job_type == 'FACULTY' %}{% trans "Faculty" %}
{% elif selected_job_type == 'TEMPORARY' %}{% trans "Temporary" %}
{% endif %}
</strong>
{# Link to clear this specific filter: use current URL but remove `employment_type` parameter #}
<a href="?{% for key, value in request.GET.items %}{% if key != 'employment_type' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
class="text-primary-theme text-decoration-none ms-2" role="button" aria-label="Remove Employment Type filter">
<i class="fas fa-times text-xs"></i>
</a>
</span>
{% endif %}
{# --- Active Workplace Type Filter Chip --- #}
{% if selected_workplace_type %}
<span class="filter-chip badge bg-primary-theme-subtle text-primary-theme fw-normal p-2 active-filter-chip">
{% trans "Workplace" %}:
{# Map the key back to its human-readable translation #}
<strong class="mx-1">
{% if selected_workplace_type == 'ON_SITE' %}{% trans "On-site" %}
{% elif selected_workplace_type == 'REMOTE' %}{% trans "Remote" %}
{% elif selected_workplace_type == 'HYBRID' %}{% trans "Hybrid" %}
{% endif %}
</strong>
{# Link to clear this specific filter: use current URL but remove `workplace_type` parameter #}
<a href="?{% for key, value in request.GET.items %}{% if key != 'workplace_type' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
class="text-primary-theme text-decoration-none ms-2" role="button" aria-label="Remove Workplace Type filter">
<i class="fas fa-times text-xs"></i>
</a>
</span>
{% endif %}
{# --- Active Department Filter Chip --- #}
{% if selected_department %}
<span class="filter-chip badge bg-primary-theme-subtle text-primary-theme fw-normal p-2 active-filter-chip">
{% trans "Department" %}:
<strong class="mx-1">{{ selected_department }}</strong>
{# Link to clear this specific filter: use current URL but remove `department` parameter #}
<a href="?{% for key, value in request.GET.items %}{% if key != 'department' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
class="text-primary-theme text-decoration-none ms-2" role="button" aria-label="Remove Department filter">
<i class="fas fa-times text-xs"></i>
</a>
</span>
{% endif %}
</div>
</div>
</div>
{# Job Cards Grid #}
<div class="mt-4 d-grid gap-3">
{% for job in active_jobs %}
{# The original card structure, now dynamically filled with job data #}
<a href="{% url 'application_detail' job.slug %}" class="card d-block text-decoration-none text-dark job-listing-card bg-white">
<div class="d-flex justify-content-between align-items-start">
<h4 class="h5 fw-bold mb-1 text-primary-theme-hover">
{# Optimized Job Listing Card #}
<a href="{% url 'application_detail' job.slug %}"
class="card d-block text-decoration-none text-dark job-listing-card p-4 border-2 shadow-hover transition-all">
<div class="d-flex justify-content-between align-items-start mb-2">
{# Job Title #}
<h4 class="h5 fw-bold mb-0 text-primary-theme-hover">
{{ job.title }}
</h4>
{# NOTE: You will need to define how job.category or job.tag is determined for the badge logic #}
<span class="badge bg-primary-theme job-tag">
{% comment %} Placeholder: Use job.tag or implement conditional logic {% endcomment %}
{% if job.tag_slug == 'clinical' %}{% translate "Clinical" %}
{% elif job.tag_slug == 'research' %}{% translate "Research/Contract" %}
{% else %}{% translate "General" %}{% endif %}
{# Tag Badge (Prominent) #}
<span class="badge rounded-pill bg-kaauh-teal job-tag px-3 py-2 fs-6">
<i class="fas fa-tag me-1"></i>{% trans "Apply Before: " %}{{job.application_deadline}}
</span>
</div>
{# NOTE: Assuming job.department and job.location exist in your context #}
<p class="text-muted small mb-3">{{ job.department }}</p>
{# Department/Context (Sub-text) #}
<p class="text-muted small mb-3">{% trans 'Department: '%}{{ job.department|default:"KAAUH Department" }}</p>
<div class="d-flex flex-wrap gap-4 small text-muted">
<span class="d-flex align-items-center">
<i class="fas fa-map-marker-alt me-2 job-detail-icon"></i>
{{ job.location|default:"Riyadh, KSA" }}
{# Job Metadata Icons (Horizontal list for quick scan) #}
<div class="d-flex flex-wrap gap-4 small text-secondary">
<span class="d-flex align-items-center fw-medium">
<i class="fas fa-map-marker-alt me-2 text-primary-theme fa-fw"></i>
{{ job.location_country|default:"Kindom of Saudi Arabia" }}&nbsp;|&nbsp;{{job.location_state|default:"Riyadh Province"}}&nbsp;|&nbsp;{{job.location_city|default:"Riyadh"}}
</span>
<span class="d-flex align-items-center">
<i class="fas fa-user-md me-2 job-detail-icon"></i>
{{ job.focus|default:"High Reliability Focus" }}
<span class="d-flex align-items-center fw-medium">
<i class="fas fa-user-md me-2 text-primary-theme fa-fw"></i>
{{ job.workplace_type|default:"" }}
</span>
<span class="d-flex align-items-center">
<i class="fas fa-calendar-alt me-2 job-detail-icon"></i>
{{ job.employment_type|default:"Full-Time" }}
<span class="d-flex align-items-center fw-medium">
<i class="fas fa-calendar-alt me-2 text-primary-theme fa-fw"></i>
{{ job.job_type|default:"Full-Time" }}
</span>
{% if job.posted_date %}
<span class="d-flex align-items-center fw-medium">
<i class="fas fa-clock me-2 text-primary-theme fa-fw"></i>
{% trans "Posted:" %} {{ job.posted_date|timesince }} {% trans "ago" %}
</span>
{% endif %}
</div>
</a>
{% empty %}
<div class="alert alert-info" role="alert">
{% translate "We currently have no open roles that match your search. Please check back soon!" %}
<div class="alert alert-info border-0 shadow-sm mt-5" role="alert">
<h5 class="alert-heading">{% trans "No Matching Opportunities" %}</h5>
<p>{% trans "We currently have no open roles that match your search and filters. Please modify your criteria or check back soon!" %}</p>
</div>
{% endfor %}
{# Load More Button #}
{% if show_load_more %}
<div class="text-center mt-5 mb-3">
<button class="btn btn-main-action btn-lg rounded-pill px-5 shadow-sm">
{% translate "Load More Jobs" %}
<button class="btn btn-main-action btn-lg rounded-pill px-5 shadow-lg">
{% trans "Load More Jobs" %} <i class="fas fa-redo ms-2"></i>
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% include "includes/paginator.html" %}
</section>
</div>
{% endblock content %}
{% endblock content %}

View File

@ -49,6 +49,10 @@
.text-primary-theme { color: var(--kaauh-teal) !important; }
.text-primary-theme-hover:hover { color: var(--kaauh-teal-dark) !important; }
.bg-kaauh-teal {
background-color: #00636e;
}
.btn-main-action {
background-color: var(--kaauh-teal);
@ -178,7 +182,7 @@
<div class="container-fluid">
<a class="navbar-brand text-dark fw-bold" href="{% url 'kaauh_career' %}">
<img src="{% static 'image/kaauh.jpeg' %}" alt="{% translate 'KAAUH IMAGE' %}" style="height: 50px; margin-right: 10px;">
KAAUH
<span style="color:#00636e;">KAAUH Careers</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
@ -205,7 +209,7 @@
<span class="d-inline">{{ LANGUAGE_CODE|upper }}</span>
</button>
<ul class="dropdown-menu {% if LANGUAGE_CODE == 'ar' %}dropdown-menu-start{% else %}dropdown-menu-end{% endif %}" aria-labelledby="navbarLanguageDropdown">
<ul class="dropdown-menu mx-auto {% if LANGUAGE_CODE == 'ar' %}dropdown-menu-end{% else %}dropdown-menu-end{% endif %}" aria-labelledby="navbarLanguageDropdown">
<li>
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
@ -231,10 +235,35 @@
</div>
</nav>
{% if messages %}
<div class="container message-container mt-3">
<div class="row">
{# Use responsive columns matching the main content block for alignment #}
<div class="col-lg-12 order-lg-1 col-12 mx-auto">
{% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i> {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{# ================================================= #}
{# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #}
{# ================================================= #}
{# ================================================= #}
{% block content %}
{% endblock content %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

View File

@ -208,7 +208,11 @@
<i class="fas fa-sign-out-alt me-3 fs-5 " style="color:red;"></i>
<span style="color:red;">{% trans "Sign Out" %}</span>
</button>
</form>
</form>
{% comment %} <a class="d-inline text-decoration-none px-4 d-flex align-items-center border-0 bg-transparent text-start text-center" href={% url "account_logout" %}>
<i class="fas fa-sign-out-alt me-3 fs-5 " style="color:red;"></i>
<span style="color:red;">{% trans "Sign Out" %}</span>
</a> {% endcomment %}
</li>
{% endif %}
</ul>
@ -255,7 +259,7 @@
</span>
</a>
</li>
<li class="nav-item me-lg-4">
{% comment %} <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">
@ -264,7 +268,7 @@
{% trans "Onsite Interviews" %}
</span>
</a>
</li>
</li> {% endcomment %}
<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">

View File

@ -1,27 +1,61 @@
{% 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 text-primary-theme" href="?page=1{% if search_query %}&q={{ search_query }}{% endif %}{% if job_filter %}&job={{ job_filter }}{% endif %}">First</a>
</li>
<li class="page-item">
<a class="page-link text-primary-theme" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if job_filter %}&job={{ job_filter }}{% endif %}">Previous</a>
</li>
{% endif %}
{% if page_obj.has_previous or page_obj.has_next %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{# Helper to build the query string while excluding the 'page' parameter #}
{% load url_extras %}
{# Build a string of all current filters (e.g., &department=IT&type=FULL_TIME) #}
{% add_get_params request.GET as filter_params %}
{% with filter_params=filter_params %}
{% if page_obj.has_previous %}
{# First Page Link #}
<li class="page-item">
<span class="page-link bg-primary-theme text-white">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
<a class="page-link text-primary-theme"
href="?page=1{{ filter_params }}">
First
</a>
</li>
{# Previous Page Link #}
<li class="page-item">
<a class="page-link text-primary-theme"
href="?page={{ page_obj.previous_page_number }}{{ filter_params }}">
Previous
</a>
</li>
{% endif %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link text-primary-theme" href="?page={{ page_obj.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if job_filter %}&job={{ job_filter }}{% endif %}">Next</a>
</li>
<li class="page-item">
<a class="page-link text-primary-theme" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&q={{ search_query }}{% endif %}{% if job_filter %}&job={{ job_filter }}{% endif %}">Last</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{# Current Page Status - Use your teal/custom background class here #}
<li class="page-item active" aria-current="page">
<span class="page-link bg-kaauh-teal text-white">
{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
</li>
{% if page_obj.has_next %}
{# Next Page Link #}
<li class="page-item">
<a class="page-link text-primary-theme"
href="?page={{ page_obj.next_page_number }}{{ filter_params }}">
Next
</a>
</li>
{# Last Page Link #}
<li class="page-item">
<a class="page-link text-primary-theme"
href="?page={{ page_obj.paginator.num_pages }}{{ filter_params }}">
Last
</a>
</li>
{% endif %}
{% endwith %}
</ul>
</nav>
{% endif %}

View File

@ -0,0 +1,708 @@
{% extends 'base.html' %}
{% load static i18n %}
{% load widget_tweaks %}
{% block customCSS %}
<style>
/* -------------------------------------------------------------------------- */
/* KAAT-S Redesign CSS - Compacted and Reordered Layout */
/* -------------------------------------------------------------------------- */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-teal-light: #e0f7f9;
--kaauh-border: #e9ecef;
--kaauh-primary-text: #212529;
--kaauh-secondary-text: #6c757d;
--kaauh-gray-light: #f8f9fa;
--kaauh-success: #198754;
--kaauh-danger: #dc3545;
--kaauh-link: #007bff;
--kaauh-link-hover: #0056b3;
}
body {
background-color: #f0f2f5;
font-family: 'Inter', sans-serif;
}
/* ------------------ Card & Header Styles ------------------ */
.card {
border: none;
border-radius: 8px; /* Slightly smaller radius */
box-shadow: 0 3px 10px rgba(0,0,0,0.04); /* Lighter shadow */
margin-bottom: 1rem;
}
.card-body {
padding: 1rem 1.25rem; /* Reduced padding */
}
#comments-card .card-header {
background-color: white;
color: var(--kaauh-teal-dark);
padding: 0.75rem 1.25rem; /* Reduced header padding */
font-weight: 600;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid var(--kaauh-border);
}
/* ------------------ Main Title & Status ------------------ */
.main-title-container {
padding: 0 0 1rem 0; /* Space below the main title */
}
.main-title-container h1 {
font-size: 1.75rem; /* Reduced size */
font-weight: 700;
}
.status-badge {
font-size: 0.7rem; /* Smaller badge */
padding: 0.3em 0.7em;
border-radius: 12px;
}
.bg-scheduled { background-color: #00636e !important; color: white !important;}
.bg-completed { background-color: #198754 !important; color: white !important;}
.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: var(--kaauh-danger) !important; color: white !important;}
/* ------------------ Detail Row & Content Styles (Made Smaller) ------------------ */
.detail-section h2, .card h2 {
color: var(--kaauh-teal-dark);
font-weight: 700;
font-size: 1.25rem; /* Reduced size */
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--kaauh-border);
}
.detail-row-simple {
display: flex;
padding: 0.4rem 0; /* Reduced vertical padding */
border-bottom: 1px dashed var(--kaauh-border);
font-size: 0.85rem; /* Smaller text */
}
.detail-label-simple {
font-weight: 600;
color: var(--kaauh-teal-dark);
flex-basis: 40%;
}
.detail-value-simple {
color: var(--kaauh-primary-text);
font-weight: 500;
flex-basis: 60%;
}
/* ------------------ Join Info & Copy Button ------------------ */
.btn-primary-teal {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
padding: 0.6rem 1.2rem;
font-size: 0.95rem; /* Slightly smaller button */
border-radius: 6px;
color: white; /* Ensure text color is white for teal primary */
}
.btn-primary-teal:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
}
/* Added Danger Button Style for main delete */
.btn-danger-red {
background-color: var(--kaauh-danger);
border-color: var(--kaauh-danger);
color: white;
padding: 0.6rem 1.2rem;
font-size: 0.95rem;
border-radius: 6px;
font-weight: 600;
}
.btn-danger-red:hover {
background-color: #c82333;
border-color: #bd2130;
}
.btn-secondary-back {
/* Subtle Back Button */
background-color: transparent;
border: none;
color: var(--kaauh-secondary-text);
font-weight: 600;
font-size: 1rem;
padding: 0.5rem 0.75rem;
transition: color 0.2s;
}
.btn-secondary-back:hover {
color: var(--kaauh-teal);
text-decoration: underline;
}
.join-url-display {
background-color: white;
border: 1px solid var(--kaauh-border);
padding: 0.5rem; /* Reduced padding */
font-size: 0.85rem; /* Smaller text */
}
.btn-copy-simple {
padding: 0.5rem 0.75rem;
background-color: var(--kaauh-teal-dark);
border: none;
color: white;
border-radius: 4px;
}
.btn-copy-simple:hover {
background-color: var(--kaauh-teal);
}
/* ------------------ Simple Table Styles ------------------ */
.simple-table {
width: 100%;
margin-top: 0.5rem;
border-collapse: collapse;
}
.simple-table th {
background-color: var(--kaauh-teal-light);
color: var(--kaauh-teal-dark);
font-weight: 700;
padding: 8px 12px; /* Reduced padding */
border: 1px solid var(--kaauh-border);
font-size: 0.8rem; /* Smaller table header text */
}
.simple-table td {
padding: 8px 12px; /* Reduced padding */
border: 1px solid var(--kaauh-border);
background-color: white;
font-size: 0.85rem; /* Smaller table body text */
}
/* ------------------ Comment Specific Styles ------------------ */
.comment-item {
border: 1px solid var(--kaauh-border);
background-color: var(--kaauh-gray-light);
border-radius: 6px;
}
/* Style for in-page edit button */
.btn-edit-comment {
background-color: transparent;
border: 1px solid var(--kaauh-teal);
color: var(--kaauh-teal);
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border-radius: 4px;
font-weight: 500;
}
.btn-edit-comment:hover {
background-color: var(--kaauh-teal-light);
}
</style>
{% endblock %}
{% block content %}
{% comment %}
NOTE: The variable 'meeting' has been renamed to 'interview' (ScheduledInterview)
NOTE: The variable 'meeting.slug' has been renamed to 'interview.slug'
NOTE: All 'meeting' URL names (update_meeting, delete_meeting, etc.) have been renamed
{% endcomment %}
<div class="container-fluid py-4">
{# --- TOP BAR / BACK BUTTON & ACTIONS (EDIT/DELETE) --- #}
<div class="d-flex justify-content-between align-items-center mb-4">
{# Back Button #}
<a href="{% url 'interview_list' %}" class="btn btn-secondary-back">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Interviews" %}
</a>
{# Edit and Delete Buttons #}
<div class="d-flex gap-2">
<a href="{% url 'update_scheduled_interview' interview.slug %}" class="btn btn-primary-teal btn-sm">
<i class="fas fa-edit me-1"></i> {% trans "Edit Interview" %}
</a>
{# DELETE MEETING FORM #}
<form method="post" action="{% url 'delete_scheduled_interview' interview.slug %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="btn btn-danger-red btn-sm" onclick="return confirm('{% trans "Are you sure you want to delete this interview? This action is permanent." %}')">
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete Interview" %}
</button>
</form>
</div>
</div>
{# ========================================================= #}
{# --- MAIN TITLE AT TOP --- #}
{# ========================================================= #}
{% with zoom_details=interview.zoom_details.0 %}
<div class="main-title-container mb-4">
<h1 class="text-start" style="color: var(--kaauh-teal-dark);">
{% if interview.schedule.interview_type == 'Remote' %}
<i class="fas fa-video me-2" style="color: var(--kaauh-teal);"></i>
{{ zoom_details.topic|default:"[Remote Interview]" }}
{% else %}
<i class="fas fa-building me-2" style="color: var(--kaauh-teal);"></i>
{{ interview.schedule.location|default:"[Onsite Interview]" }}
{% endif %}
<span class="status-badge bg-{{ interview.status|lower|default:'bg-secondary' }} ms-3">
{{ interview.status|title|default:'N/A' }} ({{ interview.schedule.interview_type }})
</span>
</h1>
</div>
{# ========================================================= #}
{# --- SECTION 1: INTERVIEW & CONNECTION/LOCATION CARDS SIDE BY SIDE --- #}
{# ========================================================= #}
<div class="row g-4 mb-5 align-items-stretch">
{# --- LEFT HALF: INTERVIEW DETAIL CARD --- #}
<div class="col-lg-6">
<div class="p-3 bg-white rounded shadow-sm h-100 d-flex flex-column">
<h2 class="text-start"><i class="fas fa-briefcase me-2"></i> {% trans "Candidate & Job" %}</h2>
<div class="detail-row-group flex-grow-1">
{# NOTE: Assuming ScheduledInterview has direct relations to candidate and job #}
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Job Title" %}:</div><div class="detail-value-simple"><a class="text-decoration-none text-dark" href="{% url 'job_detail' interview.job.slug %}">{{ interview.job.title|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Name" %}:</div><div class="detail-value-simple"><a class="text-decoration-none text-dark" href="{% url 'candidate_detail' interview.candidate.slug %}">{{ interview.candidate.name|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Email" %}:</div><div class="detail-value-simple"><a class="text-decoration-none text-dark" href="{% url 'candidate_detail' interview.candidate.slug %}">{{ interview.candidate.email|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Job Type" %}:</div><div class="detail-value-simple">{{ interview.job.job_type|default:"N/A" }}</div></div>
{% if interview.candidate.belong_to_agency %}
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Agency" %}:</div><div class="detail-value-simple"><a href="">{{ interview.candidate.hiring_agency.name|default:"N/A" }}</a></div></div>
{% endif %}
</div>
</div>
</div>
{# --- RIGHT HALF: CONNECTION/LOCATION DETAILS CARD --- #}
<div class="col-lg-6">
<div class="p-3 bg-white rounded shadow-sm h-100 d-flex flex-column">
<h2 class="text-start"><i class="fas fa-map-marker-alt me-2"></i> {% trans "Time & Location" %}</h2>
<div class="detail-row-group flex-grow-1">
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Date & Time" %}:</div><div class="detail-value-simple">{{ interview.interview_date|date:"M d, Y"|default:"N/A" }} @ {{ interview.interview_time|time:"H:i"|default:"N/A" }}</div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Duration" %}:</div><div class="detail-value-simple">{{ interview.schedule.interview_duration|default:"N/A" }} {% trans "minutes" %}</div></div>
{% if interview.schedule.interview_type == 'Onsite' %}
{# --- Onsite Details --- #}
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Location" %}:</div><div class="detail-value-simple">{{ interview.schedule.location|default:"TBD" }}</div></div>
{% elif interview.schedule.interview_type == 'Remote' and zoom_details %}
{# --- Remote/Zoom Details --- #}
<h3 class="mt-3" style="font-size: 1.05rem; color: var(--kaauh-teal); font-weight: 600;">{% trans "Remote Details" %}</h3>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Meeting ID" %}:</div><div class="detail-value-simple">{{ zoom_details.meeting_id|default:"N/A" }}</div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Host Email" %}:</div><div class="detail-value-simple">{{ zoom_details.host_email|default:"N/A" }}</div></div>
{% if zoom_details.join_url %}
<div class="join-url-container pt-3">
<div id="copy-message" class="text-white rounded px-2 py-1 small fw-bold mb-2 text-center" style="opacity: 0; transition: opacity 0.3s; position: absolute; right: 0; top: 5px; background-color: var(--kaauh-success); z-index: 10;">{% trans "Copied!" %}</div>
<div class="join-url-display d-flex justify-content-between align-items-center position-relative">
<div class="text-truncate me-2">
<strong>{% trans "Join URL" %}:</strong>
<span id="meeting-join-url">{{ zoom_details.join_url }}</span>
</div>
<button class="btn-copy-simple ms-2 flex-shrink-0" onclick="copyLink()" title="{% trans 'Copy URL' %}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
{% endif %}
{% else %}
<p class="text-muted">{% trans "Location/Connection details are not available for this interview type." %}</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endwith %}
{# ========================================================= #}
{# --- SECTION 2: PERSONNEL TABLES --- #}
{# ========================================================= #}
<div class="row g-4 mt-1 mb-5">
{# --- PARTICIPANTS TABLE --- #}
<div class="col-lg-12">
<div class="p-3 bg-white rounded shadow-sm">
<div class="d-flex justify-content-between align-item-center" >
<h2 class="text-start"><i class="fas fa-users-cog me-2"></i> {% trans "Assigned Participants" %}</h2>
<div class="d-flex justify-content-center align-item-center">
<button type="button" class="btn btn-primary-teal btn-sm me-2"
data-bs-toggle="modal"
data-bs-target="#assignParticipants">
<i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %} ({{ interview.participants.count|add:interview.system_users.count }})
</button>
<button type="button" class="btn btn-outline-info"
data-bs-toggle="modal"
title="Send Interview Emails"
data-bs-target="#emailModal">
<i class="fas fa-envelope"></i>
</button>
</div>
</div>
<table class="simple-table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Role/Designation" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Phone Number" %}</th>
<th>{% trans "Source Type" %}</th>
</tr>
</thead>
<tbody>
{# External Participants #}
{% for participant in interview.participants.all %}
<tr>
<td>{{participant.name}}</td>
<td>{{participant.designation}}</td>
<td>{{participant.email}}</td>
<td>{{participant.phone}}</td>
<td>{% trans "External Participants" %}</td>
</tr>
{% endfor %}
{# System Users (Internal Participants) #}
{% for user in interview.system_users.all %}
<tr>
<td>{{user.get_full_name}}</td>
<td>{% trans "System User" %}</td>
<td>{{user.email}}</td>
<td>{{user.phone}}</td>
<td>{% trans "System User" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# ========================================================= #}
{# --- SECTION 3: COMMENTS (CORRECTED) --- #}
{# ========================================================= #}
<div class="row g-4 mt-1">
<div class="col-lg-12">
<div class="card" id="comments-card" style="height: 100%;">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-comments me-2"></i>
{% trans "Comments" %} ({% if interview.comments %}{{ interview.comments.count }}{% else %}0{% endif %})
</h5>
</div>
<div class="card-body overflow-auto">
{# 1. COMMENT DISPLAY & IN-PAGE EDIT FORMS #}
<div id="comment-section" class="mb-4">
{# NOTE: Assuming comment model has a ForeignKey to ScheduledInterview called 'interview' #}
{% if interview.comments.all %}
{% for comment in interview.comments.all|dictsortreversed:"created_at" %}
<div class="comment-item mb-3 p-3">
{# Read-Only Comment View #}
<div id="comment-view-{{ comment.pk }}">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="comment-metadata" style="font-size: 0.9rem;">
<strong>{{ comment.author.get_full_name|default:comment.author.username }}</strong>
<span class="text-muted small ms-2">{{ comment.created_at|date:"M d, Y H:i" }}</span>
</div>
{% if comment.author == user or user.is_staff %}
<div class="comment-actions d-flex align-items-center gap-1">
{# Edit Button: Toggles the hidden form #}
<button type="button" class="btn btn-edit-comment py-0 px-1" onclick="toggleCommentEdit('{{ comment.pk }}')" id="edit-btn-{{ comment.pk }}" title="{% trans 'Edit Comment' %}">
<i class="fas fa-edit"></i>
</button>
{# Delete Form: Submits a POST request #}
<form method="post" action="{% url 'delete_meeting_comment' interview.slug comment.pk %}" style="display: inline;" id="delete-form-{{ comment.pk }}">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger py-0 px-1" title="{% trans 'Delete Comment' %}" onclick="return confirm('{% trans "Are you sure you want to delete this comment?" %}')">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
{% endif %}
</div>
<p class="mb-0 comment-content" style="font-size: 0.85rem; white-space: pre-wrap;">{{ comment.content|linebreaksbr }}</p>
</div>
{# Hidden Edit Form #}
<div id="comment-edit-form-{{ comment.pk }}" style="display: none; margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--kaauh-border);">
<form method="POST" action="{% url 'edit_meeting_comment' interview.slug comment.pk %}" id="form-{{ comment.pk }}">
{% csrf_token %}
<div class="mb-2">
<label for="id_content_{{ comment.pk }}" class="form-label small">{% trans "Edit Comment" %}</label>
{# NOTE: The textarea name must match your Comment model field (usually 'content') #}
<textarea name="content" id="id_content_{{ comment.pk }}" rows="3" class="form-control" required>{{ comment.content }}</textarea>
</div>
<button type="submit" class="btn btn-sm btn-success me-2">
<i class="fas fa-save me-1"></i> {% trans "Save Changes" %}
</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleCommentEdit('{{ comment.pk }}')">
{% trans "Cancel" %}
</button>
</form>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
{% endif %}
</div>
<hr>
{# 2. NEW COMMENT SUBMISSION (Remains the same) #}
<h6 class="mb-3" style="color: var(--kaauh-teal-dark);">{% trans "Add a New Comment" %}</h6>
{% if user.is_authenticated %}
<form method="POST" action="{% url 'add_meeting_comment' interview.slug %}">
{% csrf_token %}
{% if comment_form %}
{{ comment_form.as_p }}
{% else %}
<div class="mb-3">
<label for="id_content" class="form-label small">{% trans "Comment" %}</label>
<textarea name="content" id="id_content" rows="3" class="form-control" required></textarea>
</div>
{% endif %}
<button type="submit" class="btn btn-primary-teal btn-sm mt-2">
<i class="fas fa-paper-plane me-1"></i> {% trans "Submit Comment" %}
</button>
</form>
{% else %}
<p class="text-muted small">{% trans "You must be logged in to add a comment." %}</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{# --- MODALS (Updated to use interview.slug) --- #}
<div class="modal fade" id="assignParticipants" tabindex="-1" aria-labelledby="assignParticipantsLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="jobAssignmentLabel">{% trans "Manage all participants" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'create_interview_participants' interview.slug %}">
{% csrf_token %}
<div class="modal-body table-responsive">
{{ interview.name }} {# This might need checking - ScheduledInterview usually doesn't have a 'name' field #}
<hr>
<table class="table tab table-bordered mt-3">
<thead>
<th class="col">👥 {% trans "Participants" %}</th>
<th class="col">🧑‍💼 {% trans "Users" %}</th>
</thead>
<tbody>
<tr>
<td>
{{ form.participants.errors }}
{{ form.participants }}
</td>
<td> {{ form.system_users.errors }}
{{ form.system_users }}
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger-red" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary-teal btn-sm">{% trans "Save" %}</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-light">
<h5 class="modal-title" id="emailModalLabel">📧 {% trans "Compose Interview Invitation" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'send_interview_email' interview.slug %}">
{% csrf_token %}
<div class="modal-body">
<div class="mb-3">
<label for="{{ email_form.subject.id_for_label }}" class="form-label fw-bold">Subject</label>
{{ email_form.subject | add_class:"form-control" }}
</div>
<ul class="nav nav-tabs" id="messageTabs" role="tablist">
{# Candidate/Agency Tab - Active by default #}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="candidate-tab" data-bs-toggle="tab" data-bs-target="#candidate-pane" type="button" role="tab" aria-controls="candidate-pane" aria-selected="true">
{% if interview.candidate.belong_to_an_agency %}
{% trans "Agency Message" %}
{% else %}
{% trans "Candidate Message" %}
{% endif %}
</button>
</li>
{# Participants Tab #}
<li class="nav-item" role="presentation">
<button class="nav-link" id="participants-tab" data-bs-toggle="tab" data-bs-target="#participants-pane" type="button" role="tab" aria-controls="participants-pane" aria-selected="false">
{% trans "Panel Message (Interviewers)" %}
</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-3 bg-light-subtle">
{# --- Candidate/Agency Pane --- #}
<div class="tab-pane fade show active" id="candidate-pane" role="tabpanel" aria-labelledby="candidate-tab">
<p class="text-muted small">{% trans "This email will be sent to the candidate or their hiring agency." %}</p>
{% if not interview.candidate.belong_to_an_agency %}
<div class="form-group">
<label for="{{ email_form.message_for_candidate.id_for_label }}" class="form-label d-none">{% trans "Candidate Message" %}</label>
{{ email_form.message_for_candidate | add_class:"form-control" }}
</div>
{% endif %}
{% if interview.candidate.belong_to_an_agency %}
<div class="form-group">
<label for="{{ email_form.message_for_agency.id_for_label }}" class="form-label d-none">{% trans "Agency Message" %}</label>
{{ email_form.message_for_agency | add_class:"form-control" }}
</div>
{% endif %}
</div>
{# --- Participants Pane --- #}
<div class="tab-pane fade" id="participants-pane" role="tabpanel" aria-labelledby="participants-tab">
<p class="text-muted small">{% trans "This email will be sent to the internal and external interview participants." %}</p>
<div class="form-group">
<label for="{{ email_form.message_for_participants.id_for_label }}" class="form-label d-none">{% trans "Participants Message" %}</label>
{{ email_form.message_for_participants | add_class:"form-control" }}
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger-red" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary-teal">{% trans "Send Invitation" %}</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
// --- COMMENT EDITING FUNCTION ---
function toggleCommentEdit(commentPk) {
const viewDiv = document.getElementById(`comment-view-${commentPk}`);
const editFormDiv = document.getElementById(`comment-edit-form-${commentPk}`);
const editButton = document.getElementById(`edit-btn-${commentPk}`);
const deleteForm = document.getElementById(`delete-form-${commentPk}`);
if (viewDiv.style.display !== 'none') {
// Switch to Edit Mode
viewDiv.style.display = 'none';
editFormDiv.style.display = 'block';
if (editButton) editButton.style.display = 'none'; // Hide edit button
if (deleteForm) deleteForm.style.display = 'none'; // Hide delete button
} else {
// Switch back to View Mode (Cancel)
viewDiv.style.display = 'block';
editFormDiv.style.display = 'none';
if (editButton) editButton.style.display = 'inline-block'; // Show edit button
if (deleteForm) deleteForm.style.display = 'inline'; // Show delete button
}
}
// --- COPY LINK FUNCTION ---
// CopyLink function implementation (slightly improved for message placement)
function copyLink() {
const urlElement = document.getElementById('meeting-join-url');
const displayContainer = urlElement.closest('.join-url-display');
const messageElement = document.getElementById('copy-message');
const textToCopy = urlElement.textContent || urlElement.innerText;
clearTimeout(window.copyMessageTimeout);
function showMessage(success) {
messageElement.textContent = success ? '{% trans "Copied!" %}' : '{% trans "Copy Failed." %}';
messageElement.style.backgroundColor = success ? 'var(--kaauh-success)' : 'var(--kaauh-danger)';
messageElement.style.opacity = '1';
// Position the message relative to the display container
const rect = displayContainer.getBoundingClientRect();
// Note: This positioning logic relies on the .join-url-container being position:relative or position:absolute
messageElement.style.left = (rect.width / 2) - (messageElement.offsetWidth / 2) + 'px';
messageElement.style.top = '-35px';
window.copyMessageTimeout = setTimeout(() => {
messageElement.style.opacity = '0';
}, 2000);
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(textToCopy).then(() => {
showMessage(true);
}).catch(err => {
console.error('Could not copy text: ', err);
fallbackCopyTextToClipboard(textToCopy, showMessage);
});
} else {
fallbackCopyTextToClipboard(textToCopy, showMessage);
}
}
function fallbackCopyTextToClipboard(text, callback) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
let success = false;
try {
success = document.execCommand('copy');
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
}
document.body.removeChild(textArea);
callback(success);
}
</script>
{% endblock %}

View File

@ -0,0 +1,267 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{% trans "Scheduled Interviews List" %} - {{ block.super }}{% endblock %}
{% block customCSS %}
{# (Your existing CSS is kept here, as it is perfect for the theme) #}
<style>
/* ... (Your CSS styles) ... */
</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-calendar-alt me-2"></i> {% trans "Scheduled Interviews" %}
</h1>
{# FIX: Using safe anchor href="#" to prevent the NoReverseMatch crash. #}
{# Replace '#' with {% url 'create_scheduled_interview' %} once the URL name is defined in urls.py #}
<a href="#" class="btn btn-main-action">
<i class="fas fa-plus me-1"></i> {% trans "Schedule Interview" %}
</a>
</div>
<div class="card mb-4 shadow-sm no-hover">
<div class="card-body">
<form method="GET" class="row g-3 align-items-end">
{# Search field #}
<div class="col-md-4">
<label for="q" class="form-label small text-muted">{% trans "Search (Candidate/Job)" %}</label>
<div class="input-group">
<input type="text" class="form-control form-control-sm" id="q" name="q" placeholder="{% trans 'Search...' %}" value="{{ search_query }}">
</div>
</div>
{# Filter by Status #}
<div class="col-md-3">
<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="scheduled" {% if status_filter == 'scheduled' %}selected{% endif %}>{% trans "Scheduled" %}</option>
<option value="confirmed" {% if status_filter == 'confirmed' %}selected{% endif %}>{% trans "Confirmed" %}</option>
<option value="completed" {% if status_filter == 'completed' %}selected{% endif %}>{% trans "Completed" %}</option>
<option value="cancelled" {% if status_filter == 'cancelled' %}selected{% endif %}>{% trans "Cancelled" %}</option>
</select>
</div>
{# Filter by Interview Type (ONSITE/REMOTE) - This list now correctly populated #}
<div class="col-md-3">
<label for="interview_type" class="form-label small text-muted">{% trans "Interview Type" %}</label>
<select name="interview_type" id="interview_type" class="form-select form-select-sm">
<option value="">{% trans "All Types" %}</option>
{% for type_value, type_label in interview_types %}
<option value="{{ type_value }}" {% if type_filter == type_value %}selected{% endif %}>
{{ type_label }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<div class="filter-buttons">
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-filter me-1"></i> {% trans "Apply" %}
</button>
{% if status_filter or search_query or type_filter %}
{# Assuming 'interview_list' is the URL name for this view #}
<a href="{% url 'interview_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Clear" %}
</a>
{% endif %}
</div>
</div>
</form>
</div>
</div>
{{meetings}}
{# Using 'meetings' based on the context_object_name provided #}
{% if meetings %}
<div id="meetings-list">
{# View Switcher (kept the name for simplicity) #}
{% include "includes/_list_view_switcher.html" with list_id="meetings-list" %}
{# Card View #}
<div class="card-view active row">
{% for interview 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 'candidate_detail' interview.candidate.slug %}" class="text-decoration-none text-primary-theme">{{ interview.candidate.name }}</a>
</h5>
<span class="status-badge bg-{{ interview.status }}">
{{ interview.status|title }}
</span>
</div>
<p class="card-text text-muted small mb-3">
<i class="fas fa-briefcase"></i> {% trans "Job" %}:
<a class="text-secondary text-decoration-none" href="{% url 'job_detail' interview.job.slug %}">{{ interview.job.title }}</a><br>
{# --- Remote/Onsite Logic - Handles both cases safely --- #}
<i class="fas {% if interview.schedule.interview_type == 'Remote' %}fa-globe{% else %}fa-map-marker-alt{% endif %}"></i>
{% trans "Type" %}: {{ interview.schedule.get_interview_type_display }}
{% if interview.schedule.interview_type == 'Remote' %}<br>
{# CRITICAL FIX: Safe access to zoom_meeting details #}
<i class="fas fa-hashtag"></i> {% trans "Zoom ID" %}: {{ interview.zoom_meeting.meeting_id|default:"N/A" }}
{% else %}<br>
<i class="fas fa-building"></i> {% trans "Location" %}: {{ interview.schedule.location }}
{% endif %}<br>
<i class="fas fa-clock"></i> {% trans "Date" %}: {{ interview.interview_date|date:"M d, Y" }}<br>
<i class="fas fa-clock"></i> {% trans "Time" %}: {{ interview.interview_time|time:"H:i" }}<br>
<i class="fas fa-stopwatch"></i> {% trans "Duration" %}: {{ interview.schedule.interview_duration }} minutes
</p>
<div class="mt-auto pt-2 border-top">
<div class="d-flex gap-2">
<a href="{% url 'scheduled_interview_detail' interview.slug %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye"></i> {% trans "View" %}
</a>
{# CRITICAL FIX: Safe access to join URL #}
{% if interview.schedule.interview_type == 'Remote' and interview.zoom_meeting and interview.zoom_meeting.join_url %}
<a href="{{ interview.zoom_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_scheduled_interview' interview.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_scheduled_interview' interview.slug %}"
hx-target="#deleteModalBody"
hx-swap="outerHTML"
data-item-name="{{ interview.candidate.name }} Interview">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{# Table View (Logic is identical, safe access applied) #}
<div class="table-view">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{% trans "Candidate" %}</th>
<th scope="col">{% trans "Job" %}</th>
<th scope="col">{% trans "Type" %}</th>
<th scope="col">{% trans "Date/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 interview in meetings %}
<tr>
<td>
<strong class="text-primary-theme">
<a href="{% url 'candidate_detail' interview.candidate.slug %}" class="text-decoration-none text-primary-theme">{{ interview.candidate.name }}</a>
</strong>
</td>
<td>
<a class="text-secondary text-decoration-none" href="{% url 'job_detail' interview.job.slug %}">{{ interview.job.title }}</a>
</td>
<td>
{{ interview.schedule.get_interview_type_display }}
</td>
<td>{{ interview.interview_date|date:"M d, Y" }} <br>({{ interview.interview_time|time:"H:i" }})</td>
<td>{{ interview.schedule.interview_duration }} min</td>
<td>
<span class="badge bg-{{ interview.status }}">
{% if interview.status == 'confirmed' %}
<i class="fas fa-circle me-1 text-white"></i>
{% endif %}
{{ interview.status|title }}
</span>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
{# CRITICAL FIX: Safe access to join URL #}
{% if interview.schedule.interview_type == 'Remote' and interview.zoom_meeting and interview.zoom_meeting.join_url %}
<a href="{{ interview.zoom_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 'scheduled_interview_detail' interview.slug %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'update_scheduled_interview' interview.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_scheduled_interview' interview.slug %}"
hx-target="#meetingModalBody"
hx-swap="outerHTML"
data-item-name="{{ interview.candidate.name }} Interview">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# Pagination #}
{% 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 %}{% if type_filter %}&interview_type={{ type_filter }}{% 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 %}{% if type_filter %}&interview_type={{ type_filter }}{% 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 %}{% if type_filter %}&interview_type={{ type_filter }}{% 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 %}{% if type_filter %}&interview_type={{ type_filter }}{% 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-calendar-alt fa-3x mb-3" style="color: var(--kaauh-teal-dark);"></i>
<h3>{% trans "No Interviews found" %}</h3>
<p class="text-muted">{% trans "Schedule your first interview or adjust your filters." %}</p>
{# FIX: Using safe anchor href="#" to prevent the NoReverseMatch crash. #}
<a href="#" class="btn btn-main-action mt-3">
<i class="fas fa-plus me-1"></i> {% trans "Schedule an Interview" %}
</a>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,432 +0,0 @@
{% 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 %}

View File

@ -321,6 +321,9 @@
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action">
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applicants" %}
</a>
<a href="{% url 'job_cvs_download' job.slug %}" class="btn btn-main-action">
<i class="fa-solid fa-download me-1"></i> {% trans "Download All CVs" %}
</a>
</div>
</div>

View File

@ -229,7 +229,7 @@
<td>{{ participant.created_at|date:"d-m-Y" }}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'participants_detail' participant.slug%}" class="btn btn-outline-primary" title="{% trans 'View' %}">
<a href="{% url 'participants_detail' participant.slug%}" class="btn btn-outline-secondary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i>
</a>
{% if user.is_staff %}

View File

@ -196,7 +196,7 @@
<tr>
<td class="fw-medium">
<a href="{% url 'agency_detail' agency.slug %}"
class="text-decoration-none text-primary-theme">
class="text-decoration-none text-secondary">
{{ agency.name }}
</a>
</td>

View File

@ -320,7 +320,7 @@
<td>{{ candidate.created_at|date:"d-m-Y" }}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-secondary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i>
</a>
{% if user.is_staff %}