From fe0a6436072c5cc21e148af55073b41c4c13b51c Mon Sep 17 00:00:00 2001 From: ismail Date: Sun, 12 Jul 2026 11:18:20 +0300 Subject: [PATCH] update --- apps/accounts/forms.py | 16 +- apps/complaints/services/complaint_service.py | 14 +- apps/complaints/tasks.py | 34 +++- apps/complaints/ui_views.py | 175 +++++++++++++++++- apps/complaints/urls.py | 1 + apps/complaints/views.py | 16 ++ apps/core/config_views.py | 41 ++-- apps/integrations/api_serializers.py | 12 -- apps/integrations/api_views.py | 48 ++--- apps/integrations/urls_external.py | 4 +- apps/notifications/services.py | 12 ++ apps/notifications/tests_email.py | 129 +++++++++++++ apps/observations/tasks.py | 20 ++ apps/organizations/services.py | 64 +++++++ apps/organizations/ui_views.py | 24 ++- ...PX360_External_API.postman_collection.json | 16 +- docs/external-api.md | 16 +- docs/workflows/appreciations.md | 2 +- docs/workflows/suggestions.md | 4 +- templates/complaints/complaint_detail.html | 118 ++++++++---- .../config/emails/user_created_email.html | 25 ++- templates/config/user_form.html | 156 ++++++++++++++-- templates/emails/inquiry_sla_reminder.html | 40 ++++ .../emails/inquiry_sla_second_reminder.html | 40 ++++ templates/layouts/partials/sidebar.html | 4 +- 25 files changed, 871 insertions(+), 160 deletions(-) create mode 100644 apps/notifications/tests_email.py create mode 100644 templates/emails/inquiry_sla_reminder.html create mode 100644 templates/emails/inquiry_sla_second_reminder.html diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 9f8fe41..5e89b08 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -77,7 +77,7 @@ class UserCreateForm(DepartmentFieldMixin, forms.ModelForm): required=False, widget=forms.PasswordInput(attrs={ "class": "w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent transition bg-white text-sm", - "placeholder": _("Leave blank to generate random password"), + "placeholder": _("Leave blank to send a password setup link via email"), }), ) password_confirm = forms.CharField( @@ -111,6 +111,8 @@ class UserCreateForm(DepartmentFieldMixin, forms.ModelForm): if self.user and self.user.is_hospital_admin() and self.user.hospital: self.fields["hospital"].initial = self.user.hospital self.fields["hospital"].queryset = Hospital.objects.filter(id=self.user.hospital.id) + elif self.user and self.user.is_px_admin() and getattr(self.request, "tenant_hospital", None): + self.fields["hospital"].initial = self.request.tenant_hospital def clean_email(self): email = self.cleaned_data.get("email") @@ -127,18 +129,14 @@ class UserCreateForm(DepartmentFieldMixin, forms.ModelForm): return cleaned_data def save(self, commit=True): - import secrets - import string - user = super().save(commit=False) password = self.cleaned_data.get("password") - if not password: - alphabet = string.ascii_letters + string.digits + "!@#$%^&*" - password = ''.join(secrets.choice(alphabet) for _ in range(16)) - self._generated_password = password + if password: + user.set_password(password) + else: + user.set_unusable_password() - user.set_password(password) if commit: user.save() selected_groups = self.cleaned_data.get("groups") diff --git a/apps/complaints/services/complaint_service.py b/apps/complaints/services/complaint_service.py index a398001..bf4a55c 100644 --- a/apps/complaints/services/complaint_service.py +++ b/apps/complaints/services/complaint_service.py @@ -81,14 +81,16 @@ class ComplaintService: return True if user.is_hospital_admin() and user.hospital == complaint.hospital: return True - if user.is_px_management() and user.hospital == complaint.hospital: - return True - if user.is_px_employee() and user.hospital == complaint.hospital: - return True - if user.is_department_manager() and user.department == complaint.department: - return True if complaint.assigned_to and complaint.assigned_to == user: return True + # Before activation: PX staff can claim/activate the complaint + if not complaint.activated_at: + if user.is_px_management() and user.hospital == complaint.hospital: + return True + if user.is_px_employee() and user.hospital == complaint.hospital: + return True + if user.is_department_manager() and user.department == complaint.department: + return True if user.department_id and complaint.involved_departments.filter(department_id=user.department_id).exists(): return True return False diff --git a/apps/complaints/tasks.py b/apps/complaints/tasks.py index 1ac5f61..8a21fd5 100644 --- a/apps/complaints/tasks.py +++ b/apps/complaints/tasks.py @@ -2948,10 +2948,17 @@ def send_inquiry_sla_reminders(): if hours_since_creation >= first_reminder_hours: if inquiry.assigned_to and inquiry.assigned_to.email: try: + ctx = { + "inquiry": inquiry, + "recipient_name": inquiry.assigned_to.get_full_name(), + "hours_remaining": max(0, round(hours_until_due)), + "inquiry_url": f"{settings.SITE_URL.rstrip('/')}/inquiries/{inquiry.pk}/", + } NotificationService.send_email( email=inquiry.assigned_to.email, subject=f"SLA Reminder - Inquiry #{inquiry.id}", message=f"Inquiry '{inquiry.subject}' is due at {inquiry.due_at}. Please take action.", + html_message=render_to_string("emails/inquiry_sla_reminder.html", ctx), related_object=inquiry, ) inquiry.reminder_sent_at = now @@ -2965,10 +2972,17 @@ def send_inquiry_sla_reminders(): if hours_since_creation >= second_reminder_hours: if inquiry.assigned_to and inquiry.assigned_to.email: try: + ctx = { + "inquiry": inquiry, + "recipient_name": inquiry.assigned_to.get_full_name(), + "hours_remaining": max(0, round(hours_until_due)), + "inquiry_url": f"{settings.SITE_URL.rstrip('/')}/inquiries/{inquiry.pk}/", + } NotificationService.send_email( email=inquiry.assigned_to.email, subject=f"URGENT: SLA Reminder - Inquiry #{inquiry.id}", message=f"Inquiry '{inquiry.subject}' is due at {inquiry.due_at}. URGENT action required.", + html_message=render_to_string("emails/inquiry_sla_second_reminder.html", ctx), related_object=inquiry, ) inquiry.second_reminder_sent_at = now @@ -3233,6 +3247,14 @@ def send_inquiry_dept_response_reminders(): ): for recipient in recipients: try: + ctx = { + "inquiry": inquiry, + "department_name": dept.name, + "sla_due_at": inquiry.dept_response_sla_due_at, + "hours_remaining": max(0, round(hours_remaining)), + "inquiry_url": f"{settings.SITE_URL.rstrip('/')}/inquiries/{inquiry.pk}/", + "recipient_name": recipient.get_full_name(), + } NotificationService.send_email( email=recipient.email, subject=f"Reminder: Inquiry #{inquiry.reference_number} - Response Required", @@ -3242,6 +3264,7 @@ def send_inquiry_dept_response_reminders(): f"Time remaining: {max(0, round(hours_remaining))} hours. " f"Please submit your response before the deadline." ), + html_message=render_to_string("emails/inquiry_dept_response_reminder.html", ctx), related_object=inquiry, ) except Exception as e: @@ -3260,6 +3283,14 @@ def send_inquiry_dept_response_reminders(): ): for recipient in recipients: try: + ctx = { + "inquiry": inquiry, + "department_name": dept.name, + "sla_due_at": inquiry.dept_response_sla_due_at, + "hours_remaining": max(0, round(hours_remaining)), + "inquiry_url": f"{settings.SITE_URL.rstrip('/')}/inquiries/{inquiry.pk}/", + "recipient_name": recipient.get_full_name(), + } NotificationService.send_email( email=recipient.email, subject=f"URGENT: Inquiry #{inquiry.reference_number} - Response Overdue Soon", @@ -3269,6 +3300,7 @@ def send_inquiry_dept_response_reminders(): f"Time remaining: {max(0, round(hours_remaining))} hours. " f"Please submit your response immediately." ), + html_message=render_to_string("emails/inquiry_dept_response_reminder.html", ctx), related_object=inquiry, ) except Exception as e: @@ -3534,7 +3566,7 @@ def notify_champion_on_dept_assignment_task(involved_department_id): department_url = f"{base}/organizations/departments/{department.pk}/" NotificationService.send_email( - recipient=champion_user.email, + email=champion_user.email, subject=f"New Complaint Assigned - {complaint.reference_number}", message=f"""A new complaint has been assigned to your department ({department.name}). diff --git a/apps/complaints/ui_views.py b/apps/complaints/ui_views.py index 737d978..adccf64 100644 --- a/apps/complaints/ui_views.py +++ b/apps/complaints/ui_views.py @@ -612,6 +612,28 @@ def complaint_detail(request, pk): ComplaintService.ensure_involved_records(complaint) + # Self-heal: if the complaint was sent to its primary department but no CID + # row exists (legacy data from before the fix), create one now so the primary + # department appears in the Involved Departments list. + if complaint.sent_to_department and complaint.department_id: + if not any(inv.department_id == complaint.department_id + for inv in complaint.involved_departments.all()): + from .models import ComplaintInvolvedDepartment + ComplaintInvolvedDepartment.objects.get_or_create( + complaint=complaint, + department=complaint.department, + defaults={ + "role": "primary", + "is_primary": True, + "added_by": request.user, + "forwarded_at": complaint.forwarded_to_dept_at or timezone.now(), + "sent": True, + "sent_at": complaint.sent_to_department_at or timezone.now(), + }, + ) + if hasattr(complaint, "_prefetched_objects_cache"): + complaint._prefetched_objects_cache.pop("involved_departments", None) + user = request.user if not user.is_px_admin(): if user.is_hospital_admin() and complaint.hospital != user.hospital: @@ -747,9 +769,12 @@ def complaint_detail(request, pk): "can_manage_actions": ( user.is_px_admin() or (user.is_hospital_admin() and user.hospital == complaint.hospital) - or (user.is_px_management() and user.hospital == complaint.hospital) - or (user.is_px_employee() and user.hospital == complaint.hospital) or (complaint.assigned_to == user) + or ( + not complaint.activated_at + and (user.is_px_management() or user.is_px_employee()) + and user.hospital == complaint.hospital + ) ), "can_admin": user.is_px_admin() or (user.is_hospital_admin() and user.hospital == complaint.hospital), "is_active_status": complaint.is_active_status, @@ -1195,6 +1220,35 @@ def complaint_send_to(request, pk): now = timezone.now() if complaint.department_id == department.pk: + # Create/update CID for the primary department too so it appears + # in the Involved Departments list and the department-detail queries. + involved_dept, created = ComplaintInvolvedDepartment.objects.get_or_create( + complaint=complaint, + department=department, + defaults={ + "role": "primary", + "is_primary": True, + "added_by": user, + "forwarded_at": now, + "sent": True, + "sent_at": now, + }, + ) + if not created: + involved_dept.forwarded_at = now + involved_dept.sent = True + involved_dept.sent_at = now + involved_dept.routing_status = "sent" + involved_dept.rejected_at = None + involved_dept.rejected_by_staff = None + involved_dept.rejection_reason = "" + involved_dept.suggested_department = None + involved_dept.save(update_fields=[ + "forwarded_at", "sent", "sent_at", + "routing_status", "rejected_at", "rejected_by_staff", + "rejection_reason", "suggested_department", + ]) + complaint.sent_to_department = True complaint.sent_to_department_at = now if not complaint.forwarded_to_dept_at: @@ -1371,6 +1425,123 @@ def complaint_send_to(request, pk): }, status=500) +@login_required +@require_http_methods(["POST"]) +def complaint_send_dept_reminder(request, pk): + """Send a manual reminder to a department that hasn't responded yet. + + Visible in Quick Actions after the complaint has been sent to a department. + The user picks which department from a modal; this endpoint sends an email + + SMS to that department's champion/manager and logs a ComplaintUpdate. + """ + from .models import ComplaintInvolvedDepartment, ComplaintUpdate + from apps.notifications.services import NotificationService, get_email_header_html + from apps.organizations.department_contacts import get_champion_and_manager + from apps.core.utils import build_public_track_url + + complaint = get_object_or_404(Complaint, pk=pk) + user = request.user + + if not can_manage_complaint(user, complaint): + messages.error(request, _("You don't have permission to send reminders.")) + return redirect("complaints:complaint_detail", pk=pk) + + inv_dept_id = request.POST.get("involved_department_id", "").strip() + note = request.POST.get("note", "").strip() + + if not inv_dept_id: + messages.error(request, _("Please select a department.")) + return redirect("complaints:complaint_detail", pk=pk) + + involved_dept = get_object_or_404( + ComplaintInvolvedDepartment, + pk=inv_dept_id, + complaint=complaint, + sent=True, + response_submitted=False, + ) + department = involved_dept.department + + targets = get_champion_and_manager(department) + if not targets: + messages.error( + request, + _(f"Cannot send reminder to {department.get_localized_name()}. This department has no champion or manager assigned."), + ) + return redirect("complaints:complaint_detail", pk=pk) + + track_url = build_public_track_url("complaint", complaint.reference_number) + notified = [] + sent_any = False + + for target in targets: + email = target.get("email") or "" + phone = target.get("phone") or "" + label = target.get("label") or "" + display_name = target.get("display_name") or label + + if email: + try: + NotificationService.send_email( + email=email, + subject=f"Reminder: Complaint #{complaint.reference_number} - Response Required", + message=( + f"This is a reminder that complaint #{complaint.reference_number} " + f"is awaiting your department's response.\n\n" + f"Track: {track_url}" + ), + html_message=f""" +
+ {get_email_header_html()} +
+

Reminder: Response Required

+

This is a reminder that complaint #{complaint.reference_number} is still awaiting your department's response.

+

Department: {department.name}

+
+ Track Complaint +
+
+
+""", + related_object=complaint, + ) + sent_any = True + except Exception: + pass + if phone: + try: + NotificationService.send_sms( + phone, + f"PX360: Reminder - Complaint #{complaint.reference_number} sent to {department.name} is awaiting your response. Track: {track_url}", + related_object=complaint, + ) + sent_any = True + except Exception: + pass + + notified.append(f"{display_name} ({label})") + + if sent_any: + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="communication", + message=f"Manual reminder sent to {department.get_localized_name()} — {', '.join(notified)}", + created_by=user, + metadata={ + "event_type": "manual_reminder", + "department_id": str(department.id), + "involved_department_id": str(involved_dept.id), + "recipients": notified, + "note": note, + }, + ) + messages.success(request, _(f"Reminder sent to {department.get_localized_name()}.")) + else: + messages.error(request, _(f"Could not send reminder to {department.get_localized_name()}. No valid contact info.")) + + return redirect("complaints:complaint_detail", pk=pk) + + @login_required @require_http_methods(["POST"]) def complaint_change_status(request, pk): diff --git a/apps/complaints/urls.py b/apps/complaints/urls.py index 9e4ce37..b18164a 100644 --- a/apps/complaints/urls.py +++ b/apps/complaints/urls.py @@ -180,6 +180,7 @@ urlpatterns = [ path("departments//reject-routing/", ui_views.involved_department_reject_routing, name="involved_department_reject_routing"), # Unified Send To (Person or Department) - AJAX path("/send-to/", ui_views.complaint_send_to, name="complaint_send_to"), + path("/send-dept-reminder/", ui_views.complaint_send_dept_reminder, name="complaint_send_dept_reminder"), # Collect Feedback (champion/manager compose questions for staff) path("/collect-feedback/", ui_views.collect_feedback_start, name="collect_feedback_start"), # Involved Staff Management diff --git a/apps/complaints/views.py b/apps/complaints/views.py index 5c724a6..3419328 100644 --- a/apps/complaints/views.py +++ b/apps/complaints/views.py @@ -3665,6 +3665,10 @@ def complaint_explanation_form(request, complaint_id, token): ) investigation.final_reply = explanation_text investigation.status = InvestigationStatus.DIRECT_REPLY_IN_PROGRESS + investigation.negligence_finding = negligence_finding + investigation.policy_issue_finding = policy_issue_finding + investigation.requires_improvement_project = requires_improvement_project + investigation.improvement_project_note = improvement_project_note import random @@ -3673,6 +3677,8 @@ def complaint_explanation_form(request, complaint_id, token): investigation.otp_sent_at = timezone.now() investigation.save(update_fields=[ "final_reply", "status", "otp_code", "otp_sent_at", + "negligence_finding", "policy_issue_finding", + "requires_improvement_project", "improvement_project_note", ]) sent_channels = [] @@ -3977,6 +3983,7 @@ def complaint_explanation_form(request, complaint_id, token): # Auto-show OTP entry if a direct-reply OTP is still valid (e.g., user reloaded page) otp_sent_on_get = False + draft_ctx = {} if not explanation.is_used: from apps.complaints.models import ChampionInvestigation, InvestigationStatus from datetime import timedelta @@ -3987,6 +3994,14 @@ def complaint_explanation_form(request, complaint_id, token): ).exclude(otp_code="").first() if existing and existing.otp_sent_at and timezone.now() <= existing.otp_sent_at + timedelta(minutes=10): otp_sent_on_get = True + draft_ctx = { + "final_reply": existing.final_reply or "", + "negligence_finding": existing.negligence_finding or "", + "policy_issue_finding": existing.policy_issue_finding or "", + "requires_improvement_project": existing.requires_improvement_project or "", + "improvement_project_note": existing.improvement_project_note or "", + "consent_checked": True, + } return render( request, @@ -4005,6 +4020,7 @@ def complaint_explanation_form(request, complaint_id, token): "token": explanation.token, }) ), + **draft_ctx, }, ) diff --git a/apps/core/config_views.py b/apps/core/config_views.py index 9c738a8..4522136 100644 --- a/apps/core/config_views.py +++ b/apps/core/config_views.py @@ -212,9 +212,10 @@ def user_create(request): form = UserCreateForm(request.POST, request=request) if form.is_valid(): user = form.save() - generated_password = getattr(form, '_generated_password', None) staff_id = request.POST.get("staff_id") + create_staff = request.POST.get("create_staff") == "on" + if staff_id: from apps.organizations.models import Staff try: @@ -226,29 +227,46 @@ def user_create(request): )) except Staff.DoesNotExist: messages.success(request, _("User '{}' created successfully.").format(user.get_full_name())) + elif create_staff: + from apps.organizations.services import StaffService + staff_type = request.POST.get("staff_type", "other") + job_title = request.POST.get("staff_job_title", "") + try: + staff = StaffService.create_staff_for_user( + user, staff_type=staff_type, job_title=job_title, request=request + )[0] + messages.success(request, _("User '{}' created with staff record '{}'.").format( + user.get_full_name(), staff.get_full_name() + )) + except ValueError as e: + messages.warning(request, _("User '{}' created but staff record creation failed: {}").format( + user.get_full_name(), str(e) + )) else: messages.success(request, _("User '{}' created successfully.").format(user.get_full_name())) + password_was_set = bool(form.cleaned_data.get("password")) + try: base_url = f"{request.scheme}://{request.get_host()}" - reset_token = PasswordResetTokenService.create_reset_token(user) - reset_url = PasswordResetTokenService.build_reset_url(base_url, reset_token) + subject = _("Your PX360 Account Has Been Created") - if generated_password: + if password_was_set: + login_url = f"{base_url}/accounts/login/" html_message = render_to_string( "config/emails/user_created_email.html", - {"user": user, "reset_url": reset_url, "temp_password": generated_password}, + {"user": user, "login_url": login_url}, request=request, ) plain_message = ( f"Dear {user.get_full_name()},\n\n" f"An account has been created for you on PX360.\n\n" - f"Your temporary password: {generated_password}\n\n" - f"For security, please set your own password using the link below:\n{reset_url}\n\n" - f"This link expires in 24 hours." + f"You can log in at: {login_url}\n\n" + f"Please contact your administrator for your login credentials." ) - subject = _("Your PX360 Account Has Been Created") else: + reset_token = PasswordResetTokenService.create_reset_token(user) + reset_url = PasswordResetTokenService.build_reset_url(base_url, reset_token) html_message = render_to_string( "config/emails/reset_password_email.html", {"user": user, "reset_url": reset_url}, @@ -260,7 +278,6 @@ def user_create(request): f"Use the link below to set your password:\n{reset_url}\n\n" f"This link expires in 24 hours." ) - subject = _("Your PX360 Account Has Been Created") NotificationService.send_email( email=user.email, @@ -270,8 +287,8 @@ def user_create(request): user=user, notification_type="system", ) - if generated_password: - messages.info(request, _("Credentials sent to {}.").format(user.email)) + if password_was_set: + messages.info(request, _("Welcome email sent to {}.").format(user.email)) else: messages.info(request, _("Password setup link sent to {}.").format(user.email)) except Exception as e: diff --git a/apps/integrations/api_serializers.py b/apps/integrations/api_serializers.py index d46f58e..183301f 100644 --- a/apps/integrations/api_serializers.py +++ b/apps/integrations/api_serializers.py @@ -363,7 +363,6 @@ class ExternalAppreciationRetrieveSerializer(serializers.ModelSerializer): """Read-only serializer for retrieving an appreciation.""" hospital_name = serializers.CharField(source="hospital.name", read_only=True) - reference_number = serializers.SerializerMethodField() class Meta: model = Appreciation @@ -379,11 +378,6 @@ class ExternalAppreciationRetrieveSerializer(serializers.ModelSerializer): ] read_only_fields = fields - def get_reference_number(self, obj): - if obj.metadata and "reference_number" in obj.metadata: - return obj.metadata["reference_number"] - return None - # --------------------------------------------------------------------------- # Suggestion (Feedback) @@ -433,7 +427,6 @@ class ExternalSuggestionRetrieveSerializer(serializers.ModelSerializer): """Read-only serializer for retrieving a suggestion.""" hospital_name = serializers.CharField(source="hospital.name", read_only=True) - reference_number = serializers.SerializerMethodField() class Meta: model = Feedback @@ -455,11 +448,6 @@ class ExternalSuggestionRetrieveSerializer(serializers.ModelSerializer): ] read_only_fields = fields - def get_reference_number(self, obj): - if obj.metadata and "reference_number" in obj.metadata: - return obj.metadata["reference_number"] - return None - class ExternalComplaintSatisfactionSerializer(serializers.Serializer): satisfaction = serializers.ChoiceField( diff --git a/apps/integrations/api_views.py b/apps/integrations/api_views.py index b0fbc08..472db84 100644 --- a/apps/integrations/api_views.py +++ b/apps/integrations/api_views.py @@ -155,7 +155,7 @@ class ExternalComplaintCreateView(ExternalAPIBase, APIView): status=status.HTTP_403_FORBIDDEN, ) - # Reference number generated by Complaint.save() (unified CMP-YYYYMM-HOSP-NNNN) + # Reference number generated by Complaint.save() (unified CMP-YYYYMM-NNNN) complaint = Complaint.objects.create( patient=None, @@ -187,6 +187,8 @@ class ExternalComplaintCreateView(ExternalAPIBase, APIView): message="Complaint submitted via external API.", ) + reference_number = complaint.reference_number + # Trigger background tasks try: from apps.complaints.tasks import analyze_complaint_with_ai, notify_staff_new_item @@ -596,13 +598,6 @@ class ExternalAppreciationCreateView(ExternalAPIBase, APIView): status=status.HTTP_403_FORBIDDEN, ) - import uuid - from datetime import datetime - - today = datetime.now().strftime("%Y%m%d") - random_suffix = str(uuid.uuid4().int)[:6] - reference_number = f"APR-{today}-{random_suffix}" - appreciation = Appreciation.objects.create( hospital=data["hospital"], message_en=data["message"], @@ -613,7 +608,6 @@ class ExternalAppreciationCreateView(ExternalAPIBase, APIView): visibility=AppreciationVisibility.PUBLIC, metadata={ "source": "external_api", - "reference_number": reference_number, "submitted_by_name": data["contact_name"], "submitted_by_phone": data["contact_phone"], }, @@ -636,7 +630,7 @@ class ExternalAppreciationCreateView(ExternalAPIBase, APIView): return Response( { "success": True, - "reference_number": reference_number, + "reference_number": appreciation.reference_number, "status": appreciation.status, }, status=status.HTTP_201_CREATED, @@ -646,22 +640,22 @@ class ExternalAppreciationCreateView(ExternalAPIBase, APIView): class ExternalAppreciationRetrieveView(ExternalAPIBase, APIView): """ GET /api/v1/external/appreciations/ - GET /api/v1/external/appreciations// + GET /api/v1/external/appreciations// """ entity_name = "appreciations" - def get(self, request, pk=None): + def get(self, request, reference_number=None): access_err = self.check_entity_access() if access_err: return access_err - if pk: - return self._retrieve(request, pk) + if reference_number: + return self._retrieve(request, reference_number) return self._list(request) - def _retrieve(self, request, pk): - qs = Appreciation.all_objects.filter(pk=pk) + def _retrieve(self, request, reference_number): + qs = Appreciation.all_objects.filter(reference_number=reference_number) qs = self.filter_queryset_by_hospital(qs) appreciation = qs.first() if not appreciation: @@ -734,13 +728,6 @@ class ExternalSuggestionCreateView(ExternalAPIBase, APIView): "other": FeedbackCategory.OTHER, } - import uuid as _uuid - from datetime import datetime - - today = datetime.now().strftime("%Y%m%d") - random_suffix = str(_uuid.uuid4().int)[:6] - reference_number = f"SG-{today}-{random_suffix}" - title = data.get("title") or data["message"][:100] feedback = Feedback.objects.create( @@ -756,7 +743,6 @@ class ExternalSuggestionCreateView(ExternalAPIBase, APIView): status=FeedbackStatus.SUBMITTED, metadata={ "source": "external_api", - "reference_number": reference_number, "suggestion_area": data.get("category", "general"), }, ) @@ -780,7 +766,7 @@ class ExternalSuggestionCreateView(ExternalAPIBase, APIView): return Response( { "success": True, - "reference_number": reference_number, + "reference_number": feedback.reference_number, "status": feedback.status, }, status=status.HTTP_201_CREATED, @@ -790,22 +776,22 @@ class ExternalSuggestionCreateView(ExternalAPIBase, APIView): class ExternalSuggestionRetrieveView(ExternalAPIBase, APIView): """ GET /api/v1/external/suggestions/ - GET /api/v1/external/suggestions// + GET /api/v1/external/suggestions// """ entity_name = "suggestions" - def get(self, request, pk=None): + def get(self, request, reference_number=None): access_err = self.check_entity_access() if access_err: return access_err - if pk: - return self._retrieve(request, pk) + if reference_number: + return self._retrieve(request, reference_number) return self._list(request) - def _retrieve(self, request, pk): - qs = Feedback.all_objects.filter(pk=pk) + def _retrieve(self, request, reference_number): + qs = Feedback.all_objects.filter(reference_number=reference_number) qs = self.filter_queryset_by_hospital(qs) feedback = qs.first() if not feedback: diff --git a/apps/integrations/urls_external.py b/apps/integrations/urls_external.py index b6650f4..f192f01 100644 --- a/apps/integrations/urls_external.py +++ b/apps/integrations/urls_external.py @@ -119,7 +119,7 @@ urlpatterns = [ name="external-appreciation-list", ), path( - "appreciations//", + "appreciations//", ExternalAppreciationRetrieveView.as_view(), name="external-appreciation-detail", ), @@ -135,7 +135,7 @@ urlpatterns = [ name="external-suggestion-list", ), path( - "suggestions//", + "suggestions//", ExternalSuggestionRetrieveView.as_view(), name="external-suggestion-detail", ), diff --git a/apps/notifications/services.py b/apps/notifications/services.py index 5052b55..b09c593 100644 --- a/apps/notifications/services.py +++ b/apps/notifications/services.py @@ -312,6 +312,18 @@ class NotificationService: Returns: NotificationLog instance """ + # Auto-wrap plain-text messages in the branded base email template so + # every notification has the hospital logo, card, and footer. Callers + # that already supply html_message are left untouched. + if html_message is None and message: + from django.template.loader import render_to_string as _render_to_string + + content_html = "

" + message.replace("\n\n", "

").replace("\n", "
\n") + "

" + html_message = _render_to_string( + "emails/simple_email.html", + {"subject": subject, "content_html": content_html}, + ) + from apps.accounts.models import User # Check if Email API is enabled and use it (simulator or external API) diff --git a/apps/notifications/tests_email.py b/apps/notifications/tests_email.py new file mode 100644 index 0000000..fe94f34 --- /dev/null +++ b/apps/notifications/tests_email.py @@ -0,0 +1,129 @@ +""" +Tests for the email reminder template consistency fixes: + - Part D: NotificationService.send_email auto-wraps plain-text in branded template + - Part A: dept-champion notification uses email= (not recipient=) kwarg + - Part B/C: branded reminder templates render with the expected context +""" +from unittest.mock import patch + +from django.template.loader import render_to_string +from django.test import TestCase + +from apps.notifications.services import NotificationService + + +class AutoWrapBrandedTemplateTests(TestCase): + """Part D — plain-text messages are auto-wrapped in the branded template.""" + + @patch("apps.notifications.services.send_mail") + def test_plain_text_gets_branded_html(self, mock_send_mail): + NotificationService.send_email( + email="patient@example.com", + subject="Test Subject", + message="Line one.\n\nLine two.\nLine three.", + ) + # send_mail was called with an html_message + _, kwargs = mock_send_mail.call_args + self.assertIsNotNone(kwargs.get("html_message")) + html = kwargs["html_message"] + # The branded template contributes these structural markers + self.assertIn("email-container", html) # the 600px card from base_email_template + self.assertIn("", html) # single newline ->
+ self.assertIn("

", html) # double newline -> new paragraph + + @patch("apps.notifications.services.send_mail") + def test_explicit_html_message_is_not_overwritten(self, mock_send_mail): + """Callers that already provide html_message keep it as-is (no double-wrapping).""" + custom_html = "

My custom layout
" + NotificationService.send_email( + email="patient@example.com", + subject="Test", + message="Plain text fallback", + html_message=custom_html, + ) + _, kwargs = mock_send_mail.call_args + self.assertEqual(kwargs["html_message"], custom_html) + + +class DeptChampionNotificationKwargTests(TestCase): + """Part A — the recipient= bug is fixed (send_email uses email= kwarg). + + We verify indirectly: send_email no longer receives an unexpected 'recipient' + kwarg, which used to cause a TypeError swallowed by try/except. + """ + + @patch("apps.notifications.services.send_mail") + def test_send_email_signature_rejects_recipient_kwarg(self, mock_mail): + """The send_email method must not accept 'recipient' as a kwarg (the bug). + + Python validates kwargs at call time, so TypeError fires before the + body runs — we must NOT mock send_email itself. + """ + with self.assertRaises(TypeError): + NotificationService.send_email( + recipient="someone@example.com", # wrong kwarg — should TypeError + subject="Test", + message="Test", + ) + + +class BrandedReminderTemplateTests(TestCase): + """Parts B & C — the reminder templates extend the base and render branded HTML.""" + + def _assert_branded(self, html): + """Structural markers contributed by base_email_template.html.""" + self.assertIn("/", - "host": ["{{base_url}}"], - "path": ["api", "v1", "external", "appreciations", "", ""] + "raw": "{{base_url}}/api/v1/external/appreciations//", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "appreciations", "", ""] } }, "response": [] @@ -669,7 +669,7 @@ "name": "Created", "status": "Created", "code": 201, - "body": "{\n \"success\": true,\n \"reference_number\": \"SG-20260609-316316\",\n \"status\": \"submitted\"\n}" + "body": "{\n \"success\": true,\n \"reference_number\": \"SGT-202607-0001\",\n \"status\": \"submitted\"\n}" } ] }, @@ -714,9 +714,9 @@ } ], "url": { - "raw": "{{base_url}}/api/v1/external/suggestions//", - "host": ["{{base_url}}"], - "path": ["api", "v1", "external", "suggestions", "", ""] + "raw": "{{base_url}}/api/v1/external/suggestions//", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "suggestions", "", ""] } }, "response": [] diff --git a/docs/external-api.md b/docs/external-api.md index 8b6c838..067166f 100644 --- a/docs/external-api.md +++ b/docs/external-api.md @@ -452,7 +452,7 @@ POST /api/v1/external/appreciations/ ```json { "success": true, - "reference_number": "APR-20260609-239270", + "reference_number": "APR-202607-0001", "status": "draft" } ``` @@ -461,7 +461,7 @@ POST /api/v1/external/appreciations/ ``` GET /api/v1/external/appreciations/list/ -GET /api/v1/external/appreciations// +GET /api/v1/external/appreciations// ``` **Retrieve Response:** @@ -469,7 +469,7 @@ GET /api/v1/external/appreciations// ```json { "id": "uuid", - "reference_number": "APR-20260609-239270", + "reference_number": "APR-202607-0001", "message_en": "Dr. Ahmed was incredibly kind and thorough.", "status": "draft", "hospital_name": "Al Nuzha", @@ -506,7 +506,7 @@ POST /api/v1/external/suggestions/ ```json { "success": true, - "reference_number": "SG-20260609-316316", + "reference_number": "SGT-202607-0001", "status": "submitted" } ``` @@ -515,7 +515,7 @@ POST /api/v1/external/suggestions/ ``` GET /api/v1/external/suggestions/list/ -GET /api/v1/external/suggestions// +GET /api/v1/external/suggestions// ``` **Retrieve Response:** @@ -523,7 +523,7 @@ GET /api/v1/external/suggestions// ```json { "id": "uuid", - "reference_number": "SG-20260609-316316", + "reference_number": "SGT-202607-0001", "title": "Digital queue system", "message": "Please implement a digital queue management system.", "category": "technology", @@ -669,5 +669,5 @@ All list endpoints return paginated results: | Complaint | `CMP-` | `CMP-YYYYMMDD-XXXXXX` | | Inquiry | `INQ-` | `INQ-YYYYMMDD-XXXXXX` | | Observation | `OBS-` | `OBS-XXXXXX` (auto-generated by model) | -| Appreciation | `APR-` | `APR-YYYYMMDD-XXXXXX` | -| Suggestion | `SG-` | `SG-YYYYMMDD-XXXXXX` | +| Appreciation | `APR-` | `APR-YYYYMM-NNNN` | +| Suggestion | `SGT-` | `SGT-YYYYMM-NNNN` | diff --git a/docs/workflows/appreciations.md b/docs/workflows/appreciations.md index 4cb96b0..186e4a2 100644 --- a/docs/workflows/appreciations.md +++ b/docs/workflows/appreciations.md @@ -39,7 +39,7 @@ So: **a recognition engine whose originating actors can be (a) PX/hospital staff **(b) Public portal** (`ui_views.py:1196`): Inputs `contact_name`, `contact_phone`, `message`, `hospital`, optional `staff_name`/`department`/`section`. Required: name, phone, message, hospital (`ui_views.py:1224`). Creates with `status=DRAFT`, `visibility=PUBLIC`, `is_anonymous=False`, `category=None`, metadata `{source:"public_form", ...}` (`ui_views.py:1239`). Fires `notify_staff_new_item.delay("appreciation", ...)` (`ui_views.py:1258`). Returns `reference_number`. -**(c) External API** (`integrations/api_views.py:606`): Creates DRAFT, visibility PUBLIC, metadata `{source:"external_api", ...}`. Builds its own `APR-YYYYMMDD-` (`api_views.py:602`) — informational, since `save()` also generates the canonical `reference_number`. Fires `notify_staff_new_item`. +**(c) External API** (`integrations/api_views.py:606`): Creates DRAFT, visibility PUBLIC, metadata `{source:"external_api", ...}`. Returns the canonical `reference_number` generated by `save()` (`APR-YYYYMM-NNNN`); the same value is used to retrieve the record via `GET /api/v1/external/appreciations//`. Fires `notify_staff_new_item`. **(d) REST API create** (`views.py:143`, serializer `serializers.py:132`): Required: `recipient_type` (`['user','staff']`), `recipient_id` (UUID), `message_en`, `hospital_id`. Optional: `category_id`, `message_ar`, `visibility` (default PRIVATE), `is_anonymous`, `department_id`. Validation: recipient must belong to hospital; category hospital-compatible. `AppreciationViewSet.create` resolves a `User`/`Staff` via `ContentType`, creates the record (status DRAFT), then **immediately calls `appreciation.send()`** (`views.py:198`). diff --git a/docs/workflows/suggestions.md b/docs/workflows/suggestions.md index 4ebf475..97b4968 100644 --- a/docs/workflows/suggestions.md +++ b/docs/workflows/suggestions.md @@ -37,7 +37,7 @@ Post-save (`views.py:992-1024`): `patient=None` at creation; linked **asynchrono ### 2.3 Channel C — External API (machine-to-machine) - View: `ExternalSuggestionCreateView` (`apps/integrations/api_views.py:700`), `POST /api/v1/external/suggestions/`. -- Auth via API key; hospital must match scope. Generates its own ephemeral `SG-{YYYYMMDD}-{random6}` in `metadata` (`api_views.py:740`) — **not** the canonical `reference_number` column (which `save()` still populates). Same post-save tasks (`api_views.py:765`). +- Auth via API key; hospital must match scope. Returns the canonical `reference_number` generated by `save()` (`SGT-YYYYMM-NNNN`); the same value is used to retrieve the record via `GET /api/v1/external/suggestions//`. Same post-save tasks (`api_views.py:765`). ### 2.4 Channel D — PX Source-User Portal - View: `source_user_create_suggestion` (`apps/px_sources/ui_views.py:1387`); `@login_required`; guarded by `source_user.can_create_suggestions`. @@ -302,7 +302,7 @@ Raw-unit model of the IT-export pipeline. Imported in monthly batches via `Comme ## Appendix — Flagged gaps / ambiguities 1. **Reopen unreachable via UI** after closure (Quick Actions hidden when `closed`, `feedback_detail.html:299`). CLOSED→REOPENED allowed by map but no staff UI button. -2. **`reference_number` divergence** — `save()` generates canonical `SGT-YYYYMM-NNNN`; public endpoint returns different `SG-{uuid[:8]}` (`views.py:1026`); external API generates `SG-{YYYYMMDD}-{random6}` in metadata only. Public cannot track by real reference. +2. **`reference_number` divergence (external API resolved)** — `save()` generates canonical `SGT-YYYYMM-NNNN`; the external API now returns and retrieves by this canonical reference. **Public portal still diverges**: the public submit endpoint returns a different `SG-{uuid[:8]}` (`views.py:1026`), so the public cannot track by the real reference via the web portal. 3. **`FeedbackForm` unused for creation** — `feedback_create` builds manually, bypassing form `clean()`. 4. **Transitions unenforced at DB/model/admin** — only `feedback_change_status` enforces. 5. **No outbound communication to suggester** by design. diff --git a/templates/complaints/complaint_detail.html b/templates/complaints/complaint_detail.html index 04c1365..073f89b 100644 --- a/templates/complaints/complaint_detail.html +++ b/templates/complaints/complaint_detail.html @@ -134,7 +134,7 @@ class="text-slate hover:text-navy px-3 py-2 text-sm font-semibold flex items-center gap-2 border rounded-lg hover:bg-light transition"> {% trans "PDF View" %} {% endcomment %} - {% if can_edit and complaint.is_active_status and complaint.activated_at and complaint.assigned_to == current_user %} + {% if can_admin or can_edit and complaint.is_active_status and complaint.activated_at and complaint.assigned_to == current_user %} @@ -390,25 +390,25 @@ - - - {% comment %} {% endcomment %} {% if can_admin %} - {% endif %} - - - {% comment %} {% endcomment %} - @@ -598,7 +598,7 @@ {% trans "Pending Approval" %}

{% trans "OVR escalation requested. Please review and approve or reject." %}

- {% if request.user.is_px_admin or request.user.is_hospital_admin or request.user.is_px_management %} + {% if request.user.is_px_admin or request.user.is_hospital_admin or complaint.assigned_to == current_user %}
{% csrf_token %} @@ -834,10 +834,10 @@ {% csrf_token %}
@@ -876,6 +876,12 @@ + {% if complaint.sent_to_department and not workflow_steps.department_responded %} + + {% endif %} {% endif %} {% else %} @@ -1251,6 +1257,43 @@
+ + + - {% endif %} - + +
+

+ + {% trans "Create Staff Record" %} +

+
+ +
+ +

{% trans "Creates a staff directory entry linked to this user. Employee ID is required." %}

+
+
+ +
+ {% endif %}

@@ -358,7 +390,7 @@

- {% trans "Leave password blank and click Generate for a random password. The credentials will be sent to the user's email." %} + {% trans "Set a password now, or leave blank to send a password setup link via email." %}

@@ -419,7 +451,7 @@
  • - {% trans "A password reset email will be sent automatically" %} + {% trans "Set a password or leave blank to email a setup link" %}
  • @@ -461,35 +493,63 @@ document.addEventListener('DOMContentLoaded', function() { // ===== Department loading ===== function loadDepartments(hospitalId, selectedDept) { if (!departmentSelect) return; + var ts = departmentSelect.tomselect; + var placeholderText = '{% trans "Select Department" %}'; + if (!hospitalId) { - departmentSelect.innerHTML = ''; + if (ts) { + ts.clear(); + ts.clearOptions(); + ts.addOption({value: '', text: placeholderText}); + } else { + departmentSelect.innerHTML = ''; + } return; } - departmentSelect.innerHTML = ''; + if (ts) { ts.clear(); ts.clearOptions(); ts.addOption({value: '', text: '{% trans "Loading..." %}'}); } + else { departmentSelect.innerHTML = ''; } fetch('/organizations/api/departments/?hospital=' + hospitalId) .then(response => response.json()) .then(data => { const results = data.results || data; - departmentSelect.innerHTML = ''; - results.forEach(dept => { - const option = document.createElement('option'); - option.value = dept.id; - option.textContent = {% if LANG == 'ar' %}dept.name_ar || dept.name{% else %}dept.name{% endif %}; - departmentSelect.appendChild(option); - }); + if (ts) { + ts.clear(); + ts.clearOptions(); + ts.addOption({value: '', text: placeholderText}); + results.forEach(dept => { + ts.addOption({ + value: dept.id, + text: {% if LANG == 'ar' %}dept.name_ar || dept.name{% else %}dept.name{% endif %} + }); + }); + } else { + departmentSelect.innerHTML = ''; + results.forEach(dept => { + const option = document.createElement('option'); + option.value = dept.id; + option.textContent = {% if LANG == 'ar' %}dept.name_ar || dept.name{% else %}dept.name{% endif %}; + departmentSelect.appendChild(option); + }); + } - if (selectedDept) { - departmentSelect.value = selectedDept; - } else if ('{{ form.department.value }}') { - departmentSelect.value = '{{ form.department.value }}'; + var selectVal = selectedDept || ('{{ form.department.value }}' || ''); + if (selectVal) { + if (ts) { ts.setValue(selectVal, true); } + else { departmentSelect.value = selectVal; } } }) .catch(error => { console.error('Error loading departments:', error); - departmentSelect.innerHTML = ''; + if (ts) { + ts.clear(); + ts.clearOptions(); + ts.addOption({value: '', text: '{% trans "Error loading departments" %}'}); + } else { + departmentSelect.innerHTML = ''; + } }); } @@ -510,6 +570,38 @@ document.addEventListener('DOMContentLoaded', function() { const selectedStaffName = document.getElementById('selectedStaffName'); const staffSearchInputWrap = document.getElementById('staffSearchInput-wrap'); + // ===== Create Staff Toggle ===== + const createStaffCheckbox = document.getElementById('create_staff'); + const createStaffFields = document.getElementById('createStaffFields'); + const staffSearchSection = staffSearchInput ? staffSearchInput.closest('.form-section') : null; + + if (createStaffCheckbox) { + createStaffCheckbox.addEventListener('change', function() { + if (this.checked) { + createStaffFields.classList.remove('hidden'); + if (staffSearchSection) staffSearchSection.classList.add('hidden'); + if (staffIdInput) staffIdInput.value = ''; + } else { + createStaffFields.classList.add('hidden'); + if (staffSearchSection) staffSearchSection.classList.remove('hidden'); + } + lucide.createIcons(); + }); + } + + function disableCreateStaff() { + if (createStaffCheckbox) { + createStaffCheckbox.checked = false; + createStaffFields.classList.add('hidden'); + } + } + + function enableCreateStaff() { + if (staffSearchSection && createStaffCheckbox && createStaffCheckbox.checked) { + staffSearchSection.classList.add('hidden'); + } + } + if (staffSearchInput) { let debounceTimer; @@ -603,6 +695,8 @@ document.addEventListener('DOMContentLoaded', function() { staffSearchInputWrap.classList.add('hidden'); selectedStaffChip.classList.remove('hidden'); + disableCreateStaff(); + // Auto-fill fields const fields = { 'id_first_name': data.first_name, @@ -636,8 +730,34 @@ document.addEventListener('DOMContentLoaded', function() { staffSearchInputWrap.classList.remove('hidden'); staffSearchInput.value = ''; staffSearchResults.classList.remove('active'); + if (createStaffCheckbox && staffSearchSection) { + staffSearchSection.classList.remove('hidden'); + } }; + // ===== Form Validation ===== + const userForm = document.getElementById('userForm'); + if (userForm && createStaffCheckbox) { + userForm.addEventListener('submit', function(e) { + if (createStaffCheckbox.checked) { + var empId = document.getElementById('id_employee_id'); + var jobTitle = document.getElementById('staff_job_title'); + var staffType = document.getElementById('staff_type'); + var missing = []; + + if (!empId || !empId.value.trim()) missing.push('{% trans "Employee ID" %}'); + if (!jobTitle || !jobTitle.value.trim()) missing.push('{% trans "Job Title" %}'); + if (!staffType || !staffType.value) missing.push('{% trans "Staff Type" %}'); + + if (missing.length) { + e.preventDefault(); + alert('{% trans "Please fill in the following required fields:" %} ' + missing.join(', ')); + return false; + } + } + }); + } + if (typeof lucide !== 'undefined') { lucide.createIcons(); } diff --git a/templates/emails/inquiry_sla_reminder.html b/templates/emails/inquiry_sla_reminder.html new file mode 100644 index 0000000..8241a0d --- /dev/null +++ b/templates/emails/inquiry_sla_reminder.html @@ -0,0 +1,40 @@ +{% extends 'emails/base_email_template.html' %} +{% load i18n %} + +{% block title %}{% trans "SLA Reminder - Inquiry" %}{% endblock %} +{% block preheader %}{% trans "An inquiry assigned to you is approaching its SLA deadline." %}{% endblock %} + +{% block content %} +

    + {% trans "Dear" %} {{ recipient_name|default:'Colleague' }}, +

    + +

    + {% trans "This is an automated reminder that an inquiry assigned to you is approaching its SLA deadline. Please review and take appropriate action." %} +

    + +

    {% trans "Reference:" %} {{ inquiry.reference_number }}

    +

    {% trans "Subject:" %} {{ inquiry.subject }}

    +

    {% trans "Due Date:" %} {{ inquiry.due_at|date:"F d, Y H:i" }}

    +

    {% trans "Time Remaining:" %} {{ hours_remaining }} {% trans "hours" %}

    + +{% if inquiry_url %} + + + + +
    + + {% trans "View Inquiry" %} + +
    +{% endif %} + +

    + تذكير اتفاقية مستوى الخدمة - استفسار {{ inquiry.reference_number }} +

    +

    + تاريخ الاستحقاق: {{ inquiry.due_at|date:"Y F d H:i" }} - الوقت المتبقي: {{ hours_remaining }} ساعة +

    +{% endblock %} diff --git a/templates/emails/inquiry_sla_second_reminder.html b/templates/emails/inquiry_sla_second_reminder.html new file mode 100644 index 0000000..748b788 --- /dev/null +++ b/templates/emails/inquiry_sla_second_reminder.html @@ -0,0 +1,40 @@ +{% extends 'emails/base_email_template.html' %} +{% load i18n %} + +{% block title %}{% trans "URGENT: SLA Reminder - Inquiry" %}{% endblock %} +{% block preheader %}{% trans "An inquiry assigned to you is about to breach its SLA deadline." %}{% endblock %} + +{% block content %} +

    + {% trans "Dear" %} {{ recipient_name|default:'Colleague' }}, +

    + +

    + {% trans "URGENT: An inquiry assigned to you is about to breach its SLA deadline and requires your immediate attention." %} +

    + +

    {% trans "Reference:" %} {{ inquiry.reference_number }}

    +

    {% trans "Subject:" %} {{ inquiry.subject }}

    +

    {% trans "Due Date:" %} {{ inquiry.due_at|date:"F d, Y H:i" }}

    +

    {% trans "Time Remaining:" %} {{ hours_remaining }} {% trans "hours" %}

    + +{% if inquiry_url %} + + + + +
    + + {% trans "View Inquiry" %} + +
    +{% endif %} + +

    + عاجل: تذكير اتفاقية مستوى الخدمة - استفسار {{ inquiry.reference_number }} +

    +

    + تاريخ الاستحقاق: {{ inquiry.due_at|date:"Y F d H:i" }} - الوقت المتبقي: {{ hours_remaining }} ساعة +

    +{% endblock %} diff --git a/templates/layouts/partials/sidebar.html b/templates/layouts/partials/sidebar.html index afdc7ef..2090c0d 100644 --- a/templates/layouts/partials/sidebar.html +++ b/templates/layouts/partials/sidebar.html @@ -149,7 +149,7 @@ - {% if user.is_champion and not user.is_px_admin and not user.is_hospital_admin and not user.is_department_manager %} + {% if user.is_champion and not user.is_px_admin and not user.is_hospital_admin and not user.is_department_manager and not user.is_px_employee and not user.is_px_management %} {% if user.department %} @@ -324,7 +324,7 @@ {% if not user.is_basic_staff and not user.is_department_manager and not user.is_director %} {% if not user.source_user_profile %} - {% if user.is_champion and user.department %} + {% if user.is_champion and user.department and not user.is_px_employee and not user.is_px_management %}