Compare commits

...

7 Commits

33 changed files with 693 additions and 709 deletions

View File

@ -226,6 +226,15 @@ EMAIL_PORT = 2225
# EMAIL_PORT = '2525' # EMAIL_PORT = '2525'
# 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'
# Crispy Forms Configuration # Crispy Forms Configuration
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5" CRISPY_TEMPLATE_PACK = "bootstrap5"

BIN
ats-ERD.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

BIN
ats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -377,6 +377,13 @@ class ApplicationForm(forms.ModelForm):
Submit("submit", _("Submit"), css_class="btn btn-primary"), Submit("submit", _("Submit"), css_class="btn btn-primary"),
) )
def clean_job(self):
job = self.cleaned_data.get("job")
if job.max_applications <= Application.objects.filter(job=job).count():
raise forms.ValidationError(
"The maximum number of applicants for this job has been reached."
)
return job
# def clean(self): # def clean(self):
# cleaned_data = super().clean() # cleaned_data = super().clean()
# job = cleaned_data.get("job") # job = cleaned_data.get("job")
@ -720,7 +727,7 @@ class BulkInterviewTemplateForm(forms.ModelForm):
if end_date and start_date and end_date < start_date: if end_date and start_date and end_date < start_date:
raise forms.ValidationError(_("End date must be after start date")) raise forms.ValidationError(_("End date must be after start date"))
return end_date return end_date
def clean_end_time(self): def clean_end_time(self):
start_time = self.cleaned_data.get("start_time") start_time = self.cleaned_data.get("start_time")
end_time = self.cleaned_data.get("end_time") end_time = self.cleaned_data.get("end_time")
@ -1465,7 +1472,7 @@ class CandidateEmailForm(forms.Form):
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.",
f"We look forward to seeing you at KAAUH.", f"We look forward to seeing you at KAAUH.",
f"If you have any questions before your start date, please contact [Onboarding Contact].", f"If you have any questions before your start date, please contact [Onboarding Contact].",
] ]
elif candidate: elif candidate:
message_parts="" message_parts=""
@ -1639,7 +1646,7 @@ class MessageForm(forms.ModelForm):
# Validate messaging permissions # Validate messaging permissions
if self.user and cleaned_data.get("recipient"): if self.user and cleaned_data.get("recipient"):
self._validate_messaging_permissions(cleaned_data) self._validate_messaging_permissions(cleaned_data)
if self.cleaned_data.get('recipient')==self.user: if self.cleaned_data.get('recipient')==self.user:
raise forms.ValidationError(_("You cannot message yourself")) raise forms.ValidationError(_("You cannot message yourself"))
@ -1805,6 +1812,27 @@ class PasswordResetForm(forms.Form):
raise forms.ValidationError(_('New passwords do not match.')) raise forms.ValidationError(_('New passwords do not match.'))
return cleaned_data return cleaned_data
class PersonPasswordResetForm(forms.Form):
new_password1 = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label=_('New Password')
)
new_password2 = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label=_('Confirm New Password')
)
def clean(self):
"""Custom validation for password reset"""
cleaned_data = super().clean()
new_password1 = cleaned_data.get('new_password1')
new_password2 = cleaned_data.get('new_password2')
if new_password1 and new_password2:
if new_password1 != new_password2:
raise forms.ValidationError(_('New passwords do not match.'))
return cleaned_data
class StaffAssignmentForm(forms.ModelForm): class StaffAssignmentForm(forms.ModelForm):
@ -2225,7 +2253,7 @@ Location: {interview.physical_address}
Room No: {interview.room_number} Room No: {interview.room_number}
This is an onsite schedule. Please arrive 10 minutes early.\n\n""" This is an onsite schedule. Please arrive 10 minutes early.\n\n"""
self.fields['message'].initial = initial_message self.fields['message'].initial = initial_message

View File

@ -28,8 +28,9 @@ class EmailService:
try: try:
# Using EmailMessage for more control (e.g., HTML content) # Using EmailMessage for more control (e.g., HTML content)
from time import sleep
for recipient in recipient_list: for recipient in recipient_list:
sleep(2)
email = EmailMessage( email = EmailMessage(
subject=subject, subject=subject,
body=body, body=body,
@ -46,8 +47,6 @@ class EmailService:
recipient_user=User.objects.filter(email=recipient).first() recipient_user=User.objects.filter(email=recipient).first()
if result and recipient_user and not context["message_created"]: if result and recipient_user and not context["message_created"]:
Message.objects.create(sender=context['sender_user'],recipient=recipient_user,job=context['job'],subject=subject,content=context['email_message'],message_type='DIRECT',is_read=False) Message.objects.create(sender=context['sender_user'],recipient=recipient_user,job=context['job'],subject=subject,content=context['email_message'],message_type='DIRECT',is_read=False)
return len(recipient_list) return len(recipient_list)
except Exception as e: except Exception as e:
@ -111,7 +110,7 @@ class EmailService:
context=context, context=context,
from_email=from_email, from_email=from_email,
html_content=html_content, html_content=html_content,
) )
# Return the count of recipients if successful, or 0 if failure # Return the count of recipients if successful, or 0 if failure

View File

@ -17,6 +17,7 @@ urlpatterns = [
# Job CRUD Operations # Job CRUD Operations
path("jobs/", views.JobListView.as_view(), name="job_list"), path("jobs/", views.JobListView.as_view(), name="job_list"),
path("jobs/create/", views.create_job, name="job_create"), path("jobs/create/", views.create_job, name="job_create"),
path("jobs/bank/", views.job_bank_view, name="job_bank"),
path("jobs/<slug:slug>/", views.job_detail, name="job_detail"), path("jobs/<slug:slug>/", views.job_detail, name="job_detail"),
path("jobs/<slug:slug>/update/", views.edit_job, name="job_update"), path("jobs/<slug:slug>/update/", views.edit_job, name="job_update"),
path("jobs/<slug:slug>/upload-image/", views.job_image_upload, name="job_image_upload"), path("jobs/<slug:slug>/upload-image/", views.job_image_upload, name="job_image_upload"),
@ -25,7 +26,6 @@ urlpatterns = [
path("jobs/<slug:slug>/applicants/", views.job_applicants_view, name="job_applicants"), path("jobs/<slug:slug>/applicants/", views.job_applicants_view, name="job_applicants"),
path("jobs/<slug:slug>/applications/", views.JobApplicationListView.as_view(), name="job_applications_list"), path("jobs/<slug:slug>/applications/", views.JobApplicationListView.as_view(), name="job_applications_list"),
path("jobs/<slug:slug>/calendar/", views.interview_calendar_view, name="interview_calendar"), path("jobs/<slug:slug>/calendar/", views.interview_calendar_view, name="interview_calendar"),
path("jobs/bank/", views.job_bank_view, name="job_bank"),
# Job Actions & Integrations # Job Actions & Integrations
path("jobs/<slug:slug>/post-to-linkedin/", views.post_to_linkedin, name="post_to_linkedin"), path("jobs/<slug:slug>/post-to-linkedin/", views.post_to_linkedin, name="post_to_linkedin"),
@ -103,6 +103,7 @@ urlpatterns = [
path("persons/<slug:slug>/", views.PersonDetailView.as_view(), name="person_detail"), path("persons/<slug:slug>/", views.PersonDetailView.as_view(), name="person_detail"),
path("persons/<slug:slug>/update/", views.PersonUpdateView.as_view(), name="person_update"), path("persons/<slug:slug>/update/", views.PersonUpdateView.as_view(), name="person_update"),
path("persons/<slug:slug>/delete/", views.PersonDeleteView.as_view(), name="person_delete"), path("persons/<slug:slug>/delete/", views.PersonDeleteView.as_view(), name="person_delete"),
path("persons/<slug:slug>/password_reset/", views.password_reset, name="password_reset"),
# ======================================================================== # ========================================================================
# FORM & TEMPLATE MANAGEMENT # FORM & TEMPLATE MANAGEMENT

View File

@ -215,7 +215,7 @@ class PersonListView(StaffRequiredMixin, ListView, LoginRequiredMixin):
.distinct() .distinct()
.order_by("nationality") .order_by("nationality")
) )
nationality = self.request.GET.get("nationality") nationality = self.request.GET.get("nationality")
context["nationality"] = nationality context["nationality"] = nationality
context["nationalities"] = nationalities context["nationalities"] = nationalities
@ -280,6 +280,11 @@ class PersonDetailView(DetailView, LoginRequiredMixin, StaffRequiredMixin):
template_name = "people/person_detail.html" template_name = "people/person_detail.html"
context_object_name = "person" context_object_name = "person"
def get_context_data(self, **kwargs):
from .forms import PersonPasswordResetForm
context = super().get_context_data(**kwargs)
context['password_form'] = PersonPasswordResetForm()
return context
class PersonUpdateView(UpdateView, LoginRequiredMixin, StaffOrAgencyRequiredMixin): class PersonUpdateView(UpdateView, LoginRequiredMixin, StaffOrAgencyRequiredMixin):
model = Person model = Person
@ -1302,11 +1307,11 @@ def delete_form_template(request, template_id):
# @staff_or_candidate_required # @staff_or_candidate_required
def application_submit_form(request, slug): def application_submit_form(request, slug):
"""Display the form as a step-by-step wizard""" """Display the form as a step-by-step wizard"""
form_template = get_object_or_404(FormTemplate, slug=slug, is_active=True) job = get_object_or_404(JobPosting, slug=slug)
template_slug=job.form_template.slug
if not request.user.is_authenticated: if not request.user.is_authenticated:
return redirect("application_signup", slug=slug) return redirect("application_signup", slug=template_slug)
print(form_template.job.slug)
job = get_object_or_404(JobPosting, slug=form_template.job.slug)
if request.user.user_type == "candidate": if request.user.user_type == "candidate":
person = request.user.person_profile person = request.user.person_profile
if job.has_already_applied_to_this_job(person): if job.has_already_applied_to_this_job(person):
@ -1316,23 +1321,16 @@ def application_submit_form(request, slug):
"You have already applied to this job: Multiple applications are not allowed." "You have already applied to this job: Multiple applications are not allowed."
), ),
) )
return redirect("job_application_detail", slug=job.slug) return redirect("job_application_detail", slug=slug)
# template = get_object_or_404(FormTemplate, slug=slug, is_active=True) if job.is_application_limit_reached:
template = job.form_template
stage = template.stages.filter(name="Contact Information")
job_id = template.job.internal_job_id
job = template.job
is_limit_exceeded = job.is_application_limit_reached
if is_limit_exceeded:
messages.error( messages.error(
request, request,
_( _(
"Application limit reached: This job is no longer accepting new applications." "Application limit reached: This job is no longer accepting new applications."
), ),
) )
return redirect("application_detail", slug=job.slug) return redirect("job_application_detail", slug=slug)
if job.is_expired: if job.is_expired:
messages.error( messages.error(
request, request,
@ -1340,12 +1338,12 @@ def application_submit_form(request, slug):
"Application deadline passed: This job is no longer accepting new applications." "Application deadline passed: This job is no longer accepting new applications."
), ),
) )
return redirect("application_detail", slug=job.slug) return redirect("job_application_detail", slug=slug)
return render( return render(
request, request,
"applicant/application_submit_form.html", "applicant/application_submit_form.html",
{"template_slug": template.slug, "job_id": job_id}, {"template_slug": job.form_template.slug, "job_id": job.internal_job_id},
) )
@ -1357,8 +1355,9 @@ def application_submit(request, template_slug):
import re import re
"""Handle form submission""" """Handle form submission"""
if not request.user.is_authenticated: # or request.user.user_type != "candidate": if not request.user.is_authenticated or request.user.user_type != "candidate":
return JsonResponse({"success": False, "message": "Unauthorized access."}) return JsonResponse({"success": False, "message": "Unauthorized access."})
template = get_object_or_404(FormTemplate, slug=template_slug) template = get_object_or_404(FormTemplate, slug=template_slug)
job = template.job job = template.job
if request.method == "POST": if request.method == "POST":
@ -1414,38 +1413,38 @@ def application_submit(request, template_slug):
except FormField.DoesNotExist: except FormField.DoesNotExist:
continue continue
try: try:
gpa = submission.responses.get(field__label="GPA") # gpa = submission.responses.get(field__label="GPA")
if gpa and gpa.value: # if gpa and gpa.value:
gpa_str = gpa.value.replace("/", "").strip() # gpa_str = gpa.value.replace("/", "").strip()
if not re.match(r"^\d+(\.\d+)?$", gpa_str): # if not re.match(r"^\d+(\.\d+)?$", gpa_str):
# --- FIX APPLIED HERE --- # # --- FIX APPLIED HERE ---
return JsonResponse( # return JsonResponse(
{ # {
"success": False, # "success": False,
"message": _("GPA must be a numeric value."), # "message": _("GPA must be a numeric value."),
} # }
) # )
try: # try:
gpa_float = float(gpa_str) # gpa_float = float(gpa_str)
except ValueError: # except ValueError:
# --- FIX APPLIED HERE --- # # --- FIX APPLIED HERE ---
return JsonResponse( # return JsonResponse(
{ # {
"success": False, # "success": False,
"message": _("GPA must be a numeric value."), # "message": _("GPA must be a numeric value."),
} # }
) # )
if not (0.0 <= gpa_float <= 4.0): # if not (0.0 <= gpa_float <= 4.0):
# --- FIX APPLIED HERE --- # # --- FIX APPLIED HERE ---
return JsonResponse( # return JsonResponse(
{ # {
"success": False, # "success": False,
"message": _("GPA must be between 0.0 and 4.0."), # "message": _("GPA must be between 0.0 and 4.0."),
} # }
) # )
resume = submission.responses.get(field__label="Resume Upload") resume = submission.responses.get(field__label="Resume Upload")
@ -1456,7 +1455,7 @@ def application_submit(request, template_slug):
submission.save() submission.save()
# time=timezone.now() # time=timezone.now()
person = request.user.person_profile person = request.user.person_profile
person.gpa = gpa.value if gpa else None # person.gpa = gpa.value if gpa else None
person.save() person.save()
Application.objects.create( Application.objects.create(
person=person, person=person,
@ -2257,60 +2256,60 @@ def reschedule_meeting_for_application(request, slug):
def interview_calendar_view(request, slug): def interview_calendar_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
# # Get all scheduled interviews for this job # Get all scheduled interviews for this job
# scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related( scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related(
# "applicaton", "zoom_meeting" "interview","application"
# ) )
# # Convert interviews to calendar events # Convert interviews to calendar events
# events = [] events = []
# for interview in scheduled_interviews: for interview in scheduled_interviews:
# # Create start datetime # Create start datetime
# start_datetime = datetime.combine( start_datetime = datetime.combine(
# interview.interview_date, interview.interview_time interview.interview_date, interview.interview_time
# ) )
# # Calculate end datetime based on interview duration # Calculate end datetime based on interview duration
# duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60 duration = interview.interview.duration if interview.interview else 60
# end_datetime = start_datetime + timedelta(minutes=duration) end_datetime = start_datetime + timedelta(minutes=duration)
# # Determine event color based on status # Determine event color based on status
# color = "#00636e" # Default color color = "#00636e" # Default color
# if interview.status == "confirmed": if interview.status == "confirmed":
# color = "#00a86b" # Green for confirmed color = "#00a86b" # Green for confirmed
# elif interview.status == "cancelled": elif interview.status == "cancelled":
# color = "#e74c3c" # Red for cancelled color = "#e74c3c" # Red for cancelled
# elif interview.status == "completed": elif interview.status == "completed":
# color = "#95a5a6" # Gray for completed color = "#95a5a6" # Gray for completed
# events.append( events.append(
# { {
# "title": f"Interview: {interview.candidate.name}", "title": f"Interview: {interview.application.person.full_name}",
# "start": start_datetime.isoformat(), "start": start_datetime.isoformat(),
# "end": end_datetime.isoformat(), "end": end_datetime.isoformat(),
# "url": f"{request.path}interview/{interview.id}/", "url": f"{request.path}interview/{interview.id}/",
# "color": color, "color": color,
# "extendedProps": { "extendedProps": {
# "candidate": interview.candidate.name, "candidate": interview.application.person.full_name,
# "email": interview.candidate.email, "email": interview.application.person.email,
# "status": interview.status, "status": interview.interview.status,
# "meeting_id": interview.zoom_meeting.meeting_id "meeting_id": interview.interview.meeting_id
# if interview.zoom_meeting if interview.interview
# else None, else None,
# "join_url": interview.zoom_meeting.join_url "join_url": interview.interview.join_url
# if interview.zoom_meeting if interview.interview
# else None, else None,
# }, },
# } }
# ) )
# context = { context = {
# "job": job, "job": job,
# "events": events, "events": events,
# "calendar_color": "#00636e", "calendar_color": "#00636e",
# } }
# return render(request, "recruitment/interview_calendar.html", context) return render(request, "recruitment/interview_calendar.html", context)
def user_profile_image_update(request, pk): def user_profile_image_update(request, pk):
@ -2434,8 +2433,11 @@ def create_staff_user(request):
@superuser_required @superuser_required
def admin_settings(request): def admin_settings(request):
staffs = User.objects.filter(user_type="staff", is_superuser=False) staffs = User.objects.filter(user_type="staff", is_superuser=False)
paginator=Paginator(staffs,20)
page_number=request.GET.get('page')
page_obj=paginator.get_page(page_number)
form = ToggleAccountForm() form = ToggleAccountForm()
context = {"staffs": staffs, "form": form} context = {"staffs": page_obj, "form": form,"page_obj":page_obj}
return render(request, "user/admin_settings.html", context) return render(request, "user/admin_settings.html", context)
@ -2771,7 +2773,7 @@ def agency_assignment_list(request):
assignments = assignments.filter(status=status_filter) assignments = assignments.filter(status=status_filter)
# Pagination # Pagination
paginator = Paginator(assignments, 15) # Show 15 assignments per page paginator = Paginator(assignments, 20) # Show 15 assignments per page
page_number = request.GET.get("page") page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
@ -2990,6 +2992,22 @@ def portal_password_reset(request, pk):
for error in errors: for error in errors:
messages.error(request, f"{field}: {error}") messages.error(request, f"{field}: {error}")
@require_POST
def password_reset(request, slug):
from .forms import PersonPasswordResetForm
person = get_object_or_404(Person, slug=slug)
if request.method == "POST":
form = PersonPasswordResetForm(request.POST)
if form.is_valid():
person.user.set_password(form.cleaned_data["new_password1"])
person.user.save()
messages.success(request, "Password reset successfully.")
return redirect("person_detail", slug=person.slug)
else:
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f"{field}: {error}")
def portal_login(request): def portal_login(request):
"""Unified portal login for agency and applicant""" """Unified portal login for agency and applicant"""
@ -4500,133 +4518,133 @@ def api_application_detail(request, candidate_id):
# Source CRUD Views # Source CRUD Views
@login_required # @login_required
@staff_user_required # @staff_user_required
def source_list(request): # def source_list(request):
"""List all sources with search and pagination""" # """List all sources with search and pagination"""
search_query = request.GET.get("q", "") # search_query = request.GET.get("q", "")
sources = Source.objects.all() # sources = Source.objects.all()
if search_query: # if search_query:
sources = sources.filter( # sources = sources.filter(
Q(name__icontains=search_query) # Q(name__icontains=search_query)
| Q(source_type__icontains=search_query) # | Q(source_type__icontains=search_query)
| Q(description__icontains=search_query) # | Q(description__icontains=search_query)
) # )
# Order by most recently created # # Order by most recently created
sources = sources.order_by("-created_at") # sources = sources.order_by("-created_at")
# Pagination # # Pagination
paginator = Paginator(sources, 1) # Show 15 sources per page # paginator = Paginator(sources, 1) # Show 15 sources per page
page_number = request.GET.get("page") # page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number) # page_obj = paginator.get_page(page_number)
context = { # context = {
"page_obj": page_obj, # "page_obj": page_obj,
"search_query": search_query, # "search_query": search_query,
"total_sources": sources.count(), # "total_sources": sources.count(),
} # }
return render(request, "recruitment/source_list.html", context) # return render(request, "recruitment/source_list.html", context)
@login_required # @login_required
@staff_user_required # @staff_user_required
def source_create(request): # def source_create(request):
"""Create a new source""" # """Create a new source"""
if request.method == "POST": # if request.method == "POST":
form = SourceForm(request.POST) # form = SourceForm(request.POST)
if form.is_valid(): # if form.is_valid():
source = form.save() # source = form.save()
messages.success(request, f'Source "{source.name}" created successfully!') # messages.success(request, f'Source "{source.name}" created successfully!')
return redirect("source_detail", slug=source.slug) # return redirect("source_detail", slug=source.slug)
else: # else:
messages.error(request, "Please correct the errors below.") # messages.error(request, "Please correct the errors below.")
else: # else:
form = SourceForm() # form = SourceForm()
context = { # context = {
"form": form, # "form": form,
"title": "Create New Source", # "title": "Create New Source",
"button_text": "Create Source", # "button_text": "Create Source",
} # }
return render(request, "recruitment/source_form.html", context) # return render(request, "recruitment/source_form.html", context)
@login_required # @login_required
@staff_user_required # @staff_user_required
def source_detail(request, slug): # def source_detail(request, slug):
"""View details of a specific source""" # """View details of a specific source"""
source = get_object_or_404(Source, slug=slug) # source = get_object_or_404(Source, slug=slug)
# Get integration logs for this source # # Get integration logs for this source
integration_logs = source.integration_logs.order_by("-created_at")[ # integration_logs = source.integration_logs.order_by("-created_at")[
:10 # :10
] # Show recent 10 logs # ] # Show recent 10 logs
# Statistics # # Statistics
total_logs = source.integration_logs.count() # total_logs = source.integration_logs.count()
successful_logs = source.integration_logs.filter(method="POST").count() # successful_logs = source.integration_logs.filter(method="POST").count()
failed_logs = source.integration_logs.filter( # failed_logs = source.integration_logs.filter(
method="POST", status_code__gte=400 # method="POST", status_code__gte=400
).count() # ).count()
context = { # context = {
"source": source, # "source": source,
"integration_logs": integration_logs, # "integration_logs": integration_logs,
"total_logs": total_logs, # "total_logs": total_logs,
"successful_logs": successful_logs, # "successful_logs": successful_logs,
"failed_logs": failed_logs, # "failed_logs": failed_logs,
} # }
return render(request, "recruitment/source_detail.html", context) # return render(request, "recruitment/source_detail.html", context)
@login_required # @login_required
@staff_user_required # @staff_user_required
def source_update(request, slug): # def source_update(request, slug):
"""Update an existing source""" # """Update an existing source"""
source = get_object_or_404(Source, slug=slug) # source = get_object_or_404(Source, slug=slug)
if request.method == "POST": # if request.method == "POST":
form = SourceForm(request.POST, instance=source) # form = SourceForm(request.POST, instance=source)
if form.is_valid(): # if form.is_valid():
source = form.save() # source = form.save()
messages.success(request, f'Source "{source.name}" updated successfully!') # messages.success(request, f'Source "{source.name}" updated successfully!')
return redirect("source_detail", slug=source.slug) # return redirect("source_detail", slug=source.slug)
else: # else:
messages.error(request, "Please correct the errors below.") # messages.error(request, "Please correct the errors below.")
else: # else:
form = SourceForm(instance=source) # form = SourceForm(instance=source)
context = { # context = {
"form": form, # "form": form,
"source": source, # "source": source,
"title": _("Edit Source: %(name)s") % {"name": source.name}, # "title": _("Edit Source: %(name)s") % {"name": source.name},
"button_text": _("Update Source"), # "button_text": _("Update Source"),
} # }
return render(request, "recruitment/source_form.html", context) # return render(request, "recruitment/source_form.html", context)
@login_required # @login_required
@staff_user_required # @staff_user_required
def source_delete(request, slug): # def source_delete(request, slug):
"""Delete a source""" # """Delete a source"""
source = get_object_or_404(Source, slug=slug) # source = get_object_or_404(Source, slug=slug)
if request.method == "POST": # if request.method == "POST":
source_name = source.name # source_name = source.name
source.delete() # source.delete()
messages.success(request, f'Source "{source_name}" deleted successfully!') # messages.success(request, f'Source "{source_name}" deleted successfully!')
return redirect("source_list") # return redirect("source_list")
context = { # context = {
"source": source, # "source": source,
"title": _("Delete Source: %(name)s") % {"name": source.name}, # "title": _("Delete Source: %(name)s") % {"name": source.name},
"message": _('Are you sure you want to delete the source "%(name)s"?') # "message": _('Are you sure you want to delete the source "%(name)s"?')
% {"name": source.name}, # % {"name": source.name},
"cancel_url": reverse("source_detail", kwargs={"slug": source.slug}), # "cancel_url": reverse("source_detail", kwargs={"slug": source.slug}),
} # }
return render(request, "recruitment/source_confirm_delete.html", context) # return render(request, "recruitment/source_confirm_delete.html", context)
@login_required @login_required
@ -5365,7 +5383,7 @@ class JobListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = JobPosting model = JobPosting
template_name = "jobs/job_list.html" template_name = "jobs/job_list.html"
context_object_name = "jobs" context_object_name = "jobs"
paginate_by = 10 paginate_by = 20
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset().order_by("-created_at") queryset = super().get_queryset().order_by("-created_at")
@ -5493,8 +5511,8 @@ class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
queryset = queryset.filter(job__slug=job) queryset = queryset.filter(job__slug=job)
if stage: if stage:
queryset = queryset.filter(stage=stage) queryset = queryset.filter(stage=stage)
queryset=queryset.order_by("-created_at")
return queryset.order_by("-created_at") return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)

View File

@ -16,6 +16,7 @@
--kaauh-info: #17a2b8; --kaauh-info: #17a2b8;
--kaauh-danger: #dc3545; --kaauh-danger: #dc3545;
--kaauh-warning: #ffc107; --kaauh-warning: #ffc107;
--kaauh-border: #eaeff3;
} }
/* Primary Color Overrides */ /* Primary Color Overrides */
@ -733,5 +734,47 @@ html[dir="rtl"] .me-auto { margin-right: 0 !important; margin-left: auto !import
color: var(--kaauh-teal); color: var(--kaauh-teal);
} }
/* --- Pagination Base Styles --- */
.pagination .page-link {
color: var(--kaauh-teal); /* Teal text for links */
border-color: var(--kaauh-border); /* Light border */
background-color: #fff;
font-weight: 500;
font-size: 0.9rem;
transition: all 0.2s ease-in-out;
}
/* --- Hover State --- */
.pagination .page-link:hover {
color: var(--kaauh-teal-dark); /* Darker teal text on hover */
background-color: var(--kaauh-gray-light);
border-color: var(--kaauh-border);
z-index: 2;
}
/* --- Active State (Current Page) --- */
/* This targets the <li class="active"> and overrides the inner span */
.pagination .page-item.active .page-link {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: #ffffff;
z-index: 3;
}
/* --- Focus State (Accessibility) --- */
/* Replaces the default blue glow with a teal glow */
.pagination .page-link:focus {
box-shadow: 0 0 0 0.25rem rgba(0, 99, 110, 0.25);
background-color: var(--kaauh-gray-light);
color: var(--kaauh-teal-dark);
}
/* --- First/Last Item Border Radius (Optional Rounding) --- */
.pagination .page-item:first-child .page-link {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.pagination .page-item:last-child .page-link {
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}

View File

@ -877,33 +877,39 @@
} }
function renderCurrentStage() { function renderCurrentStage() {
if (state.isPreview) { // Always show stage container and hide preview container initially
renderPreview(); elements.stageContainer.style.display = 'block';
return; elements.previewContainer.style.display = 'none';
}
const currentStage = state.stages[state.currentStage]; if (state.isPreview) {
elements.stageContainer.innerHTML = ''; renderPreview();
elements.previewContainer.style.display = 'none'; return;
}
const stageTitle = document.createElement('h2'); const currentStage = state.stages[state.currentStage];
stageTitle.className = 'stage-title'; elements.stageContainer.innerHTML = '';
stageTitle.textContent = currentStage.name;
elements.stageContainer.appendChild(stageTitle);
currentStage.fields.forEach(field => { const stageTitle = document.createElement('h2');
const fieldElement = createFieldElement(field); stageTitle.className = 'stage-title';
elements.stageContainer.appendChild(fieldElement); stageTitle.textContent = currentStage.name;
}); elements.stageContainer.appendChild(stageTitle);
currentStage.fields.forEach(field => {
const fieldElement = createFieldElement(field);
elements.stageContainer.appendChild(fieldElement);
});
// Update navigation buttons
elements.backBtn.style.display = state.currentStage > 0 ? 'flex' : 'none';
elements.submitBtn.style.display = 'none';
elements.nextBtn.style.display = 'flex';
// Fix: Update the Next button text correctly
elements.nextBtn.innerHTML = state.currentStage === state.stages.length - 1 ?
'Preview <i class="fas fa-arrow-right"></i>' :
'Next <i class="fas fa-arrow-right"></i>';
}
// Update navigation buttons
elements.backBtn.style.display = state.currentStage > 0 ? 'flex' : 'none';
elements.submitBtn.style.display = 'none';
elements.nextBtn.style.display = 'flex';
elements.nextBtn.textContent = state.currentStage === state.stages.length - 1 ?
'Preview' :
'Next'
}
function createFieldElement(field) { function createFieldElement(field) {
const fieldDiv = document.createElement('div'); const fieldDiv = document.createElement('div');
@ -1158,103 +1164,106 @@
} }
function renderPreview() { function renderPreview() {
elements.stageContainer.style.display = 'none'; elements.stageContainer.style.display = 'none';
elements.previewContainer.style.display = 'block'; elements.previewContainer.style.display = 'block';
elements.previewContent.innerHTML = ''; elements.previewContent.innerHTML = '';
// Add applicant info if available // Add applicant info if available
if (state.formData.applicant_name || state.formData.applicant_email) { if (state.formData.applicant_name || state.formData.applicant_email) {
const applicantDiv = document.createElement('div'); const applicantDiv = document.createElement('div');
applicantDiv.className = 'preview-item'; applicantDiv.className = 'preview-item';
applicantDiv.innerHTML = ` applicantDiv.innerHTML = `
<div class="preview-label">Applicant Information</div> <div class="preview-label">Applicant Information</div>
<div class="preview-value"> <div class="preview-value">
${state.formData.applicant_name ? `<strong>Name:</strong> ${state.formData.applicant_name}<br>` : ''} ${state.formData.applicant_name ? `<strong>Name:</strong> ${state.formData.applicant_name}<br>` : ''}
${state.formData.applicant_email ? `<strong>Email:</strong> ${state.formData.applicant_email}` : ''} ${state.formData.applicant_email ? `<strong>Email:</strong> ${state.formData.applicant_email}` : ''}
</div> </div>
`; `;
elements.previewContent.appendChild(applicantDiv); elements.previewContent.appendChild(applicantDiv);
} }
// Add stage data // Add stage data
state.stages.forEach(stage => { state.stages.forEach(stage => {
const stageDiv = document.createElement('div'); const stageDiv = document.createElement('div');
stageDiv.className = 'preview-item'; stageDiv.className = 'preview-item';
const stageTitle = document.createElement('div'); const stageTitle = document.createElement('div');
stageTitle.className = 'preview-label'; stageTitle.className = 'preview-label';
stageTitle.textContent = stage.name; stageTitle.textContent = stage.name;
stageDiv.appendChild(stageTitle); stageDiv.appendChild(stageTitle);
const stageContent = document.createElement('div'); const stageContent = document.createElement('div');
stageContent.className = 'preview-value'; stageContent.className = 'preview-value';
stage.fields.forEach(field => { stage.fields.forEach(field => {
let value = state.formData[field.id]; let value = state.formData[field.id];
if (value === undefined || value === null || value === '') { if (value === undefined || value === null || value === '') {
value = '<em>Not provided</em>'; value = '<em>Not provided</em>';
} else if (field.type === 'file' && value instanceof File) { } else if (field.type === 'file' && value instanceof File) {
value = value.name; value = value.name;
} else if (field.type === 'checkbox' && Array.isArray(value)) { } else if (field.type === 'checkbox' && Array.isArray(value)) {
value = value.join(', '); value = value.join(', ');
} }
const fieldDiv = document.createElement('div'); const fieldDiv = document.createElement('div');
fieldDiv.innerHTML = `<strong>${field.label}:</strong> ${value}`; fieldDiv.innerHTML = `<strong>${field.label}:</strong> ${value}`;
stageContent.appendChild(fieldDiv); stageContent.appendChild(fieldDiv);
}); });
stageDiv.appendChild(stageContent); stageDiv.appendChild(stageContent);
elements.previewContent.appendChild(stageDiv); elements.previewContent.appendChild(stageDiv);
}); });
// Update navigation buttons // Update navigation buttons
elements.backBtn.style.display = 'flex'; elements.backBtn.style.display = 'flex';
elements.nextBtn.style.display = 'none'; elements.nextBtn.style.display = 'none';
elements.submitBtn.style.display = 'flex'; elements.submitBtn.style.display = 'flex';
} }
// Navigation Functions // Navigation Functions
function nextStage() { function nextStage() {
if (state.isPreview) { if (state.isPreview) {
submitForm(); submitForm();
return; return;
} }
if (!validateCurrentStage()) { if (!validateCurrentStage()) {
// Scroll to first error // Scroll to first error
const firstError = document.querySelector('.error-message.show'); const firstError = document.querySelector('.error-message.show');
if (firstError) { if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' }); firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
return; return;
} }
if (state.currentStage === state.stages.length - 1) { if (state.currentStage === state.stages.length - 1) {
// Go to preview // Go to preview
state.isPreview = true; state.isPreview = true;
renderCurrentStage(); renderCurrentStage();
updateProgress(); updateProgress();
} else { } else {
// Go to next stage // Go to next stage
state.currentStage++; state.currentStage++;
renderCurrentStage(); renderCurrentStage();
updateProgress(); updateProgress();
} }
} }
function prevStage() { function prevStage() {
if (state.isPreview) { if (state.isPreview) {
// Go back to last stage // Go back to last stage from preview
state.isPreview = false; state.isPreview = false;
renderCurrentStage(); // Set to the last form stage
updateProgress(); state.currentStage = state.stages.length - 1;
} else if (state.currentStage > 0) { renderCurrentStage();
state.currentStage--; updateProgress();
renderCurrentStage(); } else if (state.currentStage > 0) {
updateProgress(); // Go to previous stage
} state.currentStage--;
} renderCurrentStage();
updateProgress();
}
}
// Initialize Application // Initialize Application
function init() { function init() {

View File

@ -41,7 +41,7 @@
<i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %} <i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %}
</button> </button>
{% else %} {% else %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100"> <a href="{% url 'application_submit_form' job.slug %}" class="btn btn-main-action btn-lg w-100">
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %} <i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a> </a>
{% endif %} {% endif %}
@ -220,12 +220,10 @@
<i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %} <i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %}
</button> </button>
{% else %} {% else %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100"> <a href="{% url 'application_submit_form' job.slug %}" class="btn btn-main-action btn-lg w-100">
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %} <i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a> </a>
{% endif %} {% endif %}
</footer> </footer>
{% endif %} {% endif %}
{% endblock content%} {% endblock content%}

View File

@ -234,11 +234,7 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider my-1"></li> <li><hr class="dropdown-divider my-1"></li>
<li> <li>
<form method="post" action="{% url 'account_logout'%}" class="d-inline"> <form method="post" action="{% url 'account_logout'%}" class="d-inline">
{% csrf_token %} {% csrf_token %}

View File

@ -231,7 +231,7 @@
<div class="mt-auto pt-2 border-top"> <div class="mt-auto pt-2 border-top">
<div class="d-flex gap-2 justify-content-end"> <div class="d-flex gap-2 justify-content-end">
<a href="{% url 'application_submit_form' template.slug %}" class="btn btn-outline-primary btn-sm" title="{% trans 'Preview' %}"> <a href="{% url 'application_submit_form' template.job.slug %}" class="btn btn-outline-primary btn-sm" title="{% trans 'Preview' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}"> <a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}">
@ -286,7 +286,7 @@
<td>{{ template.updated_at|date:"M d, Y" }}</td> <td>{{ template.updated_at|date:"M d, Y" }}</td>
<td class="text-end"> <td class="text-end">
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
<a href="{% url 'application_submit_form' template.slug %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}"> <a href="{% url 'application_submit_form' template.job.slug %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}"> <a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">

View File

@ -1,60 +1,58 @@
{% if page_obj.has_previous or page_obj.has_next %}
{% load url_extras i18n %}
{% if page_obj.has_other_pages %}
<nav aria-label="Page navigation" class="mt-4"> <nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{# Helper to build the query string while excluding the 'page' parameter #} {# Get the filter parameters (e.g., "q=searchterm&job=dev") #}
{% load url_extras %}
{# Build a string of all current filters (e.g., &department=IT&type=FULL_TIME) #}
{% add_get_params request.GET as filter_params %} {% add_get_params request.GET as filter_params %}
{% with filter_params=filter_params %}
{# Define the & prefix logic inline to avoid custom tags #}
{% if filter_params %}{% if "?" in request.get_full_path or "page" in request.get_full_path %}{% endif %}{% endif %}
{% if page_obj.has_previous %} {# --- First & Previous --- #}
{% if page_obj.has_previous %}
{# First Page Link #} <li class="page-item">
<li class="page-item"> <a class="page-link text-primary-theme" href="?page=1{% if filter_params %}&{{ filter_params }}{% endif %}">
<a class="page-link text-primary-theme" <i class="fas fa-angle-double-left"></i>
href="?page=1{{ filter_params }}"> </a>
First
</a>
</li>
{# Previous Page Link #}
<li class="page-item">
<a class="page-link text-primary-theme"
href="?page={{ page_obj.previous_page_number }}{{ filter_params }}">
Previous
</a>
</li>
{% endif %}
{# Current Page Status - Use your teal/custom background class here #}
<li class="page-item active" aria-current="page">
<span class="page-link bg-kaauh-teal text-white">
{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
</li> </li>
<li class="page-item">
<a class="page-link text-primary-theme" href="?page={{ page_obj.previous_page_number }}{% if filter_params %}&{{ filter_params }}{% endif %}">
<i class="fas fa-angle-left"></i>
</a>
</li>
{% endif %}
{% if page_obj.has_next %} {# --- Page Numbers --- #}
{# --- Page Numbers --- #}
{# Next Page Link #} {% for num in page_obj.paginator.page_range %}
<li class="page-item"> {% if page_obj.number == num %}
<a class="page-link text-primary-theme" <li class="page-item active"><span class="page-link bg-kaauh-teal text-white border-kaauh-teal">{{ num }}</span></li>
href="?page={{ page_obj.next_page_number }}{{ filter_params }}"> {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
Next <li class="page-item"><a class="page-link text-primary-theme" href="?page={{ num }}{% if filter_params %}&{{ filter_params }}{% endif %}">{{ num }}</a></li>
</a> {% elif num == 1 or num == page_obj.paginator.num_pages %}
</li> {# Optional: Always show first and last page number #}
<li class="page-item"><a class="page-link text-primary-theme" href="?page={{ num }}{% if filter_params %}&{{ filter_params }}{% endif %}">{{ num }}</a></li>
{# Last Page Link #} {% elif num == page_obj.number|add:'-3' or num == page_obj.number|add:'3' %}
<li class="page-item"> {# Show the dots #}
<a class="page-link text-primary-theme" <li class="page-item disabled"><span class="page-link">...</span></li>
href="?page={{ page_obj.paginator.num_pages }}{{ filter_params }}">
Last
</a>
</li>
{% endif %} {% endif %}
{% endfor %}
{% endwith %} {# --- Next & Last --- #}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link text-primary-theme" href="?page={{ page_obj.next_page_number }}{% if filter_params %}&{{ filter_params }}{% endif %}">
<i class="fas fa-angle-right"></i>
</a>
</li>
<li class="page-item">
<a class="page-link text-primary-theme" href="?page={{ page_obj.paginator.num_pages }}{% if filter_params %}&{{ filter_params }}{% endif %}">
<i class="fas fa-angle-double-right"></i>
</a>
</li>
{% endif %}
</ul> </ul>
</nav> </nav>

View File

@ -138,19 +138,7 @@
font-size: 0.8rem !important; font-size: 0.8rem !important;
} }
/* Pagination Styling */
.pagination .page-link {
color: var(--kaauh-teal);
border-color: var(--kaauh-border);
}
.pagination .page-item.active .page-link {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
}
.pagination .page-link:hover {
color: var(--kaauh-teal-dark);
background-color: #f8f9fa;
}
</style> </style>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block title %}{{ job.title }} - Applicants{% endblock %} {% block title %}{{ job.title }} - Applicants{% endblock %}
{% block customCSS %} {% block customCSS %}
@ -374,33 +374,31 @@
{% block content %} {% block content %}
<div class="job-applicants-container"> <div class="job-applicants-container">
<!-- Job Header -->
<div class="job-header"> <div class="job-header">
<h1>{{ job.title }}</h1> <h1>{{ job.title }}</h1>
<p>{{ job.department|default:"General" }} • {{ job.get_job_type_display }} • {{ job.get_workplace_type_display }}</p> <p>{{ job.department|default:"General" }} • {{ job.get_job_type_display }} • {{ job.get_workplace_type_display }}</p>
<div class="job-meta"> <div class="job-meta">
<div class="job-meta-item"> <div class="job-meta-item">
<span></span> <i class="fas fa-users"></i>
<span>{{ total_applications }} Total Applicants</span> <span>{{ total_applications }} {% trans "Total Applicants" %}</span>
</div> </div>
{% if job.max_applications %} {% if job.max_applications %}
<div class="job-meta-item"> <div class="job-meta-item">
<span></span> <i class="fas fa-briefcase"></i>
<span>{{ job.max_applications }} Positions Available</span> <span>{{ job.max_applications }} {% trans "Positions Available" %}</span>
</div> </div>
{% endif %} {% endif %}
<div class="job-meta-item"> <div class="job-meta-item">
<span></span> <i class="fas fa-calendar-alt"></i>
<span>Posted {{ job.created_at|date:"M d, Y" }}</span> <span>{% trans "Posted" %} {{ job.created_at|date:"M d, Y" }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Statistics Section -->
<div class="stats-section"> <div class="stats-section">
<div class="stat-card"> <div class="stat-card">
<div class="stat-number">{{ total_applications }}</div> <div class="stat-number">{{ total_applications }}</div>
<div class="stat-label">Total Applications</div> <div class="stat-label">{% trans "Total Applications" %}</div>
</div> </div>
{% for stage_key, stage_data in stage_stats.items %} {% for stage_key, stage_data in stage_stats.items %}
@ -413,32 +411,29 @@
{% if ai_score_stats.average %} {% if ai_score_stats.average %}
<div class="stat-card"> <div class="stat-card">
<div class="stat-number">{{ ai_score_stats.average|floatformat:1 }}</div> <div class="stat-number">{{ ai_score_stats.average|floatformat:1 }}</div>
<div class="stat-label">Average AI Score</div> <div class="stat-label">{% trans "Average AI Score" %}</div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Filters Section -->
<div class="filters-section"> <div class="filters-section">
<form method="GET" class="filters-form"> <form method="GET" class="filters-form">
<div class="filters-grid"> <div class="filters-grid">
<!-- Search Box -->
<div class="filter-group"> <div class="filter-group">
<label for="search"> Search Applicants</label> <label for="search">{% trans "Search Applicants" %}</label>
<input <input
type="text" type="text"
id="search" id="search"
name="q" name="q"
value="{{ search_query }}" value="{{ search_query }}"
placeholder="Search by name, email, or phone..." placeholder="{% trans 'Search by name, email, or phone...' %}"
> >
</div> </div>
<!-- Stage Filter -->
<div class="filter-group"> <div class="filter-group">
<label for="stage"> Application Stage</label> <label for="stage">{% trans "Application Stage" %}</label>
<select id="stage" name="stage"> <select id="stage" name="stage">
<option value="">All Stages</option> <option value="">{% trans "All Stages" %}</option>
{% for key, value in stage_choices %} {% for key, value in stage_choices %}
<option value="{{ key }}" {% if stage_filter == key %}selected{% endif %}> <option value="{{ key }}" {% if stage_filter == key %}selected{% endif %}>
{{ value }} {{ value }}
@ -447,9 +442,8 @@
</select> </select>
</div> </div>
<!-- AI Score Range -->
<div class="filter-group"> <div class="filter-group">
<label for="min_ai_score"> Min AI Score</label> <label for="min_ai_score">{% trans "Min AI Score" %}</label>
<input <input
type="number" type="number"
id="min_ai_score" id="min_ai_score"
@ -462,7 +456,7 @@
</div> </div>
<div class="filter-group"> <div class="filter-group">
<label for="max_ai_score"> Max AI Score</label> <label for="max_ai_score">{% trans "Max AI Score" %}</label>
<input <input
type="number" type="number"
id="max_ai_score" id="max_ai_score"
@ -474,9 +468,8 @@
> >
</div> </div>
<!-- Date Range -->
<div class="filter-group"> <div class="filter-group">
<label for="date_from"> From Date</label> <label for="date_from">{% trans "From Date" %}</label>
<input <input
type="date" type="date"
id="date_from" id="date_from"
@ -486,7 +479,7 @@
</div> </div>
<div class="filter-group"> <div class="filter-group">
<label for="date_to"> To Date</label> <label for="date_to">{% trans "To Date" %}</label>
<input <input
type="date" type="date"
id="date_to" id="date_to"
@ -495,37 +488,33 @@
> >
</div> </div>
<!-- Sort By -->
<div class="filter-group"> <div class="filter-group">
<label for="sort"> Sort By</label> <label for="sort">{% trans "Sort By" %}</label>
<select id="sort" name="sort"> <select id="sort" name="sort">
<option value="-created_at" {% if sort_by == '-created_at' %}selected{% endif %}>Newest First</option> <option value="-created_at" {% if sort_by == '-created_at' %}selected{% endif %}>{% trans "Newest First" %}</option>
<option value="created_at" {% if sort_by == 'created_at' %}selected{% endif %}>Oldest First</option> <option value="created_at" {% if sort_by == 'created_at' %}selected{% endif %}>{% trans "Oldest First" %}</option>
<option value="person__first_name" {% if sort_by == 'person__first_name' %}selected{% endif %}>Name (A-Z)</option> <option value="person__first_name" {% if sort_by == 'person__first_name' %}selected{% endif %}>{% trans "Name (A-Z)" %}</option>
<option value="-person__first_name" {% if sort_by == '-person__first_name' %}selected{% endif %}>Name (Z-A)</option> <option value="-person__first_name" {% if sort_by == '-person__first_name' %}selected{% endif %}>{% trans "Name (Z-A)" %}</option>
<option value="stage" {% if sort_by == 'stage' %}selected{% endif %}>Stage</option> <option value="stage" {% if sort_by == 'stage' %}selected{% endif %}>{% trans "Stage" %}</option>
</select> </select>
</div> </div>
</div> </div>
<!-- Filter Actions -->
<div class="filters-actions"> <div class="filters-actions">
<button type="submit" class="btn-filter btn-primary"> <button type="submit" class="btn-filter btn-primary">
Apply Filters <i class="fas fa-filter"></i> {% trans "Apply Filters" %}
</button> </button>
<a href="{% url 'job_applicants' job.slug %}" class="btn-filter btn-secondary"> <a href="{% url 'job_applicants' job.slug %}" class="btn-filter btn-secondary">
Clear All <i class="fas fa-undo"></i> {% trans "Clear All" %}
</a> </a>
</div> </div>
</form> </form>
</div> </div>
<!-- Applicants Grid -->
{% if page_obj.object_list %} {% if page_obj.object_list %}
<div class="applicants-grid"> <div class="applicants-grid">
{% for application in page_obj.object_list %} {% for application in page_obj.object_list %}
<div class="applicant-card"> <div class="applicant-card">
<!-- Applicant Header -->
<div class="applicant-header"> <div class="applicant-header">
<div class="applicant-info"> <div class="applicant-info">
<div class="applicant-name"> <div class="applicant-name">
@ -541,15 +530,14 @@
</div> </div>
</div> </div>
<!-- Applicant Meta -->
<div class="applicant-meta"> <div class="applicant-meta">
<div class="meta-item"> <div class="meta-item">
<span></span> <i class="far fa-clock"></i>
<span>Applied {{ application.created_at|date:"M d, Y" }}</span> <span>{% trans "Applied" %} {{ application.created_at|date:"M d, Y" }}</span>
</div> </div>
{% if application.ai_analysis_data.analysis_data_en.match_score %} {% if application.ai_analysis_data.analysis_data_en.match_score %}
<div class="meta-item"> <div class="meta-item">
<span></span> <i class="fas fa-robot"></i>
<span class="ai-score {% if application.ai_analysis_data.analysis_data_en.match_score >= 75 %}high{% elif application.ai_analysis_data.analysis_data_en.match_score >= 50 %}medium{% else %}low{% endif %}"> <span class="ai-score {% if application.ai_analysis_data.analysis_data_en.match_score >= 75 %}high{% elif application.ai_analysis_data.analysis_data_en.match_score >= 50 %}medium{% else %}low{% endif %}">
{{ application.ai_analysis_data.analysis_data_en.match_score }}% {{ application.ai_analysis_data.analysis_data_en.match_score }}%
</span> </span>
@ -557,66 +545,46 @@
{% endif %} {% endif %}
{% if application.person.gpa %} {% if application.person.gpa %}
<div class="meta-item"> <div class="meta-item">
<span></span> <i class="fas fa-graduation-cap"></i>
<span>GPA: {{ application.person.gpa|floatformat:2 }}</span> <span>{% trans "GPA" %}: {{ application.person.gpa|floatformat:2 }}</span>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Applicant Actions -->
<div class="applicant-actions"> <div class="applicant-actions">
{% comment %} <a href="{% url 'person_detail' application.person.pk %}" class="btn-action btn-view-profile">
👁️ Profile
</a> {% endcomment %}
<a href="{% url 'application_detail' application.slug %}" class="btn-filter btn-primary btn-sm"> <a href="{% url 'application_detail' application.slug %}" class="btn-filter btn-primary btn-sm">
Application <i class="fas fa-file-alt"></i> {% trans "Application" %}
</a> </a>
{% if application.stage == 'Interview' %} {% if application.stage == 'Interview' %}
<a href="{% url 'interview_list' %}" class="btn-filter btn-primary btn-sm"> <a href="{% url 'interview_list' %}" class="btn-filter btn-primary btn-sm">
Schedule <i class="fas fa-calendar-check"></i> {% trans "Schedule" %}
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'message_create' %}?job={{ job.pk }}&candidate={{ application.pk }}" class="btn-filter btn-primary btn-sm"> <a href="{% url 'message_create' %}?job={{ job.pk }}&candidate={{ application.pk }}" class="btn-filter btn-primary btn-sm">
✉️ Email <i class="fas fa-envelope"></i> {% trans "Email" %}
</a> </a>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<!-- Pagination -->
{% if page_obj.has_other_pages %} {% if page_obj.has_other_pages %}
<div class="pagination"> {% include "includes/paginator.html" %}
{% if page_obj.has_previous %}
<a href="?page=1{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">« First</a>
<a href="?page={{ page_obj.previous_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}"> Previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Next </a>
<a href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Last »</a>
{% endif %}
</div>
{% endif %} {% endif %}
{% else %} {% else %}
<!-- No Results -->
<div class="no-results"> <div class="no-results">
<h3>😔 No Applicants Found</h3> <h3>😔 {% trans "No Applicants Found" %}</h3>
<p> <p>
{% if search_query or stage_filter or min_ai_score or max_ai_score or date_from or date_to %} {% if search_query or stage_filter or min_ai_score or max_ai_score or date_from or date_to %}
We couldn't find any applicants matching your current filters. Try adjusting your search criteria or clearing some filters. {% trans "We couldn't find any applicants matching your current filters. Try adjusting your search criteria or clearing some filters." %}
{% else %} {% else %}
There are currently no applicants for this job. {% trans "There are currently no applicants for this job." %}
{% endif %} {% endif %}
</p> </p>
<a href="{% url 'job_applicants' job.slug %}" class="btn-filter btn-primary"> <a href="{% url 'job_applicants' job.slug %}" class="btn-filter btn-primary">
🔄 Clear Filters <i class="fas fa-sync-alt"></i> {% trans "Clear Filters" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block title %}Job Bank - All Opportunities{% endblock %} {% block title %}Job Bank - All Opportunities{% endblock %}
@ -303,36 +304,7 @@
color: white; color: white;
} }
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-top: 2rem;
}
.pagination a,
.pagination span {
padding: 0.75rem 1rem;
border: 2px solid #e1e5e9;
border-radius: 8px;
text-decoration: none;
color: #333;
font-weight: 500;
transition: all 0.3s ease;
}
.pagination a:hover {
background: #667eea;
color: white;
border-color: #667eea;
}
.pagination .current {
background: #667eea;
color: white;
border-color: #667eea;
}
.no-results { .no-results {
text-align: center; text-align: center;
@ -387,8 +359,8 @@
<div class="job-bank-container"> <div class="job-bank-container">
<!-- Header Section --> <!-- Header Section -->
<div class="job-bank-header"> <div class="job-bank-header">
<h1>🏦 Job Bank</h1> <h1>🏦 {% trans "Job Bank" %}</h1>
<p>Explore all available opportunities across departments and find your perfect role</p> <p>{% trans "Explore all available opportunities across departments and find your perfect role" %}</p>
</div> </div>
<!-- Filters Section --> <!-- Filters Section -->
@ -397,21 +369,21 @@
<div class="filters-grid"> <div class="filters-grid">
<!-- Search Box --> <!-- Search Box -->
<div class="filter-group search-box"> <div class="filter-group search-box">
<label for="search">🔍 Search Jobs</label> <label for="search">🔍 {% trans "Search Jobs" %}</label>
<input <input
type="text" type="text"
id="search" id="search"
name="q" name="q"
value="{{ search_query }}" value="{{ search_query }}"
placeholder="Search by title, department, or keywords..." placeholder="{% trans "Search by title, department, or keywords..." %}"
> >
</div> </div>
<!-- Department Filter --> <!-- Department Filter -->
<div class="filter-group"> <div class="filter-group">
<label for="department">📁 Department</label> <label for="department">📁 {% trans "Department" %}</label>
<select id="department" name="department"> <select id="department" name="department">
<option value="">All Departments</option> <option value="">{% trans "All Departments" %}</option>
{% for dept in departments %} {% for dept in departments %}
<option value="{{ dept }}" {% if department_filter == dept %}selected{% endif %}> <option value="{{ dept }}" {% if department_filter == dept %}selected{% endif %}>
{{ dept }} {{ dept }}
@ -422,9 +394,9 @@
<!-- Job Type Filter --> <!-- Job Type Filter -->
<div class="filter-group"> <div class="filter-group">
<label for="job_type">💼 Job Type</label> <label for="job_type">💼 {% trans "Job Type" %}</label>
<select id="job_type" name="job_type"> <select id="job_type" name="job_type">
<option value="">All Types</option> <option value="">{% trans "All Types" %}</option>
{% for key, value in job_types.items %} {% for key, value in job_types.items %}
<option value="{{ key }}" {% if job_type_filter == key %}selected{% endif %}> <option value="{{ key }}" {% if job_type_filter == key %}selected{% endif %}>
{{ value }} {{ value }}
@ -435,9 +407,9 @@
<!-- Workplace Type Filter --> <!-- Workplace Type Filter -->
<div class="filter-group"> <div class="filter-group">
<label for="workplace_type">🏢 Workplace Type</label> <label for="workplace_type">🏢 {% trans "Workplace Type" %}</label>
<select id="workplace_type" name="workplace_type"> <select id="workplace_type" name="workplace_type">
<option value="">All Types</option> <option value="">{% trans "All Types" %}</option>
{% for key, value in workplace_types.items %} {% for key, value in workplace_types.items %}
<option value="{{ key }}" {% if workplace_type_filter == key %}selected{% endif %}> <option value="{{ key }}" {% if workplace_type_filter == key %}selected{% endif %}>
{{ value }} {{ value }}
@ -448,9 +420,9 @@
<!-- Status Filter --> <!-- Status Filter -->
<div class="filter-group"> <div class="filter-group">
<label for="status">📊 Status</label> <label for="status">📊 {% trans "Status" %}</label>
<select id="status" name="status"> <select id="status" name="status">
<option value="">All Statuses</option> <option value="">{% trans "All Statuses" %}</option>
{% for key, value in status_choices.items %} {% for key, value in status_choices.items %}
<option value="{{ key }}" {% if status_filter == key %}selected{% endif %}> <option value="{{ key }}" {% if status_filter == key %}selected{% endif %}>
{{ value }} {{ value }}
@ -461,25 +433,25 @@
<!-- Date Filter --> <!-- Date Filter -->
<div class="filter-group"> <div class="filter-group">
<label for="date_filter">📅 Posted Within</label> <label for="date_filter">📅 {% trans "Posted Within" %}</label>
<select id="date_filter" name="date_filter"> <select id="date_filter" name="date_filter">
<option value="">Any Time</option> <option value="">{% trans "Any Time" %}</option>
<option value="week" {% if date_filter == 'week' %}selected{% endif %}>Last Week</option> <option value="week" {% if date_filter == 'week' %}selected{% endif %}>{% trans "Last Week" %}</option>
<option value="month" {% if date_filter == 'month' %}selected{% endif %}>Last Month</option> <option value="month" {% if date_filter == 'month' %}selected{% endif %}>{% trans "Last Month" %}</option>
<option value="quarter" {% if date_filter == 'quarter' %}selected{% endif %}>Last 3 Months</option> <option value="quarter" {% if date_filter == 'quarter' %}selected{% endif %}>{% trans "Last 3 Months" %}</option>
</select> </select>
</div> </div>
<!-- Sort By --> <!-- Sort By -->
<div class="filter-group"> <div class="filter-group">
<label for="sort">🔄 Sort By</label> <label for="sort">🔄 {% trans "Sort By" %}</label>
<select id="sort" name="sort"> <select id="sort" name="sort">
<option value="-created_at" {% if sort_by == '-created_at' %}selected{% endif %}>Newest First</option> <option value="-created_at" {% if sort_by == '-created_at' %}selected{% endif %}>{% trans "Newest First" %}</option>
<option value="created_at" {% if sort_by == 'created_at' %}selected{% endif %}>Oldest First</option> <option value="created_at" {% if sort_by == 'created_at' %}selected{% endif %}>{% trans "Oldest First" %}</option>
<option value="title" {% if sort_by == 'title' %}selected{% endif %}>Title (A-Z)</option> <option value="title" {% if sort_by == 'title' %}selected{% endif %}>{% trans Title %} (A-Z)</option>
<option value="-title" {% if sort_by == '-title' %}selected{% endif %}>Title (Z-A)</option> <option value="-title" {% if sort_by == '-title' %}selected{% endif %}>{% trans "Title" %} (Z-A)</option>
<option value="department" {% if sort_by == 'department' %}selected{% endif %}>Department (A-Z)</option> <option value="department" {% if sort_by == 'department' %}selected{% endif %}>{% trans "Department" %} (A-Z)</option>
<option value="-department" {% if sort_by == '-department' %}selected{% endif %}>Department (Z-A)</option> <option value="-department" {% if sort_by == '-department' %}selected{% endif %}>{% trans "Department" %} (Z-A)</option>
</select> </select>
</div> </div>
</div> </div>
@ -487,10 +459,10 @@
<!-- Filter Actions --> <!-- Filter Actions -->
<div class="filters-actions"> <div class="filters-actions">
<button type="submit" class="btn-filter btn-primary"> <button type="submit" class="btn-filter btn-primary">
🔍 Apply Filters 🔍 {% trans "Apply Filters" %}
</button> </button>
<a href="{% url 'job_bank' %}" class="btn-filter btn-secondary"> <a href="{% url 'job_bank' %}" class="btn-filter btn-secondary">
🔄 Clear All 🔄 {% trans "Clear All" %}
</a> </a>
</div> </div>
</form> </form>
@ -499,9 +471,9 @@
<!-- Results Header --> <!-- Results Header -->
<div class="results-header"> <div class="results-header">
<div class="results-count"> <div class="results-count">
📊 Found <strong>{{ total_jobs }}</strong> job{{ total_jobs|pluralize }} 📊 {% trans "Found" %} <strong>{{ total_jobs }}</strong> {%trans "job" %}{{ total_jobs|pluralize }}
{% if search_query or department_filter or job_type_filter or workplace_type_filter or status_filter or date_filter %} {% if search_query or department_filter or job_type_filter or workplace_type_filter or status_filter or date_filter %}
with filters applied {% trans "with filters applied" %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -535,7 +507,7 @@
{% if job.max_applications %} {% if job.max_applications %}
<div class="meta-item"> <div class="meta-item">
<span class="meta-icon">👥</span> <span class="meta-icon">👥</span>
<span>{{ job.max_applications }} positions</span> <span>{{ job.max_applications }} {% trans "positions" %}</span>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -549,11 +521,11 @@
<div class="job-actions"> <div class="job-actions">
{% if job.status == 'ACTIVE' %} {% if job.status == 'ACTIVE' %}
<a href="{% url 'job_applicants' job.slug %}" class="btn-apply"> <a href="{% url 'job_applicants' job.slug %}" class="btn-apply">
<EFBFBD> View Applicants {% trans "View Applicants" %}
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'job_detail' job.slug %}" class="btn-view"> <a href="{% url 'job_detail' job.slug %}" class="btn-view">
👁️ View Details {% trans "View Details" %}
</a> </a>
</div> </div>
</div> </div>
@ -562,35 +534,21 @@
<!-- Pagination --> <!-- Pagination -->
{% if page_obj.has_other_pages %} {% if page_obj.has_other_pages %}
<div class="pagination"> {% include "includes/paginator.html" %}
{% if page_obj.has_previous %}
<a href="?page=1{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">« First</a>
<a href="?page={{ page_obj.previous_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}"> Previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Next </a>
<a href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Last »</a>
{% endif %}
</div>
{% endif %} {% endif %}
{% else %} {% else %}
<!-- No Results --> <!-- No Results -->
<div class="no-results"> <div class="no-results">
<h3>😔 No Jobs Found</h3> <h3> {% trans "No Jobs Found" %}</h3>
<p> <p>
{% if search_query or department_filter or job_type_filter or workplace_type_filter or status_filter or date_filter %} {% if search_query or department_filter or job_type_filter or workplace_type_filter or status_filter or date_filter %}
We couldn't find any jobs matching your current filters. Try adjusting your search criteria or clearing some filters. {% trans "We couldn't find any jobs matching your current filters. Try adjusting your search criteria or clearing some filters." %}
{% else %} {% else %}
There are currently no job postings in the system. Check back later for new opportunities! {% trans "There are currently no job postings in the system. Check back later for new opportunities!" %}
{% endif %} {% endif %}
</p> </p>
<a href="{% url 'job_bank' %}" class="btn-filter btn-primary"> <a href="{% url 'job_bank' %}" class="btn-filter btn-primary">
🔄 Clear Filters {% trans "Clear Filters" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -147,6 +147,30 @@
{% block content %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<!-- Modal for Staff Assignment -->
<div class="modal fade" id="staffAssignmentModal" tabindex="-1" aria-labelledby="staffAssignmentModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="staffAssignmentModalLabel">
<i class="fas fa-user-plus me-2"></i> {% trans "Assign Staff Member" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'staff_assignment_view' job.slug %}">
{% csrf_token %}
{{staff_form|crispy}}
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Save Assignment" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
@ -192,10 +216,11 @@
{% else %}bg-secondary{% endif %}'> {% else %}bg-secondary{% endif %}'>
{{ job.get_status_display }} {{ job.get_status_display }}
</span> </span>
{% if user.is_staff and user == application.job.assigned_to or user.is_superuser %}
<button type="button" class="btn btn-outline-light btn-sm ms-2" data-bs-toggle="modal" data-bs-target="#editStatusModal"> <button type="button" class="btn btn-outline-light btn-sm ms-2" data-bs-toggle="modal" data-bs-target="#editStatusModal">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</button> </button>
{% endif %}
</div> </div>
{# Share Public Link Button #} {# Share Public Link Button #}
@ -223,7 +248,14 @@
{% endif %} {% endif %}
<div class="float-end"> <div class="float-end">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Assigned to :" %} </strong> {{ job.assigned_to|default:"" }} {% if job.assigned_to %}
<i class="fas fa-user-tie me-2 text-primary"></i>
<strong> {% trans "Assigned to :" %} </strong> {{ job.assigned_to|default:"" }}
{% else %}
<button type="button" class="btn btn-main-action btn-sm" data-bs-toggle="modal" data-bs-target="#staffAssignmentModal">
<i class="fas fa-user-plus me-1"></i> {% trans "Click To Assign" %}
</button>
{% endif %}
</div> </div>
</div> </div>
</h5> </h5>
@ -333,7 +365,10 @@
<a href="{% url 'applications_screening_view' job.slug %}" class="btn btn-main-action"> <a href="{% url 'applications_screening_view' job.slug %}" class="btn btn-main-action">
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applications" %} <i class="fas fa-layer-group me-1"></i> {% trans "Manage Applications" %}
</a> </a>
<a href="{% url 'interview_calendar' job.slug %}" class="btn btn-main-action">
<i class="fas fa-calendar me-1"></i> {% trans "View Calendar" %}
</a>
{% if not job.form_template.is_active %} {% if not job.form_template.is_active %}
{% if not jobzip_created %} {% if not jobzip_created %}
<a href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action"> <a href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action">
@ -369,10 +404,11 @@
<p class="text-muted small mb-3"> <p class="text-muted small mb-3">
{% trans "Manage the custom application forms associated with this job posting." %} {% trans "Manage the custom application forms associated with this job posting." %}
</p> </p>
{% if user.is_staff and user == application.job.assigned_to or user.is_superuser %}
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary w-100"> <a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-list-alt me-1"></i> {% trans "Manage Job Form" %} <i class="fas fa-list-alt me-1"></i> {% trans "Manage Job Form" %}
</a> </a>
{% endif %}
{% comment %} {% if not job.form_template %} {% comment %} {% if not job.form_template %}
<a href="{% url 'create_form_template' %}" class="btn btn-main-action"> <a href="{% url 'create_form_template' %}" class="btn btn-main-action">
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %} <i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
@ -412,31 +448,6 @@
</button> </button>
{% endif %} {% endif %}
<!-- Modal for Staff Assignment -->
<div class="modal fade" id="staffAssignmentModal" tabindex="-1" aria-labelledby="staffAssignmentModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="staffAssignmentModalLabel">
<i class="fas fa-user-plus me-2"></i> {% trans "Assign Staff Member" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'staff_assignment_view' job.slug %}">
{% csrf_token %}
{{staff_form|crispy}}
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Save Assignment" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% if not job.assigned_to %} {% if not job.assigned_to %}
<div class="alert alert-info p-2 small mb-0"> <div class="alert alert-info p-2 small mb-0">
<i class="fas fa-info-circle me-1"></i> {% trans "No staff members assigned to this job yet." %} <i class="fas fa-info-circle me-1"></i> {% trans "No staff members assigned to this job yet." %}

View File

@ -182,39 +182,8 @@
</table> </table>
</div> </div>
{% if page_obj.has_other_pages %}
<nav aria-label="Message pagination"> {% include "includes/paginator.html" %}
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link text-primary-theme" href="?page={{ page_obj.previous_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link bg-primary-theme border-primary-theme">{{ num }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link text-primary-theme" href="?page={{ num }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link text-primary-theme" href="?page={{ page_obj.next_page_number }}&status={{ status_filter }}&type={{ type_filter }}&q={{ search_query }}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %} {% else %}
<div class="text-center text-muted py-5"> <div class="text-center text-muted py-5">
<i class="fas fa-inbox fa-3x mb-3 text-primary-theme"></i> <i class="fas fa-inbox fa-3x mb-3 text-primary-theme"></i>

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static i18n %} {% load static i18n crispy_forms_tags %}
{% block title %}{{ person.get_full_name }} - {{ block.super }}{% endblock %} {% block title %}{{ person.get_full_name }} - {{ block.super }}{% endblock %}
@ -536,6 +536,33 @@
</a> </a>
{% if user.is_staff %} {% if user.is_staff %}
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#setStaffPasswordModal">
<i class="fas fa-lock me-1"></i> {% trans "Set Staff Password" %}
</button>
<!-- Set Staff Password Modal -->
<div class="modal fade" id="setStaffPasswordModal" tabindex="-1" aria-labelledby="setStaffPasswordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="setStaffPasswordModalLabel">
<i class="fas fa-lock me-2"></i> {% trans "Set Staff Password" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form action="{% url 'password_reset' person.slug %}" method="post">
{% csrf_token %}
{{password_form|crispy}}
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
</div>
</form>
</div>
</div>
</div>
</div>
<a href="{% url 'person_update' person.slug %}" class="btn btn-main-action"> <a href="{% url 'person_update' person.slug %}" class="btn btn-main-action">
<i class="fas fa-edit me-1"></i> {% trans "Edit Applicant" %} <i class="fas fa-edit me-1"></i> {% trans "Edit Applicant" %}
</a> </a>
@ -543,8 +570,6 @@
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %} <i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %}
</a> </a>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -108,19 +108,7 @@
background-color: var(--kaauh-gray-light); background-color: var(--kaauh-gray-light);
} }
/* Pagination Link Styling */
.pagination .page-item .page-link {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-border);
}
.pagination .page-item.active .page-link {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
}
.pagination .page-item:hover .page-link:not(.active) {
background-color: #e9ecef;
}
/* Profile Image Styling */ /* Profile Image Styling */
.profile-image-small { .profile-image-small {

View File

@ -192,47 +192,7 @@
<!-- Pagination --> <!-- Pagination -->
{% if page_obj.has_other_pages %} {% if page_obj.has_other_pages %}
<nav aria-label="{% trans 'Assignments pagination' %}"> {% include "includes/paginator.html" %}
<ul class="pagination justify-content-center mt-4">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if search_query %}&q={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}">
<i class="fas fa-angle-double-left"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}">
<i class="fas fa-angle-left"></i>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if search_query %}&q={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}">
<i class="fas fa-angle-right"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&q={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}">
<i class="fas fa-angle-double-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %} {% endif %}
{% else %} {% else %}
<div class="text-center py-5"> <div class="text-center py-5">

View File

@ -219,6 +219,7 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% include "includes/paginator.html" %}
{% else %} {% else %}
<div class="text-center py-5"> <div class="text-center py-5">
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i> <i class="fas fa-briefcase fa-3x text-muted mb-3"></i>

View File

@ -231,7 +231,7 @@
<div class="card-footer text-center"> <div class="card-footer text-center">
<small class="text-muted"> <small class="text-muted">
{% trans "Already have an account?" %} {% trans "Already have an account?" %}
<a href="{% url 'account_login' %}?next={% url 'application_submit_form' job.form_template.slug %}" class="text-decoration-none text-kaauh-teal"> <a href="{% url 'account_login' %}?next={% url 'application_submit_form' job.slug %}" class="text-decoration-none text-kaauh-teal">
{% trans "Login here" %} {% trans "Login here" %}
</a> </a>
</small> </small>

View File

@ -298,7 +298,7 @@
</small> </small>
</div> </div>
{# Change Stage button #} {# Change Stage button #}
{% if user.is_staff %} {% if user.is_staff and user == application.job.assigned_to or user.is_superuser %}
<button type="button" class="btn btn-outline-light btn-sm mt-1" data-bs-toggle="modal" data-bs-target="#stageUpdateModal"> <button type="button" class="btn btn-outline-light btn-sm mt-1" data-bs-toggle="modal" data-bs-target="#stageUpdateModal">
<i class="fas fa-exchange-alt"></i> {% trans "Change Stage" %} <i class="fas fa-exchange-alt"></i> {% trans "Change Stage" %}
</button> </button>

View File

@ -402,7 +402,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
hx-get="{% url 'application_add_note' application.slug %}" hx-get="{% url 'application_add_note' application.slug %}"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-target=".notemodal"> hx-target=".notemodal">
<i class="fas fa-calendar-plus me-1"></i> <i class="fas fa-calendar-plus me-1"></i>
Add note Add note

View File

@ -329,7 +329,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
hx-get="{% url 'application_add_note' application.slug %}" hx-get="{% url 'application_add_note' application.slug %}"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-target=".notemodal"> hx-target=".notemodal">
<i class="fas fa-calendar-plus me-1"></i> <i class="fas fa-calendar-plus me-1"></i>
Add note Add note

View File

@ -120,20 +120,7 @@
background-color: var(--kaauh-gray-light); background-color: var(--kaauh-gray-light);
} }
/* Pagination Link Styling (Consistent) */
.pagination .page-item .page-link {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-border);
}
.pagination .page-item.active .page-link {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
}
.pagination .page-item:hover .page-link:not(.active) {
background-color: #e9ecef;
}
/* Filter & Search Layout Adjustments */ /* Filter & Search Layout Adjustments */
.filter-buttons { .filter-buttons {
display: flex; display: flex;

View File

@ -358,7 +358,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
hx-get="{% url 'application_add_note' application.slug %}" hx-get="{% url 'application_add_note' application.slug %}"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-target=".notemodal"> hx-target=".notemodal">
<i class="fas fa-calendar-plus me-1"></i> <i class="fas fa-calendar-plus me-1"></i>
Add note Add note

View File

@ -383,6 +383,9 @@
<th scope="col" style="width: 5%;"> <th scope="col" style="width: 5%;">
<i class="fas fa-graduation-cap me-1"></i> {% trans "GPA" %} <i class="fas fa-graduation-cap me-1"></i> {% trans "GPA" %}
</th> </th>
<th scope="col" style="width: 5%;">
<i class="fas fa-graduation-cap me-1"></i> {% trans "Years of Experience" %}
</th>
<th scope="col" style="width: 5%;" class="text-center"> <th scope="col" style="width: 5%;" class="text-center">
<i class="fas fa-robot me-1"></i> {% trans "AI Score" %} <i class="fas fa-robot me-1"></i> {% trans "AI Score" %}
</th> </th>
@ -426,6 +429,11 @@
</div> </div>
</td> </td>
<td class="text-center">{{application.person.gpa|default:"0"}}</td> <td class="text-center">{{application.person.gpa|default:"0"}}</td>
<td class="text-center">
<span class="badge ai-score-badge">
{{ application.years_of_experience }}
</span>
</td>
<td class="text-center"> <td class="text-center">
{% if application.is_resume_parsed %} {% if application.is_resume_parsed %}
{% if application.match_score %} {% if application.match_score %}
@ -473,7 +481,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
hx-get="{% url 'application_add_note' application.slug %}" hx-get="{% url 'application_add_note' application.slug %}"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-target=".notemodal"> hx-target=".notemodal">
<i class="fas fa-calendar-plus me-1"></i> <i class="fas fa-calendar-plus me-1"></i>
Add note Add note

View File

@ -40,14 +40,14 @@
<div class="stat-caption">{% trans "Total Slots to be Filled " %}</div> <div class="stat-caption">{% trans "Total Slots to be Filled " %}</div>
</div> </div>
{# GLOBAL - 5. Total Participants #} {% comment %} {# GLOBAL - 5. Total Participants #}
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3><i class="fas fa-address-book stat-icon"></i> {% trans "Total Participants" %}</h3> <h3><i class="fas fa-address-book stat-icon"></i> {% trans "Total Participants" %}</h3>
</div> </div>
<div class="stat-value">{{ total_participants }}</div> <div class="stat-value">{{ total_participants }}</div>
<div class="stat-caption">{% trans "Total Recruiters/Interviewers" %}</div> <div class="stat-caption">{% trans "Total Recruiters/Interviewers" %}</div>
</div> </div> {% endcomment %}
{# GLOBAL - 6. Total LinkedIn Posts #} {# GLOBAL - 6. Total LinkedIn Posts #}
{% comment %} <div class="card"> {% comment %} <div class="card">

View File

@ -4,6 +4,7 @@
{% block title %}{% trans "Sources" %}{% endblock %} {% block title %}{% trans "Sources" %}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
@ -16,7 +17,7 @@
</nav> </nav>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">{% trans "Integration Sources" %}</h1> <h1 class="h3 mb-0">{% trans "Integration Sources" %}</h1>
<a href="{% url 'source_create' %}" class="btn btn-main-action"> <a href="{% url 'source_create' %}" class="btn btn-main-action">
{% trans "Create Source for Integration" %} <i class="fas fa-plus"></i> {% trans "Create Source for Integration" %} <i class="fas fa-plus"></i>
@ -74,6 +75,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for source in page_obj %} {% for source in page_obj %}
<tr> <tr>
<td> <td>
@ -129,7 +131,7 @@
<!-- Pagination --> <!-- Pagination -->
{% include "includes/paginator.html" %} {% include "includes/paginator.html" %}
{% else %} {% else %}
<div class="text-center py-5"> <div class="text-center py-5">
<i class="fas fa-database fa-3x text-muted mb-3"></i> <i class="fas fa-database fa-3x text-muted mb-3"></i>
@ -153,7 +155,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block customJS %}
<script> <script>
// Auto-refresh after status toggle // Auto-refresh after status toggle
document.body.addEventListener('htmx:afterRequest', function(evt) { document.body.addEventListener('htmx:afterRequest', function(evt) {

View File

@ -262,6 +262,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{% include "includes/paginator.html" %}
{# NOTE: Add Pagination links here if the `staffs` object is a Paginator object #} {# NOTE: Add Pagination links here if the `staffs` object is a Paginator object #}

View File

@ -161,7 +161,7 @@
</a> </a>
</div> </div>
<div class="col-md-4 mb-4"> <div class="col-md-4 mb-4">
<a href="{% url "easy_logs" %}" class="text-decoration-none"> <a href="{% url 'easy_logs' %}" class="text-decoration-none">
<div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);"> <div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<i class="fas fa-file-alt fa-3x text-primary-theme me-4"></i> <i class="fas fa-file-alt fa-3x text-primary-theme me-4"></i>
@ -199,25 +199,44 @@
</div> </div>
<div class="text-end mt-3"> <div class="text-end mt-3">
{% if not request.session.linkedin_authenticated %} {% if not request.session.linkedin_authenticated %}
<a class="text-decoration-none text-teal" href="{% url 'linkedin_login' %}">
<a class="text-decoration-none text-teal" href="{% url 'linkedin_login' %}"> <button class="btn btn-sm btn-outline-secondary">
{% trans "Sign to linkedin" %}<i class="fas fa-arrow-right ms-1"></i>
<button class="btn btn-sm btn-outline-secondary"> </button>
{% trans "Sign to linkedin" %}<i class="fas fa-arrow-right ms-1"></i> </a>
</button> {% else %}
<p class="text-primary">
</a> <i class="fab fa-linkedin me-2"></i>
{% trans "LinkedIn Connected" %}
{% else %} </p>
<p class="text-primary">
<i class="fab fa-linkedin me-2"></i>
{% trans "LinkedIn Connected" %}
</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</a> </a>
</div> </div>
<div class="col-md-4 mb-4">
<a href="{% url 'job_bank' %}" class="text-decoration-none">
<div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);">
<div class="d-flex align-items-center">
<i class="fas fa-database fa-3x text-primary-theme me-4"></i>
<div>
<h5 class="fw-bold mb-1" style="color: var(--kaauh-teal-dark);">
{% trans "Job Bank" %}
</h5>
<p class="text-muted small mb-0">
{% trans "Store your job postings in our Job Bank to reuse them later." %}
</p>
</div>
</div>
<div class="text-end mt-3">
<button class="btn btn-sm btn-outline-secondary">
{% trans "Go to Job Bank" %}<i class="fas fa-arrow-right ms-1"></i>
</button>
</div>
</div>
</a>
</div>
</div> </div>
{% endblock %} {% endblock %}