small fix regarding application stages and agency
This commit is contained in:
parent
ce3149770a
commit
184e48c13e
@ -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/"
|
||||
@ -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.",
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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:
|
||||
@ -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,7 +2064,45 @@ def application_update_status(request, slug):
|
||||
else "Applicant",
|
||||
)
|
||||
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(
|
||||
stage=mark_as,
|
||||
hired_date=timezone.now(),
|
||||
@ -2065,6 +2110,9 @@ def application_update_status(request, slug):
|
||||
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")
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
<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>
|
||||
|
||||
<div class="content">
|
||||
{% block content %}
|
||||
<h2>Hello {{ user_name }},</h2>
|
||||
<!-- 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;">
|
||||
|
||||
<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 %}
|
||||
<div class="button-container">
|
||||
<a href="{{ cta_link }}" class="button">{{ cta_text|default:"Click to Proceed" }}</a>
|
||||
<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 %}
|
||||
|
||||
<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>King Abdullah bin Abdulaziz University Hospital</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 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">
|
||||
<p>© {% now "Y" %} Tenhal. All rights reserved.</p>
|
||||
<p><a href="{{ profile_link }}">Manage Preferences</a></p>
|
||||
</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;">© {% 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>
|
||||
@ -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()">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
34
templates/recruitment/partials/stage_confirmation_modal.html
Normal file
34
templates/recruitment/partials/stage_confirmation_modal.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user