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, ZoomMeeting, Candidate,TrainingMaterial,JobPosting,
FormTemplate,InterviewSchedule,BreakTime,JobPostingImage, FormTemplate,InterviewSchedule,BreakTime,JobPostingImage,
Profile,MeetingComment,ScheduledInterview,Source,HiringAgency, Profile,MeetingComment,ScheduledInterview,Source,HiringAgency,
AgencyJobAssignment, AgencyAccessLink,Participants AgencyJobAssignment, AgencyAccessLink,Participants,OnsiteMeeting
) )
# from django_summernote.widgets import SummernoteWidget # from django_summernote.widgets import SummernoteWidget
from django_ckeditor_5.widgets import CKEditor5Widget from django_ckeditor_5.widgets import CKEditor5Widget
@ -1594,24 +1594,29 @@ KAAUH HIRING TEAM
class InterviewScheduleLocationForm(forms.ModelForm): # class OnsiteLocationForm(forms.ModelForm):
class Meta: # class Meta:
model=InterviewSchedule # model=
fields=['location'] # fields=['location']
widgets={ # widgets={
'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}), # '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): def __str__(self):
return self.title 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 ZoomMeeting(Base):
class MeetingStatus(models.TextChoices): class MeetingStatus(models.TextChoices):
@ -1613,7 +1634,6 @@ class InterviewSchedule(Base):
verbose_name="Interview Meeting Type" verbose_name="Interview Meeting Type"
) )
location=models.CharField(null=True,blank=True,default='Remote')
job = models.ForeignKey( job = models.ForeignKey(
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules", db_index=True 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, ZoomMeeting, on_delete=models.CASCADE, related_name="interview", db_index=True,
null=True, blank=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( schedule = models.ForeignKey(
InterviewSchedule, on_delete=models.CASCADE, related_name="interviews",null=True,blank=True, db_index=True 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>/update/', views.edit_job, name='job_update'),
# path('jobs/<slug:slug>/delete/', views., name='job_delete'), # path('jobs/<slug:slug>/delete/', views., name='job_delete'),
path('jobs/<slug:slug>/', views.job_detail, name='job_detail'), 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'), 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('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/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/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 json
import io
import zipfile
from django.core.paginator import Paginator
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required 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.functions import Cast, Coalesce, TruncDate
from django.db.models.fields.json import KeyTextTransform from django.db.models.fields.json import KeyTextTransform
from django.db.models.expressions import ExpressionWrapper from django.db.models.expressions import ExpressionWrapper
from django.urls import reverse_lazy
from django.db.models import Count, Avg, F,Q from django.db.models import Count, Avg, F,Q
from .forms import ( from .forms import (
CandidateExamDateForm, CandidateExamDateForm,
@ -44,7 +48,7 @@ from .forms import (
CandidateEmailForm, CandidateEmailForm,
SourceForm, SourceForm,
InterviewEmailForm, InterviewEmailForm,
InterviewScheduleLocationForm
) )
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
from rest_framework import viewsets from rest_framework import viewsets
@ -53,7 +57,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from .linkedin_service import LinkedInService from .linkedin_service import LinkedInService
from .serializers import JobPostingSerializer, CandidateSerializer from .serializers import JobPostingSerializer, CandidateSerializer
from django.shortcuts import get_object_or_404, render, redirect 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 ( from .utils import (
create_zoom_meeting, create_zoom_meeting,
delete_zoom_meeting, delete_zoom_meeting,
@ -194,20 +198,17 @@ class ZoomMeetingListView(LoginRequiredMixin, ListView):
context["candidate_name_filter"] = self.request.GET.get("candidate_name", "") context["candidate_name_filter"] = self.request.GET.get("candidate_name", "")
return context return context
@login_required # @login_required
def InterviewListView(request): # def InterviewListView(request):
interview_type=request.GET.get('interview_type','Remote') # # interview_type=request.GET.get('interview_type','Remote')
print(interview_type) # # print(interview_type)
if interview_type=='Onsite': # interview_type='Onsite'
meetings=ScheduledInterview.objects.filter(schedule__interview_type=interview_type) # meetings=ScheduledInterview.objects.filter(schedule__interview_type=interview_type)
else: # return render(request, "meetings/list_meetings.html",{
meetings=ZoomMeeting.objects.all() # 'meetings':meetings,
print(meetings) # })
return render(request, "meetings/list_meetings.html",{
'meetings':meetings,
'current_interview_type':interview_type
})
# search_query = request.GET.get("q", "") # Renamed from 'search' to 'q' for consistency # search_query = request.GET.get("q", "") # Renamed from 'search' to 'q' for consistency
# if search_query: # if search_query:
# interviews = interviews.filter( # interviews = interviews.filter(
@ -239,7 +240,10 @@ class ZoomMeetingDetailsView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context=super().get_context_data(**kwargs) context=super().get_context_data(**kwargs)
meeting = self.object meeting = self.object
interview=meeting.interview try:
interview=meeting.interview
except Exception as e:
print(e)
candidate = interview.candidate candidate = interview.candidate
job=meeting.get_job job=meeting.get_job
@ -402,10 +406,13 @@ def edit_job(request, slug):
SCORE_PATH = 'ai_analysis_data__analysis_data__match_score' SCORE_PATH = 'ai_analysis_data__analysis_data__match_score'
HIGH_POTENTIAL_THRESHOLD=75 HIGH_POTENTIAL_THRESHOLD=75
from django.contrib.sites.shortcuts import get_current_site
@login_required @login_required
def job_detail(request, slug): def job_detail(request, slug):
"""View details of a specific job""" """View details of a specific job"""
job = get_object_or_404(JobPosting, slug=slug) 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 # Get all candidates for this job, ordered by most recent
applicants = job.candidates.all().order_by("-created_at") 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) 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 @login_required
def job_image_upload(request, slug): def job_image_upload(request, slug):
#only for handling the post request #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): def kaauh_career(request):
@ -621,8 +696,48 @@ def kaauh_career(request):
status='ACTIVE', status='ACTIVE',
form_template__is_active=True 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: # job detail facing the candidate:
def application_detail(request, slug): def application_detail(request, slug):
@ -4131,18 +4246,19 @@ def send_interview_email(request, slug):
def schedule_interview_location_form(request,slug): # def schedule_interview_location_form(request,slug):
schedule=get_object_or_404(InterviewSchedule,slug=slug) # schedule=get_object_or_404(InterviewSchedule,slug=slug)
if request.method=='POST': # if request.method=='POST':
form=InterviewScheduleLocationForm(request.POST,instance=schedule) # form=InterviewScheduleLocationForm(request.POST,instance=schedule)
form.save() # form.save()
return redirect('list_meetings') # return redirect('list_meetings')
else: # else:
form=InterviewScheduleLocationForm(instance=schedule) # form=InterviewScheduleLocationForm(instance=schedule)
return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule}) # return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule})
def onsite_interview_list_view(request): def onsite_interview_list_view(request):
onsite_interviews=ScheduledInterview.objects.filter(schedule__interview_type='Onsite') onsite_interviews=ScheduledInterview.objects.filter(schedule__interview_type='Onsite')
return render(request,'interviews/onsite_interview_list.html',{'onsite_interviews':onsite_interviews}) 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 %} {% load static i18n %}
{% block content %} {% 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"> {# 🚀 TOP NAV BAR (Sticky and Themed) #}
<span class="navbar-text text-white fw-bold">{% trans "Job Overview" %}</span> {# ------------------------------------------------ #}
<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> </div>
</nav> </nav>
{# ------------------------------------------------ #}
{# 🔔 DJANGO MESSAGES (Refined placement and styling) #}
{# ------------------------------------------------ #}
{# ================================================= #} {# ------------------------------------------------ #}
{# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #} {# 💻 MAIN CONTENT CONTAINER #}
{# ================================================= #} {# ------------------------------------------------ #}
{% if messages %} <div class="container mt-4 mb-5">
<div class="container-fluid message-container"> <div class="row g-4 main-content-area">
<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">
<div class="col-lg-4 order-lg-2 order-1 d-none d-lg-block"> {# 📌 RIGHT COLUMN: Sticky Apply Card (Desktop Only) #}
<div class="card shadow-sm sticky-top"> <div class="col-lg-4 order-lg-2 d-none d-lg-block">
<div class="card-header bg-kaauh-teal-dark bg-white p-3"> <div class="card shadow-lg border-0" style="position: sticky; top: 70px;">
<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> <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>
<div class="card-body text-center"> <div class="card-body text-center p-4">
<p class="text-muted">{% trans "Review the job details, then apply below." %}</p> <p class="text-muted small mb-3">{% trans "Review the full job details below before submitting your application." %}</p>
{% if job.form_template %} {% 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" %} <i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a> </a>
{% elif not job.is_expired %}
<p class="text-danger fw-bold">{% trans "Application form is unavailable." %}</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-8 order-lg-1 order-2"> {# 📝 LEFT COLUMN: Job Details #}
<div class="card shadow-sm"> <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"> {# Job Title Header #}
<h1 class="h3 mb-0 fw-bold" style="color: var(--kaauh-teal);">{{ job.title }}</h1> <header class="card-header bg-white border-bottom p-4">
</div> <h1 class="h2 mb-0 fw-bolder text-kaauh-teal">{{ job.title }}</h1>
</header>
<div class="card-body p-4"> <div class="card-body p-4">
<h4 class="mb-3 fw-bold" style="color: var(--kaauh-teal-dark);">{% trans "Job Overview" %}</h4> <h4 class="mb-4 fw-bold text-muted border-bottom pb-2">{% trans "Summary" %}</h4>
<div class="row row-cols-1 row-cols-md-2 g-3 mb-5 small text-secondary border p-3 rounded">
{# 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 %} {% if job.salary_range %}
<div class="col"> <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> <strong>{% trans "Salary:" %}</strong>
<span class="fw-bold text-success">{{ job.salary_range }}</span> <span class="fw-bold text-success">{{ job.salary_range }}</span>
</div> </div>
{% endif %} {% endif %}
{# DEADLINE #}
<div class="col"> <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> <strong>{% trans "Deadline:" %}</strong>
{% if job.application_deadline %} {% 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 %} {% if job.is_expired %}
<span class="badge bg-danger ms-2">{% trans "EXPIRED" %}</span> <span class="badge bg-danger ms-2">{% trans "EXPIRED" %}</span>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="text-muted">{% trans "Not specified" %}</span> <span class="text-muted">{% trans "Ongoing" %}</span>
{% endif %} {% endif %}
</div> </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> {# JOB TYPE #}
<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">
<div class="col"> <i class="fas fa-building text-muted me-2"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }} </div> <i class="fas fa-briefcase text-muted me-2 fa-fw"></i>
<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> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
<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>
<div class="accordion" id="jobDetailAccordion">
{# 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 %} {% if job.has_description_content %}
<div class="accordion-item"> <div class="accordion-item border-top border-bottom">
<h2 class="accordion-header" id="headingOne"> <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"> <button class="accordion-button fw-bold fs-5 text-kaauh-teal-dark" type="button"
<i class="fas fa-info-circle me-3"></i> {% trans "Job Description" %} 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> </button>
</h2> </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"> <div class="accordion-body text-secondary p-4">
{{ job.description|safe }} <div class="wysiwyg-content">{{ job.description|safe }}</div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{# QUALIFICATIONS #}
{% if job.has_qualifications_content %} {% if job.has_qualifications_content %}
<div class="accordion-item"> <div class="accordion-item border-bottom">
<h2 class="accordion-header" id="headingTwo"> <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"> <button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
<i class="fas fa-graduation-cap me-3"></i> {% trans "Qualifications" %} 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> </button>
</h2> </h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#jobDetailAccordion"> <div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4"> <div class="accordion-body text-secondary p-4">
{{ job.qualifications|safe }} <div class="wysiwyg-content">{{ job.qualifications|safe }}</div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{# BENEFITS #}
{% if job.has_benefits_content %} {% if job.has_benefits_content %}
<div class="accordion-item"> <div class="accordion-item border-bottom">
<h2 class="accordion-header" id="headingThree"> <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"> <button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
<i class="fas fa-hand-holding-usd me-3"></i> {% trans "Benefits" %} 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> </button>
</h2> </h2>
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-bs-parent="#jobDetailAccordion"> <div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4"> <div class="accordion-body text-secondary p-4">
{{ job.benefits|safe }} <div class="wysiwyg-content">{{ job.benefits|safe }}</div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{# APPLICATION INSTRUCTIONS #}
{% if job.has_application_instructions_content %} {% if job.has_application_instructions_content %}
<div class="accordion-item"> <div class="accordion-item border-bottom">
<h2 class="accordion-header" id="headingFour"> <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"> <button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
<i class="fas fa-file-alt me-3"></i> {% trans "Application Instructions" %} 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> </button>
</h2> </h2>
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour" data-bs-parent="#jobDetailAccordion"> <div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4"> <div class="accordion-body text-secondary p-4">
{{ job.application_instructions|safe }} <div class="wysiwyg-content">{{ job.application_instructions|safe }}</div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endwith %}
</div> </div>
</div>
</div> </div>
</article>
</div> </div>
</div> </div>
</div> </div>
<div class="mobile-fixed-apply-bar d-lg-none text-center"> {# 📱 MOBILE FIXED APPLY BAR (Replaced inline style with utility classes) #}
{% if job.form_template %} {% 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"> <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" %} <i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a> </a>
{% endif %} </footer>
</div> {% endif %}
{% endblock content%} {% 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 %} {% load static i18n %}
{# Use a dynamic title for better SEO and user context #}
{% block title %}{% trans "My Profile" %} - {{ block.super }}{% endblock %} {% block title %}{% trans "Career Opportunities" %} | KAAUH - {{ block.super }}{% endblock %}
{% block content %} {% block content %}
<div class="main-content-area"> <div class="main-content-area">
<header class="hero-section"> {# ------------------------------------------------ #}
<div class="container"> {# 🌟 HERO SECTION (High Visual Impact) #}
<div class="row"> {# ------------------------------------------------ #}
<div class="col-lg-12 col-xl-10"> <header class="hero-section text-white py-5 py-lg-6" style=" background-size: cover; background-position: center;">
<h1 class="hero-title mb-4"> {# Overlay for readability, assuming custom CSS defines .hero-overlay #}
{% translate "Your Career in Health & Academia starts here." %} <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> </h1>
<p class="lead mb-5"> <p class="lead mb-5 fs-5 animate__animated animate__fadeInUp">
{% translate "Join KAAUH, a national leader in patient care, research, and education. We are building the future of healthcare." %} {% trans "Join KAAUH, a national leader in patient care, research, and education. We are building the future of healthcare." %}
</p> </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>
<a href="https://kaauh.edu.sa/about-us" class="btn btn-outline-light rounded-pill px-4 btn-lg"> {# Secondary Action #}
{% translate "About US" %} <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> </a>
</div> </div>
</div> </div>
@ -29,128 +39,236 @@
</header> </header>
{# ------------------------------------------------ #}
{# 💻 JOB LISTING SECTION #}
<section class="py-5 job-listing-section"> {# ------------------------------------------------ #}
<section class="py-5 job-listing-section" id="job-list-start">
<div class="container"> <div class="container">
<div class="row g-5"> <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"> 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> </button>
<div class="collapse d-lg-block filter-sidebar-collapse" id="filterSidebar"> <div class="collapse d-lg-block filter-sidebar-collapse" id="filterSidebar">
<div class="card sticky-top-filters p-4 bg-white"> {# Sticky top ensures filters remain visible while scrolling results #}
<h4 class="fw-bold mb-4 text-primary-theme"> <div class="card border-0 shadow-sm sticky-top-filters p-4 bg-light-subtle" style="top: 20px;">
{% translate "Refine Your Search" %}
</h4>
<div class="d-grid gap-3"> <h4 class="fw-bold mb-4 text-dark border-bottom pb-2">
<select class="form-select" aria-label="Department filter"> <i class="fas fa-search me-2 text-primary-theme"></i>{% trans "Refine Your Search" %}
<option selected>{% translate "Department (Faculty/Admin)" %}</option> </h4>
<option value="1">{% translate "Clinical Services" %}</option> <form method="GET" action="{% url 'kaauh_career'%}" class="d-grid gap-3">
<option value="2">{% translate "Research Labs" %}</option> {# NOTE: Replace select with Django form fields for real functionality #}
<option value="3">{% translate "Training & Education" %}</option>
</select>
<select class="form-select" aria-label="Employment Type filter">
<option selected>{% translate "Employment Type" %}</option>
<option value="1">{% translate "Full-Time" %}</option> <select class="form-select form-select-lg" name="employment_type" aria-label="Employment Type filter" >
<option value="2">{% translate "Part-Time" %}</option> <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>
<select class="form-select" aria-label="Specialty filter"> <select class="form-select form-select-lg" name="workplace_type" aria-label="Workplace Type filter" >
<option selected>{% translate "Specialty / Focus" %}</option> <option value="" {% if not selected_workplace_type %}selected{% endif %}>{% trans "Workplace Type" %}</option>
<option value="1">{% translate "Women's Health" %}</option>
<option value="2">{% translate "Child Growth & Dev" %}</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> </select>
<button class="btn btn-main-action rounded-pill mt-3">{% translate "Apply Filters" %}</button> <select class="form-select form-select-lg" name="department" aria-label="Department Type filter" >
</div> <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>
</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"> <div class="d-flex flex-wrap justify-content-between align-items-center">
{# Dynamic Count #}
<h3 class="fw-bold mb-0 text-dark fs-5"> <h3 class="fw-bold mb-0 text-dark fs-5">
{% comment %} <span class="text-primary-theme">{{ total_open_roles|default:"0" }}</span> {% trans "Open Roles" %}
Assuming a 'job_count' context variable exists, otherwise
this remains static as in the original template.
{% endcomment %}
{% translate "Showing 37 Open Roles" %}
</h3> </h3>
<div class="d-flex flex-wrap gap-2">
<span class="filter-chip"> {# Active Filter Chips (Use a dedicated class for styling) #}
{% translate "Specialty: Women's Health" %} <div class="d-flex flex-wrap gap-2 pt-2 pt-md-0">
<i class="fas fa-times text-xs ms-2 cursor-pointer" role="button" aria-label="Remove filter: Women's Health"></i>
</span> {# --- Active Employment Type Filter Chip --- #}
</div> {% 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>
</div> </div>
{# Job Cards Grid #}
<div class="mt-4 d-grid gap-3"> <div class="mt-4 d-grid gap-3">
{% for job in active_jobs %} {% for job in active_jobs %}
{# The original card structure, now dynamically filled with job data #} {# Optimized Job Listing Card #}
<a href="{% url 'application_detail' job.slug %}" class="card d-block text-decoration-none text-dark job-listing-card bg-white"> <a href="{% url 'application_detail' job.slug %}"
<div class="d-flex justify-content-between align-items-start"> class="card d-block text-decoration-none text-dark job-listing-card p-4 border-2 shadow-hover transition-all">
<h4 class="h5 fw-bold mb-1 text-primary-theme-hover">
<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 }} {{ job.title }}
</h4> </h4>
{# NOTE: You will need to define how job.category or job.tag is determined for the badge logic #} {# Tag Badge (Prominent) #}
<span class="badge bg-primary-theme job-tag"> <span class="badge rounded-pill bg-kaauh-teal job-tag px-3 py-2 fs-6">
{% comment %} Placeholder: Use job.tag or implement conditional logic {% endcomment %}
{% if job.tag_slug == 'clinical' %}{% translate "Clinical" %} <i class="fas fa-tag me-1"></i>{% trans "Apply Before: " %}{{job.application_deadline}}
{% elif job.tag_slug == 'research' %}{% translate "Research/Contract" %}
{% else %}{% translate "General" %}{% endif %}
</span> </span>
</div> </div>
{# NOTE: Assuming job.department and job.location exist in your context #} {# Department/Context (Sub-text) #}
<p class="text-muted small mb-3">{{ job.department }}</p> <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"> {# Job Metadata Icons (Horizontal list for quick scan) #}
<span class="d-flex align-items-center"> <div class="d-flex flex-wrap gap-4 small text-secondary">
<i class="fas fa-map-marker-alt me-2 job-detail-icon"></i>
{{ job.location|default:"Riyadh, KSA" }} <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>
<span class="d-flex align-items-center"> <span class="d-flex align-items-center fw-medium">
<i class="fas fa-user-md me-2 job-detail-icon"></i> <i class="fas fa-user-md me-2 text-primary-theme fa-fw"></i>
{{ job.focus|default:"High Reliability Focus" }} {{ job.workplace_type|default:"" }}
</span> </span>
<span class="d-flex align-items-center"> <span class="d-flex align-items-center fw-medium">
<i class="fas fa-calendar-alt me-2 job-detail-icon"></i> <i class="fas fa-calendar-alt me-2 text-primary-theme fa-fw"></i>
{{ job.employment_type|default:"Full-Time" }} {{ job.job_type|default:"Full-Time" }}
</span> </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> </div>
</a> </a>
{% empty %} {% empty %}
<div class="alert alert-info" role="alert"> <div class="alert alert-info border-0 shadow-sm mt-5" role="alert">
{% translate "We currently have no open roles that match your search. Please check back soon!" %} <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> </div>
{% endfor %} {% endfor %}
{# Load More Button #}
{% if show_load_more %}
<div class="text-center mt-5 mb-3"> <div class="text-center mt-5 mb-3">
<button class="btn btn-main-action btn-lg rounded-pill px-5 shadow-sm"> <button class="btn btn-main-action btn-lg rounded-pill px-5 shadow-lg">
{% translate "Load More Jobs" %} {% trans "Load More Jobs" %} <i class="fas fa-redo ms-2"></i>
</button> </button>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% include "includes/paginator.html" %}
</section> </section>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -49,6 +49,10 @@
.text-primary-theme { color: var(--kaauh-teal) !important; } .text-primary-theme { color: var(--kaauh-teal) !important; }
.text-primary-theme-hover:hover { color: var(--kaauh-teal-dark) !important; } .text-primary-theme-hover:hover { color: var(--kaauh-teal-dark) !important; }
.bg-kaauh-teal {
background-color: #00636e;
}
.btn-main-action { .btn-main-action {
background-color: var(--kaauh-teal); background-color: var(--kaauh-teal);
@ -178,7 +182,7 @@
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand text-dark fw-bold" href="{% url 'kaauh_career' %}"> <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;"> <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> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
@ -205,7 +209,7 @@
<span class="d-inline">{{ LANGUAGE_CODE|upper }}</span> <span class="d-inline">{{ LANGUAGE_CODE|upper }}</span>
</button> </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> <li>
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %} <form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
@ -231,10 +235,35 @@
</div> </div>
</nav> </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 %} {% block content %}
{% endblock content %} {% endblock content %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <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> <i class="fas fa-sign-out-alt me-3 fs-5 " style="color:red;"></i>
<span style="color:red;">{% trans "Sign Out" %}</span> <span style="color:red;">{% trans "Sign Out" %}</span>
</button> </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> </li>
{% endif %} {% endif %}
</ul> </ul>
@ -255,7 +259,7 @@
</span> </span>
</a> </a>
</li> </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' %}"> <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"> <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"> <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" %} {% trans "Onsite Interviews" %}
</span> </span>
</a> </a>
</li> </li> {% endcomment %}
<li class="nav-item me-lg-4"> <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' %}"> <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"> <span class="d-flex align-items-center gap-2">

View File

@ -1,27 +1,61 @@
{% if is_paginated %} {% if page_obj.has_previous or page_obj.has_next %}
<nav aria-label="Page navigation" class="mt-4"> <nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center"> <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 %}
{# 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"> <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> </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 %} {# Current Page Status - Use your teal/custom background class here #}
<li class="page-item"> <li class="page-item active" aria-current="page">
<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> <span class="page-link bg-kaauh-teal text-white">
</li> {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
<li class="page-item"> </span>
<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>
</li>
{% endif %} {% if page_obj.has_next %}
</ul>
</nav> {# Next Page Link #}
{% endif %} <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"> <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" %} <i class="fas fa-layer-group me-1"></i> {% trans "Manage Applicants" %}
</a> </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>
</div> </div>

View File

@ -229,7 +229,7 @@
<td>{{ participant.created_at|date:"d-m-Y" }}</td> <td>{{ participant.created_at|date:"d-m-Y" }}</td>
<td class="text-end"> <td class="text-end">
<div class="btn-group btn-group-sm" role="group"> <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> <i class="fas fa-eye"></i>
</a> </a>
{% if user.is_staff %} {% if user.is_staff %}

View File

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

View File

@ -320,7 +320,7 @@
<td>{{ candidate.created_at|date:"d-m-Y" }}</td> <td>{{ candidate.created_at|date:"d-m-Y" }}</td>
<td class="text-end"> <td class="text-end">
<div class="btn-group btn-group-sm" role="group"> <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> <i class="fas fa-eye"></i>
</a> </a>
{% if user.is_staff %} {% if user.is_staff %}