fixes at while testing

This commit is contained in:
Faheed 2025-11-22 22:23:14 +03:00
parent 34e2224f80
commit 8a0f715145
8 changed files with 753 additions and 622 deletions

View File

@ -1634,7 +1634,7 @@ class CandidateEmailForm(forms.Form):
message_parts = [
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"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"https://kaauh/hire/exam",
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"Best regards, The KAAUH Hiring team"
]
elif candidate:
message_parts=""

View File

@ -99,9 +99,9 @@ class JobPosting(Base):
# Core Fields
title = models.CharField(max_length=200)
department = models.CharField(max_length=100, blank=True)
job_type = models.CharField(max_length=20, choices=JOB_TYPES, default="FULL_TIME")
job_type = models.CharField(max_length=20, choices=JOB_TYPES, default="Full-time")
workplace_type = models.CharField(
max_length=20, choices=WORKPLACE_TYPES, default="ON_SITE"
max_length=20, choices=WORKPLACE_TYPES, default="On-site"
)
# Location
@ -243,6 +243,11 @@ class JobPosting(Base):
help_text=_("Whether the job posting has been parsed by AI"),
verbose_name=_("AI Parsed"),
)
# Field to store the generated zip file
cv_zip_file = models.FileField(upload_to='job_zips/', null=True, blank=True)
# Field to track if the background task has completed
zip_created = models.BooleanField(default=False)
class Meta:
ordering = ["-created_at"]
@ -906,11 +911,35 @@ class Application(Base):
@property
def get_latest_meeting(self):
"""Legacy compatibility - get latest meeting for this application"""
"""
Retrieves the most specific location details (subclass instance)
of the latest ScheduledInterview for this application, or None.
"""
# 1. Get the latest ScheduledInterview
schedule = self.scheduled_interviews.order_by("-created_at").first()
if schedule:
return schedule.zoom_meeting
return None
# Check if a schedule exists and if it has an interview location
if not schedule or not schedule.interview_location:
return None
# Get the base location instance
interview_location = schedule.interview_location
# 2. Safely retrieve the specific subclass details
# Determine the expected subclass accessor name based on the location_type
if interview_location.location_type == 'Remote':
accessor_name = 'zoommeetingdetails'
else: # Assumes 'Onsite' or any other type defaults to Onsite
accessor_name = 'onsitelocationdetails'
# Use getattr to safely retrieve the specific meeting object (subclass instance).
# If the accessor exists but points to None (because the subclass record was deleted),
# or if the accessor name is wrong for the object's true type, it will return None.
meeting_details = getattr(interview_location, accessor_name, None)
return meeting_details
@property
def has_future_meeting(self):
@ -960,6 +989,14 @@ class Application(Base):
content_type = ContentType.objects.get_for_model(self.__class__)
return Document.objects.filter(content_type=content_type, object_id=self.id)
@property
def belong_to_an_agency(self):
if self.hiring_agency:
return True
else:
return False
class TrainingMaterial(Base):
@ -983,137 +1020,326 @@ class TrainingMaterial(Base):
return self.title
class OnsiteMeeting(Base):
class MeetingStatus(models.TextChoices):
class InterviewLocation(Base):
"""
Base model for all interview location/meeting details (remote or onsite)
using Multi-Table Inheritance.
"""
class LocationType(models.TextChoices):
REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)')
ONSITE = 'Onsite', _('In-Person (Physical Location)')
class Status(models.TextChoices):
"""Defines the possible real-time statuses for any interview location/meeting."""
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,
location_type = models.CharField(
max_length=10,
choices=LocationType.choices,
verbose_name=_("Location Type"),
db_index=True
)
details_url = models.URLField(
verbose_name=_("Meeting/Location URL"),
max_length=2048,
blank=True,
verbose_name=_("Status"),
default=MeetingStatus.WAITING,
null=True
)
topic = models.CharField( # Renamed from 'description' to 'topic' to match your input
max_length=255,
verbose_name=_("Location/Meeting Topic"),
blank=True,
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'")
)
timezone = models.CharField(
max_length=50,
verbose_name=_("Timezone"),
default='UTC'
)
def __str__(self):
# Use 'topic' instead of 'description'
return f"{self.get_location_type_display()} - {self.topic[:50]}"
class ZoomMeeting(Base):
class MeetingStatus(models.TextChoices):
WAITING = "waiting", _("Waiting")
STARTED = "started", _("Started")
ENDED = "ended", _("Ended")
CANCELLED = "cancelled", _("Cancelled")
class Meta:
verbose_name = _("Interview Location")
verbose_name_plural = _("Interview Locations")
# Basic meeting details
topic = models.CharField(max_length=255, verbose_name=_("Topic"))
meeting_id = models.CharField(
class ZoomMeetingDetails(InterviewLocation):
"""Concrete model for remote interviews (Zoom specifics)."""
status = models.CharField(
db_index=True,
max_length=20,
unique=True,
verbose_name=_("Meeting ID"), # Added index
) # Unique identifier for the meeting
choices=InterviewLocation.Status.choices,
default=InterviewLocation.Status.WAITING,
)
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"))
join_url = models.URLField(
verbose_name=_("Join URL")
) # URL for participants to join
participant_video = models.BooleanField(
default=True, verbose_name=_("Participant Video")
verbose_name=_("Duration (minutes)")
)
meeting_id = models.CharField(
db_index=True,
max_length=50,
unique=True,
verbose_name=_("External Meeting ID")
)
password = models.CharField(
max_length=20, blank=True, null=True, verbose_name=_("Password")
)
zoom_gateway_response = models.JSONField(
blank=True, null=True, verbose_name=_("Zoom Gateway Response")
)
participant_video = models.BooleanField(
default=True, verbose_name=_("Participant Video")
)
join_before_host = models.BooleanField(
default=False, verbose_name=_("Join Before Host")
)
host_email=models.CharField(null=True,blank=True)
mute_upon_entry = models.BooleanField(
default=False, verbose_name=_("Mute Upon Entry")
)
waiting_room = models.BooleanField(default=False, verbose_name=_("Waiting Room"))
zoom_gateway_response = models.JSONField(
blank=True, null=True, verbose_name=_("Zoom Gateway Response")
# *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation ***
# @classmethod
# def create(cls, **kwargs):
# """Factory method to ensure location_type is set to REMOTE."""
# return cls(location_type=InterviewLocation.LocationType.REMOTE, **kwargs)
class Meta:
verbose_name = _("Zoom Meeting Details")
verbose_name_plural = _("Zoom Meeting Details")
class OnsiteLocationDetails(InterviewLocation):
"""Concrete model for onsite interviews (Room/Address specifics)."""
physical_address = models.CharField(
max_length=255,
verbose_name=_("Physical Address"),
blank=True,
null=True
)
room_number = models.CharField(
max_length=50,
verbose_name=_("Room Number/Name"),
blank=True,
null=True
)
start_time = models.DateTimeField(
db_index=True, verbose_name=_("Start Time")
)
duration = models.PositiveIntegerField(
verbose_name=_("Duration (minutes)")
)
status = models.CharField(
db_index=True,
max_length=20, # Added index
max_length=20,
choices=InterviewLocation.Status.choices,
default=InterviewLocation.Status.WAITING,
)
# *** REVERTED TO @classmethod (Factory Method) for cleaner instantiation ***
# @classmethod
# def create(cls, **kwargs):
# """Factory method to ensure location_type is set to ONSITE."""
# return cls(location_type=InterviewLocation.LocationType.ONSITE, **kwargs)
class Meta:
verbose_name = _("Onsite Location Details")
verbose_name_plural = _("Onsite Location Details")
# --- 2. Scheduling Models ---
class InterviewSchedule(Base):
"""Stores the TEMPLATE criteria for BULK interview generation."""
# We need a field to store the template location details linked to this bulk schedule.
# This location object contains the generic Zoom/Onsite info to be cloned.
template_location = models.ForeignKey(
InterviewLocation,
on_delete=models.SET_NULL,
related_name="schedule_templates",
null=True,
blank=True,
verbose_name=_("Status"),
default=MeetingStatus.WAITING,
verbose_name=_("Location Template (Zoom/Onsite)")
)
# NOTE: schedule_interview_type field is needed in the form,
# but not on the model itself if we use template_location.
# If you want to keep it:
schedule_interview_type = models.CharField(
max_length=10,
choices=InterviewLocation.LocationType.choices,
verbose_name=_("Interview Type"),
default=InterviewLocation.LocationType.REMOTE
)
job = models.ForeignKey(
JobPosting,
on_delete=models.CASCADE,
related_name="interview_schedules",
db_index=True,
)
applications = models.ManyToManyField(
Application, related_name="interview_schedules", blank=True
)
start_date = models.DateField(db_index=True, verbose_name=_("Start Date"))
end_date = models.DateField(db_index=True, verbose_name=_("End Date"))
working_days = models.JSONField(
verbose_name=_("Working Days")
)
start_time = models.TimeField(verbose_name=_("Start Time"))
end_time = models.TimeField(verbose_name=_("End Time"))
break_start_time = models.TimeField(
verbose_name=_("Break Start Time"), null=True, blank=True
)
break_end_time = models.TimeField(
verbose_name=_("Break End Time"), null=True, blank=True
)
interview_duration = models.PositiveIntegerField(
verbose_name=_("Interview Duration (minutes)")
)
buffer_time = models.PositiveIntegerField(
verbose_name=_("Buffer Time (minutes)"), default=0
)
created_by = models.ForeignKey(
User, on_delete=models.CASCADE, db_index=True
)
# Timestamps
def __str__(self):
return self.topic
@property
def get_job(self):
return self.interview.job
@property
def get_candidate(self):
return self.interview.application.person
@property
def candidate_full_name(self):
return self.interview.application.person.full_name
@property
def get_participants(self):
return self.interview.job.participants.all()
@property
def get_users(self):
return self.interview.job.users.all()
return f"Schedule for {self.job.title}"
class MeetingComment(Base):
"""
Model for storing meeting comments/notes
"""
class ScheduledInterview(Base):
"""Stores individual scheduled interviews (whether bulk or individually created)."""
class InterviewStatus(models.TextChoices):
SCHEDULED = "scheduled", _("Scheduled")
CONFIRMED = "confirmed", _("Confirmed")
CANCELLED = "cancelled", _("Cancelled")
COMPLETED = "completed", _("Completed")
meeting = models.ForeignKey(
ZoomMeeting,
application = models.ForeignKey(
Application,
on_delete=models.CASCADE,
related_name="comments",
verbose_name=_("Meeting"),
related_name="scheduled_interviews",
db_index=True,
)
job = models.ForeignKey(
JobPosting,
on_delete=models.CASCADE,
related_name="scheduled_interviews",
db_index=True,
)
# Links to the specific, individual location/meeting details for THIS interview
interview_location = models.OneToOneField(
InterviewLocation,
on_delete=models.CASCADE,
related_name="scheduled_interview",
null=True,
blank=True,
db_index=True,
verbose_name=_("Meeting/Location Details")
)
# Link back to the bulk schedule template (optional if individually created)
schedule = models.ForeignKey(
InterviewSchedule,
on_delete=models.SET_NULL,
related_name="interviews",
null=True,
blank=True,
db_index=True,
)
participants = models.ManyToManyField('Participants', blank=True)
system_users = models.ManyToManyField(User, related_name="attended_interviews", blank=True)
interview_date = models.DateField(db_index=True, verbose_name=_("Interview Date"))
interview_time = models.TimeField(verbose_name=_("Interview Time"))
status = models.CharField(
db_index=True,
max_length=20,
choices=InterviewStatus.choices,
default=InterviewStatus.SCHEDULED,
)
def __str__(self):
return f"Interview with {self.application.person.full_name} for {self.job.title}"
class Meta:
indexes = [
models.Index(fields=["job", "status"]),
models.Index(fields=["interview_date", "interview_time"]),
models.Index(fields=["application", "job"]),
]
# --- 3. Interview Notes Model (Fixed) ---
class InterviewNote(Base):
"""Model for storing notes, feedback, or comments related to a specific ScheduledInterview."""
class NoteType(models.TextChoices):
FEEDBACK = 'Feedback', _('Candidate Feedback')
LOGISTICS = 'Logistics', _('Logistical Note')
GENERAL = 'General', _('General Comment')
1
interview = models.ForeignKey(
ScheduledInterview,
on_delete=models.CASCADE,
related_name="notes",
verbose_name=_("Scheduled Interview"),
db_index=True
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="meeting_comments",
related_name="interview_notes",
verbose_name=_("Author"),
db_index=True
)
content = CKEditor5Field(verbose_name=_("Content"), config_name="extends")
# Inherited from Base: created_at, updated_at, slug
note_type = models.CharField(
max_length=50,
choices=NoteType.choices,
default=NoteType.FEEDBACK,
verbose_name=_("Note Type")
)
content = CKEditor5Field(verbose_name=_("Content/Feedback"), config_name="extends")
class Meta:
verbose_name = _("Meeting Comment")
verbose_name_plural = _("Meeting Comments")
ordering = ["-created_at"]
verbose_name = _("Interview Note")
verbose_name_plural = _("Interview Notes")
ordering = ["created_at"]
def __str__(self):
return f"Comment by {self.author.get_username()} on {self.meeting.topic}"
return f"{self.get_note_type_display()} by {self.author.get_username()} on {self.interview.id}"
class FormTemplate(Base):
"""
@ -1926,143 +2152,6 @@ class BreakTime(models.Model):
return f"{self.start_time} - {self.end_time}"
class InterviewSchedule(Base):
"""Stores the scheduling criteria for interviews"""
class InterviewType(models.TextChoices):
REMOTE = "Remote", "Remote Interview"
ONSITE = "Onsite", "In-Person Interview"
interview_type = models.CharField(
max_length=10,
choices=InterviewType.choices,
default=InterviewType.REMOTE,
verbose_name="Interview Meeting Type",
)
job = models.ForeignKey(
JobPosting,
on_delete=models.CASCADE,
related_name="interview_schedules",
db_index=True,
)
applications = models.ManyToManyField(
Application, related_name="interview_schedules", blank=True, null=True
)
start_date = models.DateField(
db_index=True, verbose_name=_("Start Date")
) # Added index
end_date = models.DateField(
db_index=True, verbose_name=_("End Date")
) # Added index
working_days = models.JSONField(
verbose_name=_("Working Days")
) # Store days of week as [0,1,2,3,4] for Mon-Fri
start_time = models.TimeField(verbose_name=_("Start Time"))
end_time = models.TimeField(verbose_name=_("End Time"))
break_start_time = models.TimeField(
verbose_name=_("Break Start Time"), null=True, blank=True
)
break_end_time = models.TimeField(
verbose_name=_("Break End Time"), null=True, blank=True
)
interview_duration = models.PositiveIntegerField(
verbose_name=_("Interview Duration (minutes)")
)
buffer_time = models.PositiveIntegerField(
verbose_name=_("Buffer Time (minutes)"), default=0
)
created_by = models.ForeignKey(
User, on_delete=models.CASCADE, db_index=True
) # Added index
def __str__(self):
return f"Interview Schedule for {self.job.title}"
class Meta:
indexes = [
models.Index(fields=["start_date"]),
models.Index(fields=["end_date"]),
models.Index(fields=["created_by"]),
]
class ScheduledInterview(Base):
"""Stores individual scheduled interviews"""
application = models.ForeignKey(
Application,
on_delete=models.CASCADE,
related_name="scheduled_interviews",
db_index=True,
)
participants = models.ManyToManyField("Participants", blank=True)
system_users = models.ManyToManyField(User, blank=True)
job = models.ForeignKey(
"JobPosting",
on_delete=models.CASCADE,
related_name="scheduled_interviews",
db_index=True,
)
zoom_meeting = models.OneToOneField(
ZoomMeeting,
on_delete=models.CASCADE,
related_name="interview",
db_index=True,
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,
)
interview_date = models.DateField(
db_index=True, verbose_name=_("Interview Date")
) # Added index
interview_time = models.TimeField(verbose_name=_("Interview Time"))
status = models.CharField(
db_index=True,
max_length=20, # Added index
choices=[
("scheduled", _("Scheduled")),
("confirmed", _("Confirmed")),
("cancelled", _("Cancelled")),
("completed", _("Completed")),
],
default="scheduled",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return (
f"Interview with {self.application.person.full_name} for {self.job.title}"
)
class Meta:
indexes = [
models.Index(fields=["job", "status"]),
models.Index(fields=["interview_date", "interview_time"]),
models.Index(fields=["application", "job"]),
]
class Notification(models.Model):
@ -2101,7 +2190,7 @@ class Notification(models.Model):
verbose_name=_("Status"),
)
related_meeting = models.ForeignKey(
ZoomMeeting,
ZoomMeetingDetails,
on_delete=models.CASCADE,
related_name="notifications",
null=True,

View File

@ -93,8 +93,26 @@
<i class="fas fa-tachometer-alt me-1"></i> {% trans "Dashboard" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white" href="{% url 'kaauh_career' %}">
<i class="fas fa-hand me-1"></i> {% trans "KAAUH Careers" %}
</a>
</li>
{% endif %}
{% if request.user.is_authenticated %}
<li class="nav-item">
<a class="nav-link text-white" href="{% url 'user_detail' request.user.pk %}">
<i class="fas fa-user-circle me-1"></i> <span>{% trans "My Profile" %}</span></a></li>
{% endif %}
<li class="nav-item me-2">
<a class="nav-link text-white" href="{% url 'message_list' %}">
<i class="fas fa-envelope"></i> <span>{% trans "Messages" %}</span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown"
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
@ -120,16 +138,6 @@
</li>
</ul>
</li>
{% if request.user.is_authenticated %}
<li class="nav-item">
<a class="nav-link text-white" href="{% url 'user_detail' request.user.pk %}">
<i class="fas fa-user-circle me-1"></i> <span>{% trans "My Profile" %}</span></a></li>
{% endif %}
<li class="nav-item me-2">
<a class="nav-link text-white" href="{% url 'message_list' %}">
<i class="fas fa-envelope"></i> <span>{% trans "Messages" %}</span>
</a>
</li>
<li class="nav-item ms-3">
{% if request.user.is_authenticated %}

View File

@ -9,285 +9,265 @@
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #e9ecef;
--kaauh-primary-text: #212529;
--kaauh-success: #198754;
--kaauh-info: #0dcaf0;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-success: #28a745;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
--kaauh-bg-light: #f8f9fa;
}
.text-primary-teal {
color: var(--kaauh-teal) !important;
}
.kaauh-card {
border: none;
border: 1px solid var(--kaauh-border);
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;
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 {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 500;
padding: 0.5rem 1.2rem;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
color: white;
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;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.status-badge {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.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;
padding: 0.3em 0.7em;
border-radius: 0.35rem;
font-weight: 700;
}
.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 {
width: 120px;
height: 120px;
position: relative;
}
.progress-ring-circle {
transition: stroke-dashoffset 0.35s;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
.progress-ring-circle {
transition: stroke-dashoffset 0.5s ease-in-out;
.progress-ring-text {
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>
{% endblock %}
{% block content %}
<div class="container-fluid py-5 bg-light">
<div class="container-fluid py-4">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-start">
<div>
<nav aria-label="breadcrumb" class="mb-2">
<ol class="breadcrumb mb-0 small">
<li class="breadcrumb-item"><a href="{% url 'agency_assignment_list' %}"
class="text-decoration-none text-muted">{% trans "Assignments" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ assignment.job.title }}</li>
</ol>
</nav>
<h1 class="h2 fw-bold text-dark mb-2">
{{ assignment.job.title }}
</h1>
<div class="d-flex align-items-center text-muted">
<span class="me-3"><i class="fas fa-building me-1"></i> {{ assignment.agency.name }}</span>
<span class="badge status-{{ assignment.status }} rounded-pill px-3">{{
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 class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-tasks me-2"></i>
{{ assignment.agency.name }} - {{ assignment.job.title }}
</h1>
<p class="text-muted mb-0">
{% trans "Assignment Details and Management" %}
</p>
</div>
<div>
<a href="{% url 'agency_assignment_list' %}" class="btn btn-outline-secondary me-2">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Assignments" %}
</a>
<a href="{% url 'agency_assignment_update' assignment.slug %}" class="btn btn-main-action">
<i class="fas fa-edit me-1"></i> {% trans "Edit Assignment" %}
</a>
</div>
</div>
<div class="row g-4">
<!-- Left Column: Details & Candidates -->
<div class="col-lg-8 col-md-12">
<div class="row">
<!-- Assignment Overview -->
<div class="col-lg-8">
<!-- Assignment Details Card -->
<div class="kaauh-card mb-4">
<div class="card-header bg-transparent border-bottom py-3 px-4">
<h5 class="mb-0 fw-bold text-dark">
<i class="fas fa-info-circle me-2 text-primary-teal"></i>
{% trans "Assignment Details" %}
</h5>
</div>
<div class="card-body p-4">
<div class="row g-4">
<div class="col-md-6">
<div class="detail-group">
<label class="text-uppercase text-muted small fw-bold mb-1">{% trans "Agency" %}</label>
<div class="fs-6 fw-medium text-dark">{{ assignment.agency.name }}</div>
<div class="text-muted small">{{ assignment.agency.contact_person }}</div>
<div class="kaauh-card p-4 mb-4">
<h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-info-circle me-2"></i>
{% trans "Assignment Details" %}
</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="text-muted small">{% trans "Agency" %}</label>
<div class="fw-bold">{{ assignment.agency.name }}</div>
<div class="text-muted small">{{ assignment.agency.contact_person }}</div>
</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 class="col-md-6">
<div class="detail-group">
<label class="text-uppercase text-muted small fw-bold mb-1">{% trans "Department"
%}</label>
<div class="fs-6 fw-medium text-dark">{{ assignment.job.department }}</div>
<div class="mb-3">
<label class="text-muted small">{% trans "Deadline" %}</label>
<div class="{% if assignment.is_expired %}text-danger{% else %}text-muted{% endif %}">
<i class="fas fa-calendar-alt me-1"></i>
{{ assignment.deadline_date|date:"Y-m-d H:i" }}
</div>
</div>
<div class="col-md-6">
<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">
{% if assignment.is_expired %}
<small class="text-danger">
<i class="fas fa-exclamation-triangle me-1"></i>{% trans "Expired" %}
</small>
{% 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>
{% endif %}
</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>
{% 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>
{% 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 -->
<div class="kaauh-card">
<div
class="card-header bg-transparent border-bottom py-3 px-4 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold text-dark">
<i class="fas fa-users me-2 text-primary-teal"></i>
{% trans "Submitted Candidates" %}
<span class="badge bg-light text-dark border ms-2">{{ total_candidates }}</span>
<div class="kaauh-card p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-users me-2"></i>
{% trans "Submitted Candidates" %} ({{ total_candidates }})
</h5>
{% if access_link %}
<a href="{% url 'agency_portal_login' %}" target="_blank" class="btn btn-sm btn-outline-info">
<i class="fas fa-external-link-alt me-1"></i> {% trans "Portal Preview" %}
</a>
<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 "Preview Portal" %}
</a>
{% endif %}
</div>
<div class="card-body p-0">
{% if candidates %}
{% if candidates %}
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<table class="table table-hover">
<thead>
<tr>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Candidate"
%}</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Contact" %}
</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Stage" %}
</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>
<th>{% trans "Name" %}</th>
<th>{% trans "Contact" %}</th>
<th>{% trans "Stage" %}</th>
<th>{% trans "Submitted" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for candidate in candidates %}
<tr>
<td class="px-4">
<div class="d-flex align-items-center">
<div class="avatar-circle me-3 bg-soft-primary text-primary fw-bold">
{{ candidate.name|slice:":2"|upper }}
</div>
<div>
<div class="fw-bold text-dark">{{ candidate.name }}</div>
</div>
<td>
<div class="fw-bold">{{ candidate.name }}</div>
</td>
<td>
<div class="small">
<div><i class="fas fa-envelope me-1"></i> {{ candidate.email }}</div>
<div><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</div>
</div>
</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="mb-1"><i class="fas fa-envelope me-2 w-20"></i>{{
candidate.email }}</div>
<div><i class="fas fa-phone me-2 w-20"></i>{{ candidate.phone }}</div>
{{ candidate.created_at|date:"Y-m-d H:i" }}
</div>
</td>
<td class="px-4">
<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">
<td>
<a href="{% url 'candidate_detail' candidate.slug %}"
class="btn btn-sm btn-white border shadow-sm text-primary"
title="{% trans 'View Details' %}">
class="btn btn-sm btn-outline-primary" title="{% trans 'View Details' %}">
<i class="fas fa-eye"></i>
</a>
</td>
@ -296,103 +276,130 @@
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<div class="mb-3">
<div class="avatar-circle bg-light text-muted mx-auto"
style="width: 64px; height: 64px; font-size: 24px;">
<i class="fas fa-user-plus"></i>
</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." %}
{% else %}
<div class="text-center py-4">
<i class="fas fa-users fa-2x text-muted mb-3"></i>
<h6 class="text-muted">{% trans "No candidates submitted yet" %}</h6>
<p class="text-muted small">
{% trans "Candidates will appear here once the agency submits them through their portal." %}
</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Right Column: Sidebar -->
<div class="col-lg-4 col-md-12">
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Progress Card -->
<div class="kaauh-card mb-4">
<div class="card-body p-4 text-center">
<h6 class="text-uppercase text-muted small fw-bold mb-4">{% trans "Submission Goal" %}</h6>
<div class="kaauh-card p-4 mb-4">
<h5 class="mb-4 text-center" style="color: var(--kaauh-teal-dark);">
{% trans "Submission Progress" %}
</h5>
<div class="position-relative d-inline-block mb-3">
<svg class="progress-ring" width="140" height="140">
<circle class="progress-ring-bg" stroke="#f1f3f5" stroke-width="10" fill="transparent"
r="60" cx="70" cy="70" />
<circle class="progress-ring-circle" stroke="var(--kaauh-teal)" stroke-width="10"
fill="transparent" r="60" cx="70" cy="70"
style="stroke-dasharray: 376.99; stroke-dashoffset: {{ stroke_dashoffset }};" />
<div class="text-center mb-3">
<div class="progress-ring">
<svg width="120" height="120">
<circle class="progress-ring-circle"
stroke="#e9ecef"
stroke-width="8"
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>
<div class="position-absolute top-50 start-50 translate-middle text-center">
<div class="h3 fw-bold mb-0 text-dark">{{ total_candidates }}</div>
<div class="small text-muted text-uppercase">{% trans "of" %} {{ assignment.max_candidates
}}</div>
<div class="progress-ring-text">
{% widthratio total_candidates assignment.max_candidates 100 as progress %}
{{ progress|floatformat:0 }}%
</div>
</div>
</div>
<p class="text-muted small mb-0">
{% trans "Candidates submitted" %}
</p>
<div class="text-center">
<div class="h4 mb-1">{{ total_candidates }}</div>
<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>
<!-- 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 %}
<button type="button" class="btn btn-outline-warning" data-bs-toggle="modal"
data-bs-target="#extendDeadlineModal">
<i class="fas fa-clock me-2"></i> {% trans "Extend Deadline" %}
<!-- Actions Card -->
<div class="kaauh-card p-4">
<h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
<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>
{% 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 %}
</div>
</div>
{% endfor %}
</div>
<!-- Recent Messages -->
{% if messages_ %}
<div class="kaauh-card">
<div
class="card-header bg-transparent border-bottom py-3 px-4 d-flex justify-content-between align-items-center">
<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 %}
{% if messages_.count > 6 %}
<div class="text-center mt-3">
<a href="#" class="btn btn-outline-primary btn-sm">
{% trans "View All Messages" %}
</a>
</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 %}
</div>
</div>
{% endif %}
</div>
<!-- Extend Deadline Modal -->
@ -413,8 +420,8 @@
<label for="new_deadline" class="form-label">
{% trans "New Deadline" %} <span class="text-danger">*</span>
</label>
<input type="datetime-local" class="form-control" id="new_deadline" name="new_deadline"
required>
<input type="datetime-local" class="form-control" id="new_deadline"
name="new_deadline" required>
<small class="form-text text-muted">
{% trans "Current deadline:" %} {{ assignment.deadline_date|date:"Y-m-d H:i" }}
</small>
@ -436,13 +443,13 @@
{% block customJS %}
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function () {
// Show success message
const toast = document.createElement('div');
toast.className = 'position-fixed top-0 end-0 p-3';
toast.style.zIndex = '1050';
toast.innerHTML = `
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
// Show success message
const toast = document.createElement('div');
toast.className = 'position-fixed top-0 end-0 p-3';
toast.style.zIndex = '1050';
toast.innerHTML = `
<div class="toast show" role="alert">
<div class="toast-header">
<strong class="me-auto">{% trans "Success" %}</strong>
@ -453,61 +460,61 @@
</div>
</div>
`;
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');
document.body.appendChild(toast);
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;
}
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(() => {
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>
{% endblock %}
{% endblock %}

View File

@ -165,7 +165,7 @@
<tr class="person-row">
<td>
<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;">
{{ person.first_name|first|upper }}{{ person.last_name|first|upper }}
</div>
@ -178,7 +178,7 @@
</div>
</td>
<td>
<a href="mailto:{{ person.email }}" class="text-decoration-none">
<a href="mailto:{{ person.email }}" class="text-decoration-none text-dark">
{{ person.email }}
</a>
</td>

View File

@ -185,7 +185,7 @@
<a href="{% url 'candidate_portal_dashboard' %}" class=" text-decoration-none text-secondary">{% trans "Dashboard" %}</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'candidate_portal_dashboard' %}#applications" class="text-secondary text-decoration">{% trans "My Applications" %}</a>
<a href="{% url 'candidate_portal_dashboard' %}#applications" class="text-secondary text-decoration-none">{% trans "My Applications" %}</a>
</li>
<li class="breadcrumb-item active" aria-current="page" style="
color: #F43B5E; /* Rosy Accent Color */
@ -309,9 +309,10 @@
</div>
</div>
</div>
<div class="row mb-5">
<div class="col-md-6 col-6">
<div class="col-md-6 col-6">
<a href="{% url 'candidate_portal_dashboard' %}" class="text-decoration-none text-dark">
<div class="kaauh-card h-50 shadow-sm action-card">
<div class="card-body text-center mb-4">
<i class="fas fa-arrow-left fa-2x text-primary-theme mb-3"></i>
@ -322,16 +323,19 @@
</a>
</div>
</div>
</a>
</div>
{% if application.resume %}
<div class="col-md-6 col-6">
<a href="{{ application.resume.url }}"
target="_blank" class="text-decoration-none text-dark">
<div class="kaauh-card h-50 shadow-sm action-card">
<div class="card-body text-center mb-4">
<i class="fas fa-file-download fa-2x text-success mb-3"></i>
<h6>{% trans "Download Resume" %}</h6>
<p class="text-muted small">{% trans "Get your submitted file" %}</p>
<a href="{{ application.resume.url }}"
<a href="{{ application.resume.url }}"
target="_blank"
class="btn btn-main-action btn-sm w-100">
<i class="fas fa-download me-2"></i>
@ -339,9 +343,11 @@
</a>
</div>
</div>
</a>
</div>
{% endif %}
</div>
@ -617,7 +623,7 @@
<input type="hidden" name="application_slug" value="{{ application.slug }}">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="button" class="btn btn-outline-secondary btn-lg" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-main-action">
<i class="fas fa-upload me-2"></i>
{% trans "Upload" %}

View File

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

View File

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