Compare commits

...

9 Commits

Author SHA1 Message Date
cb963d454f Merge branch 'main' of http://10.10.1.136:3000/marwan/kaauh_ats into frontend 2025-11-23 13:59:04 +03:00
5b81db0b02 ui is fixed 2025-11-23 13:57:31 +03:00
1740eccf14 ui is fixed 2025-11-23 13:50:51 +03:00
f3f60d4fc5 updated the messages creation 2025-11-23 13:14:36 +03:00
8a0f715145 fixes at while testing 2025-11-22 22:23:14 +03:00
34e2224f80 revrting to a commit 2025-11-22 19:43:36 +03:00
a18baa0d73 Revert ".."
This reverts commit 47807ace90d52a389399f9c926da61c49505615d.
2025-11-22 19:40:04 +03:00
2679d0a0f5 update 2025-11-21 13:41:07 +03:00
32f2ecc989 Merge pull request 'interview-detail' (#34) from frontend into main
Reviewed-on: #34
2025-11-20 18:58:24 +03:00
14 changed files with 1450 additions and 1512 deletions

View File

@ -1634,7 +1634,7 @@ class CandidateEmailForm(forms.Form):
message_parts = [ message_parts = [
f"Than you, for your interest in the {self.job.title} role.", f"Than you, for your interest in the {self.job.title} role.",
f"We're pleased to inform you that you have cleared your exam!", f"We're pleased to inform you that you have cleared your exam!",
f"The next step is the mandatory online assessment exam.", f"The next step is the mandatory interview.",
f"Please complete the assessment by using the following link:", f"Please complete the assessment by using the following link:",
f"https://kaauh/hire/exam", f"https://kaauh/hire/exam",
f"We look forward to reviewing your results.", f"We look forward to reviewing your results.",
@ -1659,6 +1659,8 @@ class CandidateEmailForm(forms.Form):
f"If you have any questions before your start date, please contact [Onboarding Contact].", f"If you have any questions before your start date, please contact [Onboarding Contact].",
f"Best regards, The KAAUH Hiring team" f"Best regards, The KAAUH Hiring team"
] ]
elif candidate:
message_parts=""

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2025-11-23 09:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_jobposting_cv_zip_file_jobposting_zip_created'),
]
operations = [
migrations.AlterField(
model_name='interviewschedule',
name='template_location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedule_templates', to='recruitment.interviewlocation', verbose_name='Location Template (Zoom/Onsite)'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2025-11-23 09:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0004_alter_interviewschedule_template_location'),
]
operations = [
migrations.AlterField(
model_name='interviewschedule',
name='template_location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schedule_templates', to='recruitment.interviewlocation', verbose_name='Location Template (Zoom/Onsite)'),
),
]

View File

@ -997,6 +997,18 @@ class Application(Base):
else: else:
return False return False
@property
def is_active(self):
deadline=self.job.application_deadline
now=timezone.now().date()
if deadline>now:
return True
else:
return False
class TrainingMaterial(Base): class TrainingMaterial(Base):
@ -1162,6 +1174,9 @@ class OnsiteLocationDetails(InterviewLocation):
verbose_name_plural = _("Onsite Location Details") verbose_name_plural = _("Onsite Location Details")
# --- 2. Scheduling Models --- # --- 2. Scheduling Models ---
class InterviewSchedule(Base): class InterviewSchedule(Base):

View File

@ -761,6 +761,7 @@ from django.utils.html import strip_tags
def _task_send_individual_email(subject, body_message, recipient, attachments,sender,job): def _task_send_individual_email(subject, body_message, recipient, attachments,sender,job):
"""Internal helper to create and send a single email.""" """Internal helper to create and send a single email."""
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa') from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
is_html = '<' in body_message and '>' in body_message is_html = '<' in body_message and '>' in body_message
@ -780,7 +781,8 @@ def _task_send_individual_email(subject, body_message, recipient, attachments,se
try: try:
result=email_obj.send(fail_silently=False) result=email_obj.send(fail_silently=False)
if result==1: if result==1 and sender and job: # job is none when email sent after message creation
try: try:
user=get_object_or_404(User,email=recipient) user=get_object_or_404(User,email=recipient)
new_message = Message.objects.create( new_message = Message.objects.create(
@ -798,7 +800,7 @@ def _task_send_individual_email(subject, body_message, recipient, attachments,se
else: else:
logger.error("fialed to send email") logger.error("failed to send email")
except Exception as e: except Exception as e:

View File

@ -4674,55 +4674,40 @@ def message_create(request):
message.sender = request.user message.sender = request.user
message.save() message.save()
# Send email if message_type is 'email' and recipient has email # Send email if message_type is 'email' and recipient has email
if message.message_type == 'email' and message.recipient and message.recipient.email:
if message.recipient and message.recipient.email:
try: try:
from .email_service import send_bulk_email
email_result = send_bulk_email(
email_result = async_task('recruitment.tasks._task_send_individual_email',
subject=message.subject, subject=message.subject,
message=message.content, body_message=message.content,
recipient_list=[message.recipient.email], recipient=message.recipient.email,
request=request,
attachments=None, attachments=None,
async_task_=True, sender=False,
from_interview=False job=False
) )
if email_result:
if email_result["success"]:
message.is_email_sent = True
message.email_address = message.recipient.email
message.save(update_fields=['is_email_sent', 'email_address'])
messages.success(request, "Message sent successfully via email!") messages.success(request, "Message sent successfully via email!")
else: else:
messages.warning(request, f"Message saved but email failed: {email_result.get('message', 'Unknown error')}")
messages.warning(request, f"email failed: {email_result.get('message', 'Unknown error')}")
except Exception as e: except Exception as e:
messages.warning(request, f"Message saved but email sending failed: {str(e)}") messages.warning(request, f"Message saved but email sending failed: {str(e)}")
else: else:
messages.success(request, "Message sent successfully!") messages.success(request, "Message sent successfully!")
["recipient", "job", "subject", "content", "message_type"]
recipient_email = form.cleaned_data['recipient'].email # Assuming recipient is a User or Model with an 'email' field
subject = form.cleaned_data['subject']
custom_message = form.cleaned_data['content']
job_id = form.cleaned_data['job'].id if 'job' in form.cleaned_data and form.cleaned_data['job'] else None
sender_user_id = request.user.id
task_id = async_task(
'recruitment.tasks.send_bulk_email_task',
subject,
custom_message, # Pass the custom message
[recipient_email], # Pass the specific recipient as a list of one
sender_user_id=sender_user_id,
job_id=job_id,
hook='recruitment.tasks.email_success_hook')
logger.info(f"{task_id} queued.")
return redirect("message_list") return redirect("message_list")
else: else:
messages.error(request, "Please correct the errors below.") messages.error(request, "Please correct the errors below.")
else: else:
form = MessageForm(request.user) form = MessageForm(request.user)
context = { context = {
@ -4759,27 +4744,21 @@ def message_reply(request, message_id):
message.save() message.save()
# Send email if message_type is 'email' and recipient has email # Send email if message_type is 'email' and recipient has email
if message.message_type == 'email' and message.recipient and message.recipient.email: if message.recipient and message.recipient.email:
try: try:
from .email_service import send_bulk_email email_result = async_task('recruitment.tasks._task_send_individual_email',
email_result = send_bulk_email(
subject=message.subject, subject=message.subject,
message=message.content, body_message=message.content,
recipient_list=[message.recipient.email], recipient=message.recipient.email,
request=request,
attachments=None, attachments=None,
async_task_=True, sender=False,
from_interview=False job=False
) )
if email_result:
if email_result["success"]: messages.success(request, "Message sent successfully via email!")
message.is_email_sent = True
message.email_address = message.recipient.email
message.save(update_fields=['is_email_sent', 'email_address'])
messages.success(request, "Reply sent successfully via email!")
else: else:
messages.warning(request, f"Reply saved but email failed: {email_result.get('message', 'Unknown error')}")
messages.warning(request, f"email failed: {email_result.get('message', 'Unknown error')}")
except Exception as e: except Exception as e:
messages.warning(request, f"Reply saved but email sending failed: {str(e)}") messages.warning(request, f"Reply saved but email sending failed: {str(e)}")
@ -5763,15 +5742,15 @@ def send_interview_email(request, slug):
return redirect("meeting_details", slug=meeting.slug) return redirect("meeting_details", slug=meeting.slug)
# def schedule_interview_location_form(request,slug): def schedule_interview_location_form(request,slug):
# schedule=get_object_or_404(InterviewSchedule,slug=slug) schedule=get_object_or_404(InterviewSchedule,slug=slug)
# if request.method=='POST': if request.method=='POST':
# form=InterviewScheduleLocationForm(request.POST,instance=schedule) form=InterviewScheduleLocationForm(request.POST,instance=schedule)
# form.save() form.save()
# return redirect('list_meetings') return redirect('list_meetings')
# else: else:
# form=InterviewScheduleLocationForm(instance=schedule) form=InterviewScheduleLocationForm(instance=schedule)
# return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule}) return render(request,'interviews/schedule_interview_location_form.html',{'form':form,'schedule':schedule})
class MeetingListView(ListView): class MeetingListView(ListView):
@ -5783,6 +5762,7 @@ class MeetingListView(ListView):
context_object_name = "meetings" context_object_name = "meetings"
paginate_by = 100 paginate_by = 100
def get_queryset(self): def get_queryset(self):
# Start with a base queryset, ensuring an InterviewLocation link exists. # Start with a base queryset, ensuring an InterviewLocation link exists.
queryset = super().get_queryset().filter(interview_location__isnull=False).select_related( queryset = super().get_queryset().filter(interview_location__isnull=False).select_related(
@ -5794,6 +5774,7 @@ class MeetingListView(ListView):
'interview_location__zoommeetingdetails', 'interview_location__zoommeetingdetails',
'interview_location__onsitelocationdetails', 'interview_location__onsitelocationdetails',
) )
# Note: Printing the queryset here can consume memory for large sets. # Note: Printing the queryset here can consume memory for large sets.
# Get filters from GET request # Get filters from GET request
@ -5807,11 +5788,10 @@ class MeetingListView(ListView):
if type_filter: if type_filter:
# Use .title() to handle case variations from URL (e.g., 'remote' -> 'Remote') # Use .title() to handle case variations from URL (e.g., 'remote' -> 'Remote')
normalized_type = type_filter.title() normalized_type = type_filter.title()
print(normalized_type)
# Assuming InterviewLocation.LocationType is accessible/defined # Assuming InterviewLocation.LocationType is accessible/defined
if normalized_type in ['Remote', 'Onsite']: if normalized_type in ['Remote', 'Onsite']:
queryset = queryset.filter(interview_location__location_type=normalized_type) queryset = queryset.filter(interview_location__location_type=normalized_type)
print(queryset)
# 3. Search by Topic (stored on InterviewLocation) # 3. Search by Topic (stored on InterviewLocation)
if search_query: if search_query:
@ -5886,6 +5866,28 @@ class MeetingListView(ListView):
return context return context
# class MeetingListView(ListView):
# """
# A unified view to list both Remote and Onsite Scheduled Interviews.
# """
# model = InterviewLocation
# template_name = "meetings/list_meetings.html"
# context_object_name = "meetings"
# def get_queryset(self):
# # Start with a base queryset, ensuring an InterviewLocation link exists.
# queryset = super().get_queryset().prefetch_related(
# 'zoommeetingdetails',
# 'onsitelocationdetails',
# )
# print(queryset)
# return queryset
def reschedule_onsite_meeting(request, slug, candidate_id, meeting_id): def reschedule_onsite_meeting(request, slug, candidate_id, meeting_id):
"""Handles the rescheduling of an Onsite Interview (updates OnsiteLocationDetails).""" """Handles the rescheduling of an Onsite Interview (updates OnsiteLocationDetails)."""
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)

View File

@ -295,6 +295,18 @@
</span> </span>
</a> </a>
</li> </li>
<li class="nav-item me-lg-4">
<a class="nav-link {% if request.resolver_match.url_name == 'kaauh_career' %}active{% endif %}" href="{% url 'kaauh_career' %}">
<span class="d-flex align-items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />
</svg>
{% trans "Career Page" %}
</span>
</a>
</li>
{% comment %} <li class="nav-item dropdown ms-lg-2"> {% comment %} <li class="nav-item dropdown ms-lg-2">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
data-bs-offset="0, 8" data-bs-auto-close="outside"> data-bs-offset="0, 8" data-bs-auto-close="outside">

View File

@ -326,15 +326,15 @@
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action"> <a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action">
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applicants" %} <i class="fas fa-layer-group me-1"></i> {% trans "Manage Applicants" %}
</a> </a>
{% if not job.zip_created%}
<a href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action"> <a href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action">
<i class="fa-solid fa-download me-1"></i> {% trans "Download All CVs" %} <i class="fa-solid fa-download me-1"></i> {% trans "Generate All CVs" %}
</a> </a>
{% else %}
<a href="{% url 'download_ready_cvs' job.slug %}" class="btn btn-main-action"> <a href="{% url 'download_ready_cvs' job.slug %}" class="btn btn-outline-primary">
<i class="fa-solid fa-eye me-1"></i> {% trans "View All CVs" %} <i class="fa-solid fa-eye me-1"></i> {% trans "View All CVs" %}
</a> </a>
{% endif %}
</div> </div>
</div> </div>

View File

@ -9,285 +9,265 @@
:root { :root {
--kaauh-teal: #00636e; --kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53; --kaauh-teal-dark: #004a53;
--kaauh-border: #e9ecef; --kaauh-border: #eaeff3;
--kaauh-primary-text: #212529; --kaauh-primary-text: #343a40;
--kaauh-success: #198754; --kaauh-success: #28a745;
--kaauh-info: #0dcaf0; --kaauh-info: #17a2b8;
--kaauh-danger: #dc3545; --kaauh-danger: #dc3545;
--kaauh-warning: #ffc107; --kaauh-warning: #ffc107;
--kaauh-bg-light: #f8f9fa;
}
.text-primary-teal {
color: var(--kaauh-teal) !important;
} }
.kaauh-card { .kaauh-card {
border: none; border: 1px solid var(--kaauh-border);
border-radius: 0.75rem; border-radius: 0.75rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white; background-color: white;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.kaauh-card:hover {
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
} }
.btn-main-action { .btn-main-action {
background-color: var(--kaauh-teal); background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal); border-color: var(--kaauh-teal);
color: white; color: white;
font-weight: 500; font-weight: 600;
padding: 0.5rem 1.2rem;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.btn-main-action:hover { .btn-main-action:hover {
background-color: var(--kaauh-teal-dark); background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark); border-color: var(--kaauh-teal-dark);
color: white; box-shadow: 0 4px 8px rgba(0,0,0,0.15);
transform: translateY(-1px);
}
.btn-white {
background-color: white;
border-color: var(--kaauh-border);
color: var(--kaauh-primary-text);
}
.btn-white:hover {
background-color: var(--kaauh-bg-light);
border-color: #dee2e6;
} }
.status-badge { .status-badge {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; padding: 0.3em 0.7em;
letter-spacing: 0.5px; border-radius: 0.35rem;
text-transform: uppercase; font-weight: 700;
}
.status-ACTIVE {
background-color: #d1e7dd;
color: #0f5132;
}
.status-EXPIRED {
background-color: #f8d7da;
color: #842029;
}
.status-COMPLETED {
background-color: #cff4fc;
color: #055160;
}
.status-CANCELLED {
background-color: #fff3cd;
color: #664d03;
}
.bg-soft-primary {
background-color: rgba(13, 110, 253, 0.1);
}
.bg-soft-info {
background-color: rgba(13, 202, 240, 0.1);
}
.bg-soft-success {
background-color: rgba(25, 135, 84, 0.1);
}
.avatar-circle {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.w-20 {
width: 20px;
text-align: center;
} }
.status-ACTIVE { background-color: var(--kaauh-success); color: white; }
.status-EXPIRED { background-color: var(--kaauh-danger); color: white; }
.status-COMPLETED { background-color: var(--kaauh-info); color: white; }
.status-CANCELLED { background-color: var(--kaauh-warning); color: #856404; }
.progress-ring { .progress-ring {
width: 120px;
height: 120px;
position: relative;
}
.progress-ring-circle {
transition: stroke-dashoffset 0.35s;
transform: rotate(-90deg); transform: rotate(-90deg);
transform-origin: 50% 50%; transform-origin: 50% 50%;
} }
.progress-ring-circle { .progress-ring-text {
transition: stroke-dashoffset 0.5s ease-in-out; position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.5rem;
font-weight: 700;
color: var(--kaauh-teal-dark);
}
.message-item {
border-left: 4px solid var(--kaauh-teal);
background-color: #f8f9fa;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 0 0.5rem 0.5rem 0;
}
.message-item.unread {
border-left-color: var(--kaauh-info);
background-color: #e7f3ff;
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid py-5 bg-light"> <div class="container-fluid py-4">
<!-- Header --> <!-- Header -->
<div class="row mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div class="col-12"> <div>
<div class="d-flex justify-content-between align-items-start"> <h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<div> <i class="fas fa-tasks me-2"></i>
<nav aria-label="breadcrumb" class="mb-2"> {{ assignment.agency.name }} - {{ assignment.job.title }}
<ol class="breadcrumb mb-0 small"> </h1>
<li class="breadcrumb-item"><a href="{% url 'agency_assignment_list' %}" <p class="text-muted mb-0">
class="text-decoration-none text-muted">{% trans "Assignments" %}</a></li> {% trans "Assignment Details and Management" %}
<li class="breadcrumb-item active" aria-current="page">{{ assignment.job.title }}</li> </p>
</ol> </div>
</nav> <div>
<h1 class="h2 fw-bold text-dark mb-2"> <a href="{% url 'agency_assignment_list' %}" class="btn btn-outline-secondary me-2">
{{ assignment.job.title }} <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Assignments" %}
</h1> </a>
<div class="d-flex align-items-center text-muted"> <a href="{% url 'agency_assignment_update' assignment.slug %}" class="btn btn-main-action">
<span class="me-3"><i class="fas fa-building me-1"></i> {{ assignment.agency.name }}</span> <i class="fas fa-edit me-1"></i> {% trans "Edit Assignment" %}
<span class="badge status-{{ assignment.status }} rounded-pill px-3">{{ </a>
assignment.get_status_display }}</span>
</div>
</div>
<div class="d-flex gap-2">
<a href="{% url 'agency_assignment_list' %}" class="btn btn-white border shadow-sm">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back" %}
</a>
<a href="{% url 'agency_assignment_update' assignment.slug %}"
class="btn btn-main-action shadow-sm">
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
</a>
</div>
</div>
</div> </div>
</div> </div>
<div class="row g-4"> <div class="row">
<!-- Left Column: Details & Candidates --> <!-- Assignment Overview -->
<div class="col-lg-8 col-md-12"> <div class="col-lg-8">
<!-- Assignment Details Card --> <!-- Assignment Details Card -->
<div class="kaauh-card mb-4"> <div class="kaauh-card p-4 mb-4">
<div class="card-header bg-transparent border-bottom py-3 px-4"> <h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
<h5 class="mb-0 fw-bold text-dark"> <i class="fas fa-info-circle me-2"></i>
<i class="fas fa-info-circle me-2 text-primary-teal"></i> {% trans "Assignment Details" %}
{% trans "Assignment Details" %} </h5>
</h5>
</div> <div class="row">
<div class="card-body p-4"> <div class="col-md-6">
<div class="row g-4"> <div class="mb-3">
<div class="col-md-6"> <label class="text-muted small">{% trans "Agency" %}</label>
<div class="detail-group"> <div class="fw-bold">{{ assignment.agency.name }}</div>
<label class="text-uppercase text-muted small fw-bold mb-1">{% trans "Agency" %}</label> <div class="text-muted small">{{ assignment.agency.contact_person }}</div>
<div class="fs-6 fw-medium text-dark">{{ assignment.agency.name }}</div> </div>
<div class="text-muted small">{{ assignment.agency.contact_person }}</div> <div class="mb-3">
<label class="text-muted small">{% trans "Job" %}</label>
<div class="fw-bold">{{ assignment.job.title }}</div>
<div class="text-muted small">{{ assignment.job.department }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="text-muted small">{% trans "Status" %}</label>
<div>
<span class="status-badge status-{{ assignment.status }}">
{{ assignment.get_status_display }}
</span>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="mb-3">
<div class="detail-group"> <label class="text-muted small">{% trans "Deadline" %}</label>
<label class="text-uppercase text-muted small fw-bold mb-1">{% trans "Department" <div class="{% if assignment.is_expired %}text-danger{% else %}text-muted{% endif %}">
%}</label> <i class="fas fa-calendar-alt me-1"></i>
<div class="fs-6 fw-medium text-dark">{{ assignment.job.department }}</div> {{ assignment.deadline_date|date:"Y-m-d H:i" }}
</div> </div>
</div> {% if assignment.is_expired %}
<div class="col-md-6"> <small class="text-danger">
<div class="detail-group">
<label class="text-uppercase text-muted small fw-bold mb-1">{% trans "Deadline"
%}</label>
<div
class="fs-6 fw-medium {% if assignment.is_expired %}text-danger{% else %}text-dark{% endif %}">
{{ assignment.deadline_date|date:"M d, Y - H:i" }}
</div>
{% if assignment.is_expired %}
<small class="text-danger fw-bold">
<i class="fas fa-exclamation-triangle me-1"></i>{% trans "Expired" %} <i class="fas fa-exclamation-triangle me-1"></i>{% trans "Expired" %}
</small> </small>
{% endif %} {% endif %}
</div>
</div>
<div class="col-md-6">
<div class="detail-group">
<label class="text-uppercase text-muted small fw-bold mb-1">{% trans "Created At"
%}</label>
<div class="fs-6 fw-medium text-dark">{{ assignment.created_at|date:"M d, Y" }}</div>
</div>
</div> </div>
</div> </div>
{% if assignment.admin_notes %}
<div class="mt-4 pt-4 border-top">
<label class="text-uppercase text-muted small fw-bold mb-2">{% trans "Admin Notes" %}</label>
<div class="p-3 bg-light rounded border-start border-4 border-info text-muted">
{{ assignment.admin_notes }}
</div>
</div>
{% endif %}
</div> </div>
{% if assignment.admin_notes %}
<div class="mt-3 pt-3 border-top">
<label class="text-muted small">{% trans "Admin Notes" %}</label>
<div class="text-muted">{{ assignment.admin_notes }}</div>
</div>
{% endif %}
</div> </div>
{% comment %} <div class="kaauh-card shadow-sm mb-4">
<div class="card-body my-2">
<h5 class="card-title mb-3 mx-2">
<i class="fas fa-key me-2 text-warning"></i>
{% trans "Access Credentials" %}
</h5>
<div class="mb-3 mx-2">
<label class="form-label text-muted small">{% trans "Login URL" %}</label>
<div class="input-group">
<input type="text" readonly value="{{ request.scheme }}://{{ request.get_host }}{% url 'agency_portal_login' %}"
class="form-control font-monospace" id="loginUrl">
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('loginUrl')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="mb-3 mx-2">
<label class="form-label text-muted small">{% trans "Access Token" %}</label>
<div class="input-group">
<input type="text" readonly value="{{ access_link.unique_token }}"
class="form-control font-monospace" id="accessToken">
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('accessToken')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="mb-3 mx-2">
<label class="form-label text-muted small">{% trans "Password" %}</label>
<div class="input-group">
<input type="text" readonly value="{{ access_link.access_password }}"
class="form-control font-monospace" id="accessPassword">
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('accessPassword')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="alert alert-info mx-2">
<i class="fas fa-info-circle me-2"></i>
{% trans "Share these credentials securely with the agency. They can use this information to log in and submit candidates." %}
</div>
{% if access_link %}
<a href="{% url 'agency_access_link_detail' access_link.slug %}"
class="btn btn-outline-info btn-sm mx-2">
<i class="fas fa-eye me-1"></i> {% trans "View Access Links Details" %}
</a>
{% endif %}
</div>
</div> {% endcomment %}
<!-- Candidates Card --> <!-- Candidates Card -->
<div class="kaauh-card"> <div class="kaauh-card p-4">
<div <div class="d-flex justify-content-between align-items-center mb-4">
class="card-header bg-transparent border-bottom py-3 px-4 d-flex justify-content-between align-items-center"> <h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<h5 class="mb-0 fw-bold text-dark"> <i class="fas fa-users me-2"></i>
<i class="fas fa-users me-2 text-primary-teal"></i> {% trans "Submitted Candidates" %} ({{ total_candidates }})
{% trans "Submitted Candidates" %}
<span class="badge bg-light text-dark border ms-2">{{ total_candidates }}</span>
</h5> </h5>
{% if access_link %} {% if access_link %}
<a href="{% url 'agency_portal_login' %}" target="_blank" class="btn btn-sm btn-outline-info"> <a href="{% url 'agency_portal_login' %}" target="_blank" class="btn btn-outline-info btn-sm">
<i class="fas fa-external-link-alt me-1"></i> {% trans "Portal Preview" %} <i class="fas fa-external-link-alt me-1"></i> {% trans "Preview Portal" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
<div class="card-body p-0">
{% if candidates %} {% if candidates %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover align-middle mb-0"> <table class="table table-hover">
<thead class="bg-light"> <thead>
<tr> <tr>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Candidate" <th>{% trans "Name" %}</th>
%}</th> <th>{% trans "Contact" %}</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Contact" %} <th>{% trans "Stage" %}</th>
</th> <th>{% trans "Submitted" %}</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Stage" %} <th>{% trans "Actions" %}</th>
</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Submitted"
%}</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted text-end">{% trans
"Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for candidate in candidates %} {% for candidate in candidates %}
<tr> <tr>
<td class="px-4"> <td>
<div class="d-flex align-items-center"> <div class="fw-bold">{{ candidate.name }}</div>
<div class="avatar-circle me-3 bg-soft-primary text-primary fw-bold"> </td>
{{ candidate.name|slice:":2"|upper }} <td>
</div> <div class="small">
<div> <div><i class="fas fa-envelope me-1"></i> {{ candidate.email }}</div>
<div class="fw-bold text-dark">{{ candidate.name }}</div> <div><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</div>
</div>
</div> </div>
</td> </td>
<td class="px-4"> <td>
<span class="badge bg-info">{{ candidate.get_stage_display }}</span>
</td>
<td>
<div class="small text-muted"> <div class="small text-muted">
<div class="mb-1"><i class="fas fa-envelope me-2 w-20"></i>{{ {{ candidate.created_at|date:"Y-m-d H:i" }}
candidate.email }}</div>
<div><i class="fas fa-phone me-2 w-20"></i>{{ candidate.phone }}</div>
</div> </div>
</td> </td>
<td class="px-4"> <td>
<span class="badge bg-soft-info text-info rounded-pill px-3">{{
candidate.get_stage_display }}</span>
</td>
<td class="px-4">
<span class="small text-muted">{{ candidate.created_at|date:"M d, Y" }}</span>
</td>
<td class="px-4 text-end">
<a href="{% url 'candidate_detail' candidate.slug %}" <a href="{% url 'candidate_detail' candidate.slug %}"
class="btn btn-sm btn-white border shadow-sm text-primary" class="btn btn-sm btn-outline-primary" title="{% trans 'View Details' %}">
title="{% trans 'View Details' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
</td> </td>
@ -296,103 +276,130 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{% else %} {% else %}
<div class="text-center py-5"> <div class="text-center py-4">
<div class="mb-3"> <i class="fas fa-users fa-2x text-muted mb-3"></i>
<div class="avatar-circle bg-light text-muted mx-auto" <h6 class="text-muted">{% trans "No candidates submitted yet" %}</h6>
style="width: 64px; height: 64px; font-size: 24px;"> <p class="text-muted small">
<i class="fas fa-user-plus"></i> {% trans "Candidates will appear here once the agency submits them through their portal." %}
</div>
</div>
<h6 class="fw-bold text-dark">{% trans "No candidates yet" %}</h6>
<p class="text-muted small mb-0">
{% trans "Candidates submitted by the agency will appear here." %}
</p> </p>
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>
</div> </div>
<!-- Right Column: Sidebar --> <!-- Sidebar -->
<div class="col-lg-4 col-md-12"> <div class="col-lg-4">
<!-- Progress Card --> <!-- Progress Card -->
<div class="kaauh-card mb-4"> <div class="kaauh-card p-4 mb-4">
<div class="card-body p-4 text-center"> <h5 class="mb-4 text-center" style="color: var(--kaauh-teal-dark);">
<h6 class="text-uppercase text-muted small fw-bold mb-4">{% trans "Submission Goal" %}</h6> {% trans "Submission Progress" %}
</h5>
<div class="position-relative d-inline-block mb-3"> <div class="text-center mb-3">
<svg class="progress-ring" width="140" height="140"> <div class="progress-ring">
<circle class="progress-ring-bg" stroke="#f1f3f5" stroke-width="10" fill="transparent" <svg width="120" height="120">
r="60" cx="70" cy="70" /> <circle class="progress-ring-circle"
<circle class="progress-ring-circle" stroke="var(--kaauh-teal)" stroke-width="10" stroke="#e9ecef"
fill="transparent" r="60" cx="70" cy="70" stroke-width="8"
style="stroke-dasharray: 376.99; stroke-dashoffset: {{ stroke_dashoffset }};" /> fill="transparent"
r="52"
cx="60"
cy="60"/>
<circle class="progress-ring-circle"
stroke="var(--kaauh-teal)"
stroke-width="8"
fill="transparent"
r="52"
cx="60"
cy="60"
style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/>
</svg> </svg>
<div class="position-absolute top-50 start-50 translate-middle text-center"> <div class="progress-ring-text">
<div class="h3 fw-bold mb-0 text-dark">{{ total_candidates }}</div> {% widthratio total_candidates assignment.max_candidates 100 as progress %}
<div class="small text-muted text-uppercase">{% trans "of" %} {{ assignment.max_candidates {{ progress|floatformat:0 }}%
}}</div>
</div> </div>
</div> </div>
</div>
<p class="text-muted small mb-0"> <div class="text-center">
{% trans "Candidates submitted" %} <div class="h4 mb-1">{{ total_candidates }}</div>
</p> <div class="text-muted">/ {{ assignment.max_candidates }} {% trans "candidates" %}</div>
</div>
<div class="progress mt-3" style="height: 8px;">
{% widthratio total_candidates assignment.max_candidates 100 as progress %}
<div class="progress-bar" style="width: {{ progress }}%"></div>
</div> </div>
</div> </div>
<!-- Actions Card -->
<div class="kaauh-card mb-4">
<div class="card-header bg-transparent border-bottom py-3 px-4">
<h6 class="mb-0 fw-bold text-dark">{% trans "Quick Actions" %}</h6>
</div>
<div class="card-body p-4">
<div class="d-grid gap-3">
<a href="" class="btn btn-outline-primary">
<i class="fas fa-envelope me-2"></i> {% trans "Send Message" %}
</a>
{% if assignment.is_active and not assignment.is_expired %} <!-- Actions Card -->
<button type="button" class="btn btn-outline-warning" data-bs-toggle="modal" <div class="kaauh-card p-4">
data-bs-target="#extendDeadlineModal"> <h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-clock me-2"></i> {% trans "Extend Deadline" %} <i class="fas fa-cog me-2"></i>
{% trans "Actions" %}
</h5>
<div class="d-grid gap-2">
<a href=""
class="btn btn-outline-primary">
<i class="fas fa-envelope me-1"></i> {% trans "Send Message" %}
</a>
{% if assignment.is_active and not assignment.is_expired %}
<button type="button" class="btn btn-outline-warning"
data-bs-toggle="modal" data-bs-target="#extendDeadlineModal">
<i class="fas fa-clock me-1"></i> {% trans "Extend Deadline" %}
</button> </button>
{% endif %}
<a href="{% url 'agency_assignment_update' assignment.slug %}"
class="btn btn-outline-secondary">
<i class="fas fa-edit me-1"></i> {% trans "Edit Assignment" %}
</a>
</div>
</div>
</div>
</div>
<!-- Messages Section -->
{% if messages_ %}
<div class="kaauh-card p-4 mt-4">
<h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-comments me-2"></i>
{% trans "Recent Messages" %}
</h5>
<div class="row">
{% for message in messages_|slice:":6" %}
<div class="col-lg-6 mb-3">
<div class="message-item {% if not message.is_read %}unread{% endif %}">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="fw-bold">{{ message.subject }}</div>
<small class="text-muted">{{ message.created_at|date:"Y-m-d H:i" }}</small>
</div>
<div class="small text-muted mb-2">
{% trans "From" %}: {{ message.sender.get_full_name }}
</div>
<div class="small">{{ message.message|truncatewords:30 }}</div>
{% if not message.is_read %}
<span class="badge bg-info mt-2">{% trans "New" %}</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
<!-- Recent Messages --> {% if messages_.count > 6 %}
{% if messages_ %} <div class="text-center mt-3">
<div class="kaauh-card"> <a href="#" class="btn btn-outline-primary btn-sm">
<div {% trans "View All Messages" %}
class="card-header bg-transparent border-bottom py-3 px-4 d-flex justify-content-between align-items-center"> </a>
<h6 class="mb-0 fw-bold text-dark">{% trans "Recent Messages" %}</h6>
{% if messages_.count > 3 %}
<a href="#" class="small text-decoration-none">{% trans "View All" %}</a>
{% endif %}
</div> </div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for message in messages_|slice:":3" %}
<div
class="list-group-item p-3 border-bottom-0 {% if not message.is_read %}bg-soft-info{% endif %}">
<div class="d-flex justify-content-between mb-1">
<span class="fw-bold text-dark small">{{ message.sender.get_full_name }}</span>
<small class="text-muted" style="font-size: 0.7rem;">{{ message.created_at|date:"M d"
}}</small>
</div>
<div class="fw-medium small text-dark mb-1">{{ message.subject }}</div>
<div class="text-muted small text-truncate">{{ message.message }}</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %} {% endif %}
</div> </div>
</div> {% endif %}
</div> </div>
<!-- Extend Deadline Modal --> <!-- Extend Deadline Modal -->
@ -413,8 +420,8 @@
<label for="new_deadline" class="form-label"> <label for="new_deadline" class="form-label">
{% trans "New Deadline" %} <span class="text-danger">*</span> {% trans "New Deadline" %} <span class="text-danger">*</span>
</label> </label>
<input type="datetime-local" class="form-control" id="new_deadline" name="new_deadline" <input type="datetime-local" class="form-control" id="new_deadline"
required> name="new_deadline" required>
<small class="form-text text-muted"> <small class="form-text text-muted">
{% trans "Current deadline:" %} {{ assignment.deadline_date|date:"Y-m-d H:i" }} {% trans "Current deadline:" %} {{ assignment.deadline_date|date:"Y-m-d H:i" }}
</small> </small>
@ -436,13 +443,13 @@
{% block customJS %} {% block customJS %}
<script> <script>
function copyToClipboard(text) { function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function () { navigator.clipboard.writeText(text).then(function() {
// Show success message // Show success message
const toast = document.createElement('div'); const toast = document.createElement('div');
toast.className = 'position-fixed top-0 end-0 p-3'; toast.className = 'position-fixed top-0 end-0 p-3';
toast.style.zIndex = '1050'; toast.style.zIndex = '1050';
toast.innerHTML = ` toast.innerHTML = `
<div class="toast show" role="alert"> <div class="toast show" role="alert">
<div class="toast-header"> <div class="toast-header">
<strong class="me-auto">{% trans "Success" %}</strong> <strong class="me-auto">{% trans "Success" %}</strong>
@ -453,61 +460,61 @@
</div> </div>
</div> </div>
`; `;
document.body.appendChild(toast); document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
});
}
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
element.select();
document.execCommand('copy');
// Show feedback
const button = element.nextElementSibling;
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
button.classList.add('btn-success');
button.classList.remove('btn-outline-secondary');
setTimeout(() => { setTimeout(() => {
button.innerHTML = originalHTML; toast.remove();
button.classList.remove('btn-success'); }, 3000);
button.classList.add('btn-outline-secondary');
}, 2000);
}
function confirmDeactivate() {
if (confirm('{% trans "Are you sure you want to deactivate this access link? Agencies will no longer be able to use it." %}')) {
// Submit form to deactivate
window.location.href = '';
}
}
function confirmReactivate() {
if (confirm('{% trans "Are you sure you want to reactivate this access link?" %}')) {
// Submit form to reactivate
window.location.href = '';
}
}
document.addEventListener('DOMContentLoaded', function () {
// Set minimum datetime for new deadline
const deadlineInput = document.getElementById('new_deadline');
if (deadlineInput) {
const currentDeadline = new Date('{{ assignment.deadline_date|date:"Y-m-d\\TH:i" }}');
const now = new Date();
const minDateTime = new Date(Math.max(currentDeadline, now));
const localDateTime = new Date(minDateTime.getTime() - minDateTime.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16);
deadlineInput.min = localDateTime;
deadlineInput.value = localDateTime;
}
}); });
}
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
element.select();
document.execCommand('copy');
// Show feedback
const button = element.nextElementSibling;
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
button.classList.add('btn-success');
button.classList.remove('btn-outline-secondary');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 2000);
}
function confirmDeactivate() {
if (confirm('{% trans "Are you sure you want to deactivate this access link? Agencies will no longer be able to use it." %}')) {
// Submit form to deactivate
window.location.href = '';
}
}
function confirmReactivate() {
if (confirm('{% trans "Are you sure you want to reactivate this access link?" %}')) {
// Submit form to reactivate
window.location.href = '';
}
}
document.addEventListener('DOMContentLoaded', function() {
// Set minimum datetime for new deadline
const deadlineInput = document.getElementById('new_deadline');
if (deadlineInput) {
const currentDeadline = new Date('{{ assignment.deadline_date|date:"Y-m-d\\TH:i" }}');
const now = new Date();
const minDateTime = new Date(Math.max(currentDeadline, now));
const localDateTime = new Date(minDateTime.getTime() - minDateTime.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16);
deadlineInput.min = localDateTime;
deadlineInput.value = localDateTime;
}
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -165,7 +165,7 @@
<tr class="person-row"> <tr class="person-row">
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-2" <div class="rounded-circle bg-primary-theme text-white d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; font-size: 14px; font-weight: 600;"> style="width: 32px; height: 32px; font-size: 14px; font-weight: 600;">
{{ person.first_name|first|upper }}{{ person.last_name|first|upper }} {{ person.first_name|first|upper }}{{ person.last_name|first|upper }}
</div> </div>
@ -178,7 +178,7 @@
</div> </div>
</td> </td>
<td> <td>
<a href="mailto:{{ person.email }}" class="text-decoration-none"> <a href="mailto:{{ person.email }}" class="text-decoration-none text-dark">
{{ person.email }} {{ person.email }}
</a> </a>
</td> </td>

File diff suppressed because it is too large Load Diff

View File

@ -662,6 +662,7 @@
<i class="fas fa-eye me-1"></i> <i class="fas fa-eye me-1"></i>
{% trans "View Actual Resume" %} {% trans "View Actual Resume" %}
</a> {% endcomment %} </a> {% endcomment %}
<a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary"> <a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary">
<i class="fas fa-download me-1"></i> <i class="fas fa-download me-1"></i>
{% trans "Download Resume" %} {% trans "Download Resume" %}

View File

@ -395,15 +395,14 @@
{% endwith %} {% endwith %}
</td> </td>
<td class="text-center"> <td class="text-center">
<button type="button" <button type="button" class="btn btn-outline-secondary btn-sm"
class="btn btn-sm btn-main-action" data-bs-toggle="modal"
data-bs-toggle="modal" data-bs-target="#candidateviewModal"
data-bs-target="#documentModal" hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
hx-get="{% url 'candidate_application_detail' candidate.slug %}" hx-target="#candidateviewModalBody"
hx-target="#documentModalBody" title="View Profile">
title="{% trans 'View Candidate Details' %}"> <i class="fas fa-eye ms-1"></i>
<i class="fas fa-eye me-1"></i> {% trans "View Details" %} </button>
</button>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -419,30 +418,49 @@
</form> </form>
</div> </div>
<!-- Modal for viewing candidate details -->
<div class="modal fade modal-xl" id="documentModal" tabindex="-1" aria-labelledby="documentModalLabel" aria-hidden="true"> </div>
<div class="modal-dialog">
<div class="modal-content kaauh-card"> <div class="modal fade modal-xl" id="candidateviewModal" tabindex="-1" aria-labelledby="candidateviewModalLabel" aria-hidden="true">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);"> <div class="modal-dialog">
<h5 class="modal-title" id="documentModalLabel" style="color: var(--kaauh-teal-dark);"> <div class="modal-content kaauh-card"> <div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
{% trans "Candidate Details" %} <h5 class="modal-title" id="candidateviewModalLabel" style="color: var(--kaauh-teal-dark);">
</h5> {% trans "Candidate Details / Bulk Action Form" %}
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </h5>
</div> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div id="documentModalBody" class="modal-body"> </div>
<div class="text-center py-5 text-muted"> <div id="candidateviewModalBody" class="modal-body">
<i class="fas fa-spinner fa-spin fa-2x"></i><br> <div class="text-center py-5 text-muted">
{% trans "Loading candidate details..." %} <i class="fas fa-spinner fa-spin fa-2x"></i><br>
</div> {% trans "Loading content..." %}
</div>
<div class="modal-footer" style="border-top: 1px solid var(--kaauh-border);">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
{% trans "Close" %}
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="emailModalLabel" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-envelope me-2"></i>{% trans "Compose Email" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="emailModalBody" class="modal-body">
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading email form..." %}
</div>
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}

File diff suppressed because it is too large Load Diff