career page logic
This commit is contained in:
parent
4148c7eb16
commit
0c3f942161
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -11,7 +11,7 @@ from .models import (
|
||||
ZoomMeeting, Candidate,TrainingMaterial,JobPosting,
|
||||
FormTemplate,InterviewSchedule,BreakTime,JobPostingImage,
|
||||
Profile,MeetingComment,ScheduledInterview,Source,HiringAgency,
|
||||
AgencyJobAssignment, AgencyAccessLink,Participants
|
||||
AgencyJobAssignment, AgencyAccessLink,Participants,OnsiteMeeting
|
||||
)
|
||||
# from django_summernote.widgets import SummernoteWidget
|
||||
from django_ckeditor_5.widgets import CKEditor5Widget
|
||||
@ -1594,24 +1594,29 @@ KAAUH HIRING TEAM
|
||||
|
||||
|
||||
|
||||
class InterviewScheduleLocationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model=InterviewSchedule
|
||||
fields=['location']
|
||||
widgets={
|
||||
'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}),
|
||||
}
|
||||
# class OnsiteLocationForm(forms.ModelForm):
|
||||
# class Meta:
|
||||
# model=
|
||||
# fields=['location']
|
||||
# widgets={
|
||||
# 'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}),
|
||||
# }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class OnsiteMeetingForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = OnsiteMeeting
|
||||
fields = ['topic', 'start_time', 'duration', 'timezone', 'location', 'status']
|
||||
widgets = {
|
||||
'topic': forms.TextInput(attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'}),
|
||||
'start_time': forms.DateTimeInput(
|
||||
attrs={'type': 'datetime-local', 'class': 'form-control'}
|
||||
),
|
||||
'duration': forms.NumberInput(
|
||||
attrs={'min': 15, 'placeholder': 'Duration in minutes', 'class': 'form-control'}
|
||||
),
|
||||
'location': forms.TextInput(attrs={'placeholder': 'Physical location', 'class': 'form-control'}),
|
||||
'timezone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'status': forms.Select(attrs={'class': 'form-control'}),
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
45
recruitment/migrations/0013_onsitemeeting_and_more.py
Normal file
45
recruitment/migrations/0013_onsitemeeting_and_more.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
recruitment/migrations/0014_onsitemeeting_status.py
Normal file
18
recruitment/migrations/0014_onsitemeeting_status.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -731,6 +731,27 @@ class TrainingMaterial(Base):
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class OnsiteMeeting(Base):
|
||||
class MeetingStatus(models.TextChoices):
|
||||
WAITING = "waiting", _("Waiting")
|
||||
STARTED = "started", _("Started")
|
||||
ENDED = "ended", _("Ended")
|
||||
CANCELLED = "cancelled",_("Cancelled")
|
||||
# Basic meeting details
|
||||
topic = models.CharField(max_length=255, verbose_name=_("Topic"))
|
||||
start_time = models.DateTimeField(db_index=True, verbose_name=_("Start Time")) # Added index
|
||||
duration = models.PositiveIntegerField(
|
||||
verbose_name=_("Duration")
|
||||
) # Duration in minutes
|
||||
timezone = models.CharField(max_length=50, verbose_name=_("Timezone"))
|
||||
location=models.CharField(null=True,blank=True)
|
||||
status = models.CharField(
|
||||
db_index=True, max_length=20, # Added index
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Status"),
|
||||
default=MeetingStatus.WAITING,
|
||||
)
|
||||
|
||||
class ZoomMeeting(Base):
|
||||
class MeetingStatus(models.TextChoices):
|
||||
@ -1613,7 +1634,6 @@ class InterviewSchedule(Base):
|
||||
verbose_name="Interview Meeting Type"
|
||||
)
|
||||
|
||||
location=models.CharField(null=True,blank=True,default='Remote')
|
||||
|
||||
job = models.ForeignKey(
|
||||
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules", db_index=True
|
||||
@ -1673,6 +1693,11 @@ class ScheduledInterview(Base):
|
||||
ZoomMeeting, on_delete=models.CASCADE, related_name="interview", db_index=True,
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
onsite_meeting= models.OneToOneField(
|
||||
OnsiteMeeting, on_delete=models.CASCADE, related_name="onsite_interview", db_index=True,
|
||||
null=True, blank=True
|
||||
)
|
||||
schedule = models.ForeignKey(
|
||||
InterviewSchedule, on_delete=models.CASCADE, related_name="interviews",null=True,blank=True, db_index=True
|
||||
)
|
||||
|
||||
19
recruitment/templatetags/url_extras.py
Normal file
19
recruitment/templatetags/url_extras.py
Normal 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()
|
||||
@ -14,6 +14,7 @@ urlpatterns = [
|
||||
path('jobs/<slug:slug>/update/', views.edit_job, name='job_update'),
|
||||
# path('jobs/<slug:slug>/delete/', views., name='job_delete'),
|
||||
path('jobs/<slug:slug>/', views.job_detail, name='job_detail'),
|
||||
path('jobs/<slug:slug>/download/cvs/', views.job_cvs_download, name='job_cvs_download'),
|
||||
|
||||
path('careers/',views.kaauh_career,name='kaauh_career'),
|
||||
|
||||
@ -234,8 +235,14 @@ urlpatterns = [
|
||||
path('jobs/<slug:job_slug>/candidates/compose_email/', views.compose_candidate_email, name='compose_candidate_email'),
|
||||
path('interview/partcipants/<slug:slug>/',views.create_interview_participants,name='create_interview_participants'),
|
||||
path('interview/email/<slug:slug>/',views.send_interview_email,name='send_interview_email'),
|
||||
path('interview/schedule/location/<slug:slug>/',views.schedule_interview_location_form,name='schedule_interview_location_form'),
|
||||
path('interview/list',views.InterviewListView,name='interview_list')
|
||||
|
||||
|
||||
|
||||
# # --- SCHEDULED INTERVIEW URLS (New Centralized Management) ---
|
||||
# path('interview/list/', views.InterviewListView.as_view(), name='interview_list'),
|
||||
# path('interviews/<slug:slug>/', views.ScheduledInterviewDetailView.as_view(), name='scheduled_interview_detail'),
|
||||
# path('interviews/<slug:slug>/update/', views.ScheduledInterviewUpdateView.as_view(), name='update_scheduled_interview'),
|
||||
# path('interviews/<slug:slug>/delete/', views.ScheduledInterviewDeleteView.as_view(), name='delete_scheduled_interview'),
|
||||
|
||||
|
||||
]
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import json
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils.translation import gettext as _
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@ -21,6 +24,7 @@ from django.db.models import F, IntegerField, Count, Avg, Sum, Q, ExpressionWrap
|
||||
from django.db.models.functions import Cast, Coalesce, TruncDate
|
||||
from django.db.models.fields.json import KeyTextTransform
|
||||
from django.db.models.expressions import ExpressionWrapper
|
||||
from django.urls import reverse_lazy
|
||||
from django.db.models import Count, Avg, F,Q
|
||||
from .forms import (
|
||||
CandidateExamDateForm,
|
||||
@ -44,7 +48,7 @@ from .forms import (
|
||||
CandidateEmailForm,
|
||||
SourceForm,
|
||||
InterviewEmailForm,
|
||||
InterviewScheduleLocationForm
|
||||
|
||||
)
|
||||
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
|
||||
from rest_framework import viewsets
|
||||
@ -53,7 +57,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from .linkedin_service import LinkedInService
|
||||
from .serializers import JobPostingSerializer, CandidateSerializer
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.views.generic import CreateView, UpdateView, DetailView, ListView
|
||||
from django.views.generic import CreateView, UpdateView, DetailView, ListView,DeleteView
|
||||
from .utils import (
|
||||
create_zoom_meeting,
|
||||
delete_zoom_meeting,
|
||||
@ -194,20 +198,17 @@ class ZoomMeetingListView(LoginRequiredMixin, ListView):
|
||||
context["candidate_name_filter"] = self.request.GET.get("candidate_name", "")
|
||||
return context
|
||||
|
||||
@login_required
|
||||
def InterviewListView(request):
|
||||
interview_type=request.GET.get('interview_type','Remote')
|
||||
print(interview_type)
|
||||
if interview_type=='Onsite':
|
||||
meetings=ScheduledInterview.objects.filter(schedule__interview_type=interview_type)
|
||||
else:
|
||||
meetings=ZoomMeeting.objects.all()
|
||||
print(meetings)
|
||||
return render(request, "meetings/list_meetings.html",{
|
||||
'meetings':meetings,
|
||||
'current_interview_type':interview_type
|
||||
})
|
||||
# @login_required
|
||||
# def InterviewListView(request):
|
||||
# # interview_type=request.GET.get('interview_type','Remote')
|
||||
# # print(interview_type)
|
||||
# interview_type='Onsite'
|
||||
# meetings=ScheduledInterview.objects.filter(schedule__interview_type=interview_type)
|
||||
# return render(request, "meetings/list_meetings.html",{
|
||||
# 'meetings':meetings,
|
||||
# })
|
||||
|
||||
|
||||
# search_query = request.GET.get("q", "") # Renamed from 'search' to 'q' for consistency
|
||||
# if search_query:
|
||||
# interviews = interviews.filter(
|
||||
@ -239,7 +240,10 @@ class ZoomMeetingDetailsView(LoginRequiredMixin, DetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context=super().get_context_data(**kwargs)
|
||||
meeting = self.object
|
||||
interview=meeting.interview
|
||||
try:
|
||||
interview=meeting.interview
|
||||
except Exception as e:
|
||||
print(e)
|
||||
candidate = interview.candidate
|
||||
job=meeting.get_job
|
||||
|
||||
@ -402,10 +406,13 @@ def edit_job(request, slug):
|
||||
|
||||
SCORE_PATH = 'ai_analysis_data__analysis_data__match_score'
|
||||
HIGH_POTENTIAL_THRESHOLD=75
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
@login_required
|
||||
def job_detail(request, slug):
|
||||
"""View details of a specific job"""
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
current_site=get_current_site(request)
|
||||
print(current_site)
|
||||
# Get all candidates for this job, ordered by most recent
|
||||
applicants = job.candidates.all().order_by("-created_at")
|
||||
|
||||
@ -556,6 +563,60 @@ def job_detail(request, slug):
|
||||
}
|
||||
return render(request, "jobs/job_detail.html", context)
|
||||
|
||||
|
||||
|
||||
ALLOWED_EXTENSIONS = ('.pdf', '.docx')
|
||||
|
||||
def job_cvs_download(request,slug):
|
||||
|
||||
job = get_object_or_404(JobPosting,slug=slug)
|
||||
entries=Candidate.objects.filter(job=job)
|
||||
|
||||
|
||||
# 2. Create an in-memory byte stream (BytesIO)
|
||||
zip_buffer = io.BytesIO()
|
||||
|
||||
# 3. Create the ZIP archive
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
|
||||
for entry in entries:
|
||||
# Check if the file field has a file
|
||||
if not entry.resume:
|
||||
continue
|
||||
|
||||
# Get the file name and check extension (case-insensitive)
|
||||
file_name = entry.resume.name.split('/')[-1]
|
||||
file_name_lower = file_name.lower()
|
||||
|
||||
if file_name_lower.endswith(ALLOWED_EXTENSIONS):
|
||||
try:
|
||||
# Open the file object (rb is read binary)
|
||||
file_obj = entry.resume.open('rb')
|
||||
|
||||
# *** ROBUST METHOD: Read the content and write it to the ZIP ***
|
||||
file_content = file_obj.read()
|
||||
|
||||
# Write the file content directly to the ZIP archive
|
||||
zf.writestr(file_name, file_content)
|
||||
|
||||
file_obj.close()
|
||||
|
||||
except Exception as e:
|
||||
# Log the error but continue with the rest of the files
|
||||
print(f"Error processing file {file_name}: {e}")
|
||||
continue
|
||||
|
||||
# 4. Prepare the response
|
||||
zip_buffer.seek(0)
|
||||
|
||||
# 5. Create the HTTP response
|
||||
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
|
||||
|
||||
# Set the header for the browser to download the file
|
||||
response['Content-Disposition'] = 'attachment; filename=f"all_cvs_for_{job.title}.zip"'
|
||||
|
||||
return response
|
||||
|
||||
@login_required
|
||||
def job_image_upload(request, slug):
|
||||
#only for handling the post request
|
||||
@ -612,6 +673,20 @@ def edit_linkedin_post_content(request,slug):
|
||||
|
||||
|
||||
|
||||
JOB_TYPES = [
|
||||
("FULL_TIME", "Full-time"),
|
||||
("PART_TIME", "Part-time"),
|
||||
("CONTRACT", "Contract"),
|
||||
("INTERNSHIP", "Internship"),
|
||||
("FACULTY", "Faculty"),
|
||||
("TEMPORARY", "Temporary"),
|
||||
]
|
||||
|
||||
WORKPLACE_TYPES = [
|
||||
("ON_SITE", "On-site"),
|
||||
("REMOTE", "Remote"),
|
||||
("HYBRID", "Hybrid"),
|
||||
]
|
||||
|
||||
|
||||
def kaauh_career(request):
|
||||
@ -621,8 +696,48 @@ def kaauh_career(request):
|
||||
status='ACTIVE',
|
||||
form_template__is_active=True
|
||||
)
|
||||
selected_department=request.GET.get('department','')
|
||||
department_type_keys=active_jobs.exclude(
|
||||
department__isnull=True
|
||||
).exclude(department__exact=''
|
||||
).values_list(
|
||||
'department',
|
||||
flat=True
|
||||
).distinct().order_by('department')
|
||||
|
||||
if selected_department and selected_department in department_type_keys:
|
||||
active_jobs=active_jobs.filter(department=selected_department)
|
||||
selected_workplace_type=request.GET.get('workplace_type','')
|
||||
print(selected_workplace_type)
|
||||
selected_job_type = request.GET.get('employment_type', '')
|
||||
|
||||
job_type_keys = active_jobs.values_list('job_type', flat=True).distinct()
|
||||
workplace_type_keys=active_jobs.values_list('workplace_type',flat=True).distinct()
|
||||
if selected_job_type and selected_job_type in job_type_keys:
|
||||
active_jobs=active_jobs.filter(job_type=selected_job_type)
|
||||
if selected_workplace_type and selected_workplace_type in workplace_type_keys:
|
||||
active_jobs=active_jobs.filter(workplace_type=selected_workplace_type)
|
||||
|
||||
return render(request,'applicant/career.html',{'active_jobs':active_jobs})
|
||||
JOBS_PER_PAGE=10
|
||||
paginator = Paginator(active_jobs, JOBS_PER_PAGE)
|
||||
page_number = request.GET.get('page', 1)
|
||||
|
||||
try:
|
||||
page_obj = paginator.get_page(page_number)
|
||||
except EmptyPage:
|
||||
page_obj = paginator.page(paginator.num_pages)
|
||||
|
||||
total_open_roles=active_jobs.all().count()
|
||||
|
||||
|
||||
return render(request,'applicant/career.html',{'active_jobs': page_obj.object_list,
|
||||
'job_type_keys':job_type_keys,
|
||||
'selected_job_type':selected_job_type,
|
||||
'workplace_type_keys':workplace_type_keys,
|
||||
'selected_workplace_type':selected_workplace_type,
|
||||
'selected_department':selected_department,
|
||||
'department_type_keys':department_type_keys,
|
||||
'total_open_roles': total_open_roles,'page_obj': page_obj})
|
||||
|
||||
# job detail facing the candidate:
|
||||
def application_detail(request, slug):
|
||||
@ -4131,18 +4246,19 @@ def send_interview_email(request, slug):
|
||||
|
||||
|
||||
|
||||
def schedule_interview_location_form(request,slug):
|
||||
schedule=get_object_or_404(InterviewSchedule,slug=slug)
|
||||
if request.method=='POST':
|
||||
form=InterviewScheduleLocationForm(request.POST,instance=schedule)
|
||||
form.save()
|
||||
return redirect('list_meetings')
|
||||
else:
|
||||
form=InterviewScheduleLocationForm(instance=schedule)
|
||||
return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule})
|
||||
# def schedule_interview_location_form(request,slug):
|
||||
# schedule=get_object_or_404(InterviewSchedule,slug=slug)
|
||||
# if request.method=='POST':
|
||||
# form=InterviewScheduleLocationForm(request.POST,instance=schedule)
|
||||
# form.save()
|
||||
# return redirect('list_meetings')
|
||||
# else:
|
||||
# form=InterviewScheduleLocationForm(instance=schedule)
|
||||
# return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule})
|
||||
|
||||
|
||||
|
||||
def onsite_interview_list_view(request):
|
||||
onsite_interviews=ScheduledInterview.objects.filter(schedule__interview_type='Onsite')
|
||||
return render(request,'interviews/onsite_interview_list.html',{'onsite_interviews':onsite_interviews})
|
||||
|
||||
|
||||
@ -1,172 +1,216 @@
|
||||
{% extends 'applicant/partials/candidate_facing_base.html'%}
|
||||
{% extends 'applicant/partials/candidate_facing_base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: var(--kaauh-teal); z-index: 1030;">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-text text-white fw-bold">{% trans "Job Overview" %}</span>
|
||||
{# ------------------------------------------------ #}
|
||||
{# 🚀 TOP NAV BAR (Sticky and Themed) #}
|
||||
{# ------------------------------------------------ #}
|
||||
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top border-bottom"
|
||||
style="background-color: var(--kaauh-teal); z-index: 1030; height: 50px;">
|
||||
<div class="container-fluid container-lg">
|
||||
<span class="navbar-text text-white fw-bold fs-6">{% trans "Job Overview" %}</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{# ------------------------------------------------ #}
|
||||
{# 🔔 DJANGO MESSAGES (Refined placement and styling) #}
|
||||
{# ------------------------------------------------ #}
|
||||
|
||||
|
||||
{# ================================================= #}
|
||||
{# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #}
|
||||
{# ================================================= #}
|
||||
{% if messages %}
|
||||
<div class="container-fluid message-container">
|
||||
<div class="row">
|
||||
{# Using responsive columns to center the message content, similar to your form structure #}
|
||||
<div class="col-lg-8 offset-lg-2 col-md-10 offset-md-1 col-12">
|
||||
{% for message in messages %}
|
||||
{# Use 'alert-{{ message.tags }}' to apply Bootstrap styling based on Django's tag (success, error/danger, info, warning) #}
|
||||
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# ================================================= #}
|
||||
|
||||
<div class="container">
|
||||
<div class="row mb-5 mt-3 main-content-area">
|
||||
{# ------------------------------------------------ #}
|
||||
{# 💻 MAIN CONTENT CONTAINER #}
|
||||
{# ------------------------------------------------ #}
|
||||
<div class="container mt-4 mb-5">
|
||||
<div class="row g-4 main-content-area">
|
||||
|
||||
<div class="col-lg-4 order-lg-2 order-1 d-none d-lg-block">
|
||||
<div class="card shadow-sm sticky-top">
|
||||
<div class="card-header bg-kaauh-teal-dark bg-white p-3">
|
||||
<h5 class="mb-0 fw-bold"><i class="fas fa-file-signature me-2" style="color: var(--kaauh-teal);"></i>{% trans "Ready to Apply?" %}</h5>
|
||||
{# 📌 RIGHT COLUMN: Sticky Apply Card (Desktop Only) #}
|
||||
<div class="col-lg-4 order-lg-2 d-none d-lg-block">
|
||||
<div class="card shadow-lg border-0" style="position: sticky; top: 70px;">
|
||||
<div class="card-header bg-white border-bottom p-3">
|
||||
<h5 class="mb-0 fw-bold text-kaauh-teal">
|
||||
<i class="fas fa-file-signature me-2"></i>{% trans "Ready to Apply?" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<p class="text-muted">{% trans "Review the job details, then apply below." %}</p>
|
||||
<div class="card-body text-center p-4">
|
||||
<p class="text-muted small mb-3">{% trans "Review the full job details below before submitting your application." %}</p>
|
||||
|
||||
{% if job.form_template %}
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100 shadow-sm">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
{% elif not job.is_expired %}
|
||||
<p class="text-danger fw-bold">{% trans "Application form is unavailable." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8 order-lg-1 order-2">
|
||||
<div class="card shadow-sm">
|
||||
{# 📝 LEFT COLUMN: Job Details #}
|
||||
<div class="col-lg-8 order-lg-1">
|
||||
<article class="card shadow-lg border-0">
|
||||
|
||||
<div class="card-header bg-white border-bottom p-4">
|
||||
<h1 class="h3 mb-0 fw-bold" style="color: var(--kaauh-teal);">{{ job.title }}</h1>
|
||||
</div>
|
||||
{# Job Title Header #}
|
||||
<header class="card-header bg-white border-bottom p-4">
|
||||
<h1 class="h2 mb-0 fw-bolder text-kaauh-teal">{{ job.title }}</h1>
|
||||
</header>
|
||||
|
||||
<div class="card-body p-4">
|
||||
|
||||
<h4 class="mb-3 fw-bold" style="color: var(--kaauh-teal-dark);">{% trans "Job Overview" %}</h4>
|
||||
<div class="row row-cols-1 row-cols-md-2 g-3 mb-5 small text-secondary border p-3 rounded">
|
||||
<h4 class="mb-4 fw-bold text-muted border-bottom pb-2">{% trans "Summary" %}</h4>
|
||||
|
||||
{# Job Metadata/Overview Grid #}
|
||||
<section class="row row-cols-1 row-cols-md-2 g-3 mb-5 small text-secondary p-3 rounded bg-light-subtle border">
|
||||
|
||||
{# SALARY #}
|
||||
{% if job.salary_range %}
|
||||
<div class="col">
|
||||
<i class="fas fa-money-bill-wave text-success me-2"></i>
|
||||
<i class="fas fa-money-bill-wave text-success me-2 fa-fw"></i>
|
||||
<strong>{% trans "Salary:" %}</strong>
|
||||
<span class="fw-bold text-success">{{ job.salary_range }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# DEADLINE #}
|
||||
<div class="col">
|
||||
<i class="fas fa-calendar-alt text-muted me-2"></i>
|
||||
<i class="fas fa-calendar-alt text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Deadline:" %}</strong>
|
||||
{% if job.application_deadline %}
|
||||
{{ job.application_deadline|date:"M d, Y" }}
|
||||
<time datetime="{{ job.application_deadline|date:'Y-m-d' }}">
|
||||
{{ job.application_deadline|date:"M d, Y" }}
|
||||
</time>
|
||||
{% if job.is_expired %}
|
||||
<span class="badge bg-danger ms-2">{% trans "EXPIRED" %}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">{% trans "Not specified" %}</span>
|
||||
<span class="text-muted">{% trans "Ongoing" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col"> <i class="fas fa-briefcase text-muted me-2"></i> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }} </div>
|
||||
<div class="col"> <i class="fas fa-map-marker-alt text-muted me-2"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }} </div>
|
||||
<div class="col"> <i class="fas fa-building text-muted me-2"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }} </div>
|
||||
<div class="col"> <i class="fas fa-hashtag text-muted me-2"></i> <strong>{% trans "JOB ID:" %}</strong> {{ job.internal_job_id|default:"N/A" }} </div>
|
||||
<div class="col"> <i class="fas fa-desktop text-muted me-2"></i> <strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }} </div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="accordion" id="jobDetailAccordion">
|
||||
{# JOB TYPE #}
|
||||
<div class="col">
|
||||
<i class="fas fa-briefcase text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
|
||||
</div>
|
||||
|
||||
{# LOCATION #}
|
||||
<div class="col">
|
||||
<i class="fas fa-map-marker-alt text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
|
||||
</div>
|
||||
|
||||
{# DEPARTMENT #}
|
||||
<div class="col">
|
||||
<i class="fas fa-building text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
{# JOB ID #}
|
||||
<div class="col">
|
||||
<i class="fas fa-hashtag text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "JOB ID:" %}</strong> {{ job.internal_job_id|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
{# WORKPLACE TYPE #}
|
||||
<div class="col">
|
||||
<i class="fas fa-laptop-house text-muted me-2 fa-fw"></i>
|
||||
<strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
{# Detailed Accordion Section #}
|
||||
<div class="accordion accordion-flush" id="jobDetailAccordion">
|
||||
|
||||
{% with active_collapse="collapseOne" %}
|
||||
|
||||
{# JOB DESCRIPTION #}
|
||||
{% if job.has_description_content %}
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-item border-top border-bottom">
|
||||
<h2 class="accordion-header" id="headingOne">
|
||||
<button class="accordion-button fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
|
||||
<i class="fas fa-info-circle me-3"></i> {% trans "Job Description" %}
|
||||
<button class="accordion-button fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#{{ active_collapse }}" aria-expanded="true"
|
||||
aria-controls="{{ active_collapse }}">
|
||||
<i class="fas fa-info-circle me-3 fa-fw"></i> {% trans "Job Description" %}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#jobDetailAccordion">
|
||||
<div id="{{ active_collapse }}" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#jobDetailAccordion">
|
||||
<div class="accordion-body text-secondary p-4">
|
||||
{{ job.description|safe }}
|
||||
<div class="wysiwyg-content">{{ job.description|safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# QUALIFICATIONS #}
|
||||
{% if job.has_qualifications_content %}
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-item border-bottom">
|
||||
<h2 class="accordion-header" id="headingTwo">
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
|
||||
<i class="fas fa-graduation-cap me-3"></i> {% trans "Qualifications" %}
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
|
||||
<i class="fas fa-graduation-cap me-3 fa-fw"></i> {% trans "Qualifications" %}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#jobDetailAccordion">
|
||||
<div class="accordion-body text-secondary p-4">
|
||||
{{ job.qualifications|safe }}
|
||||
<div class="wysiwyg-content">{{ job.qualifications|safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# BENEFITS #}
|
||||
{% if job.has_benefits_content %}
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-item border-bottom">
|
||||
<h2 class="accordion-header" id="headingThree">
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
|
||||
<i class="fas fa-hand-holding-usd me-3"></i> {% trans "Benefits" %}
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
|
||||
<i class="fas fa-hand-holding-usd me-3 fa-fw"></i> {% trans "Benefits" %}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-bs-parent="#jobDetailAccordion">
|
||||
<div class="accordion-body text-secondary p-4">
|
||||
{{ job.benefits|safe }}
|
||||
<div class="wysiwyg-content">{{ job.benefits|safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# APPLICATION INSTRUCTIONS #}
|
||||
{% if job.has_application_instructions_content %}
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-item border-bottom">
|
||||
<h2 class="accordion-header" id="headingFour">
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
|
||||
<i class="fas fa-file-alt me-3"></i> {% trans "Application Instructions" %}
|
||||
<button class="accordion-button collapsed fw-bold fs-5 text-kaauh-teal-dark" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
|
||||
<i class="fas fa-file-alt me-3 fa-fw"></i> {% trans "Application Instructions" %}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour" data-bs-parent="#jobDetailAccordion">
|
||||
<div class="accordion-body text-secondary p-4">
|
||||
{{ job.application_instructions|safe }}
|
||||
<div class="wysiwyg-content">{{ job.application_instructions|safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-fixed-apply-bar d-lg-none text-center">
|
||||
{% if job.form_template %}
|
||||
{# 📱 MOBILE FIXED APPLY BAR (Replaced inline style with utility classes) #}
|
||||
{% if job.form_template %}
|
||||
<footer class="fixed-bottom d-lg-none bg-white border-top shadow-lg p-3">
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</footer>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock content%}
|
||||
@ -1,27 +1,37 @@
|
||||
{% extends 'applicant/partials/candidate_facing_base.html'%}
|
||||
{% extends 'applicant/partials/candidate_facing_base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
|
||||
{% block title %}{% trans "My Profile" %} - {{ block.super }}{% endblock %}
|
||||
{# Use a dynamic title for better SEO and user context #}
|
||||
{% block title %}{% trans "Career Opportunities" %} | KAAUH - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block content %}
|
||||
|
||||
<div class="main-content-area">
|
||||
|
||||
<header class="hero-section">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-xl-10">
|
||||
<h1 class="hero-title mb-4">
|
||||
{% translate "Your Career in Health & Academia starts here." %}
|
||||
{# ------------------------------------------------ #}
|
||||
{# 🌟 HERO SECTION (High Visual Impact) #}
|
||||
{# ------------------------------------------------ #}
|
||||
<header class="hero-section text-white py-5 py-lg-6" style=" background-size: cover; background-position: center;">
|
||||
{# Overlay for readability, assuming custom CSS defines .hero-overlay #}
|
||||
<div class="hero-overlay"></div>
|
||||
<div class="container position-relative" style="z-index: 2;">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-12 col-xl-10 text-center">
|
||||
{# Use a large, commanding font size #}
|
||||
<h1 class="display-4 fw-bolder mb-4 animate__animated animate__fadeInDown">
|
||||
{% trans "Your Career in Health & Academia Starts Here." %}
|
||||
</h1>
|
||||
<p class="lead mb-5">
|
||||
{% translate "Join KAAUH, a national leader in patient care, research, and education. We are building the future of healthcare." %}
|
||||
<p class="lead mb-5 fs-5 animate__animated animate__fadeInUp">
|
||||
{% trans "Join KAAUH, a national leader in patient care, research, and education. We are building the future of healthcare." %}
|
||||
</p>
|
||||
<a href="#filterSidebar" class="btn btn-hero-action me-3 mb-3 mb-lg-0">
|
||||
<i class="fas fa-compass me-2"></i> {% translate "Find Your Path" %}
|
||||
|
||||
{# Primary Action to scroll to listings/filters #}
|
||||
<a href="#job-list-start" class="btn btn-hero-action btn-lg rounded-pill px-5 me-3 mb-3 mb-lg-0 shadow-lg animate__animated animate__zoomIn">
|
||||
<i class="fas fa-compass me-2"></i> {% trans "Find Your Path" %}
|
||||
</a>
|
||||
<a href="https://kaauh.edu.sa/about-us" class="btn btn-outline-light rounded-pill px-4 btn-lg">
|
||||
{% translate "About US" %}
|
||||
{# Secondary Action #}
|
||||
<a href="https://kaauh.edu.sa/about-us" class="btn btn-outline-light rounded-pill px-5 btn-lg animate__animated animate__zoomIn">
|
||||
{% trans "About US" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -29,128 +39,236 @@
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
|
||||
<section class="py-5 job-listing-section">
|
||||
{# ------------------------------------------------ #}
|
||||
{# 💻 JOB LISTING SECTION #}
|
||||
{# ------------------------------------------------ #}
|
||||
<section class="py-5 job-listing-section" id="job-list-start">
|
||||
<div class="container">
|
||||
|
||||
<div class="row g-5">
|
||||
|
||||
<div class="col-lg-3 col-xl-3">
|
||||
{# 📌 LEFT COLUMN: FILTERS (Smaller on larger screens) #}
|
||||
<div class="col-lg-4">
|
||||
|
||||
<button class="btn btn-outline-dark filter-toggle-button d-lg-none" type="button"
|
||||
{# Mobile Filter Toggle (Used aria-controls for better accessibility) #}
|
||||
<button class="btn btn-outline-dark filter-toggle-button d-lg-none w-100 mb-3" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#filterSidebar" aria-expanded="false" aria-controls="filterSidebar">
|
||||
<i class="fas fa-filter me-2"></i> {% translate "Filter Jobs" %}
|
||||
<i class="fas fa-filter me-2"></i> {% trans "Filter Jobs" %}
|
||||
</button>
|
||||
|
||||
<div class="collapse d-lg-block filter-sidebar-collapse" id="filterSidebar">
|
||||
<div class="card sticky-top-filters p-4 bg-white">
|
||||
<h4 class="fw-bold mb-4 text-primary-theme">
|
||||
{% translate "Refine Your Search" %}
|
||||
</h4>
|
||||
{# Sticky top ensures filters remain visible while scrolling results #}
|
||||
<div class="card border-0 shadow-sm sticky-top-filters p-4 bg-light-subtle" style="top: 20px;">
|
||||
|
||||
<div class="d-grid gap-3">
|
||||
<select class="form-select" aria-label="Department filter">
|
||||
<option selected>{% translate "Department (Faculty/Admin)" %}</option>
|
||||
<option value="1">{% translate "Clinical Services" %}</option>
|
||||
<option value="2">{% translate "Research Labs" %}</option>
|
||||
<option value="3">{% translate "Training & Education" %}</option>
|
||||
</select>
|
||||
<h4 class="fw-bold mb-4 text-dark border-bottom pb-2">
|
||||
<i class="fas fa-search me-2 text-primary-theme"></i>{% trans "Refine Your Search" %}
|
||||
</h4>
|
||||
<form method="GET" action="{% url 'kaauh_career'%}" class="d-grid gap-3">
|
||||
{# NOTE: Replace select with Django form fields for real functionality #}
|
||||
|
||||
<select class="form-select" aria-label="Employment Type filter">
|
||||
<option selected>{% translate "Employment Type" %}</option>
|
||||
<option value="1">{% translate "Full-Time" %}</option>
|
||||
<option value="2">{% translate "Part-Time" %}</option>
|
||||
|
||||
|
||||
<select class="form-select form-select-lg" name="employment_type" aria-label="Employment Type filter" >
|
||||
<option value="" {% if not selected_job_type %}selected{% endif %}>{% trans "Employment Type" %}</option>
|
||||
|
||||
{% for key in job_type_keys %}
|
||||
<option value="{{ key }}" {% if key == selected_job_type %}selected{% endif %}>
|
||||
<!-- Hard-coded mapping using IF statements -->
|
||||
{% if key == 'FULL_TIME' %}{% trans "Full-time" %}{% endif %}
|
||||
{% if key == 'PART_TIME' %}{% trans "Part-time" %}{% endif %}
|
||||
{% if key == 'CONTRACT' %}{% trans "Contract" %}{% endif %}
|
||||
{% if key == 'INTERNSHIP' %}{% trans "Internship" %}{% endif %}
|
||||
{% if key == 'FACULTY' %}{% trans "Faculty" %}{% endif %}
|
||||
{% if key == 'TEMPORARY' %}{% trans "Temporary" %}{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select class="form-select" aria-label="Specialty filter">
|
||||
<option selected>{% translate "Specialty / Focus" %}</option>
|
||||
<option value="1">{% translate "Women's Health" %}</option>
|
||||
<option value="2">{% translate "Child Growth & Dev" %}</option>
|
||||
<select class="form-select form-select-lg" name="workplace_type" aria-label="Workplace Type filter" >
|
||||
<option value="" {% if not selected_workplace_type %}selected{% endif %}>{% trans "Workplace Type" %}</option>
|
||||
|
||||
{% for key in workplace_type_keys %}
|
||||
<option value="{{ key }}" {% if key == selected_workplace_type %}selected{% endif %}>
|
||||
<!-- Hard-coded mapping using IF statements -->
|
||||
{% if key == 'ON_SITE' %}{% trans "On-site" %}{% endif %}
|
||||
{% if key == 'REMOTE' %}{% trans "Remote" %}{% endif %}
|
||||
{% if key == 'HYBRID' %}{% trans "Hybrid" %}{% endif %}
|
||||
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
</select>
|
||||
|
||||
<button class="btn btn-main-action rounded-pill mt-3">{% translate "Apply Filters" %}</button>
|
||||
</div>
|
||||
|
||||
<select class="form-select form-select-lg" name="department" aria-label="Department Type filter" >
|
||||
<option value="" {% if not selected_department %}selected{% endif %}>{% trans "Departments" %}</option>
|
||||
|
||||
{% for key in department_type_keys %}
|
||||
<option value="{{ key }}" {% if key == selected_department %}selected{% endif %}>
|
||||
<!-- Hard-coded mapping using IF statements -->
|
||||
{{key}}
|
||||
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
</select>
|
||||
<button type="submit" class="btn btn-main-action btn-lg rounded-pill mt-3 shadow-sm">
|
||||
{% trans "Apply Filters" %}
|
||||
</button>
|
||||
<a href="." class="btn btn-outline-secondary btn-sm">{% trans "Clear Filters" %}</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-9 col-xl-9">
|
||||
{# 📜 RIGHT COLUMN: JOB LISTINGS (Wider on larger screens) #}
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="sticky-filter-bar">
|
||||
{# Sticky Filter Bar (Summary of results and active filters) #}
|
||||
<div class="sticky-filter-bar bg-white p-3 border-bottom mb-4 shadow-sm">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center">
|
||||
|
||||
{# Dynamic Count #}
|
||||
<h3 class="fw-bold mb-0 text-dark fs-5">
|
||||
{% comment %}
|
||||
Assuming a 'job_count' context variable exists, otherwise
|
||||
this remains static as in the original template.
|
||||
{% endcomment %}
|
||||
{% translate "Showing 37 Open Roles" %}
|
||||
<span class="text-primary-theme">{{ total_open_roles|default:"0" }}</span> {% trans "Open Roles" %}
|
||||
</h3>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<span class="filter-chip">
|
||||
{% translate "Specialty: Women's Health" %}
|
||||
<i class="fas fa-times text-xs ms-2 cursor-pointer" role="button" aria-label="Remove filter: Women's Health"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{# Active Filter Chips (Use a dedicated class for styling) #}
|
||||
<div class="d-flex flex-wrap gap-2 pt-2 pt-md-0">
|
||||
|
||||
{# --- Active Employment Type Filter Chip --- #}
|
||||
{% if selected_job_type %}
|
||||
<span class="filter-chip badge bg-primary-theme-subtle text-primary-theme fw-normal p-2 active-filter-chip">
|
||||
{% trans "Type" %}:
|
||||
{# Map the key back to its human-readable translation #}
|
||||
<strong class="mx-1">
|
||||
{% if selected_job_type == 'FULL_TIME' %}{% trans "Full-time" %}
|
||||
{% elif selected_job_type == 'PART_TIME' %}{% trans "Part-time" %}
|
||||
{% elif selected_job_type == 'CONTRACT' %}{% trans "Contract" %}
|
||||
{% elif selected_job_type == 'INTERNSHIP' %}{% trans "Internship" %}
|
||||
{% elif selected_job_type == 'FACULTY' %}{% trans "Faculty" %}
|
||||
{% elif selected_job_type == 'TEMPORARY' %}{% trans "Temporary" %}
|
||||
{% endif %}
|
||||
</strong>
|
||||
{# Link to clear this specific filter: use current URL but remove `employment_type` parameter #}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'employment_type' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
|
||||
class="text-primary-theme text-decoration-none ms-2" role="button" aria-label="Remove Employment Type filter">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{# --- Active Workplace Type Filter Chip --- #}
|
||||
{% if selected_workplace_type %}
|
||||
<span class="filter-chip badge bg-primary-theme-subtle text-primary-theme fw-normal p-2 active-filter-chip">
|
||||
{% trans "Workplace" %}:
|
||||
{# Map the key back to its human-readable translation #}
|
||||
<strong class="mx-1">
|
||||
{% if selected_workplace_type == 'ON_SITE' %}{% trans "On-site" %}
|
||||
{% elif selected_workplace_type == 'REMOTE' %}{% trans "Remote" %}
|
||||
{% elif selected_workplace_type == 'HYBRID' %}{% trans "Hybrid" %}
|
||||
{% endif %}
|
||||
</strong>
|
||||
{# Link to clear this specific filter: use current URL but remove `workplace_type` parameter #}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'workplace_type' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
|
||||
class="text-primary-theme text-decoration-none ms-2" role="button" aria-label="Remove Workplace Type filter">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{# --- Active Department Filter Chip --- #}
|
||||
{% if selected_department %}
|
||||
<span class="filter-chip badge bg-primary-theme-subtle text-primary-theme fw-normal p-2 active-filter-chip">
|
||||
{% trans "Department" %}:
|
||||
<strong class="mx-1">{{ selected_department }}</strong>
|
||||
{# Link to clear this specific filter: use current URL but remove `department` parameter #}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'department' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
|
||||
class="text-primary-theme text-decoration-none ms-2" role="button" aria-label="Remove Department filter">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Job Cards Grid #}
|
||||
<div class="mt-4 d-grid gap-3">
|
||||
|
||||
{% for job in active_jobs %}
|
||||
{# The original card structure, now dynamically filled with job data #}
|
||||
<a href="{% url 'application_detail' job.slug %}" class="card d-block text-decoration-none text-dark job-listing-card bg-white">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<h4 class="h5 fw-bold mb-1 text-primary-theme-hover">
|
||||
{# Optimized Job Listing Card #}
|
||||
<a href="{% url 'application_detail' job.slug %}"
|
||||
class="card d-block text-decoration-none text-dark job-listing-card p-4 border-2 shadow-hover transition-all">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
{# Job Title #}
|
||||
<h4 class="h5 fw-bold mb-0 text-primary-theme-hover">
|
||||
{{ job.title }}
|
||||
</h4>
|
||||
|
||||
{# NOTE: You will need to define how job.category or job.tag is determined for the badge logic #}
|
||||
<span class="badge bg-primary-theme job-tag">
|
||||
{% comment %} Placeholder: Use job.tag or implement conditional logic {% endcomment %}
|
||||
{% if job.tag_slug == 'clinical' %}{% translate "Clinical" %}
|
||||
{% elif job.tag_slug == 'research' %}{% translate "Research/Contract" %}
|
||||
{% else %}{% translate "General" %}{% endif %}
|
||||
{# Tag Badge (Prominent) #}
|
||||
<span class="badge rounded-pill bg-kaauh-teal job-tag px-3 py-2 fs-6">
|
||||
|
||||
<i class="fas fa-tag me-1"></i>{% trans "Apply Before: " %}{{job.application_deadline}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{# NOTE: Assuming job.department and job.location exist in your context #}
|
||||
<p class="text-muted small mb-3">{{ job.department }}</p>
|
||||
{# Department/Context (Sub-text) #}
|
||||
<p class="text-muted small mb-3">{% trans 'Department: '%}{{ job.department|default:"KAAUH Department" }}</p>
|
||||
|
||||
<div class="d-flex flex-wrap gap-4 small text-muted">
|
||||
<span class="d-flex align-items-center">
|
||||
<i class="fas fa-map-marker-alt me-2 job-detail-icon"></i>
|
||||
{{ job.location|default:"Riyadh, KSA" }}
|
||||
{# Job Metadata Icons (Horizontal list for quick scan) #}
|
||||
<div class="d-flex flex-wrap gap-4 small text-secondary">
|
||||
|
||||
<span class="d-flex align-items-center fw-medium">
|
||||
<i class="fas fa-map-marker-alt me-2 text-primary-theme fa-fw"></i>
|
||||
{{ job.location_country|default:"Kindom of Saudi Arabia" }} | {{job.location_state|default:"Riyadh Province"}} | {{job.location_city|default:"Riyadh"}}
|
||||
</span>
|
||||
|
||||
<span class="d-flex align-items-center">
|
||||
<i class="fas fa-user-md me-2 job-detail-icon"></i>
|
||||
{{ job.focus|default:"High Reliability Focus" }}
|
||||
<span class="d-flex align-items-center fw-medium">
|
||||
<i class="fas fa-user-md me-2 text-primary-theme fa-fw"></i>
|
||||
{{ job.workplace_type|default:"" }}
|
||||
</span>
|
||||
|
||||
<span class="d-flex align-items-center">
|
||||
<i class="fas fa-calendar-alt me-2 job-detail-icon"></i>
|
||||
{{ job.employment_type|default:"Full-Time" }}
|
||||
<span class="d-flex align-items-center fw-medium">
|
||||
<i class="fas fa-calendar-alt me-2 text-primary-theme fa-fw"></i>
|
||||
{{ job.job_type|default:"Full-Time" }}
|
||||
</span>
|
||||
|
||||
{% if job.posted_date %}
|
||||
<span class="d-flex align-items-center fw-medium">
|
||||
<i class="fas fa-clock me-2 text-primary-theme fa-fw"></i>
|
||||
{% trans "Posted:" %} {{ job.posted_date|timesince }} {% trans "ago" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% empty %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
{% translate "We currently have no open roles that match your search. Please check back soon!" %}
|
||||
<div class="alert alert-info border-0 shadow-sm mt-5" role="alert">
|
||||
<h5 class="alert-heading">{% trans "No Matching Opportunities" %}</h5>
|
||||
<p>{% trans "We currently have no open roles that match your search and filters. Please modify your criteria or check back soon!" %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{# Load More Button #}
|
||||
{% if show_load_more %}
|
||||
<div class="text-center mt-5 mb-3">
|
||||
<button class="btn btn-main-action btn-lg rounded-pill px-5 shadow-sm">
|
||||
{% translate "Load More Jobs" %}
|
||||
<button class="btn btn-main-action btn-lg rounded-pill px-5 shadow-lg">
|
||||
{% trans "Load More Jobs" %} <i class="fas fa-redo ms-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{% include "includes/paginator.html" %}
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% endblock content %}
|
||||
@ -49,6 +49,10 @@
|
||||
|
||||
.text-primary-theme { color: var(--kaauh-teal) !important; }
|
||||
.text-primary-theme-hover:hover { color: var(--kaauh-teal-dark) !important; }
|
||||
|
||||
.bg-kaauh-teal {
|
||||
background-color: #00636e;
|
||||
}
|
||||
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
@ -178,7 +182,7 @@
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand text-dark fw-bold" href="{% url 'kaauh_career' %}">
|
||||
<img src="{% static 'image/kaauh.jpeg' %}" alt="{% translate 'KAAUH IMAGE' %}" style="height: 50px; margin-right: 10px;">
|
||||
KAAUH
|
||||
<span style="color:#00636e;">KAAUH Careers</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
@ -205,7 +209,7 @@
|
||||
<span class="d-inline">{{ LANGUAGE_CODE|upper }}</span>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu {% if LANGUAGE_CODE == 'ar' %}dropdown-menu-start{% else %}dropdown-menu-end{% endif %}" aria-labelledby="navbarLanguageDropdown">
|
||||
<ul class="dropdown-menu mx-auto {% if LANGUAGE_CODE == 'ar' %}dropdown-menu-end{% else %}dropdown-menu-end{% endif %}" aria-labelledby="navbarLanguageDropdown">
|
||||
|
||||
<li>
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
@ -231,10 +235,35 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% if messages %}
|
||||
<div class="container message-container mt-3">
|
||||
<div class="row">
|
||||
{# Use responsive columns matching the main content block for alignment #}
|
||||
<div class="col-lg-12 order-lg-1 col-12 mx-auto">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i> {{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ================================================= #}
|
||||
{# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #}
|
||||
{# ================================================= #}
|
||||
|
||||
{# ================================================= #}
|
||||
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
@ -208,7 +208,11 @@
|
||||
<i class="fas fa-sign-out-alt me-3 fs-5 " style="color:red;"></i>
|
||||
<span style="color:red;">{% trans "Sign Out" %}</span>
|
||||
</button>
|
||||
</form>
|
||||
</form>
|
||||
{% comment %} <a class="d-inline text-decoration-none px-4 d-flex align-items-center border-0 bg-transparent text-start text-center" href={% url "account_logout" %}>
|
||||
<i class="fas fa-sign-out-alt me-3 fs-5 " style="color:red;"></i>
|
||||
<span style="color:red;">{% trans "Sign Out" %}</span>
|
||||
</a> {% endcomment %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
@ -255,7 +259,7 @@
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item me-lg-4">
|
||||
{% comment %} <li class="nav-item me-lg-4">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'interview_list' %}active{% endif %}" href="{% url 'interview_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
@ -264,7 +268,7 @@
|
||||
{% trans "Onsite Interviews" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</li> {% endcomment %}
|
||||
<li class="nav-item me-lg-4">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'participants_list' %}active{% endif %}" href="{% url 'participants_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
|
||||
@ -1,27 +1,61 @@
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link text-primary-theme" href="?page=1{% if search_query %}&q={{ search_query }}{% endif %}{% if job_filter %}&job={{ job_filter }}{% endif %}">First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link text-primary-theme" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if job_filter %}&job={{ job_filter }}{% endif %}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if page_obj.has_previous or page_obj.has_next %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
|
||||
{# Helper to build the query string while excluding the 'page' parameter #}
|
||||
{% load url_extras %}
|
||||
|
||||
{# Build a string of all current filters (e.g., &department=IT&type=FULL_TIME) #}
|
||||
{% add_get_params request.GET as filter_params %}
|
||||
{% with filter_params=filter_params %}
|
||||
|
||||
{% if page_obj.has_previous %}
|
||||
|
||||
{# First Page Link #}
|
||||
<li class="page-item">
|
||||
<span class="page-link bg-primary-theme text-white">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
||||
<a class="page-link text-primary-theme"
|
||||
href="?page=1{{ filter_params }}">
|
||||
First
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# Previous Page Link #}
|
||||
<li class="page-item">
|
||||
<a class="page-link text-primary-theme"
|
||||
href="?page={{ page_obj.previous_page_number }}{{ filter_params }}">
|
||||
Previous
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link text-primary-theme" href="?page={{ page_obj.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if job_filter %}&job={{ job_filter }}{% endif %}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link text-primary-theme" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&q={{ search_query }}{% endif %}{% if job_filter %}&job={{ job_filter }}{% endif %}">Last</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{# Current Page Status - Use your teal/custom background class here #}
|
||||
<li class="page-item active" aria-current="page">
|
||||
<span class="page-link bg-kaauh-teal text-white">
|
||||
{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
|
||||
{# Next Page Link #}
|
||||
<li class="page-item">
|
||||
<a class="page-link text-primary-theme"
|
||||
href="?page={{ page_obj.next_page_number }}{{ filter_params }}">
|
||||
Next
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# Last Page Link #}
|
||||
<li class="page-item">
|
||||
<a class="page-link text-primary-theme"
|
||||
href="?page={{ page_obj.paginator.num_pages }}{{ filter_params }}">
|
||||
Last
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
0
templates/interviews/delete_interview.html
Normal file
0
templates/interviews/delete_interview.html
Normal file
708
templates/interviews/detail_interview.html
Normal file
708
templates/interviews/detail_interview.html
Normal 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 %}
|
||||
267
templates/interviews/interview_list.html
Normal file
267
templates/interviews/interview_list.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
0
templates/interviews/update_interview.html
Normal file
0
templates/interviews/update_interview.html
Normal file
@ -321,6 +321,9 @@
|
||||
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applicants" %}
|
||||
</a>
|
||||
<a href="{% url 'job_cvs_download' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fa-solid fa-download me-1"></i> {% trans "Download All CVs" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -229,7 +229,7 @@
|
||||
<td>{{ participant.created_at|date:"d-m-Y" }}</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'participants_detail' participant.slug%}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||
<a href="{% url 'participants_detail' participant.slug%}" class="btn btn-outline-secondary" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
|
||||
@ -196,7 +196,7 @@
|
||||
<tr>
|
||||
<td class="fw-medium">
|
||||
<a href="{% url 'agency_detail' agency.slug %}"
|
||||
class="text-decoration-none text-primary-theme">
|
||||
class="text-decoration-none text-secondary">
|
||||
{{ agency.name }}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@ -320,7 +320,7 @@
|
||||
<td>{{ candidate.created_at|date:"d-m-Y" }}</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-secondary" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user