Merge pull request 'ui fixes' (#27) from frontend into main

Reviewed-on: #27
This commit is contained in:
ismail 2025-11-02 13:42:00 +03:00
commit eb122da037
26 changed files with 803 additions and 485 deletions

View File

@ -678,12 +678,10 @@ class Candidate(Base):
).exists() ).exists()
return future_meetings or today_future_meetings return future_meetings or today_future_meetings
# @property @property
# def time_to_hire(self): def scoring_timeout(self):
# time_to_hire=self.hired_date-self.created_at return timezone.now() <= (self.created_at + timezone.timedelta(minutes=5))
# return time_to_hire
class TrainingMaterial(Base): class TrainingMaterial(Base):

View File

@ -34,6 +34,7 @@ urlpatterns = [
path('candidate/<slug:slug>/view/', views_frontend.candidate_detail, name='candidate_detail'), path('candidate/<slug:slug>/view/', views_frontend.candidate_detail, name='candidate_detail'),
path('candidate/<slug:slug>/resume-template/', views_frontend.candidate_resume_template_view, name='candidate_resume_template'), path('candidate/<slug:slug>/resume-template/', views_frontend.candidate_resume_template_view, name='candidate_resume_template'),
path('candidate/<slug:slug>/update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'), path('candidate/<slug:slug>/update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'),
path('candidate/<slug:slug>/retry-scoring/', views_frontend.retry_scoring_view, name='candidate_retry_scoring'),
# Training URLs # Training URLs
path('training/', views_frontend.TrainingListView.as_view(), name='training_list'), path('training/', views_frontend.TrainingListView.as_view(), name='training_list'),
@ -201,23 +202,23 @@ urlpatterns = [
# API URLs for candidate management # API URLs for candidate management
path('api/candidate/<int:candidate_id>/', views.api_candidate_detail, name='api_candidate_detail'), path('api/candidate/<int:candidate_id>/', views.api_candidate_detail, name='api_candidate_detail'),
# Admin Notification API # # Admin Notification API
path('api/admin/notification-count/', views.api_notification_count, name='admin_notification_count'), # path('api/admin/notification-count/', views.api_notification_count, name='admin_notification_count'),
# Agency Notification API # # Agency Notification API
path('api/agency/notification-count/', views.api_notification_count, name='api_agency_notification_count'), # path('api/agency/notification-count/', views.api_notification_count, name='api_agency_notification_count'),
# SSE Notification Stream - temporarily disabled # # SSE Notification Stream
# path('api/notifications/stream/', views.notification_stream, name='notification_stream'), # path('api/notifications/stream/', views.notification_stream, name='notification_stream'),
# Notification URLs # # Notification URLs
path('notifications/', views.notification_list, name='notification_list'), # path('notifications/', views.notification_list, name='notification_list'),
path('notifications/<int:notification_id>/', views.notification_detail, name='notification_detail'), # path('notifications/<int:notification_id>/', views.notification_detail, name='notification_detail'),
path('notifications/<int:notification_id>/mark-read/', views.notification_mark_read, name='notification_mark_read'), # path('notifications/<int:notification_id>/mark-read/', views.notification_mark_read, name='notification_mark_read'),
path('notifications/<int:notification_id>/mark-unread/', views.notification_mark_unread, name='notification_mark_unread'), # path('notifications/<int:notification_id>/mark-unread/', views.notification_mark_unread, name='notification_mark_unread'),
path('notifications/<int:notification_id>/delete/', views.notification_delete, name='notification_delete'), # path('notifications/<int:notification_id>/delete/', views.notification_delete, name='notification_delete'),
path('notifications/mark-all-read/', views.notification_mark_all_read, name='notification_mark_all_read'), # path('notifications/mark-all-read/', views.notification_mark_all_read, name='notification_mark_all_read'),
path('api/notification-count/', views.api_notification_count, name='api_notification_count'), # path('api/notification-count/', views.api_notification_count, name='api_notification_count'),
#participants urls #participants urls

View File

@ -2583,201 +2583,314 @@ def agency_delete(request, slug):
# Notification Views # Notification Views
@login_required # @login_required
def notification_list(request): # def notification_list(request):
"""List all notifications for the current user""" # """List all notifications for the current user"""
# Get filter parameters # # Get filter parameters
status_filter = request.GET.get('status', '') # status_filter = request.GET.get('status', '')
type_filter = request.GET.get('type', '') # type_filter = request.GET.get('type', '')
# Base queryset # # Base queryset
notifications = Notification.objects.filter(recipient=request.user).order_by('-created_at') # notifications = Notification.objects.filter(recipient=request.user).order_by('-created_at')
# Apply filters # # Apply filters
if status_filter: # if status_filter:
if status_filter == 'unread': # if status_filter == 'unread':
notifications = notifications.filter(status=Notification.Status.PENDING) # notifications = notifications.filter(status=Notification.Status.PENDING)
elif status_filter == 'read': # elif status_filter == 'read':
notifications = notifications.filter(status=Notification.Status.READ) # notifications = notifications.filter(status=Notification.Status.READ)
elif status_filter == 'sent': # elif status_filter == 'sent':
notifications = notifications.filter(status=Notification.Status.SENT) # notifications = notifications.filter(status=Notification.Status.SENT)
if type_filter: # if type_filter:
if type_filter == 'in_app': # if type_filter == 'in_app':
notifications = notifications.filter(notification_type=Notification.NotificationType.IN_APP) # notifications = notifications.filter(notification_type=Notification.NotificationType.IN_APP)
elif type_filter == 'email': # elif type_filter == 'email':
notifications = notifications.filter(notification_type=Notification.NotificationType.EMAIL) # notifications = notifications.filter(notification_type=Notification.NotificationType.EMAIL)
# Pagination # # Pagination
paginator = Paginator(notifications, 20) # Show 20 notifications per page # paginator = Paginator(notifications, 20) # Show 20 notifications 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)
# Statistics # # Statistics
total_notifications = notifications.count() # total_notifications = notifications.count()
unread_notifications = notifications.filter(status=Notification.Status.PENDING).count() # unread_notifications = notifications.filter(status=Notification.Status.PENDING).count()
email_notifications = notifications.filter(notification_type=Notification.NotificationType.EMAIL).count() # email_notifications = notifications.filter(notification_type=Notification.NotificationType.EMAIL).count()
context = { # context = {
'page_obj': page_obj, # 'page_obj': page_obj,
'total_notifications': total_notifications, # 'total_notifications': total_notifications,
'unread_notifications': unread_notifications, # 'unread_notifications': unread_notifications,
'email_notifications': email_notifications, # 'email_notifications': email_notifications,
'status_filter': status_filter, # 'status_filter': status_filter,
'type_filter': type_filter, # 'type_filter': type_filter,
} # }
return render(request, 'recruitment/notification_list.html', context) # return render(request, 'recruitment/notification_list.html', context)
@login_required # @login_required
def notification_detail(request, notification_id): # def notification_detail(request, notification_id):
"""View details of a specific notification""" # """View details of a specific notification"""
notification = get_object_or_404(Notification, id=notification_id, recipient=request.user) # notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
# Mark as read if it was pending # # Mark as read if it was pending
if notification.status == Notification.Status.PENDING: # if notification.status == Notification.Status.PENDING:
notification.status = Notification.Status.READ # notification.status = Notification.Status.READ
notification.save(update_fields=['status']) # notification.save(update_fields=['status'])
context = { # context = {
'notification': notification, # 'notification': notification,
} # }
return render(request, 'recruitment/notification_detail.html', context) # return render(request, 'recruitment/notification_detail.html', context)
@login_required # @login_required
def notification_mark_read(request, notification_id): # def notification_mark_read(request, notification_id):
"""Mark a notification as read""" # """Mark a notification as read"""
notification = get_object_or_404(Notification, id=notification_id, recipient=request.user) # notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
if notification.status == Notification.Status.PENDING: # if notification.status == Notification.Status.PENDING:
notification.status = Notification.Status.READ # notification.status = Notification.Status.READ
notification.save(update_fields=['status']) # notification.save(update_fields=['status'])
if 'HX-Request' in request.headers: # if 'HX-Request' in request.headers:
return HttpResponse(status=200) # HTMX success response # return HttpResponse(status=200) # HTMX success response
return redirect('notification_list') # return redirect('notification_list')
@login_required # @login_required
def notification_mark_unread(request, notification_id): # def notification_mark_unread(request, notification_id):
"""Mark a notification as unread""" # """Mark a notification as unread"""
notification = get_object_or_404(Notification, id=notification_id, recipient=request.user) # notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
if notification.status == Notification.Status.READ: # if notification.status == Notification.Status.READ:
notification.status = Notification.Status.PENDING # notification.status = Notification.Status.PENDING
notification.save(update_fields=['status']) # notification.save(update_fields=['status'])
if 'HX-Request' in request.headers: # if 'HX-Request' in request.headers:
return HttpResponse(status=200) # HTMX success response # return HttpResponse(status=200) # HTMX success response
return redirect('notification_list') # return redirect('notification_list')
@login_required # @login_required
def notification_delete(request, notification_id): # def notification_delete(request, notification_id):
"""Delete a notification""" # """Delete a notification"""
notification = get_object_or_404(Notification, id=notification_id, recipient=request.user) # notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
if request.method == 'POST': # if request.method == 'POST':
notification.delete() # notification.delete()
messages.success(request, 'Notification deleted successfully!') # messages.success(request, 'Notification deleted successfully!')
return redirect('notification_list') # return redirect('notification_list')
# For GET requests, show confirmation page # # For GET requests, show confirmation page
context = { # context = {
'notification': notification, # 'notification': notification,
'title': 'Delete Notification', # 'title': 'Delete Notification',
'message': f'Are you sure you want to delete this notification?', # 'message': f'Are you sure you want to delete this notification?',
'cancel_url': reverse('notification_detail', kwargs={'notification_id': notification.id}), # 'cancel_url': reverse('notification_detail', kwargs={'notification_id': notification.id}),
} # }
return render(request, 'recruitment/notification_confirm_delete.html', context) # return render(request, 'recruitment/notification_confirm_delete.html', context)
@login_required # @login_required
def notification_mark_all_read(request): # def notification_mark_all_read(request):
"""Mark all notifications as read for the current user""" # """Mark all notifications as read for the current user"""
if request.method == 'POST': # if request.method == 'POST':
Notification.objects.filter( # Notification.objects.filter(
recipient=request.user, # recipient=request.user,
status=Notification.Status.PENDING # status=Notification.Status.PENDING
).update(status=Notification.Status.READ) # ).update(status=Notification.Status.READ)
messages.success(request, 'All notifications marked as read!') # messages.success(request, 'All notifications marked as read!')
return redirect('notification_list') # return redirect('notification_list')
# For GET requests, show confirmation page # # For GET requests, show confirmation page
unread_count = Notification.objects.filter( # unread_count = Notification.objects.filter(
recipient=request.user, # recipient=request.user,
status=Notification.Status.PENDING # status=Notification.Status.PENDING
).count() # ).count()
context = { # context = {
'unread_count': unread_count, # 'unread_count': unread_count,
'title': 'Mark All as Read', # 'title': 'Mark All as Read',
'message': f'Are you sure you want to mark all {unread_count} notifications as read?', # 'message': f'Are you sure you want to mark all {unread_count} notifications as read?',
'cancel_url': reverse('notification_list'), # 'cancel_url': reverse('notification_list'),
} # }
return render(request, 'recruitment/notification_confirm_all_read.html', context) # return render(request, 'recruitment/notification_confirm_all_read.html', context)
@login_required # @login_required
def api_notification_count(request): # def api_notification_count(request):
"""API endpoint to get unread notification count and recent notifications""" # """API endpoint to get unread notification count and recent notifications"""
# Get unread notifications # # Get unread notifications
unread_notifications = Notification.objects.filter( # unread_notifications = Notification.objects.filter(
recipient=request.user, # recipient=request.user,
status=Notification.Status.PENDING # status=Notification.Status.PENDING
).order_by('-created_at') # ).order_by('-created_at')
# Get recent notifications (last 5) # # Get recent notifications (last 5)
recent_notifications = Notification.objects.filter( # recent_notifications = Notification.objects.filter(
recipient=request.user # recipient=request.user
).order_by('-created_at')[:5] # ).order_by('-created_at')[:5]
# Prepare recent notifications data # # Prepare recent notifications data
recent_data = [] # recent_data = []
for notification in recent_notifications: # for notification in recent_notifications:
time_ago = '' # time_ago = ''
if notification.created_at: # if notification.created_at:
from datetime import datetime, timezone # from datetime import datetime, timezone
now = timezone.now() # now = timezone.now()
diff = now - notification.created_at # diff = now - notification.created_at
if diff.days > 0: # if diff.days > 0:
time_ago = f'{diff.days}d ago' # time_ago = f'{diff.days}d ago'
elif diff.seconds > 3600: # elif diff.seconds > 3600:
hours = diff.seconds // 3600 # hours = diff.seconds // 3600
time_ago = f'{hours}h ago' # time_ago = f'{hours}h ago'
elif diff.seconds > 60: # elif diff.seconds > 60:
minutes = diff.seconds // 60 # minutes = diff.seconds // 60
time_ago = f'{minutes}m ago' # time_ago = f'{minutes}m ago'
else: # else:
time_ago = 'Just now' # time_ago = 'Just now'
recent_data.append({ # recent_data.append({
'id': notification.id, # 'id': notification.id,
'message': notification.message[:100] + ('...' if len(notification.message) > 100 else ''), # 'message': notification.message[:100] + ('...' if len(notification.message) > 100 else ''),
'type': notification.get_notification_type_display(), # 'type': notification.get_notification_type_display(),
'status': notification.get_status_display(), # 'status': notification.get_status_display(),
'time_ago': time_ago, # 'time_ago': time_ago,
'url': reverse('notification_detail', kwargs={'notification_id': notification.id}) # 'url': reverse('notification_detail', kwargs={'notification_id': notification.id})
}) # })
return JsonResponse({ # return JsonResponse({
'count': unread_notifications.count(), # 'count': unread_notifications.count(),
'recent_notifications': recent_data # 'recent_notifications': recent_data
}) # })
# @login_required # @login_required
# def notification_stream(request): # def notification_stream(request):
# """SSE endpoint for real-time notifications - DISABLED""" # """SSE endpoint for real-time notifications"""
# # This function has been disabled due to implementation issues # from django.http import StreamingHttpResponse
# # TODO: Fix SSE implementation or replace with alternative real-time solution # import json
# from django.http import HttpResponse # import time
# return HttpResponse("SSE endpoint temporarily disabled", status=503) # from .signals import SSE_NOTIFICATION_CACHE
# def event_stream():
# """Generator function for SSE events"""
# user_id = request.user.id
# last_notification_id = 0
# # Get initial last notification ID
# last_notification = Notification.objects.filter(
# recipient=request.user
# ).order_by('-id').first()
# if last_notification:
# last_notification_id = last_notification.id
# # Send any cached notifications first
# cached_notifications = SSE_NOTIFICATION_CACHE.get(user_id, [])
# for cached_notification in cached_notifications:
# if cached_notification['id'] > last_notification_id:
# yield f"event: new_notification\n"
# yield f"data: {json.dumps(cached_notification)}\n\n"
# last_notification_id = cached_notification['id']
# while True:
# try:
# # Check for new notifications from cache first
# cached_notifications = SSE_NOTIFICATION_CACHE.get(user_id, [])
# new_cached = [n for n in cached_notifications if n['id'] > last_notification_id]
# for notification_data in new_cached:
# yield f"event: new_notification\n"
# yield f"data: {json.dumps(notification_data)}\n\n"
# last_notification_id = notification_data['id']
# # Also check database for any missed notifications
# new_notifications = Notification.objects.filter(
# recipient=request.user,
# id__gt=last_notification_id
# ).order_by('id')
# if new_notifications.exists():
# for notification in new_notifications:
# # Prepare notification data
# time_ago = ''
# if notification.created_at:
# now = timezone.now()
# diff = now - notification.created_at
# if diff.days > 0:
# time_ago = f'{diff.days}d ago'
# elif diff.seconds > 3600:
# hours = diff.seconds // 3600
# time_ago = f'{hours}h ago'
# elif diff.seconds > 60:
# minutes = diff.seconds // 60
# time_ago = f'{minutes}m ago'
# else:
# time_ago = 'Just now'
# notification_data = {
# 'id': notification.id,
# 'message': notification.message[:100] + ('...' if len(notification.message) > 100 else ''),
# 'type': notification.get_notification_type_display(),
# 'status': notification.get_status_display(),
# 'time_ago': time_ago,
# 'url': reverse('notification_detail', kwargs={'notification_id': notification.id})
# }
# # Send SSE event
# yield f"event: new_notification\n"
# yield f"data: {json.dumps(notification_data)}\n\n"
# last_notification_id = notification.id
# # Update count after sending new notifications
# unread_count = Notification.objects.filter(
# recipient=request.user,
# status=Notification.Status.PENDING
# ).count()
# count_data = {'count': unread_count}
# yield f"event: count_update\n"
# yield f"data: {json.dumps(count_data)}\n\n"
# # Send heartbeat every 30 seconds
# yield f"event: heartbeat\n"
# yield f"data: {json.dumps({'timestamp': int(time.time())})}\n\n"
# # Wait before next check
# time.sleep(5) # Check every 5 seconds
# except Exception as e:
# # Send error event and continue
# error_data = {'error': str(e)}
# yield f"event: error\n"
# yield f"data: {json.dumps(error_data)}\n\n"
# time.sleep(10) # Wait longer on error
# response = StreamingHttpResponse(
# event_stream(),
# content_type='text/event-stream'
# )
# # Set SSE headers
# response['Cache-Control'] = 'no-cache'
# response['X-Accel-Buffering'] = 'no' # Disable buffering for nginx
# response['Connection'] = 'keep-alive'
# context = {
# 'agency': agency,
# 'page_obj': page_obj,
# 'stage_filter': stage_filter,
# 'total_candidates': candidates.count(),
# }
# return render(request, 'recruitment/agency_candidates.html', context)
@login_required @login_required
@ -2864,7 +2977,7 @@ def agency_assignment_create(request,slug=None):
try: try:
from django.forms import HiddenInput from django.forms import HiddenInput
form.initial['agency'] = agency form.initial['agency'] = agency
form.fields['agency'].widget = HiddenInput() # form.fields['agency'].widget = HiddenInput()
except HiringAgency.DoesNotExist: except HiringAgency.DoesNotExist:
pass pass

View File

@ -21,8 +21,15 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView
# JobForm removed - using JobPostingForm instead # JobForm removed - using JobPostingForm instead
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.db.models import Q, Count, Avg
from django.db.models import FloatField from django.db.models import FloatField
from django.db.models import F, IntegerField, Count, Avg, Sum, Q, ExpressionWrapper, fields
from django.db.models.functions import Cast, Coalesce, TruncDate
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from django.utils import timezone
from datetime import timedelta
import json
from datastar_py.django import ( from datastar_py.django import (
DatastarResponse, DatastarResponse,
@ -215,12 +222,18 @@ class CandidateDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
slug_url_kwarg = 'slug' slug_url_kwarg = 'slug'
# def job_detail(request, slug): def retry_scoring_view(request,slug):
# job = get_object_or_404(models.JobPosting, slug=slug, status='Published') from django_q.tasks import async_task
# form = forms.CandidateForm()
# return render(request, 'jobs/job_detail.html', {'job': job, 'form': form})
candidate = get_object_or_404(models.Candidate, slug=slug)
async_task(
'recruitment.tasks.handle_reume_parsing_and_scoring',
candidate.pk,
hook='recruitment.hooks.callback_ai_parsing',
sync=True,
)
return redirect('candidate_detail', slug=candidate.slug)
@ -339,13 +352,6 @@ class TrainingDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
success_url = reverse_lazy('training_list') success_url = reverse_lazy('training_list')
success_message = 'Training material deleted successfully.' success_message = 'Training material deleted successfully.'
from django.db.models import F, IntegerField, Count, Avg, Sum, Q, ExpressionWrapper, fields
from django.db.models.functions import Cast, Coalesce, TruncDate
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from django.utils import timezone
from datetime import timedelta
import json
# IMPORTANT: Ensure 'models' correctly refers to your Django models file # IMPORTANT: Ensure 'models' correctly refers to your Django models file
# Example: from . import models # Example: from . import models
@ -494,11 +500,12 @@ def dashboard_view(request):
# A. Pipeline Funnel (Scoped) # A. Pipeline Funnel (Scoped)
stage_counts = candidate_queryset.values('stage').annotate(count=Count('stage')) stage_counts = candidate_queryset.values('stage').annotate(count=Count('stage'))
stage_map = {item['stage']: item['count'] for item in stage_counts} stage_map = {item['stage']: item['count'] for item in stage_counts}
candidate_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'HIRED'] candidate_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired']
candidates_count = [ candidates_count = [
stage_map.get('Applied', 0), stage_map.get('Exam', 0), stage_map.get('Interview', 0), stage_map.get('Applied', 0), stage_map.get('Exam', 0), stage_map.get('Interview', 0),
stage_map.get('Offer', 0), filled_positions stage_map.get('Offer', 0), stage_map.get('Hired',0)
] ]
# --- 7. GAUGE CHART CALCULATION (Time-to-Hire) --- # --- 7. GAUGE CHART CALCULATION (Time-to-Hire) ---
@ -507,6 +514,15 @@ def dashboard_view(request):
rotation_degrees = rotation_percent * 180 rotation_degrees = rotation_percent * 180
rotation_degrees_final = round(min(rotation_degrees, 180), 1) # Ensure max 180 degrees rotation_degrees_final = round(min(rotation_degrees, 180), 1) # Ensure max 180 degrees
#
hiring_source_counts = candidate_queryset.values('hiring_source').annotate(count=Count('stage'))
source_map= {item['hiring_source']: item['count'] for item in hiring_source_counts}
candidates_count_in_each_source = [
source_map.get('Public', 0), source_map.get('Internal', 0), source_map.get('Agency', 0),
]
all_hiring_sources=["Public", "Internal", "Agency"]
# --- 8. CONTEXT RETURN --- # --- 8. CONTEXT RETURN ---
@ -555,6 +571,10 @@ def dashboard_view(request):
'jobs': all_jobs_queryset, 'jobs': all_jobs_queryset,
'current_job_id': selected_job_pk, 'current_job_id': selected_job_pk,
'current_job': current_job, 'current_job': current_job,
'candidates_count_in_each_source': json.dumps(candidates_count_in_each_source),
'all_hiring_sources': json.dumps(all_hiring_sources),
} }
return render(request, 'recruitment/dashboard.html', context) return render(request, 'recruitment/dashboard.html', context)

View File

@ -9,7 +9,7 @@
<meta name="description" content="{% trans 'King Abdullah Academic University Hospital - Agency Portal' %}"> <meta name="description" content="{% trans 'King Abdullah Academic University Hospital - Agency Portal' %}">
<title>{% block title %}{% trans 'KAAUH Agency Portal' %}{% endblock %}</title> <title>{% block title %}{% trans 'KAAUH Agency Portal' %}{% endblock %}</title>
{% comment %} Load correct Bootstrap CSS file for RTL/LTR {% endcomment %} {# Load correct Bootstrap CSS file for RTL/LTR #}
{% if LANGUAGE_CODE == 'ar' %} {% if LANGUAGE_CODE == 'ar' %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
{% else %} {% else %}
@ -24,91 +24,94 @@
</head> </head>
<body class="d-flex flex-column min-vh-100" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'> <body class="d-flex flex-column min-vh-100" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
{% comment %} <div class="top-bar d-none d-md-block"> <div class="top-bar d-none d-md-block" style="background-color: #f8f9fa;">
<div class="container-fluid"> <div class="container-fluid">
<div class="d-flex justify-content-between align-items-center gap-2 max-width-1600"> <div class="d-flex justify-content-between align-items-center gap-2" style="max-width: 1600px; margin: 0 auto; padding: 0.5rem 0;">
<div class="logo-container d-flex gap-2"> <div class="logo-group d-flex gap-3 align-items-center">
</div>
<div class="clogo-container d-flex gap-2">
</div>
<div class="logo-container d-flex gap-2 align-items-center">
<img src="{% static 'image/vision.svg' %}" alt="{% trans 'Saudi Vision 2030' %}" loading="lazy" style="height: 35px; object-fit: contain;"> <img src="{% static 'image/vision.svg' %}" alt="{% trans 'Saudi Vision 2030' %}" loading="lazy" style="height: 35px; object-fit: contain;">
</div>
<div class="kaauh-logo-container d-flex flex-column flex-md-row align-items-center gap-2 me-0"> <div class="hospital-info d-flex gap-2 align-items-center">
<div class="hospital-text text-center text-md-start me-0"> <div class="hospital-text text-center text-md-end">
<div class="ar text-xs">جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية</div> <div class="small fw-semibold" style="color: #004a53;">
<div class="ar text-xs">ومستشفى الملك عبدالله بن عبدالرحمن التخصصي</div> {% if LANGUAGE_CODE == 'ar' %}
<div class="en text-xs">Princess Nourah bint Abdulrahman University</div> جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية
<div class="en text-xs">King Abdullah bin Abdulaziz University Hospital</div> <br>
ومستشفى الملك عبدالله بن عبدالعزيز التخصصي
{% else %}
Princess Nourah bint Abdulrahman University
<br>
King Abdullah bin Abdulaziz University Hospital
{% endif %}
</div> </div>
</div> </div>
<img src="{% static 'image/kaauh.png' %}" alt="KAAUH Logo" style="max-height: 40px; max-width: 40px;"> <img src="{% static 'image/kaauh.png' %}" alt="KAAUH Logo" style="max-height: 40px; max-width: 40px;">
</div> </div>
</div> </div>
</div> </div>
</div> {% endcomment %} </div>
{# Using inline style for nav background color - replace with a dedicated CSS class (e.g., .bg-kaauh-nav) if defined in main.css #}
<div style="background-color: #00636e;">
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
<div class="container-fluid" style="max-width: 1600px;">
<a class="navbar-brand text-white" href="{% url 'agency_portal_dashboard' %}" aria-label="Agency Dashboard">
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 40px; height: 40px;">
<span class="ms-3 d-none d-md-inline fw-semibold">{% trans "Agency Portal" %}</span>
</a>
<!-- Agency Portal Header --> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#agencyNavbar"
<nav class="navbar navbar-expand-lg navbar-dark sticky-top"> aria-controls="agencyNavbar" aria-expanded="false" aria-label="{% trans 'Toggle navigation' %}">
<div class="container-fluid max-width-1600"> <span class="navbar-toggler-icon"></span>
<!-- Agency Portal Brand --> </button>
<a class="navbar-brand text-white" href="{% url 'agency_portal_dashboard' %}" aria-label="Agency Dashboard">
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 60px; height: 60px;">
<span class="ms-3 d-none d-lg-inline">{% trans "Agency Portal" %}</span>
</a>
<!-- Mobile Toggler --> <div class="collapse navbar-collapse" id="agencyNavbar">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#agencyNavbar" <div class="navbar-nav ms-auto">
aria-controls="agencyNavbar" aria-expanded="false" aria-label="{% trans 'Toggle navigation' %}">
<span class="navbar-toggler-icon"></span> {# NAVIGATION LINKS (Add your portal links here if needed) #}
</button>
<!-- Agency Controls --> <li class="nav-item dropdown">
<div class="collapse navbar-collapse" id="agencyNavbar"> <a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown"
<div class="navbar-nav ms-auto"> data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
<i class="fas fa-globe me-1"></i>
<span class="d-none d-lg-inline">{{ LANGUAGE_CODE|upper }}</span>
</a>
<ul class="dropdown-menu {% if LANGUAGE_CODE == 'ar' %}dropdown-menu-start{% else %}dropdown-menu-end{% endif %}" data-bs-popper="static">
<li>
<form action="/i18n/setlang/" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="en" class="dropdown-item {% if LANGUAGE_CODE == 'en' %}active bg-light-subtle{% endif %}" type="submit">
<span class="me-2">🇺🇸</span> English
</button>
</form>
</li>
<li>
<form action="/i18n/setlang/" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="ar" class="dropdown-item {% if LANGUAGE_CODE == 'ar' %}active bg-light-subtle{% endif %}" type="submit">
<span class="me-2">🇸🇦</span> العربية (Arabic)
</button>
</form>
</li>
</ul>
</li>
<!-- Language Switcher --> <li class="nav-item ms-3">
<li class="nav-item dropdown"> <form method="post" action="{% url 'agency_portal_logout' %}" class="d-inline">
<a class="language-toggle-btn dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" {% csrf_token %}
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}"> <button type="submit" class="btn btn-outline-light btn-sm">
<i class="fas fa-globe"></i> <i class="fas fa-sign-out-alt me-1"></i> {% trans "Logout" %}
<span class="d-none d-lg-inline">{{ LANGUAGE_CODE|upper }}</span> </button>
</a> </form>
<ul class="dropdown-menu {% if LANGUAGE_CODE == 'ar' %}dropdown-menu-start{% else %}dropdown-menu-end{% endif %}" data-bs-popper="static"> </li>
<li> </div>
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="en" class="dropdown-item {% if LANGUAGE_CODE == 'en' %}active bg-light-subtle{% endif %}" type="submit">
<span class="me-2">🇺🇸</span> English
</button>
</form>
</li>
<li>
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="ar" class="dropdown-item {% if LANGUAGE_CODE == 'ar' %}active bg-light-subtle{% endif %}" type="submit">
<span class="me-2">🇸🇦</span> العربية (Arabic)
</button>
</form>
</li>
</ul>
</li>
<!-- Logout -->
<li class="nav-item ms-3">
<form method="post" action="{% url 'agency_portal_logout' %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-outline-light btn-sm">
<i class="fas fa-sign-out-alt me-1"></i> {% trans "Logout" %}
</button>
</form>
</li>
</div> </div>
</div> </div>
</div> </nav>
</nav> </div>
<main class="container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;"> <main class="container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
{# Messages Block (Correct) #}
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show mt-2" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show mt-2" role="alert">
@ -121,10 +124,11 @@
{% endblock %} {% endblock %}
</main> </main>
{# Footer (Correct) #}
<footer class="mt-auto"> <footer class="mt-auto">
<div class="footer-bottom py-3 small text-muted" style="background-color: #00363a;"> <div class="footer-bottom py-3 small text-muted" style="background-color: #00363a;">
<div class="container-fluid"> <div class="container-fluid">
<div class="d-flex justify-content-between align-items-center flex-wrap max-width-1600"> <div class="d-flex justify-content-between align-items-center flex-wrap" style="max-width: 1600px; margin: 0 auto;">
<p class="mb-0 text-white-50"> <p class="mb-0 text-white-50">
&copy; {% now "Y" %} {% trans "King Abdullah Academic University Hospital (KAAUH)." %} &copy; {% now "Y" %} {% trans "King Abdullah Academic University Hospital (KAAUH)." %}
{% trans "All rights reserved." %} {% trans "All rights reserved." %}
@ -142,6 +146,8 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"></script> <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"></script>
{# JavaScript (Left unchanged as it was mostly correct) #}
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Navbar collapse auto-close on link click (Mobile UX) // Navbar collapse auto-close on link click (Mobile UX)
@ -200,4 +206,4 @@
{% block customJS %}{% endblock %} {% block customJS %}{% endblock %}
</body> </body>
</html> </html>

View File

@ -100,7 +100,7 @@
<ul class="navbar-nav ms-2 ms-lg-4"> <ul class="navbar-nav ms-2 ms-lg-4">
<!-- Notification Bell for Admin Users --> <!-- Notification Bell for Admin Users -->
{% if request.user.is_authenticated and request.user.is_staff %} {% comment %} {% if request.user.is_authenticated and request.user.is_staff %}
<li class="nav-item dropdown me-2"> <li class="nav-item dropdown me-2">
<a class="nav-link position-relative" href="#" role="button" id="notificationDropdown" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link position-relative" href="#" role="button" id="notificationDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-bell"></i> <i class="fas fa-bell"></i>
@ -121,7 +121,7 @@
</li> </li>
</ul> </ul>
</li> </li>
{% endif %} {% endif %} {% endcomment %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<button <button
@ -390,7 +390,7 @@
</script> </script>
<!-- Notification JavaScript for Admin Users --> <!-- Notification JavaScript for Admin Users -->
{% if request.user.is_authenticated and request.user.is_staff %} {% comment %} {% if request.user.is_authenticated and request.user.is_staff %}
<script> <script>
// SSE Notification System // SSE Notification System
let eventSource = null; let eventSource = null;
@ -702,10 +702,9 @@
list.innerHTML = '<div class="px-3 py-2 text-muted text-center"><small>{% trans "Error loading messages" %}</small></div>'; list.innerHTML = '<div class="px-3 py-2 text-muted text-center"><small>{% trans "Error loading messages" %}</small></div>';
}); });
} }
</script> </script> {% endcomment %}
{% endif %} {% comment %} {% endif %} {% endcomment %}
{% block customJS %}{% endblock %} {% block customJS %}{% endblock %}
</body> </body>
</html> </html>

View File

@ -1,6 +1,6 @@
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
<div class="modal fade mt-4" id="linkedinData" tabindex="-1" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal fade mt-4" id="linkedinData" tabindex="-1" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog modal-xl" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="myModalLabel">Edit linkedin Post content</h5> <h5 class="modal-title" id="myModalLabel">Edit linkedin Post content</h5>

View File

@ -161,6 +161,60 @@
<div class="text-muted">{{ assignment.admin_notes }}</div> <div class="text-muted">{{ assignment.admin_notes }}</div>
</div> </div>
{% endif %} {% endif %}
</div>
<div class="kaauh-card shadow-sm mb-4">
<div class="card-body my-2">
<h5 class="card-title mb-3 mx-2">
<i class="fas fa-key me-2 text-warning"></i>
{% trans "Access Credentials" %}
</h5>
<div class="mb-3 mx-2">
<label class="form-label text-muted small">{% trans "Login URL" %}</label>
<div class="input-group">
<input type="text" readonly value="{{ request.scheme }}://{{ request.get_host }}{% url 'agency_portal_login' %}"
class="form-control font-monospace" id="loginUrl">
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('loginUrl')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="mb-3 mx-2">
<label class="form-label text-muted small">{% trans "Access Token" %}</label>
<div class="input-group">
<input type="text" readonly value="{{ access_link.unique_token }}"
class="form-control font-monospace" id="accessToken">
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('accessToken')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="mb-3 mx-2">
<label class="form-label text-muted small">{% trans "Password" %}</label>
<div class="input-group">
<input type="text" readonly value="{{ access_link.access_password }}"
class="form-control font-monospace" id="accessPassword">
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('accessPassword')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="alert alert-info mx-2">
<i class="fas fa-info-circle me-2"></i>
{% trans "Share these credentials securely with the agency. They can use this information to log in and submit candidates." %}
</div>
<a href="{% url 'agency_access_link_detail' access_link.slug %}"
class="btn btn-outline-info btn-sm mx-2">
<i class="fas fa-eye me-1"></i> {% trans "View Access Links Details" %}
</a>
</div>
</div> </div>
<!-- Candidates Card --> <!-- Candidates Card -->
@ -277,68 +331,7 @@
</div> </div>
</div> </div>
<!-- Access Link Card -->
<div class="kaauh-card p-4 mb-4">
<h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-link me-2"></i>
{% trans "Access Link" %}
</h5>
{% if access_link %}
<div class="mb-3">
<label class="text-muted small">{% trans "Status" %}</label>
<div>
{% if access_link.is_active %}
<span class="badge bg-success">{% trans "Active" %}</span>
{% else %}
<span class="badge bg-secondary">{% trans "Inactive" %}</span>
{% endif %}
</div>
</div>
<div class="mb-3">
<label class="text-muted small">{% trans "Token" %}</label>
<div class="font-monospace small bg-light p-2 rounded">
{{ access_link.unique_token }}
</div>
</div>
<div class="mb-3">
<label class="text-muted small">{% trans "Expires" %}</label>
<div class="{% if access_link.is_expired %}text-danger{% else %}text-muted{% endif %}">
{{ access_link.expires_at|date:"Y-m-d H:i" }}
</div>
</div>
<div class="mb-3">
<label class="text-muted small">{% trans "Access Count" %}</label>
<div>{{ access_link.access_count }} {% trans "times accessed" %}</div>
</div>
<div class="d-grid gap-2">
<a href="{% url 'agency_access_link_detail' access_link.slug %}"
class="btn btn-outline-info btn-sm">
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
</a>
<button type="button" class="btn btn-outline-secondary btn-sm"
onclick="copyToClipboard('{{ access_link.unique_token }}')">
<i class="fas fa-copy me-1"></i> {% trans "Copy Token" %}
</button>
</div>
{% else %}
<div class="text-center py-3">
<i class="fas fa-link fa-2x text-muted mb-3"></i>
<h6 class="text-muted">{% trans "No Access Link" %}</h6>
<p class="text-muted small">
{% trans "Create an access link to allow the agency to submit candidates." %}
</p>
<a href="{% url 'agency_access_link_create' %}" class="btn btn-main-action btn-sm">
<i class="fas fa-plus me-1"></i> {% trans "Create Access Link" %}
</a>
</div>
{% endif %}
</div>
<!-- Actions Card --> <!-- Actions Card -->
<div class="kaauh-card p-4"> <div class="kaauh-card p-4">
<h5 class="mb-4" style="color: var(--kaauh-teal-dark);"> <h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
@ -473,6 +466,39 @@ function copyToClipboard(text) {
}); });
} }
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
element.select();
document.execCommand('copy');
// Show feedback
const button = element.nextElementSibling;
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i>';
button.classList.add('btn-success');
button.classList.remove('btn-outline-secondary');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 2000);
}
function confirmDeactivate() {
if (confirm('{% trans "Are you sure you want to deactivate this access link? Agencies will no longer be able to use it." %}')) {
// Submit form to deactivate
window.location.href = '{% url "agency_access_link_deactivate" access_link.slug %}';
}
}
function confirmReactivate() {
if (confirm('{% trans "Are you sure you want to reactivate this access link?" %}')) {
// Submit form to reactivate
window.location.href = '{% url "agency_access_link_reactivate" access_link.slug %}';
}
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Set minimum datetime for new deadline // Set minimum datetime for new deadline
const deadlineInput = document.getElementById('new_deadline'); const deadlineInput = document.getElementById('new_deadline');

View File

@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static i18n %} {% load static i18n widget_tweaks %}
{% block title %}{{ title }} - ATS{% endblock %} {% block title %}{{ title }} - ATS{% endblock %}
@ -8,6 +8,7 @@
/* KAAT-S UI Variables */ /* KAAT-S UI Variables */
:root { :root {
--kaauh-teal: #00636e; --kaauh-teal: #00636e;
--kaauh-teal-light: #e0f7f9; /* Added for contrast */
--kaauh-teal-dark: #004a53; --kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3; --kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40; --kaauh-primary-text: #343a40;
@ -16,19 +17,27 @@
--kaauh-danger: #dc3545; --kaauh-danger: #dc3545;
--kaauh-warning: #ffc107; --kaauh-warning: #ffc107;
} }
body {
background-color: #f8f9fa; /* Light background for better contrast */
}
.kaauh-card { .kaauh-card {
padding: 2.5rem; /* Increased padding */
border: 1px solid var(--kaauh-border); border: 1px solid var(--kaauh-border);
border-radius: 0.75rem; border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06); box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white; background-color: white;
} }
/* Main Action Button (Teal Fill) */
.btn-main-action { .btn-main-action {
background-color: var(--kaauh-teal); background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal); border-color: var(--kaauh-teal);
color: white; color: white;
font-weight: 600; font-weight: 600;
padding: 0.6rem 1.25rem;
border-radius: 0.5rem;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.btn-main-action:hover { .btn-main-action:hover {
@ -36,7 +45,21 @@
border-color: var(--kaauh-teal-dark); border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15); box-shadow: 0 4px 8px rgba(0,0,0,0.15);
} }
/* Secondary Action Button (Teal Outline) */
.btn-outline-primary-teal {
color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
font-weight: 600;
padding: 0.6rem 1.25rem;
border-radius: 0.5rem;
}
.btn-outline-primary-teal:hover {
background-color: var(--kaauh-teal);
color: white;
}
/* Form Consistency */
.form-control:focus, .form-select:focus { .form-control:focus, .form-select:focus {
border-color: var(--kaauh-teal); border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25); box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
@ -46,6 +69,30 @@
font-weight: 600; font-weight: 600;
color: var(--kaauh-primary-text); color: var(--kaauh-primary-text);
} }
/* Applying Bootstrap classes to Django fields if not done in the form definition */
.kaauh-field-control > input,
.kaauh-field-control > textarea,
.kaauh-field-control > select {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--kaauh-primary-text);
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
appearance: none;
border-radius: 0.5rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
/* Specific overrides for different types */
.kaauh-field-control > select {
padding-right: 2.5rem; /* Space for the caret */
}
</style> </style>
{% endblock %} {% endblock %}
@ -53,7 +100,6 @@
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<div class="row"> <div class="row">
<div class="col-lg-8 mx-auto"> <div class="col-lg-8 mx-auto">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div> <div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;"> <h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
@ -64,36 +110,40 @@
{% trans "Assign a job to an external hiring agency" %} {% trans "Assign a job to an external hiring agency" %}
</p> </p>
</div> </div>
<a href="{% url 'agency_assignment_list' %}" class="btn btn-outline-secondary"> <a href="{% url 'agency_assignment_list' %}" class="btn btn-outline-primary-teal">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Assignments" %} <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Assignments" %}
</a> </a>
</div> </div>
<!-- Form Card -->
<div class="kaauh-card"> <div class="kaauh-card">
<form method="post" novalidate> <form method="post" novalidate>
{% csrf_token %} {% csrf_token %}
<!-- Agency and Job Selection -->
{{ form.agency }}
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
{% comment %} <div class="col-md-6">
<div class="col-md-6">
<label for="{{ form.agency.id_for_label }}" class="form-label"> <label for="{{ form.agency.id_for_label }}" class="form-label">
{{ form.agency.label }} <span class="text-danger">*</span> {{ form.agency.label }} <span class="text-danger">*</span>
</label> </label>
{{ form.agency }} {# Wrapper Div for styling consistency (Assumes agency is a SELECT field) #}
<div class="kaauh-field-control">
{{ form.agency|attr:'class:form-select' }}
</div>
{% if form.agency.errors %} {% if form.agency.errors %}
<div class="text-danger small mt-1"> <div class="text-danger small mt-1">
{% for error in form.agency.errors %}{{ error }}{% endfor %} {% for error in form.agency.errors %}{{ error }}{% endfor %}
</div> </div>
{% endif %} {% endif %}
</div> {% endcomment %} </div>
<div class="col-md-6"> <div class="col-md-6">
<label for="{{ form.job.id_for_label }}" class="form-label"> <label for="{{ form.job.id_for_label }}" class="form-label">
{{ form.job.label }} <span class="text-danger">*</span> {{ form.job.label }} <span class="text-danger">*</span>
</label> </label>
{{ form.job }} {# Wrapper Div for styling consistency (Assumes job is a SELECT field) #}
<div class="kaauh-field-control">
{{ form.job|attr:'class:form-select' }}
</div>
{% if form.job.errors %} {% if form.job.errors %}
<div class="text-danger small mt-1"> <div class="text-danger small mt-1">
{% for error in form.job.errors %}{{ error }}{% endfor %} {% for error in form.job.errors %}{{ error }}{% endfor %}
@ -102,13 +152,15 @@
</div> </div>
</div> </div>
<!-- Assignment Details -->
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-md-6"> <div class="col-md-6">
<label for="{{ form.max_candidates.id_for_label }}" class="form-label"> <label for="{{ form.max_candidates.id_for_label }}" class="form-label">
{{ form.max_candidates.label }} <span class="text-danger">*</span> {{ form.max_candidates.label }} <span class="text-danger">*</span>
</label> </label>
{{ form.max_candidates }} {# Wrapper Div for styling consistency (Assumes max_candidates is an INPUT field) #}
<div class="kaauh-field-control">
{{ form.max_candidates|attr:'class:form-control' }}
</div>
{% if form.max_candidates.errors %} {% if form.max_candidates.errors %}
<div class="text-danger small mt-1"> <div class="text-danger small mt-1">
{% for error in form.max_candidates.errors %}{{ error }}{% endfor %} {% for error in form.max_candidates.errors %}{{ error }}{% endfor %}
@ -122,7 +174,10 @@
<label for="{{ form.deadline_date.id_for_label }}" class="form-label"> <label for="{{ form.deadline_date.id_for_label }}" class="form-label">
{{ form.deadline_date.label }} <span class="text-danger">*</span> {{ form.deadline_date.label }} <span class="text-danger">*</span>
</label> </label>
{{ form.deadline_date }} {# Wrapper Div for styling consistency (Assumes deadline_date is an INPUT field) #}
<div class="kaauh-field-control">
{{ form.deadline_date|attr:'class:form-control' }}
</div>
{% if form.deadline_date.errors %} {% if form.deadline_date.errors %}
<div class="text-danger small mt-1"> <div class="text-danger small mt-1">
{% for error in form.deadline_date.errors %}{{ error }}{% endfor %} {% for error in form.deadline_date.errors %}{{ error }}{% endfor %}
@ -134,46 +189,15 @@
</div> </div>
</div> </div>
<!-- Status and Settings -->
{% comment %} <div class="row g-3 mb-4">
<div class="col-md-6">
<label for="{{ form.is_active.id_for_label }}" class="form-label">
{{ form.is_active.label }}
</label>
<div class="form-check form-switch">
{{ form.is_active }}
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">
{% trans "Enable this assignment" %}
</label>
</div>
{% if form.is_active.errors %}
<div class="text-danger small mt-1">
{% for error in form.is_active.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="col-md-6">
<label for="{{ form.status.id_for_label }}" class="form-label">
{{ form.status.label }}
</label>
{{ form.status }}
{% if form.status.errors %}
<div class="text-danger small mt-1">
{% for error in form.status.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
{% trans "Current status of this assignment" %}
</small>
</div>
</div> {% endcomment %}
<!-- Admin Notes -->
<div class="mb-4"> <div class="mb-4">
<label for="{{ form.admin_notes.id_for_label }}" class="form-label"> <label for="{{ form.admin_notes.id_for_label }}" class="form-label">
{{ form.admin_notes.label }} {{ form.admin_notes.label }}
</label> </label>
{{ form.admin_notes }} {# Wrapper Div for styling consistency (Assumes admin_notes is a TEXTAREA field) #}
<div class="kaauh-field-control">
{{ form.admin_notes|attr:'class:form-control' }}
</div>
{% if form.admin_notes.errors %} {% if form.admin_notes.errors %}
<div class="text-danger small mt-1"> <div class="text-danger small mt-1">
{% for error in form.admin_notes.errors %}{{ error }}{% endfor %} {% for error in form.admin_notes.errors %}{{ error }}{% endfor %}
@ -184,9 +208,8 @@
</small> </small>
</div> </div>
<!-- Form Actions -->
<div class="d-flex justify-content-between align-items-center pt-3 border-top"> <div class="d-flex justify-content-between align-items-center pt-3 border-top">
<a href="{% url 'agency_assignment_list' %}" class="btn btn-outline-secondary"> <a href="{% url 'agency_assignment_list' %}" class="btn btn-outline-primary-teal">
<i class="fas fa-times me-1"></i> {% trans "Cancel" %} <i class="fas fa-times me-1"></i> {% trans "Cancel" %}
</a> </a>
<button type="submit" class="btn btn-main-action"> <button type="submit" class="btn btn-main-action">
@ -195,28 +218,6 @@
</div> </div>
</form> </form>
</div> </div>
<!-- Help Information -->
{% comment %} <div class="kaauh-card mt-4">
<h5 class="mb-3" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-info-circle me-2"></i>
{% trans "Assignment Information" %}
</h5>
<div class="row">
<div class="col-md-6">
<h6 class="fw-bold text-primary">{% trans "Active Status" %}</h6>
<p class="text-muted small">
{% trans "Only active assignments allow agencies to submit candidates. Expired or cancelled assignments cannot receive new submissions." %}
</p>
</div>
<div class="col-md-6">
<h6 class="fw-bold text-primary">{% trans "Access Links" %}</h6>
<p class="text-muted small">
{% trans "After creating an assignment, you can generate access links for agencies to submit candidates through their portal." %}
</p>
</div>
</div>
</div> {% endcomment %}
</div> </div>
</div> </div>
</div> </div>
@ -225,6 +226,11 @@
{% block customJS %} {% block customJS %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// --- Consistency Check: Ensure Django widgets have the Bootstrap classes ---
// If your form fields are NOT already adding classes via widget attrs in the Django form,
// you MUST add the following utility filter to your project to make this template work:
// `|attr:'class:form-control'`
// Auto-populate agency field when job is selected // Auto-populate agency field when job is selected
const jobSelect = document.getElementById('{{ form.job.id_for_label }}'); const jobSelect = document.getElementById('{{ form.job.id_for_label }}');
const agencySelect = document.getElementById('{{ form.agency.id_for_label }}'); const agencySelect = document.getElementById('{{ form.agency.id_for_label }}');
@ -248,4 +254,4 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -312,7 +312,7 @@
<div class="info-content"> <div class="info-content">
<div class="info-label">{% trans "Website" %}</div> <div class="info-label">{% trans "Website" %}</div>
<div class="info-value"> <div class="info-value">
<a href="{{ agency.website }}" target="_blank" class="text-decoration-none"> <a href="{{ agency.website }}" target="_blank" class="text-decoration-none text-secondary">
{{ agency.website }} {{ agency.website }}
<i class="fas fa-external-link-alt ms-1 small"></i> <i class="fas fa-external-link-alt ms-1 small"></i>
</a> </a>
@ -390,7 +390,7 @@
<i class="fas fa-users me-2"></i> <i class="fas fa-users me-2"></i>
{% trans "Recent Candidates" %} {% trans "Recent Candidates" %}
</h5> </h5>
<a href="{% url 'agency_candidates' agency.slug %}" class="btn btn-outline-primary btn-sm"> <a href="{% url 'agency_candidates' agency.slug %}" class="btn btn-main-action btn-sm">
{% trans "View All Candidates" %} {% trans "View All Candidates" %}
<i class="fas fa-arrow-right ms-1"></i> <i class="fas fa-arrow-right ms-1"></i>
</a> </a>
@ -482,7 +482,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<a href="{% url 'agency_update' agency.slug %}" class="btn btn-outline-primary"> <a href="{% url 'agency_update' agency.slug %}" class="btn btn-main-action">
<i class="fas fa-edit me-2"></i> {% trans "Edit Agency" %} <i class="fas fa-edit me-2"></i> {% trans "Edit Agency" %}
</a> </a>
<a href="{% url 'agency_candidates' agency.slug %}" class="btn btn-outline-info"> <a href="{% url 'agency_candidates' agency.slug %}" class="btn btn-outline-info">

View File

@ -64,7 +64,7 @@
/* Stats Badge */ /* Stats Badge */
.stats-badge { .stats-badge {
background-color: var(--kaauh-info); background-color: var(--kaauh-teal);
color: white; color: white;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 0.375rem; border-radius: 0.375rem;
@ -168,7 +168,7 @@
{% if agency.website %} {% if agency.website %}
<p class="card-text mb-3"> <p class="card-text mb-3">
<i class="fas fa-link text-muted me-2"></i> <i class="fas fa-link text-muted me-2"></i>
<a href="{{ agency.website }}" target="_blank" class="text-decoration-none"> <a href="{{ agency.website }}" target="_blank" class="text-decoration-none text-secondary">
{{ agency.website|truncatechars:30 }} {{ agency.website|truncatechars:30 }}
<i class="fas fa-external-link-alt ms-1 small"></i> <i class="fas fa-external-link-alt ms-1 small"></i>
</a> </a>
@ -179,7 +179,7 @@
<div class="d-flex justify-content-between align-items-center mt-auto"> <div class="d-flex justify-content-between align-items-center mt-auto">
<div> <div>
<a href="{% url 'agency_detail' agency.slug %}" <a href="{% url 'agency_detail' agency.slug %}"
class="btn btn-outline-primary btn-sm me-2"> class="btn btn-main-action btn-sm me-2">
<i class="fas fa-eye me-1"></i> {% trans "View" %} <i class="fas fa-eye me-1"></i> {% trans "View" %}
</a> </a>
<a href="{% url 'agency_update' agency.slug %}" <a href="{% url 'agency_update' agency.slug %}"

View File

@ -2,11 +2,44 @@
{% load static i18n %} {% load static i18n %}
{% block title %}{% trans "Agency Dashboard" %} - ATS{% endblock %} {% block title %}{% trans "Agency Dashboard" %} - ATS{% endblock %}
{% block customCSS %}
<style>
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-success: #28a745;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
.kaauh-card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
</style>
{% endblock%}
{% block content %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div> <div class="px-2 py-2">
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;"> <h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-tachometer-alt me-2"></i> <i class="fas fa-tachometer-alt me-2"></i>
{% trans "Agency Dashboard" %} {% trans "Agency Dashboard" %}
@ -32,9 +65,9 @@
<!-- Overview Statistics --> <!-- Overview Statistics -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-3"> <div class="col-md-3 mb-2">
<div class="kaauh-card shadow-sm h-100"> <div class="kaauh-card shadow-sm h-100">
<div class="card-body text-center"> <div class="card-body text-center px-2 py-2">
<div class="text-primary mb-2"> <div class="text-primary mb-2">
<i class="fas fa-briefcase fa-2x"></i> <i class="fas fa-briefcase fa-2x"></i>
</div> </div>
@ -43,8 +76,8 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3 mb-2">
<div class="kaauh-card shadow-sm h-100"> <div class="kaauh-card shadow-sm h-100 px-2 py-2">
<div class="card-body text-center"> <div class="card-body text-center">
<div class="text-success mb-2"> <div class="text-success mb-2">
<i class="fas fa-check-circle fa-2x"></i> <i class="fas fa-check-circle fa-2x"></i>
@ -54,8 +87,8 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3 mb-2">
<div class="kaauh-card shadow-sm h-100"> <div class="kaauh-card shadow-sm h-100 px-2 py-2">
<div class="card-body text-center"> <div class="card-body text-center">
<div class="text-info mb-2"> <div class="text-info mb-2">
<i class="fas fa-users fa-2x"></i> <i class="fas fa-users fa-2x"></i>
@ -65,8 +98,8 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3 mb-2">
<div class="kaauh-card shadow-sm h-100"> <div class="kaauh-card shadow-sm h-100 px-2 py-2">
<div class="card-body text-center"> <div class="card-body text-center">
<div class="text-warning mb-2"> <div class="text-warning mb-2">
<i class="fas fa-envelope fa-2x"></i> <i class="fas fa-envelope fa-2x"></i>
@ -79,7 +112,7 @@
</div> </div>
<!-- Job Assignments List --> <!-- Job Assignments List -->
<div class="kaauh-card shadow-sm"> <div class="kaauh-card shadow-sm px-3 py-3">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0"> <h5 class="card-title mb-0">
@ -171,7 +204,7 @@
</div> </div>
<div> <div>
<a href="{% url 'agency_portal_assignment_detail' stats.assignment.slug %}" <a href="{% url 'agency_portal_assignment_detail' stats.assignment.slug %}"
class="btn btn-sm btn-outline-primary"> class="btn btn-sm btn-main-action">
<i class="fas fa-eye me-1"></i> {% trans "View Details" %} <i class="fas fa-eye me-1"></i> {% trans "View Details" %}
</a> </a>
{% if stats.unread_messages > 0 %} {% if stats.unread_messages > 0 %}

View File

@ -132,14 +132,7 @@
<!-- Login Body --> <!-- Login Body -->
<div class="login-body"> <div class="login-body">
<!-- Messages --> <!-- Messages -->
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<!-- Login Form --> <!-- Login Form -->
<form method="post" novalidate> <form method="post" novalidate>

View File

@ -654,37 +654,35 @@
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire: " %}{{candidate.time_to_hire|default:100}}&nbsp;days</h5> <h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire: " %}{{candidate.time_to_hire|default:100}}&nbsp;days</h5>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="resume-parsed-section">
{% if candidate.is_resume_parsed %} {% if candidate.is_resume_parsed %}
{% include 'recruitment/candidate_resume_template.html' %} {% include 'recruitment/candidate_resume_template.html' %}
{% else %} {% else %}
<a href="{% url 'candidate_detail' candidate.slug %}" class="text-decoration-none"> {% if candidate.scoring_timeout %}
<div style="display: flex; justify-content: center; align-items: center; height: 100%;"> <div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
<div class="ai-loading-container">
<div class="ai-loading-container"> <i class="fas fa-robot ai-robot-icon"></i>
{# Robot Icon (Requires Font Awesome or similar library) #} <span>Resume is been Scoring...</span>
<i class="fas fa-robot ai-robot-icon"></i> </div>
</div>
{# The Spinner #} {% else %}
<svg class="kaats-spinner" viewBox="0 0 50 50"> <div style="display: flex; justify-content: center; align-items: center; height: 100%;">
<circle cx="25" cy="25" r="20"></circle> <button type="submit" class="btn btn-sm btn-main-action" hx-get="{% url 'candidate_retry_scoring' candidate.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume , Please Wait.. <i class='fa-solid fa-spinner fa-spin'></i>`">
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="4" {% trans "Retry AI Scoring" %}
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle> </button>
</svg>
<span>AI Scoring...</span>
</div> </div>
{% endif %}
</div>
</a>
{% endif %} {% endif %}
</div>
{# STAGE UPDATE MODAL INCLUDED FOR STAFF USERS #}
{% if user.is_staff %} {% if user.is_staff %}

View File

@ -179,7 +179,7 @@
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'export_candidates_csv' job.slug 'exam' %}" <a href="{% url 'export_candidates_csv' job.slug 'exam' %}"
class="btn btn-outline-secondary btn-sm" class="btn btn-outline-secondary"
title="{% trans 'Export exam candidates to CSV' %}"> title="{% trans 'Export exam candidates to CSV' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %} <i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a> </a>
@ -210,7 +210,7 @@
{# Select Input Group #} {# Select Input Group #}
<div> <div>
<label for="update_status" class="form-label small mb-1 fw-bold">{% trans "Move Selected To:" %}</label>
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;"> <select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
<option selected> <option selected>
---------- ----------
@ -226,7 +226,7 @@
{# Button #} {# Button #}
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Update Status" %} <i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button> </button>
</div> </div>

View File

@ -197,13 +197,13 @@
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="button" <button type="button"
class="btn btn-main-action btn-sm" class="btn btn-main-action"
onclick="syncHiredCandidates()" onclick="syncHiredCandidates()"
title="{% trans 'Sync hired candidates to external sources' %}"> title="{% trans 'Sync hired candidates to external sources' %}">
<i class="fas fa-sync me-1"></i> {% trans "Sync to Sources" %} <i class="fas fa-sync me-1"></i> {% trans "Sync to Sources" %}
</button> </button>
<a href="{% url 'export_candidates_csv' job.slug 'hired' %}" <a href="{% url 'export_candidates_csv' job.slug 'hired' %}"
class="btn btn-outline-secondary btn-sm" class="btn btn-outline-secondary"
title="{% trans 'Export hired candidates to CSV' %}"> title="{% trans 'Export hired candidates to CSV' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %} <i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a> </a>

View File

@ -182,7 +182,7 @@
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'export_candidates_csv' job.slug 'interview' %}" <a href="{% url 'export_candidates_csv' job.slug 'interview' %}"
class="btn btn-outline-secondary btn-sm" class="btn btn-outline-secondary"
title="{% trans 'Export interview candidates to CSV' %}"> title="{% trans 'Export interview candidates to CSV' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %} <i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a> </a>
@ -206,6 +206,7 @@
{% csrf_token %} {% csrf_token %}
{# Select Input Group - No label needed for this one, so we just flex the select and button #} {# Select Input Group - No label needed for this one, so we just flex the select and button #}
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;"> <select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
<option selected> <option selected>
---------- ----------
@ -218,7 +219,7 @@
</option> </option>
</select> </select>
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Move" %} <i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button> </button>
</form> </form>
@ -241,7 +242,9 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="table-responsive">
</div>
<div class="table-responsive">
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get"> <form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
{% csrf_token %} {% csrf_token %}
<table class="table candidate-table align-middle"> <table class="table candidate-table align-middle">
@ -420,7 +423,6 @@
{% endif %} {% endif %}
</form> </form>
</div> </div>
</div>
</div> </div>
@ -445,7 +447,7 @@
</div> </div>
<div class="modal fade" id="jobAssignmentModal" tabindex="-1" aria-labelledby="jobAssignmentLabel" aria-hidden="true"> <div class="modal fade" id="jobAssignmentModal" tabindex="-1" aria-labelledby="jobAssignmentLabel" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="jobAssignmentLabel">{% trans "Manage all participants" %}</h5> <h5 class="modal-title" id="jobAssignmentLabel">{% trans "Manage all participants" %}</h5>
@ -455,11 +457,40 @@
<form method="post" action="{% url 'candidate_interview_view' job.slug %}"> <form method="post" action="{% url 'candidate_interview_view' job.slug %}">
{% csrf_token %} {% csrf_token %}
<<<<<<< HEAD
<div class="modal-body table-responsive">
=======
<div class="modal-body"> <div class="modal-body">
>>>>>>> c6fcb276135dc7e87bb0d065a93ff89091ff0207
{{ job.internal_job_id }} {{ job.title}} {{ job.internal_job_id }} {{ job.title}}
<hr> <hr>
<<<<<<< HEAD
<table class="table tab table-bordered mt-3">
<thead>
<th class="col">👥 {% trans "Participants" %}</th>
<th class="col">🧑‍💼 {% trans "Users" %}</th>
</thead>
<tbody>
<tr>
<td>
{{ form.participants.errors }}
{{ form.participants }}
</td>
<td> {{ form.users.errors }}
{{ form.users }}
</td>
</tr>
</table>
=======
<h3>👥 {% trans "Participants" %}</h3> <h3>👥 {% trans "Participants" %}</h3>
{{ form.participants.errors }} {{ form.participants.errors }}
@ -470,6 +501,7 @@
<h3>🧑‍💼 {% trans "Users" %}</h3> <h3>🧑‍💼 {% trans "Users" %}</h3>
{{ form.users.errors }} {{ form.users.errors }}
{{ form.users }} {{ form.users }}
>>>>>>> c6fcb276135dc7e87bb0d065a93ff89091ff0207
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -261,20 +261,20 @@
{% include "includes/_list_view_switcher.html" with list_id="candidate-list" %} {% include "includes/_list_view_switcher.html" with list_id="candidate-list" %}
{# Table View (Default) #} {# Table View (Default) #}
<div class="table-view active"> <div class="table-view">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover mb-0"> <table class="table table-hover align-middle mb-0">
<thead> <thead>
<tr> <tr>
<th scope="col" style="width: 12%;">{% trans "Name" %}</th> <th scope="col" >{% trans "Name" %}</th>
<th scope="col" style="width: 12%;">{% trans "Email" %}</th> <th scope="col">{% trans "Email" %}</th>
<th scope="col" style="width: 8%;">{% trans "Phone" %}</th> {% comment %} <th scope="col" style="width: 8%;">{% trans "Phone" %}</th> {% endcomment %}
<th scope="col" style="width: 12%;">{% trans "Job" %}</th> <th scope="col">{% trans "Job" %}</th>
<th scope="col" style="width: 5%;">{% trans "Major" %}</th> <th scope="col" >{% trans "Major" %}</th>
<th scope="col" style="width: 8%;">{% trans "Stage" %}</th> <th scope="col" >{% trans "Stage" %}</th>
<th scope="col" style="width: 10%;">{% trans "Hiring Source" %}</th> <th scope="col">{% trans "Hiring Source" %}</th>
<th scope="col" style="width: 13%;">{% trans "created At" %}</th> <th scope="col" >{% trans "created At" %}</th>
<th scope="col" style="width: 5%;" class="text-end">{% trans "Actions" %}</th> <th scope="col" class="text-end">{% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -282,7 +282,7 @@
<tr> <tr>
<td class="fw-medium"><a href="{% url 'candidate_detail' candidate.slug %}" class="text-decoration-none link-secondary">{{ candidate.name }}<a></td> <td class="fw-medium"><a href="{% url 'candidate_detail' candidate.slug %}" class="text-decoration-none link-secondary">{{ candidate.name }}<a></td>
<td>{{ candidate.email }}</td> <td>{{ candidate.email }}</td>
<td>{{ candidate.phone }}</td> {% comment %} <td>{{ candidate.phone }}</td> {% endcomment %}
<td> <span class="badge bg-primary"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-decoration-none text-white">{{ candidate.job.title }}</a></span></td> <td> <span class="badge bg-primary"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-decoration-none text-white">{{ candidate.job.title }}</a></span></td>
<td> <td>
{% if candidate.is_resume_parsed %} {% if candidate.is_resume_parsed %}
@ -292,16 +292,15 @@
</span> </span>
{% endif %} {% endif %}
{% else %} {% else %}
<a href="{% url 'candidate_list' %}" class="text-decoration-none"> <a href="{% url 'candidate_list' %}" class="text-decoration-none d-flex align-items-center gap-2">
<div>
<svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;"> <svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;">
<circle cx="25" cy="25" r="20"></circle> <circle cx="25" cy="25" r="20"></circle>
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5" <circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle> style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
</svg> </svg>
<span class="text-teal-primary text-nowrap">{% trans "AI Scoring..." %}</span> {# CRITICAL: Remove the DIV and the text-nowrap class #}
</div> <span class="text-teal-primary">{% trans "AI Scoring..." %}</span>
</a> </a>
{% endif %} {% endif %}
</td> </td>
<td> <td>

View File

@ -181,7 +181,7 @@
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'export_candidates_csv' job.slug 'offer' %}" <a href="{% url 'export_candidates_csv' job.slug 'offer' %}"
class="btn btn-outline-secondary btn-sm" class="btn btn-outline-secondary"
title="{% trans 'Export offer candidates to CSV' %}"> title="{% trans 'Export offer candidates to CSV' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %} <i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a> </a>
@ -219,7 +219,7 @@
{# Button #} {# Button #}
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Move" %} <i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button> </button>
</form> </form>

View File

@ -230,7 +230,7 @@
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'export_candidates_csv' job.slug 'screening' %}" <a href="{% url 'export_candidates_csv' job.slug 'screening' %}"
class="btn btn-outline-secondary btn-sm" class="btn btn-outline-secondary"
title="{% trans 'Export screening candidates to CSV' %}"> title="{% trans 'Export screening candidates to CSV' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %} <i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a> </a>
@ -324,7 +324,7 @@
{# Select Input Group #} {# Select Input Group #}
<div> <div>
<label for="update_status" class="form-label small mb-1 fw-bold">{% trans "Move Selected To:" %}</label>
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;"> <select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
<option selected> <option selected>
---------- ----------
@ -338,7 +338,7 @@
{# Button #} {# Button #}
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Update Status" %} <i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button> </button>
</div> </div>
@ -361,7 +361,7 @@
{% endif %} {% endif %}
</th> </th>
<th scope="col" style="width: 8%;"> <th scope="col" style="width: 8%;">
<i class="fas fa-user me-1"></i> {% trans "Candidate Name" %} <i class="fas fa-user me-1"></i> {% trans "Name" %}
</th> </th>
<th scope="col" style="width: 10%;"> <th scope="col" style="width: 10%;">
<i class="fas fa-phone me-1"></i> {% trans "Contact Info" %} <i class="fas fa-phone me-1"></i> {% trans "Contact Info" %}

View File

@ -223,6 +223,20 @@
</div> </div>
</div> </div>
<div class="card shadow-sm no-hover mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-chart-pie me-2 text-primary"></i>
{% trans "Candidates From Each Sources" %}
</h6>
</div>
<div class="card-body p-4">
<div style="height: 300px;">
<canvas id="candidatesourceschart"></canvas>
</div>
</div>
</div>
</div> </div>
@ -442,6 +456,86 @@
} }
}); });
// Chart for Candidate Categories and Match Scores
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('candidatesourceschart');
if (!ctx) {
console.warn('Candidates sources chart element not found.');
return;
}
const chartCtx = ctx.getContext('2d');
// Safely get job_category_data from Django context
// Using window.jobChartData to avoid template parsing issues
if (categories.length > 0) { // Only render if there's data
const chart = new Chart(chartCtx, {
type: 'doughnut',
data: {
labels: categories,
datasets: [
{
label: 'Number of Candidates',
data: candidates_count_in_each_source,
backgroundColor: [
'rgba(0, 99, 110, 0.7)', // --kaauh-teal
'rgba(23, 162, 184, 0.7)', // Teal shade
'rgba(0, 150, 136, 0.7)', // Teal green
'rgba(0, 188, 212, 0.7)', // Cyan
'rgba(38, 166, 154, 0.7)', // Turquoise
'rgba(77, 182, 172, 0.7)', // Medium teal
// Add more colors if you expect more categories
],
borderColor: [
'rgba(0, 99, 110, 1)',
'rgba(23, 162, 184, 1)',
'rgba(0, 150, 136, 1)',
'rgba(0, 188, 212, 1)',
'rgba(38, 166, 154, 1)',
'rgba(77, 182, 172, 1)',
// Add more colors if you expect more categories
],
borderWidth: 1,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false, // Important for fixed height container
plugins: {
legend: {
position: 'right', // Position legend for doughnut chart
},
title: {
display: false, // Chart title is handled by the card header
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.label || '';
if (label) {
label += ': ';
}
label += context.parsed + ' candidate(s)';
return label;
}
}
}
}
}
});
} else {
// Display a message if no data is available
chartCtx.canvas.parentNode.innerHTML = '<p class="text-center text-muted mt-4">No candidate category data available for this job.</p>';
}
});
</script> </script>
{% endblock %} {% endblock %}