diff --git a/NorahUniversity/__pycache__/settings.cpython-312.pyc b/NorahUniversity/__pycache__/settings.cpython-312.pyc index ef70d24..48c95df 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-312.pyc and b/NorahUniversity/__pycache__/settings.cpython-312.pyc differ diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index 098b384..7e98f6b 100644 Binary files a/recruitment/__pycache__/models.cpython-312.pyc and b/recruitment/__pycache__/models.cpython-312.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index 05d0cff..e468e4d 100644 Binary files a/recruitment/__pycache__/urls.cpython-312.pyc and b/recruitment/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 1b785c1..9e9649a 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views_frontend.cpython-312.pyc b/recruitment/__pycache__/views_frontend.cpython-312.pyc index a801254..a19c31f 100644 Binary files a/recruitment/__pycache__/views_frontend.cpython-312.pyc and b/recruitment/__pycache__/views_frontend.cpython-312.pyc differ diff --git a/recruitment/models.py b/recruitment/models.py index 1c7676e..802b380 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -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): diff --git a/recruitment/urls.py b/recruitment/urls.py index d15e4e4..cc4bd99 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -34,6 +34,7 @@ urlpatterns = [ path('candidate//view/', views_frontend.candidate_detail, name='candidate_detail'), path('candidate//resume-template/', views_frontend.candidate_resume_template_view, name='candidate_resume_template'), path('candidate//update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'), + path('candidate//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//', 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//', views.notification_detail, name='notification_detail'), - path('notifications//mark-read/', views.notification_mark_read, name='notification_mark_read'), - path('notifications//mark-unread/', views.notification_mark_unread, name='notification_mark_unread'), - path('notifications//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//', views.notification_detail, name='notification_detail'), + # path('notifications//mark-read/', views.notification_mark_read, name='notification_mark_read'), + # path('notifications//mark-unread/', views.notification_mark_unread, name='notification_mark_unread'), + # path('notifications//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 diff --git a/recruitment/views.py b/recruitment/views.py index fdae077..85a5b94 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -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 diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index f0b25cc..729749e 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -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) diff --git a/templates/agency_base.html b/templates/agency_base.html index e5f2451..03b71e3 100644 --- a/templates/agency_base.html +++ b/templates/agency_base.html @@ -9,7 +9,7 @@ {% block title %}{% trans 'KAAUH Agency Portal' %}{% endblock %} - {% comment %} Load correct Bootstrap CSS file for RTL/LTR {% endcomment %} + {# Load correct Bootstrap CSS file for RTL/LTR #} {% if LANGUAGE_CODE == 'ar' %} {% else %} @@ -24,91 +24,94 @@ - {% comment %}
+
-
-
-
-
-
-
+
+
{% trans 'Saudi Vision 2030' %} - -
-
-
جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية
-
ومستشفى الملك عبدالله بن عبدالرحمن التخصصي
-
Princess Nourah bint Abdulrahman University
-
King Abdullah bin Abdulaziz University Hospital
+
+
+
+
+ {% if LANGUAGE_CODE == 'ar' %} + جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية +
+ ومستشفى الملك عبدالله بن عبدالعزيز التخصصي + {% else %} + Princess Nourah bint Abdulrahman University +
+ King Abdullah bin Abdulaziz University Hospital + {% endif %}
- KAAUH Logo + KAAUH Logo
-
{% endcomment %} +
+ + {# Using inline style for nav background color - replace with a dedicated CSS class (e.g., .bg-kaauh-nav) if defined in main.css #} +
+ +
+ {# Messages Block (Correct) #} {% if messages %} {% for message in messages %}
+ {# Footer (Correct) #}