few fixes in ui and logic

This commit is contained in:
Faheed 2025-12-01 19:14:35 +03:00
parent 6dca145394
commit d4c8e634e7
13 changed files with 194 additions and 118 deletions

View File

@ -1023,6 +1023,7 @@ class HiringAgencyForm(forms.ModelForm):
def clean_email(self): def clean_email(self):
"""Validate email format and uniqueness""" """Validate email format and uniqueness"""
email = self.cleaned_data.get("email") email = self.cleaned_data.get("email")
instance=self.instance
if email: if email:
# Check email format # Check email format
if not "@" in email or "." not in email.split("@")[1]: if not "@" in email or "." not in email.split("@")[1]:
@ -1031,8 +1032,16 @@ class HiringAgencyForm(forms.ModelForm):
# Check uniqueness (optional - remove if multiple agencies can have same email) # Check uniqueness (optional - remove if multiple agencies can have same email)
# instance = self.instance # instance = self.instance
email = email.lower().strip() email = email.lower().strip()
if CustomUser.objects.filter(email=email).exists(): if not instance.pk: # Creating new instance
raise ValidationError("This email is already associated with a user account.") if HiringAgency.objects.filter(email=email).exists():
raise ValidationError("An agency with this email already exists.")
else: # Editing existing instance
if (
HiringAgency.objects.filter(email=email)
.exclude(pk=instance.pk)
.exists()
):
raise ValidationError("An agency with this email already exists.")
# if not instance.pk: # Creating new instance # if not instance.pk: # Creating new instance
# if HiringAgency.objects.filter(email=email).exists(): # if HiringAgency.objects.filter(email=email).exists():
# raise ValidationError("An agency with this email already exists.") # raise ValidationError("An agency with this email already exists.")
@ -2293,6 +2302,11 @@ class ApplicantSignupForm(forms.ModelForm):
raise forms.ValidationError("Passwords do not match.") raise forms.ValidationError("Passwords do not match.")
return cleaned_data return cleaned_data
def email_clean(self):
email = self.cleaned_data.get('email')
if CustomUser.objects.filter(email=email).exists():
raise forms.ValidationError("Email is already in use.")
return email
class DocumentUploadForm(forms.ModelForm): class DocumentUploadForm(forms.ModelForm):

View File

@ -160,6 +160,13 @@ class PersonListView(StaffRequiredMixin, ListView):
context_object_name = "people_list" context_object_name = "people_list"
def get_queryset(self): def get_queryset(self):
queryset=super().get_queryset() queryset=super().get_queryset()
search_query=self.request.GET.get('search','')
if search_query:
queryset=queryset.filter(
Q(first_name__icontains=search_query) |
Q(last_name__icontains=search_query) |
Q(email__icontains=search_query)
)
gender=self.request.GET.get('gender') gender=self.request.GET.get('gender')
if gender: if gender:
queryset=queryset.filter(gender=gender) queryset=queryset.filter(gender=gender)
@ -179,6 +186,7 @@ class PersonListView(StaffRequiredMixin, ListView):
nationality=self.request.GET.get('nationality') nationality=self.request.GET.get('nationality')
context['nationality']=nationality context['nationality']=nationality
context['nationalities']=nationalities context['nationalities']=nationalities
context['search_query'] = self.request.GET.get('search', '')
return context return context
@ -630,7 +638,7 @@ def job_detail(request, slug):
# New statistics # New statistics
"avg_match_score": avg_match_score, "avg_match_score": avg_match_score,
"high_potential_count": high_potential_count, "high_potential_count": high_potential_count,
"high_potential_ratio": high_potential_ratio, # "high_potential_ratio": high_potential_ratio,
"avg_t2i_days": avg_t2i_days, "avg_t2i_days": avg_t2i_days,
"avg_t_in_exam_days": avg_t_in_exam_days, "avg_t_in_exam_days": avg_t_in_exam_days,
"linkedin_content_form": linkedin_content_form, "linkedin_content_form": linkedin_content_form,
@ -700,6 +708,10 @@ def request_cvs_download(request, slug):
job.save(update_fields=["zip_created"]) job.save(update_fields=["zip_created"])
# Use async_task to run the function in the background # Use async_task to run the function in the background
# Pass only simple arguments (like the job ID) # Pass only simple arguments (like the job ID)
if not job.applications.exists():
messages.warning(request, _("No applications found for this job. ZIP file generation skipped."))
return redirect('job_detail', slug=slug)
async_task('recruitment.tasks.generate_and_save_cv_zip', job.id) async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
# Provide user feedback and redirect # Provide user feedback and redirect
@ -711,6 +723,9 @@ def download_ready_cvs(request, slug):
View to serve the file once it is ready. View to serve the file once it is ready.
""" """
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
if not job.applications.exists():
messages.warning(request, _("No applications found for this job. ZIP file download unavailable."))
return redirect('job_detail', slug=slug)
if job.cv_zip_file and job.zip_created: if job.cv_zip_file and job.zip_created:
# Django FileField handles the HttpResponse and file serving easily # Django FileField handles the HttpResponse and file serving easily
@ -3324,27 +3339,27 @@ def agency_detail(request, slug):
"""View details of a specific hiring agency""" """View details of a specific hiring agency"""
agency = get_object_or_404(HiringAgency, slug=slug) agency = get_object_or_404(HiringAgency, slug=slug)
# Get candidates associated with this agency # Get applications associated with this agency
candidates = Application.objects.filter(hiring_agency=agency).order_by( applications = Application.objects.filter(hiring_agency=agency).order_by(
"-created_at" "-created_at"
) )
# Statistics # Statistics
total_candidates = candidates.count() total_applications = applications.count()
active_candidates = candidates.filter( active_applications = applications.filter(
stage__in=["Applied", "Screening", "Exam", "Interview", "Offer"] stage__in=["Applied", "Screening", "Exam", "Interview", "Offer"]
).count() ).count()
hired_candidates = candidates.filter(stage="Hired").count() hired_applications = applications.filter(stage="Hired").count()
rejected_candidates = candidates.filter(stage="Rejected").count() rejected_applications = applications.filter(stage="Rejected").count()
job_assignments=AgencyJobAssignment.objects.filter(agency=agency) job_assignments=AgencyJobAssignment.objects.filter(agency=agency)
print(job_assignments) print(job_assignments)
context = { context = {
"agency": agency, "agency": agency,
"candidates": candidates[:10], # Show recent 10 candidates "applications": applications[:10], # Show recent 10 applications
"total_candidates": total_candidates, "total_applications": total_applications,
"active_candidates": active_candidates, "active_applications": active_applications,
"hired_candidates": hired_candidates, "hired_applications": hired_applications,
"rejected_candidates": rejected_candidates, "rejected_applications": rejected_applications,
"generated_password": agency.generated_password "generated_password": agency.generated_password
if agency.generated_password if agency.generated_password
else None, else None,
@ -4343,7 +4358,7 @@ def agency_portal_submit_application_page(request, slug):
"total_submitted": total_submitted, "total_submitted": total_submitted,
"job": assignment.job, "job": assignment.job,
} }
return render(request, "recruitment/agency_portal_submit_candidate.html", context) return render(request, "recruitment/agency_portal_submit_application.html", context)
@agency_user_required @agency_user_required
@ -4407,7 +4422,7 @@ def agency_portal_submit_application(request):
"title": f"Submit Candidate for {assignment.job.title}", "title": f"Submit Candidate for {assignment.job.title}",
"button_text": "Submit Candidate", "button_text": "Submit Candidate",
} }
return render(request, "recruitment/agency_portal_submit_candidate.html", context) return render(request, "recruitment/agency_portal_submit_application.html", context)
def agency_portal_assignment_detail(request, slug): def agency_portal_assignment_detail(request, slug):
@ -4450,7 +4465,7 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
return redirect("agency_portal_dashboard") return redirect("agency_portal_dashboard")
# Get candidates submitted by this agency for this job # Get candidates submitted by this agency for this job
candidates = Application.objects.filter( applications = Application.objects.filter(
hiring_agency=assignment.agency, job=assignment.job hiring_agency=assignment.agency, job=assignment.job
).order_by("-created_at") ).order_by("-created_at")
@ -4461,7 +4476,7 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
# No messages to mark as read # No messages to mark as read
# Pagination for candidates # Pagination for candidates
paginator = Paginator(candidates, 20) # Show 20 candidates per page paginator = Paginator(applications, 20) # Show 20 candidates per page
page_number = request.GET.get("page") page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
@ -4471,12 +4486,12 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
message_page_obj = message_paginator.get_page(message_page_number) message_page_obj = message_paginator.get_page(message_page_number)
# Calculate progress ring offset for circular progress indicator # Calculate progress ring offset for circular progress indicator
total_candidates = candidates.count() total_applications = applications.count()
max_candidates = assignment.max_candidates max_applications = assignment.max_candidates
circumference = 326.73 # 2 * π * r where r=52 circumference = 326.73 # 2 * π * r where r=52
if max_candidates > 0: if max_applications > 0:
progress_percentage = total_candidates / max_candidates progress_percentage = total_applications / max_applications
stroke_dashoffset = circumference - (circumference * progress_percentage) stroke_dashoffset = circumference - (circumference * progress_percentage)
else: else:
stroke_dashoffset = circumference stroke_dashoffset = circumference
@ -4485,8 +4500,9 @@ def agency_assignment_detail_agency(request, slug, assignment_id):
"assignment": assignment, "assignment": assignment,
"page_obj": page_obj, "page_obj": page_obj,
"message_page_obj": message_page_obj, "message_page_obj": message_page_obj,
"total_candidates": total_candidates, "total_applications": total_applications,
"stroke_dashoffset": stroke_dashoffset, "stroke_dashoffset": stroke_dashoffset,
"max_applications": max_applications,
} }
return render(request, "recruitment/agency_portal_assignment_detail.html", context) return render(request, "recruitment/agency_portal_assignment_detail.html", context)
@ -4667,7 +4683,7 @@ def message_list(request):
"search_query": search_query, "search_query": search_query,
} }
if request.user.user_type != "staff": if request.user.user_type != "staff":
return render(request, "messages/candidate_message_list.html", context) return render(request, "messages/application_message_list.html", context)
return render(request, "messages/message_list.html", context) return render(request, "messages/message_list.html", context)
@ -4749,7 +4765,7 @@ def message_create(request):
"form": form, "form": form,
} }
if request.user.user_type != "staff": if request.user.user_type != "staff":
return render(request, "messages/candidate_message_form.html", context) return render(request, "messages/application_message_form.html", context)
return render(request, "messages/message_form.html", context) return render(request, "messages/message_form.html", context)
@ -4817,7 +4833,7 @@ def message_reply(request, message_id):
"parent_message": parent_message, "parent_message": parent_message,
} }
if request.user.user_type != "staff": if request.user.user_type != "staff":
return render(request, "messages/candidate_message_form.html", context) return render(request, "messages/application_message_form.html", context)
return render(request, "messages/message_form.html", context) return render(request, "messages/message_form.html", context)
@ -4875,50 +4891,93 @@ def message_mark_unread(request, message_id):
@login_required @login_required
def message_delete(request, message_id): def message_delete(request, message_id):
"""Delete a message"""
""" """
Deletes a message using a POST request, primarily designed for HTMX. Deletes a message using a POST request, primarily designed for HTMX.
Redirects to the message list on success (either via standard redirect Redirects to the message list on success (either via standard redirect
or HTMX's hx-redirect header). or HTMX's hx-redirect header).
""" """
# 1. Retrieve the message # 1. Retrieve the message
# Use select_related to fetch linked objects efficiently for checks/logging # Use select_related to fetch linked objects efficiently for checks/logging
message = get_object_or_404( message = get_object_or_404(
Message.objects.select_related("sender", "recipient"), id=message_id Message.objects.select_related("sender", "recipient"), id=message_id
) )
# Check if user has permission to delete this message # 2. Permission Check
# Only the sender or recipient can delete the message
if message.sender != request.user and message.recipient != request.user: if message.sender != request.user and message.recipient != request.user:
messages.error(request, "You don't have permission to delete this message.") messages.error(request, "You don't have permission to delete this message.")
# HTMX requests should handle redirection via client-side logic (hx-redirect) # HTMX requests should handle redirection via client-side logic (hx-redirect)
if "HX-Request" in request.headers: if "HX-Request" in request.headers:
# Returning 403 or 400 is ideal, but 200 with an empty body is often accepted # Returning 403 or 400 is ideal, but 200 with an empty body is often accepted
# by HTMX and the message is shown on the next page/refresh. # by HTMX and the message is shown on the next page/refresh.
return HttpResponse(status=403) return HttpResponse(status=403)
# Standard navigation redirect # Standard navigation redirect
return redirect("message_list") return redirect("message_list")
# 3. Handle POST Request (Deletion)
if request.method == "POST": if request.method == "POST":
message.delete() message.delete()
messages.success(request, "Message deleted successfully.") messages.success(request, "Message deleted successfully.")
# Handle HTMX requests # Handle HTMX requests
if "HX-Request" in request.headers: if "HX-Request" in request.headers:
return HttpResponse(status=200) # HTMX success response # 1. Set the HTMX response header for redirection
response = HttpResponse(status=200)
response["HX-Redirect"] = reverse("message_list") # <--- EXPLICIT HEADER
return response
return redirect("message_list") # Standard navigation fallback
return redirect("message_list")
# For GET requests, show confirmation page # @login_required
context = { # def message_delete(request, message_id):
"message": message, # """Delete a message"""
"title": "Delete Message", # """
"message": f"Are you sure you want to delete this message from {message.sender.get_full_name() or message.sender.username}?", # Deletes a message using a POST request, primarily designed for HTMX.
"cancel_url": reverse("message_detail", kwargs={"message_id": message_id}), # Redirects to the message list on success (either via standard redirect
} # or HTMX's hx-redirect header).
return render(request, "messages/message_confirm_delete.html", context) # """
# # 1. Retrieve the message
# # Use select_related to fetch linked objects efficiently for checks/logging
# message = get_object_or_404(
# Message.objects.select_related("sender", "recipient"), id=message_id
# )
# # Check if user has permission to delete this message
# if message.sender != request.user and message.recipient != request.user:
# messages.error(request, "You don't have permission to delete this message.")
# # HTMX requests should handle redirection via client-side logic (hx-redirect)
# if "HX-Request" in request.headers:
# # Returning 403 or 400 is ideal, but 200 with an empty body is often accepted
# # by HTMX and the message is shown on the next page/refresh.
# return HttpResponse(status=403)
# # Standard navigation redirect
# return redirect("message_list")
# if request.method == "POST":
# message.delete()
# messages.success(request, "Message deleted successfully.")
# # Handle HTMX requests
# if "HX-Request" in request.headers:
# return HttpResponse(status=200) # HTMX success response
# return redirect("message_list")
# # For GET requests, show confirmation page
# context = {
# "message": message,
# "title": "Delete Message",
# "message": f"Are you sure you want to delete this message from {message.sender.get_full_name() or message.sender.username}?",
# "cancel_url": reverse("message_detail", kwargs={"message_id": message_id}),
# }
# return render(request, "messages/message_confirm_delete.html", context)
@login_required @login_required
@ -5038,7 +5097,7 @@ def document_upload(request, slug):
if upload_target == 'person': if upload_target == 'person':
return redirect("applicant_portal_dashboard") return redirect("applicant_portal_dashboard")
else: else:
return redirect("applicant_application_detail", application_slug=application.slug) return redirect("applicant_application_detail", slug=application.slug)
# Handle GET request for AJAX # Handle GET request for AJAX
if request.headers.get("X-Requested-With") == "XMLHttpRequest": if request.headers.get("X-Requested-With") == "XMLHttpRequest":
@ -5050,6 +5109,7 @@ def document_upload(request, slug):
def document_delete(request, document_id): def document_delete(request, document_id):
"""Delete a document""" """Delete a document"""
document = get_object_or_404(Document, id=document_id) document = get_object_or_404(Document, id=document_id)
print(document)
# Initialize variables for redirection outside of the complex logic # Initialize variables for redirection outside of the complex logic
is_htmx = "HX-Request" in request.headers is_htmx = "HX-Request" in request.headers
@ -5655,8 +5715,8 @@ def source_update(request, slug):
context = { context = {
"form": form, "form": form,
"source": source, "source": source,
"title": f"Edit Source: {source.name}", "title": _("Edit Source: %(name)s") % {'name': source.name},
"button_text": "Update Source", "button_text": _("Update Source"),
} }
return render(request, "recruitment/source_form.html", context) return render(request, "recruitment/source_form.html", context)
@ -5674,8 +5734,8 @@ def source_delete(request, slug):
context = { context = {
"source": source, "source": source,
"title": "Delete Source", "title": _("Delete Source: %(name)s") % {'name': source.name},
"message": f'Are you sure you want to delete the source "{source.name}"?', "message": _('Are you sure you want to delete the source "%(name)s"?') % {'name': source.name},
"cancel_url": reverse("source_detail", kwargs={"slug": source.slug}), "cancel_url": reverse("source_detail", kwargs={"slug": source.slug}),
} }
return render(request, "recruitment/source_confirm_delete.html", context) return render(request, "recruitment/source_confirm_delete.html", context)
@ -5775,6 +5835,12 @@ def application_signup(request, slug):
"recruitment/applicant_signup.html", "recruitment/applicant_signup.html",
{"form": form, "job": job}, {"form": form, "job": job},
) )
else:
# messages.error(request, "Please correct the errors below.")
form = ApplicantSignupForm(request.POST)
return render(
request, "recruitment/applicant_signup.html", {"form": form, "job": job}
)
form = ApplicantSignupForm() form = ApplicantSignupForm()
return render( return render(

View File

@ -201,10 +201,10 @@
</h4> </h4>
{# Tag Badge (Prominent) #} {# Tag Badge (Prominent) #}
<span class="badge rounded-pill bg-kaauh-teal job-tag px-3 py-2 fs-6"> <span class="badge rounded-pill bg-kaauh-teal job-tag px-3 py-2 fs-6 d-none d-lg-inline-block">
<i class="fas fa-tag me-1"></i>{% trans "Apply Before: " %}{{job.application_deadline}}
<i class="fas fa-tag me-1"></i>{% trans "Apply Before: " %}{{job.application_deadline}}
</span> </span>
</div> </div>
{# Department/Context (Sub-text) #} {# Department/Context (Sub-text) #}

View File

@ -26,7 +26,7 @@
<form <form
method="post" method="post"
enctype="multipart/form-data" enctype="multipart/form-data"
hx-post="{% url 'document_upload' application.id %}" hx-post="{% url 'application_document_upload' application.slug %}"
hx-target="#documents-pane" hx-target="#documents-pane"
hx-select="#documents-pane" hx-select="#documents-pane"
hx-swap="outerHTML" hx-swap="outerHTML"

View File

@ -8,14 +8,13 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">{% trans "Messages" %}</h4> <h4 class="mb-0 text-primary-theme">{% trans "Messages" %}</h4>
<a href="{% url 'message_create' %}" class="btn btn-main-action"> <a href="{% url 'message_create' %}" class="btn btn-main-action">
<i class="fas fa-plus"></i> {% trans "Compose Message" %} <i class="fas fa-plus"></i> {% trans "Compose Message" %}
</a> </a>
</div> </div>
<!-- Filters --> <div class="card mb-4 border-primary-theme-subtle">
<div class="card mb-4">
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
<div class="col-md-3"> <div class="col-md-3">
@ -41,41 +40,41 @@
<div class="input-group"> <div class="input-group">
<input type="text" name="q" id="q" class="form-control" <input type="text" name="q" id="q" class="form-control"
value="{{ search_query }}" placeholder="{% trans 'Search messages...' %}"> value="{{ search_query }}" placeholder="{% trans 'Search messages...' %}">
<button class="btn btn-outline-secondary" type="submit"> <button class="btn btn-outline-primary-theme" type="submit">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label">&nbsp;</label> <label class="form-label">&nbsp;</label>
<button type="submit" class="btn btn-secondary w-100">{% trans "Filter" %}</button>
<button type="submit" class="btn btn-main-action w-100">
<i class="fa-solid fa-filter me-1"></i>{% trans "Filter" %}</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Statistics -->
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<div class="card bg-light"> <div class="card bg-primary-theme-subtle border-primary-theme-subtle">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">{% trans "Total Messages" %}</h6> <h6 class="card-title text-primary-theme">{% trans "Total Messages" %}</h6>
<h3 class="text-primary">{{ total_messages }}</h3> <h3 class="text-primary-theme">{{ total_messages }}</h3>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card bg-light"> <div class="card bg-warning-subtle border-warning-subtle">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">{% trans "Unread Messages" %}</h6> <h6 class="card-title text-warning">{% trans "Unread Messages" %}</h6>
<h3 class="text-warning">{{ unread_messages }}</h3> <h3 class="text-warning">{{ unread_messages }}</h3>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Messages List --> <div class="card border-primary-theme-subtle">
<div class="card">
<div class="card-body"> <div class="card-body">
{% if page_obj %} {% if page_obj %}
<div class="table-responsive"> <div class="table-responsive">
@ -93,14 +92,14 @@
</thead> </thead>
<tbody> <tbody>
{% for message in page_obj %} {% for message in page_obj %}
<tr class="{% if not message.is_read %}table-secondary{% endif %}"> <tr class="{% if not message.is_read %}table-secondary-theme-light{% endif %}">
<td> <td>
<a href="{% url 'message_detail' message.id %}" <a href="{% url 'message_detail' message.id %}"
class="{% if not message.is_read %}fw-bold {% endif %}"> class="{% if not message.is_read %}fw-bold text-primary-theme text-decoration-none{% endif %}">
{{ message.subject }} {{ message.subject|truncatechars:50 }}
</a> </a>
{% if message.parent_message %} {% if message.parent_message %}
<span class="badge bg-secondary ms-2">{% trans "Reply" %}</span> <span class="badge bg-secondary-theme ms-2 text-decoration-none">{% trans "Reply" %}</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ message.sender.get_full_name|default:message.sender.username }}</td> <td>{{ message.sender.get_full_name|default:message.sender.username }}</td>
@ -120,27 +119,27 @@
<td>{{ message.created_at|date:"M d, Y H:i" }}</td> <td>{{ message.created_at|date:"M d, Y H:i" }}</td>
<td> <td>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<a href="{% url 'message_detail' message.id %}" <a href="{% url 'message_detail' message.id %}"
class="btn btn-sm btn-outline-primary" title="{% trans 'View' %}"> class="btn btn-sm btn-outline-primary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
{% if not message.is_read and message.recipient == request.user %} {% if not message.is_read and message.recipient == request.user %}
<a href="{% url 'message_mark_read' message.id %}" <a href="{% url 'message_mark_read' message.id %}"
class="btn btn-sm btn-outline-success" class="btn btn-sm btn-outline-primary"
hx-post="{% url 'message_mark_read' message.id %}" hx-post="{% url 'message_mark_read' message.id %}"
title="{% trans 'Mark as Read' %}"> title="{% trans 'Mark as Read' %}">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'message_reply' message.id %}" <a href="{% url 'message_reply' message.id %}"
class="btn btn-sm btn-outline-primary" title="{% trans 'Reply' %}"> class="btn btn-sm btn-outline-primary" title="{% trans 'Reply' %}">
<i class="fas fa-reply"></i> <i class="fas fa-reply"></i>
</a> </a>
<a href="{% url 'message_delete' message.id %}" <a href="{% url 'message_delete' message.id %}"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
hx-post="{% url 'message_delete' message.id %}" hx-post="{% url 'message_delete' message.id %}"
hx-confirm="{% trans 'Are you sure you want to delete this message?' %}" hx-confirm="{% trans 'Are you sure you want to delete this message?' %}"
title="{% trans 'Delete' %}"> title="{% trans 'Delete' %}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</div> </div>
@ -149,7 +148,7 @@
{% empty %} {% empty %}
<tr> <tr>
<td colspan="7" class="text-center text-muted"> <td colspan="7" class="text-center text-muted">
<i class="fas fa-inbox fa-3x mb-3"></i> <i class="fas fa-inbox fa-3x mb-3 text-primary-theme"></i>
<p class="mb-0">{% trans "No messages found." %}</p> <p class="mb-0">{% trans "No messages found." %}</p>
<p class="small">{% trans "Try adjusting your filters or compose a new message." %}</p> <p class="small">{% trans "Try adjusting your filters or compose a new message." %}</p>
</td> </td>
@ -159,13 +158,12 @@
</table> </table>
</div> </div>
<!-- Pagination -->
{% if page_obj.has_other_pages %} {% if page_obj.has_other_pages %}
<nav aria-label="Message pagination"> <nav aria-label="Message pagination">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}"> <a class="page-link text-primary-theme" href="?page={{ page_obj.previous_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
<i class="fas fa-chevron-left"></i> <i class="fas fa-chevron-left"></i>
</a> </a>
</li> </li>
@ -174,18 +172,18 @@
{% for num in page_obj.paginator.page_range %} {% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %} {% if page_obj.number == num %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ num }}</span> <span class="page-link bg-primary-theme border-primary-theme">{{ num }}</span>
</li> </li>
{% else %} {% else %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ num }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">{{ num }}</a> <a class="page-link text-primary-theme" href="?page={{ num }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">{{ num }}</a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if page_obj.has_next %} {% if page_obj.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}"> <a class="page-link text-primary-theme" href="?page={{ page_obj.next_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</a> </a>
</li> </li>
@ -195,7 +193,7 @@
{% endif %} {% endif %}
{% else %} {% else %}
<div class="text-center text-muted py-5"> <div class="text-center text-muted py-5">
<i class="fas fa-inbox fa-3x mb-3"></i> <i class="fas fa-inbox fa-3x mb-3 text-primary-theme"></i>
<p class="mb-0">{% trans "No messages found." %}</p> <p class="mb-0">{% trans "No messages found." %}</p>
<p class="small">{% trans "Try adjusting your filters or compose a new message." %}</p> <p class="small">{% trans "Try adjusting your filters or compose a new message." %}</p>
<a href="{% url 'message_create' %}" class="btn btn-main-action"> <a href="{% url 'message_create' %}" class="btn btn-main-action">
@ -210,7 +208,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block customJS %}
<script> <script>
// Auto-refresh unread count every 30 seconds // Auto-refresh unread count every 30 seconds
setInterval(() => { setInterval(() => {
@ -227,5 +225,4 @@ setInterval(() => {
.catch(error => console.error('Error fetching unread count:', error)); .catch(error => console.error('Error fetching unread count:', error));
}, 30000); }, 30000);
</script> </script>
{% endblock %} {% endblock %}

View File

@ -109,6 +109,7 @@
</div> </div>
<div class="row"> <div class="row">
<!-- Assignment Overview --> <!-- Assignment Overview -->
<div class="col-lg-8"> <div class="col-lg-8">
<!-- Assignment Details Card --> <!-- Assignment Details Card -->

View File

@ -161,9 +161,9 @@
<!-- Header --> <!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;"> <h6 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-building me-2"></i> {{ title }} <i class="fas fa-building me-2"></i> {{ title }}
</h1> </h6>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
{% if agency %} {% if agency %}
<a href="{% url 'agency_detail' agency.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'agency_detail' agency.slug %}" class="btn btn-outline-secondary">
@ -186,9 +186,7 @@
<div class="current-profile"> <div class="current-profile">
<h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6> <h6><i class="fas fa-info-circle me-2"></i>{% trans "Currently Editing" %}</h6>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="current-image d-flex align-items-center justify-content-center bg-light">
<i class="fas fa-building text-muted"></i>
</div>
<div> <div>
<h5 class="mb-1">{{ agency.name }}</h5> <h5 class="mb-1">{{ agency.name }}</h5>
{% if agency.contact_person %} {% if agency.contact_person %}

View File

@ -43,10 +43,10 @@
border-radius: 0.35rem; border-radius: 0.35rem;
font-weight: 700; font-weight: 700;
} }
.status-ACTIVE { background-color: var(--kaauh-success); color: white; } .status-ACTIVE { background-color: var(--kaauh-teal-dark); color: white; }
.status-EXPIRED { background-color: var(--kaauh-danger); color: white; } .status-EXPIRED { background-color: var(--kaauh-teal-dark); color: white; }
.status-COMPLETED { background-color: var(--kaauh-info); color: white; } .status-COMPLETED { background-color: var(--kaauh-teal-dark); color: white; }
.status-CANCELLED { background-color: var(--kaauh-warning); color: #856404; } .status-CANCELLED { background-color: var(--kaauh-teal-dark); color: white; }
.progress-ring { .progress-ring {
width: 120px; width: 120px;
@ -164,14 +164,14 @@
<i class="fas fa-exclamation-triangle me-1"></i>{% trans "Expired" %} <i class="fas fa-exclamation-triangle me-1"></i>{% trans "Expired" %}
</small> </small>
{% else %} {% else %}
<small class="text-success"> <small class="text-primary-theme">
<i class="fas fa-clock me-1"></i>{{ assignment.days_remaining }} {% trans "days remaining" %} <i class="fas fa-clock me-1"></i>{{ assignment.days_remaining }} {% trans "days remaining" %}
</small> </small>
{% endif %} {% endif %}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="text-muted small">{% trans "Maximum applications" %}</label> <label class="text-muted small">{% trans "Maximum applications" %}</label>
<div class="fw-bold">{{ assignment.max_candidates }} {% trans "applications" %}</div> <div class="fw-bold">{{max_applications }} {% trans "applications" %}</div>
</div> </div>
</div> </div>
</div> </div>
@ -228,7 +228,7 @@
<i class="fas fa-users me-2"></i> <i class="fas fa-users me-2"></i>
{% trans "Submitted applications" %} ({{ total_applications }}) {% trans "Submitted applications" %} ({{ total_applications }})
</h5> </h5>
<span class="badge bg-info">{{ total_applications }}/{{ assignment.max_applications }}</span> <span class="badge bg-primary-theme">{{ total_applications }}/{{ max_applications }}</span>
</div> </div>
{% if page_obj %} {% if page_obj %}
@ -256,7 +256,7 @@
</div> </div>
</td> </td>
<td> <td>
<span class="badge bg-info">{{ application.get_stage_display }}</span> <span class="badge bg-primary-theme">{{ application.get_stage_display }}</span>
</td> </td>
<td> <td>
<div class="small text-muted"> <div class="small text-muted">
@ -361,12 +361,12 @@
<div class="progress mt-3" style="height: 8px;"> <div class="progress mt-3" style="height: 8px;">
{% widthratio total_applications assignment.max_candidates 100 as progress %} {% widthratio total_applications assignment.max_candidates 100 as progress %}
<div class="progress-bar" style="width: {{ progress }}%"></div> <div class="progress-bar bg-primary-theme" style="width: {{ progress }}%"></div>
</div> </div>
<div class="mt-3 text-center"> <div class="mt-3 text-center">
{% if assignment.can_submit %} {% if assignment.can_submit %}
<span class="badge bg-success">{% trans "Can Submit" %}</span> <span class="badge bg-primary-theme">{% trans "Can Submit" %}</span>
{% else %} {% else %}
<span class="badge bg-danger">{% trans "Cannot Submit" %}</span> <span class="badge bg-danger">{% trans "Cannot Submit" %}</span>
{% endif %} {% endif %}
@ -500,7 +500,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> <button type="button" class="btn btn-outline-primary" data-bs-dismiss="modal">
{% trans "Cancel" %} {% trans "Cancel" %}
</button> </button>
<button type="submit" class="btn btn-main-action"> <button type="submit" class="btn btn-main-action">

View File

@ -204,7 +204,7 @@
<div class="col-md-6"> <div class="col-md-6">
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label> <label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
<div class="input-group input-group-lg"> <div class="input-group input-group-lg">
<form method="get" action="" class="w-100"> <form method="get" action="." class="w-100">
{% include 'includes/search_form.html' %} {% include 'includes/search_form.html' %}
</form> </form>
</div> </div>
@ -426,9 +426,9 @@
{# ------------------------------------------------------------------------------------------ #} {# ------------------------------------------------------------------------------------------ #}
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true"> <div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-danger shadow-lg"> <div class="modal-content shadow-lg">
<div class="modal-header bg-danger text-white"> <div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel"><i class="fas fa-exclamation-triangle me-2"></i> {% trans "Confirm Deletion" %}</h5> <h5 class="modal-title text-primary-theme" id="deleteModalLabel"><i class="fas fa-exclamation-triangle me-2"></i> {% trans "Confirm Deletion" %}</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -437,7 +437,7 @@
<p class="text-muted small">{% trans "This action is irreversible and the application data will be permanently removed." %}</p> <p class="text-muted small">{% trans "This action is irreversible and the application data will be permanently removed." %}</p>
</div> </div>
<div class="modal-footer"> <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-primary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<form id="deleteForm" method="post" action=""> <form id="deleteForm" method="post" action="">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-danger d-flex align-items-center"> <button type="submit" class="btn btn-danger d-flex align-items-center">

View File

@ -8,8 +8,8 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">{{ title }}</h1> <h4 class="h3 mb-0">{{ title }}</h4>
<a href="{% url 'source_detail' source.slug %}" class="btn btn-outline-secondary"> <a href="{% url 'source_detail' source.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Source <i class="fas fa-arrow-left"></i> Back to Source
</a> </a>
</div> </div>

View File

@ -171,9 +171,9 @@
<!-- Header --> <!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;"> <h4 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-plug me-2"></i> {{ title }} <i class="fas fa-plug me-2"></i> {{ title }}
</h1> </h4>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
{% if source %} {% if source %}
<a href="{% url 'source_detail' source.pk %}" class="btn btn-outline-secondary"> <a href="{% url 'source_detail' source.pk %}" class="btn btn-outline-secondary">

View File

@ -68,23 +68,23 @@
{% for source in page_obj %} {% for source in page_obj %}
<tr> <tr>
<td> <td>
<a href="{% url 'source_detail' source.pk %}" class="text-decoration-none"> <a href="{% url 'source_detail' source.pk %}" class="text-decoration-none text-primary-theme">
<strong>{{ source.name }}</strong> <strong>{{ source.name }}</strong>
</a> </a>
</td> </td>
<td> <td>
<span class="badge bg-info">{{ source.source_type }}</span> <span class="badge bg-primary-theme">{{ source.source_type }}</span>
</td> </td>
<td> <td>
{% if source.is_active %} {% if source.is_active %}
<span class="badge bg-success">{% trans "Active" %}</span> <span class="badge bg-primary-theme">{% trans "Active" %}</span>
{% else %} {% else %}
<span class="badge bg-secondary">{% trans "Inactive" %}</span> <span class="badge bg-primary-theme">{% trans "Inactive" %}</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
<code class="small">{{ source.api_key|truncatechars:20 }}</code> <code class="small text-primary-theme">{{ source.api_key|truncatechars:20 }}</code>
</td> </td>
<td> <td>
<small class="text-muted">{{ source.created_at|date:"M d, Y" }}</small> <small class="text-muted">{{ source.created_at|date:"M d, Y" }}</small>

View File

@ -221,7 +221,7 @@
</a> {% endcomment %} </a> {% endcomment %}
{# 2. Change Password Button (Key Icon) #} {# 2. Change Password Button (Key Icon) #}
<a href="{% url 'set_staff_password' user.pk %}" class="btn btn-sm btn-outline-info" title="{% trans 'Change Password' %}"> <a href="{% url 'set_staff_password' user.pk %}" class="btn btn-sm btn-main-action" title="{% trans 'Change Password' %}">
<i class="fas fa-key"></i> <i class="fas fa-key"></i>
</a> </a>