""" On-Call Admin Schedule UI Views Views for managing on-call admin schedules and assignments. Only PX Admins can access these views. """ import logging from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db import transaction from django.db.models import Q from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_http_methods from apps.accounts.models import User from apps.core.services import AuditService from apps.organizations.models import Hospital from .models import OnCallAdminSchedule, OnCallAdmin logger = logging.getLogger(__name__) def check_px_admin(request): """Check if user is PX Admin, return redirect if not.""" if not request.user.is_px_admin(): messages.error(request, _("You do not have permission to access this page.")) return redirect("dashboard") return None @login_required def oncall_schedule_list(request): """ List all on-call schedules (system-wide and hospital-specific). """ redirect_response = check_px_admin(request) if redirect_response: return redirect_response schedules = OnCallAdminSchedule.objects.select_related("hospital").all() context = { "schedules": schedules, "title": _("On-Call Admin Schedules"), } return render(request, "complaints/oncall/schedule_list.html", context) @login_required def oncall_schedule_create(request): """ Create a new on-call schedule. """ redirect_response = check_px_admin(request) if redirect_response: return redirect_response hospitals = Hospital.objects.filter(status="active") if request.method == "POST": try: # Parse working days from checkboxes working_days = [] for day in range(7): if request.POST.get(f"working_day_{day}"): working_days.append(day) if not working_days: working_days = [0, 1, 2, 3, 4] # Default to Mon-Fri # Get form data hospital_id = request.POST.get("hospital") hospital = Hospital.objects.get(id=hospital_id) if hospital_id else None work_start_time = request.POST.get("work_start_time", "08:00") work_end_time = request.POST.get("work_end_time", "17:00") timezone_str = request.POST.get("timezone", "Asia/Riyadh") is_active = request.POST.get("is_active") == "on" # Create schedule schedule = OnCallAdminSchedule.objects.create( hospital=hospital, working_days=working_days, work_start_time=work_start_time, work_end_time=work_end_time, timezone=timezone_str, is_active=is_active, ) # Log audit AuditService.log_event( event_type="oncall_schedule_created", description=f"On-call schedule created: {schedule}", user=request.user, content_object=schedule, metadata={ "hospital": str(hospital) if hospital else "system-wide", "working_days": working_days, "work_hours": f"{work_start_time}-{work_end_time}", }, ) messages.success(request, _("On-call schedule created successfully.")) return redirect("complaints:oncall_schedule_detail", pk=schedule.id) except Exception as e: logger.error(f"Error creating on-call schedule: {str(e)}") messages.error(request, _("Error creating on-call schedule. Please try again.")) context = { "hospitals": hospitals, "timezones": [ "Asia/Riyadh", "Asia/Dubai", "Asia/Kuwait", "Asia/Qatar", "Asia/Bahrain", "Asia/Muscat", "Asia/Amman", "Asia/Beirut", "Asia/Cairo", "Asia/Jerusalem", "Asia/Baghdad", ], "title": _("Create On-Call Schedule"), } return render(request, "complaints/oncall/schedule_form.html", context) @login_required def oncall_schedule_detail(request, pk): """ View on-call schedule details with list of assigned admins. """ redirect_response = check_px_admin(request) if redirect_response: return redirect_response schedule = get_object_or_404(OnCallAdminSchedule.objects.select_related("hospital"), pk=pk) on_call_admins = schedule.on_call_admins.select_related("admin_user").all() # Check if currently working hours is_working_hours = schedule.is_working_time() context = { "schedule": schedule, "on_call_admins": on_call_admins, "is_working_hours": is_working_hours, "title": _("On-Call Schedule Details"), } return render(request, "complaints/oncall/schedule_detail.html", context) @login_required def oncall_schedule_edit(request, pk): """ Edit an on-call schedule. """ redirect_response = check_px_admin(request) if redirect_response: return redirect_response schedule = get_object_or_404(OnCallAdminSchedule, pk=pk) hospitals = Hospital.objects.filter(status="active") if request.method == "POST": try: # Parse working days from checkboxes working_days = [] for day in range(7): if request.POST.get(f"working_day_{day}"): working_days.append(day) if not working_days: working_days = [0, 1, 2, 3, 4] # Default to Mon-Fri # Get form data hospital_id = request.POST.get("hospital") schedule.hospital = Hospital.objects.get(id=hospital_id) if hospital_id else None schedule.working_days = working_days schedule.work_start_time = request.POST.get("work_start_time", "08:00") schedule.work_end_time = request.POST.get("work_end_time", "17:00") schedule.timezone = request.POST.get("timezone", "Asia/Riyadh") schedule.is_active = request.POST.get("is_active") == "on" schedule.save() # Log audit AuditService.log_event( event_type="oncall_schedule_updated", description=f"On-call schedule updated: {schedule}", user=request.user, content_object=schedule, metadata={ "hospital": str(schedule.hospital) if schedule.hospital else "system-wide", "working_days": working_days, "is_active": schedule.is_active, }, ) messages.success(request, _("On-call schedule updated successfully.")) return redirect("complaints:oncall_schedule_detail", pk=schedule.id) except Exception as e: logger.error(f"Error updating on-call schedule: {str(e)}") messages.error(request, _("Error updating on-call schedule. Please try again.")) context = { "schedule": schedule, "hospitals": hospitals, "timezones": [ "Asia/Riyadh", "Asia/Dubai", "Asia/Kuwait", "Asia/Qatar", "Asia/Bahrain", "Asia/Muscat", "Asia/Amman", "Asia/Beirut", "Asia/Cairo", "Asia/Jerusalem", "Asia/Baghdad", ], "title": _("Edit On-Call Schedule"), } return render(request, "complaints/oncall/schedule_form.html", context) @login_required @require_http_methods(["POST"]) def oncall_schedule_delete(request, pk): """ Delete an on-call schedule. """ redirect_response = check_px_admin(request) if redirect_response: return redirect_response schedule = get_object_or_404(OnCallAdminSchedule, pk=pk) try: # Log before deletion AuditService.log_event( event_type="oncall_schedule_deleted", description=f"On-call schedule deleted: {schedule}", user=request.user, metadata={ "hospital": str(schedule.hospital) if schedule.hospital else "system-wide", "schedule_id": str(pk), }, ) schedule.delete() messages.success(request, _("On-call schedule deleted successfully.")) except Exception as e: logger.error(f"Error deleting on-call schedule: {str(e)}") messages.error(request, _("Error deleting on-call schedule.")) return redirect("complaints:oncall_schedule_list") @login_required def oncall_admin_add(request, schedule_pk): """ Add an admin to the on-call schedule. """ redirect_response = check_px_admin(request) if redirect_response: return redirect_response schedule = get_object_or_404(OnCallAdminSchedule, pk=schedule_pk) # Get all PX Admins not already on this schedule existing_admin_ids = schedule.on_call_admins.values_list("admin_user_id", flat=True) available_admins = ( User.objects.filter( Q(groups__name="PX Admin") | Q(groups__name="PX Coordinator") | Q(groups__name="Hospital Admin"), is_active=True, ) .exclude(id__in=existing_admin_ids) .distinct() ) if request.method == "POST": try: admin_user_id = request.POST.get("admin_user") if not admin_user_id: messages.error(request, _("Please select an admin user.")) return redirect("complaints:oncall_admin_add", schedule_pk=schedule_pk) admin_user = User.objects.get(id=admin_user_id) # Parse dates start_date = request.POST.get("start_date") or None end_date = request.POST.get("end_date") or None # Create on-call admin assignment on_call_admin = OnCallAdmin.objects.create( schedule=schedule, admin_user=admin_user, start_date=start_date, end_date=end_date, notification_priority=int(request.POST.get("notification_priority", 1)), is_active=request.POST.get("is_active") == "on", notify_email=request.POST.get("notify_email") == "on", notify_sms=request.POST.get("notify_sms") == "on", sms_phone=request.POST.get("sms_phone", ""), ) # Log audit AuditService.log_event( event_type="oncall_admin_added", description=f"Admin {admin_user.get_full_name()} added to on-call schedule", user=request.user, content_object=on_call_admin, metadata={ "schedule": str(schedule), "admin_user": str(admin_user), "start_date": start_date, "end_date": end_date, }, ) messages.success(request, _("On-call admin added successfully.")) return redirect("complaints:oncall_schedule_detail", pk=schedule_pk) except Exception as e: logger.error(f"Error adding on-call admin: {str(e)}") messages.error(request, _("Error adding on-call admin. Please try again.")) context = { "schedule": schedule, "available_admins": available_admins, "available_px_admins": available_admins.filter(groups__name="PX Admin"), "available_coordinators": available_admins.filter(groups__name="PX Coordinator"), "available_hospital_admins": available_admins.filter(groups__name="Hospital Admin"), "title": _("Add On-Call Admin"), } return render(request, "complaints/oncall/admin_form.html", context) @login_required def oncall_admin_edit(request, pk): """ Edit an on-call admin assignment. """ redirect_response = check_px_admin(request) if redirect_response: return redirect_response on_call_admin = get_object_or_404(OnCallAdmin.objects.select_related("schedule", "admin_user"), pk=pk) if request.method == "POST": try: # Parse dates start_date = request.POST.get("start_date") or None end_date = request.POST.get("end_date") or None # Update fields on_call_admin.start_date = start_date on_call_admin.end_date = end_date on_call_admin.notification_priority = int(request.POST.get("notification_priority", 1)) on_call_admin.is_active = request.POST.get("is_active") == "on" on_call_admin.notify_email = request.POST.get("notify_email") == "on" on_call_admin.notify_sms = request.POST.get("notify_sms") == "on" on_call_admin.sms_phone = request.POST.get("sms_phone", "") on_call_admin.save() # Log audit AuditService.log_event( event_type="oncall_admin_updated", description=f"On-call admin updated: {on_call_admin}", user=request.user, content_object=on_call_admin, metadata={ "schedule": str(on_call_admin.schedule), "admin_user": str(on_call_admin.admin_user), "is_active": on_call_admin.is_active, }, ) messages.success(request, _("On-call admin updated successfully.")) return redirect("complaints:oncall_schedule_detail", pk=on_call_admin.schedule.id) except Exception as e: logger.error(f"Error updating on-call admin: {str(e)}") messages.error(request, _("Error updating on-call admin. Please try again.")) context = { "on_call_admin": on_call_admin, "schedule": on_call_admin.schedule, "title": _("Edit On-Call Admin"), } return render(request, "complaints/oncall/admin_form.html", context) @login_required @require_http_methods(["POST"]) def oncall_admin_delete(request, pk): """ Remove an admin from the on-call schedule. """ redirect_response = check_px_admin(request) if redirect_response: return redirect_response on_call_admin = get_object_or_404(OnCallAdmin.objects.select_related("schedule", "admin_user"), pk=pk) schedule_pk = on_call_admin.schedule.id try: # Log before deletion AuditService.log_event( event_type="oncall_admin_removed", description=f"Admin removed from on-call schedule: {on_call_admin}", user=request.user, metadata={ "schedule": str(on_call_admin.schedule), "admin_user": str(on_call_admin.admin_user), "oncall_admin_id": str(pk), }, ) on_call_admin.delete() messages.success(request, _("On-call admin removed successfully.")) except Exception as e: logger.error(f"Error removing on-call admin: {str(e)}") messages.error(request, _("Error removing on-call admin.")) return redirect("complaints:oncall_schedule_detail", pk=schedule_pk) @login_required def oncall_dashboard(request): """ Dashboard view showing current on-call status and admins. """ redirect_response = check_px_admin(request) if redirect_response: return redirect_response # Get all schedules schedules = OnCallAdminSchedule.objects.select_related("hospital").all() # Get currently active on-call admins now = timezone.now() today = now.date() active_on_call_admins = ( OnCallAdmin.objects.filter(is_active=True, schedule__is_active=True) .select_related("admin_user", "schedule", "schedule__hospital") .filter( Q(start_date__isnull=True) | Q(start_date__lte=today), Q(end_date__isnull=True) | Q(end_date__gte=today) ) ) # Check each schedule's current status schedule_statuses = [] for schedule in schedules: is_working = schedule.is_working_time() schedule_oncall = active_on_call_admins.filter(schedule=schedule) schedule_statuses.append( { "schedule": schedule, "is_working_hours": is_working, "on_call_count": schedule_oncall.count(), "on_call_admins": schedule_oncall, } ) context = { "schedule_statuses": schedule_statuses, "total_schedules": schedules.count(), "total_active_oncall": active_on_call_admins.count(), "current_time": now, "title": _("On-Call Dashboard"), } return render(request, "complaints/oncall/dashboard.html", context)