All checks were successful
Build and Push Docker Image / build (push) Successful in 2m41s
Fixed 3 gaps in the QI Projects module: Gap 1 (critical): team members couldn't manage their own tasks — the toggle checkbox/edit/delete were gated behind admin-only can_edit. Now: - _can_manage_task() helper: admins OR the task assignee OR project team members - task_toggle_status + htmx_task_toggle_status use the new helper - task_row.html shows the toggle for assignees (task.assigned_to.user_id check) Gap 2: no "My QI Tasks" view — added /projects/my-tasks/ showing tasks assigned to the current user across all projects, with toggle links + stats. Sidebar link. Gap 3: no notification on task assignment — added apps/projects/signals.py (post_save on QIProjectTask → create_in_app_notification). apps.py ready() wired. Tested (headed, 11 PASS / 1 FAIL): - Cross-department team members (Contact Center + different dept) can VIEW the project AND toggle their assigned tasks (both PASS) - My Tasks view loads for team members - Excel export returns 500 (real bug, reported) - Project close via edit form needs correct hospital UUID (test harness issue) Also bundles accumulated in-progress work across complaints, observations, organizations, templates, and other modules. Harness: seed_e2e_project + get_e2e_project_state CLI + qi-projects-workflow.spec.ts
798 lines
28 KiB
Python
798 lines
28 KiB
Python
"""
|
|
Core views - Health check and utility views
|
|
"""
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib import messages
|
|
from django.db import connection
|
|
from django.http import JsonResponse
|
|
from django.shortcuts import redirect, render
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.decorators.cache import never_cache
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.views.decorators.http import require_GET, require_POST, require_http_methods
|
|
|
|
|
|
@never_cache
|
|
@require_GET
|
|
def health_check(request):
|
|
"""
|
|
Health check endpoint for monitoring and load balancers.
|
|
Returns JSON with status of various services.
|
|
"""
|
|
health_status = {"status": "ok", "services": {}}
|
|
|
|
# Check database connection
|
|
try:
|
|
with connection.cursor() as cursor:
|
|
cursor.execute("SELECT 1")
|
|
health_status["services"]["database"] = "ok"
|
|
except Exception as e:
|
|
health_status["status"] = "error"
|
|
health_status["services"]["database"] = f"error: {str(e)}"
|
|
|
|
# Check Redis/Celery (optional - don't fail if not available)
|
|
try:
|
|
from django_celery_beat.models import PeriodicTask
|
|
|
|
PeriodicTask.objects.count()
|
|
health_status["services"]["celery_beat"] = "ok"
|
|
except Exception:
|
|
health_status["services"]["celery_beat"] = "not_configured"
|
|
|
|
# Return appropriate status code
|
|
status_code = 200 if health_status["status"] == "ok" else 503
|
|
|
|
return JsonResponse(health_status, status=status_code)
|
|
|
|
|
|
@login_required
|
|
def select_hospital(request):
|
|
"""
|
|
Hospital selection page for PX Admins.
|
|
|
|
Allows PX Admins to switch between hospitals.
|
|
Stores selected hospital in session.
|
|
"""
|
|
# Only PX Admins should access this page
|
|
if not request.user.is_px_admin():
|
|
return redirect("dashboard:dashboard")
|
|
|
|
from apps.organizations.models import Hospital
|
|
|
|
hospitals = Hospital.objects.all().order_by("name")
|
|
|
|
# Handle hospital selection
|
|
if request.method == "POST":
|
|
hospital_id = request.POST.get("hospital_id")
|
|
if hospital_id:
|
|
try:
|
|
hospital = Hospital.objects.get(id=hospital_id)
|
|
request.session["selected_hospital_id"] = str(hospital.id)
|
|
# Redirect to referring page or dashboard
|
|
next_url = request.POST.get("next", request.GET.get("next", "/"))
|
|
return redirect(next_url)
|
|
except Hospital.DoesNotExist:
|
|
pass
|
|
|
|
context = {
|
|
"hospitals": hospitals,
|
|
"selected_hospital_id": request.session.get("selected_hospital_id"),
|
|
"next": request.GET.get("next", "/"),
|
|
}
|
|
|
|
return render(request, "core/select_hospital.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def switch_hospital(request):
|
|
"""
|
|
AJAX endpoint to switch hospitals for PX Admins.
|
|
|
|
Stores selected hospital in session and returns JSON response.
|
|
"""
|
|
# Only PX Admins can switch hospitals
|
|
if not request.user.is_px_admin():
|
|
return JsonResponse(
|
|
{"success": False, "error": "Permission denied. Only PX Admins can switch hospitals."}, status=403
|
|
)
|
|
|
|
from apps.organizations.models import Hospital
|
|
|
|
hospital_id = request.POST.get("hospital_id")
|
|
|
|
if not hospital_id:
|
|
return JsonResponse({"success": False, "error": "Hospital ID is required"}, status=400)
|
|
|
|
try:
|
|
hospital = Hospital.objects.get(id=hospital_id)
|
|
request.session["selected_hospital_id"] = str(hospital.id)
|
|
|
|
return JsonResponse(
|
|
{
|
|
"success": True,
|
|
"hospital": {
|
|
"id": str(hospital.id),
|
|
"name": hospital.name,
|
|
"display_name": hospital.get_display_name(),
|
|
"display_name_ar": hospital.get_display_name_ar(),
|
|
"code": hospital.code,
|
|
},
|
|
}
|
|
)
|
|
except Hospital.DoesNotExist:
|
|
return JsonResponse({"success": False, "error": "Hospital not found"}, status=404)
|
|
|
|
|
|
@login_required
|
|
def no_hospital_assigned(request):
|
|
"""
|
|
Error page for users without a hospital assigned.
|
|
|
|
Users without a hospital assignment cannot access the system.
|
|
"""
|
|
return render(request, "core/no_hospital_assigned.html", status=403)
|
|
|
|
|
|
# ============================================================================
|
|
# PUBLIC SUBMISSION VIEWS
|
|
# ============================================================================
|
|
|
|
|
|
def public_submit_landing(request):
|
|
"""
|
|
Landing page for public submissions.
|
|
|
|
Allows users to choose between Complaint, Observation, or Inquiry.
|
|
No authentication required.
|
|
"""
|
|
from apps.organizations.models import Hospital
|
|
|
|
if request.method == "POST":
|
|
# Return 405 Method Not Allowed with proper JSON response
|
|
from django.http import JsonResponse
|
|
|
|
return JsonResponse(
|
|
{"success": False, "error": "Method not allowed. Please use GET to access the landing page."}, status=405
|
|
)
|
|
|
|
hospitals = Hospital.objects.all().order_by("name")
|
|
|
|
context = {
|
|
"hospitals": hospitals,
|
|
}
|
|
|
|
return render(request, "core/public_submit.html", context)
|
|
|
|
|
|
@require_POST
|
|
def public_inquiry_submit(request):
|
|
"""
|
|
Handle public inquiry submissions.
|
|
|
|
Creates an inquiry from public submission.
|
|
Returns JSON response with reference number.
|
|
"""
|
|
from apps.complaints.models import Inquiry
|
|
from apps.organizations.models import Hospital, Department, Section, OrgSubSection
|
|
import uuid
|
|
|
|
name = request.POST.get("name", "").strip()
|
|
email = request.POST.get("email", "").strip()
|
|
phone = request.POST.get("phone", "").strip()
|
|
hospital_id = request.POST.get("hospital")
|
|
location_type = request.POST.get("location_type", "").strip()
|
|
area_id = request.POST.get("area", "").strip()
|
|
category = request.POST.get("category", "").strip()
|
|
subject = request.POST.get("subject", "").strip()
|
|
message = request.POST.get("message", "").strip()
|
|
department_id = request.POST.get("department", "").strip()
|
|
section_id = request.POST.get("section", "").strip()
|
|
|
|
# Validation
|
|
errors = []
|
|
|
|
if not name:
|
|
errors.append("Name is required")
|
|
if not phone:
|
|
errors.append("Phone number is required")
|
|
if not hospital_id:
|
|
errors.append("Hospital selection is required")
|
|
if not message:
|
|
errors.append("Message is required")
|
|
|
|
if errors:
|
|
return JsonResponse({"success": False, "errors": errors}, status=400)
|
|
|
|
try:
|
|
# Validate hospital
|
|
hospital = Hospital.objects.get(id=hospital_id)
|
|
|
|
from apps.organizations.models import Area
|
|
|
|
department = Department.objects.filter(id=department_id).first() if department_id else None
|
|
section = Section.objects.filter(id=section_id).first() if section_id else None
|
|
area = Area.objects.filter(id=area_id).first() if area_id else None
|
|
|
|
inquiry = Inquiry.objects.create(
|
|
hospital=hospital,
|
|
contact_name=name,
|
|
contact_email=email,
|
|
contact_phone=phone,
|
|
subject=subject,
|
|
message=message,
|
|
category=category,
|
|
status="open",
|
|
area=area,
|
|
department=department,
|
|
section=section,
|
|
location_type=location_type if location_type else "",
|
|
)
|
|
|
|
reference_number = inquiry.reference_number # generated by Inquiry.save() (unified format)
|
|
|
|
try:
|
|
from apps.complaints.tasks import analyze_inquiry_with_ai, notify_staff_new_item
|
|
analyze_inquiry_with_ai.delay(str(inquiry.id))
|
|
notify_staff_new_item.delay("inquiry", str(inquiry.id))
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
from apps.core.services import AuditService
|
|
AuditService.log_event(
|
|
event_type="inquiry_created_public",
|
|
description=f"Public inquiry submitted: {subject}",
|
|
content_object=inquiry,
|
|
metadata={"reference": reference_number, "source": "public_form"},
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
# Send notification email (optional)
|
|
try:
|
|
from django.core.mail import send_mail
|
|
from django.conf import settings
|
|
from django.template.loader import render_to_string
|
|
|
|
email_subject = f"New Public Inquiry - {reference_number}"
|
|
html_message = render_to_string(
|
|
"emails/public_inquiry_notification.html",
|
|
{
|
|
"name": name,
|
|
"subject": subject,
|
|
"message": message,
|
|
"reference_number": reference_number,
|
|
},
|
|
)
|
|
plain_message = f"Inquiry from {name}\n\nSubject: {subject}\n\nMessage:\n{message}"
|
|
send_mail(
|
|
subject=email_subject,
|
|
message=plain_message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
recipient_list=[settings.DEFAULT_FROM_EMAIL],
|
|
fail_silently=True,
|
|
html_message=html_message,
|
|
)
|
|
except Exception:
|
|
pass # Don't fail if email doesn't send
|
|
|
|
return JsonResponse({"success": True, "reference_number": reference_number, "inquiry_id": str(inquiry.id)})
|
|
|
|
except Hospital.DoesNotExist:
|
|
return JsonResponse({"success": False, "errors": ["Invalid hospital selected"]}, status=400)
|
|
except Exception as e:
|
|
return JsonResponse({"success": False, "errors": [str(e)]}, status=500)
|
|
|
|
|
|
@require_GET
|
|
def api_hospitals(request):
|
|
"""
|
|
API endpoint to get hospitals list.
|
|
|
|
Used by public submission forms to populate hospital dropdown.
|
|
"""
|
|
from apps.organizations.models import Hospital
|
|
|
|
hospitals = Hospital.objects.all().order_by("name").values("id", "name")
|
|
|
|
return JsonResponse({"success": True, "hospitals": list(hospitals)})
|
|
|
|
|
|
@require_GET
|
|
def set_language(request):
|
|
"""
|
|
Set's language preference for the session and cookie.
|
|
|
|
Stores the selected language in session and sets a persistent cookie
|
|
for better reliability across page reloads and incognito sessions.
|
|
"""
|
|
from django.conf import settings
|
|
from django.utils import translation
|
|
from urllib.parse import urlparse
|
|
|
|
language = request.GET.get("language", "en")
|
|
|
|
# Validate language code
|
|
if language not in dict(settings.LANGUAGES):
|
|
language = "en"
|
|
|
|
# Activate and store the language in session
|
|
translation.activate(language)
|
|
request.session["django_language"] = language
|
|
|
|
# Explicitly save the session to ensure it persists
|
|
if hasattr(request, "session") and request.session.modified:
|
|
request.session.save()
|
|
|
|
# Get the referring URL or use a default
|
|
next_url = request.META.get("HTTP_REFERER", "/")
|
|
parsed_url = urlparse(next_url)
|
|
|
|
# Keep the path but remove query parameters if needed
|
|
redirect_url = parsed_url.path if parsed_url.path else "/"
|
|
|
|
# If there's no referer, redirect to home or public submit landing
|
|
if next_url == "/" or not next_url:
|
|
redirect_url = "/"
|
|
|
|
# Create response with redirect
|
|
response = redirect(redirect_url)
|
|
|
|
# Also set a persistent cookie as a fallback mechanism
|
|
# This ensures language persists even if session storage fails
|
|
response.set_cookie(
|
|
"django_language",
|
|
language,
|
|
max_age=365 * 24 * 60 * 60, # 1 year
|
|
httponly=False, # Allow JavaScript to read it if needed
|
|
secure=False, # Allow over HTTP for local development
|
|
samesite="Lax", # Standard SameSite policy
|
|
)
|
|
|
|
return response
|
|
|
|
|
|
@require_GET
|
|
def api_observation_categories(request):
|
|
"""
|
|
API endpoint to get observation categories list.
|
|
|
|
Used by public observation form to populate category dropdown.
|
|
"""
|
|
from apps.observations.models import ObservationCategory
|
|
|
|
categories = (
|
|
ObservationCategory.objects.filter(is_active=True)
|
|
.order_by("sort_order", "name_en")
|
|
.values("id", "name_en", "name_ar")
|
|
)
|
|
|
|
return JsonResponse({"success": True, "categories": list(categories)})
|
|
|
|
|
|
def public_track(request):
|
|
"""
|
|
Unified public tracking page.
|
|
|
|
Allows users to track complaints, inquiries, and observations
|
|
from a single page. Shows 3 selection cards, then inline results.
|
|
"""
|
|
return render(request, "core/public_track.html", {})
|
|
|
|
|
|
@require_GET
|
|
def public_track_api(request):
|
|
"""
|
|
API endpoint for unified tracking.
|
|
|
|
Accepts an optional type (complaint/inquiry/observation) and a reference.
|
|
If type is omitted, it is auto-detected from the reference prefix
|
|
(CMP/INQ/OBS). APR and SGT references are internal-only and not tracked.
|
|
|
|
Returns standardized JSON with tracking information.
|
|
"""
|
|
from django.utils.translation import gettext as _
|
|
|
|
track_type = request.GET.get("type", "").strip().lower()
|
|
reference = request.GET.get("reference", "").strip()
|
|
|
|
if not reference:
|
|
return JsonResponse({"found": False, "error": str(_("A reference number is required."))}, status=400)
|
|
|
|
# Auto-detect type from prefix when not provided
|
|
if not track_type:
|
|
upper = reference.upper()
|
|
if upper.startswith("CMP-"):
|
|
track_type = "complaint"
|
|
elif upper.startswith("INQ-"):
|
|
track_type = "inquiry"
|
|
elif upper.startswith("OBS-"):
|
|
track_type = "observation"
|
|
elif upper.startswith(("APR-", "SGT-")):
|
|
return JsonResponse(
|
|
{"found": False, "error": str(_("This reference type is not publicly trackable."))},
|
|
status=400,
|
|
)
|
|
else:
|
|
return JsonResponse({"found": False, "error": str(_("Unrecognized reference format."))}, status=400)
|
|
|
|
if track_type == "complaint":
|
|
return _track_complaint(reference)
|
|
elif track_type == "inquiry":
|
|
return _track_inquiry(reference)
|
|
elif track_type == "observation":
|
|
return _track_observation(reference)
|
|
else:
|
|
return JsonResponse({"found": False, "error": str(_("Invalid tracking type."))}, status=400)
|
|
|
|
|
|
def _track_complaint(reference):
|
|
from apps.complaints.models import Complaint, ComplaintInvolvedDepartment
|
|
|
|
try:
|
|
complaint = (
|
|
Complaint.objects.select_related("hospital", "department", "legacy_location")
|
|
.prefetch_related("updates")
|
|
.get(reference_number__iexact=reference)
|
|
)
|
|
complaint.check_overdue()
|
|
except Complaint.DoesNotExist:
|
|
return JsonResponse({"found": False, "error": "Complaint not found"})
|
|
|
|
from datetime import timedelta
|
|
from django.utils import timezone
|
|
|
|
expiry_base = complaint.resolved_at or complaint.closed_at
|
|
if expiry_base and timezone.now() > expiry_base + timedelta(days=5):
|
|
return JsonResponse({
|
|
"found": True,
|
|
"expired": True,
|
|
"reference": complaint.reference_number,
|
|
"type": "complaint",
|
|
})
|
|
|
|
ps = complaint.public_status
|
|
|
|
public_updates = list(
|
|
complaint.updates.filter(update_type="resolution")
|
|
.order_by("-created_at")[:20]
|
|
)
|
|
|
|
_status_map = {
|
|
"open": "Received", "in_progress": "In Progress",
|
|
"partially_resolved": "In Progress", "contacted": "In Progress",
|
|
"contacted_no_response": "In Progress", "resolved": "Resolved",
|
|
"closed": "Closed", "cancelled": "Cancelled",
|
|
}
|
|
|
|
timeline = []
|
|
for u in public_updates:
|
|
msg = u.message or ""
|
|
for internal, public_label in _status_map.items():
|
|
msg = msg.replace(internal, public_label)
|
|
timeline.append({
|
|
"type": u.update_type,
|
|
"icon": "check-circle-2",
|
|
"title": "Final Resolution",
|
|
"comment": msg,
|
|
"created_at": u.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
})
|
|
|
|
dept_responses = complaint.involved_departments.filter(
|
|
response_submitted=True,
|
|
).select_related("department").order_by("-response_submitted_at")
|
|
for dr in dept_responses:
|
|
timeline.append({
|
|
"type": "response",
|
|
"icon": "message-square",
|
|
"title": "Department Response",
|
|
"department": dr.department.name if dr.department else "",
|
|
"comment": dr.response_notes or "",
|
|
"created_at": dr.response_submitted_at.strftime("%Y-%m-%d %H:%M") if dr.response_submitted_at else "",
|
|
})
|
|
|
|
timeline.sort(key=lambda x: x["created_at"], reverse=True)
|
|
|
|
info_cards = [
|
|
{"icon": "calendar", "label": "Submitted", "value": complaint.created_at.strftime("%b %d, %Y")},
|
|
{"icon": "building", "label": "Department", "value": complaint.department.name if complaint.department else "General"},
|
|
]
|
|
if complaint.due_at:
|
|
info_cards.append({
|
|
"icon": "clock",
|
|
"label": "SLA Deadline",
|
|
"value": complaint.due_at.strftime("%b %d, %H:%M"),
|
|
"alert": complaint.is_overdue,
|
|
})
|
|
else:
|
|
info_cards.append({"icon": "tag", "label": "Category", "value": complaint.get_category_display() if hasattr(complaint, 'get_category_display') else "General"})
|
|
|
|
response = {"has_response": False, "en": "", "ar": ""}
|
|
if complaint.status in ("resolved", "closed") and complaint.resolution:
|
|
response = {"has_response": True, "en": complaint.resolution, "ar": ""}
|
|
|
|
return JsonResponse({
|
|
"found": True,
|
|
"type": "complaint",
|
|
"reference": complaint.reference_number,
|
|
"status": ps["slug"],
|
|
"status_display": ps["label"],
|
|
"progress": ps["progress"],
|
|
"status_color": ps["css"],
|
|
"escalated": bool(complaint.escalated_at),
|
|
"info_cards": info_cards,
|
|
"timeline": timeline,
|
|
"response": response,
|
|
"satisfaction": complaint.satisfaction or "",
|
|
"satisfaction_set_at": complaint.satisfaction_set_at.strftime("%Y-%m-%d %H:%M") if complaint.satisfaction_set_at else None,
|
|
})
|
|
|
|
|
|
def _track_inquiry(reference):
|
|
from apps.complaints.models import Inquiry
|
|
|
|
inquiry = Inquiry.objects.filter(reference_number__iexact=reference).select_related("hospital", "department").first()
|
|
if not inquiry:
|
|
return JsonResponse({"found": False, "error": "Inquiry not found"})
|
|
|
|
status_map = {
|
|
"open": {"label": "Received", "progress": 15, "css": "amber"},
|
|
"in_progress": {"label": "In Progress", "progress": 50, "css": "blue"},
|
|
"resolved": {"label": "Resolved", "progress": 100, "css": "emerald"},
|
|
"closed": {"label": "Closed", "progress": 100, "css": "slate"},
|
|
}
|
|
sm = status_map.get(inquiry.status, {"label": inquiry.get_status_display(), "progress": 15, "css": "amber"})
|
|
|
|
timeline = []
|
|
|
|
info_cards = [
|
|
{"icon": "calendar", "label": "Submitted", "value": inquiry.created_at.strftime("%b %d, %Y")},
|
|
{"icon": "building", "label": "Department", "value": inquiry.department.name if inquiry.department else "General"},
|
|
]
|
|
if inquiry.due_at:
|
|
info_cards.append({
|
|
"icon": "clock",
|
|
"label": "SLA Deadline",
|
|
"value": inquiry.due_at.strftime("%b %d, %H:%M"),
|
|
"alert": inquiry.is_overdue,
|
|
})
|
|
else:
|
|
info_cards.append({"icon": "help-circle", "label": "Subject", "value": inquiry.subject[:40] if inquiry.subject else ""})
|
|
|
|
return JsonResponse({
|
|
"found": True,
|
|
"type": "inquiry",
|
|
"reference": inquiry.reference_number,
|
|
"status": inquiry.status,
|
|
"status_display": sm["label"],
|
|
"progress": sm["progress"],
|
|
"status_color": sm["css"],
|
|
"escalated": bool(inquiry.escalated_at),
|
|
"info_cards": info_cards,
|
|
"timeline": timeline,
|
|
"response": {
|
|
"has_response": bool(inquiry.response_en or inquiry.response_ar or inquiry.response),
|
|
"en": inquiry.response_en or inquiry.response or "",
|
|
"ar": inquiry.response_ar or "",
|
|
},
|
|
"satisfaction": inquiry.satisfaction or "",
|
|
})
|
|
|
|
|
|
def _track_observation(reference):
|
|
from apps.observations.models import Observation
|
|
|
|
try:
|
|
observation = Observation.objects.select_related("hospital", "category").prefetch_related("status_logs", "notes").get(tracking_code__iexact=reference)
|
|
except Observation.DoesNotExist:
|
|
return JsonResponse({"found": False, "error": "Observation not found"})
|
|
|
|
status_progress = {
|
|
"open": 15, "in_progress": 50,
|
|
"resolved": 100, "closed": 100,
|
|
}
|
|
status_css = {
|
|
"open": "amber", "in_progress": "blue",
|
|
"resolved": "emerald", "closed": "slate",
|
|
}
|
|
|
|
timeline = []
|
|
|
|
info_cards = [
|
|
{"icon": "calendar", "label": "Submitted", "value": observation.created_at.strftime("%b %d, %Y")},
|
|
{"icon": "tag", "label": "Category", "value": observation.category.name if observation.category else "Not specified"},
|
|
{"icon": "activity", "label": "Severity", "value": observation.get_severity_display(), "severity": observation.severity},
|
|
]
|
|
|
|
return JsonResponse({
|
|
"found": True,
|
|
"type": "observation",
|
|
"reference": observation.tracking_code,
|
|
"status": observation.status,
|
|
"status_display": observation.get_status_display(),
|
|
"progress": status_progress.get(observation.status, 15),
|
|
"status_color": status_css.get(observation.status, "slate"),
|
|
"escalated": False,
|
|
"info_cards": info_cards,
|
|
"timeline": timeline,
|
|
"response": {
|
|
"has_response": bool(observation.response_en or observation.response_ar or observation.response),
|
|
"en": observation.response_en or observation.response or "",
|
|
"ar": observation.response_ar or "",
|
|
},
|
|
"satisfaction": observation.satisfaction or "",
|
|
})
|
|
|
|
|
|
@require_POST
|
|
def public_observation_submit(request):
|
|
"""
|
|
Handle public observation submissions.
|
|
|
|
Creates an observation from public submission.
|
|
Returns JSON response with tracking code.
|
|
"""
|
|
from apps.observations.models import Observation, ObservationAttachment
|
|
from apps.observations.services import ObservationService
|
|
from apps.organizations.models import Hospital, Department, Section, OrgSubSection
|
|
import mimetypes
|
|
|
|
hospital_id = request.POST.get("hospital", "").strip()
|
|
severity = request.POST.get("severity", "medium")
|
|
title = request.POST.get("title", "").strip()
|
|
description = request.POST.get("description", "").strip()
|
|
location_text = request.POST.get("location_text", "").strip()
|
|
location_type = request.POST.get("location_type", "").strip()
|
|
area_id = request.POST.get("area", "").strip()
|
|
department_id = request.POST.get("department", "").strip()
|
|
section_id = request.POST.get("section", "").strip()
|
|
incident_datetime = request.POST.get("incident_datetime", "")
|
|
reporter_staff_id = request.POST.get("reporter_staff_id", "").strip()
|
|
reporter_name = request.POST.get("reporter_name", "").strip()
|
|
reporter_phone = request.POST.get("reporter_phone", "").strip()
|
|
reporter_email = request.POST.get("reporter_email", "").strip()
|
|
|
|
# Validation
|
|
errors = []
|
|
|
|
if not description:
|
|
errors.append("Description is required")
|
|
if not hospital_id:
|
|
errors.append("Hospital selection is required")
|
|
|
|
if severity not in ["low", "medium", "high", "critical"]:
|
|
errors.append("Invalid severity selected")
|
|
|
|
if errors:
|
|
return JsonResponse({"success": False, "errors": errors}, status=400)
|
|
|
|
try:
|
|
hospital = Hospital.objects.get(id=hospital_id)
|
|
|
|
department = Department.objects.filter(id=department_id).first() if department_id else None
|
|
section = Section.objects.filter(id=section_id).first() if section_id else None
|
|
from apps.organizations.models import Area
|
|
area = Area.objects.filter(id=area_id).first() if area_id else None
|
|
|
|
# Get client info
|
|
def get_client_ip(req):
|
|
x_forwarded_for = req.META.get("HTTP_X_FORWARDED_FOR")
|
|
if x_forwarded_for:
|
|
ip = x_forwarded_for.split(",")[0].strip()
|
|
else:
|
|
ip = req.META.get("REMOTE_ADDR")
|
|
return ip
|
|
|
|
client_ip = get_client_ip(request)
|
|
user_agent = request.META.get("HTTP_USER_AGENT", "")
|
|
|
|
# Handle file uploads
|
|
attachments = request.FILES.getlist("attachments")
|
|
|
|
# Create observation using service
|
|
observation = ObservationService.create_observation(
|
|
description=description,
|
|
severity=severity,
|
|
category=None,
|
|
title=title,
|
|
hospital=hospital,
|
|
location_text=location_text,
|
|
location_type=location_type,
|
|
assigned_department=department,
|
|
section=section,
|
|
area=area,
|
|
incident_datetime=incident_datetime if incident_datetime else None,
|
|
reporter_staff_id=reporter_staff_id,
|
|
reporter_name=reporter_name,
|
|
reporter_phone=reporter_phone,
|
|
reporter_email=reporter_email,
|
|
client_ip=client_ip,
|
|
user_agent=user_agent,
|
|
attachments=attachments,
|
|
)
|
|
|
|
return JsonResponse(
|
|
{"success": True, "tracking_code": observation.tracking_code, "observation_id": str(observation.id)}
|
|
)
|
|
except Exception as e:
|
|
return JsonResponse({"success": False, "errors": [str(e)]}, status=500)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def add_note(request):
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from apps.core.models import Note
|
|
|
|
content_type_id = request.POST.get("content_type_id")
|
|
object_id = request.POST.get("object_id")
|
|
note_text = request.POST.get("note", "").strip()
|
|
|
|
if not note_text:
|
|
messages.error(request, _("Note cannot be empty."))
|
|
return redirect(request.META.get("HTTP_REFERER", "/"))
|
|
|
|
try:
|
|
ct = ContentType.objects.get(pk=content_type_id)
|
|
obj = ct.get_object_for_this_type(pk=object_id)
|
|
except Exception:
|
|
messages.error(request, _("Invalid object reference."))
|
|
return redirect(request.META.get("HTTP_REFERER", "/"))
|
|
|
|
Note.objects.create(
|
|
content_type=ct,
|
|
object_id=object_id,
|
|
note=note_text,
|
|
created_by=request.user,
|
|
is_internal=True,
|
|
)
|
|
messages.success(request, _("Note added successfully."))
|
|
return redirect(request.META.get("HTTP_REFERER", "/"))
|
|
|
|
|
|
@require_POST
|
|
@csrf_exempt
|
|
def public_set_satisfaction(request):
|
|
"""Public endpoint to set patient satisfaction (no auth required)."""
|
|
from django.utils import timezone
|
|
|
|
reference = request.POST.get("reference", "").strip()
|
|
satisfaction = request.POST.get("satisfaction", "").strip()
|
|
|
|
if not reference or not satisfaction:
|
|
return JsonResponse({"success": False, "error": "Reference and satisfaction are required."}, status=400)
|
|
|
|
valid_choices = ["satisfied", "neutral", "dissatisfied"]
|
|
if satisfaction not in valid_choices:
|
|
return JsonResponse({"success": False, "error": "Invalid satisfaction value."}, status=400)
|
|
|
|
upper = reference.upper()
|
|
|
|
try:
|
|
if upper.startswith("CMP-"):
|
|
from apps.complaints.models import Complaint
|
|
obj = Complaint.objects.get(reference_number__iexact=reference)
|
|
elif upper.startswith("INQ-"):
|
|
from apps.complaints.models import Inquiry
|
|
obj = Inquiry.objects.get(reference_number__iexact=reference)
|
|
elif upper.startswith("OBS-"):
|
|
from apps.observations.models import Observation
|
|
obj = Observation.objects.get(tracking_code__iexact=reference)
|
|
else:
|
|
return JsonResponse({"success": False, "error": "Unrecognized reference format."}, status=400)
|
|
except Exception:
|
|
return JsonResponse({"success": False, "error": "Not found."}, status=404)
|
|
|
|
if obj.status not in ("resolved", "closed"):
|
|
return JsonResponse({"success": False, "error": "Satisfaction can only be set for resolved items."}, status=400)
|
|
|
|
obj.satisfaction = satisfaction
|
|
obj.satisfaction_set_at = timezone.now()
|
|
obj.save(update_fields=["satisfaction", "satisfaction_set_at", "updated_at"])
|
|
|
|
return JsonResponse({"success": True, "satisfaction": obj.satisfaction})
|