small fix regarding application stages and agency

This commit is contained in:
Faheed 2026-01-25 11:47:46 +03:00
parent ce3149770a
commit 184e48c13e
27 changed files with 1151 additions and 172 deletions

View File

@ -209,9 +209,9 @@ ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
ACCOUNT_FORMS = {"signup": "recruitment.forms.StaffSignupForm"}
MAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_HOST = "10.10.1.110"
EMAIL_PORT = 2225
# MAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# EMAIL_HOST = "10.10.1.110"
# EMAIL_PORT = 2225
# EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD", "mssp.0Q0rSwb.zr6ke4n2k3e4on12.aHwJqnI")
@ -227,13 +227,13 @@ EMAIL_PORT = 2225
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = 'smtp.gmail.com'
# EMAIL_PORT = 587
# EMAIL_USE_TLS = True
# EMAIL_HOST_USER = 'faheedk215@gmail.com' # Use your actual Gmail email address
# EMAIL_HOST_PASSWORD = 'nfxf xpzo bpsb lqje' #
# DEFAULT_FROM_EMAIL='faheedlearn@gmail.com'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'faheedk215@gmail.com' # Use your actual Gmail email address
EMAIL_HOST_PASSWORD = 'mduo mcsn lwih irkf' #
DEFAULT_FROM_EMAIL = 'faheedk215@gmail.com'
# Crispy Forms Configuration
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
@ -575,4 +575,6 @@ CACHES = {
}
CSRF_TRUSTED_ORIGINS=["http://10.10.1.126","https://kaauh1.tenhal.sa",'http://127.0.0.1']
CSRF_TRUSTED_ORIGINS=["http://10.10.1.126","https://kaauh1.tenhal.sa",'http://127.0.0.1',]
CAREER_PAGE_URL = "http://localhost:8000/en/careers/"

View File

@ -7,6 +7,7 @@ from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
from django.conf import settings
from .models import (
Application,
@ -498,6 +499,7 @@ class JobPostingForm(forms.ModelForm):
"class": "form-control",
"min": 1,
"placeholder": "Number of open positions",
"required": True,
}
),
"hash_tags": forms.TextInput(
@ -534,6 +536,13 @@ class JobPostingForm(forms.ModelForm):
self.fields["location_city"].initial = "Riyadh"
self.fields["location_state"].initial = "Riyadh Province"
self.fields["location_country"].initial = "Saudi Arabia"
def clean_open_positions(self):
open_positions = self.cleaned_data.get("open_positions")
if open_positions is None or open_positions < 1:
raise forms.ValidationError(
"Open positions must be at least 1."
)
return open_positions
def clean_hash_tags(self):
hash_tags = self.cleaned_data.get("hash_tags")
@ -1029,6 +1038,40 @@ class HiringAgencyForm(forms.ModelForm):
return website
class AgencyJobAssignmentCancelForm(forms.ModelForm):
"""Form for cancelling agency job assignments"""
class Meta:
model = AgencyJobAssignment
fields = ["cancel_reason"]
widgets = {
"cancel_reason": forms.Textarea(
attrs={
"class": "form-control",
"rows": 4,
"placeholder": "Enter reason for cancelling this assignment (optional)..."
}
),
}
labels = {
"cancel_reason": _("Cancellation Reason"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_class = "g-3"
self.helper.layout = Layout(
Field("cancel_reason", css_class="form-control"),
Div(
Submit("submit", _("Cancel Assignment"), css_class="btn btn-danger"),
css_class="col-12 mt-4",
),
)
class AgencyJobAssignmentForm(forms.ModelForm):
"""Form for creating and editing agency job assignments"""
@ -1400,11 +1443,12 @@ class CandidateEmailForm(forms.Form):
if candidate and candidate.stage == 'Applied':
message_parts = [
f"Dear Candidate,",
f"Thank you for your interest in the {self.job.title} position at KAAUH and for taking the time to submit your application.",
f"We have carefully reviewed your qualifications; however, we regret to inform you that your application was not selected to proceed to the examination round at this time.",
f"The selection process was highly competitive, and we had a large number of highly qualified applicants.",
f"We encourage you to review other opportunities and apply for roles that align with your skills on our career portal:",
f"[settings.CAREER_PAGE_URL]", # Use a Django setting for the URL for flexibility
f"{settings.CAREER_PAGE_URL}", # Use a Django setting for the URL for flexibility
f"We wish you the best of luck in your current job search and future career endeavors.",
f"Sincerely,",
f"The KAAUH Recruitment Team",
@ -1457,6 +1501,7 @@ class CandidateEmailForm(forms.Form):
]
elif candidate and candidate.stage == 'Document Review':
message_parts = [
f"Dear Candidate,",
f"Congratulations on progressing to the final stage for the {self.job.title} role!",
f"The next critical step is to complete your application by uploading the required employment verification documents.",
f"**Please log into the Candidate Portal immediately** to access the 'Document Upload' section.",
@ -1467,6 +1512,7 @@ class CandidateEmailForm(forms.Form):
]
elif candidate and candidate.stage == 'Hired':
message_parts = [
f"Dear Candidate,",
f"Welcome aboard,!",
f"We are thrilled to officially confirm your employment as our new {self.job.title}.",
f"You will receive a separate email shortly with details regarding your start date, first-day instructions, and onboarding documents.",

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-01-19 20:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='agencyjobassignment',
name='cancel_reason',
field=models.TextField(blank=True, help_text='Reason for cancelling this assignment', null=True, verbose_name='Cancel Reason'),
),
migrations.AddField(
model_name='agencyjobassignment',
name='cancelled_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Cancelled At'),
),
migrations.AddField(
model_name='agencyjobassignment',
name='cancelled_by',
field=models.CharField(blank=True, help_text='Name of person who cancelled this assignment', max_length=100, null=True, verbose_name='Cancelled By'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-01-19 21:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0002_add_cancellation_fields_to_agency_job_assignment'),
]
operations = [
migrations.AlterField(
model_name='agencyjobassignment',
name='status',
field=models.CharField(choices=[('ACTIVE', 'Active'), ('COMPLETED', 'Completed'), ('CANCELLED', 'Cancelled')], default='ACTIVE', max_length=20, verbose_name='Status'),
),
]

View File

@ -89,6 +89,8 @@ class CustomUser(AbstractUser):
return message_list.count() or 0
User = get_user_model()
@ -2006,7 +2008,6 @@ class AgencyJobAssignment(Base):
class AssignmentStatus(models.TextChoices):
ACTIVE = "ACTIVE", _("Active")
COMPLETED = "COMPLETED", _("Completed")
EXPIRED = "EXPIRED", _("Expired")
CANCELLED = "CANCELLED", _("Cancelled")
agency = models.ForeignKey(
@ -2069,6 +2070,26 @@ class AgencyJobAssignment(Base):
help_text=_("Internal notes about this assignment"),
)
# Cancellation tracking
cancelled_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Cancelled At")
)
cancelled_by = models.CharField(
max_length=100,
blank=True,
null=True,
verbose_name=_("Cancelled By"),
help_text=_("Name of person who cancelled this assignment")
)
cancel_reason = models.TextField(
blank=True,
null=True,
verbose_name=_("Cancel Reason"),
help_text=_("Reason for cancelling this assignment")
)
class Meta:
verbose_name = _("Agency Job Assignment")
verbose_name_plural = _("Agency Job Assignments")

View File

@ -112,6 +112,7 @@ class EmailService:
html_content=html_content,
)
print(f"Bulk email sent to {sent_count} recipients.")
# Return the count of recipients if successful, or 0 if failure
return len(recipient_emails) if sent_count > 0 else 0

View File

@ -18,6 +18,7 @@ from .models import (
HiringAgency,
Person,
Source,
AgencyJobAssignment,
)
from .forms import generate_api_key, generate_api_secret
from django.contrib.auth import get_user_model
@ -197,6 +198,41 @@ def notification_created(sender, instance, created, **kwargs):
from .utils import generate_random_password
@receiver(post_save, sender=Application)
def trigger_erp_sync_on_hired(sender, instance, created, **kwargs):
"""
Automatically trigger ERP sync when an application is moved to 'Hired' stage.
"""
# Only trigger on updates (not new applications)
if created:
return
# Only trigger if stage changed to 'Hired'
if instance.stage == 'Hired':
try:
# Get the previous state to check if stage actually changed
from_db = Application.objects.get(pk=instance.pk)
if from_db.stage != 'Hired':
# Stage changed to Hired - trigger sync once per job
from django_q.tasks import async_task
from .tasks import sync_hired_candidates_task
job_slug = instance.job.slug
logger.info(f"Triggering automatic ERP sync for job {job_slug}")
# Queue sync task for background processing
async_task(
sync_hired_candidates_task,
job_slug,
group=f"auto_sync_job_{job_slug}",
timeout=300, # 5 minutes
)
except Application.DoesNotExist:
pass
@receiver(post_save, sender=HiringAgency)
def hiring_agency_created(sender, instance, created, **kwargs):
if created:
@ -254,3 +290,42 @@ def source_created(sender, instance, created, **kwargs):
logger.info(f"API keys generated successfully for Source: {instance.name} (Key: {api_key[:8]}...)")
else:
logger.info(f"Source {instance.name} already has API keys, skipping generation")
@receiver(post_save, sender=AgencyJobAssignment)
def auto_update_agency_assignment_status(sender, instance, created, **kwargs):
"""
Automatically update AgencyJobAssignment status based on conditions:
- Set to COMPLETED when candidates_submitted >= max_candidates
- Keep is_active synced with status field
"""
# Only process updates (skip new records)
if created:
return
# Auto-complete when max candidates reached
if instance.candidates_submitted >= instance.max_candidates:
if instance.status != AgencyJobAssignment.AssignmentStatus.COMPLETED:
logger.info(
f"Auto-completing assignment {instance.pk}: "
f"Max candidates ({instance.max_candidates}) reached"
)
# Use filter().update() to avoid triggering post_save signal again
AgencyJobAssignment.objects.filter(pk=instance.pk).update(
status=AgencyJobAssignment.AssignmentStatus.COMPLETED,
is_active=False
)
return
# Sync is_active with status - only if it actually changed
if instance.status == AgencyJobAssignment.AssignmentStatus.ACTIVE:
AgencyJobAssignment.objects.filter(pk=instance.pk).update(
is_active=True
)
elif instance.status in [
AgencyJobAssignment.AssignmentStatus.COMPLETED,
AgencyJobAssignment.AssignmentStatus.CANCELLED,
]:
AgencyJobAssignment.objects.filter(pk=instance.pk).update(
is_active=False
)

View File

@ -1108,7 +1108,7 @@ def _task_send_individual_email(
"email_message": body_message,
"user_email": recipient,
"logo_url": context.pop(
"logo_url", settings.MEDIA_URL + "/images/kaauh-logo.png"
"logo_url", settings.STATIC_URL + "/images/kaauh-logo.png"
),
# Merge any other custom context variables
**context,

View File

@ -87,7 +87,7 @@ urlpatterns = [
path("interviews/<slug:slug>/update_interview_result", views.update_interview_result, name="update_interview_result"),
path("interviews/<slug:slug>/cancel_interview_for_application", views.cancel_interview_for_application, name="cancel_interview_for_application"),
path("interview/<slug:slug>/interview-email/",views.send_interview_email,name="send_interview_email"),
path("interviews/<slug:slug>/interview-email/",views.send_interview_email,name="send_interview_email"),
# Interview Creation
path("interviews/create/<slug:application_slug>/", views.interview_create_type_selection, name="interview_create_type_selection"),
@ -166,10 +166,11 @@ urlpatterns = [
# Agency Assignment Management
path("agency-assignments/", views.agency_assignment_list, name="agency_assignment_list"),
path("agency-assignments/create/", views.agency_assignment_create, name="agency_assignment_create"),
path("agency-assignments/<slug:slug>/create/", views.agency_assignment_create, name="agency_assignment_create"),
path("agency-assignments/create/<slug:slug>/", views.agency_assignment_create, name="agency_assignment_create_with_agency"),
path("agency-assignments/<slug:slug>/", views.agency_assignment_detail, name="agency_assignment_detail"),
path("agency-assignments/<slug:slug>/update/", views.agency_assignment_update, name="agency_assignment_update"),
path("agency-assignments/<slug:slug>/extend-deadline/", views.agency_assignment_extend_deadline, name="agency_assignment_extend_deadline"),
path("agency-assignments/<slug:slug>/cancel/", views.agency_assignment_cancel, name="agency_assignment_cancel"),
# Agency Access Links
path("agency-access-links/create/", views.agency_access_link_create, name="agency_access_link_create"),

View File

@ -404,6 +404,10 @@ def job_detail(request, slug):
interview_count = stage_stats["interview_count"]
offer_count = stage_stats["offer_count"]
# Position statistics
positions_filled = job.applications.filter(stage="Hired").count()
vacant_positions = job.open_positions - positions_filled
status_form = JobPostingStatusForm(instance=job)
linkedin_content_form = LinkedPostContentForm(instance=job)
try:
@ -529,7 +533,7 @@ def job_detail(request, slug):
context = {
"job": job,
"applications": applications,
"total_applications": total_applications, # This was total_candidates in the prompt, using total_applicant for consistency
"total_applications": total_applications, # This was total_candidates in the prompt, using total_applicant for consistency
"applied_count": applied_count,
"exam_count": exam_count,
"interview_count": interview_count,
@ -545,6 +549,9 @@ def job_detail(request, slug):
# "high_potential_ratio": high_potential_ratio,
"avg_t2i_days": avg_t2i_days,
"avg_t_in_exam_days": avg_t_in_exam_days,
# Position statistics
"positions_filled": positions_filled,
"vacant_positions": vacant_positions,
"linkedin_content_form": linkedin_content_form,
"staff_form": StaffAssignmentForm(),
}
@ -2057,14 +2064,55 @@ def application_update_status(request, slug):
else "Applicant",
)
elif mark_as == "Hired":
print("hired")
c.update(
stage=mark_as,
hired_date=timezone.now(),
applicant_status="Candidate"
if mark_as in ["Exam", "Interview", "Offer"]
else "Applicant",
)
# Check if number of hired candidates (stage="Hired") >= total open positions for the job
current_hired_count = job.applications.filter(stage="Hired").count()
print(f"Current hired count: {current_hired_count}")
open_positions = job.open_positions if job.open_positions is not None else 0
print(f"Open positions: {open_positions}")
total_selected = c.count()
print(f"Total selected for hiring: {total_selected}")
if current_hired_count >= open_positions or (current_hired_count + total_selected) > open_positions:
# Log warning to system and prevent action
logger.warning(
f"Attempted to hire candidates for job '{job.title}'. "
f"Current hired count ({current_hired_count}) has reached/open positions limit ({open_positions})."
)
messages.error(
request,
f"Cannot hire more candidates than available positions ({open_positions}). "
f"Hired count: {current_hired_count+total_selected}, Open positions: {open_positions}."
f"Increase open positions to hire more candidates in the job creation/edit page."
)
return redirect("applications_offer_view", slug=job.slug)
# Do not update the application status
# Redirect back to the job offer view
# if request.headers.get("HX-Request"):
# # HTMX response
# response = HttpResponse(status=400)
# response["HX-Trigger"] = '{"type": "alert", "title": "Hiring Limit Reached", "body": f"You cannot hire more candidates than the available positions ({open_positions})."}'
# return response
# else:
# # Standard response
# messages.warning(
# request,
# f"Cannot hire more candidates than available positions ({open_positions}). "
# f"Hired count: {current_hired_count}, Open positions: {open_positions}."
# )
# return redirect("applications_offer_view", slug=job.slug)
else:
c.update(
stage=mark_as,
hired_date=timezone.now(),
applicant_status="Candidate"
if mark_as in ["Exam", "Interview", "Offer"]
else "Applicant",
)
messages.success(
request, f"Applications Updated and marked as Hired"
)
else:
print("rejected")
c.update(
@ -2762,6 +2810,7 @@ def agency_assignment_list(request):
"""List all agency job assignments"""
search_query = request.GET.get("q", "")
status_filter = request.GET.get("status", "")
print(status_filter)
assignments = AgencyJobAssignment.objects.select_related("agency", "job").order_by(
"-created_at"
@ -2954,7 +3003,7 @@ def agency_assignment_extend_deadline(request, slug):
new_deadline_dt = datetime.fromisoformat(
new_deadline.replace("Z", "+00:00")
)
# Ensure the new deadline is timezone-aware
# Ensure to new deadline is timezone-aware
if timezone.is_naive(new_deadline_dt):
new_deadline_dt = timezone.make_aware(new_deadline_dt)
@ -2975,6 +3024,57 @@ def agency_assignment_extend_deadline(request, slug):
return redirect("agency_assignment_detail", slug=assignment.slug)
@login_required
@staff_user_required
def agency_assignment_cancel(request, slug):
"""Cancel an agency job assignment"""
from .forms import AgencyJobAssignmentCancelForm
from .models import AgencyAccessLink
assignment = get_object_or_404(AgencyJobAssignment, slug=slug)
if request.method == "POST":
form = AgencyJobAssignmentCancelForm(request.POST, instance=assignment)
if form.is_valid():
# Update assignment fields
assignment.status = "CANCELLED"
assignment.is_active = False
assignment.cancelled_at = timezone.now()
assignment.cancelled_by = request.user.username
assignment.save()
# Deactivate the associated access link if it exists
try:
access_link = assignment.access_link
if access_link and access_link.is_active:
access_link.is_active = False
access_link.save()
except AgencyAccessLink.DoesNotExist:
pass
messages.success(
request,
f'Assignment for {assignment.agency.name} - {assignment.job.title} has been cancelled successfully.'
)
return redirect("agency_assignment_detail", slug=assignment.slug)
else:
messages.error(request, "Please correct errors below.")
else:
form = AgencyJobAssignmentCancelForm(instance=assignment)
return render(
request,
"recruitment/agency_assignment_cancel.html",
{
"form": form,
"assignment": assignment,
"title": f"Cancel Assignment: {assignment.agency.name} - {assignment.job.title}",
"message": f'Are you sure you want to cancel the assignment for {assignment.agency.name}?',
"cancel_url": reverse("agency_assignment_detail", kwargs={"slug": assignment.slug}),
},
)
@require_POST
def portal_password_reset(request, pk):
user = get_object_or_404(User, pk=pk)
@ -6009,6 +6109,7 @@ def applications_hired_view(request, slug):
@login_required
@staff_user_required
@csrf_exempt
def update_application_status(request, job_slug, application_slug, stage_type, status):
"""Handle exam/interview/offer status updates"""
from django.utils import timezone
@ -6651,6 +6752,7 @@ def compose_application_email(request, slug):
if form.is_valid():
# Get email addresses
email_addresses = form.get_email_addresses()
print("email_addresses", email_addresses)
if not email_addresses:
messages.error(request, "No email selected")

View File

@ -43,24 +43,24 @@
<a class="nav-link-custom {% if 'job' in request.path and 'bank' not in request.path %}active{% endif %}" href="{% url 'job_list' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'job_list' %}active{% endif %}" href="{% url 'job_list' %}">
<i class="fas fa-briefcase me-2"></i> <span>{% trans "Jobs" %}</span>
</a>
<a class="nav-link-custom {% if 'job_bank' in request.resolver_match.url_name %}active{% endif %}" href="{% url 'job_bank' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'job_bank' %}active{% endif %}" href="{% url 'job_bank' %}">
<i class="fas fa-university me-2"></i> <span>{% trans "Job Bank" %}</span>
</a>
<a class="nav-link-custom {% if 'application' in request.path %}active{% endif %}" href="{% url 'application_list' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'application_list' %}active{% endif %}" href="{% url 'application_list' %}">
<i class="fas fa-user-tie me-2"></i> <span>{% trans "Applications" %}</span>
</a>
<a class="nav-link-custom {% if 'person' in request.path %}active{% endif %}" href="{% url 'person_list' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'person_list' %}active{% endif %}" href="{% url 'person_list' %}">
<i class="fas fa-user me-2"></i> <span>{% trans "Applicants" %}</span>
</a>
<a class="nav-link-custom {% if 'agency' in request.path %}active{% endif %}" href="{% url 'agency_list' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'agency_list' %}active{% endif %}" href="{% url 'agency_list' %}">
<i class="fas fa-building me-2"></i> <span>{% trans "Agencies" %}</span>
</a>
<a class="nav-link-custom {% if 'interview' in request.path %}active{% endif %}" href="{% url 'interview_list' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'interview_list' %}active{% endif %}" href="{% url 'interview_list' %}">
<i class="fas fa-calendar-alt me-2"></i> <span>{% trans "Interviews" %}</span>
</a>
@ -68,7 +68,7 @@
<div class="mt-4 pt-3 border-top border-secondary mx-3 uppercase">
{% comment %} <small class="text-white-50 px-2">{% trans "System" %}</small> {% endcomment %}
</div>
<a class="nav-link-custom {% if 'message' in request.path %}active{% endif %}" href="{% url 'message_list' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'message_list' %}active{% endif %}" href="{% url 'message_list' %}">
<i class="fas fa-envelope me-2"></i>
<span>{% trans "Messages" %}</span>
{% if request.user.get_unread_message_count > 0 %}
@ -88,7 +88,7 @@
<div class="mt-4 pt-3 border-top border-secondary mx-3 uppercase">
{% comment %} <small class="text-white-50 px-2">{% trans "System" %}</small> {% endcomment %}
</div>
<a class="nav-link-custom {% if 'settings' in request.path %}active{% endif %}" href="{% url 'settings' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'settings' %}active{% endif %}" href="{% url 'settings' %}">
<i class="fas fa-cog me-2"></i> <span>{% trans "Settings" %}</span>
</a>
</div>

View File

@ -1,117 +1,265 @@
{% load static %}
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ subject }}</title>
<!--[if mso]>
<style type="text/css">
body, table, td {font-family: Arial, sans-serif !important;}
</style>
<![endif]-->
<style>
/* Define your custom colors */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
}
/* General Styling */
/* Reset and Base Styles */
body {
margin: 0;
padding: 0;
background-color: #f4f4f4;
font-family: Arial, sans-serif;
color: #333333;
}
.container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
border: 1px solid #dddddd;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); /* Soft shadow */
width: 100% !important;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
background-color: #f5f7fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Header Section */
table {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
/* Color Variables */
.kaauh-teal { background-color: #00636e; }
.kaauh-teal-dark { color: #004a53; }
.text-primary { color: #333333; }
.text-secondary { color: #666666; }
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
}
/* Header */
.header {
background-color: #00636e; /* --kaauh-teal */
padding: 20px;
background: linear-gradient(135deg, #00636e 0%, #004a53 100%);
padding: 40px 20px;
text-align: center;
}
.logo-wrapper {
background-color: #ffffff;
width: 100px;
height: 100px;
border-radius: 50%;
margin: 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.logo {
max-width: 80px;
height: auto;
border: 2px solid #ffffff; /* White border to make it pop */
border-radius: 50%;
max-height: 80px;
display: block;
}
/* Content Section */
/* Content */
.content {
padding: 30px;
line-height: 1.6;
}
h2 {
color: #004a53; /* --kaauh-teal-dark for headings */
background-color: #ffffff;
padding: 40px 30px;
}
/* Button/Call to Action */
.button-container {
text-align: center;
margin: 20px 0;
.content h2 {
color: #004a53;
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
line-height: 1.3;
}
.content p {
color: #333333;
font-size: 15px;
line-height: 1.7;
margin: 0 0 16px 0;
}
/* Button */
.button-wrapper {
text-align: center;
margin: 30px 0;
}
.button {
display: inline-block;
padding: 12px 25px;
background-color: #00636e; /* --kaauh-teal */
padding: 14px 32px;
background-color: #00636e;
color: #ffffff !important;
text-decoration: none;
border-radius: 8px;
font-weight: bold;
border-radius: 6px;
font-weight: 600;
font-size: 16px;
/* Simple hover simulation for supporting clients */
border-bottom: 4px solid #004a53;
box-shadow: 0 4px 12px rgba(0, 99, 110, 0.25);
transition: all 0.3s ease;
}
/* Footer Section */
/* Divider */
.divider {
height: 1px;
background-color: #e5e7eb;
margin: 30px 0;
}
/* Footer */
.footer {
background-color: #f0f0f0;
padding: 20px;
background-color: #f9fafb;
padding: 30px 30px 20px;
border-top: 3px solid #00636e;
}
.footer-branding {
text-align: center;
margin-bottom: 20px;
}
.footer-branding p {
color: #004a53;
font-size: 16px;
font-weight: 600;
margin: 0 0 5px 0;
}
.footer-branding small {
color: #666666;
font-size: 13px;
}
.footer-info {
text-align: center;
font-size: 12px;
color: #777777;
border-top: 2px solid #00636e;
color: #888888;
line-height: 1.6;
margin-top: 15px;
}
.footer a {
color: #00636e; /* --kaauh-teal for links */
.footer-info p {
margin: 5px 0;
}
.footer-info a {
color: #00636e;
text-decoration: none;
font-weight: 500;
}
.footer-info a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.content {
padding: 30px 20px !important;
}
.content h2 {
font-size: 22px !important;
}
.button {
display: block !important;
width: 100% !important;
box-sizing: border-box;
}
.logo-wrapper {
width: 90px !important;
height: 90px !important;
}
.logo {
max-width: 70px !important;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="{{ logo_url }}" alt="Your Organization Logo" class="logo">
</div>
<div class="content">
{% block content %}
<h2>Hello {{ user_name }},</h2>
<p>{{ email_message|safe }}</p>
{% if cta_link %}
<div class="button-container">
<a href="{{ cta_link }}" class="button">{{ cta_text|default:"Click to Proceed" }}</a>
</div>
{% endif %}
<p>If you have any questions, please reply to this email.</p>
<p>Thank you,</p>
<p>King Abdullah bin Abdulaziz University Hospital</p>
{% endblock %}
</div>
<div class="footer">
<p>&copy; {% now "Y" %} Tenhal. All rights reserved.</p>
<p><a href="{{ profile_link }}">Manage Preferences</a></p>
</div>
<body style="margin: 0; padding: 0; background-color: #f5f7fa;">
<!-- Preheader Text (hidden but appears in inbox preview) -->
<div style="display: none; max-height: 0; overflow: hidden;">
{{ email_preview_text|default:"Important message from King Abdullah bin Abdulaziz University Hospital" }}
</div>
<!-- Main Container -->
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: #f5f7fa;">
<tr>
<td align="center" style="padding: 20px 0;">
<table role="presentation" class="email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="background-color: #ffffff; box-shadow: 0 2px 8px rgba(0,0,0,0.08); border-radius: 8px; overflow: hidden;">
<!-- Header -->
<tr>
<td class="header" style="background: linear-gradient(135deg, #00636e 0%, #004a53 100%); padding: 40px 20px; text-align: center;">
<div class="logo-wrapper" style="background-color: #ffffff; width: 100px; height: 100px; border-radius: 50%; margin: 0 auto; display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);">
<img src="{{ logo_url }}" alt="KAAUH Logo" class="logo" style="max-width: 80px; max-height: 80px; display: block;">
</div>
</td>
</tr>
<!-- Content -->
<tr>
<td class="content" style="padding: 40px 30px;">
{% block content %}
<h2 style="color: #004a53; font-size: 24px; font-weight: 600; margin: 0 0 20px 0;">Hello {{ user_name }},</h2>
<p style="color: #333333; font-size: 15px; line-height: 1.7; margin: 0 0 16px 0;">{{ email_message|safe }}</p>
{% if cta_link %}
<div class="button-wrapper" style="text-align: center; margin: 30px 0;">
<a href="{{ cta_link }}" class="button" style="display: inline-block; padding: 14px 32px; background-color: #00636e; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 16px; box-shadow: 0 4px 12px rgba(0, 99, 110, 0.25);">{{ cta_text|default:"Click to Proceed" }}</a>
</div>
{% endif %}
<div class="divider" style="height: 1px; background-color: #e5e7eb; margin: 30px 0;"></div>
<p style="color: #666666; font-size: 14px; margin: 0 0 10px 0;">If you have any questions or need assistance, please don't hesitate to reply to this email or contact our support team.</p>
<p style="color: #333333; font-size: 15px; margin: 20px 0 5px 0; font-weight: 500;">Best regards,</p>
<p style="color: #004a53; font-size: 15px; margin: 0; font-weight: 600;">King Abdullah bin Abdulaziz University Hospital</p>
{% endblock %}
</td>
</tr>
<!-- Footer -->
<tr>
<td class="footer" style="background-color: #f9fafb; padding: 30px 30px 20px; border-top: 3px solid #00636e;">
<div class="footer-branding" style="text-align: center; margin-bottom: 20px;">
<p style="color: #004a53; font-size: 16px; font-weight: 600; margin: 0 0 5px 0;">King Abdullah bin Abdulaziz University Hospital</p>
<small style="color: #666666; font-size: 13px;">Excellence in Healthcare</small>
</div>
<div class="footer-info" style="text-align: center; font-size: 12px; color: #888888; line-height: 1.6; margin-top: 15px;">
<p style="margin: 5px 0;">&copy; {% now "Y" %} Tenhal. All rights reserved.</p>
<p style="margin: 5px 0;">
<a href="{{ profile_link }}" style="color: #00636e; text-decoration: none; font-weight: 500;">Manage Preferences</a>
{% if privacy_link %} | <a href="{{ privacy_link }}" style="color: #00636e; text-decoration: none; font-weight: 500;">Privacy Policy</a>{% endif %}
</p>
<p style="margin: 15px 0 5px 0; color: #999999; font-size: 11px;">This email was sent to you as part of your engagement with our services.<br>Please do not reply to this automated message.</p>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -19,8 +19,7 @@
<div class="card-body">
<form hx-boost="true" method="post" id="email-compose-form" action="{% url 'compose_application_email' job.slug %}"
hx-include="#application-form"
hx-target="#messageContent"
hx-select="#messageContent"
hx-push-url="false"
hx-swap="outerHTML"
hx-on::after-request="new bootstrap.Modal('#emailModal')).hide()">

View File

@ -326,14 +326,14 @@
<i class="fas fa-clock"></i> {% trans "Time" %}: {{ interview.interview_time|time:"H:i A" }}<br>
{# --- Type/Location --- #}
<i class="fas {% if interview.location_type == 'Remote' %}fa-globe{% else %}fa-map-marker-alt{% endif %}"></i>
{% trans "Type" %}: {{ interview.location_type }}
{% if interview.location_type == 'Remote' %}<br>
<i class="fas {% if interview.interview.location_type == 'Remote' %}fa-globe{% else %}fa-map-marker-alt{% endif %}"></i>
{% trans "Type" %}: {{ interview.interview.location_type }}
{% comment %} {% if interview.interview.location_type == 'Remote' %}<br>
{# Using interview.join_url directly if available, assuming interview is the full object #}
<i class="fas fa-link"></i> {% trans "Link" %}: {% if interview.join_url %}<a href="{{ interview.join_url }}" target="_blank">Join Meeting</a>{% else %}N/A{% endif %}
<i class="fas fa-link"></i> {% trans "Link" %}: {% if interview.interview.join_url %}<a href="{{ interview.interview.join_url }}" target="_blank">Join Meeting</a>{% else %}N/A{% endif %}
{% else %}<br>
<i class="fas fa-building"></i> {% trans "Location" %}: {{ interview.location_details|default:"Onsite" }}
{% endif %}
<i class="fas fa-building"></i> {% trans "Location" %}: {{ interview.interview.location_details|default:"Onsite" }}
{% endif %} {% endcomment %}
</p>
<div class="mt-auto pt-2 border-top">
@ -342,8 +342,8 @@
<i class="fas fa-eye"></i> {% trans "View" %}
</a>
{# Join button logic simplified #}
{% if interview.location_type == 'Remote' and interview.join_url %}
<a href="{{ interview.join_url }}" target="_blank" class="btn btn-sm btn-outline-secondary">
{% if interview.interview.location_type == 'Remote' and interview.interview.join_url %}
<a href="{{ interview.interview.join_url }}" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-link"></i> {% trans "Join" %}
</a>
{% endif %}

View File

@ -157,7 +157,7 @@
</div>
<div class="col-md-6">
<div>
<label for="{{ form.open_positions.id_for_label }}" class="form-label">{% trans "Open Positions" %}</label>
<label for="{{ form.open_positions.id_for_label }}" class="form-label">{% trans "Open Positions" %}<span class="text-danger">*</span></label>
{{ form.open_positions }}
{% if form.open_positions.errors %}<div class="text-danger small mt-1">{{ form.open_positions.errors }}</div>{% endif %}
</div>

View File

@ -526,7 +526,55 @@
</div>
</div>
{# Card 3: KPIs #}
{# Card 3: Position Stats #}
<div class="card shadow-sm no-hover mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-briefcase me-1 text-primary"></i>
{% trans "Position Statistics" %}
</h6>
</div>
<div class="card-body p-4">
<div class="row g-3 stats-grid">
{# 1. Open Positions #}
<div class="col-4">
<div class="card text-center h-100 kpi-card">
<div class="card-body p-2">
<i class="fas fa-door-open text-primary mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-primary fw-bold">{{ job.open_positions }}</div>
<small class="text-muted d-block">{% trans "Open Positions" %}</small>
</div>
</div>
</div>
{# 2. Positions Filled #}
<div class="col-4">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-user-check text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-success fw-bold">{{ positions_filled }}</div>
<small class="text-muted d-block">{% trans "Positions Filled" %}</small>
</div>
</div>
</div>
{# 3. Vacant Positions #}
<div class="col-4">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-hourglass-half text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-secondary fw-bold">{{ vacant_positions }}</div>
<small class="text-muted d-block">{% trans "Vacant Positions" %}</small>
</div>
</div>
</div>
</div>
</div>
</div>
{# Card 4: KPIs #}
<div class="card shadow-sm no-hover mb-4">
<div class="card-header">
<h6 class="mb-0">
@ -560,28 +608,7 @@
</div>
</div>
{# 3. Avg. Time to Interview #}
{% comment %} <div class="col-6">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-calendar-alt text-info mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-info fw-bold">{{ avg_t2i_days|floatformat:1 }}d</div>
<small class="text-muted d-block">{% trans "Time to Interview" %}</small>
</div>
</div>
</div>
{# 4. Avg. Exam Review Time #}
<div class="col-6">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-hourglass-half text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-secondary fw-bold">{{ avg_t_in_exam_days|floatformat:1 }}d</div>
<small class="text-muted d-block">{% trans "Avg. Exam Review" %}</small>
</div>
</div>
</div> {% endcomment %}
<!--Vacancy fill rate-->
{# 3. Vacancy Fill Rate #}
<div class="col-4">
<div class="card text-center h-100">
<div class="card-body p-2">

View File

@ -39,22 +39,22 @@
<nav class="sidebar-nav">
{% if request.user.user_type == 'agency' %}
<a class="nav-link-custom {% if 'dashboard' in request.path %}active{% endif %}" href="{% url 'agency_portal_dashboard' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'agency_portal_dashboard' %}active{% endif %}" href="{% url 'agency_portal_dashboard' %}">
<i class="fas fa-th-large me-2"></i> <span>{% trans "Dashboard" %}</span>
</a>
<a class="nav-link-custom {% if 'persons' in request.path %}active{% endif %}" href="{% url 'agency_portal_persons_list' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'agency_portal_persons_list' %}active{% endif %}" href="{% url 'agency_portal_persons_list' %}">
<i class="fas fa-users me-2"></i> <span>{% trans "Applicants" %}</span>
</a>
{% else %}
<a class="nav-link-custom {% if 'dashboard' in request.path %}active{% endif %}" href="{% url 'applicant_portal_dashboard' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'applicant_portal_dashboard' %}active{% endif %}" href="{% url 'applicant_portal_dashboard' %}">
<i class="fas fa-th-large me-2"></i> <span>{% trans "Dashboard" %}</span>
</a>
<a class="nav-link-custom {% if 'career' in request.path %}active{% endif %}" href="{% url 'kaauh_career' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'kaauh_career' %}active{% endif %}" href="{% url 'kaauh_career' %}">
<i class="fas fa-briefcase me-2"></i> <span>{% trans "Careers" %}</span>
</a>
{% endif %}
<a class="nav-link-custom {% if 'message' in request.path %}active{% endif %}" href="{% url 'message_list' %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'message_list' %}active{% endif %}" href="{% url 'message_list' %}">
<i class="fas fa-envelope me-2"></i>
<span>{% trans "Messages" %}</span>
{% if request.user.get_unread_message_count > 0 %}
@ -66,7 +66,7 @@
<div class="mt-4 pt-3 border-top border-secondary mx-3 brand-subtitle">
<small class="text-white-50 px-2 uppercase">{% trans "Account" %}</small>
</div>
<a class="nav-link-custom {% if 'profile' in request.path %}active{% endif %}" href="{% url 'user_detail' request.user.pk %}">
<a class="nav-link-custom {% if request.resolver_match.url_name == 'user_detail' %}active{% endif %}" href="{% url 'user_detail' request.user.pk %}">
<i class="fas fa-user-circle me-2"></i> <span>{% trans "My Profile" %}</span>
</a>
</div>

View File

@ -366,6 +366,13 @@
</button>
{% endif %}
{% if assignment.is_active and assignment.status == 'ACTIVE' %}
<button type="button" class="btn btn-danger"
data-bs-toggle="modal" data-bs-target="#cancelAssignmentModal">
<i class="fas fa-times me-1"></i> {% trans "Cancel Assignment" %}
</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" %}
@ -414,6 +421,65 @@
{% endif %}
</div>
<!-- Cancel Assignment Modal -->
<div class="modal fade" id="cancelAssignmentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle me-2"></i>
{% trans "Cancel Assignment" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning mb-3">
<i class="fas fa-info-circle me-2"></i>
<strong>{% trans "Warning:" %}</strong>
{% trans "This action cannot be undone. The agency will no longer be able to submit applications." %}
</div>
<div class="card mb-3">
<div class="card-body bg-light">
<h6 class="card-title text-primary mb-0">
<i class="fas fa-building me-2"></i>
{{ assignment.agency.name }}
</h6>
<p class="card-text text-muted mb-0">
<i class="fas fa-briefcase me-2"></i>
{{ assignment.job.title }}
</p>
</div>
</div>
<form method="post" action="{% url 'agency_assignment_cancel' assignment.slug %}">
{% csrf_token %}
<div class="mb-3">
<label for="cancel_reason" class="form-label fw-bold">
<i class="fas fa-comment-alt me-2"></i>
{% trans "Cancellation Reason" %}
<span class="text-muted fw-normal">({% trans "Optional" %})</span>
</label>
<textarea class="form-control" id="cancel_reason" name="cancel_reason" rows="4"
placeholder="{% trans 'Enter reason for cancelling this assignment (optional)...' %}"></textarea>
</div>
<div class="d-flex justify-content-between align-items-center">
<a href="{% url 'agency_assignment_detail' assignment.slug %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger">
<i class="fas fa-times-circle me-1"></i>
{% trans "Cancel Assignment" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Extend Deadline Modal -->
<div class="modal fade" id="extendDeadlineModal" tabindex="-1">
<div class="modal-dialog">

View File

@ -91,7 +91,7 @@
<select class="form-select form-select-sm" id="status" name="status">
<option value="">{% trans "All Statuses" %}</option>
<option value="ACTIVE" {% if status_filter == 'ACTIVE' %}selected{% endif %}>{% trans "Active" %}</option>
<option value="EXPIRED" {% if status_filter == 'EXPIRED' %}selected{% endif %}>{% trans "Expired" %}</option>
{% comment %} <option value="EXPIRED" {% if status_filter == 'EXPIRED' %}selected{% endif %}>{% trans "Expired" %}</option> {% endcomment %}
<option value="COMPLETED" {% if status_filter == 'COMPLETED' %}selected{% endif %}>{% trans "Completed" %}</option>
<option value="CANCELLED" {% if status_filter == 'CANCELLED' %}selected{% endif %}>{% trans "Cancelled" %}</option>
</select>

View File

@ -320,7 +320,7 @@
<a href="{% url 'agency_assignment_list' %}" class="btn btn-main-action me-2">
<i class="fas fa-tasks me-1"></i> {% trans "All Assignments" %}
</a>
<a href="{% url 'agency_assignment_create' agency.slug %}" class="btn btn-main-action me-2">
<a href="{% url 'agency_assignment_create_with_agency' agency.slug %}" class="btn btn-main-action me-2">
<i class="fas fa-edit me-1"></i> {% trans "Assign job" %}
</a>
<a href="{% url 'agency_update' agency.slug %}" class="btn btn-main-action me-2">
@ -652,7 +652,7 @@
<i class="fas fa-briefcase-slash"></i>
<h6>{% trans "No jobs assigned" %}</h6>
<p class="mb-0">{% trans "There are no open job assignments for this agency." %}</p>
<a href="{% url 'agency_assignment_create' agency.slug %}" class="btn btn-main-action mt-3 btn-sm">
<a href="{% url 'agency_assignment_create_with_agency' agency.slug %}" class="btn btn-main-action mt-3 btn-sm">
<i class="fas fa-plus me-1"></i> {% trans "Assign New Job" %}
</a>
</div>

View File

@ -476,6 +476,8 @@
</div>
</div>
{% include "recruitment/partials/note_modal.html" %}
{% include "recruitment/partials/stage_confirmation_modal.html" %}
{% endblock %}
{% block customJS %}
@ -486,6 +488,9 @@
const changeStageButton = document.getElementById('changeStage');
const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status');
const stageConfirmationModal = new bootstrap.Modal(document.getElementById('stageConfirmationModal'));
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isConfirmed = false;
if (selectAllCheckbox) {
@ -547,6 +552,57 @@
// Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState();
}
// Stage Confirmation Logic
if (changeStageButton) {
changeStageButton.addEventListener('click', function(event) {
const selectedStage = updateStatus.value;
// Check if a stage is selected (not default empty option)
if (selectedStage && selectedStage.trim() !== '') {
// If not yet confirmed, show modal and prevent submission
if (!isConfirmed) {
event.preventDefault();
// Count selected candidates
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
// Update confirmation message
const messageElement = document.getElementById('stageConfirmationMessage');
const targetStageElement = document.getElementById('targetStageName');
if (messageElement && targetStageElement) {
if (checkedCount > 0) {
messageElement.textContent = `{% trans "Are you sure you want to move" %} ${checkedCount} {% trans "candidate(s) to this stage?" %}`;
targetStageElement.textContent = selectedStage;
} else {
messageElement.textContent = '{% trans "Please select at least one candidate." %}';
targetStageElement.textContent = '--';
}
}
// Show confirmation modal
stageConfirmationModal.show();
return false;
}
// If confirmed, let's form submit normally (reset flag for next time)
isConfirmed = false;
}
});
// Handle confirm button click in modal
if (confirmStageChangeButton) {
confirmStageChangeButton.addEventListener('click', function() {
// Hide modal
stageConfirmationModal.hide();
// Set confirmed flag
isConfirmed = true;
// Programmatically trigger's button click to submit form
changeStageButton.click();
});
}
}
});
</script>
{% endblock %}

View File

@ -406,6 +406,8 @@
</div>
</div>
{% include "recruitment/partials/note_modal.html" %}
{% include "recruitment/partials/stage_confirmation_modal.html" %}
{% endblock %}
@ -417,6 +419,9 @@
const changeStageButton = document.getElementById('changeStage');
const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status');
const stageConfirmationModal = new bootstrap.Modal(document.getElementById('stageConfirmationModal'));
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isConfirmed = false;
if (selectAllCheckbox) {
@ -470,6 +475,57 @@
// Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState();
}
// Stage Confirmation Logic
if (changeStageButton) {
changeStageButton.addEventListener('click', function(event) {
const selectedStage = updateStatus.value;
// Check if a stage is selected (not default empty option)
if (selectedStage && selectedStage.trim() !== '') {
// If not yet confirmed, show modal and prevent submission
if (!isConfirmed) {
event.preventDefault();
// Count selected candidates
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
// Update confirmation message
const messageElement = document.getElementById('stageConfirmationMessage');
const targetStageElement = document.getElementById('targetStageName');
if (messageElement && targetStageElement) {
if (checkedCount > 0) {
messageElement.textContent = `{% trans "Are you sure you want to move" %} ${checkedCount} {% trans "candidate(s) to this stage?" %}`;
targetStageElement.textContent = selectedStage;
} else {
messageElement.textContent = '{% trans "Please select at least one candidate." %}';
targetStageElement.textContent = '--';
}
}
// Show confirmation modal
stageConfirmationModal.show();
return false;
}
// If confirmed, let's form submit normally (reset flag for next time)
isConfirmed = false;
}
});
// Handle confirm button click in modal
if (confirmStageChangeButton) {
confirmStageChangeButton.addEventListener('click', function() {
// Hide modal
stageConfirmationModal.hide();
// Set confirmed flag
isConfirmed = true;
// Programmatically trigger's button click to submit form
changeStageButton.click();
});
}
}
});
</script>
{% endblock %}

View File

@ -196,14 +196,6 @@
</h2>
</div>
<div class="d-flex gap-2">
<button type="button"
class="btn btn-main-action"
onclick="syncHiredCandidates()"
title="{% trans 'Sync hired applications to external sources' %}">
<i class="fas fa-sync me-1"></i> {% trans "Sync to Sources" %}
</button>
<a href="{% url 'export_applications_csv' job.slug 'hired' %}"
class="btn btn-outline-secondary"
title="{% trans 'Export hired applications to CSV' %}">
@ -222,12 +214,71 @@
<p class="mb-0">{% trans "These applications have successfully completed the hiring process and joined your team." %}</p>
</div>
<!-- ERP Sync Status -->
{% if job.source %}
<div class="kaauh-card shadow-sm p-3 mb-4">
<div class="d-flex justify-content-between align-items-start">
<div>
<h5 class="mb-2" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-database me-2"></i> {% trans "ERP Sync Status" %}
</h5>
<div class="row g-3">
<div class="col-md-3">
<small class="text-muted d-block">{% trans "Source:" %}</small>
<strong>{{ job.source.name }}</strong>
</div>
<div class="col-md-3">
<small class="text-muted d-block">{% trans "Sync Status:" %}</small>
<span class="badge
{% if job.source.sync_status == 'SUCCESS' %}bg-success
{% elif job.source.sync_status == 'SYNCING' %}bg-warning
{% elif job.source.sync_status == 'ERROR' %}bg-danger
{% else %}bg-secondary{% endif %}">
{{ job.source.get_sync_status_display }}
</span>
</div>
<div class="col-md-3">
<small class="text-muted d-block">{% trans "Last Sync:" %}</small>
<strong>
{% if job.source.last_sync_at %}
{{ job.source.last_sync_at|date:"M d, Y H:i" }}
{% else %}
<span class="text-muted">{% trans "Never" %}</span>
{% endif %}
</strong>
</div>
<div class="col-md-3">
<small class="text-muted d-block">{% trans "Hired Candidates:" %}</small>
<strong>{{ applications|length }}</strong>
</div>
</div>
</div>
{# Manual sync button commented out - sync is now automatic via Django signals #}
{# <button type="button"
class="btn btn-main-action"
onclick="syncHiredCandidates()"
title="{% trans 'Manually sync hired applications to ERP source (use for re-syncs)' %}">
<i class="fas fa-sync me-1"></i> {% trans "Sync to Sources" %}
</button> #}
</div>
<div class="alert alert-info mt-3 mb-0" style="font-size: 0.85rem;">
<i class="fas fa-info-circle me-2"></i>
{% trans "ERP sync is automatically triggered when candidates are moved to 'Hired' stage. Use the 'Sync to Sources' button for manual re-syncs if needed." %}
</div>
</div>
{% else %}
<div class="alert alert-warning mb-4">
<i class="fas fa-exclamation-triangle me-2"></i>
{% trans "No ERP source configured for this job. Automatic sync is disabled." %}
</div>
{% endif %}
<div class="applicant-tracking-timeline">
{% include 'jobs/partials/applicant_tracking.html' %}
</div>
<div class="kaauh-card shadow-sm p-3">
{% if applications %}
{% comment %} {% if applications %}
<div class="bulk-action-bar p-3 bg-light border-bottom">
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
{% csrf_token %}
@ -268,7 +319,7 @@
</div>
</form>
</div>
{% endif %}
{% endif %} {% endcomment %}
<div class="table-responsive">
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get">
@ -276,14 +327,14 @@
<table class="table application-table align-middle">
<thead>
<tr>
<th style="width: 2%">
{% comment %} <th style="width: 2%">
{% if applications %}
<div class="form-check">
<input
type="checkbox" class="form-check-input" id="selectAllCheckbox">
</div>
{% endif %}
</th>
</th> {% endcomment %}
<th style="width: 15%"><i class="fas fa-user me-1"></i> {% trans "Name" %}</th>
<th style="width: 15%"><i class="fas fa-phone me-1"></i> {% trans "Contact" %}</th>
<th style="width: 15%"><i class="fas fa-briefcase me-1"></i> {% trans "Applied Position" %}</th>
@ -295,12 +346,12 @@
<tbody>
{% for application in applications %}
<tr>
<td>
{% comment %} <td>
<div class="form-check">
<input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
</div>
</td>
</td> {% endcomment %}
<td>
<div class="application-name">
{{ application.name }}

View File

@ -514,6 +514,7 @@
{% include "recruitment/partials/note_modal.html" %}
{% include "recruitment/partials/stage_confirmation_modal.html" %}
{% endblock %}
@ -526,6 +527,9 @@
const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status');
const scheduleInterviewButton = document.getElementById('scheduleInterview');
const stageConfirmationModal = new bootstrap.Modal(document.getElementById('stageConfirmationModal'));
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isConfirmed = false;
if (selectAllCheckbox) {
@ -590,6 +594,57 @@
// Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState();
}
// Stage Confirmation Logic
if (changeStageButton) {
changeStageButton.addEventListener('click', function(event) {
const selectedStage = updateStatus.value;
// Check if a stage is selected (not default empty option)
if (selectedStage && selectedStage.trim() !== '') {
// If not yet confirmed, show modal and prevent submission
if (!isConfirmed) {
event.preventDefault();
// Count selected candidates
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
// Update confirmation message
const messageElement = document.getElementById('stageConfirmationMessage');
const targetStageElement = document.getElementById('targetStageName');
if (messageElement && targetStageElement) {
if (checkedCount > 0) {
messageElement.textContent = `{% trans "Are you sure you want to move" %} ${checkedCount} {% trans "candidate(s) to this stage?" %}`;
targetStageElement.textContent = selectedStage;
} else {
messageElement.textContent = '{% trans "Please select at least one candidate." %}';
targetStageElement.textContent = '--';
}
}
// Show confirmation modal
stageConfirmationModal.show();
return false;
}
// If confirmed, let's form submit normally (reset flag for next time)
isConfirmed = false;
}
});
// Handle confirm button click in modal
if (confirmStageChangeButton) {
confirmStageChangeButton.addEventListener('click', function() {
// Hide modal
stageConfirmationModal.hide();
// Set confirmed flag
isConfirmed = true;
// Programmatically trigger's button click to submit form
changeStageButton.click();
});
}
}
});
// Handle Meeting Modal Opening (Rescheduling/Scheduling)

View File

@ -200,7 +200,7 @@
<div class="d-flex align-items-end gap-3">
{# Form: Hired/Rejected Status Update #}
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
<form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group" id="stageUpdateForm">
{% csrf_token %}
{# Select element #}
@ -430,7 +430,43 @@
</div>
</div>
</div>
<!-- Hiring Confirmation Modal -->
<div class="modal fade" id="hiringConfirmationModal" tabindex="-1" aria-labelledby="hiringConfirmationModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" 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="hiringConfirmationModalLabel" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-exclamation-triangle me-2"></i>{% trans "Confirm Hiring" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning" role="alert">
<i class="fas fa-sync-alt me-2"></i>
<strong>{% trans "Important:" %}</strong> {% trans "This action will sync the selected applicant(s) to the ERP source." %}
</div>
<p class="mb-3">
<i class="fas fa-info-circle me-2 text-primary"></i>
<span id="hiringConfirmationMessage">{% trans "Are you sure you want to proceed?" %}</span>
</p>
<div class="d-flex align-items-center justify-content-center py-3">
<i class="fas fa-user-check fa-3x" style="color: var(--kaauh-teal);"></i>
</div>
</div>
<div class="modal-footer" style="border-top: 1px solid var(--kaauh-border);">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>{% trans "Cancel" %}
</button>
<button type="button" class="btn btn-main-action" id="confirmHiringButton">
<i class="fas fa-check me-1"></i>{% trans "Confirm & Hire" %}
</button>
</div>
</div>
</div>
</div>
{% include "recruitment/partials/note_modal.html" %}
{% include "recruitment/partials/stage_confirmation_modal.html" %}
{% endblock %}
@ -442,6 +478,9 @@
const changeStageButton = document.getElementById('changeStage');
const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status');
const stageUpdateForm = document.getElementById('stageUpdateForm');
const hiringConfirmationModal = new bootstrap.Modal(document.getElementById('hiringConfirmationModal'));
const confirmHiringButton = document.getElementById('confirmHiringButton');
if (selectAllCheckbox) {
@ -503,6 +542,105 @@
// Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState();
}
// Generic Stage Confirmation (for non-Hired stages)
const stageConfirmationModal = new bootstrap.Modal(document.getElementById('stageConfirmationModal'));
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isStageConfirmed = false;
// Hiring Confirmation Logic (for Hired stage)
let isHiringConfirmed = false;
if (stageUpdateForm && changeStageButton) {
changeStageButton.addEventListener('click', function(event) {
const selectedStage = updateStatus.value;
// Check if "Hired" stage is selected
if (selectedStage === 'Hired') {
// If not yet confirmed, show modal and prevent submission
if (!isHiringConfirmed) {
event.preventDefault(); // Prevent form submission
// Count selected candidates
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
// Update confirmation message
const messageElement = document.getElementById('hiringConfirmationMessage');
if (messageElement) {
if (checkedCount > 0) {
messageElement.textContent = `{% trans "Are you sure you want to hire" %} ${checkedCount} {% trans "candidate(s)? This action will be synced to the ERP source." %}`;
} else {
messageElement.textContent = '{% trans "Please select at least one candidate to hire." %}';
}
}
// Show hiring confirmation modal
hiringConfirmationModal.show();
return false;
}
// If confirmed, let's form submit normally (reset flag for next time)
isHiringConfirmed = false;
}
// For other stages, show generic confirmation
else if (selectedStage && selectedStage.trim() !== '') {
// If not yet confirmed, show modal and prevent submission
if (!isStageConfirmed) {
event.preventDefault();
// Count selected candidates
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
// Update confirmation message
const messageElement = document.getElementById('stageConfirmationMessage');
const targetStageElement = document.getElementById('targetStageName');
if (messageElement && targetStageElement) {
if (checkedCount > 0) {
messageElement.textContent = `{% trans "Are you sure you want to move" %} ${checkedCount} {% trans "candidate(s) to this stage?" %}`;
targetStageElement.textContent = selectedStage;
} else {
messageElement.textContent = '{% trans "Please select at least one candidate." %}';
targetStageElement.textContent = '--';
}
}
// Show generic stage confirmation modal
stageConfirmationModal.show();
return false;
}
// If confirmed, let's form submit normally (reset flag for next time)
isStageConfirmed = false;
}
// For other stages, allow normal form submission
});
// Handle confirm button click in hiring modal
if (confirmHiringButton) {
confirmHiringButton.addEventListener('click', function() {
// Hide modal
hiringConfirmationModal.hide();
// Set confirmed flag
isHiringConfirmed = true;
// Programmatically trigger's button click to submit form
changeStageButton.click();
});
}
// Handle confirm button click in generic stage modal
if (confirmStageChangeButton) {
confirmStageChangeButton.addEventListener('click', function() {
// Hide modal
stageConfirmationModal.hide();
// Set confirmed flag
isStageConfirmed = true;
// Programmatically trigger's button click to submit form
changeStageButton.click();
});
}
}
});
</script>
{% endblock %}

View File

@ -556,6 +556,7 @@
</div>
</div>
{% include "recruitment/partials/note_modal.html" %}
{% include "recruitment/partials/stage_confirmation_modal.html" %}
{% endblock %}
@ -568,6 +569,9 @@
const changeStageButton = document.getElementById('changeStage');
const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status');
const stageConfirmationModal = new bootstrap.Modal(document.getElementById('stageConfirmationModal'));
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isConfirmed = false;
if (selectAllCheckbox) {
@ -629,6 +633,57 @@
// Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState();
}
// Stage Confirmation Logic
if (changeStageButton) {
changeStageButton.addEventListener('click', function(event) {
const selectedStage = updateStatus.value;
// Check if a stage is selected (not the default empty option)
if (selectedStage && selectedStage.trim() !== '') {
// If not yet confirmed, show modal and prevent submission
if (!isConfirmed) {
event.preventDefault();
// Count selected candidates
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
// Update confirmation message
const messageElement = document.getElementById('stageConfirmationMessage');
const targetStageElement = document.getElementById('targetStageName');
if (messageElement && targetStageElement) {
if (checkedCount > 0) {
messageElement.textContent = `{% trans "Are you sure you want to move" %} ${checkedCount} {% trans "candidate(s) to this stage?" %}`;
targetStageElement.textContent = selectedStage;
} else {
messageElement.textContent = '{% trans "Please select at least one candidate." %}';
targetStageElement.textContent = '--';
}
}
// Show confirmation modal
stageConfirmationModal.show();
return false;
}
// If confirmed, let the form submit normally (reset flag for next time)
isConfirmed = false;
}
});
// Handle confirm button click in modal
if (confirmStageChangeButton) {
confirmStageChangeButton.addEventListener('click', function() {
// Hide modal
stageConfirmationModal.hide();
// Set confirmed flag
isConfirmed = true;
// Programmatically trigger the button click to submit form
changeStageButton.click();
});
}
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% load i18n %}
<div class="modal fade" id="stageConfirmationModal" tabindex="-1" aria-labelledby="stageConfirmationModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" 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="stageConfirmationModalLabel" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-info-circle me-2"></i>{% trans "Confirm Stage Change" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="d-flex align-items-center justify-content-center py-3 mb-3">
<i class="fas fa-exchange-alt fa-4x" style="color: var(--kaauh-teal);"></i>
</div>
<p class="text-center mb-2" style="font-size: 1.1rem; color: var(--kaauh-primary-text);">
<span id="stageConfirmationMessage">{% trans "Are you sure you want to change the stage?" %}</span>
</p>
<div class="alert alert-info text-center" role="alert">
<i class="fas fa-user-check me-2"></i>
<strong>{% trans "Selected Stage:" %}</strong>
<span id="targetStageName" class="fw-bold">{% trans "--" %}</span>
</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">
<i class="fas fa-times me-1"></i>{% trans "Cancel" %}
</button>
<button type="button" class="btn btn-main-action" id="confirmStageChangeButton">
<i class="fas fa-check me-1"></i>{% trans "Confirm" %}
</button>
</div>
</div>
</div>
</div>