HH/apps/complaints/ui_views_oncall.py
2026-02-22 08:35:53 +03:00

480 lines
16 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(
groups__name='PX Admin',
is_active=True
).exclude(id__in=existing_admin_ids)
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,
'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)