1538 lines
57 KiB
Python
1538 lines
57 KiB
Python
"""
|
|
Observations views - Public and internal views for observation management.
|
|
|
|
Public views (no login required):
|
|
- observation_create_public: Submit new observation
|
|
- observation_submitted: Success page with tracking code
|
|
- observation_track: Track observation by code
|
|
|
|
Internal views (login required):
|
|
- observation_list: List all observations with filters
|
|
- observation_detail: View observation details
|
|
- observation_triage: Triage observation
|
|
- observation_change_status: Change observation status
|
|
- observation_add_note: Add internal note
|
|
- observation_convert_to_action: Convert to PX Action
|
|
- category_list: Manage categories
|
|
- category_create/edit/delete: Category CRUD
|
|
"""
|
|
|
|
import logging
|
|
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import login_required, permission_required
|
|
from django.core.paginator import Paginator
|
|
from django.db.models import Q
|
|
from django.http import HttpResponseForbidden, JsonResponse
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext as _
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from apps.accounts.models import User
|
|
from apps.accounts.services import StaffActivityService
|
|
from apps.organizations.models import Department
|
|
|
|
from .forms import (
|
|
ConvertToActionForm,
|
|
ObservationCategoryForm,
|
|
ObservationDepartmentResponseForm,
|
|
ObservationInternalForm,
|
|
ObservationNoteForm,
|
|
ObservationPublicForm,
|
|
ObservationSendToDepartmentForm,
|
|
ObservationStatusForm,
|
|
ObservationTrackForm,
|
|
ObservationTriageForm,
|
|
)
|
|
from .models import (
|
|
Observation,
|
|
ObservationAttachment,
|
|
ObservationCategory,
|
|
ObservationNote,
|
|
ObservationStatus,
|
|
ObservationStatusLog,
|
|
)
|
|
from .services import ObservationService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _format_duration(start, end):
|
|
"""Format duration between two datetimes as a human-readable string."""
|
|
if not start or not end:
|
|
return None
|
|
duration = end - start
|
|
total_seconds = int(duration.total_seconds())
|
|
if total_seconds < 60:
|
|
return "< 1m"
|
|
days = total_seconds // 86400
|
|
hours = (total_seconds % 86400) // 3600
|
|
minutes = (total_seconds % 3600) // 60
|
|
parts = []
|
|
if days > 0:
|
|
parts.append(f"{days}d")
|
|
if hours > 0:
|
|
parts.append(f"{hours}h")
|
|
if minutes > 0 and days == 0:
|
|
parts.append(f"{minutes}m")
|
|
return " ".join(parts) if parts else "< 1m"
|
|
|
|
|
|
def _build_observation_stage_timeline(observation):
|
|
"""Build stage timeline with timestamps and durations for an observation."""
|
|
stages = []
|
|
|
|
if observation.created_at:
|
|
stages.append({
|
|
"label": _("Created"),
|
|
"timestamp": observation.created_at,
|
|
"color": "bg-slate-400",
|
|
})
|
|
|
|
if observation.triaged_at:
|
|
stages.append({
|
|
"label": _("Triaged"),
|
|
"timestamp": observation.triaged_at,
|
|
"duration_from_prev": _format_duration(observation.created_at, observation.triaged_at),
|
|
"color": "bg-indigo-500",
|
|
})
|
|
|
|
if observation.activated_at:
|
|
stages.append({
|
|
"label": _("Activated"),
|
|
"timestamp": observation.activated_at,
|
|
"duration_from_prev": _format_duration(observation.triaged_at or observation.created_at, observation.activated_at),
|
|
"color": "bg-blue-500",
|
|
})
|
|
|
|
if observation.forwarded_to_dept_at:
|
|
stages.append({
|
|
"label": _("Forwarded to Department"),
|
|
"timestamp": observation.forwarded_to_dept_at,
|
|
"duration_from_prev": _format_duration(observation.activated_at or observation.triaged_at, observation.forwarded_to_dept_at),
|
|
"color": "bg-purple-500",
|
|
})
|
|
|
|
if observation.department_responded_at:
|
|
stages.append({
|
|
"label": _("Department Responded"),
|
|
"timestamp": observation.department_responded_at,
|
|
"duration_from_prev": _format_duration(observation.forwarded_to_dept_at or observation.activated_at, observation.department_responded_at),
|
|
"color": "bg-amber-500",
|
|
})
|
|
|
|
if observation.resolved_at:
|
|
stages.append({
|
|
"label": _("Resolved"),
|
|
"timestamp": observation.resolved_at,
|
|
"duration_from_prev": _format_duration(observation.department_responded_at or observation.forwarded_to_dept_at, observation.resolved_at),
|
|
"color": "bg-green-500",
|
|
})
|
|
|
|
if observation.closed_at:
|
|
stages.append({
|
|
"label": _("Closed"),
|
|
"timestamp": observation.closed_at,
|
|
"duration_from_prev": _format_duration(observation.resolved_at or observation.department_responded_at, observation.closed_at),
|
|
"color": "bg-emerald-600",
|
|
})
|
|
|
|
return stages
|
|
|
|
|
|
# =============================================================================
|
|
# PUBLIC VIEWS (No Login Required)
|
|
# =============================================================================
|
|
|
|
|
|
def observation_create_public(request):
|
|
"""
|
|
Public view for submitting observations.
|
|
|
|
No login required - anonymous submissions allowed.
|
|
"""
|
|
if request.method == "POST":
|
|
form = ObservationPublicForm(request.POST, request.FILES)
|
|
|
|
if form.is_valid():
|
|
try:
|
|
client_ip = get_client_ip(request)
|
|
user_agent = request.META.get("HTTP_USER_AGENT", "")
|
|
attachments = request.FILES.getlist("attachments")
|
|
|
|
from apps.organizations.models import Location, MainSection, SubSection
|
|
|
|
location = None
|
|
main_section = None
|
|
subsection = None
|
|
loc_id = request.POST.get("location")
|
|
sec_id = request.POST.get("main_section")
|
|
sub_id = request.POST.get("subsection")
|
|
|
|
if loc_id:
|
|
try:
|
|
location = Location.objects.get(id=loc_id)
|
|
except Location.DoesNotExist:
|
|
pass
|
|
if sec_id:
|
|
try:
|
|
main_section = MainSection.objects.get(id=sec_id)
|
|
except MainSection.DoesNotExist:
|
|
pass
|
|
if sub_id:
|
|
try:
|
|
subsection = SubSection.objects.get(internal_id=sub_id)
|
|
except SubSection.DoesNotExist:
|
|
pass
|
|
|
|
hospital_id = request.POST.get("hospital")
|
|
hospital = None
|
|
if hospital_id:
|
|
try:
|
|
from apps.organizations.models import Hospital
|
|
hospital = Hospital.objects.get(id=hospital_id)
|
|
except Exception:
|
|
pass
|
|
|
|
assigned_department = None
|
|
if location and hasattr(location, 'department_id') and location.department_id:
|
|
assigned_department = location.department
|
|
|
|
observation = ObservationService.create_observation(
|
|
description=form.cleaned_data["description"],
|
|
severity=form.cleaned_data.get("severity", "medium"),
|
|
category=form.cleaned_data.get("category"),
|
|
title=form.cleaned_data.get("title", ""),
|
|
location_text=form.cleaned_data.get("location_text", ""),
|
|
location=location,
|
|
main_section=main_section,
|
|
subsection=subsection,
|
|
incident_datetime=form.cleaned_data.get("incident_datetime"),
|
|
reporter_staff_id=form.cleaned_data.get("reporter_staff_id", ""),
|
|
reporter_name=form.cleaned_data.get("reporter_name", ""),
|
|
reporter_phone=form.cleaned_data.get("reporter_phone", ""),
|
|
reporter_email=form.cleaned_data.get("reporter_email", ""),
|
|
client_ip=client_ip,
|
|
user_agent=user_agent,
|
|
attachments=attachments,
|
|
hospital=hospital,
|
|
assigned_department=assigned_department,
|
|
source_legacy="public_form",
|
|
)
|
|
|
|
return redirect("observations:observation_submitted", tracking_code=observation.tracking_code)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating observation: {e}")
|
|
messages.error(request, "An error occurred while submitting your observation. Please try again.")
|
|
else:
|
|
form = ObservationPublicForm()
|
|
|
|
from apps.organizations.models import Hospital
|
|
|
|
context = {
|
|
"form": form,
|
|
"categories": ObservationCategory.objects.filter(is_active=True).order_by("sort_order"),
|
|
"hospitals": Hospital.objects.filter(is_active=True).order_by("name_en"),
|
|
}
|
|
|
|
return render(request, "observations/public_new.html", context)
|
|
|
|
|
|
def observation_submitted(request, tracking_code):
|
|
"""
|
|
Success page after observation submission.
|
|
|
|
Shows tracking code for future reference.
|
|
"""
|
|
observation = get_object_or_404(Observation, tracking_code=tracking_code)
|
|
|
|
context = {
|
|
"observation": observation,
|
|
"tracking_code": tracking_code,
|
|
}
|
|
|
|
return render(request, "observations/public_success.html", context)
|
|
|
|
|
|
def observation_track(request):
|
|
"""
|
|
Public view to track observation status by tracking code.
|
|
|
|
Shows minimal status information only (no internal notes).
|
|
"""
|
|
observation = None
|
|
error_message = None
|
|
tracking_code = request.GET.get("tracking_code", "").strip()
|
|
|
|
if request.method == "POST":
|
|
tracking_code = request.POST.get("tracking_code", "").strip()
|
|
if not tracking_code:
|
|
error_message = _("Please enter a tracking code.")
|
|
else:
|
|
try:
|
|
observation = (
|
|
Observation.objects.select_related("category", "hospital")
|
|
.prefetch_related("status_logs", "notes")
|
|
.get(tracking_code__iexact=tracking_code)
|
|
)
|
|
except Observation.DoesNotExist:
|
|
error_message = _("No observation found with this tracking code. Please check and try again.")
|
|
elif tracking_code:
|
|
try:
|
|
observation = (
|
|
Observation.objects.select_related("category", "hospital")
|
|
.prefetch_related("status_logs", "notes")
|
|
.get(tracking_code__iexact=tracking_code)
|
|
)
|
|
except Observation.DoesNotExist:
|
|
error_message = _("No observation found with this tracking code. Please check and try again.")
|
|
|
|
public_timeline = []
|
|
if observation:
|
|
for log in observation.status_logs.all().order_by("-created_at"):
|
|
public_timeline.append(
|
|
{
|
|
"type": "status_change",
|
|
"title": _("Status Updated"),
|
|
"comment": log.comment,
|
|
"created_at": log.created_at,
|
|
"from_status": log.from_status,
|
|
"to_status": log.to_status,
|
|
}
|
|
)
|
|
for note in observation.notes.filter(is_internal=False).order_by("-created_at"):
|
|
public_timeline.append(
|
|
{
|
|
"type": "note",
|
|
"title": _("Update Received"),
|
|
"comment": note.note,
|
|
"created_at": note.created_at,
|
|
}
|
|
)
|
|
public_timeline.sort(key=lambda x: x["created_at"], reverse=True)
|
|
|
|
context = {
|
|
"observation": observation,
|
|
"public_timeline": public_timeline,
|
|
"error_message": error_message,
|
|
"tracking_code": tracking_code,
|
|
}
|
|
|
|
return render(request, "observations/public_track.html", context)
|
|
|
|
|
|
# =============================================================================
|
|
# INTERNAL VIEWS (Login Required)
|
|
# =============================================================================
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def observation_create(request):
|
|
"""
|
|
Internal view for authenticated staff to create observations.
|
|
"""
|
|
communication_request = None
|
|
if request.method == "POST":
|
|
form = ObservationInternalForm(request.POST, request.FILES, request=request)
|
|
|
|
if form.is_valid():
|
|
try:
|
|
client_ip = get_client_ip(request)
|
|
user_agent = request.META.get("HTTP_USER_AGENT", "")
|
|
user = request.user
|
|
attachments = request.FILES.getlist("attachments")
|
|
|
|
observation = ObservationService.create_observation(
|
|
description=form.cleaned_data["description"],
|
|
severity=form.cleaned_data.get("severity") or request.POST.get("severity", "medium"),
|
|
category=form.cleaned_data.get("category"),
|
|
title=form.cleaned_data.get("title", ""),
|
|
location_text=form.cleaned_data.get("location_text", ""),
|
|
location=form.cleaned_data.get("location"),
|
|
main_section=form.cleaned_data.get("main_section"),
|
|
subsection=form.cleaned_data.get("subsection"),
|
|
incident_datetime=form.cleaned_data.get("incident_datetime"),
|
|
reporter_staff_id=user.employee_id or "",
|
|
reporter_name=user.get_full_name(),
|
|
reporter_phone="",
|
|
reporter_email=user.email,
|
|
client_ip=client_ip,
|
|
user_agent=user_agent,
|
|
attachments=attachments,
|
|
hospital=user.hospital if hasattr(user, 'hospital') else None,
|
|
assigned_department=form.cleaned_data.get("assigned_department"),
|
|
source=form.cleaned_data.get("px_source"),
|
|
source_legacy="staff_portal",
|
|
)
|
|
|
|
assigned_dept = form.cleaned_data.get("assigned_department")
|
|
assigned_user = form.cleaned_data.get("assigned_to")
|
|
if assigned_dept:
|
|
observation.assigned_department = assigned_dept
|
|
if assigned_user:
|
|
observation.assigned_to = assigned_user
|
|
observation.status = ObservationStatus.ASSIGNED
|
|
ObservationStatusLog.objects.create(
|
|
observation=observation,
|
|
from_status=ObservationStatus.NEW,
|
|
to_status=ObservationStatus.ASSIGNED,
|
|
comment="Auto-assigned during creation",
|
|
)
|
|
observation.save()
|
|
|
|
comm_req_id = request.POST.get("comm_req")
|
|
if comm_req_id:
|
|
try:
|
|
from apps.px_sources.models import CommunicationRequest
|
|
cr = CommunicationRequest.objects.get(pk=comm_req_id)
|
|
cr.link_to_record(observation)
|
|
except Exception:
|
|
pass
|
|
|
|
StaffActivityService.log_from_request(
|
|
request,
|
|
activity_type="create",
|
|
description=f"Created observation {observation.tracking_code}",
|
|
content_object=observation,
|
|
module="observations",
|
|
)
|
|
|
|
messages.success(request, f"Observation {observation.tracking_code} created successfully.")
|
|
return redirect("observations:observation_detail", pk=observation.id)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating observation: {e}")
|
|
messages.error(request, "An error occurred while creating the observation. Please try again.")
|
|
else:
|
|
communication_request = None
|
|
comm_req_id = request.GET.get("comm_req")
|
|
initial_data = {}
|
|
if comm_req_id:
|
|
try:
|
|
from apps.px_sources.models import CommunicationRequest
|
|
cr_data = CommunicationRequest.get_initial_data(comm_req_id)
|
|
communication_request = cr_data["communication_request"]
|
|
for key in ("patient_file_number", "description"):
|
|
if key in cr_data["initial"] and cr_data["initial"][key]:
|
|
initial_data[key] = cr_data["initial"][key]
|
|
except Exception:
|
|
pass
|
|
|
|
form = ObservationInternalForm(request=request, initial=initial_data)
|
|
|
|
context = {
|
|
"form": form,
|
|
"communication_request": communication_request,
|
|
}
|
|
|
|
return render(request, "observations/observation_create.html", context)
|
|
|
|
|
|
@login_required
|
|
def observation_list(request):
|
|
"""
|
|
Internal view for listing observations with filters.
|
|
|
|
Features:
|
|
- Server-side pagination
|
|
- Advanced filters (status, severity, category, department, etc.)
|
|
- Search by tracking code, description
|
|
- RBAC filtering
|
|
"""
|
|
# Base queryset with optimizations
|
|
queryset = Observation.objects.select_related(
|
|
"category", "assigned_department", "assigned_to", "triaged_by", "resolved_by", "closed_by"
|
|
)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
# Get selected hospital for PX Admins (from middleware)
|
|
selected_hospital = getattr(request, "tenant_hospital", None)
|
|
|
|
if user.is_px_admin():
|
|
# PX Admins see all, but filter by selected hospital if set
|
|
if selected_hospital:
|
|
queryset = queryset.filter(
|
|
Q(assigned_department__hospital=selected_hospital) | Q(assigned_department__isnull=True)
|
|
)
|
|
elif user.is_hospital_admin() and user.hospital:
|
|
queryset = queryset.filter(Q(assigned_department__hospital=user.hospital) | Q(assigned_department__isnull=True))
|
|
elif user.is_department_manager() and user.department:
|
|
queryset = queryset.filter(assigned_department=user.department)
|
|
elif user.hospital:
|
|
queryset = queryset.filter(Q(assigned_department__hospital=user.hospital) | Q(assigned_department__isnull=True))
|
|
|
|
# Apply filters from request
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
severity_filter = request.GET.get("severity")
|
|
if severity_filter:
|
|
queryset = queryset.filter(severity=severity_filter)
|
|
|
|
category_filter = request.GET.get("category")
|
|
if category_filter:
|
|
queryset = queryset.filter(category_id=category_filter)
|
|
|
|
department_filter = request.GET.get("assigned_department")
|
|
if department_filter:
|
|
queryset = queryset.filter(assigned_department_id=department_filter)
|
|
|
|
assigned_to_filter = request.GET.get("assigned_to")
|
|
if assigned_to_filter:
|
|
queryset = queryset.filter(assigned_to_id=assigned_to_filter)
|
|
|
|
is_anonymous_filter = request.GET.get("is_anonymous")
|
|
if is_anonymous_filter == "yes":
|
|
queryset = queryset.filter(reporter_staff_id="", reporter_name="")
|
|
elif is_anonymous_filter == "no":
|
|
queryset = queryset.exclude(reporter_staff_id="", reporter_name="")
|
|
|
|
# Search
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(tracking_code__icontains=search_query)
|
|
| Q(title__icontains=search_query)
|
|
| Q(description__icontains=search_query)
|
|
| Q(reporter_name__icontains=search_query)
|
|
| Q(reporter_staff_id__icontains=search_query)
|
|
| Q(location_text__icontains=search_query)
|
|
)
|
|
|
|
# Date range filters
|
|
date_from = request.GET.get("date_from")
|
|
if date_from:
|
|
queryset = queryset.filter(created_at__date__gte=date_from)
|
|
|
|
date_to = request.GET.get("date_to")
|
|
if date_to:
|
|
queryset = queryset.filter(created_at__date__lte=date_to)
|
|
|
|
# Ordering
|
|
order_by = request.GET.get("order_by", "-created_at")
|
|
queryset = queryset.order_by(order_by)
|
|
|
|
# Pagination
|
|
page_size = int(request.GET.get("page_size", 25))
|
|
paginator = Paginator(queryset, page_size)
|
|
page_number = request.GET.get("page", 1)
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
# Get filter options
|
|
departments = Department.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
from apps.core.utils import get_assignable_users
|
|
assignable_users = get_assignable_users(user.hospital)
|
|
|
|
categories = ObservationCategory.objects.filter(is_active=True)
|
|
|
|
# Statistics
|
|
stats = ObservationService.get_statistics()
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"observations": page_obj.object_list,
|
|
"stats": stats,
|
|
"departments": departments,
|
|
"assignable_users": assignable_users,
|
|
"categories": categories,
|
|
"status_choices": ObservationStatus.choices,
|
|
"filters": request.GET,
|
|
}
|
|
|
|
return render(request, "observations/observation_list.html", context)
|
|
|
|
|
|
@login_required
|
|
def observation_detail(request, pk):
|
|
"""
|
|
Internal view for observation details.
|
|
|
|
Features:
|
|
- Full observation details
|
|
- Timeline (status logs + notes)
|
|
- Attachments
|
|
- Linked PX Action
|
|
- Workflow actions
|
|
"""
|
|
observation = get_object_or_404(
|
|
Observation.objects.select_related(
|
|
"category", "assigned_department", "assigned_to", "triaged_by", "resolved_by", "closed_by",
|
|
"location", "main_section", "subsection", "hospital",
|
|
"taxonomy_domain", "taxonomy_category", "taxonomy_subcategory", "taxonomy_classification",
|
|
).prefetch_related("attachments", "notes__created_by", "status_logs__changed_by"),
|
|
pk=pk,
|
|
)
|
|
|
|
# Check access
|
|
user = request.user
|
|
if not user.is_px_admin():
|
|
if user.is_hospital_admin() and observation.assigned_department:
|
|
if observation.assigned_department.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to view this observation.")
|
|
return redirect("observations:observation_list")
|
|
elif user.is_department_manager() and observation.assigned_department:
|
|
if observation.assigned_department != user.department:
|
|
messages.error(request, "You don't have permission to view this observation.")
|
|
return redirect("observations:observation_list")
|
|
|
|
# Get timeline (combine status logs and notes)
|
|
status_logs = list(observation.status_logs.all())
|
|
notes = list(observation.notes.all())
|
|
|
|
timeline = []
|
|
for log in status_logs:
|
|
timeline.append(
|
|
{
|
|
"type": "status_change",
|
|
"created_at": log.created_at,
|
|
"item": log,
|
|
}
|
|
)
|
|
for note in notes:
|
|
timeline.append(
|
|
{
|
|
"type": "note",
|
|
"created_at": note.created_at,
|
|
"item": note,
|
|
}
|
|
)
|
|
timeline.sort(key=lambda x: x["created_at"], reverse=True)
|
|
stage_timeline = _build_observation_stage_timeline(observation)
|
|
|
|
# Get attachments
|
|
attachments = observation.attachments.all()
|
|
|
|
# Get linked PX Action if exists
|
|
px_action = None
|
|
if observation.action_id:
|
|
from apps.px_action_center.models import PXAction
|
|
|
|
try:
|
|
px_action = PXAction.objects.get(id=observation.action_id)
|
|
except PXAction.DoesNotExist:
|
|
pass
|
|
|
|
# Get assignable users and departments
|
|
departments = Department.objects.filter(status="active")
|
|
from apps.core.utils import get_assignable_users
|
|
assignable_users = get_assignable_users(user.hospital)
|
|
if user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
# Forms
|
|
triage_form = ObservationTriageForm(
|
|
initial={
|
|
"assigned_department": observation.assigned_department,
|
|
"assigned_to": observation.assigned_to,
|
|
"status": observation.status,
|
|
},
|
|
hospital=user.hospital,
|
|
)
|
|
status_form = ObservationStatusForm(initial={"status": observation.status})
|
|
note_form = ObservationNoteForm()
|
|
|
|
from apps.rca.models import RootCauseAnalysis as RCA
|
|
from django.contrib.contenttypes.models import ContentType as CT
|
|
|
|
observation_ct = CT.objects.get_for_model(Observation)
|
|
linked_rcas = RCA.objects.filter(
|
|
content_type=observation_ct, object_id=observation.pk, is_deleted=False
|
|
).select_related("assigned_to", "created_by")
|
|
|
|
context = {
|
|
"observation": observation,
|
|
"timeline": timeline,
|
|
"stage_timeline": stage_timeline,
|
|
"attachments": attachments,
|
|
"px_action": px_action,
|
|
"departments": departments,
|
|
"assignable_users": assignable_users,
|
|
"triage_form": triage_form,
|
|
"status_form": status_form,
|
|
"note_form": note_form,
|
|
"status_choices": ObservationStatus.choices,
|
|
"can_triage": user.has_perm("observations.triage_observation") or user.is_px_admin(),
|
|
"can_convert": user.is_px_admin() or user.is_hospital_admin(),
|
|
"can_send_to_department": user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager() or user.is_px_management(),
|
|
"can_respond_to_department": user.is_px_admin() or user.is_hospital_admin() or (getattr(user, 'is_department_respondent', lambda: False)() and observation.assigned_department == user.department),
|
|
"can_review_dept_response": user.is_px_admin() or user.is_hospital_admin(),
|
|
"can_send_reminder": user.is_px_admin() or user.is_hospital_admin(),
|
|
"can_delete": user.is_px_admin() or user.is_hospital_admin(),
|
|
"linked_rcas": linked_rcas,
|
|
}
|
|
|
|
return render(request, "observations/observation_detail.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_triage(request, pk):
|
|
"""
|
|
Triage an observation - assign department/owner and update status.
|
|
"""
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.has_perm("observations.triage_observation") or user.is_px_admin()):
|
|
messages.error(request, "You don't have permission to triage observations.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
form = ObservationTriageForm(request.POST, hospital=user.hospital)
|
|
|
|
if form.is_valid():
|
|
try:
|
|
ObservationService.triage_observation(
|
|
observation=observation,
|
|
triaged_by=user,
|
|
assigned_department=form.cleaned_data.get("assigned_department"),
|
|
assigned_to=form.cleaned_data.get("assigned_to"),
|
|
new_status=form.cleaned_data.get("status"),
|
|
note=form.cleaned_data.get("note", ""),
|
|
)
|
|
messages.success(request, "Observation triaged successfully.")
|
|
except Exception as e:
|
|
logger.error(f"Error triaging observation: {e}")
|
|
messages.error(request, f"Error triaging observation: {str(e)}")
|
|
else:
|
|
messages.error(request, "Invalid form data.")
|
|
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_change_status(request, pk):
|
|
"""
|
|
Change observation status.
|
|
"""
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.has_perm("observations.triage_observation") or user.is_px_admin()):
|
|
messages.error(request, "You don't have permission to change observation status.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
form = ObservationStatusForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
try:
|
|
ObservationService.change_status(
|
|
observation=observation,
|
|
new_status=form.cleaned_data["status"],
|
|
changed_by=user,
|
|
comment=form.cleaned_data.get("comment", ""),
|
|
)
|
|
messages.success(request, f"Status changed to {form.cleaned_data['status']}.")
|
|
except Exception as e:
|
|
logger.error(f"Error changing status: {e}")
|
|
messages.error(request, f"Error changing status: {str(e)}")
|
|
else:
|
|
messages.error(request, "Invalid form data.")
|
|
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_assign(request, pk):
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to assign observations.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
user_id = request.POST.get("user_id")
|
|
if not user_id:
|
|
messages.error(request, "Please select a user to assign.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
try:
|
|
assignee = User.objects.get(id=user_id)
|
|
old_assignee = observation.assigned_to
|
|
old_status = observation.status
|
|
|
|
observation.assigned_to = assignee
|
|
observation.assigned_at = timezone.now()
|
|
|
|
reopened = False
|
|
if old_status in ("resolved", "closed"):
|
|
observation.status = ObservationStatus.IN_PROGRESS
|
|
observation.resolved_at = None
|
|
observation.resolved_by = None
|
|
observation.closed_at = None
|
|
observation.closed_by = None
|
|
reopened = True
|
|
|
|
observation.save()
|
|
|
|
ObservationStatusLog.objects.create(
|
|
observation=observation,
|
|
from_status=old_status,
|
|
to_status=observation.status,
|
|
changed_by=request.user,
|
|
comment=f"{'Reopened and a' if reopened else 'A'}ssigned to {assignee.get_full_name()}"
|
|
+ (f" (reassigned from {old_assignee.get_full_name()})" if old_assignee else ""),
|
|
)
|
|
|
|
ObservationNote.objects.create(
|
|
observation=observation,
|
|
note=f"{'Reopened and a' if reopened else 'A'}ssigned to {assignee.get_full_name()}"
|
|
+ (f" (reassigned from {old_assignee.get_full_name()})" if old_assignee else ""),
|
|
created_by=request.user,
|
|
is_internal=True,
|
|
)
|
|
|
|
messages.success(request, f"Observation {'reopened and ' if reopened else ''}assigned to {assignee.get_full_name()}.")
|
|
except User.DoesNotExist:
|
|
messages.error(request, "User not found.")
|
|
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_reopen(request, pk):
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to reopen observations.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
if observation.status not in ("resolved", "closed"):
|
|
messages.error(request, "Only resolved or closed observations can be reopened.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
old_status = observation.status
|
|
note_text = request.POST.get("note", "Observation reopened")
|
|
|
|
observation.status = ObservationStatus.IN_PROGRESS
|
|
observation.resolved_at = None
|
|
observation.resolved_by = None
|
|
observation.closed_at = None
|
|
observation.closed_by = None
|
|
observation.save()
|
|
|
|
ObservationStatusLog.objects.create(
|
|
observation=observation,
|
|
from_status=old_status,
|
|
to_status=ObservationStatus.IN_PROGRESS,
|
|
changed_by=user,
|
|
comment=note_text,
|
|
)
|
|
|
|
ObservationNote.objects.create(
|
|
observation=observation,
|
|
note=note_text,
|
|
created_by=user,
|
|
is_internal=True,
|
|
)
|
|
|
|
messages.success(request, "Observation reopened successfully.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_add_note(request, pk):
|
|
"""
|
|
Add internal note to observation.
|
|
"""
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
|
|
form = ObservationNoteForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
try:
|
|
ObservationService.add_note(
|
|
observation=observation,
|
|
note=form.cleaned_data["note"],
|
|
created_by=request.user,
|
|
is_internal=form.cleaned_data.get("is_internal", True),
|
|
)
|
|
messages.success(request, "Note added successfully.")
|
|
except Exception as e:
|
|
logger.error(f"Error adding note: {e}")
|
|
messages.error(request, f"Error adding note: {str(e)}")
|
|
else:
|
|
messages.error(request, "Please enter a note.")
|
|
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def observation_convert_to_action(request, pk):
|
|
"""
|
|
Convert observation to PX Action.
|
|
"""
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to convert observations to actions.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
# Check if already converted
|
|
if observation.action_id:
|
|
messages.warning(request, "This observation has already been converted to an action.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
if request.method == "POST":
|
|
form = ConvertToActionForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
try:
|
|
action = ObservationService.convert_to_action(
|
|
observation=observation,
|
|
created_by=user,
|
|
title=form.cleaned_data["title"],
|
|
description=form.cleaned_data["description"],
|
|
category=form.cleaned_data["category"],
|
|
priority=form.cleaned_data["priority"],
|
|
assigned_department=form.cleaned_data.get("assigned_department"),
|
|
assigned_to=form.cleaned_data.get("assigned_to"),
|
|
)
|
|
messages.success(request, f"Observation converted to action successfully.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
except Exception as e:
|
|
logger.error(f"Error converting to action: {e}")
|
|
messages.error(request, f"Error converting to action: {str(e)}")
|
|
else:
|
|
# Pre-populate form
|
|
initial = {
|
|
"title": f"Observation {observation.tracking_code}",
|
|
"description": observation.description,
|
|
"priority": observation.severity,
|
|
"assigned_department": observation.assigned_department,
|
|
"assigned_to": observation.assigned_to,
|
|
}
|
|
if observation.title:
|
|
initial["title"] += f" - {observation.title}"
|
|
elif observation.category:
|
|
initial["title"] += f" - {observation.category.name_en}"
|
|
|
|
form = ConvertToActionForm(initial=initial)
|
|
|
|
context = {
|
|
"observation": observation,
|
|
"form": form,
|
|
}
|
|
|
|
return render(request, "observations/convert_to_action.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_send_to_department(request, pk):
|
|
"""
|
|
Send an observation to a department for response.
|
|
"""
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
|
|
user = request.user
|
|
if not (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or user.is_department_manager()
|
|
or user.is_px_management()
|
|
):
|
|
messages.error(request, _("You don't have permission to send observations to departments."))
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
department_id = request.POST.get("department_id")
|
|
if not department_id:
|
|
messages.error(request, _("Please select a department."))
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
from apps.organizations.models import Department
|
|
try:
|
|
department = Department.objects.get(pk=department_id, status="active")
|
|
except Department.DoesNotExist:
|
|
messages.error(request, _("Department not found."))
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
note_en = request.POST.get("note_en", "").strip()
|
|
note_ar = request.POST.get("note_ar", "").strip()
|
|
recipient_type = request.POST.get("recipient_type", "staff")
|
|
|
|
note_parts = []
|
|
if note_en:
|
|
note_parts.append(note_en)
|
|
if note_ar:
|
|
note_parts.append(note_ar)
|
|
combined_note = "\n\n".join(note_parts)
|
|
|
|
# Use assigned_department as the outgoing department (reusing existing field)
|
|
observation.assigned_department = department
|
|
observation.forwarded_to_dept_at = timezone.now()
|
|
|
|
sla_config = observation.get_sla_config()
|
|
if sla_config and sla_config.dept_response_hours:
|
|
from datetime import timedelta
|
|
observation.dept_response_sla_due_at = timezone.now() + timedelta(hours=sla_config.dept_response_hours)
|
|
observation.dept_response_is_overdue = False
|
|
observation.dept_response_reminder_sent_at = None
|
|
observation.dept_response_second_reminder_sent_at = None
|
|
observation.dept_response_escalated_at = None
|
|
|
|
if observation.status in ("new", "triaged"):
|
|
observation.status = "contacted"
|
|
observation.save()
|
|
|
|
# Create status log
|
|
ObservationStatusLog.objects.create(
|
|
observation=observation,
|
|
from_status="",
|
|
to_status="contacted",
|
|
changed_by=user,
|
|
comment=combined_note or f"Observation sent to {department.get_localized_name()} for response",
|
|
)
|
|
|
|
# Create note
|
|
ObservationNote.objects.create(
|
|
observation=observation,
|
|
note=combined_note or f"Sent to {department.get_localized_name()} for response",
|
|
created_by=user,
|
|
is_internal=True,
|
|
)
|
|
|
|
try:
|
|
from apps.notifications.settings_service import NotificationServiceWithSettings
|
|
NotificationServiceWithSettings.send_observation_department_assigned(
|
|
department, observation, context_note_en=note_en, context_note_ar=note_ar,
|
|
recipient_type=recipient_type,
|
|
)
|
|
|
|
if department.respondent and department.respondent.user and department.respondent.user.email:
|
|
from apps.notifications.services import NotificationService
|
|
NotificationService.send_email(
|
|
email=department.respondent.user.email,
|
|
subject=f"Observation {observation.tracking_code} - Response Required",
|
|
message=f"An observation has been sent to your department ({department.get_localized_name()}) for response. Please submit your response before the deadline: {observation.dept_response_sla_due_at}",
|
|
related_object=observation,
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to send department notification: {e}")
|
|
|
|
if recipient_type == "department_email":
|
|
messages.success(
|
|
request,
|
|
_("Observation sent to %(dept)s department email.")
|
|
% {"dept": department.get_localized_name()},
|
|
)
|
|
else:
|
|
messages.success(
|
|
request,
|
|
_("Observation sent to %(dept)s. Department respondents have been notified.")
|
|
% {"dept": department.get_localized_name()},
|
|
)
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_send_to(request, pk):
|
|
"""
|
|
Unified AJAX endpoint to send observation to either a person or department.
|
|
"""
|
|
from django.http import JsonResponse
|
|
from apps.notifications.services import NotificationService
|
|
from apps.accounts.models import User
|
|
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
user = request.user
|
|
|
|
# Check permission
|
|
if not (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or user.is_department_manager()
|
|
or user.is_px_management()
|
|
):
|
|
return JsonResponse({
|
|
"success": False,
|
|
"error": str(_("You don't have permission to send this observation.")),
|
|
}, status=403)
|
|
|
|
recipient_type = request.POST.get("recipient_type", "department")
|
|
note = request.POST.get("note", "").strip()
|
|
|
|
try:
|
|
if recipient_type == "person":
|
|
person_id = request.POST.get("person_id")
|
|
if not person_id:
|
|
return JsonResponse({
|
|
"success": False,
|
|
"error": str(_("Please select a person.")),
|
|
}, status=400)
|
|
|
|
try:
|
|
person = User.objects.get(id=person_id)
|
|
except User.DoesNotExist:
|
|
return JsonResponse({
|
|
"success": False,
|
|
"error": str(_("User not found.")),
|
|
}, status=400)
|
|
|
|
# Assign observation to person
|
|
observation.assigned_to = person
|
|
observation.assigned_at = timezone.now()
|
|
observation.save(update_fields=["assigned_to", "assigned_at"])
|
|
|
|
# Send notification
|
|
if person.email:
|
|
NotificationService.send_email(
|
|
email=person.email,
|
|
subject=f"Observation Assigned - {observation.tracking_code}",
|
|
message=f"You have been assigned to observation #{observation.tracking_code}.",
|
|
html_message=f"""
|
|
<p>You have been assigned to observation <strong>#{observation.tracking_code}</strong>.</p>
|
|
<p><strong>Title:</strong> {observation.title or 'N/A'}</p>
|
|
{f'<p><strong>Note:</strong> {note}</p>' if note else ''}
|
|
<p><a href="https://{request.get_host()}/observations/{observation.pk}/">View Observation</a></p>
|
|
""",
|
|
related_object=observation,
|
|
)
|
|
|
|
message = f"Observation sent to {person.get_full_name()}."
|
|
|
|
else: # department
|
|
department_id = request.POST.get("department_id")
|
|
if not department_id:
|
|
return JsonResponse({
|
|
"success": False,
|
|
"error": str(_("Please select a department.")),
|
|
}, status=400)
|
|
|
|
try:
|
|
department = Department.objects.get(pk=department_id, status="active")
|
|
except Department.DoesNotExist:
|
|
return JsonResponse({
|
|
"success": False,
|
|
"error": str(_("Department not found.")),
|
|
}, status=400)
|
|
|
|
# Send to department
|
|
observation.assigned_department = department
|
|
observation.forwarded_to_dept_at = timezone.now()
|
|
|
|
sla_config = observation.get_sla_config()
|
|
if sla_config and sla_config.dept_response_hours:
|
|
from datetime import timedelta
|
|
observation.dept_response_sla_due_at = timezone.now() + timedelta(hours=sla_config.dept_response_hours)
|
|
observation.dept_response_is_overdue = False
|
|
observation.dept_response_reminder_sent_at = None
|
|
observation.dept_response_second_reminder_sent_at = None
|
|
observation.dept_response_escalated_at = None
|
|
|
|
message = f"Observation sent to {department.get_localized_name()}."
|
|
|
|
# Change status to contacted if new or triaged
|
|
if observation.status in ("new", "triaged"):
|
|
observation.status = "contacted"
|
|
|
|
observation.save()
|
|
|
|
# Create status log
|
|
ObservationStatusLog.objects.create(
|
|
observation=observation,
|
|
from_status="",
|
|
to_status="contacted",
|
|
changed_by=user,
|
|
comment=note or f"Observation sent to {recipient_type}",
|
|
)
|
|
|
|
# Send department notification if applicable
|
|
if recipient_type == "department":
|
|
try:
|
|
from apps.notifications.settings_service import NotificationServiceWithSettings
|
|
NotificationServiceWithSettings.send_observation_department_assigned(
|
|
department, observation, context_note_en=note, context_note_ar="",
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to send department notification: {e}")
|
|
|
|
return JsonResponse({
|
|
"success": True,
|
|
"message": message,
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in observation_send_to: {str(e)}")
|
|
return JsonResponse({
|
|
"success": False,
|
|
"error": str(_("An error occurred while sending the observation.")),
|
|
}, status=500)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def observation_department_response(request, pk):
|
|
"""
|
|
Submit a department response for an observation.
|
|
"""
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
|
|
user = request.user
|
|
if not (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or (user.is_champion() and observation.assigned_department == user.department)
|
|
):
|
|
messages.error(request, "You don't have permission to respond to this observation.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
if request.method == "POST":
|
|
response_en = request.POST.get("response_en", "").strip()
|
|
response_ar = request.POST.get("response_ar", "").strip()
|
|
response = response_en or response_ar
|
|
|
|
if not response:
|
|
messages.error(request, "Please enter a response in at least one language.")
|
|
return redirect("observations:observation_department_response", pk=pk)
|
|
|
|
observation.department_response_en = response_en
|
|
observation.department_response_ar = response_ar
|
|
observation.department_responded_at = timezone.now()
|
|
observation.department_responded_by = request.user
|
|
observation.dept_response_is_overdue = False
|
|
observation.dept_response_acceptance_status = "pending"
|
|
observation.dept_response_accepted_by = None
|
|
observation.dept_response_accepted_at = None
|
|
observation.dept_response_acceptance_notes = ""
|
|
observation.save()
|
|
|
|
# Generate AI summary
|
|
try:
|
|
from apps.core.ai_service import AIService
|
|
import json
|
|
|
|
prompt = f"""You are an AI assistant for a hospital patient experience system. Summarize the following department response to a staff observation in 2-3 concise sentences.
|
|
|
|
Observation: {observation.description[:500]}
|
|
Department response: {response[:500]}
|
|
|
|
Generate a JSON response with:
|
|
- "summary_en": A brief summary of the department's response in English (2-3 sentences)
|
|
- "summary_ar": The same summary translated to Modern Standard Arabic"""
|
|
|
|
result = AIService.chat_completion(
|
|
prompt=prompt,
|
|
response_format="json_object",
|
|
)
|
|
parsed = json.loads(result)
|
|
observation.department_response_summary_en = parsed.get("summary_en", "")
|
|
observation.department_response_summary_ar = parsed.get("summary_ar", "")
|
|
observation.save(update_fields=["department_response_summary_en", "department_response_summary_ar"])
|
|
except Exception as e:
|
|
logger.warning(f"AI summary of department response failed: {e}")
|
|
|
|
# Create status log
|
|
ObservationStatusLog.objects.create(
|
|
observation=observation,
|
|
from_status="",
|
|
to_status=observation.status,
|
|
changed_by=user,
|
|
comment=f"Department response submitted by {user.get_full_name()}",
|
|
)
|
|
|
|
# Create note
|
|
ObservationNote.objects.create(
|
|
observation=observation,
|
|
note=f"Department response submitted by {user.get_full_name()}",
|
|
created_by=user,
|
|
is_internal=True,
|
|
)
|
|
|
|
messages.success(request, "Department response submitted successfully.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
context = {
|
|
"observation": observation,
|
|
}
|
|
return render(request, "observations/observation_department_response.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_review_dept_response(request, pk):
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to review department responses.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
if not observation.department_responded_at:
|
|
messages.error(request, "No department response to review.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
status = request.POST.get("acceptance_status")
|
|
if status not in ("acceptable", "not_acceptable"):
|
|
messages.error(request, "Invalid acceptance status.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
notes = request.POST.get("acceptance_notes", "").strip()
|
|
|
|
observation.dept_response_acceptance_status = status
|
|
observation.dept_response_accepted_by = user
|
|
observation.dept_response_accepted_at = timezone.now()
|
|
observation.dept_response_acceptance_notes = notes
|
|
observation.save(
|
|
update_fields=[
|
|
"dept_response_acceptance_status",
|
|
"dept_response_accepted_by",
|
|
"dept_response_accepted_at",
|
|
"dept_response_acceptance_notes",
|
|
]
|
|
)
|
|
|
|
ObservationStatusLog.objects.create(
|
|
observation=observation,
|
|
from_status="",
|
|
to_status=observation.status,
|
|
changed_by=user,
|
|
comment=f"Department response marked as {status} by {user.get_full_name()}",
|
|
)
|
|
|
|
ObservationNote.objects.create(
|
|
observation=observation,
|
|
note=f"Department response review: {status}. {notes}",
|
|
created_by=user,
|
|
is_internal=True,
|
|
)
|
|
|
|
messages.success(request, f"Department response marked as {status}.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_send_dept_response_reminder(request, pk):
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to send reminders.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
if observation.department_responded_at:
|
|
messages.warning(request, "Department has already responded.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
dept = observation.assigned_department
|
|
if not dept:
|
|
messages.error(request, "No department assigned.")
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
reminder_type = request.POST.get("reminder_type", "first")
|
|
|
|
try:
|
|
from apps.notifications.services import NotificationService
|
|
|
|
recipients = []
|
|
if dept.respondent and dept.respondent.user and dept.respondent.user.email:
|
|
recipients.append(dept.respondent.user)
|
|
|
|
for recipient in recipients:
|
|
NotificationService.send_email(
|
|
email=recipient.email,
|
|
subject=f"Reminder: Observation {observation.tracking_code} - Response Required",
|
|
message=f"This is a reminder that observation {observation.tracking_code} is awaiting your department's response. Please submit your response as soon as possible.",
|
|
related_object=observation,
|
|
)
|
|
|
|
if reminder_type == "first":
|
|
observation.dept_response_reminder_sent_at = timezone.now()
|
|
else:
|
|
observation.dept_response_second_reminder_sent_at = timezone.now()
|
|
observation.save()
|
|
|
|
ObservationNote.objects.create(
|
|
observation=observation,
|
|
note=f"Manual {reminder_type} reminder sent by {user.get_full_name()}",
|
|
created_by=user,
|
|
is_internal=True,
|
|
)
|
|
|
|
messages.success(request, f"Reminder sent to {len(recipients)} recipient(s).")
|
|
except Exception as e:
|
|
logger.error(f"Failed to send reminder: {e}")
|
|
messages.error(request, "Failed to send reminder.")
|
|
|
|
return redirect("observations:observation_detail", pk=pk)
|
|
|
|
|
|
# =============================================================================
|
|
# CATEGORY MANAGEMENT VIEWS
|
|
# =============================================================================
|
|
|
|
|
|
@login_required
|
|
@permission_required("observations.manage_categories", raise_exception=True)
|
|
def category_list(request):
|
|
"""
|
|
List observation categories.
|
|
"""
|
|
categories = ObservationCategory.objects.all().order_by("sort_order", "name_en")
|
|
|
|
context = {
|
|
"categories": categories,
|
|
}
|
|
|
|
return render(request, "observations/category_list.html", context)
|
|
|
|
|
|
@login_required
|
|
@permission_required("observations.manage_categories", raise_exception=True)
|
|
@require_http_methods(["GET", "POST"])
|
|
def category_create(request):
|
|
"""
|
|
Create new observation category.
|
|
"""
|
|
if request.method == "POST":
|
|
form = ObservationCategoryForm(request.POST)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, "Category created successfully.")
|
|
return redirect("observations:category_list")
|
|
else:
|
|
form = ObservationCategoryForm()
|
|
|
|
context = {
|
|
"form": form,
|
|
"title": "Create Category",
|
|
}
|
|
|
|
return render(request, "observations/category_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@permission_required("observations.manage_categories", raise_exception=True)
|
|
@require_http_methods(["GET", "POST"])
|
|
def category_edit(request, pk):
|
|
"""
|
|
Edit observation category.
|
|
"""
|
|
category = get_object_or_404(ObservationCategory, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
form = ObservationCategoryForm(request.POST, instance=category)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, "Category updated successfully.")
|
|
return redirect("observations:category_list")
|
|
else:
|
|
form = ObservationCategoryForm(instance=category)
|
|
|
|
context = {
|
|
"form": form,
|
|
"category": category,
|
|
"title": "Edit Category",
|
|
}
|
|
|
|
return render(request, "observations/category_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@permission_required("observations.manage_categories", raise_exception=True)
|
|
@require_http_methods(["POST"])
|
|
def category_delete(request, pk):
|
|
"""
|
|
Delete observation category.
|
|
"""
|
|
category = get_object_or_404(ObservationCategory, pk=pk)
|
|
|
|
# Check if category is in use
|
|
if category.observations.exists():
|
|
messages.error(request, "Cannot delete category that is in use.")
|
|
return redirect("observations:category_list")
|
|
|
|
category.delete()
|
|
messages.success(request, "Category deleted successfully.")
|
|
return redirect("observations:category_list")
|
|
|
|
|
|
# =============================================================================
|
|
# AJAX/API HELPERS
|
|
# =============================================================================
|
|
|
|
|
|
@login_required
|
|
def get_users_by_department(request):
|
|
"""
|
|
Get users for a department (AJAX).
|
|
"""
|
|
department_id = request.GET.get("department_id")
|
|
if not department_id:
|
|
return JsonResponse({"users": []})
|
|
|
|
users = User.objects.filter(is_active=True, department_id=department_id).values(
|
|
"id", "first_name", "last_name", "email"
|
|
)
|
|
|
|
return JsonResponse(
|
|
{
|
|
"users": [
|
|
{
|
|
"id": str(u["id"]),
|
|
"name": f"{u['first_name']} {u['last_name']}",
|
|
"email": u["email"],
|
|
}
|
|
for u in users
|
|
]
|
|
}
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# UTILITY FUNCTIONS
|
|
# =============================================================================
|
|
|
|
|
|
def get_client_ip(request):
|
|
"""
|
|
Get client IP address from request.
|
|
"""
|
|
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
if x_forwarded_for:
|
|
ip = x_forwarded_for.split(",")[0].strip()
|
|
else:
|
|
ip = request.META.get("REMOTE_ADDR")
|
|
return ip
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_soft_delete(request, pk):
|
|
observation = get_object_or_404(Observation, pk=pk)
|
|
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
|
|
return HttpResponseForbidden(_("You don't have permission to delete observations."))
|
|
observation.soft_delete(user=request.user)
|
|
messages.success(request, _("Observation moved to trash."))
|
|
return redirect("observations:observation_list")
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def observation_restore(request, pk):
|
|
observation = get_object_or_404(Observation.all_objects, pk=pk, is_deleted=True)
|
|
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
|
|
return HttpResponseForbidden(_("You don't have permission to restore observations."))
|
|
observation.restore()
|
|
messages.success(request, _("Observation restored successfully."))
|
|
return redirect("observations:observation_list")
|