updates
This commit is contained in:
parent
b37af920ba
commit
08774489bc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -679,6 +679,32 @@ class Candidate(Base):
|
||||
|
||||
return future_meetings or today_future_meetings
|
||||
|
||||
@property
|
||||
def check_and_retry_ai_scoring(self):
|
||||
"""
|
||||
Triggers an immediate save ONLY if:
|
||||
1. The resume hasn't been parsed yet.
|
||||
2. At least 5 minutes have passed since the last attempt.
|
||||
Returns True if a save was performed, False otherwise.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
min_delay = timedelta(minutes=5)
|
||||
|
||||
time_since_last_attempt = timezone.now() - self.created_at
|
||||
|
||||
if not self.is_resume_parsed and time_since_last_attempt >= min_delay:
|
||||
|
||||
# 1. Update the retry timestamp
|
||||
self.last_retry_attempt = timezone.now()
|
||||
|
||||
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# @property
|
||||
# def time_to_hire(self):
|
||||
# time_to_hire=self.hired_date-self.created_at
|
||||
|
||||
@ -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
|
||||
path('api/notifications/stream/', views.notification_stream, name='notification_stream'),
|
||||
# # 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
|
||||
|
||||
@ -2582,314 +2582,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"""
|
||||
from django.http import StreamingHttpResponse
|
||||
import json
|
||||
import time
|
||||
from .signals import SSE_NOTIFICATION_CACHE
|
||||
# @login_required
|
||||
# def notification_stream(request):
|
||||
# """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
|
||||
# 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
|
||||
# # 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']
|
||||
# # 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]
|
||||
# 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']
|
||||
# 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')
|
||||
# # 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 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'
|
||||
# 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})
|
||||
}
|
||||
# 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"
|
||||
# # Send SSE event
|
||||
# yield f"event: new_notification\n"
|
||||
# yield f"data: {json.dumps(notification_data)}\n\n"
|
||||
|
||||
last_notification_id = notification.id
|
||||
# last_notification_id = notification.id
|
||||
|
||||
# Update count after sending new notifications
|
||||
unread_count = Notification.objects.filter(
|
||||
recipient=request.user,
|
||||
status=Notification.Status.PENDING
|
||||
).count()
|
||||
# # 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"
|
||||
# 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"
|
||||
# # 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
|
||||
# # 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
|
||||
# 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'
|
||||
)
|
||||
# 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'
|
||||
# # 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)
|
||||
# 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
|
||||
@ -2976,7 +2976,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
|
||||
|
||||
|
||||
@ -222,12 +222,11 @@ 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):
|
||||
if request.method == 'POST':
|
||||
candidate = get_object_or_404(models.Candidate, slug=slug)
|
||||
candidate.save()
|
||||
return redirect('candidate_detail', slug=candidate.slug)
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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">
|
||||
© {% 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>
|
||||
@ -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;
|
||||
@ -701,8 +701,8 @@
|
||||
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 %}
|
||||
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 %}
|
||||
@ -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">
|
||||
|
||||
@ -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 %}"
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -654,7 +654,7 @@
|
||||
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire: " %}{{candidate.time_to_hire|default:100}} days</h5>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
@ -666,7 +666,7 @@
|
||||
{% 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 style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
||||
|
||||
<div class="ai-loading-container">
|
||||
{# Robot Icon (Requires Font Awesome or similar library) #}
|
||||
@ -681,9 +681,23 @@
|
||||
|
||||
<span>AI Scoring...</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</a>
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
||||
{% if candidate.check_and_retry_ai_scoring %}
|
||||
<form method="post" action="{% url 'candidate_retry_scoring' slug=candidate.slug %}" class="d-inline">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Use your established teal button style #}
|
||||
<button type="submit" class="btn btn-sm btn-main-action">
|
||||
<i class="fas fa-redo me-1"></i>
|
||||
{% trans "Retry AI Scoring" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -240,7 +241,9 @@
|
||||
</button>
|
||||
</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">
|
||||
@ -411,7 +414,6 @@
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user