496 lines
17 KiB
Python
496 lines
17 KiB
Python
"""
|
|
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 Staff") | 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_px_staff": available_admins.filter(groups__name="PX Staff"),
|
|
"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)
|