ui fixes #27

Merged
ismail merged 4 commits from frontend into main 2025-11-02 13:42:00 +03:00
26 changed files with 803 additions and 485 deletions

View File

@ -678,12 +678,10 @@ class Candidate(Base):
).exists()
return future_meetings or today_future_meetings
# @property
# def time_to_hire(self):
# time_to_hire=self.hired_date-self.created_at
# return time_to_hire
@property
def scoring_timeout(self):
return timezone.now() <= (self.created_at + timezone.timedelta(minutes=5))
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>/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>/retry-scoring/', views_frontend.retry_scoring_view, name='candidate_retry_scoring'),
# Training URLs
path('training/', views_frontend.TrainingListView.as_view(), name='training_list'),
@ -201,23 +202,23 @@ urlpatterns = [
# API URLs for candidate management
path('api/candidate/<int:candidate_id>/', views.api_candidate_detail, name='api_candidate_detail'),
# Admin Notification API
path('api/admin/notification-count/', views.api_notification_count, name='admin_notification_count'),
# # Admin Notification API
# path('api/admin/notification-count/', views.api_notification_count, name='admin_notification_count'),
# Agency Notification API
path('api/agency/notification-count/', views.api_notification_count, name='api_agency_notification_count'),
# # Agency Notification API
# 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'),
# Notification URLs
path('notifications/', views.notification_list, name='notification_list'),
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-unread/', views.notification_mark_unread, name='notification_mark_unread'),
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('api/notification-count/', views.api_notification_count, name='api_notification_count'),
# # Notification URLs
# path('notifications/', views.notification_list, name='notification_list'),
# 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-unread/', views.notification_mark_unread, name='notification_mark_unread'),
# 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('api/notification-count/', views.api_notification_count, name='api_notification_count'),
#participants urls

View File

@ -2583,201 +2583,314 @@ def agency_delete(request, slug):
# Notification Views
@login_required
def notification_list(request):
"""List all notifications for the current user"""
# Get filter parameters
status_filter = request.GET.get('status', '')
type_filter = request.GET.get('type', '')
# @login_required
# def notification_list(request):
# """List all notifications for the current user"""
# # Get filter parameters
# status_filter = request.GET.get('status', '')
# type_filter = request.GET.get('type', '')
# Base queryset
notifications = Notification.objects.filter(recipient=request.user).order_by('-created_at')
# # Base queryset
# notifications = Notification.objects.filter(recipient=request.user).order_by('-created_at')
# Apply filters
if status_filter:
if status_filter == 'unread':
notifications = notifications.filter(status=Notification.Status.PENDING)
elif status_filter == 'read':
notifications = notifications.filter(status=Notification.Status.READ)
elif status_filter == 'sent':
notifications = notifications.filter(status=Notification.Status.SENT)
# # Apply filters
# if status_filter:
# if status_filter == 'unread':
# notifications = notifications.filter(status=Notification.Status.PENDING)
# elif status_filter == 'read':
# notifications = notifications.filter(status=Notification.Status.READ)
# elif status_filter == 'sent':
# notifications = notifications.filter(status=Notification.Status.SENT)
if type_filter:
if type_filter == 'in_app':
notifications = notifications.filter(notification_type=Notification.NotificationType.IN_APP)
elif type_filter == 'email':
notifications = notifications.filter(notification_type=Notification.NotificationType.EMAIL)
# if type_filter:
# if type_filter == 'in_app':
# notifications = notifications.filter(notification_type=Notification.NotificationType.IN_APP)
# elif type_filter == 'email':
# notifications = notifications.filter(notification_type=Notification.NotificationType.EMAIL)
# Pagination
paginator = Paginator(notifications, 20) # Show 20 notifications per page
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
# # Pagination
# paginator = Paginator(notifications, 20) # Show 20 notifications per page
# page_number = request.GET.get('page')
# page_obj = paginator.get_page(page_number)
# Statistics
total_notifications = notifications.count()
unread_notifications = notifications.filter(status=Notification.Status.PENDING).count()
email_notifications = notifications.filter(notification_type=Notification.NotificationType.EMAIL).count()
# # Statistics
# total_notifications = notifications.count()
# unread_notifications = notifications.filter(status=Notification.Status.PENDING).count()
# email_notifications = notifications.filter(notification_type=Notification.NotificationType.EMAIL).count()
context = {
'page_obj': page_obj,
'total_notifications': total_notifications,
'unread_notifications': unread_notifications,
'email_notifications': email_notifications,
'status_filter': status_filter,
'type_filter': type_filter,
}
return render(request, 'recruitment/notification_list.html', context)
# context = {
# 'page_obj': page_obj,
# 'total_notifications': total_notifications,
# 'unread_notifications': unread_notifications,
# 'email_notifications': email_notifications,
# 'status_filter': status_filter,
# 'type_filter': type_filter,
# }
# return render(request, 'recruitment/notification_list.html', context)
@login_required
def notification_detail(request, notification_id):
"""View details of a specific notification"""
notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
# @login_required
# def notification_detail(request, notification_id):
# """View details of a specific notification"""
# notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
# Mark as read if it was pending
if notification.status == Notification.Status.PENDING:
notification.status = Notification.Status.READ
notification.save(update_fields=['status'])
# # Mark as read if it was pending
# if notification.status == Notification.Status.PENDING:
# notification.status = Notification.Status.READ
# notification.save(update_fields=['status'])
context = {
'notification': notification,
}
return render(request, 'recruitment/notification_detail.html', context)
# context = {
# 'notification': notification,
# }
# return render(request, 'recruitment/notification_detail.html', context)
@login_required
def notification_mark_read(request, notification_id):
"""Mark a notification as read"""
notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
# @login_required
# def notification_mark_read(request, notification_id):
# """Mark a notification as read"""
# notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
if notification.status == Notification.Status.PENDING:
notification.status = Notification.Status.READ
notification.save(update_fields=['status'])
# if notification.status == Notification.Status.PENDING:
# notification.status = Notification.Status.READ
# notification.save(update_fields=['status'])
if 'HX-Request' in request.headers:
return HttpResponse(status=200) # HTMX success response
# if 'HX-Request' in request.headers:
# return HttpResponse(status=200) # HTMX success response
return redirect('notification_list')
# return redirect('notification_list')
@login_required
def notification_mark_unread(request, notification_id):
"""Mark a notification as unread"""
notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
# @login_required
# def notification_mark_unread(request, notification_id):
# """Mark a notification as unread"""
# notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
if notification.status == Notification.Status.READ:
notification.status = Notification.Status.PENDING
notification.save(update_fields=['status'])
# if notification.status == Notification.Status.READ:
# notification.status = Notification.Status.PENDING
# notification.save(update_fields=['status'])
if 'HX-Request' in request.headers:
return HttpResponse(status=200) # HTMX success response
# if 'HX-Request' in request.headers:
# return HttpResponse(status=200) # HTMX success response
return redirect('notification_list')
# return redirect('notification_list')
@login_required
def notification_delete(request, notification_id):
"""Delete a notification"""
notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
# @login_required
# def notification_delete(request, notification_id):
# """Delete a notification"""
# notification = get_object_or_404(Notification, id=notification_id, recipient=request.user)
if request.method == 'POST':
notification.delete()
messages.success(request, 'Notification deleted successfully!')
return redirect('notification_list')
# if request.method == 'POST':
# notification.delete()
# messages.success(request, 'Notification deleted successfully!')
# return redirect('notification_list')
# For GET requests, show confirmation page
context = {
'notification': notification,
'title': 'Delete Notification',
'message': f'Are you sure you want to delete this notification?',
'cancel_url': reverse('notification_detail', kwargs={'notification_id': notification.id}),
}
return render(request, 'recruitment/notification_confirm_delete.html', context)
# # For GET requests, show confirmation page
# context = {
# 'notification': notification,
# 'title': 'Delete Notification',
# 'message': f'Are you sure you want to delete this notification?',
# 'cancel_url': reverse('notification_detail', kwargs={'notification_id': notification.id}),
# }
# return render(request, 'recruitment/notification_confirm_delete.html', context)
@login_required
def notification_mark_all_read(request):
"""Mark all notifications as read for the current user"""
if request.method == 'POST':
Notification.objects.filter(
recipient=request.user,
status=Notification.Status.PENDING
).update(status=Notification.Status.READ)
# @login_required
# def notification_mark_all_read(request):
# """Mark all notifications as read for the current user"""
# if request.method == 'POST':
# Notification.objects.filter(
# recipient=request.user,
# status=Notification.Status.PENDING
# ).update(status=Notification.Status.READ)
messages.success(request, 'All notifications marked as read!')
return redirect('notification_list')
# messages.success(request, 'All notifications marked as read!')
# return redirect('notification_list')
# For GET requests, show confirmation page
unread_count = Notification.objects.filter(
recipient=request.user,
status=Notification.Status.PENDING
).count()
# # For GET requests, show confirmation page
# unread_count = Notification.objects.filter(
# recipient=request.user,
# status=Notification.Status.PENDING
# ).count()
context = {
'unread_count': unread_count,
'title': 'Mark All as Read',
'message': f'Are you sure you want to mark all {unread_count} notifications as read?',
'cancel_url': reverse('notification_list'),
}
return render(request, 'recruitment/notification_confirm_all_read.html', context)
# context = {
# 'unread_count': unread_count,
# 'title': 'Mark All as Read',
# 'message': f'Are you sure you want to mark all {unread_count} notifications as read?',
# 'cancel_url': reverse('notification_list'),
# }
# return render(request, 'recruitment/notification_confirm_all_read.html', context)
@login_required
def api_notification_count(request):
"""API endpoint to get unread notification count and recent notifications"""
# Get unread notifications
unread_notifications = Notification.objects.filter(
recipient=request.user,
status=Notification.Status.PENDING
).order_by('-created_at')
# @login_required
# def api_notification_count(request):
# """API endpoint to get unread notification count and recent notifications"""
# # Get unread notifications
# unread_notifications = Notification.objects.filter(
# recipient=request.user,
# status=Notification.Status.PENDING
# ).order_by('-created_at')
# Get recent notifications (last 5)
recent_notifications = Notification.objects.filter(
recipient=request.user
).order_by('-created_at')[:5]
# # Get recent notifications (last 5)
# recent_notifications = Notification.objects.filter(
# recipient=request.user
# ).order_by('-created_at')[:5]
# Prepare recent notifications data
recent_data = []
for notification in recent_notifications:
time_ago = ''
if notification.created_at:
from datetime import datetime, timezone
now = timezone.now()
diff = now - notification.created_at
# # Prepare recent notifications data
# recent_data = []
# for notification in recent_notifications:
# time_ago = ''
# if notification.created_at:
# from datetime import datetime, timezone
# 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'
# 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'
recent_data.append({
'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})
})
# recent_data.append({
# '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})
# })
return JsonResponse({
'count': unread_notifications.count(),
'recent_notifications': recent_data
})
# return JsonResponse({
# 'count': unread_notifications.count(),
# 'recent_notifications': recent_data
# })
# @login_required
# def notification_stream(request):
# """SSE endpoint for real-time notifications - DISABLED"""
# # This function has been disabled due to implementation issues
# # TODO: Fix SSE implementation or replace with alternative real-time solution
# from django.http import HttpResponse
# return HttpResponse("SSE endpoint temporarily disabled", status=503)
# """SSE endpoint for real-time notifications"""
# from django.http import StreamingHttpResponse
# import json
# import time
# 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
@ -2864,7 +2977,7 @@ def agency_assignment_create(request,slug=None):
try:
from django.forms import HiddenInput
form.initial['agency'] = agency
form.fields['agency'].widget = HiddenInput()
# form.fields['agency'].widget = HiddenInput()
except HiringAgency.DoesNotExist:
pass

View File

@ -21,8 +21,15 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView
# JobForm removed - using JobPostingForm instead
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 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 (
DatastarResponse,
@ -215,12 +222,18 @@ class CandidateDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
slug_url_kwarg = 'slug'
# def job_detail(request, slug):
# job = get_object_or_404(models.JobPosting, slug=slug, status='Published')
# form = forms.CandidateForm()
# return render(request, 'jobs/job_detail.html', {'job': job, 'form': form})
def retry_scoring_view(request,slug):
from django_q.tasks import async_task
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_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
# Example: from . import models
@ -494,11 +500,12 @@ def dashboard_view(request):
# A. Pipeline Funnel (Scoped)
stage_counts = candidate_queryset.values('stage').annotate(count=Count('stage'))
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 = [
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) ---
@ -507,6 +514,15 @@ def dashboard_view(request):
rotation_degrees = rotation_percent * 180
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 ---
@ -555,6 +571,10 @@ def dashboard_view(request):
'jobs': all_jobs_queryset,
'current_job_id': selected_job_pk,
'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)

View File

@ -9,7 +9,7 @@
<meta name="description" content="{% trans 'King Abdullah Academic University Hospital - Agency Portal' %}">
<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' %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
{% else %}
@ -24,91 +24,94 @@
</head>
<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="d-flex justify-content-between align-items-center gap-2 max-width-1600">
<div class="logo-container d-flex gap-2">
</div>
<div class="clogo-container d-flex gap-2">
</div>
<div class="logo-container d-flex gap-2 align-items-center">
<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-group d-flex gap-3 align-items-center">
<img src="{% static 'image/vision.svg' %}" alt="{% trans 'Saudi Vision 2030' %}" loading="lazy" style="height: 35px; object-fit: contain;">
<div class="kaauh-logo-container d-flex flex-column flex-md-row align-items-center gap-2 me-0">
<div class="hospital-text text-center text-md-start me-0">
<div class="ar text-xs">جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية</div>
<div class="ar text-xs">ومستشفى الملك عبدالله بن عبدالرحمن التخصصي</div>
<div class="en text-xs">Princess Nourah bint Abdulrahman University</div>
<div class="en text-xs">King Abdullah bin Abdulaziz University Hospital</div>
</div>
<div class="hospital-info d-flex gap-2 align-items-center">
<div class="hospital-text text-center text-md-end">
<div class="small fw-semibold" style="color: #004a53;">
{% if LANGUAGE_CODE == 'ar' %}
جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية
<br>
ومستشفى الملك عبدالله بن عبدالعزيز التخصصي
{% else %}
Princess Nourah bint Abdulrahman University
<br>
King Abdullah bin Abdulaziz University Hospital
{% endif %}
</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> {% 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 -->
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
<div class="container-fluid max-width-1600">
<!-- Agency Portal Brand -->
<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>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#agencyNavbar"
aria-controls="agencyNavbar" aria-expanded="false" aria-label="{% trans 'Toggle navigation' %}">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Mobile Toggler -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#agencyNavbar"
aria-controls="agencyNavbar" aria-expanded="false" aria-label="{% trans 'Toggle navigation' %}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="agencyNavbar">
<div class="navbar-nav ms-auto">
{# NAVIGATION LINKS (Add your portal links here if needed) #}
<!-- Agency Controls -->
<div class="collapse navbar-collapse" id="agencyNavbar">
<div class="navbar-nav ms-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown"
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 dropdown">
<a class="language-toggle-btn dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
<i class="fas fa-globe"></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="{% 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>
<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>
</nav>
</nav>
</div>
<main class="container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
{# Messages Block (Correct) #}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show mt-2" role="alert">
@ -121,10 +124,11 @@
{% endblock %}
</main>
{# Footer (Correct) #}
<footer class="mt-auto">
<div class="footer-bottom py-3 small text-muted" style="background-color: #00363a;">
<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">
&copy; {% now "Y" %} {% trans "King Abdullah Academic University Hospital (KAAUH)." %}
{% 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/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>
{# JavaScript (Left unchanged as it was mostly correct) #}
<script>
document.addEventListener('DOMContentLoaded', () => {
// Navbar collapse auto-close on link click (Mobile UX)
@ -200,4 +206,4 @@
{% block customJS %}{% endblock %}
</body>
</html>
</html>

View File

@ -100,7 +100,7 @@
<ul class="navbar-nav ms-2 ms-lg-4">
<!-- 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">
<a class="nav-link position-relative" href="#" role="button" id="notificationDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-bell"></i>
@ -121,7 +121,7 @@
</li>
</ul>
</li>
{% endif %}
{% endif %} {% endcomment %}
<li class="nav-item dropdown">
<button
@ -390,7 +390,7 @@
</script>
<!-- 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>
// SSE Notification System
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>';
});
}
</script>
{% endif %}
</script> {% endcomment %}
{% comment %} {% endif %} {% endcomment %}
{% block customJS %}{% endblock %}
</body>
</html>

View File

@ -1,6 +1,6 @@
{% load crispy_forms_tags %}
<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-header">
<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>
{% 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>
<!-- Candidates Card -->
@ -277,68 +331,7 @@
</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 -->
<div class="kaauh-card p-4">
<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() {
// Set minimum datetime for new deadline
const deadlineInput = document.getElementById('new_deadline');

View File

@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% load static i18n %}
{% load static i18n widget_tweaks %}
{% block title %}{{ title }} - ATS{% endblock %}
@ -8,6 +8,7 @@
/* KAAT-S UI Variables */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-light: #e0f7f9; /* Added for contrast */
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
@ -16,19 +17,27 @@
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
body {
background-color: #f8f9fa; /* Light background for better contrast */
}
.kaauh-card {
padding: 2.5rem; /* Increased padding */
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Main Action Button (Teal Fill) */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
padding: 0.6rem 1.25rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
}
.btn-main-action:hover {
@ -36,7 +45,21 @@
border-color: var(--kaauh-teal-dark);
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 {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
@ -46,6 +69,30 @@
font-weight: 600;
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>
{% endblock %}
@ -53,7 +100,6 @@
<div class="container-fluid py-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<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" %}
</p>
</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" %}
</a>
</div>
<!-- Form Card -->
<div class="kaauh-card">
<form method="post" novalidate>
{% csrf_token %}
<!-- Agency and Job Selection -->
{{ form.agency }}
<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">
{{ form.agency.label }} <span class="text-danger">*</span>
</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 %}
<div class="text-danger small mt-1">
{% for error in form.agency.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div> {% endcomment %}
</div>
<div class="col-md-6">
<label for="{{ form.job.id_for_label }}" class="form-label">
{{ form.job.label }} <span class="text-danger">*</span>
</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 %}
<div class="text-danger small mt-1">
{% for error in form.job.errors %}{{ error }}{% endfor %}
@ -102,13 +152,15 @@
</div>
</div>
<!-- Assignment Details -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label for="{{ form.max_candidates.id_for_label }}" class="form-label">
{{ form.max_candidates.label }} <span class="text-danger">*</span>
</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 %}
<div class="text-danger small mt-1">
{% for error in form.max_candidates.errors %}{{ error }}{% endfor %}
@ -122,7 +174,10 @@
<label for="{{ form.deadline_date.id_for_label }}" class="form-label">
{{ form.deadline_date.label }} <span class="text-danger">*</span>
</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 %}
<div class="text-danger small mt-1">
{% for error in form.deadline_date.errors %}{{ error }}{% endfor %}
@ -134,46 +189,15 @@
</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">
<label for="{{ form.admin_notes.id_for_label }}" class="form-label">
{{ form.admin_notes.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 %}
<div class="text-danger small mt-1">
{% for error in form.admin_notes.errors %}{{ error }}{% endfor %}
@ -184,9 +208,8 @@
</small>
</div>
<!-- Form Actions -->
<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" %}
</a>
<button type="submit" class="btn btn-main-action">
@ -195,28 +218,6 @@
</div>
</form>
</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>
@ -225,6 +226,11 @@
{% block customJS %}
<script>
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
const jobSelect = document.getElementById('{{ form.job.id_for_label }}');
const agencySelect = document.getElementById('{{ form.agency.id_for_label }}');
@ -248,4 +254,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
</script>
{% endblock %}
{% endblock %}

View File

@ -312,7 +312,7 @@
<div class="info-content">
<div class="info-label">{% trans "Website" %}</div>
<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 }}
<i class="fas fa-external-link-alt ms-1 small"></i>
</a>
@ -390,7 +390,7 @@
<i class="fas fa-users me-2"></i>
{% trans "Recent Candidates" %}
</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" %}
<i class="fas fa-arrow-right ms-1"></i>
</a>
@ -482,7 +482,7 @@
</div>
<div class="card-body">
<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" %}
</a>
<a href="{% url 'agency_candidates' agency.slug %}" class="btn btn-outline-info">

View File

@ -64,7 +64,7 @@
/* Stats Badge */
.stats-badge {
background-color: var(--kaauh-info);
background-color: var(--kaauh-teal);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
@ -168,7 +168,7 @@
{% if agency.website %}
<p class="card-text mb-3">
<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 }}
<i class="fas fa-external-link-alt ms-1 small"></i>
</a>
@ -179,7 +179,7 @@
<div class="d-flex justify-content-between align-items-center mt-auto">
<div>
<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" %}
</a>
<a href="{% url 'agency_update' agency.slug %}"

View File

@ -2,11 +2,44 @@
{% load static i18n %}
{% 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 %}
<div class="container-fluid py-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;">
<i class="fas fa-tachometer-alt me-2"></i>
{% trans "Agency Dashboard" %}
@ -32,9 +65,9 @@
<!-- Overview Statistics -->
<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="card-body text-center">
<div class="card-body text-center px-2 py-2">
<div class="text-primary mb-2">
<i class="fas fa-briefcase fa-2x"></i>
</div>
@ -43,8 +76,8 @@
</div>
</div>
</div>
<div class="col-md-3">
<div class="kaauh-card shadow-sm h-100">
<div class="col-md-3 mb-2">
<div class="kaauh-card shadow-sm h-100 px-2 py-2">
<div class="card-body text-center">
<div class="text-success mb-2">
<i class="fas fa-check-circle fa-2x"></i>
@ -54,8 +87,8 @@
</div>
</div>
</div>
<div class="col-md-3">
<div class="kaauh-card shadow-sm h-100">
<div class="col-md-3 mb-2">
<div class="kaauh-card shadow-sm h-100 px-2 py-2">
<div class="card-body text-center">
<div class="text-info mb-2">
<i class="fas fa-users fa-2x"></i>
@ -65,8 +98,8 @@
</div>
</div>
</div>
<div class="col-md-3">
<div class="kaauh-card shadow-sm h-100">
<div class="col-md-3 mb-2">
<div class="kaauh-card shadow-sm h-100 px-2 py-2">
<div class="card-body text-center">
<div class="text-warning mb-2">
<i class="fas fa-envelope fa-2x"></i>
@ -79,7 +112,7 @@
</div>
<!-- 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="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">
@ -171,7 +204,7 @@
</div>
<div>
<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" %}
</a>
{% if stats.unread_messages > 0 %}

View File

@ -132,14 +132,7 @@
<!-- Login Body -->
<div class="login-body">
<!-- 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 -->
<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>
</div>
</div>
</div>
</div>
<div class="resume-parsed-section">
{% if candidate.is_resume_parsed %}
{% include 'recruitment/candidate_resume_template.html' %}
{% else %}
<a href="{% url 'candidate_detail' candidate.slug %}" class="text-decoration-none">
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
<div class="ai-loading-container">
{# Robot Icon (Requires Font Awesome or similar library) #}
<i class="fas fa-robot ai-robot-icon"></i>
{# The Spinner #}
<svg class="kaats-spinner" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20"></circle>
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="4"
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
</svg>
<span>AI Scoring...</span>
{% if candidate.scoring_timeout %}
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
<div class="ai-loading-container">
<i class="fas fa-robot ai-robot-icon"></i>
<span>Resume is been Scoring...</span>
</div>
</div>
{% else %}
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
<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>`">
{% trans "Retry AI Scoring" %}
</button>
</div>
</div>
</a>
{% endif %}
{% endif %}
</div>
{# STAGE UPDATE MODAL INCLUDED FOR STAFF USERS #}
{% if user.is_staff %}

View File

@ -179,7 +179,7 @@
</div>
<div class="d-flex gap-2">
<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' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a>
@ -210,7 +210,7 @@
{# Select Input Group #}
<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;">
<option selected>
----------
@ -226,7 +226,7 @@
{# Button #}
<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>
</div>

View File

@ -197,13 +197,13 @@
</div>
<div class="d-flex gap-2">
<button type="button"
class="btn btn-main-action btn-sm"
class="btn btn-main-action"
onclick="syncHiredCandidates()"
title="{% trans 'Sync hired candidates to external sources' %}">
<i class="fas fa-sync me-1"></i> {% trans "Sync to Sources" %}
</button>
<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' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a>

View File

@ -182,7 +182,7 @@
</div>
<div class="d-flex gap-2">
<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' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a>
@ -206,6 +206,7 @@
{% csrf_token %}
{# 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;">
<option selected>
----------
@ -218,7 +219,7 @@
</option>
</select>
<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>
</form>
@ -241,7 +242,9 @@
</div>
</div>
{% endif %}
<div class="table-responsive">
</div>
<div class="table-responsive">
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
{% csrf_token %}
<table class="table candidate-table align-middle">
@ -420,7 +423,6 @@
{% endif %}
</form>
</div>
</div>
</div>
@ -445,7 +447,7 @@
</div>
<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-header">
<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 %}">
{% csrf_token %}
<<<<<<< HEAD
<div class="modal-body table-responsive">
=======
<div class="modal-body">
>>>>>>> c6fcb276135dc7e87bb0d065a93ff89091ff0207
{{ job.internal_job_id }} {{ job.title}}
<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>
{{ form.participants.errors }}
@ -470,6 +501,7 @@
<h3>🧑‍💼 {% trans "Users" %}</h3>
{{ form.users.errors }}
{{ form.users }}
>>>>>>> c6fcb276135dc7e87bb0d065a93ff89091ff0207
</div>
<div class="modal-footer">

View File

@ -261,20 +261,20 @@
{% include "includes/_list_view_switcher.html" with list_id="candidate-list" %}
{# Table View (Default) #}
<div class="table-view active">
<div class="table-view">
<div class="table-responsive">
<table class="table table-hover mb-0">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th scope="col" style="width: 12%;">{% trans "Name" %}</th>
<th scope="col" style="width: 12%;">{% trans "Email" %}</th>
<th scope="col" style="width: 8%;">{% trans "Phone" %}</th>
<th scope="col" style="width: 12%;">{% trans "Job" %}</th>
<th scope="col" style="width: 5%;">{% trans "Major" %}</th>
<th scope="col" style="width: 8%;">{% trans "Stage" %}</th>
<th scope="col" style="width: 10%;">{% trans "Hiring Source" %}</th>
<th scope="col" style="width: 13%;">{% trans "created At" %}</th>
<th scope="col" style="width: 5%;" class="text-end">{% trans "Actions" %}</th>
<th scope="col" >{% trans "Name" %}</th>
<th scope="col">{% trans "Email" %}</th>
{% comment %} <th scope="col" style="width: 8%;">{% trans "Phone" %}</th> {% endcomment %}
<th scope="col">{% trans "Job" %}</th>
<th scope="col" >{% trans "Major" %}</th>
<th scope="col" >{% trans "Stage" %}</th>
<th scope="col">{% trans "Hiring Source" %}</th>
<th scope="col" >{% trans "created At" %}</th>
<th scope="col" class="text-end">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
@ -282,7 +282,7 @@
<tr>
<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.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>
{% if candidate.is_resume_parsed %}
@ -292,16 +292,15 @@
</span>
{% endif %}
{% else %}
<a href="{% url 'candidate_list' %}" class="text-decoration-none">
<div>
<a href="{% url 'candidate_list' %}" class="text-decoration-none d-flex align-items-center gap-2">
<svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;">
<circle cx="25" cy="25" r="20"></circle>
<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>
</svg>
<span class="text-teal-primary text-nowrap">{% trans "AI Scoring..." %}</span>
</div>
</a>
{# CRITICAL: Remove the DIV and the text-nowrap class #}
<span class="text-teal-primary">{% trans "AI Scoring..." %}</span>
</a>
{% endif %}
</td>
<td>

View File

@ -181,7 +181,7 @@
</div>
<div class="d-flex gap-2">
<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' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a>
@ -219,7 +219,7 @@
{# Button #}
<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>
</form>

View File

@ -230,7 +230,7 @@
</div>
<div class="d-flex gap-2">
<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' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a>
@ -324,7 +324,7 @@
{# Select Input Group #}
<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;">
<option selected>
----------
@ -338,7 +338,7 @@
{# Button #}
<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>
</div>
@ -361,7 +361,7 @@
{% endif %}
</th>
<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 scope="col" style="width: 10%;">
<i class="fas fa-phone me-1"></i> {% trans "Contact Info" %}

View File

@ -223,6 +223,20 @@
</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>
@ -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>
{% endblock %}