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"} ACCOUNT_FORMS = {"signup": "recruitment.forms.StaffSignupForm"}
MAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # MAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_HOST = "10.10.1.110" # EMAIL_HOST = "10.10.1.110"
EMAIL_PORT = 2225 # EMAIL_PORT = 2225
# EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD", "mssp.0Q0rSwb.zr6ke4n2k3e4on12.aHwJqnI") # 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_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = 'smtp.gmail.com' EMAIL_HOST = 'smtp.gmail.com'
# EMAIL_PORT = 587 EMAIL_PORT = 587
# EMAIL_USE_TLS = True EMAIL_USE_TLS = True
# EMAIL_HOST_USER = 'faheedk215@gmail.com' # Use your actual Gmail email address EMAIL_HOST_USER = 'faheedk215@gmail.com' # Use your actual Gmail email address
# EMAIL_HOST_PASSWORD = 'nfxf xpzo bpsb lqje' # EMAIL_HOST_PASSWORD = 'mduo mcsn lwih irkf' #
# DEFAULT_FROM_EMAIL='faheedlearn@gmail.com' DEFAULT_FROM_EMAIL = 'faheedk215@gmail.com'
# Crispy Forms Configuration # Crispy Forms Configuration
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" 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 crispy_forms.layout import Layout, Submit, Row, Column, Field, Div
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
from django.conf import settings
from .models import ( from .models import (
Application, Application,
@ -498,6 +499,7 @@ class JobPostingForm(forms.ModelForm):
"class": "form-control", "class": "form-control",
"min": 1, "min": 1,
"placeholder": "Number of open positions", "placeholder": "Number of open positions",
"required": True,
} }
), ),
"hash_tags": forms.TextInput( "hash_tags": forms.TextInput(
@ -534,6 +536,13 @@ class JobPostingForm(forms.ModelForm):
self.fields["location_city"].initial = "Riyadh" self.fields["location_city"].initial = "Riyadh"
self.fields["location_state"].initial = "Riyadh Province" self.fields["location_state"].initial = "Riyadh Province"
self.fields["location_country"].initial = "Saudi Arabia" 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): def clean_hash_tags(self):
hash_tags = self.cleaned_data.get("hash_tags") hash_tags = self.cleaned_data.get("hash_tags")
@ -1029,6 +1038,40 @@ class HiringAgencyForm(forms.ModelForm):
return website 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): class AgencyJobAssignmentForm(forms.ModelForm):
"""Form for creating and editing agency job assignments""" """Form for creating and editing agency job assignments"""
@ -1400,11 +1443,12 @@ class CandidateEmailForm(forms.Form):
if candidate and candidate.stage == 'Applied': if candidate and candidate.stage == 'Applied':
message_parts = [ 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"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"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"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"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"We wish you the best of luck in your current job search and future career endeavors.",
f"Sincerely,", f"Sincerely,",
f"The KAAUH Recruitment Team", f"The KAAUH Recruitment Team",
@ -1457,6 +1501,7 @@ class CandidateEmailForm(forms.Form):
] ]
elif candidate and candidate.stage == 'Document Review': elif candidate and candidate.stage == 'Document Review':
message_parts = [ message_parts = [
f"Dear Candidate,",
f"Congratulations on progressing to the final stage for the {self.job.title} role!", 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"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.", 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': elif candidate and candidate.stage == 'Hired':
message_parts = [ message_parts = [
f"Dear Candidate,",
f"Welcome aboard,!", f"Welcome aboard,!",
f"We are thrilled to officially confirm your employment as our new {self.job.title}.", 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.", 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 return message_list.count() or 0
User = get_user_model() User = get_user_model()
@ -2006,7 +2008,6 @@ class AgencyJobAssignment(Base):
class AssignmentStatus(models.TextChoices): class AssignmentStatus(models.TextChoices):
ACTIVE = "ACTIVE", _("Active") ACTIVE = "ACTIVE", _("Active")
COMPLETED = "COMPLETED", _("Completed") COMPLETED = "COMPLETED", _("Completed")
EXPIRED = "EXPIRED", _("Expired")
CANCELLED = "CANCELLED", _("Cancelled") CANCELLED = "CANCELLED", _("Cancelled")
agency = models.ForeignKey( agency = models.ForeignKey(
@ -2069,6 +2070,26 @@ class AgencyJobAssignment(Base):
help_text=_("Internal notes about this assignment"), 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: class Meta:
verbose_name = _("Agency Job Assignment") verbose_name = _("Agency Job Assignment")
verbose_name_plural = _("Agency Job Assignments") verbose_name_plural = _("Agency Job Assignments")

View File

@ -112,6 +112,7 @@ class EmailService:
html_content=html_content, 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 the count of recipients if successful, or 0 if failure
return len(recipient_emails) if sent_count > 0 else 0 return len(recipient_emails) if sent_count > 0 else 0

View File

@ -18,6 +18,7 @@ from .models import (
HiringAgency, HiringAgency,
Person, Person,
Source, Source,
AgencyJobAssignment,
) )
from .forms import generate_api_key, generate_api_secret from .forms import generate_api_key, generate_api_secret
from django.contrib.auth import get_user_model 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 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) @receiver(post_save, sender=HiringAgency)
def hiring_agency_created(sender, instance, created, **kwargs): def hiring_agency_created(sender, instance, created, **kwargs):
if created: 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]}...)") logger.info(f"API keys generated successfully for Source: {instance.name} (Key: {api_key[:8]}...)")
else: else:
logger.info(f"Source {instance.name} already has API keys, skipping generation") 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, "email_message": body_message,
"user_email": recipient, "user_email": recipient,
"logo_url": context.pop( "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 # Merge any other custom context variables
**context, **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>/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("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 # Interview Creation
path("interviews/create/<slug:application_slug>/", views.interview_create_type_selection, name="interview_create_type_selection"), path("interviews/create/<slug:application_slug>/", views.interview_create_type_selection, name="interview_create_type_selection"),
@ -166,10 +166,11 @@ urlpatterns = [
# Agency Assignment Management # Agency Assignment Management
path("agency-assignments/", views.agency_assignment_list, name="agency_assignment_list"), 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/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>/", 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>/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>/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 # Agency Access Links
path("agency-access-links/create/", views.agency_access_link_create, name="agency_access_link_create"), 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"] interview_count = stage_stats["interview_count"]
offer_count = stage_stats["offer_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) status_form = JobPostingStatusForm(instance=job)
linkedin_content_form = LinkedPostContentForm(instance=job) linkedin_content_form = LinkedPostContentForm(instance=job)
try: try:
@ -545,6 +549,9 @@ def job_detail(request, slug):
# "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,
# Position statistics
"positions_filled": positions_filled,
"vacant_positions": vacant_positions,
"linkedin_content_form": linkedin_content_form, "linkedin_content_form": linkedin_content_form,
"staff_form": StaffAssignmentForm(), "staff_form": StaffAssignmentForm(),
} }
@ -2057,7 +2064,45 @@ def application_update_status(request, slug):
else "Applicant", else "Applicant",
) )
elif mark_as == "Hired": elif mark_as == "Hired":
print("hired") # 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( c.update(
stage=mark_as, stage=mark_as,
hired_date=timezone.now(), hired_date=timezone.now(),
@ -2065,6 +2110,9 @@ def application_update_status(request, slug):
if mark_as in ["Exam", "Interview", "Offer"] if mark_as in ["Exam", "Interview", "Offer"]
else "Applicant", else "Applicant",
) )
messages.success(
request, f"Applications Updated and marked as Hired"
)
else: else:
print("rejected") print("rejected")
c.update( c.update(
@ -2762,6 +2810,7 @@ def agency_assignment_list(request):
"""List all agency job assignments""" """List all agency job assignments"""
search_query = request.GET.get("q", "") search_query = request.GET.get("q", "")
status_filter = request.GET.get("status", "") status_filter = request.GET.get("status", "")
print(status_filter)
assignments = AgencyJobAssignment.objects.select_related("agency", "job").order_by( assignments = AgencyJobAssignment.objects.select_related("agency", "job").order_by(
"-created_at" "-created_at"
@ -2954,7 +3003,7 @@ def agency_assignment_extend_deadline(request, slug):
new_deadline_dt = datetime.fromisoformat( new_deadline_dt = datetime.fromisoformat(
new_deadline.replace("Z", "+00:00") 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): if timezone.is_naive(new_deadline_dt):
new_deadline_dt = timezone.make_aware(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) 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 @require_POST
def portal_password_reset(request, pk): def portal_password_reset(request, pk):
user = get_object_or_404(User, pk=pk) user = get_object_or_404(User, pk=pk)
@ -6009,6 +6109,7 @@ def applications_hired_view(request, slug):
@login_required @login_required
@staff_user_required @staff_user_required
@csrf_exempt
def update_application_status(request, job_slug, application_slug, stage_type, status): def update_application_status(request, job_slug, application_slug, stage_type, status):
"""Handle exam/interview/offer status updates""" """Handle exam/interview/offer status updates"""
from django.utils import timezone from django.utils import timezone
@ -6651,6 +6752,7 @@ def compose_application_email(request, slug):
if form.is_valid(): if form.is_valid():
# Get email addresses # Get email addresses
email_addresses = form.get_email_addresses() email_addresses = form.get_email_addresses()
print("email_addresses", email_addresses)
if not email_addresses: if not email_addresses:
messages.error(request, "No email selected") 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> <i class="fas fa-briefcase me-2"></i> <span>{% trans "Jobs" %}</span>
</a> </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> <i class="fas fa-university me-2"></i> <span>{% trans "Job Bank" %}</span>
</a> </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> <i class="fas fa-user-tie me-2"></i> <span>{% trans "Applications" %}</span>
</a> </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> <i class="fas fa-user me-2"></i> <span>{% trans "Applicants" %}</span>
</a> </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> <i class="fas fa-building me-2"></i> <span>{% trans "Agencies" %}</span>
</a> </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> <i class="fas fa-calendar-alt me-2"></i> <span>{% trans "Interviews" %}</span>
</a> </a>
@ -68,7 +68,7 @@
<div class="mt-4 pt-3 border-top border-secondary mx-3 uppercase"> <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 %} {% comment %} <small class="text-white-50 px-2">{% trans "System" %}</small> {% endcomment %}
</div> </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> <i class="fas fa-envelope me-2"></i>
<span>{% trans "Messages" %}</span> <span>{% trans "Messages" %}</span>
{% if request.user.get_unread_message_count > 0 %} {% 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"> <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 %} {% comment %} <small class="text-white-50 px-2">{% trans "System" %}</small> {% endcomment %}
</div> </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> <i class="fas fa-cog me-2"></i> <span>{% trans "Settings" %}</span>
</a> </a>
</div> </div>

View File

@ -1,117 +1,265 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ subject }}</title> <title>{{ subject }}</title>
<!--[if mso]>
<style type="text/css">
body, table, td {font-family: Arial, sans-serif !important;}
</style>
<![endif]-->
<style> <style>
/* Define your custom colors */ /* Reset and Base Styles */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
}
/* General Styling */
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
background-color: #f4f4f4; width: 100% !important;
font-family: Arial, sans-serif; -webkit-text-size-adjust: 100%;
color: #333333; -ms-text-size-adjust: 100%;
} background-color: #f5f7fa;
.container { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
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 */
} }
/* 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 { .header {
background-color: #00636e; /* --kaauh-teal */ background: linear-gradient(135deg, #00636e 0%, #004a53 100%);
padding: 20px; padding: 40px 20px;
text-align: center; 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 { .logo {
max-width: 80px; max-width: 80px;
height: auto; max-height: 80px;
border: 2px solid #ffffff; /* White border to make it pop */ display: block;
border-radius: 50%;
} }
/* Content Section */ /* Content */
.content { .content {
padding: 30px; background-color: #ffffff;
line-height: 1.6; padding: 40px 30px;
}
h2 {
color: #004a53; /* --kaauh-teal-dark for headings */
} }
/* Button/Call to Action */ .content h2 {
.button-container { color: #004a53;
text-align: center; font-size: 24px;
margin: 20px 0; 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 { .button {
display: inline-block; display: inline-block;
padding: 12px 25px; padding: 14px 32px;
background-color: #00636e; /* --kaauh-teal */ background-color: #00636e;
color: #ffffff !important; color: #ffffff !important;
text-decoration: none; text-decoration: none;
border-radius: 8px; border-radius: 6px;
font-weight: bold; font-weight: 600;
font-size: 16px; font-size: 16px;
/* Simple hover simulation for supporting clients */ box-shadow: 0 4px 12px rgba(0, 99, 110, 0.25);
border-bottom: 4px solid #004a53; transition: all 0.3s ease;
} }
/* Footer Section */ /* Divider */
.divider {
height: 1px;
background-color: #e5e7eb;
margin: 30px 0;
}
/* Footer */
.footer { .footer {
background-color: #f0f0f0; background-color: #f9fafb;
padding: 20px; 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; text-align: center;
font-size: 12px; font-size: 12px;
color: #777777; color: #888888;
border-top: 2px solid #00636e; 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; 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> </style>
</head> </head>
<body> <body style="margin: 0; padding: 0; background-color: #f5f7fa;">
<div class="container"> <!-- Preheader Text (hidden but appears in inbox preview) -->
<div class="header"> <div style="display: none; max-height: 0; overflow: hidden;">
<img src="{{ logo_url }}" alt="Your Organization Logo" class="logo"> {{ email_preview_text|default:"Important message from King Abdullah bin Abdulaziz University Hospital" }}
</div> </div>
<div class="content"> <!-- Main Container -->
{% block content %} <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: #f5f7fa;">
<h2>Hello {{ user_name }},</h2> <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;">
<p>{{ email_message|safe }}</p> <!-- 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 %} {% if cta_link %}
<div class="button-container"> <div class="button-wrapper" style="text-align: center; margin: 30px 0;">
<a href="{{ cta_link }}" class="button">{{ cta_text|default:"Click to Proceed" }}</a> <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> </div>
{% endif %} {% endif %}
<p>If you have any questions, please reply to this email.</p> <div class="divider" style="height: 1px; background-color: #e5e7eb; margin: 30px 0;"></div>
<p>Thank you,</p> <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>King Abdullah bin Abdulaziz University Hospital</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 %} {% 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>
<div class="footer"> <div class="footer-info" style="text-align: center; font-size: 12px; color: #888888; line-height: 1.6; margin-top: 15px;">
<p>&copy; {% now "Y" %} Tenhal. All rights reserved.</p> <p style="margin: 5px 0;">&copy; {% now "Y" %} Tenhal. All rights reserved.</p>
<p><a href="{{ profile_link }}">Manage Preferences</a></p> <p style="margin: 5px 0;">
</div> <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> </div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body> </body>
</html> </html>

View File

@ -19,8 +19,7 @@
<div class="card-body"> <div class="card-body">
<form hx-boost="true" method="post" id="email-compose-form" action="{% url 'compose_application_email' job.slug %}" <form hx-boost="true" method="post" id="email-compose-form" action="{% url 'compose_application_email' job.slug %}"
hx-include="#application-form" hx-include="#application-form"
hx-target="#messageContent"
hx-select="#messageContent"
hx-push-url="false" hx-push-url="false"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-on::after-request="new bootstrap.Modal('#emailModal')).hide()"> 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> <i class="fas fa-clock"></i> {% trans "Time" %}: {{ interview.interview_time|time:"H:i A" }}<br>
{# --- Type/Location --- #} {# --- Type/Location --- #}
<i class="fas {% if interview.location_type == 'Remote' %}fa-globe{% else %}fa-map-marker-alt{% endif %}"></i> <i class="fas {% if interview.interview.location_type == 'Remote' %}fa-globe{% else %}fa-map-marker-alt{% endif %}"></i>
{% trans "Type" %}: {{ interview.location_type }} {% trans "Type" %}: {{ interview.interview.location_type }}
{% if interview.location_type == 'Remote' %}<br> {% comment %} {% if interview.interview.location_type == 'Remote' %}<br>
{# Using interview.join_url directly if available, assuming interview is the full object #} {# 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> {% else %}<br>
<i class="fas fa-building"></i> {% trans "Location" %}: {{ interview.location_details|default:"Onsite" }} <i class="fas fa-building"></i> {% trans "Location" %}: {{ interview.interview.location_details|default:"Onsite" }}
{% endif %} {% endif %} {% endcomment %}
</p> </p>
<div class="mt-auto pt-2 border-top"> <div class="mt-auto pt-2 border-top">
@ -342,8 +342,8 @@
<i class="fas fa-eye"></i> {% trans "View" %} <i class="fas fa-eye"></i> {% trans "View" %}
</a> </a>
{# Join button logic simplified #} {# Join button logic simplified #}
{% if interview.location_type == 'Remote' and interview.join_url %} {% if interview.interview.location_type == 'Remote' and interview.interview.join_url %}
<a href="{{ interview.join_url }}" target="_blank" class="btn btn-sm btn-outline-secondary"> <a href="{{ interview.interview.join_url }}" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-link"></i> {% trans "Join" %} <i class="fas fa-link"></i> {% trans "Join" %}
</a> </a>
{% endif %} {% endif %}

View File

@ -157,7 +157,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div> <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 }} {{ form.open_positions }}
{% if form.open_positions.errors %}<div class="text-danger small mt-1">{{ form.open_positions.errors }}</div>{% endif %} {% if form.open_positions.errors %}<div class="text-danger small mt-1">{{ form.open_positions.errors }}</div>{% endif %}
</div> </div>

View File

@ -526,7 +526,55 @@
</div> </div>
</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 shadow-sm no-hover mb-4">
<div class="card-header"> <div class="card-header">
<h6 class="mb-0"> <h6 class="mb-0">
@ -560,28 +608,7 @@
</div> </div>
</div> </div>
{# 3. Avg. Time to Interview #} {# 3. Vacancy Fill Rate #}
{% 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-->
<div class="col-4"> <div class="col-4">
<div class="card text-center h-100"> <div class="card text-center h-100">
<div class="card-body p-2"> <div class="card-body p-2">

View File

@ -39,22 +39,22 @@
<nav class="sidebar-nav"> <nav class="sidebar-nav">
{% if request.user.user_type == 'agency' %} {% 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> <i class="fas fa-th-large me-2"></i> <span>{% trans "Dashboard" %}</span>
</a> </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> <i class="fas fa-users me-2"></i> <span>{% trans "Applicants" %}</span>
</a> </a>
{% else %} {% 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> <i class="fas fa-th-large me-2"></i> <span>{% trans "Dashboard" %}</span>
</a> </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> <i class="fas fa-briefcase me-2"></i> <span>{% trans "Careers" %}</span>
</a> </a>
{% endif %} {% 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> <i class="fas fa-envelope me-2"></i>
<span>{% trans "Messages" %}</span> <span>{% trans "Messages" %}</span>
{% if request.user.get_unread_message_count > 0 %} {% 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"> <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> <small class="text-white-50 px-2 uppercase">{% trans "Account" %}</small>
</div> </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> <i class="fas fa-user-circle me-2"></i> <span>{% trans "My Profile" %}</span>
</a> </a>
</div> </div>

View File

@ -366,6 +366,13 @@
</button> </button>
{% endif %} {% 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 %}" <a href="{% url 'agency_assignment_update' assignment.slug %}"
class="btn btn-outline-secondary"> class="btn btn-outline-secondary">
<i class="fas fa-edit me-1"></i> {% trans "Edit Assignment" %} <i class="fas fa-edit me-1"></i> {% trans "Edit Assignment" %}
@ -414,6 +421,65 @@
{% endif %} {% endif %}
</div> </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 --> <!-- Extend Deadline Modal -->
<div class="modal fade" id="extendDeadlineModal" tabindex="-1"> <div class="modal fade" id="extendDeadlineModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">

View File

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

View File

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

View File

@ -476,6 +476,8 @@
</div> </div>
</div> </div>
{% include "recruitment/partials/note_modal.html" %} {% include "recruitment/partials/note_modal.html" %}
{% include "recruitment/partials/stage_confirmation_modal.html" %}
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
@ -486,6 +488,9 @@
const changeStageButton = document.getElementById('changeStage'); const changeStageButton = document.getElementById('changeStage');
const emailButton = document.getElementById('emailBotton'); const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status'); const updateStatus = document.getElementById('update_status');
const stageConfirmationModal = new bootstrap.Modal(document.getElementById('stageConfirmationModal'));
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isConfirmed = false;
if (selectAllCheckbox) { if (selectAllCheckbox) {
@ -547,6 +552,57 @@
// Initial check to set the correct state on load (in case items are pre-checked) // Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState(); 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> </script>
{% endblock %} {% endblock %}

View File

@ -406,6 +406,8 @@
</div> </div>
</div> </div>
{% include "recruitment/partials/note_modal.html" %} {% include "recruitment/partials/note_modal.html" %}
{% include "recruitment/partials/stage_confirmation_modal.html" %}
{% endblock %} {% endblock %}
@ -417,6 +419,9 @@
const changeStageButton = document.getElementById('changeStage'); const changeStageButton = document.getElementById('changeStage');
const emailButton = document.getElementById('emailBotton'); const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status'); const updateStatus = document.getElementById('update_status');
const stageConfirmationModal = new bootstrap.Modal(document.getElementById('stageConfirmationModal'));
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isConfirmed = false;
if (selectAllCheckbox) { if (selectAllCheckbox) {
@ -470,6 +475,57 @@
// Initial check to set the correct state on load (in case items are pre-checked) // Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState(); 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> </script>
{% endblock %} {% endblock %}

View File

@ -196,14 +196,6 @@
</h2> </h2>
</div> </div>
<div class="d-flex gap-2"> <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' %}" <a href="{% url 'export_applications_csv' job.slug 'hired' %}"
class="btn btn-outline-secondary" class="btn btn-outline-secondary"
title="{% trans 'Export hired applications to CSV' %}"> 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> <p class="mb-0">{% trans "These applications have successfully completed the hiring process and joined your team." %}</p>
</div> </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"> <div class="applicant-tracking-timeline">
{% include 'jobs/partials/applicant_tracking.html' %} {% include 'jobs/partials/applicant_tracking.html' %}
</div> </div>
<div class="kaauh-card shadow-sm p-3"> <div class="kaauh-card shadow-sm p-3">
{% if applications %} {% comment %} {% if applications %}
<div class="bulk-action-bar p-3 bg-light border-bottom"> <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"> <form hx-boost="true" hx-include="#application-form" action="{% url 'application_update_status' job.slug %}" method="post" class="action-group">
{% csrf_token %} {% csrf_token %}
@ -268,7 +319,7 @@
</div> </div>
</form> </form>
</div> </div>
{% endif %} {% endif %} {% endcomment %}
<div class="table-responsive"> <div class="table-responsive">
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get"> <form id="application-form" action="{% url 'application_update_status' job.slug %}" method="get">
@ -276,14 +327,14 @@
<table class="table application-table align-middle"> <table class="table application-table align-middle">
<thead> <thead>
<tr> <tr>
<th style="width: 2%"> {% comment %} <th style="width: 2%">
{% if applications %} {% if applications %}
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" class="form-check-input" id="selectAllCheckbox"> type="checkbox" class="form-check-input" id="selectAllCheckbox">
</div> </div>
{% endif %} {% 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-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-phone me-1"></i> {% trans "Contact" %}</th>
<th style="width: 15%"><i class="fas fa-briefcase me-1"></i> {% trans "Applied Position" %}</th> <th style="width: 15%"><i class="fas fa-briefcase me-1"></i> {% trans "Applied Position" %}</th>
@ -295,12 +346,12 @@
<tbody> <tbody>
{% for application in applications %} {% for application in applications %}
<tr> <tr>
<td> {% comment %} <td>
<div class="form-check"> <div class="form-check">
<input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}"> <input name="candidate_ids" value="{{ application.id }}" type="checkbox" class="form-check-input rowCheckbox" id="application-{{ application.id }}">
</div> </div>
</td> </td> {% endcomment %}
<td> <td>
<div class="application-name"> <div class="application-name">
{{ application.name }} {{ application.name }}

View File

@ -514,6 +514,7 @@
{% include "recruitment/partials/note_modal.html" %} {% include "recruitment/partials/note_modal.html" %}
{% include "recruitment/partials/stage_confirmation_modal.html" %}
{% endblock %} {% endblock %}
@ -526,6 +527,9 @@
const emailButton = document.getElementById('emailBotton'); const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status'); const updateStatus = document.getElementById('update_status');
const scheduleInterviewButton = document.getElementById('scheduleInterview'); const scheduleInterviewButton = document.getElementById('scheduleInterview');
const stageConfirmationModal = new bootstrap.Modal(document.getElementById('stageConfirmationModal'));
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isConfirmed = false;
if (selectAllCheckbox) { if (selectAllCheckbox) {
@ -590,6 +594,57 @@
// Initial check to set the correct state on load (in case items are pre-checked) // Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState(); 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) // Handle Meeting Modal Opening (Rescheduling/Scheduling)

View File

@ -200,7 +200,7 @@
<div class="d-flex align-items-end gap-3"> <div class="d-flex align-items-end gap-3">
{# Form: Hired/Rejected Status Update #} {# 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 %} {% csrf_token %}
{# Select element #} {# Select element #}
@ -430,7 +430,43 @@
</div> </div>
</div> </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/note_modal.html" %}
{% include "recruitment/partials/stage_confirmation_modal.html" %}
{% endblock %} {% endblock %}
@ -442,6 +478,9 @@
const changeStageButton = document.getElementById('changeStage'); const changeStageButton = document.getElementById('changeStage');
const emailButton = document.getElementById('emailBotton'); const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status'); 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) { if (selectAllCheckbox) {
@ -503,6 +542,105 @@
// Initial check to set the correct state on load (in case items are pre-checked) // Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState(); 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> </script>
{% endblock %} {% endblock %}

View File

@ -556,6 +556,7 @@
</div> </div>
</div> </div>
{% include "recruitment/partials/note_modal.html" %} {% include "recruitment/partials/note_modal.html" %}
{% include "recruitment/partials/stage_confirmation_modal.html" %}
{% endblock %} {% endblock %}
@ -568,6 +569,9 @@
const changeStageButton = document.getElementById('changeStage'); const changeStageButton = document.getElementById('changeStage');
const emailButton = document.getElementById('emailBotton'); const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status'); const updateStatus = document.getElementById('update_status');
const stageConfirmationModal = new bootstrap.Modal(document.getElementById('stageConfirmationModal'));
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isConfirmed = false;
if (selectAllCheckbox) { if (selectAllCheckbox) {
@ -629,6 +633,57 @@
// Initial check to set the correct state on load (in case items are pre-checked) // Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState(); 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> </script>
{% endblock %} {% 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>