HH/qwen-code-export-2026-04-14T14-41-09-777Z.md
2026-04-19 10:53:12 +03:00

11759 lines
536 KiB
Markdown

# Chat Session Export
- **Session ID**: `3e2a1965-502e-41ef-ab15-f68735ae82f9`
- **Start Time**: 2026-04-14T12:06:59.629Z
- **Exported**: 2026-04-14T14:41:09.771Z
- **Working Directory**: `/home/ismail/projects/HH`
- **Git Repository**: marwan/HH
- **Git Branch**: `main`
- **Model**: coder-model
- **Prompt Count**: 3
- **Total Tokens**: 4148669
- **Context Window Size**: 1000000
- **Context Usage**: 7.2%
- **Files Written**: 14
- **Lines Added**: 1813
- **Lines Removed**: 3
<details>
<summary><strong>Unique Files Referenced (14)</strong></summary>
- `/home/ismail/projects/HH/apps/accounts/models.py`
- `/home/ismail/projects/HH/apps/accounts/permissions.py`
- `/home/ismail/projects/HH/templates/layouts/partials/sidebar.html`
- `/home/ismail/projects/HH/config/settings/base.py`
- `/home/ismail/projects/HH/apps/executive_summary/models.py`
- `/home/ismail/projects/HH/apps/executive_summary/templates/executive/dashboard.html`
- `/home/ismail/projects/HH/apps/executive_summary/templates/executive/partials/variance_badge.html`
- `/home/ismail/projects/HH/apps/executive_summary/templates/executive/insights.html`
- `/home/ismail/projects/HH/apps/executive_summary/templates/executive/qa.html`
- `/home/ismail/projects/HH/apps/executive_summary/admin.py`
- `/home/ismail/projects/HH/apps/executive_summary/tasks.py`
- `/home/ismail/projects/HH/config/urls.py`
- `/home/ismail/projects/HH/config/celery.py`
- `/home/ismail/projects/HH/apps/executive_summary/apps.py`
</details>
---
## User
i want to add Executive Summary pages and role for top management , lets make the best good of AI in this
i want to add Executive Summary pages and role for top management , lets make the best good of AI in this
## Assistant
I need to explore the codebase first to understand the project structure, existing roles/permissions, and current pages before I can plan the Executive Summary feature.
## Assistant
I'll help you design an Executive Summary feature with AI-powered insights for top management. Let me first explore your codebase to understand the current structure and capabilities.
### Tool: Agent: Explore project structure
**Status**: completed
**Input:**
```json
{
"description": "Explore project structure",
"prompt": "Explore this codebase thoroughly to understand:\n1. What type of application is this (Django, Flask, etc.)?\n2. What is the current user role/permission system?\n3. What are the existing page structures and navigation?\n4. Is there any existing AI/ML integration?\n5. What data models exist, especially related to complaints, analytics, or reporting?\n6. What's the frontend stack (templates, React, etc.)?\n7. Are there any existing dashboards or summary pages?\n\nLook at:\n- manage.py, requirements.txt, pyproject.toml for framework info\n- Any models.py files\n- Any views.py or API files\n- Templates or frontend files\n- Settings/configuration files\n- Any existing role/permission system\n\nProvide a comprehensive summary of the architecture and existing features.",
"subagent_type": "Explore"
}
```
**Output:**
```
Now I have a comprehensive understanding of the codebase. Here is the detailed summary:
---
## PX360 - Comprehensive Architecture Summary
### 1. Application Type
This is a **Django 6.0** (upgraded from Django 5.0) enterprise application called **PX360 - Patient Experience 360 Management System** built for **AlHammadi Group** (a healthcare organization in Saudi Arabia). It is a multi-tenant, event-driven healthcare patient experience management system.
### 2. Tech Stack
**Backend:**
- Django 6.0.1 with Django REST Framework 3.16.1
- JWT authentication (djangorestframework-simplejwt)
- Celery 5.6.2 + Redis 7.1.0 for async task queue
- Celery Beat for scheduled tasks
- PostgreSQL 15 database
- psycopg2-binary for database connectivity
**Frontend:**
- **Server-rendered Django templates** with HTML (no React/SPA)
- **TailwindCSS 3.4** for styling (compiled via npm)
- **HTMX** for dynamic updates without full page reloads
- **ApexCharts** for data visualization
- **Lucide Icons** for UI icons
- Bootstrap-to-Tailwind migration helper script in place
- Supports RTL (right-to-left) for Arabic language
**Infrastructure:**
- Docker + Docker Compose (web, db, redis, celery, celery-beat)
- Gunicorn WSGI server
- WhiteNoise for static file serving
- OpenRouter AI API integration (LiteLLM)
---
### 3. User Role/Permission System (RBAC)
The system uses **Django Groups** mapped to a custom `Role` model. The `User` model extends `AbstractUser` with UUID primary key and email-based authentication.
**Predefined Roles** (defined in `/home/ismail/projects/HH/apps/accounts/models.py`):
- **PX Admin** - Full system access, can see all hospitals
- **Hospital Admin** - Hospital-level access
- **Department Manager** - Department-level access
- **PX Staff** - Action management capabilities
- **Physician** - Limited access
- **Nurse** - Limited access
- **Staff** - Limited access
- **Viewer** - Read-only access
- **PX Source User** - Source-specific data access (special restricted role with shorter sessions)
**Permission Classes** (in `/home/ismail/projects/HH/apps/accounts/permissions.py`):
- `IsPXAdmin`, `IsHospitalAdmin`, `IsDepartmentManager` - DRF permission classes
- `IsPXAdminOrHospitalAdmin`, `IsPXAdminOrReadOnly`, `IsOwnerOrPXAdmin`
- `IsSourceUser`, `IsSourceUserOrAdmin`, `IsSourceUserOwnData`
- `CanAccessHospitalData`, `CanAccessDepartmentData`
- Onboarding-specific permissions for provisional users
**Key features:**
- Multi-tenancy via `TenantMiddleware` - users see only their hospital's data
- Source user session restriction middleware (shorter sessions)
- User model has helper methods: `is_px_admin()`, `is_hospital_admin()`, etc.
- Role hierarchy levels for escalation logic
---
### 4. Existing Page Structure and Navigation
The sidebar navigation (in `/home/ismail/projects/HH/templates/layouts/partials/sidebar.html`) contains:
**Top Level:**
- Dashboard
- Command Center
- Performance (submenu: Admin Evaluation, Employee Evaluation) - Admin only
- My Source Dashboard - Source Users only
- Complaints (with count badge)
- Inquiries
- Observations
- QI Projects (submenu: All Projects, Create Project, Templates)
- Surveys (submenu: Survey List, Templates, Create Survey, Manual Send, HIS Import, Mapping)
- Staff (submenu: All Staff, Hierarchy)
- Patients
- Social Media - PX Admin only
- Physicians (submenu: Dashboard, All Physicians, Leaderboard, Fetch Ratings, Import Ratings)
- Call Center
- AI Engine
- Actions (PX Action Center)
- Reports
- RCA (Root Cause Analysis)
- Standards
- References
- Settings (submenu: Notifications, Hospitals, Departments, Acknowledgements, Roles & Permissions, Integrations)
- User profile / Logout
**Base Templates:**
- `/home/ismail/projects/HH/templates/layouts/base.html` - Main authenticated layout with sidebar
- `/home/ismail/projects/HH/templates/layouts/public_base.html` - Public-facing pages
- `/home/ismail/projects/HH/templates/layouts/source_user_base.html` - Source user specific layout
---
### 5. AI/ML Integration
The system has an **AI Engine** app (`/home/ismail/projects/HH/apps/ai_engine/`) with:
**Current Implementation:**
- **Keyword-based sentiment analysis** (stub implementation) - supports English and Arabic
- Sentiment detection: positive, neutral, negative with scores (-1 to 1)
- Keyword extraction from text
- Entity recognition (email, phone pattern matching)
- Emotion detection (joy, anger, sadness, fear, surprise)
- Language auto-detection (English vs Arabic)
**Models:**
- `SentimentResult` - stores analysis results linked to any model via GenericForeignKey
**API Endpoints:**
- `POST /api/ai-engine/analyze/` - Single text analysis
- `POST /api/ai-engine/analyze-batch/` - Batch analysis
- `GET /api/ai-engine/sentiment/{content_type_id}/{object_id}/` - Get sentiment for object
**AI Configuration** (in settings):
- LiteLLM with OpenRouter integration configured
- Model: `google/gemma-3-27b-it:free` (configurable)
- AI comment analysis for surveys
**Where AI is applied:**
- Complaint descriptions generate AI brief summaries (`ai_brief_en`, `ai_brief_ar`)
- Observations have AI analysis stored in metadata (emotion, suggested actions)
- Survey comments analyzed by AI
- Sentiment analysis can be applied to complaints, survey responses, social media mentions, call center notes
---
### 6. Data Models
#### Core Models (`/home/ismail/projects/HH/apps/core/models.py`)
- `TimeStampedModel`, `UUIDModel`, `SoftDeleteModel` - Base abstract models
- `AuditEvent` - Generic audit logging system
- `TenantModel` - Multi-tenant base model
- `PriorityChoices`, `SeverityChoices`, `StatusChoices` - Common enums
#### Organizations (`/home/ismail/projects/HH/apps/organizations/models.py`)
- `Organization` - Top-level organization
- `Hospital` - Hospital/facility with executive leadership (CEO, Medical Director, COO, CFO)
- `Department` - Hierarchical departments within hospitals
- `Staff` - Staff members (physicians, nurses, admin) with section/subsection hierarchy
- `Patient` - Patients with MRN, encrypted national ID
- `Location`, `MainSection`, `SubSection` - Location hierarchy for complaints
- `StaffSection`, `StaffSubsection` - Staff organization hierarchy
#### Accounts (`/home/ismail/projects/HH/apps/accounts/models.py`)
- `User` - Custom user model with UUID PK, email auth, hospital/department links
- `Role` - Predefined roles mapped to Django Groups
- `AcknowledgementCategory`, `AcknowledgementContent`, `AcknowledgementChecklistItem` - Onboarding system
- `UserAcknowledgement` - Digital signature acknowledgements
- `UserProvisionalLog` - Onboarding audit trail
- `SimpleAcknowledgement`, `EmployeeAcknowledgement` - Employee acknowledgement tracking
#### Complaints (`/home/ismail/projects/HH/apps/complaints/models.py`) - 2790+ lines
- `ComplaintCategory` - 4-level SHCT taxonomy (Domain > Category > Subcategory > Classification)
- `Complaint` - Full complaint lifecycle with SLA tracking, AI briefs, resolution tracking, satisfaction
- `ComplaintSLAConfig` - Per-source and per-severity SLA configuration
- `ComplaintUpdate`, `ComplaintAttachment` - Updates and file attachments
- `Inquiry` - Inquiry management
- `ComplaintThreshold`, `EscalationRule` - Threshold and escalation configuration
- `OncallSchedule`, `OncallShift` - On-call scheduling
#### Surveys (`/home/ismail/projects/HH/apps/surveys/models.py`)
- `SurveyTemplate` - Bilingual survey templates with scoring configuration
- `SurveyQuestion` - Questions with types (Rating, NPS, Yes/No, Multiple Choice, Text, Likert)
- `QuestionRoutingRule` - Conditional survey flow logic
- `SurveyInstance` - Actual survey sent to patient/staff with token-based access
- `SurveyResponse` - Individual question responses
- `SurveyTracking` - Engagement tracking (page views, time spent, abandonment)
- `BulkSurveyJob` - Background bulk survey sending
#### Journeys (`/home/ismail/projects/HH/apps/journeys/models.py`)
- `PatientJourneyTemplate` - Journey templates (EMS, Inpatient, OPD, Day Case)
- `PatientJourneyStageTemplate` - Stage definitions with event triggers
- `PatientJourneyInstance` - Actual patient journey instances
- `PatientJourneyStageInstance` - Stage completion tracking
#### Integrations (`/home/ismail/projects/HH/apps/integrations/models.py`)
- `InboundEvent` - External system events (HIS, Lab, Radiology, Pharmacy)
- `IntegrationConfig` - External system API configuration
- `SurveyTemplateMapping` - Maps patient types to survey templates
- `HISPatientVisit`, `HISVisitEvent` - HIS visit data
- `HISEventType` - Unique event types from HIS
- `EventMapping` - External-to-internal event code mapping
#### PX Action Center (`/home/ismail/projects/HH/apps/px_action_center/models.py`)
- `PXAction` - Improvement actions with SLA, approval workflow, escalation
- `PXActionLog` - Action activity timeline
- `PXActionAttachment` - Evidence attachments
- `PXActionSLAConfig` - Per-hospital SLA configuration
- `RoutingRule` - Automatic action routing rules
#### Analytics (`/home/ismail/projects/HH/apps/analytics/models.py`)
- `KPI` - Key Performance Indicator definitions
- `KPIValue` - KPI measurements over time (hourly, daily, weekly, monthly, quarterly, yearly)
#### Observations (`/home/ismail/projects/HH/apps/observations/models.py`)
- `Observation` - Staff-reported issues with anonymous submission support
- `ObservationCategory` - Classification categories
- `ObservationSLAConfig` - SLA per severity
- `ObservationAttachment`, `ObservationNote`, `ObservationStatusLog`
#### Dashboard (`/home/ismail/projects/HH/apps/dashboard/models.py`)
- `EvaluationNote` - Employee evaluation notes
- `ComplaintRequest` - Complaint request tracking
- `ReportCompletion` - Weekly report completion tracking
- `EscalatedComplaintLog` - Escalation timing logs
- `InquiryDetail` - Detailed inquiry tracking
#### Reports (`/home/ismail/projects/HH/apps/reports/models.py`)
- `SavedReport`, `GeneratedReport`, `ReportTemplate`, `DataSource`, `ReportFormat`
#### Other Apps:
- **Call Center** (`apps/callcenter/`) - Call center interactions and ratings
- **Social Media** (`apps/social/`) - Social media monitoring (YouTube, Facebook, Instagram, Twitter, LinkedIn, Google Reviews)
- **Physicians** (`apps/physicians/`) - Physician monthly ratings and leaderboards
- **Projects** (`apps/projects/`) - QI projects with tasks
- **Notifications** (`apps/notifications/`) - SMS, WhatsApp, Email delivery
- **Feedback** (`apps/feedback/`) - General feedback
- **Appreciation** (`apps/appreciation/`) - Appreciation management
- **PX Sources** (`apps/px_sources/`) - Source management (where complaints come from)
- **References** (`apps/references/`) - Reference data management
- **Standards** (`apps/standards/`) - Standards management
- **RCA** (`apps/rca/`) - Root Cause Analysis
- **Simulator** (`apps/simulator/`) - Testing/simulation tools
---
### 7. Existing Dashboards and Summary Pages
**Command Center Dashboard** (`/home/ismail/projects/HH/apps/dashboard/views.py`, `CommandCenterView`):
- Red Alert Banner (critical complaints, overdue items, escalated actions, negative surveys)
- KPI cards: Complaints (current vs previous, variance), Surveys (satisfaction, NPS, response rate), PX Actions (open, in-progress, overdue)
- Charts: Complaints trend, severity donut, department heatmap, NPS trend, actions funnel
- Live feed: Latest high-severity complaints, escalated actions, inquiries, observations, integration events
- Physician leaderboard (top 5 staff by rating)
- Filters: Date range, hospital, department
- API endpoint: `/dashboard/api/` for live data refresh
**My Dashboard** (`my_dashboard` view):
- Personal view of all items assigned to the current user
- Tabbed interface: Complaints, Inquiries, Observations, PX Actions, Tasks, Feedback
- Search, date range, status, priority filtering
- Bulk actions, export (CSV/Excel)
**Admin Evaluation Dashboard** (`admin_evaluation` view):
- Staff performance evaluation
- Department benchmarks
- Staff performance detail views
- Export functionality
**Employee Evaluation** (`employee_evaluation` view):
- PAD Department Weekly Dashboard
- Evaluation notes tracking
- Complaint request monitoring
- Report completion tracking
**Complaint Request Report** (`complaint_request_list` view):
- Step 0 complaint request tracking
- Export functionality
**Physician Ratings Dashboard** (`apps/physicians/`):
- Physician monthly ratings
- Leaderboards
- External rating fetching (Google, etc.)
**Social Media Dashboard** (`apps/social/`):
- Social media mentions monitoring
- Sentiment analysis integration
**Analytics Reports** (via Surveys app):
- Survey analytics and reports
---
### Key URLs
- Main site: `http://localhost:8000/`
- Admin: `http://localhost:8000/admin/`
- API Docs (Swagger): `http://localhost:8000/api/docs/`
- API Docs (ReDoc): `http://localhost:8000/api/redoc/`
- Health Check: `http://localhost:8000/health/`
### Celery Scheduled Tasks
- Process integration events: Every 1 minute
- Check overdue complaints: Every 15 minutes
- Check overdue actions: Every 15 minutes
- Send SLA reminders: Every hour
- Calculate daily KPIs: Daily at 1 AM
- Calculate physician ratings: Monthly on 1st at 2 AM
### Internationalization
- Arabic and English supported throughout
- Bilingual models (name_en/name_ar, title_en/title_ar, etc.)
- RTL-ready UI components
- Language code stored in user preferences
```
## Assistant
Now that I have a comprehensive understanding of the codebase, I should clarify the user's preferences for the Executive Summary feature before presenting the final plan.
## Assistant
Excellent! This is a sophisticated healthcare patient experience management system. Now let me explore the existing dashboard structure and AI capabilities in more detail to design the best Executive Summary feature.
### Tool: AskUserQuestion
**Status**: failed
**Input:**
```json
{
"metadata": {
"source": "remember"
},
"questions": [
{
"header": "Executive Focus",
"options": [
{
"description": "High-level organizational health metrics, trends, and AI-powered insights across all hospitals",
"label": "Overall KPIs & Metrics"
},
{
"description": "Predictive risk scores, early warning indicators, and proactive issue identification using AI",
"label": "Risk & Early Warnings"
},
{
"description": "AI-generated natural language summaries of performance, trends, concerns, and recommendations",
"label": "AI-Generated Summaries"
},
{
"description": "Hospital/department comparisons, leaderboards, and performance rankings",
"label": "Comparative Analytics"
}
],
"question": "What key metrics would top management (executives/leadership) want to see in the Executive Summary? (Select all that apply)",
"multiSelect": true
},
{
"header": "AI Features",
"options": [
{
"description": "Natural language summary of monthly/weekly performance (e.g., 'This month saw 15% improvement in patient satisfaction...')",
"label": "Automated Narrative Reports"
},
{
"description": "AI detects emerging patterns, recurring issues, and suggests root causes",
"label": "Predictive Insights & Anomaly Detection"
},
{
"description": "AI suggests specific actions to improve metrics based on historical data and best practices",
"label": "Recommendation Engine"
},
{
"description": "\"What was the complaint rate for Hospital A last month?\" - AI answers from data",
"label": "Natural Language Q&A"
}
],
"question": "Which AI-powered features would you like to include? (Select all that apply)",
"multiSelect": true
},
{
"header": "Access Level",
"options": [
{
"description": "System-wide view across all hospitals with ability to drill down",
"label": "All Hospitals Overview"
},
{
"description": "See only their own hospital's metrics and comparisons",
"label": "Individual Hospital View"
},
{
"description": "Both - can toggle between system-wide and individual hospital views",
"label": "Both (Recommended)"
},
{
"description": "Custom filters to select specific hospitals, departments, or time periods",
"label": "Custom Selection"
}
],
"question": "Should executives be able to see data across all hospitals, or should it be restricted?",
"multiSelect": false
},
{
"header": "Delivery",
"options": [
{
"description": "Dedicated Executive Summary page with interactive charts and AI insights",
"label": "Web Dashboard (Recommended)"
},
{
"description": "Scheduled PDF reports emailed to executives (daily/weekly/monthly)",
"label": "PDF Report Generation"
},
{
"description": "Mobile-responsive view optimized for executives checking on-the-go",
"label": "Mobile-Friendly View"
},
{
"description": "All of the above",
"label": "All Channels (Recommended)"
}
],
"question": "How should the Executive Summary be delivered?",
"multiSelect": true
}
]
}
```
**Output:**
```
Question 1: "header" must be 12 characters or less.
```
## Assistant
The previous question failed validation due to header length; I need to retry with shorter headers while keeping the same question content.
### Tool: AskUserQuestion: Ask user 4 questions
**Status**: completed
**Input:**
```json
{
"metadata": {
"source": "remember"
},
"questions": [
{
"header": "Key Metrics",
"options": [
{
"description": "High-level organizational health metrics, trends, and AI-powered insights across all hospitals",
"label": "Overall KPIs & Metrics"
},
{
"description": "Predictive risk scores, early warning indicators, and proactive issue identification using AI",
"label": "Risk & Early Warnings"
},
{
"description": "AI-generated natural language summaries of performance, trends, concerns, and recommendations",
"label": "AI-Generated Summaries"
},
{
"description": "Hospital/department comparisons, leaderboards, and performance rankings",
"label": "Comparative Analytics"
}
],
"question": "What key metrics would top management (executives/leadership) want to see in the Executive Summary? (Select all that apply)",
"multiSelect": true
},
{
"header": "AI Features",
"options": [
{
"description": "Natural language summary of monthly/weekly performance (e.g., 'This month saw 15% improvement in patient satisfaction...')",
"label": "Automated Narratives"
},
{
"description": "AI detects emerging patterns, recurring issues, and suggests root causes",
"label": "Predictive Insights"
},
{
"description": "AI suggests specific actions to improve metrics based on historical data and best practices",
"label": "Recommendations"
},
{
"description": "\"What was the complaint rate for Hospital A last month?\" - AI answers from data",
"label": "Natural Language Q&A"
}
],
"question": "Which AI-powered features would you like to include? (Select all that apply)",
"multiSelect": true
},
{
"header": "Data Access",
"options": [
{
"description": "System-wide view across all hospitals with ability to drill down",
"label": "All Hospitals"
},
{
"description": "See only their own hospital's metrics and comparisons",
"label": "Single Hospital"
},
{
"description": "Both - can toggle between system-wide and individual hospital views",
"label": "Both (Recommended)"
},
{
"description": "Custom filters to select specific hospitals, departments, or time periods",
"label": "Custom Selection"
}
],
"question": "Should executives be able to see data across all hospitals, or restricted to their hospital?",
"multiSelect": false
},
{
"header": "Delivery",
"options": [
{
"description": "Dedicated Executive Summary page with interactive charts and AI insights",
"label": "Web Dashboard"
},
{
"description": "Scheduled PDF reports emailed to executives (daily/weekly/monthly)",
"label": "PDF Reports"
},
{
"description": "Mobile-responsive view optimized for executives checking on-the-go",
"label": "Mobile View"
},
{
"description": "All of the above",
"label": "All Channels"
}
],
"question": "How should the Executive Summary be delivered?",
"multiSelect": true
}
]
}
```
**Output:**
```
User has provided the following answers:
**Key Metrics**: Overall KPIs & Metrics, Risk & Early Warnings, AI-Generated Summaries
**AI Features**: Automated Narratives, Predictive Insights, Recommendations, Natural Language Q&A
**Data Access**: All Hospitals
**Delivery**: Web Dashboard, PDF Reports, Mobile View, All Channels
```
## Assistant
Now that I have the user's requirements confirmed, I need to examine the existing dashboard views, URL routing, and AI engine implementation to understand the patterns I should follow when designing the Executive Summary feature.
## Assistant
Perfect! Now let me examine the existing dashboard implementation and AI engine to create a comprehensive plan.
### Tool: ReadFile: apps/dashboard/views.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/dashboard/views.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/dashboard/views.py`
**Output:**
```python
Showing lines 1-569 of 1754 total lines.
---
"""
Dashboard views - PX Command Center and analytics dashboards
"""
import json
from datetime import timedelta, datetime
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.db.models import Avg, Count, Q, Sum
from django.http import JsonResponse
from django.shortcuts import redirect, render, reverse
from django.utils import timezone
from django.views.generic import TemplateView
from django.utils.translation import gettext_lazy as _
from django.contrib import messages
class CommandCenterView(LoginRequiredMixin, TemplateView):
"""
PX Command Center Dashboard - Real-time control panel.
Shows:
- Red Alert Banner (urgent items requiring immediate attention)
- Top KPI cards (complaints, actions, surveys, etc.) with drill-down
- Charts (trends, satisfaction, leaderboards)
- Live feed (latest complaints, actions, events)
- Enhanced modules (Inquiries, Observations)
- Filters (date range, hospital, department)
Follows the "5-Second Rule": Critical signals dominant and comprehensible within 5 seconds.
Uses modular tile/card system with 30-60 second auto-refresh capability.
"""
template_name = "dashboard/command_center.html"
def dispatch(self, request, *args, **kwargs):
"""Check user type and redirect accordingly"""
# Redirect Source Users to their dashboard
if request.user.is_authenticated and request.user.is_source_user():
return redirect("px_sources:source_user_dashboard")
# Check PX Admin has selected a hospital before processing request
if request.user.is_authenticated and request.user.is_px_admin() and not request.tenant_hospital:
return redirect("core:select_hospital")
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = self.request.user
# Import models
from apps.complaints.models import Complaint, Inquiry
from apps.px_action_center.models import PXAction
from apps.surveys.models import SurveyInstance
from apps.callcenter.models import CallCenterInteraction
from apps.integrations.models import InboundEvent
from apps.physicians.models import PhysicianMonthlyRating
from apps.organizations.models import Staff
from apps.observations.models import Observation
# Date filters
now = timezone.now()
last_24h = now - timedelta(hours=24)
last_7d = now - timedelta(days=7)
last_30d = now - timedelta(days=30)
last_60d = now - timedelta(days=60)
# Base querysets (filtered by user role and tenant_hospital)
if user.is_px_admin():
# PX Admins use their selected hospital from session
hospital = self.request.tenant_hospital
complaints_qs = Complaint.objects.filter(hospital=hospital) if hospital else Complaint.objects.none()
inquiries_qs = Inquiry.objects.filter(hospital=hospital) if hospital else Inquiry.objects.none()
actions_qs = PXAction.objects.filter(hospital=hospital) if hospital else PXAction.objects.none()
surveys_qs = (
SurveyInstance.objects.filter(hospital=hospital) if hospital else SurveyInstance.objects.none()
) # Filter by tenant hospital
calls_qs = (
CallCenterInteraction.objects.filter(hospital=hospital)
if hospital
else CallCenterInteraction.objects.none()
)
observations_qs = Observation.objects.filter(hospital=hospital) if hospital else Observation.objects.none()
elif user.is_hospital_admin() and user.hospital:
complaints_qs = Complaint.objects.filter(hospital=user.hospital)
inquiries_qs = Inquiry.objects.filter(hospital=user.hospital)
actions_qs = PXAction.objects.filter(hospital=user.hospital)
surveys_qs = SurveyInstance.objects.filter(hospital=user.hospital)
calls_qs = CallCenterInteraction.objects.filter(hospital=user.hospital)
observations_qs = Observation.objects.filter(hospital=user.hospital)
elif user.is_department_manager() and user.department:
complaints_qs = Complaint.objects.filter(department=user.department)
inquiries_qs = Inquiry.objects.filter(department=user.department)
actions_qs = PXAction.objects.filter(department=user.department)
surveys_qs = SurveyInstance.objects.filter(journey_stage_instance__department=user.department)
calls_qs = CallCenterInteraction.objects.filter(department=user.department)
observations_qs = Observation.objects.filter(assigned_department=user.department)
else:
complaints_qs = Complaint.objects.none()
inquiries_qs = Inquiry.objects.none()
actions_qs = PXAction.objects.none()
surveys_qs = SurveyInstance.objects.none()
calls_qs = CallCenterInteraction.objects.none()
observations_qs = Observation.objects.none()
# ========================================
# RED ALERT ITEMS (5-Second Rule)
# ========================================
red_alerts = []
# Critical complaints
critical_complaints = complaints_qs.filter(severity="critical", status__in=["open", "in_progress"]).count()
if critical_complaints > 0:
red_alerts.append(
{
"type": "critical_complaints",
"label": _("Critical Complaints"),
"value": critical_complaints,
"icon": "alert-octagon",
"color": "red",
"url": f"{reverse('complaints:complaint_list')}?severity=critical&status=open,in_progress",
"priority": 1,
}
)
# Overdue complaints
overdue_complaints = complaints_qs.filter(is_overdue=True, status__in=["open", "in_progress"]).count()
if overdue_complaints > 0:
red_alerts.append(
{
"type": "overdue_complaints",
"label": _("Overdue Complaints"),
"value": overdue_complaints,
"icon": "clock",
"color": "orange",
"url": f"{reverse('complaints:complaint_list')}?is_overdue=true&status=open,in_progress",
"priority": 2,
}
)
# Escalated actions
escalated_actions = actions_qs.filter(escalation_level__gt=0, status__in=["open", "in_progress"]).count()
if escalated_actions > 0:
red_alerts.append(
{
"type": "escalated_actions",
"label": _("Escalated Actions"),
"value": escalated_actions,
"icon": "arrow-up-circle",
"color": "red",
"url": reverse("actions:action_list"),
"priority": 3,
}
)
# Negative surveys in last 24h
negative_surveys_24h = surveys_qs.filter(is_negative=True, completed_at__gte=last_24h).count()
if negative_surveys_24h > 0:
red_alerts.append(
{
"type": "negative_surveys",
"label": _("Negative Surveys (24h)"),
"value": negative_surveys_24h,
"icon": "frown",
"color": "yellow",
"url": reverse("surveys:instance_list"),
"priority": 4,
}
)
# Sort by priority
red_alerts.sort(key=lambda x: x["priority"])
context["red_alerts"] = red_alerts
context["has_red_alerts"] = len(red_alerts) > 0
# ========================================
# COMPLAINTS MODULE DATA
# ========================================
complaints_current = complaints_qs.filter(created_at__gte=last_30d).count()
complaints_previous = complaints_qs.filter(created_at__gte=last_60d, created_at__lt=last_30d).count()
complaints_variance = 0
if complaints_previous > 0:
complaints_variance = round(((complaints_current - complaints_previous) / complaints_previous) * 100, 1)
# Resolution time calculation
resolved_complaints = complaints_qs.filter(status="closed", closed_at__isnull=False, created_at__gte=last_30d)
avg_resolution_hours = 0
if resolved_complaints.exists():
total_hours = sum((c.closed_at - c.created_at).total_seconds() / 3600 for c in resolved_complaints)
avg_resolution_hours = round(total_hours / resolved_complaints.count(), 1)
# Complaints by severity for donut chart
complaints_by_severity = {
"critical": complaints_qs.filter(severity="critical", status__in=["open", "in_progress"]).count(),
"high": complaints_qs.filter(severity="high", status__in=["open", "in_progress"]).count(),
"medium": complaints_qs.filter(severity="medium", status__in=["open", "in_progress"]).count(),
"low": complaints_qs.filter(severity="low", status__in=["open", "in_progress"]).count(),
}
# Complaints by department for heatmap
complaints_by_department = list(
complaints_qs.filter(status__in=["open", "in_progress"], department__isnull=False)
.values("department__name")
.annotate(count=Count("id"))
.order_by("-count")[:10]
)
context["complaints_module"] = {
"total_active": complaints_qs.filter(status__in=["open", "in_progress"]).count(),
"current_period": complaints_current,
"previous_period": complaints_previous,
"variance": complaints_variance,
"variance_direction": "up" if complaints_variance > 0 else "down" if complaints_variance < 0 else "neutral",
"avg_resolution_hours": avg_resolution_hours,
"overdue": complaints_qs.filter(is_overdue=True, status__in=["open", "in_progress"]).count(),
"by_severity": complaints_by_severity,
"by_department": complaints_by_department,
"critical_new": complaints_qs.filter(severity="critical", created_at__gte=last_24h).count(),
}
# ========================================
# SURVEY INSIGHTS MODULE DATA
# ========================================
surveys_completed_30d = surveys_qs.filter(completed_at__gte=last_30d)
total_surveys_30d = surveys_completed_30d.count()
# Calculate average satisfaction
avg_satisfaction = (
surveys_completed_30d.filter(total_score__isnull=False).aggregate(Avg("total_score"))["total_score__avg"]
or 0
)
# NPS-style calculation (promoters - detractors)
positive_count = surveys_completed_30d.filter(is_negative=False).count()
negative_count = surveys_completed_30d.filter(is_negative=True).count()
nps_score = 0
if total_surveys_30d > 0:
nps_score = round(((positive_count - negative_count) / total_surveys_30d) * 100)
# Response rate (completed vs sent in last 30 days)
surveys_sent_30d = surveys_qs.filter(sent_at__gte=last_30d).count()
response_rate = 0
if surveys_sent_30d > 0:
response_rate = round((total_surveys_30d / surveys_sent_30d) * 100, 1)
context["survey_module"] = {
"avg_satisfaction": round(avg_satisfaction, 1),
"nps_score": nps_score,
"response_rate": response_rate,
"total_completed": total_surveys_30d,
"positive_count": positive_count,
"negative_count": negative_count,
"neutral_count": total_surveys_30d - positive_count - negative_count,
"negative_24h": surveys_qs.filter(is_negative=True, completed_at__gte=last_24h).count(),
}
# ========================================
# PX ACTIONS MODULE DATA
# ========================================
actions_open = actions_qs.filter(status="open").count()
actions_in_progress = actions_qs.filter(status="in_progress").count()
actions_pending_approval = actions_qs.filter(status="pending_approval").count()
actions_closed_30d = actions_qs.filter(status="closed", closed_at__gte=last_30d).count()
# Time to close calculation
closed_actions = actions_qs.filter(status="closed", closed_at__isnull=False, created_at__gte=last_30d)
avg_time_to_close_hours = 0
if closed_actions.exists():
total_hours = sum((a.closed_at - a.created_at).total_seconds() / 3600 for a in closed_actions)
avg_time_to_close_hours = round(total_hours / closed_actions.count(), 1)
# Actions by source for breakdown
actions_by_source = list(
actions_qs.filter(status__in=["open", "in_progress"])
.values("source_type")
.annotate(count=Count("id"))
.order_by("-count")
)
context["actions_module"] = {
"open": actions_open,
"in_progress": actions_in_progress,
"pending_approval": actions_pending_approval,
"overdue": actions_qs.filter(is_overdue=True, status__in=["open", "in_progress"]).count(),
"escalated": actions_qs.filter(escalation_level__gt=0, status__in=["open", "in_progress"]).count(),
"closed_30d": actions_closed_30d,
"avg_time_to_close_hours": avg_time_to_close_hours,
"by_source": actions_by_source,
"new_today": actions_qs.filter(created_at__gte=last_24h).count(),
}
# ========================================
# INQUIRIES MODULE DATA
# ========================================
inquiries_open = inquiries_qs.filter(status="open").count()
inquiries_in_progress = inquiries_qs.filter(status="in_progress").count()
inquiries_resolved_30d = inquiries_qs.filter(status="resolved", updated_at__gte=last_30d).count()
context["inquiries_module"] = {
"open": inquiries_open,
"in_progress": inquiries_in_progress,
"total_active": inquiries_open + inquiries_in_progress,
"resolved_30d": inquiries_resolved_30d,
"new_24h": inquiries_qs.filter(created_at__gte=last_24h).count(),
}
# ========================================
# OBSERVATIONS MODULE DATA
# ========================================
observations_new = observations_qs.filter(status="new").count()
observations_in_progress = observations_qs.filter(status="in_progress").count()
observations_critical = observations_qs.filter(
severity="critical", status__in=["new", "triaged", "assigned", "in_progress"]
).count()
# Observations by category
observations_by_category = list(
observations_qs.filter(status__in=["new", "triaged", "assigned", "in_progress"])
.values("category__name_en")
.annotate(count=Count("id"))
.order_by("-count")[:5]
)
context["observations_module"] = {
"new": observations_new,
"in_progress": observations_in_progress,
"critical": observations_critical,
"total_active": observations_new + observations_in_progress,
"resolved_30d": observations_qs.filter(status="resolved", resolved_at__gte=last_30d).count(),
"by_category": observations_by_category,
}
# ========================================
# COMMUNICATION/CALL CENTER MODULE DATA
# ========================================
calls_7d = calls_qs.filter(call_started_at__gte=last_7d)
total_calls = calls_7d.count()
low_rating_calls = calls_7d.filter(is_low_rating=True).count()
context["calls_module"] = {
"total_7d": total_calls,
"low_ratings": low_rating_calls,
"satisfaction_rate": round(((total_calls - low_rating_calls) / total_calls * 100), 1)
if total_calls > 0
else 0,
}
# ========================================
# LEGACY STATS (for backward compatibility)
# ========================================
context["stats"] = {
"total_complaints": context["complaints_module"]["total_active"],
"avg_resolution_time": f"{avg_resolution_hours}h",
"satisfaction_score": round(avg_satisfaction, 0),
"active_actions": context["actions_module"]["open"] + context["actions_module"]["in_progress"],
"new_today": context["actions_module"]["new_today"],
}
# ========================================
# LATEST ITEMS FOR LIVE FEED
# ========================================
# Latest high severity complaints
context["latest_complaints"] = (
complaints_qs.filter(severity__in=["high", "critical"])
.select_related("patient", "hospital", "department")
.order_by("-created_at")[:5]
)
# Latest escalated actions
context["latest_actions"] = (
actions_qs.filter(escalation_level__gt=0)
.select_related("hospital", "assigned_to")
.order_by("-escalated_at")[:5]
)
# Latest inquiries
context["latest_inquiries"] = (
inquiries_qs.filter(status__in=["open", "in_progress"])
.select_related("patient", "hospital", "department")
.order_by("-created_at")[:5]
)
# Latest observations
context["latest_observations"] = (
observations_qs.filter(status__in=["new", "triaged", "assigned"])
.select_related("hospital", "category")
.order_by("-created_at")[:5]
)
# Latest integration events
context["latest_events"] = (
InboundEvent.objects.filter(status="processed").select_related().order_by("-processed_at")[:10]
)
# ========================================
# PHYSICIAN LEADERBOARD
# ========================================
current_month_ratings = PhysicianMonthlyRating.objects.filter(year=now.year, month=now.month).select_related(
"staff", "staff__hospital", "staff__department"
)
# Filter by user role
if user.is_hospital_admin() and user.hospital:
current_month_ratings = current_month_ratings.filter(staff__hospital=user.hospital)
elif user.is_department_manager() and user.department:
current_month_ratings = current_month_ratings.filter(staff__department=user.department)
# Top 5 staff this month
context["top_physicians"] = current_month_ratings.order_by("-average_rating")[:5]
# Staff stats
physician_stats = current_month_ratings.aggregate(
total_physicians=Count("id"), avg_rating=Avg("average_rating"), total_surveys=Count("total_surveys")
)
context["physician_stats"] = physician_stats
# ========================================
# CHART DATA
# ========================================
context["chart_data"] = {
"complaints_trend": json.dumps(self.get_complaints_trend(complaints_qs, last_30d)),
"complaints_by_severity": json.dumps(complaints_by_severity),
"survey_satisfaction": avg_satisfaction,
"nps_trend": json.dumps(self.get_nps_trend(surveys_qs, last_30d)),
"actions_funnel": json.dumps(
{
"open": actions_open,
"in_progress": actions_in_progress,
"pending_approval": actions_pending_approval,
"closed": actions_closed_30d,
}
),
}
# Add hospital context
context["current_hospital"] = self.request.tenant_hospital
context["is_px_admin"] = user.is_px_admin()
# Last updated timestamp
context["last_updated"] = now.strftime("%Y-%m-%d %H:%M:%S")
return context
def get_complaints_trend(self, queryset, start_date):
"""Get complaints trend data for chart"""
# Group by day for last 30 days
data = []
for i in range(30):
date = start_date + timedelta(days=i)
count = queryset.filter(created_at__date=date.date()).count()
data.append({"date": date.strftime("%Y-%m-%d"), "count": count})
return data
def get_nps_trend(self, queryset, start_date):
"""Get NPS trend data for chart"""
data = []
for i in range(30):
date = start_date + timedelta(days=i)
day_surveys = queryset.filter(completed_at__date=date.date())
total = day_surveys.count()
if total > 0:
positive = day_surveys.filter(is_negative=False).count()
negative = day_surveys.filter(is_negative=True).count()
nps = round(((positive - negative) / total) * 100)
else:
nps = 0
data.append({"date": date.strftime("%Y-%m-%d"), "nps": nps})
return data
def get_survey_satisfaction(self, queryset, start_date):
"""Get survey satisfaction averages"""
return (
queryset.filter(completed_at__gte=start_date, total_score__isnull=False).aggregate(Avg("total_score"))[
"total_score__avg"
]
or 0
)
@login_required
def my_dashboard(request):
"""
My Dashboard - Personal view of all assigned items.
Shows:
- Summary cards with statistics
- Tabbed interface for 6 model types:
* Complaints
* Inquiries
* Observations
* PX Actions
* Tasks (QI Project Tasks)
* Feedback
- Date range filtering
- Search and filter controls
- Export functionality (CSV/Excel)
- Bulk actions support
- Charts showing trends
"""
# Redirect Source Users to their dashboard
if request.user.is_source_user():
return redirect("px_sources:source_user_dashboard")
user = request.user
# Get selected hospital for PX Admins (from middleware)
selected_hospital = getattr(request, "tenant_hospital", None)
# Get date range filter
date_range_days = int(request.GET.get("date_range", 30))
if date_range_days == -1: # All time
start_date = None
else:
start_date = timezone.now() - timedelta(days=date_range_days)
# Get active tab
active_tab = request.GET.get("tab", "complaints")
# Get search query
search_query = request.GET.get("search", "")
# Get status filter
status_filter = request.GET.get("status", "")
# Get priority/severity filter
priority_filter = request.GET.get("priority", "")
# Build querysets for all models
querysets = {}
# 1. Complaints
from apps.complaints.models import Complaint
complaints_qs = Complaint.objects.filter(assigned_to=user)
# Filter by selected hospital for PX Admins
if selected_hospital:
complaints_qs = complaints_qs.filter(hospital=selected_hospital)
if start_date:
complaints_qs = complaints_qs.filter(created_at__gte=start_date)
if search_query:
complaints_qs = complaints_qs.filter(Q(title__icontains=search_query) | Q(description__icontains=search_query))
if status_filter:
complaints_qs = complaints_qs.filter(status=status_filter)
if priority_filter:
complaints_qs = complaints_qs.filter(severity=priority_filter)
querysets["complaints"] = complaints_qs.select_related(
"patient", "hospital", "department", "source", "created_by"
).order_by("-created_at")
# 2. Inquiries
from apps.complaints.models import Inquiry
inquiries_qs = Inquiry.objects.filter(assigned_to=user)
# Filter by selected hospital for PX Admins
if selected_hospital:
inquiries_qs = inquiries_qs.filter(hospital=selected_hospital)
if start_date:
inquiries_qs = inquiries_qs.filter(created_at__gte=start_date)
if search_query:
inquiries_qs = inquiries_qs.filter(Q(subject__icontains=search_query) | Q(message__icontains=search_query))
if status_filter:
inquiries_qs = inquiries_qs.filter(status=status_filter)
querysets["inquiries"] = inquiries_qs.select_related("patient", "hospital", "department").order_by("-created_at")
# 3. Observations
from apps.observations.models... [truncated]
```
### Tool: Glob: '**/urls.py'
**Status**: completed
**Input:**
```json
{
"pattern": "**/urls.py"
}
```
**Output:**
```
Found 27 file(s) matching "**/urls.py" in the workspace directory, sorted by modification time (newest first):
---
/home/ismail/projects/HH/apps/projects/urls.py
/home/ismail/projects/HH/apps/complaints/urls.py
/home/ismail/projects/HH/apps/accounts/urls.py
/home/ismail/projects/HH/apps/ai_engine/urls.py
/home/ismail/projects/HH/apps/analytics/urls.py
/home/ismail/projects/HH/apps/appreciation/urls.py
/home/ismail/projects/HH/apps/callcenter/urls.py
/home/ismail/projects/HH/apps/core/urls.py
/home/ismail/projects/HH/apps/dashboard/urls.py
/home/ismail/projects/HH/apps/feedback/urls.py
/home/ismail/projects/HH/apps/integrations/urls.py
/home/ismail/projects/HH/apps/journeys/urls.py
/home/ismail/projects/HH/apps/notifications/urls.py
/home/ismail/projects/HH/apps/observations/urls.py
/home/ismail/projects/HH/apps/organizations/urls.py
/home/ismail/projects/HH/apps/physicians/urls.py
/home/ismail/projects/HH/apps/px_action_center/urls.py
/home/ismail/projects/HH/apps/px_sources/urls.py
/home/ismail/projects/HH/apps/rca/urls.py
/home/ismail/projects/HH/apps/references/urls.py
/home/ismail/projects/HH/apps/reports/urls.py
/home/ismail/projects/HH/apps/simulator/urls.py
/home/ismail/projects/HH/apps/social/urls.py
/home/ismail/projects/HH/apps/standards/urls.py
/home/ismail/projects/HH/apps/surveys/urls.py
/home/ismail/projects/HH/config/urls.py
/home/ismail/projects/HH/PX360/urls.py
```
### Tool: ReadFile: apps/ai_engine
**Status**: failed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/ai_engine"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/ai_engine`
**Output:**
```
Path is a directory, not a file: /home/ismail/projects/HH/apps/ai_engine
```
## Assistant
Let me examine the AI engine app structure and views to understand current AI capabilities before designing the Executive Summary feature.
### Tool: ListFiles: apps/ai_engine
**Status**: completed
**Input:**
```json
{
"path": "/home/ismail/projects/HH/apps/ai_engine"
}
```
**Output:**
```
Listed 15 item(s) in /home/ismail/projects/HH/apps/ai_engine:
---
[DIR] __pycache__
[DIR] migrations
[DIR] templatetags
__init__.py
admin.py
apps.py
forms.py
models.py
serializers.py
services.py
signals.py
ui_views.py
urls.py
utils.py
views.py
```
### Tool: ReadFile: apps/ai_engine/views.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/ai_engine/views.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/ai_engine/views.py`
**Output:**
```python
"""
AI Engine API views
"""
import time
from django.db.models import Q
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status, viewsets
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from apps.accounts.permissions import IsPXAdmin, IsHospitalAdmin
from .models import SentimentResult
from .serializers import (
AnalyzeTextRequestSerializer,
AnalyzeTextResponseSerializer,
BatchAnalyzeRequestSerializer,
BatchAnalyzeResponseSerializer,
SentimentResultSerializer,
SentimentStatsSerializer,
)
from .services import AIEngineService
@extend_schema_view(
list=extend_schema(
summary="List sentiment results",
description="Get a list of all sentiment analysis results with filtering options"
),
retrieve=extend_schema(
summary="Get sentiment result",
description="Get details of a specific sentiment analysis result"
),
)
class SentimentResultViewSet(viewsets.ReadOnlyModelViewSet):
"""
Sentiment result viewset - Read-only API for sentiment results.
Provides:
- List all sentiment results with filters
- Retrieve specific sentiment result
- Statistics endpoint
"""
serializer_class = SentimentResultSerializer
permission_classes = [IsAuthenticated]
filterset_fields = ['sentiment', 'language', 'ai_service']
search_fields = ['text']
ordering_fields = ['created_at', 'sentiment_score', 'confidence']
ordering = ['-created_at']
def get_queryset(self):
"""Filter queryset based on user permissions"""
queryset = SentimentResult.objects.select_related('content_type').all()
# Apply filters from query params
sentiment = self.request.query_params.get('sentiment')
if sentiment:
queryset = queryset.filter(sentiment=sentiment)
language = self.request.query_params.get('language')
if language:
queryset = queryset.filter(language=language)
ai_service = self.request.query_params.get('ai_service')
if ai_service:
queryset = queryset.filter(ai_service=ai_service)
min_confidence = self.request.query_params.get('min_confidence')
if min_confidence:
queryset = queryset.filter(confidence__gte=min_confidence)
search = self.request.query_params.get('search')
if search:
queryset = queryset.filter(text__icontains=search)
return queryset
@extend_schema(
summary="Get sentiment statistics",
description="Get aggregated statistics for sentiment results",
responses={200: SentimentStatsSerializer}
)
@action(detail=False, methods=['get'])
def stats(self, request):
"""Get sentiment statistics"""
queryset = self.get_queryset()
stats = AIEngineService.get_sentiment_stats(queryset)
serializer = SentimentStatsSerializer(stats)
return Response(serializer.data)
@extend_schema(
summary="Analyze text",
description="Analyze text for sentiment, keywords, entities, and emotions",
request=AnalyzeTextRequestSerializer,
responses={200: AnalyzeTextResponseSerializer}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def analyze_text(request):
"""
Analyze text for sentiment.
POST /api/ai-engine/analyze/
Request body:
{
"text": "The service was excellent!",
"language": "en", // optional, auto-detected if not provided
"extract_keywords": true,
"extract_entities": true,
"detect_emotions": true
}
Response:
{
"text": "The service was excellent!",
"language": "en",
"sentiment": "positive",
"sentiment_score": 0.8,
"confidence": 0.9,
"keywords": ["excellent"],
"entities": [],
"emotions": {"joy": 0.6},
"ai_service": "stub",
"ai_model": "keyword_matching_v1",
"processing_time_ms": 5
}
"""
serializer = AnalyzeTextRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)
# Perform analysis
result = AIEngineService.sentiment.analyze_text(
text=serializer.validated_data['text'],
language=serializer.validated_data.get('language'),
extract_keywords=serializer.validated_data.get('extract_keywords', True),
extract_entities=serializer.validated_data.get('extract_entities', True),
detect_emotions=serializer.validated_data.get('detect_emotions', True),
)
response_serializer = AnalyzeTextResponseSerializer(result)
return Response(response_serializer.data)
@extend_schema(
summary="Analyze batch of texts",
description="Analyze multiple texts in a single request",
request=BatchAnalyzeRequestSerializer,
responses={200: BatchAnalyzeResponseSerializer}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def analyze_batch(request):
"""
Analyze multiple texts in batch.
POST /api/ai-engine/analyze-batch/
Request body:
{
"texts": [
"The service was excellent!",
"Very disappointed with the wait time.",
"Average experience overall."
],
"language": "en" // optional
}
Response:
{
"results": [
{
"text": "The service was excellent!",
"sentiment": "positive",
...
},
...
],
"total": 3,
"processing_time_ms": 15
}
"""
serializer = BatchAnalyzeRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)
start_time = time.time()
# Perform batch analysis
results = AIEngineService.sentiment.analyze_batch(
texts=serializer.validated_data['texts'],
language=serializer.validated_data.get('language'),
)
processing_time_ms = int((time.time() - start_time) * 1000)
response_data = {
'results': results,
'total': len(results),
'processing_time_ms': processing_time_ms,
}
response_serializer = BatchAnalyzeResponseSerializer(response_data)
return Response(response_serializer.data)
@extend_schema(
summary="Get sentiment for object",
description="Get sentiment analysis result for a specific object",
responses={200: SentimentResultSerializer}
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_sentiment_for_object(request, content_type_id, object_id):
"""
Get sentiment result for a specific object.
GET /api/ai-engine/sentiment/{content_type_id}/{object_id}/
"""
from django.contrib.contenttypes.models import ContentType
try:
content_type = ContentType.objects.get(id=content_type_id)
except ContentType.DoesNotExist:
return Response(
{'error': 'Content type not found'},
status=status.HTTP_404_NOT_FOUND
)
sentiment_result = SentimentResult.objects.filter(
content_type=content_type,
object_id=object_id
).first()
if not sentiment_result:
return Response(
{'error': 'Sentiment result not found'},
status=status.HTTP_404_NOT_FOUND
)
serializer = SentimentResultSerializer(sentiment_result)
return Response(serializer.data)
```
### Tool: ReadFile: config/urls.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/config/urls.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/config/urls.py`
**Output:**
```python
"""
URL configuration for PX360 project.
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from django.conf.urls.i18n import i18n_patterns
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from apps.organizations.views import api_patient_search, api_staff_search
urlpatterns = [
# Language switching
path("i18n/", include("django.conf.urls.i18n")),
# Admin
path("admin/", admin.site.urls),
# Dashboard
path("", include("apps.dashboard.urls")),
# Health check endpoint
path("health/", include("apps.core.urls", namespace="health")),
# Core pages (public submissions, hospital selection)
path("core/", include("apps.core.urls", namespace="core")),
# UI Pages
path("complaints/", include("apps.complaints.urls")),
path("inquiries/", include("apps.complaints.urls_inquiries")),
path("physicians/", include("apps.physicians.urls", namespace="physicians")),
path("feedback/", include("apps.feedback.urls")),
path("actions/", include("apps.px_action_center.urls")),
path("accounts/", include("apps.accounts.urls", namespace="accounts")),
path("journeys/", include("apps.journeys.urls")),
path("surveys/", include("apps.surveys.urls")),
path("social/", include("apps.social.urls")),
path("callcenter/", include("apps.callcenter.urls")),
path("analytics/", include("apps.analytics.urls")),
path("organizations/", include("apps.organizations.urls")),
path("projects/", include("apps.projects.urls")),
path("config/", include("apps.core.config_urls")),
path("ai-engine/", include("apps.ai_engine.urls")),
path("appreciation/", include("apps.appreciation.urls", namespace="appreciation")),
path("notifications/", include("apps.notifications.urls", namespace="notifications")),
path("observations/", include("apps.observations.urls")),
path("px-sources/", include("apps.px_sources.urls")),
path("references/", include("apps.references.urls", namespace="references")),
path("standards/", include("apps.standards.urls")),
path("reports/", include("apps.reports.urls", namespace="reports")),
path("rca/", include("apps.rca.urls", namespace="rca")),
# API endpoints
path("api/auth/", include("apps.accounts.urls", namespace="api_auth")),
path("api/physicians/", include("apps.physicians.urls", namespace="api_physicians")),
path("api/integrations/", include("apps.integrations.urls")),
path("api/notifications/", include("apps.notifications.urls", namespace="api_notifications")),
path("api/v1/appreciation/", include("apps.appreciation.urls", namespace="api_appreciation")),
path("api/simulator/", include("apps.simulator.urls", namespace="api_simulator")),
path("api/patients/search/", api_patient_search, name="api_patient_search"),
path("api/staffs/search/", api_staff_search, name="api_staff_search"),
# OpenAPI/Swagger documentation
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# Customize admin site
admin.site.site_header = "PX360 Administration"
admin.site.site_title = "PX360 Admin Portal"
admin.site.index_title = "Welcome to PX360 Administration"
```
## Assistant
Let me examine the AI engine services, models, and settings to understand the existing AI capabilities and LLM configuration available for building the Executive Summary feature.
### Tool: ReadFile: apps/ai_engine/services.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/ai_engine/services.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/ai_engine/services.py`
**Output:**
```python
"""
AI Engine services - Sentiment analysis and NLP
This module provides AI services for:
- Sentiment analysis (positive, neutral, negative)
- Keyword extraction
- Entity recognition
- Emotion detection
- Language detection
Currently uses a stub implementation that can be replaced with:
- OpenAI API
- Azure Cognitive Services
- AWS Comprehend
- Custom ML models
"""
import re
import time
from decimal import Decimal
from typing import Dict, List, Optional, Tuple
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from .models import SentimentResult
class SentimentAnalysisService:
"""
Sentiment analysis service with stub implementation.
This service provides realistic sentiment analysis without external API calls.
Replace the stub methods with real AI service calls when ready.
"""
# Positive keywords (English and Arabic)
POSITIVE_KEYWORDS = {
'en': [
'excellent', 'great', 'good', 'wonderful', 'amazing', 'fantastic',
'outstanding', 'superb', 'perfect', 'best', 'love', 'happy',
'satisfied', 'pleased', 'thank', 'appreciate', 'helpful', 'kind',
'professional', 'caring', 'friendly', 'clean', 'comfortable'
],
'ar': [
'ممتاز', 'رائع', 'جيد', 'جميل', 'مذهل', 'رائع',
'متميز', 'ممتاز', 'مثالي', 'أفضل', 'أحب', 'سعيد',
'راض', 'مسرور', 'شكر', 'أقدر', 'مفيد', 'لطيف',
'محترف', 'مهتم', 'ودود', 'نظيف', 'مريح'
]
}
# Negative keywords (English and Arabic)
NEGATIVE_KEYWORDS = {
'en': [
'bad', 'terrible', 'horrible', 'awful', 'poor', 'worst',
'disappointed', 'unhappy', 'unsatisfied', 'angry', 'frustrated',
'rude', 'unprofessional', 'dirty', 'uncomfortable', 'painful',
'long wait', 'delayed', 'ignored', 'neglected', 'complaint'
],
'ar': [
'سيء', 'فظيع', 'مروع', 'سيء', 'ضعيف', 'أسوأ',
'خائب', 'غير سعيد', 'غير راض', 'غاضب', 'محبط',
'وقح', 'غير محترف', 'قذر', 'غير مريح', 'مؤلم',
'انتظار طويل', 'متأخر', 'تجاهل', 'مهمل', 'شكوى'
]
}
# Emotion keywords
EMOTION_KEYWORDS = {
'joy': ['happy', 'joy', 'pleased', 'delighted', 'سعيد', 'فرح', 'مسرور'],
'anger': ['angry', 'furious', 'mad', 'غاضب', 'غضب', 'حنق'],
'sadness': ['sad', 'unhappy', 'disappointed', 'حزين', 'خائب', 'محبط'],
'fear': ['afraid', 'scared', 'worried', 'خائف', 'قلق', 'مذعور'],
'surprise': ['surprised', 'shocked', 'amazed', 'متفاجئ', 'مندهش', 'مذهول'],
}
@classmethod
def detect_language(cls, text: str) -> str:
"""
Detect language of text (English or Arabic).
Simple detection based on character ranges.
"""
# Count Arabic characters
arabic_chars = len(re.findall(r'[\u0600-\u06FF]', text))
# Count English characters
english_chars = len(re.findall(r'[a-zA-Z]', text))
if arabic_chars > english_chars:
return 'ar'
return 'en'
@classmethod
def extract_keywords(cls, text: str, language: str, max_keywords: int = 10) -> List[str]:
"""
Extract keywords from text.
Stub implementation: Returns words that appear in positive/negative keyword lists.
Replace with proper NLP keyword extraction (TF-IDF, RAKE, etc.)
"""
text_lower = text.lower()
keywords = []
# Check positive keywords
for keyword in cls.POSITIVE_KEYWORDS.get(language, []):
if keyword in text_lower:
keywords.append(keyword)
# Check negative keywords
for keyword in cls.NEGATIVE_KEYWORDS.get(language, []):
if keyword in text_lower:
keywords.append(keyword)
return keywords[:max_keywords]
@classmethod
def extract_entities(cls, text: str, language: str) -> List[Dict[str, str]]:
"""
Extract named entities from text.
Stub implementation: Returns basic pattern matching.
Replace with proper NER (spaCy, Stanford NER, etc.)
"""
entities = []
# Simple email detection
emails = re.findall(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', text)
for email in emails:
entities.append({'text': email, 'type': 'EMAIL'})
# Simple phone detection
phones = re.findall(r'\b\d{10,}\b', text)
for phone in phones:
entities.append({'text': phone, 'type': 'PHONE'})
return entities
@classmethod
def detect_emotions(cls, text: str) -> Dict[str, float]:
"""
Detect emotions in text.
Stub implementation: Returns emotion scores based on keyword matching.
Replace with proper emotion detection model.
"""
text_lower = text.lower()
emotions = {}
for emotion, keywords in cls.EMOTION_KEYWORDS.items():
score = 0.0
for keyword in keywords:
if keyword in text_lower:
score += 0.2
emotions[emotion] = min(score, 1.0)
return emotions
@classmethod
def calculate_sentiment_score(cls, text: str, language: str) -> Tuple[str, float, float]:
"""
Calculate sentiment score for text.
Returns:
Tuple of (sentiment, score, confidence)
- sentiment: 'positive', 'neutral', or 'negative'
- score: float from -1 (very negative) to 1 (very positive)
- confidence: float from 0 to 1
Stub implementation: Uses keyword matching.
Replace with ML model (BERT, RoBERTa, etc.)
"""
text_lower = text.lower()
# Count positive and negative keywords
positive_count = 0
negative_count = 0
for keyword in cls.POSITIVE_KEYWORDS.get(language, []):
positive_count += text_lower.count(keyword)
for keyword in cls.NEGATIVE_KEYWORDS.get(language, []):
negative_count += text_lower.count(keyword)
# Calculate score
total_keywords = positive_count + negative_count
if total_keywords == 0:
# No sentiment keywords found - neutral
return 'neutral', 0.0, 0.5
# Calculate sentiment score (-1 to 1)
score = (positive_count - negative_count) / max(total_keywords, 1)
# Determine sentiment category
if score > 0.2:
sentiment = 'positive'
elif score < -0.2:
sentiment = 'negative'
else:
sentiment = 'neutral'
# Calculate confidence (higher when more keywords found)
confidence = min(total_keywords / 10.0, 1.0)
confidence = max(confidence, 0.3) # Minimum confidence
return sentiment, score, confidence
@classmethod
def analyze_text(
cls,
text: str,
language: Optional[str] = None,
extract_keywords: bool = True,
extract_entities: bool = True,
detect_emotions: bool = True
) -> Dict:
"""
Perform complete sentiment analysis on text.
Args:
text: Text to analyze
language: Language code ('en' or 'ar'), auto-detected if None
extract_keywords: Whether to extract keywords
extract_entities: Whether to extract entities
detect_emotions: Whether to detect emotions
Returns:
Dictionary with analysis results
"""
start_time = time.time()
# Detect language if not provided
if language is None:
language = cls.detect_language(text)
# Calculate sentiment
sentiment, score, confidence = cls.calculate_sentiment_score(text, language)
# Extract additional features
keywords = []
if extract_keywords:
keywords = cls.extract_keywords(text, language)
entities = []
if extract_entities:
entities = cls.extract_entities(text, language)
emotions = {}
if detect_emotions:
emotions = cls.detect_emotions(text)
# Calculate processing time
processing_time_ms = int((time.time() - start_time) * 1000)
return {
'text': text,
'language': language,
'sentiment': sentiment,
'sentiment_score': score,
'confidence': confidence,
'keywords': keywords,
'entities': entities,
'emotions': emotions,
'ai_service': 'stub',
'ai_model': 'keyword_matching_v1',
'processing_time_ms': processing_time_ms,
}
@classmethod
@transaction.atomic
def analyze_and_save(
cls,
text: str,
content_object,
language: Optional[str] = None,
**kwargs
) -> SentimentResult:
"""
Analyze text and save result to database.
Args:
text: Text to analyze
content_object: Django model instance to link to
language: Language code ('en' or 'ar'), auto-detected if None
**kwargs: Additional arguments for analyze_text
Returns:
SentimentResult instance
"""
# Perform analysis
analysis = cls.analyze_text(text, language, **kwargs)
# Get content type
content_type = ContentType.objects.get_for_model(content_object)
# Create sentiment result
sentiment_result = SentimentResult.objects.create(
content_type=content_type,
object_id=content_object.id,
text=analysis['text'],
language=analysis['language'],
sentiment=analysis['sentiment'],
sentiment_score=Decimal(str(analysis['sentiment_score'])),
confidence=Decimal(str(analysis['confidence'])),
keywords=analysis['keywords'],
entities=analysis['entities'],
emotions=analysis['emotions'],
ai_service=analysis['ai_service'],
ai_model=analysis['ai_model'],
processing_time_ms=analysis['processing_time_ms'],
)
return sentiment_result
@classmethod
def analyze_batch(cls, texts: List[str], language: Optional[str] = None) -> List[Dict]:
"""
Analyze multiple texts in batch.
Args:
texts: List of texts to analyze
language: Language code ('en' or 'ar'), auto-detected if None
Returns:
List of analysis results
"""
results = []
for text in texts:
result = cls.analyze_text(text, language)
results.append(result)
return results
class AIEngineService:
"""
Main AI Engine service - facade for all AI capabilities.
"""
sentiment = SentimentAnalysisService
@classmethod
def get_sentiment_for_object(cls, content_object) -> Optional[SentimentResult]:
"""
Get the most recent sentiment result for an object.
"""
content_type = ContentType.objects.get_for_model(content_object)
return SentimentResult.objects.filter(
content_type=content_type,
object_id=content_object.id
).first()
@classmethod
def get_sentiment_stats(cls, queryset=None) -> Dict:
"""
Get sentiment statistics.
Args:
queryset: Optional SentimentResult queryset to filter
Returns:
Dictionary with statistics
"""
if queryset is None:
queryset = SentimentResult.objects.all()
total = queryset.count()
if total == 0:
return {
'total': 0,
'positive': 0,
'neutral': 0,
'negative': 0,
'positive_pct': 0,
'neutral_pct': 0,
'negative_pct': 0,
'avg_score': 0,
'avg_confidence': 0,
}
positive = queryset.filter(sentiment='positive').count()
neutral = queryset.filter(sentiment='neutral').count()
negative = queryset.filter(sentiment='negative').count()
# Calculate averages
from django.db.models import Avg
avg_score = queryset.aggregate(Avg('sentiment_score'))['sentiment_score__avg'] or 0
avg_confidence = queryset.aggregate(Avg('confidence'))['confidence__avg'] or 0
return {
'total': total,
'positive': positive,
'neutral': neutral,
'negative': negative,
'positive_pct': round((positive / total) * 100, 1),
'neutral_pct': round((neutral / total) * 100, 1),
'negative_pct': round((negative / total) * 100, 1),
'avg_score': float(avg_score),
'avg_confidence': float(avg_confidence),
}
```
### Tool: ReadFile: apps/ai_engine/models.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/ai_engine/models.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/ai_engine/models.py`
**Output:**
```python
"""
AI Engine models - AI sentiment analysis and NLP
This module implements AI capabilities:
- Sentiment analysis for text (complaints, comments, social posts)
- Generic sentiment result storage
- Stubbed AI service interface for future integration
"""
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from apps.core.models import TimeStampedModel, UUIDModel
class SentimentResult(UUIDModel, TimeStampedModel):
"""
AI Sentiment analysis result - linked to any text content.
Uses generic foreign key to link to:
- Complaints
- Survey responses (text answers)
- Social media mentions
- Call center notes
"""
# Related object (generic foreign key)
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE
)
object_id = models.UUIDField()
content_object = GenericForeignKey('content_type', 'object_id')
# Text analyzed
text = models.TextField(help_text="Text that was analyzed")
language = models.CharField(
max_length=5,
choices=[('en', 'English'), ('ar', 'Arabic')],
default='en'
)
# Sentiment result
sentiment = models.CharField(
max_length=20,
choices=[
('positive', 'Positive'),
('neutral', 'Neutral'),
('negative', 'Negative'),
],
db_index=True
)
# Sentiment score (-1 to 1, where -1 is very negative, 1 is very positive)
sentiment_score = models.DecimalField(
max_digits=5,
decimal_places=4,
help_text="Sentiment score from -1 (negative) to 1 (positive)"
)
# Confidence level (0 to 1)
confidence = models.DecimalField(
max_digits=5,
decimal_places=4,
help_text="Confidence level of the sentiment analysis"
)
# AI service information
ai_service = models.CharField(
max_length=100,
default='stub',
help_text="AI service used (e.g., 'openai', 'azure', 'aws', 'stub')"
)
ai_model = models.CharField(
max_length=100,
blank=True,
help_text="Specific AI model used"
)
# Processing metadata
processing_time_ms = models.IntegerField(
null=True,
blank=True,
help_text="Time taken to analyze (milliseconds)"
)
# Additional analysis (optional)
keywords = models.JSONField(
default=list,
blank=True,
help_text="Extracted keywords"
)
entities = models.JSONField(
default=list,
blank=True,
help_text="Extracted entities (people, places, etc.)"
)
emotions = models.JSONField(
default=dict,
blank=True,
help_text="Emotion scores (joy, anger, sadness, etc.)"
)
# Metadata
metadata = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['sentiment', '-created_at']),
models.Index(fields=['content_type', 'object_id']),
]
def __str__(self):
return f"{self.sentiment} ({self.sentiment_score}) - {self.text[:50]}"
```
### Tool: Grep: 'LITELLM|OPENROUTER|AI_MODEL|ai.*model' (filter: '**/settings*.py')
**Status**: completed
**Input:**
```json
{
"pattern": "LITELLM|OPENROUTER|AI_MODEL|ai.*model",
"glob": "**/settings*.py"
}
```
**Output:**
```
Found 31 matches for pattern "LITELLM|OPENROUTER|AI_MODEL|ai.*model" in the workspace directory (filter: "**/settings*.py"):
---
/home/ismail/projects/HH/apps/notifications/settings_models.py:77: complaint_acknowledgment_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:81: complaint_acknowledgment_sms = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:85: complaint_acknowledgment_whatsapp = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:91: complaint_assigned_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:95: complaint_assigned_sms = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:99: complaint_assigned_whatsapp = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:105: complaint_status_changed_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:109: complaint_status_changed_sms = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:113: complaint_status_changed_whatsapp = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:119: complaint_resolved_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:123: complaint_resolved_sms = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:127: complaint_resolved_whatsapp = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:133: complaint_closed_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:137: complaint_closed_sms = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:141: complaint_closed_whatsapp = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:149: explanation_requested_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:163: explanation_reminder_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:177: explanation_overdue_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:191: explanation_received_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:213: survey_invitation_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:227: survey_reminder_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:241: survey_completed_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:253: action_assigned_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:267: action_due_soon_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:277: action_overdue_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:289: sla_reminder_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:299: sla_breach_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:315: onboarding_invitation_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:329: onboarding_reminder_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:343: onboarding_completion_email = models.BooleanField(
/home/ismail/projects/HH/apps/notifications/settings_models.py:375: retry_failed_notifications = models.BooleanField(
```
## Assistant
The grep search didn't find the LiteLLM/OpenRouter AI configuration, so I need to search for the actual settings file to understand how AI/LLM is configured in this project.
### Tool: Glob: '**/settings.py'
**Status**: completed
**Input:**
```json
{
"pattern": "**/settings.py"
}
```
**Output:**
```
Found 1 file(s) matching "**/settings.py" in the workspace directory, sorted by modification time (newest first):
---
/home/ismail/projects/HH/PX360/settings.py
```
## Assistant
Let me search for the LiteLLM/OpenRouter AI configuration in the settings file to understand what AI capabilities are already set up.
### Tool: Grep: 'LITELLM|OPENROUTER|AI.*API|ai.*config' in path '/home/ismail/projects/HH/PX360/settings.py'
**Status**: completed
**Input:**
```json
{
"path": "/home/ismail/projects/HH/PX360/settings.py",
"limit": 50,
"pattern": "LITELLM|OPENROUTER|AI.*API|ai.*config"
}
```
**Output:**
```
No matches found for pattern "LITELLM|OPENROUTER|AI.*API|ai.*config" in path "/home/ismail/projects/HH/PX360/settings.py".
```
## Assistant
Let me search more broadly for any AI-related configuration in the settings file.
### Tool: Grep: 'AI|ai_' in path '/home/ismail/projects/HH/PX360/settings.py'
**Status**: completed
**Input:**
```json
{
"path": "/home/ismail/projects/HH/PX360/settings.py",
"limit": 30,
"pattern": "AI|ai_"
}
```
**Output:**
```
Found 1 match for pattern "AI|ai_" in path "/home/ismail/projects/HH/PX360/settings.py":
---
/home/ismail/projects/HH/PX360/settings.py:188:EMAIL_LOGO_URL = "http://127.0.0.1:8000/static/img/HH_R_H_Logo.png"
```
## Assistant
Need to check requirements.txt to understand what AI/LLM packages are available in the project for building the Executive Summary AI features.
### Tool: ReadFile: requirements.txt
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/requirements.txt"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/requirements.txt`
**Output:**
```
aiohappyeyeballs==2.6.1
aiohttp==3.13.3
aiohttp-retry==2.9.1
aiosignal==1.4.0
amqp==5.3.1
annotated-types==0.7.0
anyio==4.12.0
asgiref==3.11.0
attrs==25.4.0
billiard==4.2.4
brotli==1.2.0
cachetools==6.2.4
celery==5.6.2
certifi==2026.1.4
cffi==2.0.0
charset-normalizer==3.4.4
click==8.3.1
click-didyoumean==0.3.1
click-plugins==1.1.1.2
click-repl==0.3.0
cron-descriptor==1.4.5
cssselect2==0.8.0
distro==1.9.0
django==6.0.1
django-celery-beat==2.9.0
django-environ==0.12.0
django-extensions==4.1
django-filter==25.1
django-stubs==5.2.8
django-stubs-ext==5.2.8
django-timezone-field==7.2.1
djangorestframework==3.16.1
djangorestframework-simplejwt==5.5.1
djangorestframework-stubs==3.16.6
drf-spectacular==0.29.0
et-xmlfile==2.0.0
fastuuid==0.14.0
filelock==3.20.2
fonttools==4.61.1
frozenlist==1.8.0
fsspec==2025.12.0
google-api-core==2.29.0
google-api-python-client==2.187.0
google-auth==2.41.1
google-auth-httplib2==0.3.0
google-auth-oauthlib==1.2.3
googleapis-common-protos==1.72.0
grpcio==1.67.1
gunicorn==23.0.0
h11==0.16.0
hf-xet==1.2.0
httpcore==1.0.9
httplib2==0.31.0
httpx==0.28.1
huggingface-hub==1.2.3
idna==3.11
importlib-metadata==8.7.1
inflection==0.5.1
jinja2==3.1.6
jiter==0.12.0
jsonschema==4.25.1
jsonschema-specifications==2025.9.1
kombu==5.6.2
litellm==1.80.11
markdown-it-py==4.0.0
markupsafe==3.0.3
mdurl==0.1.2
multidict==6.7.0
numpy==2.4.3
oauthlib==3.3.1
openai==2.14.0
openpyxl==3.1.5
packaging==25.0
pandas==3.0.1
pillow==12.1.0
pip==24.0
polib==1.2.0
prompt-toolkit==3.0.52
propcache==0.4.1
proto-plus==1.27.0
protobuf==6.33.3
psycopg2-binary==2.9.11
-e file:///home/ismail/projects/HH
pyasn1==0.6.1
pyasn1-modules==0.4.2
pycparser==2.23
pydantic==2.12.5
pydantic-core==2.41.5
pydyf==0.12.1
pygments==2.19.2
pyjwt==2.10.1
pyparsing==3.3.1
pyphen==0.17.2
python-crontab==3.3.0
python-dateutil==2.9.0.post0
python-dotenv==1.2.1
pytz==2025.2
pyyaml==6.0.3
redis==7.1.0
referencing==0.37.0
regex==2025.11.3
reportlab==4.4.7
requests==2.32.5
requests-oauthlib==2.0.0
rich==14.2.0
rpds-py==0.30.0
rsa==4.9.1
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
sqlparse==0.5.5
tiktoken==0.12.0
tinycss2==1.5.1
tinyhtml5==2.0.0
tokenizers==0.22.2
tqdm==4.67.1
tweepy==4.16.0
twilio==9.10.3
typer-slim==0.21.0
types-pyyaml==6.0.12.20250915
types-requests==2.32.4.20250913
typing-extensions==4.15.0
typing-inspection==0.4.2
tzdata==2025.3
tzlocal==5.3.1
ua-parser==1.0.1
ua-parser-builtins==202601
unidecode==1.4.0
uritemplate==4.2.0
urllib3==2.6.2
user-agents==2.2.0
vine==5.1.0
watchdog==6.0.0
wcwidth==0.2.14
weasyprint==67.0
webencodings==0.5.1
whitenoise==6.11.0
xlrd==2.0.2
yarl==1.22.0
zipp==3.23.0
zopfli==0.4.0
```
## Assistant
I should explore the dashboard templates to understand the existing UI structure and patterns for designing the Executive Summary page.
## Assistant
Perfect! I can see LiteLLM is installed. Now let me check the existing dashboard templates to understand the UI structure better.
### Tool: Glob: '**/dashboard/templates/**/*.html'
**Status**: completed
**Input:**
```json
{
"pattern": "**/dashboard/templates/**/*.html"
}
```
**Output:**
```
No files found matching pattern "**/dashboard/templates/**/*.html" in the workspace directory
```
### Tool: ReadFile: apps/dashboard/urls.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/dashboard/urls.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/dashboard/urls.py`
**Output:**
```python
"""
Dashboard URLs
"""
from django.urls import path
from .views import (
CommandCenterView,
my_dashboard,
dashboard_bulk_action,
admin_evaluation,
admin_evaluation_chart_data,
staff_performance_detail,
staff_performance_trends,
department_benchmarks,
export_staff_performance,
performance_analytics_api,
command_center_api,
employee_evaluation,
employee_evaluation_data,
complaint_request_list,
complaint_request_export,
)
app_name = "dashboard"
urlpatterns = [
path("", CommandCenterView.as_view(), name="command-center"),
path("api/", command_center_api, name="command_center_api"),
path("my/", my_dashboard, name="my_dashboard"),
path("bulk-action/", dashboard_bulk_action, name="bulk_action"),
# Admin Evaluation
path("admin-evaluation/", admin_evaluation, name="admin_evaluation"),
path("admin-evaluation/chart-data/", admin_evaluation_chart_data, name="admin_evaluation_chart_data"),
# Employee Evaluation (PAD Department Weekly Dashboard)
path("employee-evaluation/", employee_evaluation, name="employee_evaluation"),
path("employee-evaluation/data/", employee_evaluation_data, name="employee_evaluation_data"),
# Enhanced Staff Performance
path("admin-evaluation/staff/<str:staff_id>/", staff_performance_detail, name="staff_performance_detail"),
path("admin-evaluation/staff/<str:staff_id>/trends/", staff_performance_trends, name="staff_performance_trends"),
path("admin-evaluation/benchmarks/", department_benchmarks, name="department_benchmarks"),
path("admin-evaluation/export/", export_staff_performance, name="export_staff_performance"),
path("admin-evaluation/analytics/", performance_analytics_api, name="performance_analytics_api"),
# Step 0 — Complaint Requests Report
path("complaint-requests/", complaint_request_list, name="complaint_request_list"),
path("complaint-requests/export/", complaint_request_export, name="complaint_request_export"),
]
```
## Assistant
I need to find the dashboard templates to understand the existing UI structure and patterns for building the Executive Summary page.
### Tool: Glob: '**/templates/dashboard/*.html'
**Status**: completed
**Input:**
```json
{
"pattern": "**/templates/dashboard/*.html"
}
```
**Output:**
```
Found 8 file(s) matching "**/templates/dashboard/*.html" in the workspace directory, sorted by modification time (newest first):
---
/home/ismail/projects/HH/templates/dashboard/analytics-template.html
/home/ismail/projects/HH/templates/dashboard/admin_evaluation.html
/home/ismail/projects/HH/templates/dashboard/command_center.html
/home/ismail/projects/HH/templates/dashboard/complaint_request_list.html
/home/ismail/projects/HH/templates/dashboard/department_benchmarks.html
/home/ismail/projects/HH/templates/dashboard/employee_evaluation.html
/home/ismail/projects/HH/templates/dashboard/my_dashboard.html
/home/ismail/projects/HH/templates/dashboard/staff_performance_detail.html
```
## Assistant
Now I have enough information about the codebase architecture, AI capabilities, and user requirements to create a comprehensive implementation plan for the Executive Summary feature.
## Assistant
Now I have enough information to create a comprehensive plan. Let me present it to you.
## Executive Summary Feature Plan
Based on your requirements and the existing PX360 architecture, here's my comprehensive plan:
### **Overview**
Create an AI-powered Executive Summary system for top management with:
1. **Executive Dashboard** - Real-time high-level overview across all hospitals
2. **AI-Generated Narratives** - Automated weekly/monthly performance summaries
3. **Predictive Insights** - Early warning system and risk detection
4. **Recommendation Engine** - AI-suggested actions
5. **Natural Language Q&A** - Ask questions about your data
6. **PDF Report Generation** - Scheduled executive reports
7. **Mobile-Optimized View** - On-the-go access
---
### **Architecture**
#### **1. New Role: Executive (C-Suite/Top Management)**
- System-wide view across all hospitals
- Can drill down to individual hospitals/departments
- Read-only access to all metrics
- Special mobile-optimized interface
#### **2. New Django App: `apps/executive_summary/`**
```
Models:
- ExecutiveReport (scheduled AI-generated reports)
- PredictiveInsight (AI-detected patterns/risks)
- AIRecommendation (suggested actions)
- ExecutiveMetric (KPI snapshots for trending)
Views:
- ExecutiveDashboardView (main dashboard)
- ExecutiveReportAPIView (AI narrative generation)
- PredictiveInsightsView (risk analysis)
- ExecutiveQ&AView (natural language queries)
- ExecutivePDFReportView (PDF generation)
Services:
- ExecutiveSummaryService (data aggregation)
- AINarrativeService (LLM-powered summaries)
- PredictiveAnalyticsService (trend/risk detection)
- RecommendationService (action suggestions)
- ExecutiveQAService (natural language to queries)
Tasks (Celery):
- generate_weekly_executive_summary
- generate_monthly_executive_summary
- calculate_predictive_insights
- generate_recommendations
- send_executive_pdf_report
```
#### **3. AI-Powered Features**
**A. Automated Narratives** (using LiteLLM + Gemma)
- Weekly/Monthly natural language summaries
- Example: "This month, patient complaints decreased 12% across all hospitals. Hospital A showed significant improvement in resolution time (from 48h to 32h). However, we detected an emerging pattern of communication-related complaints in the Emergency department that requires attention."
- Bilingual (English/Arabic)
**B. Predictive Insights**
- Trend analysis with anomaly detection
- Risk scoring for hospitals/departments
- Early warning indicators:
- Rising complaint categories
- Declining satisfaction trends
- SLA breach predictions
- Staff performance degradation
**C. Recommendation Engine**
- AI-suggested actions based on:
- Historical resolution patterns
- Best practices from successful departments
- Resource allocation optimization
- Preventive measures for predicted issues
**D. Natural Language Q&A**
- "What was the complaint rate for Hospital A last month?"
- "Show me departments with declining satisfaction"
- "Which hospital has the best resolution time?"
- Converts natural language to Django ORM queries
#### **4. Dashboard UI Components**
**Executive Summary Page:**
```
┌─────────────────────────────────────────────┐
│ 📊 Executive Summary | All Hospitals │
│ [Date Range] [Hospital Filter] [Export PDF]│
├─────────────────────────────────────────────┤
│ 🤖 AI Executive Brief (Narrative Summary) │
│ "This week showed overall improvement..." │
├─────────────────────────────────────────────┤
│ ⚠️ Risk Alerts | 🔮 Predictions │
│ • Hospital B: Rising complaints (+23%) │
│ • Emergency Dept: SLA breach risk (85%) │
│ • Survey satisfaction trending down │
├─────────────────────────────────────────────┤
│ 📈 Key Metrics Cards │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Compl.│ │Surveys│ │Actions│ │NPS │ │
│ │ 1,234│ │ 89% │ │ 456 │ │ +72 │ │
│ │ ↓8% │ │ ↑3% │ │ ↓12% │ │ ↑5 │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
├─────────────────────────────────────────────┤
│ 📊 Charts │
│ • Multi-hospital comparison trends │
│ • Complaint category breakdown │
│ • Satisfaction heatmap by department │
│ • Resolution time distribution │
│ • NPS trend across hospitals │
├─────────────────────────────────────────────┤
│ 🏆 Hospital Leaderboard │
│ 1. Hospital A - 92% satisfaction │
│ 2. Hospital C - 87% satisfaction │
│ 3. Hospital B - 81% satisfaction ⚠️ │
├─────────────────────────────────────────────┤
│ 💡 AI Recommendations │
│ • Implement staff training in Emergency │
│ • Review communication protocols │
│ • Increase survey follow-up rate │
├─────────────────────────────────────────────┤
│ ❓ Ask AI (Q&A Input) │
│ [Type your question...] │
└─────────────────────────────────────────────┘
```
#### **5. PDF Report Generation**
- Weekly Executive Summary (auto-generated, emailed)
- Monthly Deep Analysis Report
- Custom date range reports
- Professional formatting with charts
- Bilingual support
#### **6. Implementation Steps**
**Phase 1: Foundation** (Week 1)
1. Create `Executive` role and permissions
2. Create `apps/executive_summary/` app structure
3. Build data aggregation service
4. Create basic dashboard view with metrics
**Phase 2: AI Integration** (Week 2)
5. Implement AI Narrative Service using LiteLLM
6. Build predictive analytics service
7. Create recommendation engine
8. Add AI brief generation to dashboard
**Phase 3: Advanced Features** (Week 3)
9. Implement Natural Language Q&A
10. Build predictive insights detection
11. Create hospital comparison views
12. Add risk scoring system
**Phase 4: Reports & Mobile** (Week 4)
13. Implement PDF report generation (WeasyPrint already installed)
14. Create Celery scheduled tasks for reports
15. Build mobile-optimized view
16. Add email delivery system
**Phase 5: Polish** (Week 5)
17. Add interactive charts (ApexCharts)
18. Implement real-time updates (HTMX)
19. Add Arabic translations
20. Testing and optimization
---
### **Technical Decisions**
1. **AI Model**: Use existing LiteLLM + Gemma setup for narratives
2. **Data Aggregation**: Pre-compute metrics daily for performance
3. **Caching**: Redis caching for dashboard data
4. **PDF**: WeasyPrint (already installed) for report generation
5. **Charts**: ApexCharts (already in use) for consistency
6. **Mobile**: Responsive TailwindCSS design
7. **Async**: Celery for AI generation and reports
8. **Security**: Role-based access, audit logging
---
### **Key Benefits**
**Leverages existing AI infrastructure** (LiteLLM, Celery, Redis)
**Consistent with current architecture** (Django patterns, HTMX, ApexCharts)
**Bilingual** (English/Arabic) for Saudi healthcare context
**Multi-tenant** (respects existing hospital filtering)
**Scalable** (async processing, caching)
**Actionable** (not just data, but insights and recommendations)
---
Would you like me to proceed with this implementation? I can start with Phase 1 (Foundation) and work through each phase systematically.
### Tool: ExitPlanMode: Plan:
**Status**: completed
**Input:**
```json
{
"plan": "## Executive Summary Implementation Plan\n\n### Overview\nBuild an AI-powered Executive Summary system for top management with real-time dashboards, automated narratives, predictive insights, recommendations, natural language Q&A, and PDF reports.\n\n### Phase 1: Foundation\n1. Create `Executive` role with system-wide access permissions\n2. Create new Django app `apps/executive_summary/`\n3. Build data aggregation service for cross-hospital metrics\n4. Create main Executive Dashboard view with KPI cards, charts, and filters\n\n### Phase 2: AI Integration\n5. Implement AI Narrative Service using existing LiteLLM infrastructure\n6. Build predictive analytics service (trend analysis, risk detection)\n7. Create recommendation engine based on historical patterns\n8. Integrate AI-generated briefs into dashboard\n\n### Phase 3: Advanced Features\n9. Natural Language Q&A interface (convert questions to data queries)\n10. Predictive insights detection and risk scoring\n11. Hospital comparison and leaderboard views\n12. Multi-hospital comparison charts\n\n### Phase 4: Reports & Mobile\n13. PDF report generation using WeasyPrint (weekly/monthly executive reports)\n14. Celery scheduled tasks for automated report generation and email delivery\n15. Mobile-optimized responsive view\n16. Email delivery system for scheduled reports\n\n### Phase 5: Polish\n17. Interactive ApexCharts visualizations\n18. HTMX for real-time updates\n19. Arabic translations and RTL support\n20. Performance optimization and testing\n\n### Technical Approach\n- Use existing LiteLLM + Gemma for AI narratives\n- Leverage Celery + Redis for async processing\n- Use WeasyPrint for PDF generation (already installed)\n- Follow existing Django patterns (CommandCenterView, HTMX, ApexCharts)\n- Maintain multi-tenancy and RBAC consistency\n- Bilingual support (English/Arabic)"
}
```
**Output:**
```
User has approved your plan. You can now start coding. Start with updating your todo list if applicable.
```
## Assistant
Now that the plan is approved, I need to create a structured todo list to track all the implementation tasks organized by phase.
## Assistant
Excellent! Let me start implementing the Executive Summary feature. I'll create a comprehensive todo list and begin with Phase 1.
### Tool: TodoWrite
**Status**: completed
**Output:**
```
- [ ] Create Executive role and permissions
- [ ] Create executive_summary Django app structure (models, views, services, tasks)
- [ ] Build ExecutiveMetric and ExecutiveReport models
- [ ] Create ExecutiveSummaryService for data aggregation
- [ ] Build Executive Dashboard view with KPI cards and filters
- [ ] Create Executive Dashboard template with ApexCharts
- [ ] Add URL routing and sidebar navigation
- [ ] Implement AI Narrative Service using LiteLLM
- [ ] Build Predictive Analytics Service (trend analysis, risk detection)
- [ ] Create Recommendation Engine
- [ ] Integrate AI-generated briefs into dashboard
- [ ] Implement Natural Language Q&A interface
- [ ] Build Predictive Insights view and risk scoring
- [ ] Create Hospital comparison and leaderboard views
- [ ] Implement PDF report generation with WeasyPrint
- [ ] Create Celery scheduled tasks for automated reports
- [ ] Build mobile-optimized responsive view
- [ ] Add email delivery system for scheduled reports
- [ ] Add Arabic translations and RTL support
- [ ] Performance optimization and testing
```
## Assistant
Now that the plan is approved and todos are set up, I need to mark the first task as in_progress before starting the implementation.
### Tool: TodoWrite
**Status**: completed
**Output:**
```
- [-] Create Executive role and permissions
- [ ] Create executive_summary Django app structure (models, views, services, tasks)
- [ ] Build ExecutiveMetric and ExecutiveReport models
- [ ] Create ExecutiveSummaryService for data aggregation
- [ ] Build Executive Dashboard view with KPI cards and filters
- [ ] Create Executive Dashboard template with ApexCharts
- [ ] Add URL routing and sidebar navigation
- [ ] Implement AI Narrative Service using LiteLLM
- [ ] Build Predictive Analytics Service (trend analysis, risk detection)
- [ ] Create Recommendation Engine
- [ ] Integrate AI-generated briefs into dashboard
- [ ] Implement Natural Language Q&A interface
- [ ] Build Predictive Insights view and risk scoring
- [ ] Create Hospital comparison and leaderboard views
- [ ] Implement PDF report generation with WeasyPrint
- [ ] Create Celery scheduled tasks for automated reports
- [ ] Build mobile-optimized responsive view
- [ ] Add email delivery system for scheduled reports
- [ ] Add Arabic translations and RTL support
- [ ] Performance optimization and testing
```
## Assistant
Now that I'm starting work on the Executive role, I need to first read the existing accounts models to understand how roles are defined and how to add the new Executive role.
## Assistant
Let me start by reading the existing role/permission structure to add the Executive role properly.
### Tool: ReadFile: apps/accounts/models.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/accounts/models.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/accounts/models.py`
**Output:**
```python
"""
Accounts models - Custom User model and roles
"""
import uuid
from django.contrib.auth.models import AbstractUser, Group, Permission, BaseUserManager
from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.core.models import TimeStampedModel, UUIDModel
class UserManager(BaseUserManager):
"""
Custom user manager for email-based authentication.
"""
def create_user(self, email, password=None, **extra_fields):
"""
Create and save a regular user with the given email and password.
"""
if not email:
raise ValueError("The Email field must be set")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
"""
Create and save a superuser with the given email and password.
"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self.create_user(email, password, **extra_fields)
class User(AbstractUser, TimeStampedModel):
"""
Custom User model extending Django's AbstractUser.
Uses UUID as primary key and adds additional fields for PX360.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Override email to make it unique and required
email = models.EmailField(unique=True)
# Override username to be optional and non-unique (for backward compatibility)
# Note: Using email as USERNAME_FIELD for authentication
username = models.CharField(max_length=150, blank=True, default="", unique=False)
# Use email as username field for authentication
USERNAME_FIELD = "email"
# Required fields when creating superuser
REQUIRED_FIELDS = ["first_name", "last_name"]
# Custom user manager
objects = UserManager()
# Additional fields
phone = models.CharField(max_length=20, blank=True)
employee_id = models.CharField(max_length=50, blank=True)
# Organization relationships
hospital = models.ForeignKey(
"organizations.Hospital", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
)
department = models.ForeignKey(
"organizations.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
)
# Role - using Django's built-in Group for RBAC
# Groups will represent roles: PX Admin, Hospital Admin, Department Manager, etc.
# Profile
avatar = models.ImageField(upload_to="avatars/", null=True, blank=True)
bio = models.TextField(blank=True)
# Preferences
language = models.CharField(max_length=5, choices=[("en", "English"), ("ar", "Arabic")], default="en")
# Notification preferences
notification_email_enabled = models.BooleanField(default=True, help_text="Enable email notifications")
notification_sms_enabled = models.BooleanField(default=False, help_text="Enable SMS notifications")
preferred_notification_channel = models.CharField(
max_length=10,
choices=[("email", "Email"), ("sms", "SMS"), ("both", "Both")],
default="email",
help_text="Preferred notification channel for general notifications",
)
explanation_notification_channel = models.CharField(
max_length=10,
choices=[("email", "Email"), ("sms", "SMS"), ("both", "Both")],
default="email",
help_text="Preferred channel for explanation requests",
)
# Status
is_active = models.BooleanField(default=True)
# Onboarding / Acknowledgement
is_provisional = models.BooleanField(default=False, help_text="User is in onboarding process")
invitation_token = models.CharField(
max_length=100, unique=True, null=True, blank=True, help_text="Token for account activation"
)
invitation_expires_at = models.DateTimeField(null=True, blank=True, help_text="When the invitation token expires")
acknowledgement_completed = models.BooleanField(
default=False, help_text="User has completed acknowledgement wizard"
)
acknowledgement_completed_at = models.DateTimeField(
null=True, blank=True, help_text="When acknowledgement was completed"
)
current_wizard_step = models.IntegerField(default=0, help_text="Current step in onboarding wizard")
wizard_completed_steps = models.JSONField(default=list, blank=True, help_text="List of completed wizard step IDs")
class Meta:
ordering = ["-date_joined"]
indexes = [
models.Index(fields=["is_active", "-date_joined"]),
]
def __str__(self):
return f"{self.get_full_name()} ({self.email})"
def get_role_names(self):
"""Get list of role names for this user"""
return list(self.groups.values_list("name", flat=True))
def has_role(self, role_name):
"""Check if user has a specific role"""
return self.groups.filter(name=role_name).exists()
def is_px_admin(self):
"""Check if user is PX Admin"""
return self.has_role("PX Admin")
def is_hospital_admin(self):
"""Check if user is Hospital Admin"""
return self.has_role("Hospital Admin")
def is_department_manager(self):
"""Check if user is Department Manager"""
return self.has_role("Department Manager")
def is_px_staff(self):
"""Check if user is PX Staff"""
return self.has_role("PX Staff")
def is_source_user(self):
"""Check if user is a PX Source User"""
return self.has_role("PX Source User")
def get_source_user_profile_active(self):
"""Get active source user profile if exists"""
if hasattr(self, "source_user_profile"):
profile = self.source_user_profile
if profile and profile.is_active:
return profile
return None
def needs_onboarding(self):
"""Check if user needs to complete onboarding"""
return self.is_provisional and not self.acknowledgement_completed
def get_onboarding_progress_percentage(self):
"""Get onboarding progress percentage"""
from .services import OnboardingService
return OnboardingService.get_user_progress_percentage(self)
class AcknowledgementCategory(UUIDModel, TimeStampedModel):
"""
Categories/Departments for acknowledgements.
Admins can add, edit, or deactivate categories.
Replaces hardcoded department choices with database-driven categories.
"""
name_en = models.CharField(max_length=200)
name_ar = models.CharField(max_length=200, blank=True, help_text="Arabic name")
code = models.CharField(max_length=50, unique=True, help_text="Unique code (e.g., CLINICS, ADMISSIONS)")
description = models.TextField(blank=True, help_text="Category description")
# Display & ordering
icon = models.CharField(max_length=50, blank=True, help_text="Icon class (e.g., 'building', 'user')")
color = models.CharField(max_length=7, default="#007bbd", help_text="Hex color code")
order = models.IntegerField(default=0, help_text="Display order")
# Status
is_active = models.BooleanField(default=True, db_index=True)
is_default = models.BooleanField(default=False, help_text="One of the 14 default categories")
class Meta:
ordering = ["order", "name_en"]
verbose_name_plural = "Acknowledgement Categories"
indexes = [
models.Index(fields=["is_active", "order"]),
models.Index(fields=["is_default"]),
]
def __str__(self):
return self.name_en
def get_localized_name(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.name_ar:
return self.name_ar
return self.name_en
def activate(self):
self.is_active = True
self.save(update_fields=["is_active"])
def deactivate(self):
self.is_active = False
self.save(update_fields=["is_active"])
class AcknowledgementContent(UUIDModel, TimeStampedModel):
"""
Acknowledgement content sections.
Provides bilingual educational content for each acknowledgement category.
"""
# Link to category (replaces role field)
category = models.ForeignKey(
AcknowledgementCategory,
on_delete=models.CASCADE,
related_name="content_sections",
help_text="Category this content belongs to",
)
# Content section
code = models.CharField(max_length=100, unique=True, help_text="Unique code for this content section")
title_en = models.CharField(max_length=200)
title_ar = models.CharField(max_length=200, blank=True)
description_en = models.TextField()
description_ar = models.TextField(blank=True)
# Content details
content_en = models.TextField(blank=True)
content_ar = models.TextField(blank=True)
# Visual elements
icon = models.CharField(max_length=50, blank=True, help_text="Icon class (e.g., 'fa-user', 'fa-shield')")
color = models.CharField(max_length=7, blank=True, help_text="Hex color code (e.g., '#007bff')")
# Organization
order = models.IntegerField(default=0, help_text="Display order")
# Status
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["category", "order", "code"]
indexes = [
models.Index(fields=["category", "is_active", "order"]),
]
def __str__(self):
return f"{self.category.name_en} - {self.title_en}"
def get_localized_title(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.title_ar:
return self.title_ar
return self.title_en
def get_localized_description(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.description_ar:
return self.description_ar
return self.description_en
def get_localized_content(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.content_ar:
return self.content_ar
return self.content_en
class AcknowledgementChecklistItem(UUIDModel, TimeStampedModel):
"""
Checklist items that users must acknowledge.
Each item is linked to a category (department).
"""
# Link to category (replaces role field)
category = models.ForeignKey(
AcknowledgementCategory,
on_delete=models.PROTECT,
related_name="checklist_items",
help_text="Category/Department this acknowledgement belongs to",
)
# Linked content (optional)
content = models.ForeignKey(
AcknowledgementContent,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="checklist_items",
help_text="Related content section",
)
# Item details
code = models.CharField(max_length=100, unique=True, help_text="Unique code for this checklist item")
text_en = models.CharField(max_length=500)
text_ar = models.CharField(max_length=500, blank=True)
description_en = models.TextField(blank=True)
description_ar = models.TextField(blank=True)
# Configuration
is_required = models.BooleanField(default=True, help_text="Item must be acknowledged")
order = models.IntegerField(default=0, help_text="Display order in checklist")
# Status
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["category", "order", "code"]
indexes = [
models.Index(fields=["category", "is_active", "order"]),
]
def __str__(self):
return f"{self.category.name_en} - {self.text_en}"
def get_localized_text(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.text_ar:
return self.text_ar
return self.text_en
def get_localized_description(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.description_ar:
return self.description_ar
return self.description_en
class UserAcknowledgement(UUIDModel, TimeStampedModel):
"""
Records of user acknowledgements.
Tracks when each user acknowledges specific items with digital signatures.
"""
# User
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="acknowledgements")
# Checklist item
checklist_item = models.ForeignKey(
AcknowledgementChecklistItem, on_delete=models.CASCADE, related_name="user_acknowledgements"
)
# Acknowledgement details
is_acknowledged = models.BooleanField(default=True)
acknowledged_at = models.DateTimeField(auto_now_add=True)
# Digital signature
signature = models.TextField(blank=True, help_text="Digital signature data (base64 encoded)")
signature_ip = models.GenericIPAddressField(null=True, blank=True, help_text="IP address when signed")
signature_user_agent = models.TextField(blank=True, help_text="User agent when signed")
# PDF document
pdf_file = models.FileField(
upload_to="acknowledgements/pdfs/", null=True, blank=True, help_text="PDF document of signed acknowledgement"
)
# Metadata
metadata = models.JSONField(default=dict, blank=True, help_text="Additional metadata")
class Meta:
ordering = ["-acknowledged_at"]
unique_together = [["user", "checklist_item"]]
indexes = [
models.Index(fields=["user", "-acknowledged_at"]),
models.Index(fields=["checklist_item", "-acknowledged_at"]),
]
def __str__(self):
return f"{self.user.email} - {self.checklist_item.text_en}"
class UserProvisionalLog(UUIDModel, TimeStampedModel):
"""
Audit trail for provisional user lifecycle.
Tracks all key events in the onboarding process.
"""
EVENT_TYPES = [
("created", _("User Created")),
("invitation_sent", _("Invitation Sent")),
("invitation_resent", _("Invitation Resent")),
("wizard_started", _("Wizard Started")),
("step_completed", _("Wizard Step Completed")),
("wizard_completed", _("Wizard Completed")),
("user_activated", _("User Activated")),
("invitation_expired", _("Invitation Expired")),
]
# User
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="provisional_logs")
# Event details
event_type = models.CharField(max_length=50, choices=EVENT_TYPES, db_index=True)
description = models.TextField()
# Context
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
# Additional data
metadata = models.JSONField(default=dict, blank=True, help_text="Additional event data")
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["user", "-created_at"]),
models.Index(fields=["event_type", "-created_at"]),
]
def __str__(self):
return f"{self.user.email} - {self.get_event_type_display()} - {self.created_at}"
class Role(models.Model):
"""
Role model for managing predefined roles and their permissions.
This is a helper model - actual role assignment uses Django Groups.
"""
ROLE_CHOICES = [
("px_admin", _("PX Admin")),
("hospital_admin", _("Hospital Admin")),
("department_manager", _("Department Manager")),
("px_staff", _("PX Staff")),
("physician", _("Physician")),
("nurse", _("Nurse")),
("staff", _("Staff")),
("viewer", _("Viewer")),
]
name = models.CharField(max_length=50, unique=True, choices=ROLE_CHOICES)
display_name = models.CharField(max_length=100)
description = models.TextField(blank=True)
# Link to Django Group
group = models.OneToOneField(Group, on_delete=models.CASCADE, related_name="role_config")
# Permissions
permissions = models.ManyToManyField(Permission, blank=True)
# Hierarchy level (for escalation logic)
level = models.IntegerField(default=0, help_text="Higher number = higher authority")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-level", "name"]
def __str__(self):
return self.display_name
# Import version models to ensure they are registered with Django
from .version_models import ContentVersion, ChecklistItemVersion, ContentChangeLog
# ============================================================================
# SIMPLE ACKNOWLEDGEMENT MODELS
# ============================================================================
class SimpleAcknowledgement(UUIDModel, TimeStampedModel):
"""
Simple acknowledgement item that employees must sign.
"""
title = models.CharField(max_length=200, help_text="Acknowledgement title")
description = models.TextField(blank=True, help_text="Detailed description")
pdf_document = models.FileField(
upload_to="acknowledgements/documents/", null=True, blank=True, help_text="PDF document for employees to review"
)
# Status
is_active = models.BooleanField(default=True, help_text="Show in employee checklist")
is_required = models.BooleanField(default=True, help_text="Must be signed by all employees")
# Display order
order = models.IntegerField(default=0)
class Meta:
ordering = ["order", "title"]
verbose_name = "Acknowledgement"
verbose_name_plural = "Acknowledgements"
def __str__(self):
return self.title
@property
def signed_count(self):
"""Number of employees who have signed this acknowledgement."""
return self.employee_signatures.filter(is_signed=True).count()
class EmployeeAcknowledgement(UUIDModel, TimeStampedModel):
"""
Records which employees have signed which acknowledgements.
"""
employee = models.ForeignKey(
"accounts.User",
on_delete=models.CASCADE,
related_name="employee_acknowledgements",
help_text="Employee who signed",
)
acknowledgement = models.ForeignKey(
SimpleAcknowledgement,
on_delete=models.CASCADE,
related_name="employee_signatures",
help_text="Acknowledgement that was signed",
)
# Sent tracking
sent_at = models.DateTimeField(null=True, blank=True, help_text="When acknowledgement was sent to employee")
sent_by = models.ForeignKey(
"accounts.User",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="sent_acknowledgements",
help_text="Admin who sent this acknowledgement",
)
# Signature details
is_signed = models.BooleanField(default=False)
signed_at = models.DateTimeField(null=True, blank=True)
signature_name = models.CharField(max_length=200, blank=True, help_text="Name used when signing")
signature_employee_id = models.CharField(max_length=100, blank=True, help_text="Employee ID when signing")
# Signed PDF
signed_pdf = models.FileField(
upload_to="acknowledgements/signed/%Y/%m/", null=True, blank=True, help_text="PDF with employee signature"
)
# IP and user agent for audit
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
# Notes
notes = models.TextField(blank=True, help_text="Admin notes about this acknowledgement")
class Meta:
ordering = ["-sent_at", "-signed_at"]
unique_together = [["employee", "acknowledgement"]]
verbose_name = "Employee Acknowledgement"
verbose_name_plural = "Employee Acknowledgements"
def __str__(self):
if self.is_signed:
return f"✓ {self.employee.get_full_name()} - {self.acknowledgement.title}"
elif self.sent_at:
return f"→ {self.employee.get_full_name()} - {self.acknowledgement.title} (Sent)"
return f"○ {self.employee.get_full_name()} - {self.acknowledgement.title}"
```
### Tool: ReadFile: apps/accounts/permissions.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/accounts/permissions.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/accounts/permissions.py`
**Output:**
```python
"""
Accounts permissions - RBAC permission classes
"""
from rest_framework import permissions
class IsPXAdmin(permissions.BasePermission):
"""
Permission class to check if user is PX Admin.
PX Admins have full system access.
"""
message = "You must be a PX Admin to perform this action."
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and request.user.is_px_admin()
class IsHospitalAdmin(permissions.BasePermission):
"""
Permission class to check if user is Hospital Admin.
Hospital Admins have access to their hospital's data.
"""
message = "You must be a Hospital Admin to perform this action."
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and request.user.is_hospital_admin()
class IsDepartmentManager(permissions.BasePermission):
"""
Permission class to check if user is Department Manager.
Department Managers have access to their department's data.
"""
message = "You must be a Department Manager to perform this action."
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and request.user.is_department_manager()
class IsPXAdminOrHospitalAdmin(permissions.BasePermission):
"""
Permission class for PX Admin or Hospital Admin.
"""
message = "You must be a PX Admin or Hospital Admin to perform this action."
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
return request.user.is_px_admin() or request.user.is_hospital_admin()
class IsPXAdminOrReadOnly(permissions.BasePermission):
"""
Permission class that allows PX Admins full access,
but only read access for others.
"""
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
# Read permissions for any authenticated user
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions only for PX Admins
return request.user.is_px_admin()
class IsOwnerOrPXAdmin(permissions.BasePermission):
"""
Permission class that allows users to access their own data,
or PX Admins to access any data.
"""
def has_object_permission(self, request, view, obj):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins can access anything
if request.user.is_px_admin():
return True
# Users can access their own data
if hasattr(obj, 'user'):
return obj.user == request.user
return obj == request.user
class HasRolePermission(permissions.BasePermission):
"""
Permission class that checks if user has specific role.
Usage: Set required_roles on the view.
"""
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
required_roles = getattr(view, 'required_roles', [])
if not required_roles:
return True
user_roles = request.user.get_role_names()
return any(role in user_roles for role in required_roles)
class CanAccessHospitalData(permissions.BasePermission):
"""
Permission class that checks if user can access hospital data.
- PX Admins can access all hospitals
- Hospital Admins can access their own hospital
- Department Managers can access their hospital
"""
def has_object_permission(self, request, view, obj):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins can access all
if request.user.is_px_admin():
return True
# Get hospital from object
hospital = None
if hasattr(obj, 'hospital'):
hospital = obj.hospital
elif obj.__class__.__name__ == 'Hospital':
hospital = obj
if not hospital:
return False
# Check if user belongs to this hospital
return request.user.hospital == hospital
class CanAccessDepartmentData(permissions.BasePermission):
"""
Permission class that checks if user can access department data.
- PX Admins can access all departments
- Hospital Admins can access departments in their hospital
- Department Managers can access their own department
"""
def has_object_permission(self, request, view, obj):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins can access all
if request.user.is_px_admin():
return True
# Get department from object
department = None
if hasattr(obj, 'department'):
department = obj.department
elif obj.__class__.__name__ == 'Department':
department = obj
if not department:
return False
# Hospital Admins can access departments in their hospital
if request.user.is_hospital_admin() and request.user.hospital == department.hospital:
return True
# Department Managers can access their own department
if request.user.is_department_manager() and request.user.department == department:
return True
return False
class IsSourceUser(permissions.BasePermission):
"""
Permission class to check if user is a PX Source User.
Source Users can only access data from their assigned source.
"""
message = "You must be a Source User to perform this action."
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
request.user.is_source_user()
)
class IsSourceUserOrAdmin(permissions.BasePermission):
"""
Permission class for Source Users or Admins.
Allows access to:
- PX Admins
- Hospital Admins
- Department Managers
- Active Source Users
"""
message = "You must be a Source User or Admin to perform this action."
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
# Admins have access
if (request.user.is_px_admin() or
request.user.is_hospital_admin() or
request.user.is_department_manager()):
return True
# Source Users have access
return request.user.is_source_user()
class IsSourceUserOwnData(permissions.BasePermission):
"""
Object-level permission to ensure source users can only access their own data.
"""
message = "You can only access data you created."
def has_object_permission(self, request, view, obj):
if not (request.user and request.user.is_authenticated):
return False
# Admins can access everything
if (request.user.is_px_admin() or
request.user.is_hospital_admin() or
request.user.is_department_manager()):
return True
# Source Users can only access their own created data
if request.user.is_source_user():
return getattr(obj, 'created_by', None) == request.user
return False
# ==================== Onboarding Permissions ====================
class IsProvisionalUser(permissions.BasePermission):
"""
Permission class to check if user is provisional (in onboarding).
"""
message = "You must be a provisional user to access this resource."
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
request.user.is_provisional and
not request.user.acknowledgement_completed
)
class CanViewOnboarding(permissions.BasePermission):
"""
Permission class for viewing onboarding content.
- Provisional users can view their own onboarding
- PX Admins can view all onboarding data
"""
message = "You do not have permission to view onboarding data."
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
# Provisional users can view their own
if request.user.is_provisional:
return True
# PX Admins can view all
if request.user.is_px_admin():
return True
return False
class CanManageOnboarding(permissions.BasePermission):
"""
Permission class for managing onboarding.
- PX Admins have full access
- Hospital Admins can manage onboarding for their hospital
"""
message = "You must be a PX Admin or Hospital Admin to manage onboarding."
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins have full access
if request.user.is_px_admin():
return True
# Hospital Admins can manage their hospital
if request.user.is_hospital_admin():
return True
return False
class IsOnboardingOwnerOrAdmin(permissions.BasePermission):
"""
Permission class for onboarding object access.
- Users can access their own onboarding data
- PX Admins can access all
"""
def has_object_permission(self, request, view, obj):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins can access all
if request.user.is_px_admin():
return True
# Users can access their own data
if hasattr(obj, 'user'):
return obj.user == request.user
return obj == request.user
class CanManageAcknowledgementContent(permissions.BasePermission):
"""
Permission class for managing acknowledgement content and checklist items.
Only PX Admins can manage these.
"""
message = "Only PX Admins can manage acknowledgement content."
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins can manage content
if request.user.is_px_admin():
return True
# Read-only for others
if request.method in permissions.SAFE_METHODS:
return True
return False
```
## Assistant
Now I understand the role structure - I need to add 'Executive' to the ROLE_CHOICES in the Role model to create the new executive role.
## Assistant
Perfect! Now I'll add the Executive role. Let me update the Role model and add the helper method to User:
### Tool: Edit: apps/accounts/models.py: class Role(models.Model):... => class Role(models.Model):...
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/accounts/models.py",
"new_string": "class Role(models.Model):\n \"\"\"\n Role model for managing predefined roles and their permissions.\n This is a helper model - actual role assignment uses Django Groups.\n \"\"\"\n\n ROLE_CHOICES = [\n (\"px_admin\", _(\"PX Admin\")),\n (\"hospital_admin\", _(\"Hospital Admin\")),\n (\"department_manager\", _(\"Department Manager\")),\n (\"px_staff\", _(\"PX Staff\")),\n (\"physician\", _(\"Physician\")),\n (\"nurse\", _(\"Nurse\")),\n (\"staff\", _(\"Staff\")),\n (\"viewer\", _(\"Viewer\")),\n (\"executive\", _(\"Executive\")),\n ]",
"old_string": "class Role(models.Model):\n \"\"\"\n Role model for managing predefined roles and their permissions.\n This is a helper model - actual role assignment uses Django Groups.\n \"\"\"\n\n ROLE_CHOICES = [\n (\"px_admin\", _(\"PX Admin\")),\n (\"hospital_admin\", _(\"Hospital Admin\")),\n (\"department_manager\", _(\"Department Manager\")),\n (\"px_staff\", _(\"PX Staff\")),\n (\"physician\", _(\"Physician\")),\n (\"nurse\", _(\"Nurse\")),\n (\"staff\", _(\"Staff\")),\n (\"viewer\", _(\"Viewer\")),\n ]"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/accounts/models.py`
**Output:**
*Diff for `models.py`:*
```diff
"""
Accounts models - Custom User model and roles
"""
import uuid
from django.contrib.auth.models import AbstractUser, Group, Permission, BaseUserManager
from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.core.models import TimeStampedModel, UUIDModel
class UserManager(BaseUserManager):
"""
Custom user manager for email-based authentication.
"""
def create_user(self, email, password=None, **extra_fields):
"""
Create and save a regular user with the given email and password.
"""
if not email:
raise ValueError("The Email field must be set")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
"""
Create and save a superuser with the given email and password.
"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self.create_user(email, password, **extra_fields)
class User(AbstractUser, TimeStampedModel):
"""
Custom User model extending Django's AbstractUser.
Uses UUID as primary key and adds additional fields for PX360.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Override email to make it unique and required
email = models.EmailField(unique=True)
# Override username to be optional and non-unique (for backward compatibility)
# Note: Using email as USERNAME_FIELD for authentication
username = models.CharField(max_length=150, blank=True, default="", unique=False)
# Use email as username field for authentication
USERNAME_FIELD = "email"
# Required fields when creating superuser
REQUIRED_FIELDS = ["first_name", "last_name"]
# Custom user manager
objects = UserManager()
# Additional fields
phone = models.CharField(max_length=20, blank=True)
employee_id = models.CharField(max_length=50, blank=True)
# Organization relationships
hospital = models.ForeignKey(
"organizations.Hospital", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
)
department = models.ForeignKey(
"organizations.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
)
# Role - using Django's built-in Group for RBAC
# Groups will represent roles: PX Admin, Hospital Admin, Department Manager, etc.
# Profile
avatar = models.ImageField(upload_to="avatars/", null=True, blank=True)
bio = models.TextField(blank=True)
# Preferences
language = models.CharField(max_length=5, choices=[("en", "English"), ("ar", "Arabic")], default="en")
# Notification preferences
notification_email_enabled = models.BooleanField(default=True, help_text="Enable email notifications")
notification_sms_enabled = models.BooleanField(default=False, help_text="Enable SMS notifications")
preferred_notification_channel = models.CharField(
max_length=10,
choices=[("email", "Email"), ("sms", "SMS"), ("both", "Both")],
default="email",
help_text="Preferred notification channel for general notifications",
)
explanation_notification_channel = models.CharField(
max_length=10,
choices=[("email", "Email"), ("sms", "SMS"), ("both", "Both")],
default="email",
help_text="Preferred channel for explanation requests",
)
# Status
is_active = models.BooleanField(default=True)
# Onboarding / Acknowledgement
is_provisional = models.BooleanField(default=False, help_text="User is in onboarding process")
invitation_token = models.CharField(
max_length=100, unique=True, null=True, blank=True, help_text="Token for account activation"
)
invitation_expires_at = models.DateTimeField(null=True, blank=True, help_text="When the invitation token expires")
acknowledgement_completed = models.BooleanField(
default=False, help_text="User has completed acknowledgement wizard"
)
acknowledgement_completed_at = models.DateTimeField(
null=True, blank=True, help_text="When acknowledgement was completed"
)
current_wizard_step = models.IntegerField(default=0, help_text="Current step in onboarding wizard")
wizard_completed_steps = models.JSONField(default=list, blank=True, help_text="List of completed wizard step IDs")
class Meta:
ordering = ["-date_joined"]
indexes = [
models.Index(fields=["is_active", "-date_joined"]),
]
def __str__(self):
return f"{self.get_full_name()} ({self.email})"
def get_role_names(self):
"""Get list of role names for this user"""
return list(self.groups.values_list("name", flat=True))
def has_role(self, role_name):
"""Check if user has a specific role"""
return self.groups.filter(name=role_name).exists()
def is_px_admin(self):
"""Check if user is PX Admin"""
return self.has_role("PX Admin")
def is_hospital_admin(self):
"""Check if user is Hospital Admin"""
return self.has_role("Hospital Admin")
def is_department_manager(self):
"""Check if user is Department Manager"""
return self.has_role("Department Manager")
def is_px_staff(self):
"""Check if user is PX Staff"""
return self.has_role("PX Staff")
def is_source_user(self):
"""Check if user is a PX Source User"""
return self.has_role("PX Source User")
def get_source_user_profile_active(self):
"""Get active source user profile if exists"""
if hasattr(self, "source_user_profile"):
profile = self.source_user_profile
if profile and profile.is_active:
return profile
return None
def needs_onboarding(self):
"""Check if user needs to complete onboarding"""
return self.is_provisional and not self.acknowledgement_completed
def get_onboarding_progress_percentage(self):
"""Get onboarding progress percentage"""
from .services import OnboardingService
return OnboardingService.get_user_progress_percentage(self)
class AcknowledgementCategory(UUIDModel, TimeStampedModel):
"""
Categories/Departments for acknowledgements.
Admins can add, edit, or deactivate categories.
Replaces hardcoded department choices with database-driven categories.
"""
name_en = models.CharField(max_length=200)
name_ar = models.CharField(max_length=200, blank=True, help_text="Arabic name")
code = models.CharField(max_length=50, unique=True, help_text="Unique code (e.g., CLINICS, ADMISSIONS)")
description = models.TextField(blank=True, help_text="Category description")
# Display & ordering
icon = models.CharField(max_length=50, blank=True, help_text="Icon class (e.g., 'building', 'user')")
color = models.CharField(max_length=7, default="#007bbd", help_text="Hex color code")
order = models.IntegerField(default=0, help_text="Display order")
# Status
is_active = models.BooleanField(default=True, db_index=True)
is_default = models.BooleanField(default=False, help_text="One of the 14 default categories")
class Meta:
ordering = ["order", "name_en"]
verbose_name_plural = "Acknowledgement Categories"
indexes = [
models.Index(fields=["is_active", "order"]),
models.Index(fields=["is_default"]),
]
def __str__(self):
return self.name_en
def get_localized_name(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.name_ar:
return self.name_ar
return self.name_en
def activate(self):
self.is_active = True
self.save(update_fields=["is_active"])
def deactivate(self):
self.is_active = False
self.save(update_fields=["is_active"])
class AcknowledgementContent(UUIDModel, TimeStampedModel):
"""
Acknowledgement content sections.
Provides bilingual educational content for each acknowledgement category.
"""
# Link to category (replaces role field)
category = models.ForeignKey(
AcknowledgementCategory,
on_delete=models.CASCADE,
related_name="content_sections",
help_text="Category this content belongs to",
)
# Content section
code = models.CharField(max_length=100, unique=True, help_text="Unique code for this content section")
title_en = models.CharField(max_length=200)
title_ar = models.CharField(max_length=200, blank=True)
description_en = models.TextField()
description_ar = models.TextField(blank=True)
# Content details
content_en = models.TextField(blank=True)
content_ar = models.TextField(blank=True)
# Visual elements
icon = models.CharField(max_length=50, blank=True, help_text="Icon class (e.g., 'fa-user', 'fa-shield')")
color = models.CharField(max_length=7, blank=True, help_text="Hex color code (e.g., '#007bff')")
# Organization
order = models.IntegerField(default=0, help_text="Display order")
# Status
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["category", "order", "code"]
indexes = [
models.Index(fields=["category", "is_active", "order"]),
]
def __str__(self):
return f"{self.category.name_en} - {self.title_en}"
def get_localized_title(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.title_ar:
return self.title_ar
return self.title_en
def get_localized_description(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.description_ar:
return self.description_ar
return self.description_en
def get_localized_content(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.content_ar:
return self.content_ar
return self.content_en
class AcknowledgementChecklistItem(UUIDModel, TimeStampedModel):
"""
Checklist items that users must acknowledge.
Each item is linked to a category (department).
"""
# Link to category (replaces role field)
category = models.ForeignKey(
AcknowledgementCategory,
on_delete=models.PROTECT,
related_name="checklist_items",
help_text="Category/Department this acknowledgement belongs to",
)
# Linked content (optional)
content = models.ForeignKey(
AcknowledgementContent,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="checklist_items",
help_text="Related content section",
)
# Item details
code = models.CharField(max_length=100, unique=True, help_text="Unique code for this checklist item")
text_en = models.CharField(max_length=500)
text_ar = models.CharField(max_length=500, blank=True)
description_en = models.TextField(blank=True)
description_ar = models.TextField(blank=True)
# Configuration
is_required = models.BooleanField(default=True, help_text="Item must be acknowledged")
order = models.IntegerField(default=0, help_text="Display order in checklist")
# Status
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["category", "order", "code"]
indexes = [
models.Index(fields=["category", "is_active", "order"]),
]
def __str__(self):
return f"{self.category.name_en} - {self.text_en}"
def get_localized_text(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.text_ar:
return self.text_ar
return self.text_en
def get_localized_description(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.description_ar:
return self.description_ar
return self.description_en
class UserAcknowledgement(UUIDModel, TimeStampedModel):
"""
Records of user acknowledgements.
Tracks when each user acknowledges specific items with digital signatures.
"""
# User
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="acknowledgements")
# Checklist item
checklist_item = models.ForeignKey(
AcknowledgementChecklistItem, on_delete=models.CASCADE, related_name="user_acknowledgements"
)
# Acknowledgement details
is_acknowledged = models.BooleanField(default=True)
acknowledged_at = models.DateTimeField(auto_now_add=True)
# Digital signature
signature = models.TextField(blank=True, help_text="Digital signature data (base64 encoded)")
signature_ip = models.GenericIPAddressField(null=True, blank=True, help_text="IP address when signed")
signature_user_agent = models.TextField(blank=True, help_text="User agent when signed")
# PDF document
pdf_file = models.FileField(
upload_to="acknowledgements/pdfs/", null=True, blank=True, help_text="PDF document of signed acknowledgement"
)
# Metadata
metadata = models.JSONField(default=dict, blank=True, help_text="Additional metadata")
class Meta:
ordering = ["-acknowledged_at"]
unique_together = [["user", "checklist_item"]]
indexes = [
models.Index(fields=["user", "-acknowledged_at"]),
models.Index(fields=["checklist_item", "-acknowledged_at"]),
]
def __str__(self):
return f"{self.user.email} - {self.checklist_item.text_en}"
class UserProvisionalLog(UUIDModel, TimeStampedModel):
"""
Audit trail for provisional user lifecycle.
Tracks all key events in the onboarding process.
"""
EVENT_TYPES = [
("created", _("User Created")),
("invitation_sent", _("Invitation Sent")),
("invitation_resent", _("Invitation Resent")),
("wizard_started", _("Wizard Started")),
("step_completed", _("Wizard Step Completed")),
("wizard_completed", _("Wizard Completed")),
("user_activated", _("User Activated")),
("invitation_expired", _("Invitation Expired")),
]
# User
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="provisional_logs")
# Event details
event_type = models.CharField(max_length=50, choices=EVENT_TYPES, db_index=True)
description = models.TextField()
# Context
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
# Additional data
metadata = models.JSONField(default=dict, blank=True, help_text="Additional event data")
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["user", "-created_at"]),
models.Index(fields=["event_type", "-created_at"]),
]
def __str__(self):
return f"{self.user.email} - {self.get_event_type_display()} - {self.created_at}"
class Role(models.Model):
"""
Role model for managing predefined roles and their permissions.
This is a helper model - actual role assignment uses Django Groups.
"""
ROLE_CHOICES = [
("px_admin", _("PX Admin")),
("hospital_admin", _("Hospital Admin")),
("department_manager", _("Department Manager")),
("px_staff", _("PX Staff")),
("physician", _("Physician")),
("nurse", _("Nurse")),
("staff", _("Staff")),
("viewer", _("Viewer")),
("executive", _("Executive")),
]
name = models.CharField(max_length=50, unique=True, choices=ROLE_CHOICES)
display_name = models.CharField(max_length=100)
description = models.TextField(blank=True)
# Link to Django Group
group = models.OneToOneField(Group, on_delete=models.CASCADE, related_name="role_config")
# Permissions
permissions = models.ManyToManyField(Permission, blank=True)
# Hierarchy level (for escalation logic)
level = models.IntegerField(default=0, help_text="Higher number = higher authority")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-level", "name"]
def __str__(self):
return self.display_name
# Import version models to ensure they are registered with Django
from .version_models import ContentVersion, ChecklistItemVersion, ContentChangeLog
# ============================================================================
# SIMPLE ACKNOWLEDGEMENT MODELS
# ============================================================================
class SimpleAcknowledgement(UUIDModel, TimeStampedModel):
"""
Simple acknowledgement item that employees must sign.
"""
title = models.CharField(max_length=200, help_text="Acknowledgement title")
description = models.TextField(blank=True, help_text="Detailed description")
pdf_document = models.FileField(
upload_to="acknowledgements/documents/", null=True, blank=True, help_text="PDF document for employees to review"
)
# Status
is_active = models.BooleanField(default=True, help_text="Show in employee checklist")
is_required = models.BooleanField(default=True, help_text="Must be signed by all employees")
# Display order
order = models.IntegerField(default=0)
class Meta:
ordering = ["order", "title"]
verbose_name = "Acknowledgement"
verbose_name_plural = "Acknowledgements"
def __str__(self):
return self.title
@property
def signed_count(self):
"""Number of employees who have signed this acknowledgement."""
return self.employee_signatures.filter(is_signed=True).count()
class EmployeeAcknowledgement(UUIDModel, TimeStampedModel):
"""
Records which employees have signed which acknowledgements.
"""
employee = models.ForeignKey(
"accounts.User",
on_delete=models.CASCADE,
related_name="employee_acknowledgements",
help_text="Employee who signed",
)
acknowledgement = models.ForeignKey(
SimpleAcknowledgement,
on_delete=models.CASCADE,
related_name="employee_signatures",
help_text="Acknowledgement that was signed",
)
# Sent tracking
sent_at = models.DateTimeField(null=True, blank=True, help_text="When acknowledgement was sent to employee")
sent_by = models.ForeignKey(
"accounts.User",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="sent_acknowledgements",
help_text="Admin who sent this acknowledgement",
)
# Signature details
is_signed = models.BooleanField(default=False)
signed_at = models.DateTimeField(null=True, blank=True)
signature_name = models.CharField(max_length=200, blank=True, help_text="Name used when signing")
signature_employee_id = models.CharField(max_length=100, blank=True, help_text="Employee ID when signing")
# Signed PDF
signed_pdf = models.FileField(
upload_to="acknowledgements/signed/%Y/%m/", null=True, blank=True, help_text="PDF with employee signature"
)
# IP and user agent for audit
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
# Notes
notes = models.TextField(blank=True, help_text="Admin notes about this acknowledgement")
class Meta:
ordering = ["-sent_at", "-signed_at"]
unique_together = [["employee", "acknowledgement"]]
verbose_name = "Employee Acknowledgement"
verbose_name_plural = "Employee Acknowledgements"
def __str__(self):
if self.is_signed:
return f"✓ {self.employee.get_full_name()} - {self.acknowledgement.title}"
elif self.sent_at:
return f"→ {self.employee.get_full_name()} - {self.acknowledgement.title} (Sent)"
return f"○ {self.employee.get_full_name()} - {self.acknowledgement.title}"
```
## Assistant
Now I need to add the `is_executive()` helper method to the User model to match the pattern of other role checking methods.
### Tool: Edit: apps/accounts/models.py: def is_source_user(self):... => def is_source_user(self):...
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/accounts/models.py",
"new_string": " def is_source_user(self):\n \"\"\"Check if user is a PX Source User\"\"\"\n return self.has_role(\"PX Source User\")\n\n def is_executive(self):\n \"\"\"Check if user is Executive (C-Suite/Top Management)\"\"\"\n return self.has_role(\"Executive\")",
"old_string": " def is_source_user(self):\n \"\"\"Check if user is a PX Source User\"\"\"\n return self.has_role(\"PX Source User\")"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/accounts/models.py`
**Output:**
*Diff for `models.py`:*
```diff
"""
Accounts models - Custom User model and roles
"""
import uuid
from django.contrib.auth.models import AbstractUser, Group, Permission, BaseUserManager
from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.core.models import TimeStampedModel, UUIDModel
class UserManager(BaseUserManager):
"""
Custom user manager for email-based authentication.
"""
def create_user(self, email, password=None, **extra_fields):
"""
Create and save a regular user with the given email and password.
"""
if not email:
raise ValueError("The Email field must be set")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
"""
Create and save a superuser with the given email and password.
"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self.create_user(email, password, **extra_fields)
class User(AbstractUser, TimeStampedModel):
"""
Custom User model extending Django's AbstractUser.
Uses UUID as primary key and adds additional fields for PX360.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Override email to make it unique and required
email = models.EmailField(unique=True)
# Override username to be optional and non-unique (for backward compatibility)
# Note: Using email as USERNAME_FIELD for authentication
username = models.CharField(max_length=150, blank=True, default="", unique=False)
# Use email as username field for authentication
USERNAME_FIELD = "email"
# Required fields when creating superuser
REQUIRED_FIELDS = ["first_name", "last_name"]
# Custom user manager
objects = UserManager()
# Additional fields
phone = models.CharField(max_length=20, blank=True)
employee_id = models.CharField(max_length=50, blank=True)
# Organization relationships
hospital = models.ForeignKey(
"organizations.Hospital", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
)
department = models.ForeignKey(
"organizations.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
)
# Role - using Django's built-in Group for RBAC
# Groups will represent roles: PX Admin, Hospital Admin, Department Manager, etc.
# Profile
avatar = models.ImageField(upload_to="avatars/", null=True, blank=True)
bio = models.TextField(blank=True)
# Preferences
language = models.CharField(max_length=5, choices=[("en", "English"), ("ar", "Arabic")], default="en")
# Notification preferences
notification_email_enabled = models.BooleanField(default=True, help_text="Enable email notifications")
notification_sms_enabled = models.BooleanField(default=False, help_text="Enable SMS notifications")
preferred_notification_channel = models.CharField(
max_length=10,
choices=[("email", "Email"), ("sms", "SMS"), ("both", "Both")],
default="email",
help_text="Preferred notification channel for general notifications",
)
explanation_notification_channel = models.CharField(
max_length=10,
choices=[("email", "Email"), ("sms", "SMS"), ("both", "Both")],
default="email",
help_text="Preferred channel for explanation requests",
)
# Status
is_active = models.BooleanField(default=True)
# Onboarding / Acknowledgement
is_provisional = models.BooleanField(default=False, help_text="User is in onboarding process")
invitation_token = models.CharField(
max_length=100, unique=True, null=True, blank=True, help_text="Token for account activation"
)
invitation_expires_at = models.DateTimeField(null=True, blank=True, help_text="When the invitation token expires")
acknowledgement_completed = models.BooleanField(
default=False, help_text="User has completed acknowledgement wizard"
)
acknowledgement_completed_at = models.DateTimeField(
null=True, blank=True, help_text="When acknowledgement was completed"
)
current_wizard_step = models.IntegerField(default=0, help_text="Current step in onboarding wizard")
wizard_completed_steps = models.JSONField(default=list, blank=True, help_text="List of completed wizard step IDs")
class Meta:
ordering = ["-date_joined"]
indexes = [
models.Index(fields=["is_active", "-date_joined"]),
]
def __str__(self):
return f"{self.get_full_name()} ({self.email})"
def get_role_names(self):
"""Get list of role names for this user"""
return list(self.groups.values_list("name", flat=True))
def has_role(self, role_name):
"""Check if user has a specific role"""
return self.groups.filter(name=role_name).exists()
def is_px_admin(self):
"""Check if user is PX Admin"""
return self.has_role("PX Admin")
def is_hospital_admin(self):
"""Check if user is Hospital Admin"""
return self.has_role("Hospital Admin")
def is_department_manager(self):
"""Check if user is Department Manager"""
return self.has_role("Department Manager")
def is_px_staff(self):
"""Check if user is PX Staff"""
return self.has_role("PX Staff")
def is_source_user(self):
"""Check if user is a PX Source User"""
return self.has_role("PX Source User")
def is_executive(self):
"""Check if user is Executive (C-Suite/Top Management)"""
return self.has_role("Executive")
def get_source_user_profile_active(self):
"""Get active source user profile if exists"""
if hasattr(self, "source_user_profile"):
profile = self.source_user_profile
if profile and profile.is_active:
return profile
return None
def needs_onboarding(self):
"""Check if user needs to complete onboarding"""
return self.is_provisional and not self.acknowledgement_completed
def get_onboarding_progress_percentage(self):
"""Get onboarding progress percentage"""
from .services import OnboardingService
return OnboardingService.get_user_progress_percentage(self)
class AcknowledgementCategory(UUIDModel, TimeStampedModel):
"""
Categories/Departments for acknowledgements.
Admins can add, edit, or deactivate categories.
Replaces hardcoded department choices with database-driven categories.
"""
name_en = models.CharField(max_length=200)
name_ar = models.CharField(max_length=200, blank=True, help_text="Arabic name")
code = models.CharField(max_length=50, unique=True, help_text="Unique code (e.g., CLINICS, ADMISSIONS)")
description = models.TextField(blank=True, help_text="Category description")
# Display & ordering
icon = models.CharField(max_length=50, blank=True, help_text="Icon class (e.g., 'building', 'user')")
color = models.CharField(max_length=7, default="#007bbd", help_text="Hex color code")
order = models.IntegerField(default=0, help_text="Display order")
# Status
is_active = models.BooleanField(default=True, db_index=True)
is_default = models.BooleanField(default=False, help_text="One of the 14 default categories")
class Meta:
ordering = ["order", "name_en"]
verbose_name_plural = "Acknowledgement Categories"
indexes = [
models.Index(fields=["is_active", "order"]),
models.Index(fields=["is_default"]),
]
def __str__(self):
return self.name_en
def get_localized_name(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.name_ar:
return self.name_ar
return self.name_en
def activate(self):
self.is_active = True
self.save(update_fields=["is_active"])
def deactivate(self):
self.is_active = False
self.save(update_fields=["is_active"])
class AcknowledgementContent(UUIDModel, TimeStampedModel):
"""
Acknowledgement content sections.
Provides bilingual educational content for each acknowledgement category.
"""
# Link to category (replaces role field)
category = models.ForeignKey(
AcknowledgementCategory,
on_delete=models.CASCADE,
related_name="content_sections",
help_text="Category this content belongs to",
)
# Content section
code = models.CharField(max_length=100, unique=True, help_text="Unique code for this content section")
title_en = models.CharField(max_length=200)
title_ar = models.CharField(max_length=200, blank=True)
description_en = models.TextField()
description_ar = models.TextField(blank=True)
# Content details
content_en = models.TextField(blank=True)
content_ar = models.TextField(blank=True)
# Visual elements
icon = models.CharField(max_length=50, blank=True, help_text="Icon class (e.g., 'fa-user', 'fa-shield')")
color = models.CharField(max_length=7, blank=True, help_text="Hex color code (e.g., '#007bff')")
# Organization
order = models.IntegerField(default=0, help_text="Display order")
# Status
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["category", "order", "code"]
indexes = [
models.Index(fields=["category", "is_active", "order"]),
]
def __str__(self):
return f"{self.category.name_en} - {self.title_en}"
def get_localized_title(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.title_ar:
return self.title_ar
return self.title_en
def get_localized_description(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.description_ar:
return self.description_ar
return self.description_en
def get_localized_content(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.content_ar:
return self.content_ar
return self.content_en
class AcknowledgementChecklistItem(UUIDModel, TimeStampedModel):
"""
Checklist items that users must acknowledge.
Each item is linked to a category (department).
"""
# Link to category (replaces role field)
category = models.ForeignKey(
AcknowledgementCategory,
on_delete=models.PROTECT,
related_name="checklist_items",
help_text="Category/Department this acknowledgement belongs to",
)
# Linked content (optional)
content = models.ForeignKey(
AcknowledgementContent,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="checklist_items",
help_text="Related content section",
)
# Item details
code = models.CharField(max_length=100, unique=True, help_text="Unique code for this checklist item")
text_en = models.CharField(max_length=500)
text_ar = models.CharField(max_length=500, blank=True)
description_en = models.TextField(blank=True)
description_ar = models.TextField(blank=True)
# Configuration
is_required = models.BooleanField(default=True, help_text="Item must be acknowledged")
order = models.IntegerField(default=0, help_text="Display order in checklist")
# Status
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["category", "order", "code"]
indexes = [
models.Index(fields=["category", "is_active", "order"]),
]
def __str__(self):
return f"{self.category.name_en} - {self.text_en}"
def get_localized_text(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.text_ar:
return self.text_ar
return self.text_en
def get_localized_description(self):
from django.utils.translation import get_language
if get_language() == "ar" and self.description_ar:
return self.description_ar
return self.description_en
class UserAcknowledgement(UUIDModel, TimeStampedModel):
"""
Records of user acknowledgements.
Tracks when each user acknowledges specific items with digital signatures.
"""
# User
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="acknowledgements")
# Checklist item
checklist_item = models.ForeignKey(
AcknowledgementChecklistItem, on_delete=models.CASCADE, related_name="user_acknowledgements"
)
# Acknowledgement details
is_acknowledged = models.BooleanField(default=True)
acknowledged_at = models.DateTimeField(auto_now_add=True)
# Digital signature
signature = models.TextField(blank=True, help_text="Digital signature data (base64 encoded)")
signature_ip = models.GenericIPAddressField(null=True, blank=True, help_text="IP address when signed")
signature_user_agent = models.TextField(blank=True, help_text="User agent when signed")
# PDF document
pdf_file = models.FileField(
upload_to="acknowledgements/pdfs/", null=True, blank=True, help_text="PDF document of signed acknowledgement"
)
# Metadata
metadata = models.JSONField(default=dict, blank=True, help_text="Additional metadata")
class Meta:
ordering = ["-acknowledged_at"]
unique_together = [["user", "checklist_item"]]
indexes = [
models.Index(fields=["user", "-acknowledged_at"]),
models.Index(fields=["checklist_item", "-acknowledged_at"]),
]
def __str__(self):
return f"{self.user.email} - {self.checklist_item.text_en}"
class UserProvisionalLog(UUIDModel, TimeStampedModel):
"""
Audit trail for provisional user lifecycle.
Tracks all key events in the onboarding process.
"""
EVENT_TYPES = [
("created", _("User Created")),
("invitation_sent", _("Invitation Sent")),
("invitation_resent", _("Invitation Resent")),
("wizard_started", _("Wizard Started")),
("step_completed", _("Wizard Step Completed")),
("wizard_completed", _("Wizard Completed")),
("user_activated", _("User Activated")),
("invitation_expired", _("Invitation Expired")),
]
# User
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="provisional_logs")
# Event details
event_type = models.CharField(max_length=50, choices=EVENT_TYPES, db_index=True)
description = models.TextField()
# Context
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
# Additional data
metadata = models.JSONField(default=dict, blank=True, help_text="Additional event data")
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["user", "-created_at"]),
models.Index(fields=["event_type", "-created_at"]),
]
def __str__(self):
return f"{self.user.email} - {self.get_event_type_display()} - {self.created_at}"
class Role(models.Model):
"""
Role model for managing predefined roles and their permissions.
This is a helper model - actual role assignment uses Django Groups.
"""
ROLE_CHOICES = [
("px_admin", _("PX Admin")),
("hospital_admin", _("Hospital Admin")),
("department_manager", _("Department Manager")),
("px_staff", _("PX Staff")),
("physician", _("Physician")),
("nurse", _("Nurse")),
("staff", _("Staff")),
("viewer", _("Viewer")),
("executive", _("Executive")),
]
name = models.CharField(max_length=50, unique=True, choices=ROLE_CHOICES)
display_name = models.CharField(max_length=100)
description = models.TextField(blank=True)
# Link to Django Group
group = models.OneToOneField(Group, on_delete=models.CASCADE, related_name="role_config")
# Permissions
permissions = models.ManyToManyField(Permission, blank=True)
# Hierarchy level (for escalation logic)
level = models.IntegerField(default=0, help_text="Higher number = higher authority")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-level", "name"]
def __str__(self):
return self.display_name
# Import version models to ensure they are registered with Django
from .version_models import ContentVersion, ChecklistItemVersion, ContentChangeLog
# ============================================================================
# SIMPLE ACKNOWLEDGEMENT MODELS
# ============================================================================
class SimpleAcknowledgement(UUIDModel, TimeStampedModel):
"""
Simple acknowledgement item that employees must sign.
"""
title = models.CharField(max_length=200, help_text="Acknowledgement title")
description = models.TextField(blank=True, help_text="Detailed description")
pdf_document = models.FileField(
upload_to="acknowledgements/documents/", null=True, blank=True, help_text="PDF document for employees to review"
)
# Status
is_active = models.BooleanField(default=True, help_text="Show in employee checklist")
is_required = models.BooleanField(default=True, help_text="Must be signed by all employees")
# Display order
order = models.IntegerField(default=0)
class Meta:
ordering = ["order", "title"]
verbose_name = "Acknowledgement"
verbose_name_plural = "Acknowledgements"
def __str__(self):
return self.title
@property
def signed_count(self):
"""Number of employees who have signed this acknowledgement."""
return self.employee_signatures.filter(is_signed=True).count()
class EmployeeAcknowledgement(UUIDModel, TimeStampedModel):
"""
Records which employees have signed which acknowledgements.
"""
employee = models.ForeignKey(
"accounts.User",
on_delete=models.CASCADE,
related_name="employee_acknowledgements",
help_text="Employee who signed",
)
acknowledgement = models.ForeignKey(
SimpleAcknowledgement,
on_delete=models.CASCADE,
related_name="employee_signatures",
help_text="Acknowledgement that was signed",
)
# Sent tracking
sent_at = models.DateTimeField(null=True, blank=True, help_text="When acknowledgement was sent to employee")
sent_by = models.ForeignKey(
"accounts.User",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="sent_acknowledgements",
help_text="Admin who sent this acknowledgement",
)
# Signature details
is_signed = models.BooleanField(default=False)
signed_at = models.DateTimeField(null=True, blank=True)
signature_name = models.CharField(max_length=200, blank=True, help_text="Name used when signing")
signature_employee_id = models.CharField(max_length=100, blank=True, help_text="Employee ID when signing")
# Signed PDF
signed_pdf = models.FileField(
upload_to="acknowledgements/signed/%Y/%m/", null=True, blank=True, help_text="PDF with employee signature"
)
# IP and user agent for audit
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
# Notes
notes = models.TextField(blank=True, help_text="Admin notes about this acknowledgement")
class Meta:
ordering = ["-sent_at", "-signed_at"]
unique_together = [["employee", "acknowledgement"]]
verbose_name = "Employee Acknowledgement"
verbose_name_plural = "Employee Acknowledgements"
def __str__(self):
if self.is_signed:
return f"✓ {self.employee.get_full_name()} - {self.acknowledgement.title}"
elif self.sent_at:
return f"→ {self.employee.get_full_name()} - {self.acknowledgement.title} (Sent)"
return f"○ {self.employee.get_full_name()} - {self.acknowledgement.title}"
```
## Assistant
Now I need to add the corresponding DRF permission class for Executive role in the permissions file to complete the role setup.
## Assistant
Now let me add the Executive permission class:
### Tool: Edit: apps/accounts/permissions.py: class IsDepartmentManager(perm... => class IsDepartmentManager(perm...
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/accounts/permissions.py",
"new_string": "class IsDepartmentManager(permissions.BasePermission):\n \"\"\"\n Permission class to check if user is Department Manager.\n Department Managers have access to their department's data.\n \"\"\"\n message = \"You must be a Department Manager to perform this action.\"\n\n def has_permission(self, request, view):\n return request.user and request.user.is_authenticated and request.user.is_department_manager()\n\n\nclass IsExecutive(permissions.BasePermission):\n \"\"\"\n Permission class to check if user is Executive (C-Suite/Top Management).\n Executives have system-wide read-only access with analytics capabilities.\n \"\"\"\n message = \"You must be an Executive to perform this action.\"\n\n def has_permission(self, request, view):\n return request.user and request.user.is_authenticated and request.user.is_executive()",
"old_string": "class IsDepartmentManager(permissions.BasePermission):\n \"\"\"\n Permission class to check if user is Department Manager.\n Department Managers have access to their department's data.\n \"\"\"\n message = \"You must be a Department Manager to perform this action.\"\n\n def has_permission(self, request, view):\n return request.user and request.user.is_authenticated and request.user.is_department_manager()"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/accounts/permissions.py`
**Output:**
*Diff for `permissions.py`:*
```diff
"""
Accounts permissions - RBAC permission classes
"""
from rest_framework import permissions
class IsPXAdmin(permissions.BasePermission):
"""
Permission class to check if user is PX Admin.
PX Admins have full system access.
"""
message = "You must be a PX Admin to perform this action."
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and request.user.is_px_admin()
class IsHospitalAdmin(permissions.BasePermission):
"""
Permission class to check if user is Hospital Admin.
Hospital Admins have access to their hospital's data.
"""
message = "You must be a Hospital Admin to perform this action."
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and request.user.is_hospital_admin()
class IsDepartmentManager(permissions.BasePermission):
"""
Permission class to check if user is Department Manager.
Department Managers have access to their department's data.
"""
message = "You must be a Department Manager to perform this action."
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and request.user.is_department_manager()
class IsExecutive(permissions.BasePermission):
"""
Permission class to check if user is Executive (C-Suite/Top Management).
Executives have system-wide read-only access with analytics capabilities.
"""
message = "You must be an Executive to perform this action."
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and request.user.is_executive()
class IsPXAdminOrHospitalAdmin(permissions.BasePermission):
"""
Permission class for PX Admin or Hospital Admin.
"""
message = "You must be a PX Admin or Hospital Admin to perform this action."
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
return request.user.is_px_admin() or request.user.is_hospital_admin()
class IsPXAdminOrReadOnly(permissions.BasePermission):
"""
Permission class that allows PX Admins full access,
but only read access for others.
"""
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
# Read permissions for any authenticated user
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions only for PX Admins
return request.user.is_px_admin()
class IsOwnerOrPXAdmin(permissions.BasePermission):
"""
Permission class that allows users to access their own data,
or PX Admins to access any data.
"""
def has_object_permission(self, request, view, obj):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins can access anything
if request.user.is_px_admin():
return True
# Users can access their own data
if hasattr(obj, 'user'):
return obj.user == request.user
return obj == request.user
class HasRolePermission(permissions.BasePermission):
"""
Permission class that checks if user has specific role.
Usage: Set required_roles on the view.
"""
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
required_roles = getattr(view, 'required_roles', [])
if not required_roles:
return True
user_roles = request.user.get_role_names()
return any(role in user_roles for role in required_roles)
class CanAccessHospitalData(permissions.BasePermission):
"""
Permission class that checks if user can access hospital data.
- PX Admins can access all hospitals
- Hospital Admins can access their own hospital
- Department Managers can access their hospital
"""
def has_object_permission(self, request, view, obj):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins can access all
if request.user.is_px_admin():
return True
# Get hospital from object
hospital = None
if hasattr(obj, 'hospital'):
hospital = obj.hospital
elif obj.__class__.__name__ == 'Hospital':
hospital = obj
if not hospital:
return False
# Check if user belongs to this hospital
return request.user.hospital == hospital
class CanAccessDepartmentData(permissions.BasePermission):
"""
Permission class that checks if user can access department data.
- PX Admins can access all departments
- Hospital Admins can access departments in their hospital
- Department Managers can access their own department
"""
def has_object_permission(self, request, view, obj):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins can access all
if request.user.is_px_admin():
return True
# Get department from object
department = None
if hasattr(obj, 'department'):
department = obj.department
elif obj.__class__.__name__ == 'Department':
department = obj
if not department:
return False
# Hospital Admins can access departments in their hospital
if request.user.is_hospital_admin() and request.user.hospital == department.hospital:
return True
# Department Managers can access their own department
if request.user.is_department_manager() and request.user.department == department:
return True
return False
class IsSourceUser(permissions.BasePermission):
"""
Permission class to check if user is a PX Source User.
Source Users can only access data from their assigned source.
"""
message = "You must be a Source User to perform this action."
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
request.user.is_source_user()
)
class IsSourceUserOrAdmin(permissions.BasePermission):
"""
Permission class for Source Users or Admins.
Allows access to:
- PX Admins
- Hospital Admins
- Department Managers
- Active Source Users
"""
message = "You must be a Source User or Admin to perform this action."
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
# Admins have access
if (request.user.is_px_admin() or
request.user.is_hospital_admin() or
request.user.is_department_manager()):
return True
# Source Users have access
return request.user.is_source_user()
class IsSourceUserOwnData(permissions.BasePermission):
"""
Object-level permission to ensure source users can only access their own data.
"""
message = "You can only access data you created."
def has_object_permission(self, request, view, obj):
if not (request.user and request.user.is_authenticated):
return False
# Admins can access everything
if (request.user.is_px_admin() or
request.user.is_hospital_admin() or
request.user.is_department_manager()):
return True
# Source Users can only access their own created data
if request.user.is_source_user():
return getattr(obj, 'created_by', None) == request.user
return False
# ==================== Onboarding Permissions ====================
class IsProvisionalUser(permissions.BasePermission):
"""
Permission class to check if user is provisional (in onboarding).
"""
message = "You must be a provisional user to access this resource."
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
request.user.is_provisional and
not request.user.acknowledgement_completed
)
class CanViewOnboarding(permissions.BasePermission):
"""
Permission class for viewing onboarding content.
- Provisional users can view their own onboarding
- PX Admins can view all onboarding data
"""
message = "You do not have permission to view onboarding data."
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
# Provisional users can view their own
if request.user.is_provisional:
return True
# PX Admins can view all
if request.user.is_px_admin():
return True
return False
class CanManageOnboarding(permissions.BasePermission):
"""
Permission class for managing onboarding.
- PX Admins have full access
- Hospital Admins can manage onboarding for their hospital
"""
message = "You must be a PX Admin or Hospital Admin to manage onboarding."
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins have full access
if request.user.is_px_admin():
return True
# Hospital Admins can manage their hospital
if request.user.is_hospital_admin():
return True
return False
class IsOnboardingOwnerOrAdmin(permissions.BasePermission):
"""
Permission class for onboarding object access.
- Users can access their own onboarding data
- PX Admins can access all
"""
def has_object_permission(self, request, view, obj):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins can access all
if request.user.is_px_admin():
return True
# Users can access their own data
if hasattr(obj, 'user'):
return obj.user == request.user
return obj == request.user
class CanManageAcknowledgementContent(permissions.BasePermission):
"""
Permission class for managing acknowledgement content and checklist items.
Only PX Admins can manage these.
"""
message = "Only PX Admins can manage acknowledgement content."
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
# PX Admins can manage content
if request.user.is_px_admin():
return True
# Read-only for others
if request.method in permissions.SAFE_METHODS:
return True
return False
```
## Assistant
Now I need to update the sidebar navigation to include the Executive Summary menu item visible only to executives, so let me read the sidebar template first.
## Assistant
Now let me update the sidebar to add Executive Summary navigation (visible to Executives and PX Admins):
### Tool: ReadFile: templates/layouts/partials/sidebar.html (first 100 lines)
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/templates/layouts/partials/sidebar.html",
"limit": 100
}
```
**Affected Files:**
- `/home/ismail/projects/HH/templates/layouts/partials/sidebar.html`
**Output:**
```html
Showing lines 1-100 of 784 total lines.
---
{% load i18n %}
{% load hospital_filters %}
{% load static %}
<style>
.sidebar-icon-only {
width: 5rem;
transition: width 0.3s ease;
}
.sidebar-icon-only:hover {
width: 16rem;
}
.sidebar-icon-only .sidebar-text {
opacity: 0;
visibility: hidden;
width: 0;
overflow: hidden;
transition: opacity 0.2s ease;
white-space: nowrap;
}
.sidebar-icon-only:hover .sidebar-text {
opacity: 1;
visibility: visible;
width: auto;
}
.sidebar-icon-only .sidebar-logo-text {
opacity: 0;
width: 0;
overflow: hidden;
transition: opacity 0.2s ease;
}
.sidebar-icon-only:hover .sidebar-logo-text {
opacity: 1;
width: auto;
}
.sidebar-icon-only .sidebar-logo-extended {
opacity: 0;
transition: opacity 0.3s ease;
}
.sidebar-icon-only:hover .sidebar-logo-extended {
opacity: 1;
}
.sidebar-icon-only:hover .sidebar-logo-collapsed {
display: none;
}
.sidebar-icon-only .submenu {
display: none;
}
.sidebar-icon-only:hover .submenu {
display: block;
}
.nav-item-active {
background-color: rgba(255,255,255,0.1);
border-left: 3px solid #fff;
}
.nav-item-sublink-active {
background-color: rgba(255,255,255,0.15);
}
.submenu {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.submenu.open {
max-height: 500px;
}
.submenu a {
padding-left: 3.5rem;
font-size: 0.75rem;
}
[dir="rtl"] .submenu a {
padding-left: 0.5rem;
padding-right: 3.5rem;
}
[dir="rtl"] .nav-item-active {
border-left: none;
border-right: 3px solid #fff;
}
[dir="rtl"] #sidebar button span.sidebar-text.flex-1,
[dir="rtl"] #sidebar .sidebar-text.text-left {
text-align: right;
}
</style>
<aside id="sidebar" class="sidebar-icon-only bg-navy text-white flex flex-col shadow-2xl z-20 fixed left-0 top-0 h-screen overflow-y-auto overflow-x-hidden">
{% csrf_token %}
<script>window.sidebarCsrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;</script>
<!-- Brand -->
<div class="p-4 flex items-center gap-3 border-b border-white/10">
<!-- Collapsed logo (icon only) -->
<div class="sidebar-logo-collapsed w-10 h-10 flex-shrink-0">
<img src="{% static 'img/HH_R_ICON.png' %}" alt="Al Hammadi" class="w-full h-full object-contain">
</div>
<!-- Extended logo (full logo with text) - shown on hover -->
<div class="sidebar-logo-extended flex-shrink-0">
<img src="{% static 'img/HH_R_H_Logo.png' %}" alt="Al Hammadi" class="h-10 w-auto object-contain">
</div>
</div>
<!-- Navigation -->
```
## Assistant
I need to continue reading the sidebar file to find where the navigation items are defined so I can add the Executive Summary link.
### Tool: ReadFile: templates/layouts/partials/sidebar.html (lines 101-200)
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/templates/layouts/partials/sidebar.html",
"limit": 100,
"offset": 100
}
```
**Affected Files:**
- `/home/ismail/projects/HH/templates/layouts/partials/sidebar.html:100`
**Output:**
```html
Showing lines 101-200 of 784 total lines.
---
<nav class="flex-1 px-2 py-4 space-y-1">
<!-- Dashboard -->
<a href="{% url 'analytics:dashboard' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if request.resolver_match.url_name == 'dashboard' and 'analytics' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="layout-dashboard" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Dashboard" %}</span>
</a>
<a href="{% url 'dashboard:command-center' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if request.resolver_match.url_name == 'command-center' %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="command" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Command Center" %}</span>
</a>
<!-- Performance (Admin only - NOT for Source Users) -->
{% if user.is_px_admin or user.is_hospital_admin %}
{% if not user.source_user_profile %}
<div class="performance-nav-container">
<button onclick="togglePerformanceMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'admin-evaluation' in request.path or 'employee-evaluation' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="bar-chart-2" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "Performance" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'admin-evaluation' in request.path or 'employee-evaluation' in request.path %}rotate-180{% endif %}" id="performance-chevron"></i>
</button>
<div class="submenu {% if 'admin-evaluation' in request.path or 'employee-evaluation' in request.path %}open{% endif %} space-y-1 mt-1" id="performance-submenu">
<a href="{% url 'dashboard:admin_evaluation' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'admin-evaluation' in request.path and 'employee' not in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="shield-check" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Admin Evaluation" %}</span>
</a>
<a href="{% url 'dashboard:employee_evaluation' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'employee-evaluation' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="users" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Employee Evaluation" %}</span>
</a>
</div>
</div>
{% endif %}
{% endif %}
<!-- Source User Dashboard (Source Users only) -->
{% if user.source_user_profile %}
<a href="{% url 'px_sources:source_user_dashboard' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'source_user_dashboard' in request.resolver_match.url_name %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="radio" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "My Source Dashboard" %}</span>
</a>
{% endif %}
<!-- Complaints -->
<a href="{% url 'complaints:complaint_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'complaints' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="file-text" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Complaints" %}</span>
{% if complaint_count|default:0 > 0 %}
<span class="sidebar-text ml-auto bg-white/20 text-white text-xs px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center">{{ complaint_count }}</span>
{% endif %}
</a>
<!-- Inquiries -->
<a href="{% url 'inquiries:inquiry_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if '/inquiries' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="help-circle" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Inquiries" %}</span>
</a>
<!-- Observations -->
<a href="{% url 'observations:observation_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'observations' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="eye" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Observations" %}</span>
</a>
<!-- QI Projects - Hidden for Source Users -->
{% if not user.source_user_profile %}
<div class="qi-nav-container">
<button onclick="toggleQIMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'projects' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="target" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "QI Projects" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'projects' in request.path %}rotate-180{% endif %}" id="qi-chevron"></i>
</button>
<div class="submenu {% if 'projects' in request.path %}open{% endif %} space-y-1 mt-1" id="qi-submenu">
<a href="{% url 'projects:project_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'project_list' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="list" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "All Projects" %}</span>
</a>
{% if user.is_px_admin or user.is_hospital_admin or user.is_department_manager %}
<a href="{% url 'projects:project_create' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'create' in request.path and 'projects' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="plus-circle" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Create Project" %}</span>
</a>
{% endif %}
{% if user.is_px_admin or user.is_hospital_admin %}
<a href="{% url 'projects:template_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'template' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="copy" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Templates" %}</span>
</a>
```
## Assistant
I need to find the exact location in the sidebar where the Command Center link is to add the Executive Summary navigation link right after it.
## Assistant
Let me find a good place to insert the Executive Summary link - right after Command Center:
### Tool: Grep: 'Command Center' in path '/home/ismail/projects/HH/templates/layouts/partials/sidebar.html'
**Status**: completed
**Input:**
```json
{
"path": "/home/ismail/projects/HH/templates/layouts/partials/sidebar.html",
"limit": 5,
"pattern": "Command Center"
}
```
**Output:**
```
Found 1 match for pattern "Command Center" in path "/home/ismail/projects/HH/templates/layouts/partials/sidebar.html":
---
/home/ismail/projects/HH/templates/layouts/partials/sidebar.html:111: <span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Command Center" %}</span>
```
## Assistant
Now I'll add the Executive Summary navigation link to the sidebar, visible to Executives and PX Admins, right after the Command Center link.
### Tool: Edit: templates/layouts/partials/sidebar.html: &lt;a href="{% url 'dashb... => &lt;a href="{% url 'dashb...
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/templates/layouts/partials/sidebar.html",
"new_string": " <a href=\"{% url 'dashboard:command-center' %}\"\n class=\"flex items-center gap-3 p-3 rounded-lg transition {% if request.resolver_match.url_name == 'command-center' %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}\">\n <i data-lucide=\"command\" class=\"w-5 h-5 flex-shrink-0\"></i>\n <span class=\"sidebar-text text-sm font-semibold whitespace-nowrap\">{% trans \"Command Center\" %}</span>\n </a>\n\n <!-- Executive Summary (Executive & PX Admin only) -->\n {% if user.is_executive or user.is_px_admin %}\n <a href=\"{% url 'executive:executive_dashboard' %}\"\n class=\"flex items-center gap-3 p-3 rounded-lg transition {% if 'executive' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}\">\n <i data-lucide=\"briefcase\" class=\"w-5 h-5 flex-shrink-0\"></i>\n <span class=\"sidebar-text text-sm font-semibold whitespace-nowrap\">{% trans \"Executive Summary\" %}</span>\n </a>\n {% endif %}",
"old_string": " <a href=\"{% url 'dashboard:command-center' %}\"\n class=\"flex items-center gap-3 p-3 rounded-lg transition {% if request.resolver_match.url_name == 'command-center' %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}\">\n <i data-lucide=\"command\" class=\"w-5 h-5 flex-shrink-0\"></i>\n <span class=\"sidebar-text text-sm font-semibold whitespace-nowrap\">{% trans \"Command Center\" %}</span>\n </a>"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/templates/layouts/partials/sidebar.html`
**Output:**
*Diff for `sidebar.html`:*
```diff
{% load i18n %}
{% load hospital_filters %}
{% load static %}
<style>
.sidebar-icon-only {
width: 5rem;
transition: width 0.3s ease;
}
.sidebar-icon-only:hover {
width: 16rem;
}
.sidebar-icon-only .sidebar-text {
opacity: 0;
visibility: hidden;
width: 0;
overflow: hidden;
transition: opacity 0.2s ease;
white-space: nowrap;
}
.sidebar-icon-only:hover .sidebar-text {
opacity: 1;
visibility: visible;
width: auto;
}
.sidebar-icon-only .sidebar-logo-text {
opacity: 0;
width: 0;
overflow: hidden;
transition: opacity 0.2s ease;
}
.sidebar-icon-only:hover .sidebar-logo-text {
opacity: 1;
width: auto;
}
.sidebar-icon-only .sidebar-logo-extended {
opacity: 0;
transition: opacity 0.3s ease;
}
.sidebar-icon-only:hover .sidebar-logo-extended {
opacity: 1;
}
.sidebar-icon-only:hover .sidebar-logo-collapsed {
display: none;
}
.sidebar-icon-only .submenu {
display: none;
}
.sidebar-icon-only:hover .submenu {
display: block;
}
.nav-item-active {
background-color: rgba(255,255,255,0.1);
border-left: 3px solid #fff;
}
.nav-item-sublink-active {
background-color: rgba(255,255,255,0.15);
}
.submenu {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.submenu.open {
max-height: 500px;
}
.submenu a {
padding-left: 3.5rem;
font-size: 0.75rem;
}
[dir="rtl"] .submenu a {
padding-left: 0.5rem;
padding-right: 3.5rem;
}
[dir="rtl"] .nav-item-active {
border-left: none;
border-right: 3px solid #fff;
}
[dir="rtl"] #sidebar button span.sidebar-text.flex-1,
[dir="rtl"] #sidebar .sidebar-text.text-left {
text-align: right;
}
</style>
<aside id="sidebar" class="sidebar-icon-only bg-navy text-white flex flex-col shadow-2xl z-20 fixed left-0 top-0 h-screen overflow-y-auto overflow-x-hidden">
{% csrf_token %}
<script>window.sidebarCsrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;</script>
<!-- Brand -->
<div class="p-4 flex items-center gap-3 border-b border-white/10">
<!-- Collapsed logo (icon only) -->
<div class="sidebar-logo-collapsed w-10 h-10 flex-shrink-0">
<img src="{% static 'img/HH_R_ICON.png' %}" alt="Al Hammadi" class="w-full h-full object-contain">
</div>
<!-- Extended logo (full logo with text) - shown on hover -->
<div class="sidebar-logo-extended flex-shrink-0">
<img src="{% static 'img/HH_R_H_Logo.png' %}" alt="Al Hammadi" class="h-10 w-auto object-contain">
</div>
</div>
<!-- Navigation -->
<nav class="flex-1 px-2 py-4 space-y-1">
<!-- Dashboard -->
<a href="{% url 'analytics:dashboard' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if request.resolver_match.url_name == 'dashboard' and 'analytics' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="layout-dashboard" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Dashboard" %}</span>
</a>
<a href="{% url 'dashboard:command-center' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if request.resolver_match.url_name == 'command-center' %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="command" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Command Center" %}</span>
</a>
<!-- Executive Summary (Executive & PX Admin only) -->
{% if user.is_executive or user.is_px_admin %}
<a href="{% url 'executive:executive_dashboard' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'executive' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="briefcase" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Executive Summary" %}</span>
</a>
{% endif %}
<!-- Performance (Admin only - NOT for Source Users) -->
{% if user.is_px_admin or user.is_hospital_admin %}
{% if not user.source_user_profile %}
<div class="performance-nav-container">
<button onclick="togglePerformanceMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'admin-evaluation' in request.path or 'employee-evaluation' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="bar-chart-2" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "Performance" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'admin-evaluation' in request.path or 'employee-evaluation' in request.path %}rotate-180{% endif %}" id="performance-chevron"></i>
</button>
<div class="submenu {% if 'admin-evaluation' in request.path or 'employee-evaluation' in request.path %}open{% endif %} space-y-1 mt-1" id="performance-submenu">
<a href="{% url 'dashboard:admin_evaluation' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'admin-evaluation' in request.path and 'employee' not in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="shield-check" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Admin Evaluation" %}</span>
</a>
<a href="{% url 'dashboard:employee_evaluation' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'employee-evaluation' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="users" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Employee Evaluation" %}</span>
</a>
</div>
</div>
{% endif %}
{% endif %}
<!-- Source User Dashboard (Source Users only) -->
{% if user.source_user_profile %}
<a href="{% url 'px_sources:source_user_dashboard' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'source_user_dashboard' in request.resolver_match.url_name %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="radio" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "My Source Dashboard" %}</span>
</a>
{% endif %}
<!-- Complaints -->
<a href="{% url 'complaints:complaint_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'complaints' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="file-text" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Complaints" %}</span>
{% if complaint_count|default:0 > 0 %}
<span class="sidebar-text ml-auto bg-white/20 text-white text-xs px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center">{{ complaint_count }}</span>
{% endif %}
</a>
<!-- Inquiries -->
<a href="{% url 'inquiries:inquiry_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if '/inquiries' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="help-circle" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Inquiries" %}</span>
</a>
<!-- Observations -->
<a href="{% url 'observations:observation_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'observations' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="eye" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Observations" %}</span>
</a>
<!-- QI Projects - Hidden for Source Users -->
{% if not user.source_user_profile %}
<div class="qi-nav-container">
<button onclick="toggleQIMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'projects' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="target" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "QI Projects" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'projects' in request.path %}rotate-180{% endif %}" id="qi-chevron"></i>
</button>
<div class="submenu {% if 'projects' in request.path %}open{% endif %} space-y-1 mt-1" id="qi-submenu">
<a href="{% url 'projects:project_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'project_list' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="list" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "All Projects" %}</span>
</a>
{% if user.is_px_admin or user.is_hospital_admin or user.is_department_manager %}
<a href="{% url 'projects:project_create' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'create' in request.path and 'projects' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="plus-circle" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Create Project" %}</span>
</a>
{% endif %}
{% if user.is_px_admin or user.is_hospital_admin %}
<a href="{% url 'projects:template_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'template' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="copy" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Templates" %}</span>
</a>
{% endif %}
</div>
</div>
{% endif %}
<!-- Feedback -->
{% comment %} <a href="{% url 'feedback:feedback_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'feedback' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="message-square-heart" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Feedback" %}</span>
</a>
<!-- Appreciation -->
<a href="{% url 'appreciation:appreciation_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'appreciation' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="heart" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Appreciation" %}</span>
</a> {% endcomment %}
<!-- Surveys (with submenu) - Hidden for Source Users -->
{% if not user.source_user_profile %}
{% url 'surveys:instance_list' as survey_instances_url %}
{% url 'surveys:template_list' as survey_templates_url %}
{% url 'surveys:template_create' as survey_create_url %}
{% url 'surveys:manual_send' as survey_send_url %}
{% url 'surveys:analytics_reports' as survey_reports_url %}
{% url 'integrations:survey-mapping-settings' as survey_mapping_url %}
<div class="survey-nav-container">
<button onclick="toggleSurveyMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'surveys' in request.path or 'survey-mapping' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="clipboard-list" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "Surveys" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'surveys' in request.path or 'survey-mapping' in request.path %}rotate-180{% endif %}" id="survey-chevron"></i>
</button>
<div class="submenu {% if 'surveys' in request.path or 'survey-mapping' in request.path %}open{% endif %} space-y-1 mt-1" id="survey-submenu">
<a href="{{ survey_instances_url }}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'surveys' in request.path and 'templates' not in request.path and 'send' not in request.path and 'reports' not in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="list" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Survey List" %}</span>
</a>
<a href="{{ survey_templates_url }}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'templates' in request.path and 'questions' not in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="files" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Templates" %}</span>
</a>
<a href="{{ survey_create_url }}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'templates/create' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="plus-circle" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Create Survey" %}</span>
</a>
<a href="{{ survey_send_url }}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if '/send' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="send" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Manual Send" %}</span>
</a>
<a href="{% url 'surveys:his_patient_import' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'his-import' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="upload" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "HIS Import" %}</span>
</a>
{% comment %} <a href="{% url 'surveys:bulk_job_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'bulk-jobs' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="layers" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Bulk Jobs" %}</span>
</a> {% endcomment %}
{% comment %} <a href="{{ survey_reports_url }}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if '/reports' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="bar-chart-2" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Reports" %}</span>
</a> {% endcomment %}
<a href="{{ survey_mapping_url }}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'survey-mapping' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="link" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Mapping" %}</span>
</a>
</div>
</div>
{% endif %}
<!-- Staff (with submenu) - Hidden for Source Users -->
{% if not user.source_user_profile %}
<div class="staff-nav-container">
<button onclick="toggleStaffMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'staff' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="users" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "Staff" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'staff' in request.path %}rotate-180{% endif %}" id="staff-chevron"></i>
</button>
<div class="submenu {% if 'staff' in request.path %}open{% endif %} space-y-1 mt-1" id="staff-submenu">
<a href="{% url 'organizations:staff_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'staff_list' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="list" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "All Staff" %}</span>
</a>
<a href="{% url 'organizations:staff_hierarchy_d3' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'hierarchy' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="git-branch" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Hierarchy" %}</span>
</a>
{% comment %} <a href="{% url 'organizations:section_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'section' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="layers" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Sections" %}</span>
</a> {% endcomment %}
{% comment %} <a href="{% url 'organizations:subsection_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'subsection' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="layout-grid" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Subsections" %}</span>
</a> {% endcomment %}
</div>
</div>
{% endif %}
<!-- Patients -->
<a href="{% url 'organizations:patient_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'patients' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="heart-pulse" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Patients" %}</span>
</a>
<!-- Social Media - PX Admin only -->
{% if user.is_px_admin %}
<a href="{% url 'social:dashboard' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'social' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="share-2" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Social Media" %}</span>
</a>
{% endif %}
<!-- Physicians (with submenu) - Hidden for Source Users -->
{% if not user.source_user_profile %}
<div class="physicians-nav-container">
<button onclick="togglePhysiciansMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'physicians' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="stethoscope" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "Physicians" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'physicians' in request.path %}rotate-180{% endif %}" id="physicians-chevron"></i>
</button>
<div class="submenu {% if 'physicians' in request.path %}open{% endif %} space-y-1 mt-1" id="physicians-submenu">
<a href="{% url 'physicians:physician_ratings_dashboard' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'physician_ratings_dashboard' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="layout-dashboard" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Dashboard" %}</span>
</a>
<a href="{% url 'physicians:physician_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'physician_list' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="users" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "All Physicians" %}</span>
</a>
<a href="{% url 'physicians:leaderboard' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'leaderboard' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="trophy" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Leaderboard" %}</span>
</a>
{% if user.is_px_admin or user.is_hospital_admin %}
<a href="{% url 'physicians:doctor_rating_fetch' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'doctor_rating_fetch' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="cloud-download" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Fetch Ratings" %}</span>
</a>
<a href="{% url 'physicians:doctor_rating_import' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'import' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="upload" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Import Ratings" %}</span>
</a>
<a href="{% url 'physicians:individual_ratings_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'individual' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="star" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Individual Ratings" %}</span>
</a>
{% endif %}
</div>
</div>
{% endif %}
<!-- PX Sources (with submenu) - Admin view only -->
{% if not user.source_user_profile %}
<div class="px-sources-nav-container">
<button onclick="togglePXSourcesMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'px-sources' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="radio" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "PX Sources" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'px-sources' in request.path %}rotate-180{% endif %}" id="px-sources-chevron"></i>
</button>
<div class="submenu {% if 'px-sources' in request.path %}open{% endif %} space-y-1 mt-1" id="px-sources-submenu">
<a href="{% url 'px_sources:source_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'source_list' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="list" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "All Sources" %}</span>
</a>
{% comment %} <a href="{% url 'px_sources:source_user_dashboard' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'dashboard' in request.path and 'px-sources' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="layout-dashboard" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Dashboard" %}</span>
</a>
<a href="{% url 'px_sources:source_user_complaint_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'complaints' in request.path and 'px-sources' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="file-text" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Source Complaints" %}</span>
</a>
<a href="{% url 'px_sources:source_user_inquiry_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'inquiries' in request.path and 'px-sources' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="help-circle" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Source Inquiries" %}</span>
</a> {% endcomment %}
</div>
</div>
{% endif %}
<!-- Reports (Admin only - NOT for Source Users) -->
{% if user.is_px_admin or user.is_hospital_admin %}
{% if not user.source_user_profile %}
<div class="reports-nav-container">
<button onclick="toggleReportsMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if '/reports/' in request.path or 'kpi-reports' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="file-bar-chart" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "Reports" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if '/reports/' in request.path or 'kpi-reports' in request.path %}rotate-180{% endif %}" id="reports-chevron"></i>
</button>
<div class="submenu {% if '/reports/' in request.path or 'kpi-reports' in request.path %}open{% endif %} space-y-1 mt-1" id="reports-submenu">
<a href="{% url 'reports:builder' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'builder' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="plus-circle" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Create Report" %}</span>
</a>
<a href="{% url 'reports:saved_reports' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'saved' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="folder-open" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Saved Reports" %}</span>
</a>
<a href="{% url 'analytics:kpi_report_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'kpi-reports' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="target" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "KPI Reports" %}</span>
</a>
</div>
</div>
{% endif %}
{% endif %}
<!-- Send SMS (Admin only - NOT for Source Users) -->
{% if user.is_px_admin or user.is_hospital_admin %}
{% if not user.source_user_profile %}
<a href="{% url 'notifications:send_sms_direct' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'send-sms' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="message-square" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Send SMS" %}</span>
</a>
{% endif %}
{% endif %}
<!-- System Config (Admin only - NOT for Source Users) -->
{% if user.is_px_admin or user.is_hospital_admin %}
{% if not user.source_user_profile %}
<a href="{% url 'config:dashboard' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'config' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="settings" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "System Config" %}</span>
</a>
{% endif %}
{% endif %}
<!-- Acknowledgements (All Employees - NOT for Source Users) -->
{% if user.is_authenticated and not user.source_user_profile %}
<a href="{% url 'accounts:simple_acknowledgements:employee_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'simple-acknowledgements' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="clipboard-signature" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "Acknowledgements" %}</span>
{% if pending_acknowledgements_count > 0 %}
<span class="sidebar-text bg-red-500 text-white text-xs font-bold px-2 py-0.5 rounded-full min-w-[1.5rem] text-center">
{{ pending_acknowledgements_count }}
</span>
{% endif %}
</a>
{% endif %}
<!-- References (All Users - NOT for Source Users) -->
{% if not user.source_user_profile %}
<div class="references-nav-container">
<button onclick="toggleReferencesMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'references' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="folder-open" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "References" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'references' in request.path %}rotate-180{% endif %}" id="references-chevron"></i>
</button>
<div class="submenu {% if 'references' in request.path %}open{% endif %} space-y-1 mt-1" id="references-submenu">
<a href="{% url 'references:dashboard' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'dashboard' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="layout-dashboard" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Dashboard" %}</span>
</a>
<a href="{% url 'references:search' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'search' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="search" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Search Documents" %}</span>
</a>
<a href="{% url 'references:document_create' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'document_create' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="upload" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Upload Document" %}</span>
</a>
<a href="{% url 'references:folder_create' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'folder_create' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="folder-plus" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "New Folder" %}</span>
</a>
</div>
</div>
{% endif %}
<!-- Standards (Admin only - NOT for Source Users) -->
{% if user.is_px_admin or user.is_hospital_admin %}
{% if not user.source_user_profile %}
<div class="standards-nav-container">
<button onclick="toggleStandardsMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'standards' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="clipboard-check" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1">{% trans "Standards" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'standards' in request.path %}rotate-180{% endif %}" id="standards-chevron"></i>
</button>
<div class="submenu {% if 'standards' in request.path %}open{% endif %} space-y-1 mt-1" id="standards-submenu">
<a href="{% url 'standards:dashboard' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'dashboard' and 'standards' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="layout-dashboard" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Dashboard" %}</span>
</a>
<a href="{% url 'standards:search' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'search' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="search" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Search Standards" %}</span>
</a>
<a href="{% url 'standards:source_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'sources' in request.path and 'standards' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="folder" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Sources" %}</span>
</a>
<a href="{% url 'standards:category_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'categories' in request.path and 'standards' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="tags" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Categories" %}</span>
</a>
</div>
</div>
{% endif %}
{% endif %}
</nav>
<!-- Hospital Switcher (PX Admin only) -->
{% if is_px_admin and hospitals_list %}
<div class="px-3 py-2 border-t border-white/10">
<div class="mb-2 px-2">
<p class="text-[10px] font-semibold text-white/50 uppercase tracking-wider">{% trans "Hospital" %}</p>
</div>
<div class="relative">
<select
id="hospitalSwitcher"
onchange="switchHospital(this.value)"
class="w-full bg-white/10 border border-white/20 rounded-lg py-2 px-3 pr-8 text-xs font-semibold text-white appearance-none cursor-pointer hover:bg-white/15 transition focus:outline-none focus:ring-2 focus:ring-blue/50"
>
{% for hospital in hospitals_list %}
<option
value="{{ hospital.id }}"
{% if current_hospital and current_hospital.id == hospital.id %}selected{% endif %}
>
{{ hospital.name }}
</option>
{% endfor %}
</select>
<div class="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none">
<i data-lucide="chevron-down" class="w-3.5 h-3.5 text-white/50"></i>
</div>
</div>
</div>
{% endif %}
<!-- Language Switcher -->
<div class="px-3 py-2">
<div class="flex items-center gap-2">
{% get_available_languages as LANGUAGES %}
{% get_current_language as LANGUAGE_CODE %}
{% for lang_code, lang_name in LANGUAGES %}
<a href="{% url 'core:set_language' %}?language={{ lang_code }}"
class="flex-1 flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg border transition-all duration-200 font-semibold text-xs
{% if LANGUAGE_CODE == lang_code %}
border-white/40 bg-white/20 text-white
{% else %}
border-white/15 text-white/60 hover:bg-white/10 hover:text-white hover:border-white/30
{% endif %}">
<span class="text-sm">
{% if lang_code == 'en' %}🇬🇧{% elif lang_code == 'ar' %}🇸🇦{% endif %}
</span>
<span class="sidebar-text">{{ lang_name }}</span>
</a>
{% endfor %}
</div>
</div>
<!-- User Profile & Logout -->
<div class="p-3 border-t border-white/10">
<div class="flex items-center gap-3 p-2 rounded-lg hover:bg-white/10 transition cursor-pointer" onclick="toggleUserMenu()">
<div class="w-8 h-8 rounded-full bg-blue border-2 border-white/20 flex items-center justify-center font-bold text-xs flex-shrink-0">
{{ user.first_name|first|default:"U" }}{{ user.last_name|first|default:"" }}
</div>
<div class="sidebar-text flex-1 overflow-hidden min-w-0">
<p class="text-xs font-bold truncate">{{ user.get_full_name|default:user.username }}</p>
<p class="text-[10px] text-white/50 truncate">{{ user.get_role_display|default:"User" }}</p>
</div>
<i data-lucide="log-out" class="w-4 h-4 text-white/40 hover:text-white cursor-pointer flex-shrink-0 sidebar-text" onclick="event.stopPropagation(); logout()"></i>
</div>
<!-- User Dropdown Menu (hidden by default) -->
<div id="userMenu" class="hidden mt-2 space-y-1">
<a href="{% url 'accounts:settings' %}" class="flex items-center gap-3 p-2 rounded-lg opacity-70 hover:opacity-100 hover:bg-white/10 transition">
<i data-lucide="user" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text text-xs font-semibold whitespace-nowrap">{% trans "Profile" %}</span>
</a>
<form method="post" action="{% url 'accounts:logout' %}" class="m-0">
{% csrf_token %}
<button type="submit" class="w-full flex items-center gap-3 p-2 rounded-lg opacity-70 hover:opacity-100 hover:bg-white/10 transition text-left">
<i data-lucide="log-out" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text text-xs font-semibold whitespace-nowrap">{% trans "Logout" %}</span>
</button>
</form>
</div>
</div>
</aside>
<script>
function toggleUserMenu() {
const menu = document.getElementById('userMenu');
menu.classList.toggle('hidden');
}
function switchHospital(hospitalId) {
if (!hospitalId) return;
// Show loading state
const select = document.getElementById('hospitalSwitcher');
select.disabled = true;
// Get CSRF token from global variable (not from cookie due to httpOnly)
const csrfToken = window.sidebarCsrfToken || getCookie('csrftoken');
// Create form data
const formData = new FormData();
formData.append('hospital_id', hospitalId);
formData.append('csrfmiddlewaretoken', csrfToken);
// Make AJAX request
fetch('{% url "core:switch_hospital" %}', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload page to apply new hospital context
window.location.reload();
} else {
alert('{% trans "Failed to switch hospital: " %}' + (data.error || '{% trans "Unknown error" %}'));
select.disabled = false;
}
})
.catch(error => {
console.error('Error switching hospital:', error);
alert('{% trans "An error occurred while switching hospitals" %}');
select.disabled = false;
});
}
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function toggleSurveyMenu(event) {
event.preventDefault();
const submenu = document.getElementById('survey-submenu');
const chevron = document.getElementById('survey-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function togglePhysiciansMenu(event) {
event.preventDefault();
const submenu = document.getElementById('physicians-submenu');
const chevron = document.getElementById('physicians-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function toggleStaffMenu(event) {
event.preventDefault();
const submenu = document.getElementById('staff-submenu');
const chevron = document.getElementById('staff-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function togglePXSourcesMenu(event) {
event.preventDefault();
const submenu = document.getElementById('px-sources-submenu');
const chevron = document.getElementById('px-sources-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function toggleAcknowledgementsMenu(event) {
event.preventDefault();
const submenu = document.getElementById('acknowledgements-submenu');
const chevron = document.getElementById('acknowledgements-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function toggleReportsMenu(event) {
event.preventDefault();
const submenu = document.getElementById('reports-submenu');
const chevron = document.getElementById('reports-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function toggleQIMenu(event) {
event.preventDefault();
const submenu = document.getElementById('qi-submenu');
const chevron = document.getElementById('qi-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function toggleReferencesMenu(event) {
event.preventDefault();
const submenu = document.getElementById('references-submenu');
const chevron = document.getElementById('references-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function toggleStandardsMenu(event) {
event.preventDefault();
const submenu = document.getElementById('standards-submenu');
const chevron = document.getElementById('standards-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function togglePerformanceMenu(event) {
event.preventDefault();
const submenu = document.getElementById('performance-submenu');
const chevron = document.getElementById('performance-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function logout() {
if (confirm('{% trans "Are you sure you want to logout?" %}')) {
document.querySelector('form[action="{% url 'accounts:logout' %}"]').submit();
}
}
// Close user menu when clicking outside
document.addEventListener('click', function(e) {
const userMenu = document.getElementById('userMenu');
const userProfile = e.target.closest('.cursor-pointer');
if (userMenu && !userMenu.contains(e.target) && !userProfile) {
userMenu.classList.add('hidden');
}
});
</script>
```
## Assistant
The Executive role and sidebar navigation have been added, so I should update the todo list to mark task 1 as completed and move to task 2 (creating the executive_summary Django app structure).
## Assistant
Perfect! Now let me mark task 1 as complete and move to task 2 - creating the executive_summary app structure:
### Tool: TodoWrite
**Status**: completed
**Output:**
```
- [x] Create Executive role and permissions
- [-] Create executive_summary Django app structure (models, views, services, tasks)
- [ ] Build ExecutiveMetric and ExecutiveReport models
- [ ] Create ExecutiveSummaryService for data aggregation
- [ ] Build Executive Dashboard view with KPI cards and filters
- [ ] Create Executive Dashboard template with ApexCharts
- [ ] Add URL routing and sidebar navigation
- [ ] Implement AI Narrative Service using LiteLLM
- [ ] Build Predictive Analytics Service (trend analysis, risk detection)
- [ ] Create Recommendation Engine
- [ ] Integrate AI-generated briefs into dashboard
- [ ] Implement Natural Language Q&A interface
- [ ] Build Predictive Insights view and risk scoring
- [ ] Create Hospital comparison and leaderboard views
- [ ] Implement PDF report generation with WeasyPrint
- [ ] Create Celery scheduled tasks for automated reports
- [ ] Build mobile-optimized responsive view
- [ ] Add email delivery system for scheduled reports
- [ ] Add Arabic translations and RTL support
- [ ] Performance optimization and testing
```
## Assistant
Now I need to create the executive_summary Django app structure by running the startapp command.
## Assistant
Now let me create the executive_summary Django app. First, I'll create the app structure:
### Tool: Shell: cd /home/ismail/projects/HH &amp;&amp; python manage.py startapp executive_summary apps/executive_summary (Create executive_summary Django app)
**Status**: completed
**Input:**
```json
{
"command": "cd /home/ismail/projects/HH && python manage.py startapp executive_summary apps/executive_summary",
"description": "Create executive_summary Django app"
}
```
**Output:**
```
Command: cd /home/ismail/projects/HH && python manage.py startapp executive_summary apps/executive_summary
Directory: (root)
Output: bash: line 1: python: command not found
Error: (none)
Exit Code: 127
Signal: 0
Process Group PGID: 273022
```
## Assistant
The python command failed, so I'll check if uv is available since the project has a .python-version file indicating it might use uv for Python management.
### Tool: Shell: cd /home/ismail/projects/HH &amp;&amp; uv run python manage.py startapp executive_summary apps/executive_summary (Create executive_summary Django app with uv)
**Status**: completed
**Input:**
```json
{
"command": "cd /home/ismail/projects/HH && uv run python manage.py startapp executive_summary apps/executive_summary",
"description": "Create executive_summary Django app with uv"
}
```
**Output:**
```
Command: cd /home/ismail/projects/HH && uv run python manage.py startapp executive_summary apps/executive_summary
Directory: (root)
Output: (empty)
Error: (none)
Exit Code: 0
Signal: 0
Process Group PGID: 273269
```
## Assistant
Now I need to register the new executive_summary app in Django settings to make it available to the project.
## Assistant
Great! Now let me register the app in settings and create the comprehensive models and services. Let me first check the INSTALLED_APPS:
### Tool: Grep: 'INSTALLED_APPS' in path '/home/ismail/projects/HH/PX360/settings.py'
**Status**: completed
**Input:**
```json
{
"path": "/home/ismail/projects/HH/PX360/settings.py",
"limit": 50,
"pattern": "INSTALLED_APPS"
}
```
**Output:**
```
Found 1 match for pattern "INSTALLED_APPS" in path "/home/ismail/projects/HH/PX360/settings.py":
---
/home/ismail/projects/HH/PX360/settings.py:36:INSTALLED_APPS = [
```
## Assistant
Now I need to read the INSTALLED_APPS section in settings.py to see the existing app registrations and find where to add the new executive_summary app.
### Tool: ReadFile: PX360/settings.py (lines 36-115)
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/PX360/settings.py",
"limit": 80,
"offset": 35
}
```
**Affected Files:**
- `/home/ismail/projects/HH/PX360/settings.py:35`
**Output:**
```python
Showing lines 36-115 of 189 total lines.
---
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Apps
"apps.core",
"apps.accounts",
"apps.dashboard",
"apps.social",
"django_celery_beat",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
# PX Source User Access Control
"apps.px_sources.middleware.SourceUserRestrictionMiddleware",
"apps.px_sources.middleware.SourceUserSessionMiddleware",
]
ROOT_URLCONF = "PX360.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"apps.core.context_processors.hospital_context",
"apps.core.context_processors.sidebar_counts",
],
},
},
]
WSGI_APPLICATION = "PX360.wsgi.application"
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
```
## Assistant
The INSTALLED_APPS list found in settings.py seems incomplete - I need to search for other apps like 'apps.complaints' to understand the full app registration pattern used in this project.
## Assistant
I notice the settings file seems truncated. Let me check the full file to see all apps:
### Tool: Grep: 'apps\\.complaints|apps\\.surveys' in path '/home/ismail/projects/HH/PX360/settings.py'
**Status**: completed
**Input:**
```json
{
"path": "/home/ismail/projects/HH/PX360/settings.py",
"limit": 5,
"pattern": "apps\\\\.complaints|apps\\\\.surveys"
}
```
**Output:**
```
No matches found for pattern "apps\\.complaints|apps\\.surveys" in path "/home/ismail/projects/HH/PX360/settings.py".
```
## Assistant
The INSTALLED_APPS seems incomplete - let me check if there are multiple settings files in the project.
## Assistant
Let me check if there's a different settings structure:
### Tool: Glob: '**/settings*.py'
**Status**: completed
**Input:**
```json
{
"pattern": "**/settings*.py"
}
```
**Output:**
```
Found 3 file(s) matching "**/settings*.py" in the workspace directory, sorted by modification time (newest first):
---
/home/ismail/projects/HH/apps/notifications/settings_models.py
/home/ismail/projects/HH/apps/notifications/settings_service.py
/home/ismail/projects/HH/PX360/settings.py
```
## Assistant
Let me verify Django configuration by checking how the project is configured to run.
## Assistant
Let me read the manage.py to understand the setup:
### Tool: ReadFile: manage.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/manage.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/manage.py`
**Output:**
```python
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
```
## Assistant
The manage.py reveals the actual settings module is 'config.settings.dev', so I need to find and read that file to properly register the new app.
## Assistant
Ah! The settings module is `config.settings.dev`. Let me find that:
### Tool: Glob: '**/config/settings/**/*.py'
**Status**: completed
**Input:**
```json
{
"pattern": "**/config/settings/**/*.py"
}
```
**Output:**
```
Found 4 file(s) matching "**/config/settings/**/*.py" in the workspace directory, sorted by modification time (newest first):
---
/home/ismail/projects/HH/config/settings/dev.py
/home/ismail/projects/HH/config/settings/base.py
/home/ismail/projects/HH/config/settings/__init__.py
/home/ismail/projects/HH/config/settings/prod.py
```
## Assistant
Now I need to read the base settings file to see the INSTALLED_APPS configuration and add the new executive_summary app.
### Tool: ReadFile: config/settings/base.py (first 100 lines)
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/config/settings/base.py",
"limit": 100
}
```
**Affected Files:**
- `/home/ismail/projects/HH/config/settings/base.py`
**Output:**
```python
Showing lines 1-100 of 510 total lines.
---
"""
Base settings for PX360 project.
This file contains settings common to all environments.
"""
import os
from pathlib import Path
import environ
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Initialize environment variables
env = environ.Env(
DEBUG=(bool, False),
ALLOWED_HOSTS=(list, []),
)
# Read .env file if it exists
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env("SECRET_KEY", default="django-insecure-change-this-in-production")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DEBUG")
ALLOWED_HOSTS = env("ALLOWED_HOSTS")
# Application definition
DJANGO_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
THIRD_PARTY_APPS = [
"rest_framework",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"django_filters",
"drf_spectacular",
"django_celery_beat",
]
LOCAL_APPS = [
"apps.core",
"apps.accounts",
"apps.organizations",
"apps.journeys",
"apps.surveys",
"apps.complaints",
"apps.feedback",
"apps.callcenter",
"apps.social",
"apps.px_action_center",
"apps.analytics",
"apps.physicians",
"apps.projects",
"apps.integrations",
"apps.notifications",
"apps.ai_engine",
"apps.dashboard",
"apps.appreciation",
"apps.observations",
"apps.px_sources",
"apps.references",
"apps.standards",
"apps.simulator",
"apps.reports",
"apps.rca",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware", # i18n support
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"apps.core.middleware.TenantMiddleware", # Multi-tenancy support
"apps.px_sources.middleware.SourceUserRestrictionMiddleware", # STRICT source user restrictions
"apps.px_sources.middleware.SourceUserSessionMiddleware", # Shorter sessions for source users
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
```
## Assistant
Now I need to add the executive_summary app to LOCAL_APPS in the base settings file.
## Assistant
Perfect! Now let me add the executive_summary app to LOCAL_APPS:
### Tool: Edit: config/settings/base.py: LOCAL_APPS = [... => LOCAL_APPS = [...
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/config/settings/base.py",
"new_string": "LOCAL_APPS = [\n \"apps.core\",\n \"apps.accounts\",\n \"apps.organizations\",\n \"apps.journeys\",\n \"apps.surveys\",\n \"apps.complaints\",\n \"apps.feedback\",\n \"apps.callcenter\",\n \"apps.social\",\n \"apps.px_action_center\",\n \"apps.analytics\",\n \"apps.physicians\",\n \"apps.projects\",\n \"apps.integrations\",\n \"apps.notifications\",\n \"apps.ai_engine\",\n \"apps.dashboard\",\n \"apps.appreciation\",\n \"apps.observations\",\n \"apps.px_sources\",\n \"apps.references\",\n \"apps.standards\",\n \"apps.simulator\",\n \"apps.reports\",\n \"apps.rca\",\n \"apps.executive_summary\",\n]",
"old_string": "LOCAL_APPS = [\n \"apps.core\",\n \"apps.accounts\",\n \"apps.organizations\",\n \"apps.journeys\",\n \"apps.surveys\",\n \"apps.complaints\",\n \"apps.feedback\",\n \"apps.callcenter\",\n \"apps.social\",\n \"apps.px_action_center\",\n \"apps.analytics\",\n \"apps.physicians\",\n \"apps.projects\",\n \"apps.integrations\",\n \"apps.notifications\",\n \"apps.ai_engine\",\n \"apps.dashboard\",\n \"apps.appreciation\",\n \"apps.observations\",\n \"apps.px_sources\",\n \"apps.references\",\n \"apps.standards\",\n \"apps.simulator\",\n \"apps.reports\",\n \"apps.rca\",\n]"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/config/settings/base.py`
**Output:**
*Diff for `base.py`:*
```diff
"""
Base settings for PX360 project.
This file contains settings common to all environments.
"""
import os
from pathlib import Path
import environ
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Initialize environment variables
env = environ.Env(
DEBUG=(bool, False),
ALLOWED_HOSTS=(list, []),
)
# Read .env file if it exists
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env("SECRET_KEY", default="django-insecure-change-this-in-production")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DEBUG")
ALLOWED_HOSTS = env("ALLOWED_HOSTS")
# Application definition
DJANGO_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
THIRD_PARTY_APPS = [
"rest_framework",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"django_filters",
"drf_spectacular",
"django_celery_beat",
]
LOCAL_APPS = [
"apps.core",
"apps.accounts",
"apps.organizations",
"apps.journeys",
"apps.surveys",
"apps.complaints",
"apps.feedback",
"apps.callcenter",
"apps.social",
"apps.px_action_center",
"apps.analytics",
"apps.physicians",
"apps.projects",
"apps.integrations",
"apps.notifications",
"apps.ai_engine",
"apps.dashboard",
"apps.appreciation",
"apps.observations",
"apps.px_sources",
"apps.references",
"apps.standards",
"apps.simulator",
"apps.reports",
"apps.rca",
"apps.executive_summary",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware", # i18n support
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"apps.core.middleware.TenantMiddleware", # Multi-tenancy support
"apps.px_sources.middleware.SourceUserRestrictionMiddleware", # STRICT source user restrictions
"apps.px_sources.middleware.SourceUserSessionMiddleware", # Shorter sessions for source users
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.i18n",
"apps.core.context_processors.sidebar_counts",
"apps.core.context_processors.hospital_context",
"apps.accounts.context_processors.acknowledgement_counts",
],
},
},
]
WSGI_APPLICATION = "config.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
# DATABASES = {
# 'default': env.db('DATABASE_URL', default='postgresql://px360:px360@localhost:5432/px360')
# }
DATABASES = {
"default": env.db(
"DATABASE_URL",
default="postgresql://px360:px360@localhost:5433/px360",
)
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Custom User Model
AUTH_USER_MODEL = "accounts.User"
# Authentication URLs
LOGIN_URL = "/accounts/login/"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/accounts/login/"
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Asia/Riyadh"
USE_I18N = True
USE_TZ = True
# Languages supported (Arabic and English)
LANGUAGES = [
("en", "English"),
("ar", "Arabic"),
]
# Locale paths for translation files
LOCALE_PATHS = [
BASE_DIR / "locale",
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [
BASE_DIR / "static",
]
# Media files
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
# Data upload settings
# Increased limit to support bulk patient imports from HIS
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000
# WhiteNoise configuration
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Django REST Framework
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 50,
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
# JWT Settings
from datetime import timedelta
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(hours=1),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"UPDATE_LAST_LOGIN": True,
}
# DRF Spectacular (OpenAPI/Swagger)
SPECTACULAR_SETTINGS = {
"TITLE": "PX360 API",
"DESCRIPTION": "Patient Experience 360 Management System API",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
}
# Celery Configuration
CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="redis://localhost:6379/0")
CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND", default="redis://localhost:6379/0")
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes
# Celery Beat Schedule
CELERY_BEAT_SCHEDULER = "config.celery_scheduler:PatchedDatabaseScheduler"
# Logging Configuration
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
},
"simple": {
"format": "{levelname} {message}",
"style": "{",
},
},
"filters": {
"require_debug_true": {
"()": "django.utils.log.RequireDebugTrue",
},
},
"handlers": {
"console": {"level": "INFO", "class": "logging.StreamHandler", "formatter": "verbose"},
"file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / "logs" / "px360.log",
"maxBytes": 1024 * 1024 * 15, # 15MB
"backupCount": 10,
"formatter": "verbose",
},
"integration_file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / "logs" / "integrations.log",
"maxBytes": 1024 * 1024 * 15, # 15MB
"backupCount": 10,
"formatter": "verbose",
},
},
"loggers": {
"django": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
"apps": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": False,
},
"apps.integrations": {
"handlers": ["console", "integration_file"],
"level": "INFO",
"propagate": False,
},
},
}
# Create logs directory if it doesn't exist
LOGS_DIR = BASE_DIR / "logs"
LOGS_DIR.mkdir(exist_ok=True)
# PX360 Business Configuration
# These will be overridden by database configurations but provide defaults
# Survey Configuration
SURVEY_NEGATIVE_THRESHOLD = 3 # Scores below this trigger actions (out of 5)
SURVEY_TOKEN_EXPIRY_DAYS = 30
# SLA Configuration (in hours)
SLA_DEFAULTS = {
"complaint": {
"low": 72,
"medium": 48,
"high": 24,
"critical": 12,
},
"action": {
"low": 120,
"medium": 72,
"high": 48,
"critical": 24,
},
}
COMPLAINT_LINK_EXPIRY_DAYS = env.int("COMPLAINT_LINK_EXPIRY_DAYS", default=7)
# AI Configuration (LiteLLM with OpenRouter)
OPENROUTER_API_KEY = env(
"OPENROUTER_API_KEY", default="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"
)
AI_MODEL = env("AI_MODEL", default="z-ai/glm-4.5-air:free")
AI_TEMPERATURE = env.float("AI_TEMPERATURE", default=0.3)
AI_MAX_TOKENS = env.int("AI_MAX_TOKENS", default=500)
# Notification Configuration
NOTIFICATION_CHANNELS = {
"sms": {
"enabled": env.bool("SMS_ENABLED", default=False),
"provider": env("SMS_PROVIDER", default="console"),
},
"whatsapp": {
"enabled": env.bool("WHATSAPP_ENABLED", default=False),
"provider": env("WHATSAPP_PROVIDER", default="console"),
},
"email": {
"enabled": env.bool("EMAIL_ENABLED", default=True),
"provider": env("EMAIL_PROVIDER", default="console"),
},
}
# Twilio Configuration
TWILIO_ACCOUNT_SID = env("TWILIO_ACCOUNT_SID", default="")
TWILIO_AUTH_TOKEN = env("TWILIO_AUTH_TOKEN", default="")
TWILIO_PHONE_NUMBER = env("TWILIO_PHONE_NUMBER", default="")
TWILIO_MESSAGING_SERVICE_SID = env("TWILIO_MESSAGING_SERVICE_SID", default="")
# Mshastra SMS Configuration
MSHASTRA_USERNAME = env("MSHASTRA_USERNAME", default="")
MSHASTRA_PASSWORD = env("MSHASTRA_PASSWORD", default="")
MSHASTRA_SENDER_ID = env("MSHASTRA_SENDER_ID", default="")
# Dev SMS recipient - when DEBUG=True and this is set, all SMS are redirected here
DEV_SMS_RECIPIENT = env("DEV_SMS_RECIPIENT", default="")
# External API Notification Configuration
EXTERNAL_NOTIFICATION_API = {
"email": {
"enabled": env.bool("EMAIL_API_ENABLED", default=False),
"url": env("EMAIL_API_URL", default=""),
"api_key": env("EMAIL_API_KEY", default=""),
"auth_method": env("EMAIL_API_AUTH_METHOD", default="bearer"),
"method": env("EMAIL_API_METHOD", default="POST"),
"timeout": env.int("EMAIL_API_TIMEOUT", default=10),
"max_retries": env.int("EMAIL_API_MAX_RETRIES", default=3),
"retry_delay": env.int("EMAIL_API_RETRY_DELAY", default=2),
},
"sms": {
"enabled": env.bool("SMS_API_ENABLED", default=False),
"url": env("SMS_API_URL", default=""),
"api_key": env("SMS_API_KEY", default=""),
"auth_method": env("SMS_API_AUTH_METHOD", default="bearer"),
"method": env("SMS_API_METHOD", default="POST"),
"timeout": env.int("SMS_API_TIMEOUT", default=10),
"max_retries": env.int("SMS_API_MAX_RETRIES", default=3),
"retry_delay": env.int("SMS_API_RETRY_DELAY", default=2),
},
}
# Email Configuration
EMAIL_BACKEND = env("EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend")
EMAIL_HOST = env("EMAIL_HOST", default="localhost")
EMAIL_PORT = env.int("EMAIL_PORT", default=2525)
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", default=False)
EMAIL_HOST_USER = env("EMAIL_HOST_USER", default="")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD", default="")
DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL", default="noreply@px360.sa")
SITE_URL = env("SITE_URL", default="http://localhost:8000")
EMAIL_LOGO_URL = env("EMAIL_LOGO_URL", default=f"{SITE_URL}{STATIC_URL}img/HH_P_H_Logo.png")
# HIS Integration Settings
HIS_API_URL = env("HIS_API_URL", default="https://his.alhammadi.med.sa/SSRCE/API/FetchPatientVisitTimeStamps")
HIS_API_USERNAME = env("HIS_API_USERNAME", default="AlhhSUNZHippo")
HIS_RATINGS_API_URL = env("HIS_RATINGS_API_URL", default="")
# Password contains special characters (*#$@PAlhh^2106) - set directly to avoid parsing issues
# Check .env first, fall back to hardcoded default
try:
_env_password = env("HIS_API_PASSWORD")
if _env_password and _env_password != "*":
HIS_API_PASSWORD = _env_password
else:
HIS_API_PASSWORD = "*#$@PAlhh^2106"
except Exception:
HIS_API_PASSWORD = "*#$@PAlhh^2106"
# Security Settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
SECURE_SSL_REDIRECT = env.bool("SECURE_SSL_REDIRECT", default=False)
SESSION_COOKIE_SECURE = env.bool("SESSION_COOKIE_SECURE", default=False)
CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE", default=False)
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SAMESITE = "Lax"
# Password Policy Settings
PASSWORD_MIN_LENGTH = 8
PASSWORD_COMPLEXITY = True
# Login Security - Rate Limiting
# Login attempts rate limiting (Django Axes would be recommended for production)
MAX_LOGIN_ATTEMPTS = 5
LOGIN_ATTEMPT_TIMEOUT_MINUTES = 30
# Session Security
SESSION_COOKIE_AGE = 120 * 60 # 2 hours
SESSION_EXPIRE_AT_BROWSER_CLOSE = env.bool("SESSION_EXPIRE_AT_BROWSER_CLOSE", default=True)
SESSION_SAVE_EVERY_REQUEST = True
# Multi-Tenancy Settings
TENANCY_ENABLED = True
TENANT_MODEL = "organizations.Hospital"
TENANT_FIELD = "hospital"
# Tenant isolation level
# 'strict' - Complete isolation (users only see their hospital)
# 'relaxed' - PX admins can see all hospitals
TENANT_ISOLATION_LEVEL = "strict"
# Social Media API Configuration
YOUTUBE_API_KEY = env("YOUTUBE_API_KEY", default="AIzaSyAem20etP6GkRNMmCyI1pRJF7v8U_xDyMM")
YOUTUBE_CHANNEL_ID = env("YOUTUBE_CHANNEL_ID", default="UCKoEfCXsm4_cQMtqJTvZUVQ")
FACEBOOK_PAGE_ID = env("FACEBOOK_PAGE_ID", default="938104059393026")
FACEBOOK_ACCESS_TOKEN = env(
"FACEBOOK_ACCESS_TOKEN",
default="EAATrDf0UAS8BQWSKbljCUDMbluZBbxZCSWLJkZBGIviBtK8IQ7FDHfGQZBHHm7lsgLhZBL2trT3ZBGPtsWRjntFWQovhkhx726ZBexRZCKitEMhxAiZBmls7uX946432k963Myl6aYBzJzwLhSyygZAFOGP7iIIZANVf6GtLlvAnWn0NXRwZAYR0CNNUwCEEsZAAc",
)
INSTAGRAM_ACCOUNT_ID = env("INSTAGRAM_ACCOUNT_ID", default="17841431861985364")
INSTAGRAM_ACCESS_TOKEN = env(
"INSTAGRAM_ACCESS_TOKEN",
default="EAATrDf0UAS8BQWSKbljCUDMbluZBbxZCSWLJkZBGIviBtK8IQ7FDHfGQZBHHm7lsgLhZBL2trT3ZBGPtsWRjntFWQovhkhx726ZBexRZCKitEMhxAiZBmls7uX946432k963Myl6aYBzJzwLhSyygZAFOGP7iIIZANVf6GtLlvAnWn0NXRwZAYR0CNNUwCEEsZAAc",
)
# Twitter/X Configuration
TWITTER_BEARER_TOKEN = env("TWITTER_BEARER_TOKEN", default=None)
TWITTER_USERNAME = env("TWITTER_USERNAME", default=None)
# LinkedIn Configuration
LINKEDIN_ACCESS_TOKEN = env("LINKEDIN_ACCESS_TOKEN", default=None)
LINKEDIN_ORGANIZATION_ID = env("LINKEDIN_ORGANIZATION_ID", default=None)
# Google Reviews Configuration
GOOGLE_CREDENTIALS_FILE = env("GOOGLE_CREDENTIALS_FILE", default="client_secret.json")
GOOGLE_TOKEN_FILE = env("GOOGLE_TOKEN_FILE", default="token.json")
GOOGLE_LOCATIONS = env.list("GOOGLE_LOCATIONS", default=[])
# OpenRouter Configuration for AI Comment Analysis
OPENROUTER_API_KEY = env(
"OPENROUTER_API_KEY", default="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"
)
OPENROUTER_MODEL = env("OPENROUTER_MODEL", default="google/gemma-3-27b-it:free")
ANALYSIS_BATCH_SIZE = env.int("ANALYSIS_BATCH_SIZE", default=2)
ANALYSIS_ENABLED = env.bool("ANALYSIS_ENABLED", default=True)
```
## Assistant
Now I need to build the comprehensive models for the executive_summary app, including ExecutiveMetric for KPI snapshots, ExecutiveReport for AI-generated reports, PredictiveInsight for AI-detected risks, and AIRecommendation for suggested actions.
## Assistant
Excellent! Now let me create the comprehensive models for the executive summary app. Let me write the models file:
### Tool: WriteFile: Writing to apps/executive_summary/models.py
**Status**: completed
**Input:**
```json
{
"content": "\"\"\"\nExecutive Summary models - AI-powered executive dashboards and reports\n\nThis module implements:\n- Executive metrics (KPI snapshots for trending)\n- Executive reports (AI-generated weekly/monthly summaries)\n- Predictive insights (AI-detected patterns and risks)\n- AI recommendations (suggested actions)\n\"\"\"\nfrom django.db import models\nfrom django.utils.translation import gettext_lazy as _\n\nfrom apps.core.models import TimeStampedModel, UUIDModel\n\n\nclass ExecutiveMetric(UUIDModel, TimeStampedModel):\n \"\"\"\n Executive Metric - Daily KPI snapshots for trending and analysis.\n \n Stores aggregated metrics across all hospitals for executive overview.\n Used for trend analysis and dashboard display.\n \"\"\"\n\n METRIC_TYPES = [\n (\"complaints_total\", _(\"Total Complaints\")),\n (\"complaints_critical\", _(\"Critical Complaints\")),\n (\"complaints_overdue\", _(\"Overdue Complaints\")),\n (\"complaints_resolution_time\", _(\"Avg Resolution Time (hours)\")),\n (\"surveys_total\", _(\"Total Surveys\")),\n (\"surveys_satisfaction\", _(\"Satisfaction Rate %\")),\n (\"surveys_nps\", _(\"NPS Score\")),\n (\"surveys_response_rate\", _(\"Response Rate %\")),\n (\"actions_total\", _(\"Total Actions\")),\n (\"actions_open\", _(\"Open Actions\")),\n (\"actions_overdue\", _(\"Overdue Actions\")),\n (\"actions_closed\", _(\"Closed Actions\")),\n (\"observations_total\", _(\"Total Observations\")),\n (\"observations_critical\", _(\"Critical Observations\")),\n (\"inquiries_total\", _(\"Total Inquiries\")),\n (\"inquiries_resolved\", _(\"Resolved Inquiries\")),\n (\"call_center_total\", _(\"Call Center Interactions\")),\n (\"call_center_satisfaction\", _(\"Call Center Satisfaction %\")),\n (\"physician_avg_rating\", _(\"Avg Physician Rating\")),\n ]\n\n # Date for this metric snapshot\n metric_date = models.DateField(db_index=True, help_text=\"Date of this metric snapshot\")\n \n # Metric type\n metric_type = models.CharField(max_length=50, choices=METRIC_TYPES, db_index=True)\n \n # Metric value\n metric_value = models.DecimalField(max_digits=10, decimal_places=2)\n \n # Variance from previous period\n variance = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, \n help_text=\"Variance from previous period\")\n \n # Variance direction\n variance_direction = models.CharField(\n max_length=10,\n choices=[(\"up\", \"Up\"), (\"down\", \"Down\"), (\"neutral\", \"Neutral\")],\n default=\"neutral\",\n help_text=\"Direction of variance\"\n )\n \n # Hospital (null = system-wide aggregate)\n hospital = models.ForeignKey(\n \"organizations.Hospital\",\n on_delete=models.CASCADE,\n null=True,\n blank=True,\n related_name=\"executive_metrics\"\n )\n \n # Metadata\n metadata = models.JSONField(default=dict, blank=True)\n\n class Meta:\n ordering = [\"-metric_date\", \"metric_type\"]\n indexes = [\n models.Index(fields=[\"metric_date\", \"metric_type\"]),\n models.Index(fields=[\"hospital\", \"metric_date\"]),\n ]\n verbose_name = _(\"Executive Metric\")\n verbose_name_plural = _(\"Executive Metrics\")\n\n def __str__(self):\n hospital_name = f\" - {self.hospital.name}\" if self.hospital else \" (All)\"\n return f\"{self.get_metric_type_display()}: {self.metric_value}{hospital_name} ({self.metric_date})\"\n\n\nclass ExecutiveReport(UUIDModel, TimeStampedModel):\n \"\"\"\n Executive Report - AI-generated weekly/monthly narrative summaries.\n \n Contains natural language summaries generated by AI analyzing\n performance data across all hospitals.\n \"\"\"\n\n REPORT_TYPES = [\n (\"weekly\", _(\"Weekly Summary\")),\n (\"monthly\", _(\"Monthly Summary\")),\n (\"quarterly\", _(\"Quarterly Summary\")),\n (\"custom\", _(\"Custom Report\")),\n ]\n\n STATUS_CHOICES = [\n (\"pending\", _(\"Pending\")),\n (\"generating\", _(\"Generating\")),\n (\"completed\", _(\"Completed\")),\n (\"failed\", _(\"Failed\")),\n ]\n\n # Report metadata\n report_type = models.CharField(max_length=20, choices=REPORT_TYPES, db_index=True)\n status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=\"pending\", db_index=True)\n \n # Date range\n start_date = models.DateField()\n end_date = models.DateField()\n \n # AI-generated narratives (bilingual)\n narrative_en = models.TextField(blank=True, help_text=\"English narrative summary\")\n narrative_ar = models.TextField(blank=True, help_text=\"Arabic narrative summary\")\n \n # Key highlights\n highlights_en = models.JSONField(default=list, blank=True, help_text=\"Key highlights (English)\")\n highlights_ar = models.JSONField(default=list, blank=True, help_text=\"Key highlights (Arabic)\")\n \n # Key concerns\n concerns_en = models.JSONField(default=list, blank=True, help_text=\"Key concerns (English)\")\n concerns_ar = models.JSONField(default=list, blank=True, help_text=\"Key concerns (Arabic)\")\n \n # Summary metrics snapshot\n metrics_snapshot = models.JSONField(default=dict, blank=True, help_text=\"Key metrics at report time\")\n \n # AI metadata\n ai_model = models.CharField(max_length=100, blank=True, help_text=\"AI model used for generation\")\n generation_time_ms = models.IntegerField(null=True, blank=True, help_text=\"Generation time in milliseconds\")\n \n # PDF report file (if generated)\n pdf_file = models.FileField(\n upload_to=\"executive_reports/pdfs/%Y/%m/\",\n null=True,\n blank=True,\n help_text=\"Generated PDF report\"\n )\n \n # Error message if failed\n error_message = models.TextField(blank=True, help_text=\"Error message if generation failed\")\n\n class Meta:\n ordering = [\"-created_at\"]\n indexes = [\n models.Index(fields=[\"report_type\", \"-created_at\"]),\n models.Index(fields=[\"status\", \"-created_at\"]),\n ]\n verbose_name = _(\"Executive Report\")\n verbose_name_plural = _(\"Executive Reports\")\n\n def __str__(self):\n return f\"{self.get_report_type_display()} ({self.start_date} to {self.end_date}) - {self.get_status_display()}\"\n\n\nclass PredictiveInsight(UUIDModel, TimeStampedModel):\n \"\"\"\n Predictive Insight - AI-detected patterns, anomalies, and early warnings.\n \n Automatically generated insights identifying:\n - Emerging complaint patterns\n - Declining satisfaction trends\n - SLA breach risks\n - Performance anomalies\n - Resource bottlenecks\n \"\"\"\n\n INSIGHT_TYPES = [\n (\"trend_change\", _(\"Trend Change\")),\n (\"anomaly\", _(\"Anomaly Detected\")),\n (\"risk_warning\", _(\"Risk Warning\")),\n (\"sla_breach_risk\", _(\"SLA Breach Risk\")),\n (\"performance_drop\", _(\"Performance Drop\")),\n (\"volume_spike\", _(\"Volume Spike\")),\n (\"satisfaction_decline\", _(\"Satisfaction Decline\")),\n (\"positive_trend\", _(\"Positive Trend\")),\n ]\n\n SEVERITY_LEVELS = [\n (\"low\", _(\"Low\")),\n (\"medium\", _(\"Medium\")),\n (\"high\", _(\"High\")),\n (\"critical\", _(\"Critical\")),\n ]\n\n STATUS_CHOICES = [\n (\"new\", _(\"New\")),\n (\"acknowledged\", _(\"Acknowledged\")),\n (\"resolved\", _(\"Resolved\")),\n (\"dismissed\", _(\"Dismissed\")),\n ]\n\n # Insight details\n insight_type = models.CharField(max_length=30, choices=INSIGHT_TYPES, db_index=True)\n severity = models.CharField(max_length=10, choices=SEVERITY_LEVELS, default=\"medium\", db_index=True)\n status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=\"new\", db_index=True)\n \n # Title and description (bilingual)\n title_en = models.CharField(max_length=300)\n title_ar = models.CharField(max_length=300, blank=True)\n description_en = models.TextField()\n description_ar = models.TextField(blank=True)\n \n # AI-generated recommendation\n recommendation_en = models.TextField(blank=True)\n recommendation_ar = models.TextField(blank=True)\n \n # Affected entities\n hospital = models.ForeignKey(\n \"organizations.Hospital\",\n on_delete=models.SET_NULL,\n null=True,\n blank=True,\n related_name=\"predictive_insights\"\n )\n department = models.ForeignKey(\n \"organizations.Department\",\n on_delete=models.SET_NULL,\n null=True,\n blank=True,\n related_name=\"predictive_insights\"\n )\n \n # Metric affected\n metric_type = models.CharField(max_length=50, blank=True, help_text=\"Related metric type\")\n \n # Predicted values\n current_value = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)\n predicted_value = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)\n confidence_score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True, \n help_text=\"AI confidence score (0-100)\")\n \n # Time-related\n predicted_date = models.DateField(null=True, blank=True, help_text=\"When this is expected to occur\")\n \n # Acknowledgement\n acknowledged_by = models.ForeignKey(\n \"accounts.User\",\n on_delete=models.SET_NULL,\n null=True,\n blank=True,\n related_name=\"acknowledged_insights\"\n )\n acknowledged_at = models.DateTimeField(null=True, blank=True)\n \n # AI metadata\n ai_model = models.CharField(max_length=100, blank=True)\n detection_metadata = models.JSONField(default=dict, blank=True)\n\n class Meta:\n ordering = [\"-severity\", \"-created_at\"]\n indexes = [\n models.Index(fields=[\"status\", \"severity\", \"-created_at\"]),\n models.Index(fields=[\"hospital\", \"status\"]),\n ]\n verbose_name = _(\"Predictive Insight\")\n verbose_name_plural = _(\"Predictive Insights\")\n\n def __str__(self):\n return f\"[{self.get_severity_display()}] {self.title_en}\"\n\n\nclass AIRecommendation(UUIDModel, TimeStampedModel):\n \"\"\"\n AI Recommendation - Suggested actions based on data analysis.\n \n AI-generated recommendations for:\n - Process improvements\n - Resource allocation\n - Training needs\n - Policy changes\n - Preventive measures\n \"\"\"\n\n CATEGORY_CHOICES = [\n (\"process_improvement\", _(\"Process Improvement\")),\n (\"resource_allocation\", _(\"Resource Allocation\")),\n (\"training\", _(\"Training\")),\n (\"policy_change\", _(\"Policy Change\")),\n (\"preventive_action\", _(\"Preventive Action\")),\n (\"performance_optimization\", _(\"Performance Optimization\")),\n (\"communication\", _(\"Communication\")),\n (\"quality_assurance\", _(\"Quality Assurance\")),\n ]\n\n PRIORITY_LEVELS = [\n (\"low\", _(\"Low\")),\n (\"medium\", _(\"Medium\")),\n (\"high\", _(\"High\")),\n (\"urgent\", _(\"Urgent\")),\n ]\n\n STATUS_CHOICES = [\n (\"new\", _(\"New\")),\n (\"under_review\", _(\"Under Review\")),\n (\"approved\", _(\"Approved\")),\n (\"implemented\", _(\"Implemented\")),\n (\"rejected\", _(\"Rejected\")),\n ]\n\n # Recommendation details\n category = models.CharField(max_length=30, choices=CATEGORY_CHOICES, db_index=True)\n priority = models.CharField(max_length=10, choices=PRIORITY_LEVELS, default=\"medium\", db_index=True)\n status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=\"new\", db_index=True)\n \n # Title and description (bilingual)\n title_en = models.CharField(max_length=300)\n title_ar = models.CharField(max_length=300, blank=True)\n description_en = models.TextField()\n description_ar = models.TextField(blank=True)\n \n # Expected impact\n expected_impact_en = models.TextField(blank=True, help_text=\"Expected impact if implemented (English)\")\n expected_impact_ar = models.TextField(blank=True, help_text=\"Expected impact if implemented (Arabic)\")\n \n # Related data\n hospital = models.ForeignKey(\n \"organizations.Hospital\",\n on_delete=models.SET_NULL,\n null=True,\n blank=True,\n related_name=\"ai_recommend\"\n )\n department = models.ForeignKey(\n \"organizations.Department\",\n on_delete=models.SET_NULL,\n null=True,\n blank=True,\n related_name=\"ai_recommend\"\n )\n \n # Related insight (if generated from one)\n related_insight = models.ForeignKey(\n PredictiveInsight,\n on_delete=models.SET_NULL,\n null=True,\n blank=True,\n related_name=\"recommendations\"\n )\n \n # Implementation tracking\n implemented_by = models.ForeignKey(\n \"accounts.User\",\n on_delete=models.SET_NULL,\n null=True,\n blank=True,\n related_name=\"implemented_recommendations\"\n )\n implemented_at = models.DateTimeField(null=True, blank=True)\n \n # AI metadata\n confidence_score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)\n ai_model = models.CharField(max_length=100, blank=True)\n generation_metadata = models.JSONField(default=dict, blank=True)\n\n class Meta:\n ordering = [\"-priority\", \"-created_at\"]\n indexes = [\n models.Index(fields=[\"status\", \"priority\", \"-created_at\"]),\n models.Index(fields=[\"hospital\", \"status\"]),\n ]\n verbose_name = _(\"AI Recommendation\")\n verbose_name_plural = _(\"AI Recommendations\")\n\n def __str__(self):\n return f\"[{self.get_priority_display()}] {self.title_en}\"\n",
"file_path": "/home/ismail/projects/HH/apps/executive_summary/models.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/models.py`
**Output:**
*Diff for `models.py`:*
```diff
"""
Executive Summary models - AI-powered executive dashboards and reports
This module implements:
- Executive metrics (KPI snapshots for trending)
- Executive reports (AI-generated weekly/monthly summaries)
- Predictive insights (AI-detected patterns and risks)
- AI recommendations (suggested actions)
"""
from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.core.models import TimeStampedModel, UUIDModel
class ExecutiveMetric(UUIDModel, TimeStampedModel):
"""
Executive Metric - Daily KPI snapshots for trending and analysis.
Stores aggregated metrics across all hospitals for executive overview.
Used for trend analysis and dashboard display.
"""
METRIC_TYPES = [
("complaints_total", _("Total Complaints")),
("complaints_critical", _("Critical Complaints")),
("complaints_overdue", _("Overdue Complaints")),
("complaints_resolution_time", _("Avg Resolution Time (hours)")),
("surveys_total", _("Total Surveys")),
("surveys_satisfaction", _("Satisfaction Rate %")),
("surveys_nps", _("NPS Score")),
("surveys_response_rate", _("Response Rate %")),
("actions_total", _("Total Actions")),
("actions_open", _("Open Actions")),
("actions_overdue", _("Overdue Actions")),
("actions_closed", _("Closed Actions")),
("observations_total", _("Total Observations")),
("observations_critical", _("Critical Observations")),
("inquiries_total", _("Total Inquiries")),
("inquiries_resolved", _("Resolved Inquiries")),
("call_center_total", _("Call Center Interactions")),
("call_center_satisfaction", _("Call Center Satisfaction %")),
("physician_avg_rating", _("Avg Physician Rating")),
]
# Date for this metric snapshot
metric_date = models.DateField(db_index=True, help_text="Date of this metric snapshot")
# Metric type
metric_type = models.CharField(max_length=50, choices=METRIC_TYPES, db_index=True)
# Metric value
metric_value = models.DecimalField(max_digits=10, decimal_places=2)
# Variance from previous period
variance = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True,
help_text="Variance from previous period")
# Variance direction
variance_direction = models.CharField(
max_length=10,
choices=[("up", "Up"), ("down", "Down"), ("neutral", "Neutral")],
default="neutral",
help_text="Direction of variance"
)
# Hospital (null = system-wide aggregate)
hospital = models.ForeignKey(
"organizations.Hospital",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="executive_metrics"
)
# Metadata
metadata = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ["-metric_date", "metric_type"]
indexes = [
models.Index(fields=["metric_date", "metric_type"]),
models.Index(fields=["hospital", "metric_date"]),
]
verbose_name = _("Executive Metric")
verbose_name_plural = _("Executive Metrics")
def __str__(self):
hospital_name = f" - {self.hospital.name}" if self.hospital else " (All)"
return f"{self.get_metric_type_display()}: {self.metric_value}{hospital_name} ({self.metric_date})"
class ExecutiveReport(UUIDModel, TimeStampedModel):
"""
Executive Report - AI-generated weekly/monthly narrative summaries.
Contains natural language summaries generated by AI analyzing
performance data across all hospitals.
"""
REPORT_TYPES = [
("weekly", _("Weekly Summary")),
("monthly", _("Monthly Summary")),
("quarterly", _("Quarterly Summary")),
("custom", _("Custom Report")),
]
STATUS_CHOICES = [
("pending", _("Pending")),
("generating", _("Generating")),
("completed", _("Completed")),
("failed", _("Failed")),
]
# Report metadata
report_type = models.CharField(max_length=20, choices=REPORT_TYPES, db_index=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending", db_index=True)
# Date range
start_date = models.DateField()
end_date = models.DateField()
# AI-generated narratives (bilingual)
narrative_en = models.TextField(blank=True, help_text="English narrative summary")
narrative_ar = models.TextField(blank=True, help_text="Arabic narrative summary")
# Key highlights
highlights_en = models.JSONField(default=list, blank=True, help_text="Key highlights (English)")
highlights_ar = models.JSONField(default=list, blank=True, help_text="Key highlights (Arabic)")
# Key concerns
concerns_en = models.JSONField(default=list, blank=True, help_text="Key concerns (English)")
concerns_ar = models.JSONField(default=list, blank=True, help_text="Key concerns (Arabic)")
# Summary metrics snapshot
metrics_snapshot = models.JSONField(default=dict, blank=True, help_text="Key metrics at report time")
# AI metadata
ai_model = models.CharField(max_length=100, blank=True, help_text="AI model used for generation")
generation_time_ms = models.IntegerField(null=True, blank=True, help_text="Generation time in milliseconds")
# PDF report file (if generated)
pdf_file = models.FileField(
upload_to="executive_reports/pdfs/%Y/%m/",
null=True,
blank=True,
help_text="Generated PDF report"
)
# Error message if failed
error_message = models.TextField(blank=True, help_text="Error message if generation failed")
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["report_type", "-created_at"]),
models.Index(fields=["status", "-created_at"]),
]
verbose_name = _("Executive Report")
verbose_name_plural = _("Executive Reports")
def __str__(self):
return f"{self.get_report_type_display()} ({self.start_date} to {self.end_date}) - {self.get_status_display()}"
class PredictiveInsight(UUIDModel, TimeStampedModel):
"""
Predictive Insight - AI-detected patterns, anomalies, and early warnings.
Automatically generated insights identifying:
- Emerging complaint patterns
- Declining satisfaction trends
- SLA breach risks
- Performance anomalies
- Resource bottlenecks
"""
INSIGHT_TYPES = [
("trend_change", _("Trend Change")),
("anomaly", _("Anomaly Detected")),
("risk_warning", _("Risk Warning")),
("sla_breach_risk", _("SLA Breach Risk")),
("performance_drop", _("Performance Drop")),
("volume_spike", _("Volume Spike")),
("satisfaction_decline", _("Satisfaction Decline")),
("positive_trend", _("Positive Trend")),
]
SEVERITY_LEVELS = [
("low", _("Low")),
("medium", _("Medium")),
("high", _("High")),
("critical", _("Critical")),
]
STATUS_CHOICES = [
("new", _("New")),
("acknowledged", _("Acknowledged")),
("resolved", _("Resolved")),
("dismissed", _("Dismissed")),
]
# Insight details
insight_type = models.CharField(max_length=30, choices=INSIGHT_TYPES, db_index=True)
severity = models.CharField(max_length=10, choices=SEVERITY_LEVELS, default="medium", db_index=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="new", db_index=True)
# Title and description (bilingual)
title_en = models.CharField(max_length=300)
title_ar = models.CharField(max_length=300, blank=True)
description_en = models.TextField()
description_ar = models.TextField(blank=True)
# AI-generated recommendation
recommendation_en = models.TextField(blank=True)
recommendation_ar = models.TextField(blank=True)
# Affected entities
hospital = models.ForeignKey(
"organizations.Hospital",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="predictive_insights"
)
department = models.ForeignKey(
"organizations.Department",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="predictive_insights"
)
# Metric affected
metric_type = models.CharField(max_length=50, blank=True, help_text="Related metric type")
# Predicted values
current_value = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
predicted_value = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
confidence_score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True,
help_text="AI confidence score (0-100)")
# Time-related
predicted_date = models.DateField(null=True, blank=True, help_text="When this is expected to occur")
# Acknowledgement
acknowledged_by = models.ForeignKey(
"accounts.User",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="acknowledged_insights"
)
acknowledged_at = models.DateTimeField(null=True, blank=True)
# AI metadata
ai_model = models.CharField(max_length=100, blank=True)
detection_metadata = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ["-severity", "-created_at"]
indexes = [
models.Index(fields=["status", "severity", "-created_at"]),
models.Index(fields=["hospital", "status"]),
]
verbose_name = _("Predictive Insight")
verbose_name_plural = _("Predictive Insights")
def __str__(self):
return f"[{self.get_severity_display()}] {self.title_en}"
class AIRecommendation(UUIDModel, TimeStampedModel):
"""
AI Recommendation - Suggested actions based on data analysis.
AI-generated recommendations for:
- Process improvements
- Resource allocation
- Training needs
- Policy changes
- Preventive measures
"""
CATEGORY_CHOICES = [
("process_improvement", _("Process Improvement")),
("resource_allocation", _("Resource Allocation")),
("training", _("Training")),
("policy_change", _("Policy Change")),
("preventive_action", _("Preventive Action")),
("performance_optimization", _("Performance Optimization")),
("communication", _("Communication")),
("quality_assurance", _("Quality Assurance")),
]
PRIORITY_LEVELS = [
("low", _("Low")),
("medium", _("Medium")),
("high", _("High")),
("urgent", _("Urgent")),
]
STATUS_CHOICES = [
("new", _("New")),
("under_review", _("Under Review")),
("approved", _("Approved")),
("implemented", _("Implemented")),
("rejected", _("Rejected")),
]
# Recommendation details
category = models.CharField(max_length=30, choices=CATEGORY_CHOICES, db_index=True)
priority = models.CharField(max_length=10, choices=PRIORITY_LEVELS, default="medium", db_index=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="new", db_index=True)
# Title and description (bilingual)
title_en = models.CharField(max_length=300)
title_ar = models.CharField(max_length=300, blank=True)
description_en = models.TextField()
description_ar = models.TextField(blank=True)
# Expected impact
expected_impact_en = models.TextField(blank=True, help_text="Expected impact if implemented (English)")
expected_impact_ar = models.TextField(blank=True, help_text="Expected impact if implemented (Arabic)")
# Related data
hospital = models.ForeignKey(
"organizations.Hospital",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="ai_recommend"
)
department = models.ForeignKey(
"organizations.Department",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="ai_recommend"
)
# Related insight (if generated from one)
related_insight = models.ForeignKey(
PredictiveInsight,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="recommendations"
)
# Implementation tracking
implemented_by = models.ForeignKey(
"accounts.User",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="implemented_recommendations"
)
implemented_at = models.DateTimeField(null=True, blank=True)
# AI metadata
confidence_score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
ai_model = models.CharField(max_length=100, blank=True)
generation_metadata = models.JSONField(default=dict, blank=True)
class Meta:
ordering = ["-priority", "-created_at"]
indexes = [
models.Index(fields=["status", "priority", "-created_at"]),
models.Index(fields=["hospital", "status"]),
]
verbose_name = _("AI Recommendation")
verbose_name_plural = _("AI Recommendations")
def __str__(self):
return f"[{self.get_priority_display()}] {self.title_en}"
```
## Assistant
Now that the models are created, I need to update the todo list to mark the completed tasks and move to the next one.
## Assistant
Perfect! Now let me update the todo and create the services. Due to the comprehensive nature of this feature, I'll create the services file with all AI and data aggregation logic:
### Tool: TodoWrite
**Status**: completed
**Output:**
```
- [x] Create Executive role and permissions
- [x] Create executive_summary Django app structure (models, views, services, tasks)
- [x] Build ExecutiveMetric and ExecutiveReport models
- [-] Create ExecutiveSummaryService for data aggregation
- [ ] Build Executive Dashboard view with KPI cards and filters
- [ ] Create Executive Dashboard template with ApexCharts
- [x] Add URL routing and sidebar navigation
- [ ] Implement AI Narrative Service using LiteLLM
- [ ] Build Predictive Analytics Service (trend analysis, risk detection)
- [ ] Create Recommendation Engine
- [ ] Integrate AI-generated briefs into dashboard
- [ ] Implement Natural Language Q&A interface
- [ ] Build Predictive Insights view and risk scoring
- [ ] Create Hospital comparison and leaderboard views
- [ ] Implement PDF report generation with WeasyPrint
- [ ] Create Celery scheduled tasks for automated reports
- [ ] Build mobile-optimized responsive view
- [ ] Add email delivery system for scheduled reports
- [ ] Add Arabic translations and RTL support
- [ ] Performance optimization and testing
```
## Assistant
Given the comprehensive scope of this feature, I should delegate the services implementation to a general-purpose agent while I proceed with creating the views and templates.
## Assistant
Now let me create the comprehensive services file. This will include data aggregation, AI narratives, predictive analytics, and recommendations. Due to the size, I'll create it in parts:
### Tool: Agent: Create executive summary services
**Status**: completed
**Input:**
````json
{
"description": "Create executive summary services",
"prompt": "Create a comprehensive services.py file for the executive_summary Django app at /home/ismail/projects/HH/apps/executive_summary/services.py\n\nThis file should include:\n\n1. **ExecutiveSummaryService** class with methods to:\n - aggregate_daily_metrics() - aggregates complaints, surveys, actions, observations data across all hospitals for a given date\n - get_dashboard_data(hospital=None, date_range=30) - returns comprehensive dashboard data with KPIs, trends, hospital comparisons\n - calculate_variance(current, previous) - calculates percentage variance\n - get_hospital_leaderboard() - ranks hospitals by satisfaction, complaint resolution, etc.\n - get_trend_data(metric_type, days=30) - returns trend data for charts\n\n2. **AINarrativeService** class using LiteLLM (import litellm from existing setup):\n - generate_weekly_narrative(start_date, end_date, hospital=None) - generates English narrative summary\n - generate_arabic_narrative(start_date, end_date, hospital=None) - generates Arabic narrative\n - format_metrics_for_ai(metrics_data) - formats metrics into prompt\n - The AI should analyze trends, highlight achievements, identify concerns, and provide actionable insights\n\n3. **PredictiveAnalyticsService** class:\n - detect_anomalies(metric_type, days=90) - detects statistical anomalies in metrics\n - predict_sla_breach(hospital=None, days_ahead=7) - predicts potential SLA breaches\n - identify_trend_changes(metric_type, days=60) - identifies significant trend changes\n - calculate_risk_scores(hospital=None) - calculates risk scores for hospitals/departments\n - generate_predictive_insights() - creates PredictiveInsight objects from analysis\n\n4. **RecommendationService** class:\n - generate_recommendations_from_insights() - creates AIRecommendation objects based on PredictiveInsights\n - analyze_best_practices() - identifies successful patterns from high-performing hospitals\n - suggest_resource_allocation() - suggests optimal resource allocation based on data\n\nUse Django ORM queries similar to the existing dashboard/views.py patterns. Reference models from apps.complaints.models, apps.surveys.models, apps.px_action_center.models, apps.observations.models.\n\nFor LiteLLM usage, use:\n```python\nfrom litellm import completion\n\nresponse = completion(\n model=\"openrouter/google/gemma-3-27b-it:free\",\n messages=[{\"role\": \"user\", \"content\": prompt}]\n)\n```\n\nMake the code production-ready with proper error handling, logging, and type hints. Use the translation framework for bilingual support.",
"subagent_type": "general-purpose"
}
````
**Output:**
```
The file has been created and passes syntax validation. Here is a summary of what was built:
**File created:** `/home/ismail/projects/HH/apps/executive_summary/services.py`
**Four service classes:**
1. **`ExecutiveSummaryService`** -- Aggregates metrics from complaints, surveys, PX actions, observations, inquiries, call center interactions, and physician ratings. Key methods:
- `aggregate_daily_metrics(target_date, hospital)` -- daily snapshot of all metric types
- `get_dashboard_data(hospital, date_range)` -- full dashboard payload with KPIs, variances, trends, and hospital leaderboard
- `calculate_variance(current, previous)` -- percentage variance with direction
- `get_hospital_leaderboard()` -- ranks hospitals by composite score (resolution rate 30%, satisfaction 30%, overdue rate 20%, action closure 20%)
- `get_trend_data(metric_type, days, hospital)` -- daily trend data for charting
2. **`AINarrativeService`** -- Uses LiteLLM (`openrouter/google/gemma-3-27b-it:free`) to generate executive narratives. Key methods:
- `generate_weekly_narrative(start_date, end_date, hospital)` -- English narrative with JSON-parsed executive summary, highlights, concerns, and actionable insights
- `generate_arabic_narrative(start_date, end_date, hospital)` -- Arabic equivalent with Arabic prompt
- `format_metrics_for_ai(metrics_data)` -- formats metrics into a structured human-readable prompt
3. **`PredictiveAnalyticsService`** -- Statistical analysis and prediction. Key methods:
- `detect_anomalies(metric_type, days, hospital)` -- z-score based anomaly detection (thresholds: >2 = medium, >2.5 = high, >3 = critical)
- `predict_sla_breach(hospital, days_ahead)` -- predicts SLA breaches for complaints, PX actions, and observations approaching their deadline
- `identify_trend_changes(metric_type, days, hospital)` -- compares first/second half averages, detects acceleration/deceleration
- `calculate_risk_scores(hospital)` -- composite risk score (SLA breach 30%, overdue 25%, satisfaction decline 20%, critical volume 15%, resolution time trend 10%)
- `generate_predictive_insights()` -- creates `PredictiveInsight` ORM objects from all analyses
4. **`RecommendationService`** -- Generates actionable recommendations. Key methods:
- `generate_recommendations_from_insights()` -- creates `AIRecommendation` objects for unhandled `PredictiveInsight` entries (with AI-generated content via LiteLLM)
- `analyze_best_practices()` -- identifies patterns from top-quartile hospitals and produces gap analysis for lower performers
- `suggest_resource_allocation()` -- analyzes workload distribution across hospitals and produces prioritized resource allocation recommendations
All classes use proper error handling with logging, type hints, and Django ORM query patterns consistent with `apps/dashboard/views.py`.
```
## Assistant
Now that services are created, I need to update the todo list to reflect progress and move on to building the views.
## Assistant
Excellent! Now let me create the views file for the Executive Dashboard. Let me update the todo and create the views:
### Tool: TodoWrite
**Status**: completed
**Output:**
```
- [x] Create Executive role and permissions
- [x] Create executive_summary Django app structure (models, views, services, tasks)
- [x] Build ExecutiveMetric and ExecutiveReport models
- [x] Create ExecutiveSummaryService for data aggregation
- [-] Build Executive Dashboard view with KPI cards and filters
- [ ] Create Executive Dashboard template with ApexCharts
- [x] Add URL routing and sidebar navigation
- [x] Implement AI Narrative Service using LiteLLM
- [x] Build Predictive Analytics Service (trend analysis, risk detection)
- [x] Create Recommendation Engine
- [ ] Integrate AI-generated briefs into dashboard
- [ ] Implement Natural Language Q&A interface
- [ ] Build Predictive Insights view and risk scoring
- [ ] Create Hospital comparison and leaderboard views
- [ ] Implement PDF report generation with WeasyPrint
- [ ] Create Celery scheduled tasks for automated reports
- [ ] Build mobile-optimized responsive view
- [ ] Add email delivery system for scheduled reports
- [ ] Add Arabic translations and RTL support
- [ ] Performance optimization and testing
```
## Assistant
Given the comprehensive scope of the remaining implementation tasks (views, templates, Q&A, PDF generation, Celery tasks, etc.), I should delegate these to a general-purpose agent to implement in parallel while keeping the todo list updated.
## Assistant
Now let me create the comprehensive views, URLs, templates, and tasks. I'll create them in parallel:
### Tool: Agent: Create executive summary views
**Status**: completed
**Input:**
````json
{
"description": "Create executive summary views",
"prompt": "Create a comprehensive views.py file for /home/ismail/projects/HH/apps/executive_summary/views.py\n\nThis Django app provides an AI-powered Executive Summary dashboard for top management in a healthcare patient experience system (PX360).\n\nCreate these views:\n\n1. **ExecutiveDashboardView** (LoginRequiredMixin, TemplateView):\n - URL: /executive/\n - Template: executive/dashboard.html\n - Permission: Requires user.is_executive() OR user.is_px_admin()\n - Context data:\n - AI narrative brief (latest ExecutiveReport)\n - KPI cards: complaints, surveys, actions, observations with variances\n - Risk alerts (high/critical PredictiveInsights)\n - Hospital leaderboard\n - AI recommendations (top 5)\n - Chart data (trends)\n - Filters: hospital selection, date range\n - Follow same patterns as /home/ismail/projects/HH/apps/dashboard/views.py CommandCenterView\n - Support filtering by hospital and date range\n\n2. **executive_dashboard_api** (login_required API view):\n - URL: /executive/api/\n - Returns JSON with latest metrics for HTMX updates\n - Same data structure as dashboard context\n\n3. **PredictiveInsightsView** (LoginRequiredMixin, TemplateView):\n - URL: /executive/insights/\n - Template: executive/insights.html\n - Shows all predictive insights with filtering\n - Allows acknowledging insights\n\n4. **acknowledge_insight** (POST view):\n - URL: /executive/insights/<uuid>/acknowledge/\n - Marks insight as acknowledged by user\n\n5. **ExecutiveQ&AView** (LoginRequiredMixin, TemplateView):\n - URL: /executive/qa/\n - Template: executive/qa.html\n - Natural language Q&A interface\n - Accepts question, returns AI answer with data\n\n6. **generate_executive_report** (admin view):\n - URL: /executive/reports/generate/\n - Triggers AI report generation\n - Creates ExecutiveReport object\n\n7. **ExecutivePDFReportView**:\n - URL: /executive/reports/<uuid>/pdf/\n - Generates PDF report using WeasyPrint\n - Uses WeasyPrint (already installed)\n - Returns PDF download\n\nUse these imports:\n```python\nfrom django.contrib.auth.decorators import login_required\nfrom django.contrib.auth.mixins import LoginRequiredMixin\nfrom django.shortcuts import render, redirect, get_object_or_404\nfrom django.views.generic import TemplateView\nfrom django.http import JsonResponse, HttpResponse\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\nfrom django.conf import settings\nfrom datetime import timedelta, datetime\nimport json\nimport logging\n```\n\nFor PDF generation, use WeasyPrint:\n```python\nfrom weasyprint import HTML\nhtml_string = render_to_string('executive/pdf_report.html', context)\npdf_file = HTML(string=html_string).write_pdf()\n```\n\nFor AI Q&A, use LiteLLM:\n```python\nfrom litellm import completion\nresponse = completion(\n model=\"openrouter/google/gemma-3-27b-it:free\",\n messages=[{\"role\": \"user\", \"content\": prompt}]\n)\n```\n\nMake sure all views check for executive or px_admin permissions. Add proper logging and error handling. Follow Django best practices.",
"subagent_type": "general-purpose"
}
````
**Output:**
```
[Operation Cancelled] Reason: User cancelled tool execution.
```
### Tool: Agent: Create executive URLs
**Status**: completed
**Input:**
````json
{
"description": "Create executive URLs",
"prompt": "Create a urls.py file for /home/ismail/projects/HH/apps/executive_summary/urls.py\n\nThis should define URL patterns for the executive summary Django app.\n\nImport these views from .views:\n- ExecutiveDashboardView\n- executive_dashboard_api\n- PredictiveInsightsView\n- acknowledge_insight\n- ExecutiveQ&AView (use ExecutiveQAView instead to avoid special chars)\n- generate_executive_report\n- ExecutivePDFReportView\n\nCreate urlpatterns with:\n```python\nfrom django.urls import path\nfrom . import views\n\napp_name = \"executive\"\n\nurlpatterns = [\n path(\"\", views.ExecutiveDashboardView.as_view(), name=\"executive_dashboard\"),\n path(\"api/\", views.executive_dashboard_api, name=\"executive_api\"),\n path(\"insights/\", views.PredictiveInsightsView.as_view(), name=\"predictive_insights\"),\n path(\"insights/<uuid:insight_id>/acknowledge/\", views.acknowledge_insight, name=\"acknowledge_insight\"),\n path(\"qa/\", views.ExecutiveQAView.as_view(), name=\"executive_qa\"),\n path(\"qa/api/\", views.executive_qa_api, name=\"executive_qa_api\"),\n path(\"reports/generate/\", views.generate_executive_report, name=\"generate_report\"),\n path(\"reports/<uuid:report_id>/pdf/\", views.ExecutivePDFReportView.as_view(), name=\"report_pdf\"),\n]\n```\n\nMake sure to handle the import correctly - ExecutiveQ&AView should be ExecutiveQAView in the actual code.",
"subagent_type": "general-purpose"
}
````
**Output:**
```
Created `/home/ismail/projects/HH/apps/executive_summary/urls.py` with the URL patterns for the executive summary app. The file defines 8 routes under the `executive` app namespace, covering the dashboard, API endpoints, predictive insights, Q&A, and PDF report generation.
```
### Tool: Agent: Create executive Celery tasks
**Status**: completed
**Input:**
````json
{
"description": "Create executive Celery tasks",
"prompt": "Create a tasks.py file for /home/ismail/projects/HH/apps/executive_summary/tasks.py\n\nThis file defines Celery tasks for the executive summary app in a Django healthcare system (PX360).\n\nTasks to create:\n\n1. **generate_weekly_executive_summary**:\n - Runs every Monday at 6 AM\n - Generates weekly executive summary report\n - Uses AINarrativeService.generate_weekly_narrative()\n - Saves ExecutiveReport object\n - Logs success/failure\n\n2. **generate_monthly_executive_summary**:\n - Runs on 1st of each month at 7 AM\n - Generates monthly executive summary report\n - Same pattern as weekly\n\n3. **calculate_daily_metrics**:\n - Runs daily at 1 AM\n - Aggregates metrics for previous day across all hospitals\n - Uses ExecutiveSummaryService.aggregate_daily_metrics()\n - Creates ExecutiveMetric objects\n\n4. **generate_predictive_insights**:\n - Runs every 6 hours\n - Analyzes trends, anomalies, SLA breach risks\n - Creates PredictiveInsight objects\n - Uses PredictiveAnalyticsService\n\n5. **generate_ai_recommendations**:\n - Runs daily at 3 AM\n - Generates recommendations from insights\n - Uses RecommendationService\n\n6. **send_executive_pdf_report**:\n - Runs weekly and monthly after report generation\n - Emails PDF reports to executives\n - Uses Django email functionality\n\nUse this pattern:\n```python\nfrom celery import shared_task\nfrom django.utils import timezone\nfrom datetime import timedelta\nimport logging\n\nlogger = logging.getLogger(__name__)\n\n@shared_task\ndef task_name():\n \"\"\"Task docstring\"\"\"\n try:\n # Task logic\n logger.info(f\"Task completed successfully\")\n return {\"status\": \"success\"}\n except Exception as e:\n logger.error(f\"Task failed: {str(e)}\", exc_info=True)\n return {\"status\": \"error\", \"error\": str(e)}\n```\n\nImport services from .services import ExecutiveSummaryService, AINarrativeService, PredictiveAnalyticsService, RecommendationService\nImport models from .models import ExecutiveMetric, ExecutiveReport, PredictiveInsight, AIRecommendation\n\nMake tasks production-ready with proper error handling, logging, and retry logic.",
"subagent_type": "general-purpose"
}
````
**Output:**
```
Failed to run subagent: Model stream ended with empty response text.
```
## User
continue
## Assistant
Let me check what was created and continue building the remaining components for the executive summary feature.
## Assistant
Let me check what was created and continue building the remaining components:
### Tool: ReadFile: apps/executive_summary/urls.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/executive_summary/urls.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/urls.py`
**Output:**
```python
from django.urls import path
from . import views
app_name = "executive"
urlpatterns = [
path("", views.ExecutiveDashboardView.as_view(), name="executive_dashboard"),
path("api/", views.executive_dashboard_api, name="executive_api"),
path("insights/", views.PredictiveInsightsView.as_view(), name="predictive_insights"),
path("insights/<uuid:insight_id>/acknowledge/", views.acknowledge_insight, name="acknowledge_insight"),
path("qa/", views.ExecutiveQAView.as_view(), name="executive_qa"),
path("qa/api/", views.executive_qa_api, name="executive_qa_api"),
path("reports/generate/", views.generate_executive_report, name="generate_report"),
path("reports/<uuid:report_id>/pdf/", views.ExecutivePDFReportView.as_view(), name="report_pdf"),
]
```
## Assistant
Let me check what was created and continue building the remaining components for the executive summary feature.
### Tool: ReadFile: apps/executive_summary/views.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/executive_summary/views.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/views.py`
**Output:**
```python
Showing lines 1-660 of 749 total lines.
---
"""
Executive Summary Views - AI-powered executive dashboard and analytics
This module provides views for:
- Executive dashboard with KPIs, AI narratives, and risk alerts
- Predictive insights management
- Executive Q&A with AI
- Executive report generation and PDF export
"""
import json
import logging
from datetime import timedelta, datetime
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Avg, Count, Q
from django.http import JsonResponse, HttpResponse
from django.shortcuts import redirect, render, get_object_or_404
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from django.conf import settings
from django.contrib import messages
from weasyprint import HTML
from .models import ExecutiveReport, PredictiveInsight, AIRecommendation, ExecutiveMetric
from .services import (
ExecutiveSummaryService,
AINarrativeService,
PredictiveAnalyticsService,
RecommendationService,
)
logger = logging.getLogger(__name__)
# =============================================================================
# Mixins
# =============================================================================
class ExecutiveAccessMixin:
"""Mixin to restrict access to executive and px_admin users only."""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect("account_login")
if not (request.user.is_executive() or request.user.is_px_admin()):
messages.error(request, _("You do not have permission to access the executive dashboard."))
return redirect("core:home")
return super().dispatch(request, *args, **kwargs)
# =============================================================================
# Executive Dashboard Views
# =============================================================================
class ExecutiveDashboardView(ExecutiveAccessMixin, LoginRequiredMixin, TemplateView):
"""
Executive Dashboard - AI-powered overview for top management.
Shows:
- AI-generated narrative brief (latest ExecutiveReport)
- KPI cards with variances (complaints, surveys, actions, observations)
- Risk alerts (high/critical PredictiveInsights)
- Hospital leaderboard
- AI recommendations (top 5)
- Chart data (trends)
- Filters (hospital selection, date range)
"""
template_name = "executive/dashboard.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = self.request.user
# Parse filters
date_range = int(self.request.GET.get("date_range", 30))
hospital_id = self.request.GET.get("hospital")
# Initialize services
summary_service = ExecutiveSummaryService()
predictive_service = PredictiveAnalyticsService()
# Determine hospital scope
hospital = None
if hospital_id:
from apps.organizations.models import Hospital
hospital = get_object_or_404(Hospital, id=hospital_id)
elif user.is_hospital_admin() and user.hospital:
hospital = user.hospital
# ========================================
# AI NARRATIVE BRIEF
# ========================================
latest_report = ExecutiveReport.objects.filter(
status="completed"
).order_by("-created_at").first()
context["latest_report"] = latest_report
if latest_report:
context["ai_narrative"] = latest_report.narrative_en
context["ai_highlights"] = latest_report.highlights_en
context["ai_concerns"] = latest_report.concerns_en
context["report_period"] = f"{latest_report.start_date} to {latest_report.end_date}"
context["report_type"] = latest_report.get_report_type_display()
else:
context["ai_narrative"] = None
context["ai_highlights"] = []
context["ai_concerns"] = []
context["report_period"] = None
context["report_type"] = None
# ========================================
# DASHBOARD DATA (KPIs, trends, variances)
# ========================================
dashboard_data = summary_service.get_dashboard_data(
hospital=hospital,
date_range=date_range,
)
kpis = dashboard_data.get("kpis", {})
variances = dashboard_data.get("variances", {})
# KPI cards
context["kpi_cards"] = {
"complaints": {
"total": int(kpis.get("complaints_total", 0)),
"critical": int(kpis.get("complaints_critical", 0)),
"overdue": int(kpis.get("complaints_overdue", 0)),
"variance": variances.get("complaints_total", {}),
"resolution_time": kpis.get("complaints_resolution_time", 0),
},
"surveys": {
"total": int(kpis.get("surveys_total", 0)),
"satisfaction": float(kpis.get("surveys_satisfaction", 0)),
"nps": float(kpis.get("surveys_nps", 0)),
"variance": variances.get("surveys_satisfaction", {}),
},
"actions": {
"total": int(kpis.get("actions_total", 0)),
"open": int(kpis.get("actions_open", 0)),
"overdue": int(kpis.get("actions_overdue", 0)),
"closed": int(kpis.get("actions_closed", 0)),
"variance": variances.get("actions_total", {}),
},
"observations": {
"total": int(kpis.get("observations_total", 0)),
"critical": int(kpis.get("observations_critical", 0)),
"variance": variances.get("observations_total", {}),
},
}
# ========================================
# RISK ALERTS
# ========================================
risk_alerts = PredictiveInsight.objects.filter(
severity__in=["high", "critical"],
status__in=["new", "acknowledged"],
).select_related("hospital", "department").order_by("-severity", "-created_at")[:10]
context["risk_alerts"] = risk_alerts
context["has_risk_alerts"] = risk_alerts.exists()
# ========================================
# HOSPITAL LEADERBOARD
# ========================================
leaderboard = dashboard_data.get("hospital_leaderboard", [])
context["hospital_leaderboard"] = leaderboard[:10] # Top 10
# ========================================
# AI RECOMMENDATIONS
# ========================================
recommendations = AIRecommendation.objects.filter(
status__in=["new", "under_review"],
).select_related("hospital", "department").order_by("-priority", "-created_at")[:5]
context["ai_recommendations"] = recommendations
# ========================================
# CHART DATA
# ========================================
trends = dashboard_data.get("trends", {})
context["chart_data"] = {
"complaints_trend": json.dumps(trends.get("complaints_total", [])),
"surveys_satisfaction_trend": json.dumps(trends.get("surveys_satisfaction", [])),
"actions_trend": json.dumps(trends.get("actions_total", [])),
"observations_trend": json.dumps(trends.get("observations_total", [])),
}
# ========================================
# FILTERS
# ========================================
from apps.organizations.models import Hospital
if user.is_px_admin():
context["available_hospitals"] = Hospital.objects.filter(status="active")
elif user.is_hospital_admin() and user.hospital:
context["available_hospitals"] = [user.hospital]
else:
context["available_hospitals"] = Hospital.objects.filter(status="active")
context["selected_hospital"] = hospital
context["date_range"] = date_range
context["date_range_options"] = [
(7, _("Last 7 days")),
(14, _("Last 14 days")),
(30, _("Last 30 days")),
(60, _("Last 60 days")),
(90, _("Last 90 days")),
]
# ========================================
# METADATA
# ========================================
context["last_updated"] = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
context["is_px_admin"] = user.is_px_admin()
return context
@login_required
def executive_dashboard_api(request):
"""
API endpoint for executive dashboard HTMX updates.
Returns JSON with latest metrics, KPIs, and risk alerts.
"""
if not (request.user.is_executive() or request.user.is_px_admin()):
return JsonResponse({"error": _("Permission denied")}, status=403)
# Parse filters
date_range = int(request.GET.get("date_range", 30))
hospital_id = request.GET.get("hospital")
summary_service = ExecutiveSummaryService()
# Determine hospital scope
hospital = None
if hospital_id:
from apps.organizations.models import Hospital
try:
hospital = Hospital.objects.get(id=hospital_id)
except Hospital.DoesNotExist:
pass
# Get dashboard data
dashboard_data = summary_service.get_dashboard_data(
hospital=hospital,
date_range=date_range,
)
# Get risk alerts
risk_alerts = PredictiveInsight.objects.filter(
severity__in=["high", "critical"],
status__in=["new", "acknowledged"],
).order_by("-severity", "-created_at")[:5]
risk_alerts_data = []
for insight in risk_alerts:
risk_alerts_data.append({
"id": str(insight.id),
"title": insight.title_en,
"severity": insight.severity,
"insight_type": insight.insight_type,
"hospital": insight.hospital.name if insight.hospital else None,
"created_at": insight.created_at.isoformat(),
})
# Get latest report narrative
latest_report = ExecutiveReport.objects.filter(
status="completed"
).order_by("-created_at").first()
response_data = {
"kpis": dashboard_data.get("kpis", {}),
"variances": dashboard_data.get("variances", {}),
"risk_alerts": risk_alerts_data,
"hospital_leaderboard": dashboard_data.get("hospital_leaderboard", [])[:5],
"ai_narrative": latest_report.narrative_en if latest_report else None,
"last_updated": timezone.now().isoformat(),
}
return JsonResponse(response_data)
# =============================================================================
# Predictive Insights Views
# =============================================================================
class PredictiveInsightsView(ExecutiveAccessMixin, LoginRequiredMixin, TemplateView):
"""
Predictive Insights - View and manage AI-detected patterns and risks.
Shows all predictive insights with filtering by:
- Severity (low, medium, high, critical)
- Status (new, acknowledged, resolved, dismissed)
- Insight type
- Hospital
"""
template_name = "executive/insights.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = self.request.user
# Filters
severity_filter = self.request.GET.get("severity", "")
status_filter = self.request.GET.get("status", "")
insight_type_filter = self.request.GET.get("insight_type", "")
hospital_id = self.request.GET.get("hospital")
# Base queryset
insights_qs = PredictiveInsight.objects.select_related(
"hospital", "department", "acknowledged_by"
).order_by("-severity", "-created_at")
# Apply filters
if severity_filter:
insights_qs = insights_qs.filter(severity=severity_filter)
if status_filter:
insights_qs = insights_qs.filter(status=status_filter)
if insight_type_filter:
insights_qs = insights_qs.filter(insight_type=insight_type_filter)
if hospital_id:
from apps.organizations.models import Hospital
try:
hospital = Hospital.objects.get(id=hospital_id)
insights_qs = insights_qs.filter(hospital=hospital)
except Hospital.DoesNotExist:
pass
elif user.is_hospital_admin() and user.hospital:
insights_qs = insights_qs.filter(hospital=user.hospital)
# Filter counts for sidebar
context["total_insights"] = insights_qs.count()
context["new_insights"] = insights_qs.filter(status="new").count()
context["critical_insights"] = insights_qs.filter(severity="critical").count()
context["high_insights"] = insights_qs.filter(severity="high").count()
# Pagination
from django.core.paginator import Paginator
paginator = Paginator(insights_qs, 25)
page_number = self.request.GET.get("page")
page_obj = paginator.get_page(page_number)
context["insights"] = page_obj
context["page_obj"] = page_obj
# Filter options
context["severity_choices"] = PredictiveInsight.SEVERITY_LEVELS
context["status_choices"] = PredictiveInsight.STATUS_CHOICES
context["insight_type_choices"] = PredictiveInsight.INSIGHT_TYPES
# Hospitals for filter
if user.is_px_admin():
from apps.organizations.models import Hospital
context["available_hospitals"] = Hospital.objects.filter(
predictive_insights__isnull=False
).distinct()
else:
context["available_hospitals"] = []
# Current filter values
context["current_filters"] = {
"severity": severity_filter,
"status": status_filter,
"insight_type": insight_type_filter,
"hospital": hospital_id,
}
return context
@login_required
def acknowledge_insight(request, insight_id):
"""
Acknowledge a predictive insight.
Marks the insight as acknowledged by the current user.
"""
if not (request.user.is_executive() or request.user.is_px_admin()):
return JsonResponse({"error": _("Permission denied")}, status=403)
insight = get_object_or_404(PredictiveInsight, id=insight_id)
if request.method == "POST":
try:
insight.status = "acknowledged"
insight.acknowledged_by = request.user
insight.acknowledged_at = timezone.now()
insight.save(update_fields=["status", "acknowledged_by", "acknowledged_at"])
if request.headers.get("HX-Request"):
return HttpResponse(
f'<span class="text-green-600">{_("Acknowledged")}</span>'
)
return JsonResponse({
"status": "success",
"message": _("Insight acknowledged successfully"),
"insight_id": str(insight.id),
})
except Exception as e:
logger.error(f"Error acknowledging insight {insight_id}: {e}", exc_info=True)
if request.headers.get("HX-Request"):
return HttpResponse(
f'<span class="text-red-600">{_("Error acknowledging insight")}</span>',
status=500,
)
return JsonResponse({
"error": _("Error acknowledging insight"),
"details": str(e),
}, status=500)
return JsonResponse({"error": _("POST required")}, status=405)
# =============================================================================
# Executive Q&A Views
# =============================================================================
class ExecutiveQAView(ExecutiveAccessMixin, LoginRequiredMixin, TemplateView):
"""
Executive Q&A - Natural language Q&A interface for executives.
Allows executives to ask questions about PX data and receive
AI-powered answers with supporting data.
"""
template_name = "executive/qa.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["recent_questions"] = self._get_recent_questions()
context["suggested_questions"] = [
_("What is the overall satisfaction trend this month?"),
_("Which hospital has the highest complaint volume?"),
_("Are there any SLA breaches predicted this week?"),
_("What are the top 3 areas for improvement?"),
_("How does our NPS compare to last quarter?"),
]
return context
def _get_recent_questions(self):
"""Get recent questions from session for demo purposes."""
return self.request.session.get("executive_qa_history", [])
@login_required
def executive_qa_api(request):
"""
API endpoint for Executive Q&A.
Accepts a question via POST, returns an AI-generated answer with data.
"""
if request.method != "POST":
return JsonResponse({"error": _("POST required")}, status=405)
if not (request.user.is_executive() or request.user.is_px_admin()):
return JsonResponse({"error": _("Permission denied")}, status=403)
question = request.POST.get("question", "").strip()
if not question:
return JsonResponse({"error": _("Question is required")}, status=400)
try:
# Gather current data for context
summary_service = ExecutiveSummaryService()
now = timezone.now()
last_30d = now - timedelta(days=30)
# Collect key metrics
metrics = summary_service.aggregate_daily_metrics()
# Get some context data
from apps.complaints.models import Complaint
from apps.surveys.models import SurveyInstance
from apps.px_action_center.models import PXAction
total_complaints_30d = Complaint.objects.filter(created_at__gte=last_30d).count()
avg_satisfaction = SurveyInstance.objects.filter(
completed_at__gte=last_30d, total_score__isnull=False
).aggregate(avg=Avg("total_score"))["avg"] or 0
open_actions = PXAction.objects.filter(status__in=["open", "in_progress"]).count()
critical_insights = PredictiveInsight.objects.filter(
severity__in=["high", "critical"], status="new"
).count()
# Build AI prompt with data context
prompt = f"""You are an executive analyst for PX360, a healthcare patient experience platform.
Answer the following question based on the current system data.
## Current System Data:
- Total complaints (today): {metrics.get('complaints_total', 0)}
- Critical complaints: {metrics.get('complaints_critical', 0)}
- Overdue complaints: {metrics.get('complaints_overdue', 0)}
- Average resolution time: {metrics.get('complaints_resolution_time', 0):.1f} hours
- Total surveys (today): {metrics.get('surveys_total', 0)}
- Satisfaction rate: {metrics.get('surveys_satisfaction', 0):.1f}%
- NPS score: {metrics.get('surveys_nps', 0):.1f}
- Total actions (today): {metrics.get('actions_total', 0)}
- Open actions: {metrics.get('actions_open', 0)}
- Total observations (today): {metrics.get('observations_total', 0)}
- Critical observations: {metrics.get('observations_critical', 0)}
- Complaints (last 30 days): {total_complaints_30d}
- Average satisfaction (last 30 days): {float(avg_satisfaction):.1f}/5.0
- Open/in-progress actions: {open_actions}
- Critical/new insights: {critical_insights}
## Question:
{question}
Provide a concise, data-driven answer. Reference specific numbers when relevant.
If the question cannot be answered with the available data, say so and suggest what data would be needed.
Format your response as JSON:
{{
"answer": "Your answer here...",
"supporting_data": ["key data point 1", "key data point 2"],
"confidence": "high|medium|low"
}}
"""
# Call AI
from litellm import completion
response = completion(
model="openrouter/google/gemma-3-27b-it:free",
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=1000,
)
# Parse response
try:
result = json.loads(response.choices[0].message.content)
except json.JSONDecodeError:
result = {
"answer": response.choices[0].message.content,
"supporting_data": [],
"confidence": "medium",
}
# Store in session history
qa_history = request.session.get("executive_qa_history", [])
qa_history.insert(0, {
"question": question,
"answer": result.get("answer", ""),
"timestamp": timezone.now().isoformat(),
})
request.session["executive_qa_history"] = qa_history[:10] # Keep last 10
return JsonResponse({
"status": "success",
"question": question,
"answer": result.get("answer", ""),
"supporting_data": result.get("supporting_data", []),
"confidence": result.get("confidence", "medium"),
})
except Exception as e:
logger.error(f"Error in executive Q&A: {e}", exc_info=True)
return JsonResponse({
"status": "error",
"error": _("Unable to process question. Please try again."),
"details": str(e) if settings.DEBUG else None,
}, status=500)
# =============================================================================
# Executive Report Generation
# =============================================================================
@login_required
def generate_executive_report(request):
"""
Trigger AI report generation for executive summary.
Creates an ExecutiveReport object and generates both English and Arabic narratives.
Only accessible to px_admin and executive users.
"""
if not (request.user.is_executive() or request.user.is_px_admin()):
messages.error(request, _("You do not have permission to generate reports."))
return redirect("executive:executive_dashboard")
if request.method != "POST":
return redirect("executive:executive_dashboard")
# Get report parameters
report_type = request.POST.get("report_type", "weekly")
start_date_str = request.POST.get("start_date")
end_date_str = request.POST.get("end_date")
now = timezone.now()
# Calculate date range
if start_date_str and end_date_str:
try:
start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date()
end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date()
except ValueError:
messages.error(request, _("Invalid date format. Use YYYY-MM-DD."))
return redirect("executive:executive_dashboard")
else:
# Default based on report type
if report_type == "weekly":
end_date = now.date()
start_date = end_date - timedelta(days=7)
elif report_type == "monthly":
end_date = now.date()
start_date = end_date - timedelta(days=30)
elif report_type == "quarterly":
end_date = now.date()
start_date = end_date - timedelta(days=90)
else:
end_date = now.date()
start_date = end_date - timedelta(days=7)
try:
# Create report object
report = ExecutiveReport.objects.create(
report_type=report_type,
status="generating",
start_date=start_date,
end_date=end_date,
ai_model="openrouter/google/gemma-3-27b-it:free",
)
# Generate narratives using AI services
narrative_service = AINarrativeService()
# Generate English narrative
en_result = narrative_service.generate_weekly_narrative(
start_date=start_date,
end_date=end_date,
)
# Generate Arabic narrative
ar_result = narrative_service.generate_arabic_narrative(
start_date=start_date,
end_date=end_date,
)
# Update report with results
report.status = en_result.get("status", "completed")
report.narrative_en = en_result.get("narrative_en", "")
report.narrative_ar = ar_result.get("narrative_ar", "")
report.highlights_en = en_result.get("highlights_en", [])
report.highlights_ar = ar_result.get("highlights_ar", [])
report.concerns_en = en_result.get("concerns_en", [])
report.concerns_ar = ar_result.get("concerns_ar", [])
report.ai_model = en_result.get("ai_model", "")
re... [truncated]
```
## Assistant
Let me check what files exist and create the missing ones.
## Assistant
Good, the views.py is created. Now let me create the templates and admin. Let me create them directly:
### Tool: Shell: mkdir -p /home/ismail/projects/HH/apps/executive_summary/templates/executive (Create templates directory for executive app)
**Status**: completed
**Input:**
```json
{
"command": "mkdir -p /home/ismail/projects/HH/apps/executive_summary/templates/executive",
"description": "Create templates directory for executive app"
}
```
**Output:**
```
Command: mkdir -p /home/ismail/projects/HH/apps/executive_summary/templates/executive
Directory: (root)
Output: (empty)
Error: (none)
Exit Code: 0
Signal: 0
Process Group PGID: 291611
```
## Assistant
Now I need to create the dashboard template with all the UI components for the executive summary.
## Assistant
Now let me create the main dashboard template with all the AI-powered components:
### Tool: WriteFile: Writing to apps/executive_summary/templates/executive/dashboard.html
**Status**: completed
**Input:**
```json
{
"content": "{% extends \"layouts/base.html\" %}\n{% load i18n %}\n{% load static %}\n\n{% block title %}{% trans \"Executive Summary\" %} | PX360{% endblock %}\n\n{% block extra_css %}\n<style>\n .kpi-card {\n transition: all 0.3s ease;\n }\n .kpi-card:hover {\n transform: translateY(-4px);\n box-shadow: 0 10px 25px rgba(0,0,0,0.1);\n }\n .variance-up {\n color: #10b981;\n }\n .variance-down {\n color: #ef4444;\n }\n .variance-neutral {\n color: #6b7280;\n }\n .risk-alert {\n border-left: 4px solid #ef4444;\n }\n .risk-alert.high {\n border-left-color: #f97316;\n }\n .risk-alert.critical {\n border-left-color: #ef4444;\n }\n .ai-brief {\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n }\n .leaderboard-item {\n transition: all 0.2s ease;\n }\n .leaderboard-item:hover {\n background-color: rgba(99, 102, 241, 0.05);\n }\n .pulse-dot {\n animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n }\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: .5; }\n }\n</style>\n{% endblock %}\n\n{% block content %}\n<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8\">\n <!-- Page Header -->\n <div class=\"flex flex-col md:flex-row md:items-center md:justify-between mb-8\">\n <div>\n <h1 class=\"text-3xl font-bold text-gray-900\">{% trans \"Executive Summary\" %}</h1>\n <p class=\"mt-2 text-sm text-gray-600\">\n {% trans \"AI-powered insights and analytics\" %}\n {% if selected_hospital %}\n • {{ selected_hospital.name }}\n {% else %}\n • {% trans \"All Hospitals\" %}\n {% endif %}\n </p>\n </div>\n \n <div class=\"mt-4 md:mt-0 flex items-center gap-3\">\n <!-- Date Range Filter -->\n <form method=\"get\" class=\"flex items-center gap-2\">\n {% if selected_hospital %}\n <input type=\"hidden\" name=\"hospital\" value=\"{{ selected_hospital.id }}\">\n {% endif %}\n <select name=\"date_range\" onchange=\"this.form.submit()\" \n class=\"form-select text-sm border-gray-300 rounded-lg\">\n {% for days, label in date_range_options %}\n <option value=\"{{ days }}\" {% if date_range == days %}selected{% endif %}>\n {{ label }}\n </option>\n {% endfor %}\n </select>\n \n <!-- Hospital Filter (PX Admin only) -->\n {% if is_px_admin %}\n <select name=\"hospital\" onchange=\"this.form.submit()\" \n class=\"form-select text-sm border-gray-300 rounded-lg\">\n <option value=\"\">{% trans \"All Hospitals\" %}</option>\n {% for hospital in available_hospitals %}\n <option value=\"{{ hospital.id }}\" \n {% if selected_hospital and selected_hospital.id == hospital.id %}selected{% endif %}>\n {{ hospital.name }}\n </option>\n {% endfor %}\n </select>\n {% endif %}\n </form>\n \n <!-- Refresh Button -->\n <button onclick=\"location.reload()\" class=\"p-2 text-gray-600 hover:text-indigo-600 transition\">\n <i data-lucide=\"refresh-cw\" class=\"w-5 h-5\"></i>\n </button>\n </div>\n </div>\n \n <!-- Last Updated -->\n <div class=\"flex items-center gap-2 mb-6 text-sm text-gray-500\">\n <div class=\"w-2 h-2 bg-green-500 rounded-full pulse-dot\"></div>\n <span>{% trans \"Live data\" %}</span>\n <span>•</span>\n <span>{% trans \"Last updated\" %}: {{ last_updated }}</span>\n </div>\n\n <!-- AI Executive Brief -->\n {% if ai_narrative %}\n <div class=\"ai-brief rounded-xl p-6 mb-8 shadow-lg\">\n <div class=\"flex items-start gap-3 mb-4\">\n <i data-lucide=\"sparkles\" class=\"w-6 h-6 flex-shrink-0\"></i>\n <div>\n <h2 class=\"text-xl font-bold\">{% trans \"AI Executive Brief\" %}</h2>\n <p class=\"text-sm text-white/80\">\n {% trans \"Generated by AI\" %} • {{ report_type }} • {{ report_period }}\n </p>\n </div>\n </div>\n \n <div class=\"bg-white/10 backdrop-blur rounded-lg p-4 mb-4\">\n <p class=\"text-base leading-relaxed whitespace-pre-line\">{{ ai_narrative }}</p>\n </div>\n \n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n {% if ai_highlights %}\n <div>\n <h3 class=\"text-sm font-semibold mb-2 flex items-center gap-2\">\n <i data-lucide=\"trending-up\" class=\"w-4 h-4\"></i>\n {% trans \"Key Highlights\" %}\n </h3>\n <ul class=\"space-y-1 text-sm\">\n {% for highlight in ai_highlights|slice:\":5\" %}\n <li class=\"flex items-start gap-2\">\n <i data-lucide=\"check-circle\" class=\"w-4 h-4 flex-shrink-0 mt-0.5\"></i>\n <span>{{ highlight }}</span>\n </li>\n {% endfor %}\n </ul>\n </div>\n {% endif %}\n \n {% if ai_concerns %}\n <div>\n <h3 class=\"text-sm font-semibold mb-2 flex items-center gap-2\">\n <i data-lucide=\"alert-triangle\" class=\"w-4 h-4\"></i>\n {% trans \"Key Concerns\" %}\n </h3>\n <ul class=\"space-y-1 text-sm\">\n {% for concern in ai_concerns|slice:\":5\" %}\n <li class=\"flex items-start gap-2\">\n <i data-lucide=\"alert-circle\" class=\"w-4 h-4 flex-shrink-0 mt-0.5\"></i>\n <span>{{ concern }}</span>\n </li>\n {% endfor %}\n </ul>\n </div>\n {% endif %}\n </div>\n </div>\n {% else %}\n <div class=\"bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl p-6 mb-8 text-center shadow-lg\">\n <i data-lucide=\"sparkles\" class=\"w-12 h-12 mx-auto mb-4 text-white/80\"></i>\n <h2 class=\"text-xl font-bold text-white mb-2\">{% trans \"No AI Report Available\" %}</h2>\n <p class=\"text-white/90 mb-4\">\n {% trans \"Generate your first executive summary to get AI-powered insights\" %}\n </p>\n <form method=\"post\" action=\"{% url 'executive:generate_report' %}\" class=\"inline\">\n {% csrf_token %}\n <input type=\"hidden\" name=\"report_type\" value=\"weekly\">\n <button type=\"submit\" class=\"bg-white text-indigo-600 px-6 py-2 rounded-lg font-semibold hover:bg-gray-100 transition\">\n {% trans \"Generate Report\" %}\n </button>\n </form>\n </div>\n {% endif %}\n\n <!-- Risk Alerts -->\n {% if has_risk_alerts %}\n <div class=\"mb-8\">\n <div class=\"flex items-center justify-between mb-4\">\n <h2 class=\"text-xl font-bold text-gray-900 flex items-center gap-2\">\n <i data-lucide=\"alert-triangle\" class=\"w-5 h-5 text-red-500\"></i>\n {% trans \"Risk Alerts\" %}\n <span class=\"ml-2 bg-red-100 text-red-800 text-xs font-semibold px-2.5 py-0.5 rounded-full\">\n {{ risk_alerts|length }}\n </span>\n </h2>\n <a href=\"{% url 'executive:predictive_insights' %}\" class=\"text-sm text-indigo-600 hover:text-indigo-700\">\n {% trans \"View All Insights\" %} →\n </a>\n </div>\n \n <div class=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n {% for insight in risk_alerts %}\n <div class=\"risk-alert {{ insight.severity }} bg-white rounded-lg p-4 shadow-sm\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"flex-1\">\n <div class=\"flex items-center gap-2 mb-2\">\n <span class=\"px-2 py-0.5 text-xs font-semibold rounded-full\n {% if insight.severity == 'critical' %}bg-red-100 text-red-800\n {% elif insight.severity == 'high' %}bg-orange-100 text-orange-800\n {% else %}bg-yellow-100 text-yellow-800{% endif %}\">\n {{ insight.get_severity_display }}\n </span>\n <span class=\"text-xs text-gray-500\">{{ insight.get_insight_type_display }}</span>\n </div>\n <h3 class=\"font-semibold text-gray-900 mb-1\">{{ insight.title_en }}</h3>\n <p class=\"text-sm text-gray-600 line-clamp-2\">{{ insight.description_en }}</p>\n {% if insight.hospital %}\n <p class=\"text-xs text-gray-500 mt-2\">\n <i data-lucide=\"building\" class=\"w-3 h-3 inline\"></i>\n {{ insight.hospital.name }}\n </p>\n {% endif %}\n </div>\n <form method=\"post\" action=\"{% url 'executive:acknowledge_insight' insight.id %}\" \n class=\"flex-shrink-0\">\n {% csrf_token %}\n <button type=\"submit\" \n class=\"px-3 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded transition\">\n {% trans \"Acknowledge\" %}\n </button>\n </form>\n </div>\n </div>\n {% endfor %}\n </div>\n </div>\n {% endif %}\n\n <!-- KPI Cards -->\n <div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8\">\n <!-- Complaints -->\n <div class=\"kpi-card bg-white rounded-xl p-6 shadow-sm border border-gray-200\">\n <div class=\"flex items-center justify-between mb-4\">\n <div class=\"p-2 bg-red-100 rounded-lg\">\n <i data-lucide=\"message-square\" class=\"w-6 h-6 text-red-600\"></i>\n </div>\n {% include \"executive/partials/variance_badge.html\" with variance=kpi_cards.complaints.variance %}\n </div>\n <h3 class=\"text-2xl font-bold text-gray-900\">{{ kpi_cards.complaints.total }}</h3>\n <p class=\"text-sm text-gray-600\">{% trans \"Total Complaints\" %}</p>\n <div class=\"mt-3 pt-3 border-t border-gray-100 grid grid-cols-2 gap-2 text-xs\">\n <div>\n <span class=\"text-gray-500\">{% trans \"Critical\" %}:</span>\n <span class=\"font-semibold text-red-600 ml-1\">{{ kpi_cards.complaints.critical }}</span>\n </div>\n <div>\n <span class=\"text-gray-500\">{% trans \"Overdue\" %}:</span>\n <span class=\"font-semibold text-orange-600 ml-1\">{{ kpi_cards.complaints.overdue }}</span>\n </div>\n <div class=\"col-span-2\">\n <span class=\"text-gray-500\">{% trans \"Avg Resolution\" %}:</span>\n <span class=\"font-semibold ml-1\">{{ kpi_cards.complaints.resolution_time }}h</span>\n </div>\n </div>\n </div>\n\n <!-- Surveys -->\n <div class=\"kpi-card bg-white rounded-xl p-6 shadow-sm border border-gray-200\">\n <div class=\"flex items-center justify-between mb-4\">\n <div class=\"p-2 bg-green-100 rounded-lg\">\n <i data-lucide=\"clipboard-check\" class=\"w-6 h-6 text-green-600\"></i>\n </div>\n {% include \"executive/partials/variance_badge.html\" with variance=kpi_cards.surveys.variance %}\n </div>\n <h3 class=\"text-2xl font-bold text-gray-900\">{{ kpi_cards.surveys.satisfaction }}%</h3>\n <p class=\"text-sm text-gray-600\">{% trans \"Satisfaction Rate\" %}</p>\n <div class=\"mt-3 pt-3 border-t border-gray-100 grid grid-cols-2 gap-2 text-xs\">\n <div>\n <span class=\"text-gray-500\">{% trans \"NPS\" %}:</span>\n <span class=\"font-semibold text-green-600 ml-1\">{{ kpi_cards.surveys.nps }}</span>\n </div>\n <div>\n <span class=\"text-gray-500\">{% trans \"Total\" %}:</span>\n <span class=\"font-semibold ml-1\">{{ kpi_cards.surveys.total }}</span>\n </div>\n </div>\n </div>\n\n <!-- Actions -->\n <div class=\"kpi-card bg-white rounded-xl p-6 shadow-sm border border-gray-200\">\n <div class=\"flex items-center justify-between mb-4\">\n <div class=\"p-2 bg-blue-100 rounded-lg\">\n <i data-lucide=\"activity\" class=\"w-6 h-6 text-blue-600\"></i>\n </div>\n {% include \"executive/partials/variance_badge.html\" with variance=kpi_cards.actions.variance %}\n </div>\n <h3 class=\"text-2xl font-bold text-gray-900\">{{ kpi_cards.actions.total }}</h3>\n <p class=\"text-sm text-gray-600\">{% trans \"Total Actions\" %}</p>\n <div class=\"mt-3 pt-3 border-t border-gray-100 grid grid-cols-3 gap-2 text-xs\">\n <div>\n <span class=\"text-gray-500\">{% trans \"Open\" %}:</span>\n <span class=\"font-semibold text-blue-600 ml-1\">{{ kpi_cards.actions.open }}</span>\n </div>\n <div>\n <span class=\"text-gray-500\">{% trans \"Closed\" %}:</span>\n <span class=\"font-semibold text-green-600 ml-1\">{{ kpi_cards.actions.closed }}</span>\n </div>\n <div>\n <span class=\"text-gray-500\">{% trans \"Overdue\" %}:</span>\n <span class=\"font-semibold text-red-600 ml-1\">{{ kpi_cards.actions.overdue }}</span>\n </div>\n </div>\n </div>\n\n <!-- Observations -->\n <div class=\"kpi-card bg-white rounded-xl p-6 shadow-sm border border-gray-200\">\n <div class=\"flex items-center justify-between mb-4\">\n <div class=\"p-2 bg-yellow-100 rounded-lg\">\n <i data-lucide=\"eye\" class=\"w-6 h-6 text-yellow-600\"></i>\n </div>\n {% include \"executive/partials/variance_badge.html\" with variance=kpi_cards.observations.variance %}\n </div>\n <h3 class=\"text-2xl font-bold text-gray-900\">{{ kpi_cards.observations.total }}</h3>\n <p class=\"text-sm text-gray-600\">{% trans \"Observations\" %}</p>\n <div class=\"mt-3 pt-3 border-t border-gray-100 grid grid-cols-2 gap-2 text-xs\">\n <div>\n <span class=\"text-gray-500\">{% trans \"Critical\" %}:</span>\n <span class=\"font-semibold text-red-600 ml-1\">{{ kpi_cards.observations.critical }}</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Charts & Leaderboard -->\n <div class=\"grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8\">\n <!-- Complaints Trend Chart -->\n <div class=\"lg:col-span-2 bg-white rounded-xl p-6 shadow-sm border border-gray-200\">\n <h3 class=\"text-lg font-bold text-gray-900 mb-4\">{% trans \"Complaints Trend\" %}</h3>\n <div id=\"complaintsChart\"></div>\n </div>\n\n <!-- Hospital Leaderboard -->\n <div class=\"bg-white rounded-xl p-6 shadow-sm border border-gray-200\">\n <h3 class=\"text-lg font-bold text-gray-900 mb-4 flex items-center gap-2\">\n <i data-lucide=\"trophy\" class=\"w-5 h-5 text-yellow-500\"></i>\n {% trans \"Hospital Leaderboard\" %}\n </h3>\n <div class=\"space-y-3\">\n {% for hospital in hospital_leaderboard|slice:\":5\" %}\n <div class=\"leaderboard-item flex items-center justify-between p-3 rounded-lg {% if forloop.counter == 1 %}bg-yellow-50 border border-yellow-200{% elif forloop.counter == 2 %}bg-gray-50 border border-gray-200{% elif forloop.counter == 3 %}bg-orange-50 border border-orange-200{% else %}bg-gray-50{% endif %}\">\n <div class=\"flex items-center gap-3\">\n <span class=\"w-8 h-8 flex items-center justify-center rounded-full text-sm font-bold\n {% if forloop.counter == 1 %}bg-yellow-500 text-white\n {% elif forloop.counter == 2 %}bg-gray-400 text-white\n {% elif forloop.counter == 3 %}bg-orange-400 text-white\n {% else %}bg-gray-300 text-gray-700{% endif %}\">\n {{ forloop.counter }}\n </span>\n <div>\n <p class=\"font-semibold text-gray-900 text-sm\">{{ hospital.name }}</p>\n <p class=\"text-xs text-gray-500\">{% trans \"Score\" %}: {{ hospital.composite_score|floatformat:1 }}</p>\n </div>\n </div>\n <div class=\"text-right\">\n <p class=\"text-sm font-bold text-green-600\">{{ hospital.satisfaction_rate|floatformat:0 }}%</p>\n <p class=\"text-xs text-gray-500\">{% trans \"satisfaction\" %}</p>\n </div>\n </div>\n {% empty %}\n <p class=\"text-sm text-gray-500 text-center py-4\">{% trans \"No hospital data available\" %}</p>\n {% endfor %}\n </div>\n </div>\n </div>\n\n <!-- Satisfaction Trend & AI Recommendations -->\n <div class=\"grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8\">\n <!-- Satisfaction Trend -->\n <div class=\"lg:col-span-2 bg-white rounded-xl p-6 shadow-sm border border-gray-200\">\n <h3 class=\"text-lg font-bold text-gray-900 mb-4\">{% trans \"Satisfaction Trend\" %}</h3>\n <div id=\"satisfactionChart\"></div>\n </div>\n\n <!-- AI Recommendations -->\n <div class=\"bg-white rounded-xl p-6 shadow-sm border border-gray-200\">\n <h3 class=\"text-lg font-bold text-gray-900 mb-4 flex items-center gap-2\">\n <i data-lucide=\"lightbulb\" class=\"w-5 h-5 text-amber-500\"></i>\n {% trans \"AI Recommendations\" %}\n </h3>\n <div class=\"space-y-3\">\n {% for rec in ai_recommendations %}\n <div class=\"p-3 rounded-lg border-l-4 {% if rec.priority == 'urgent' %}border-red-500 bg-red-50{% elif rec.priority == 'high' %}border-orange-500 bg-orange-50{% elif rec.priority == 'medium' %}border-yellow-500 bg-yellow-50{% else %}border-blue-500 bg-blue-50{% endif %}\">\n <h4 class=\"font-semibold text-gray-900 text-sm mb-1\">{{ rec.title_en }}</h4>\n <p class=\"text-xs text-gray-600 line-clamp-2\">{{ rec.description_en }}</p>\n {% if rec.hospital %}\n <p class=\"text-xs text-gray-500 mt-2\">\n <i data-lucide=\"building\" class=\"w-3 h-3 inline\"></i>\n {{ rec.hospital.name }}\n </p>\n {% endif %}\n </div>\n {% empty %}\n <p class=\"text-sm text-gray-500 text-center py-4\">{% trans \"No recommendations available\" %}</p>\n {% endfor %}\n </div>\n <a href=\"#\" class=\"mt-3 text-sm text-indigo-600 hover:text-indigo-700 inline-block\">\n {% trans \"View All Recommendations\" %} →\n </a>\n </div>\n </div>\n\n <!-- Quick Actions -->\n <div class=\"bg-white rounded-xl p-6 shadow-sm border border-gray-200\">\n <h3 class=\"text-lg font-bold text-gray-900 mb-4\">{% trans \"Quick Actions\" %}</h3>\n <div class=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n <a href=\"{% url 'executive:predictive_insights' %}\" \n class=\"flex items-center gap-3 p-4 rounded-lg border-2 border-gray-200 hover:border-indigo-300 hover:bg-indigo-50 transition\">\n <i data-lucide=\"trending-up\" class=\"w-6 h-6 text-indigo-600\"></i>\n <div>\n <p class=\"font-semibold text-gray-900\">{% trans \"Predictive Insights\" %}</p>\n <p class=\"text-xs text-gray-600\">{% trans \"View AI-detected patterns and risks\" %}</p>\n </div>\n </a>\n \n <a href=\"{% url 'executive:executive_qa' %}\" \n class=\"flex items-center gap-3 p-4 rounded-lg border-2 border-gray-200 hover:border-purple-300 hover:bg-purple-50 transition\">\n <i data-lucide=\"message-circle\" class=\"w-6 h-6 text-purple-600\"></i>\n <div>\n <p class=\"font-semibold text-gray-900\">{% trans \"Ask AI\" %}</p>\n <p class=\"text-xs text-gray-600\">{% trans \"Ask questions about your data\" %}</p>\n </div>\n </a>\n \n <form method=\"post\" action=\"{% url 'executive:generate_report' %}\" class=\"flex items-center gap-3 p-4 rounded-lg border-2 border-gray-200 hover:border-green-300 hover:bg-green-50 transition cursor-pointer\">\n {% csrf_token %}\n <input type=\"hidden\" name=\"report_type\" value=\"weekly\">\n <i data-lucide=\"file-text\" class=\"w-6 h-6 text-green-600\"></i>\n <div>\n <p class=\"font-semibold text-gray-900\">{% trans \"Generate Report\" %}</p>\n <p class=\"text-xs text-gray-600\">{% trans \"Create AI executive summary\" %}</p>\n </div>\n </form>\n </div>\n </div>\n</div>\n{% endblock %}\n\n{% block extra_js %}\n<script src=\"https://cdn.jsdelivr.net/npm/apexcharts\"></script>\n<script>\ndocument.addEventListener('DOMContentLoaded', function() {\n // Complaints Trend Chart\n const complaintsData = JSON.parse('{{ chart_data.complaints_trend|escapejs }}');\n if (complaintsData && complaintsData.length > 0) {\n const complaintsOptions = {\n series: [{\n name: '{% trans \"Complaints\" %}',\n data: complaintsData.map(d => d.value)\n }],\n chart: {\n type: 'area',\n height: 300,\n toolbar: { show: false }\n },\n colors: ['#ef4444'],\n stroke: { curve: 'smooth', width: 2 },\n fill: {\n type: 'gradient',\n gradient: {\n shadeIntensity: 1,\n opacityFrom: 0.4,\n opacityTo: 0.1,\n }\n },\n xaxis: {\n categories: complaintsData.map(d => d.date),\n labels: { rotate: -45 }\n },\n yaxis: {\n labels: { formatter: function(val) { return Math.round(val); } }\n },\n tooltip: {\n x: { format: 'dd MMM yyyy' }\n }\n };\n const complaintsChart = new ApexCharts(document.querySelector(\"#complaintsChart\"), complaintsOptions);\n complaintsChart.render();\n }\n\n // Satisfaction Trend Chart\n const satisfactionData = JSON.parse('{{ chart_data.surveys_satisfaction_trend|escapejs }}');\n if (satisfactionData && satisfactionData.length > 0) {\n const satisfactionOptions = {\n series: [{\n name: '{% trans \"Satisfaction %\" %}',\n data: satisfactionData.map(d => d.value)\n }],\n chart: {\n type: 'line',\n height: 300,\n toolbar: { show: false }\n },\n colors: ['#10b981'],\n stroke: { curve: 'smooth', width: 2 },\n xaxis: {\n categories: satisfactionData.map(d => d.date),\n labels: { rotate: -45 }\n },\n yaxis: {\n min: 0,\n max: 100,\n labels: { formatter: function(val) { return val.toFixed(0) + '%'; } }\n },\n tooltip: {\n x: { format: 'dd MMM yyyy' }\n }\n };\n const satisfactionChart = new ApexCharts(document.querySelector(\"#satisfactionChart\"), satisfactionOptions);\n satisfactionChart.render();\n }\n\n // Initialize Lucide icons\n lucide.createIcons();\n});\n</script>\n{% endblock %}\n",
"file_path": "/home/ismail/projects/HH/apps/executive_summary/templates/executive/dashboard.html"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/templates/executive/dashboard.html`
**Output:**
*Diff for `dashboard.html`:*
```diff
{% extends "layouts/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Executive Summary" %} | PX360{% endblock %}
{% block extra_css %}
<style>
.kpi-card {
transition: all 0.3s ease;
}
.kpi-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.variance-up {
color: #10b981;
}
.variance-down {
color: #ef4444;
}
.variance-neutral {
color: #6b7280;
}
.risk-alert {
border-left: 4px solid #ef4444;
}
.risk-alert.high {
border-left-color: #f97316;
}
.risk-alert.critical {
border-left-color: #ef4444;
}
.ai-brief {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.leaderboard-item {
transition: all 0.2s ease;
}
.leaderboard-item:hover {
background-color: rgba(99, 102, 241, 0.05);
}
.pulse-dot {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .5; }
}
</style>
{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Page Header -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8">
<div>
<h1 class="text-3xl font-bold text-gray-900">{% trans "Executive Summary" %}</h1>
<p class="mt-2 text-sm text-gray-600">
{% trans "AI-powered insights and analytics" %}
{% if selected_hospital %}
• {{ selected_hospital.name }}
{% else %}
• {% trans "All Hospitals" %}
{% endif %}
</p>
</div>
<div class="mt-4 md:mt-0 flex items-center gap-3">
<!-- Date Range Filter -->
<form method="get" class="flex items-center gap-2">
{% if selected_hospital %}
<input type="hidden" name="hospital" value="{{ selected_hospital.id }}">
{% endif %}
<select name="date_range" onchange="this.form.submit()"
class="form-select text-sm border-gray-300 rounded-lg">
{% for days, label in date_range_options %}
<option value="{{ days }}" {% if date_range == days %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
<!-- Hospital Filter (PX Admin only) -->
{% if is_px_admin %}
<select name="hospital" onchange="this.form.submit()"
class="form-select text-sm border-gray-300 rounded-lg">
<option value="">{% trans "All Hospitals" %}</option>
{% for hospital in available_hospitals %}
<option value="{{ hospital.id }}"
{% if selected_hospital and selected_hospital.id == hospital.id %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
{% endif %}
</form>
<!-- Refresh Button -->
<button onclick="location.reload()" class="p-2 text-gray-600 hover:text-indigo-600 transition">
<i data-lucide="refresh-cw" class="w-5 h-5"></i>
</button>
</div>
</div>
<!-- Last Updated -->
<div class="flex items-center gap-2 mb-6 text-sm text-gray-500">
<div class="w-2 h-2 bg-green-500 rounded-full pulse-dot"></div>
<span>{% trans "Live data" %}</span>
<span>•</span>
<span>{% trans "Last updated" %}: {{ last_updated }}</span>
</div>
<!-- AI Executive Brief -->
{% if ai_narrative %}
<div class="ai-brief rounded-xl p-6 mb-8 shadow-lg">
<div class="flex items-start gap-3 mb-4">
<i data-lucide="sparkles" class="w-6 h-6 flex-shrink-0"></i>
<div>
<h2 class="text-xl font-bold">{% trans "AI Executive Brief" %}</h2>
<p class="text-sm text-white/80">
{% trans "Generated by AI" %} • {{ report_type }} • {{ report_period }}
</p>
</div>
</div>
<div class="bg-white/10 backdrop-blur rounded-lg p-4 mb-4">
<p class="text-base leading-relaxed whitespace-pre-line">{{ ai_narrative }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% if ai_highlights %}
<div>
<h3 class="text-sm font-semibold mb-2 flex items-center gap-2">
<i data-lucide="trending-up" class="w-4 h-4"></i>
{% trans "Key Highlights" %}
</h3>
<ul class="space-y-1 text-sm">
{% for highlight in ai_highlights|slice:":5" %}
<li class="flex items-start gap-2">
<i data-lucide="check-circle" class="w-4 h-4 flex-shrink-0 mt-0.5"></i>
<span>{{ highlight }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if ai_concerns %}
<div>
<h3 class="text-sm font-semibold mb-2 flex items-center gap-2">
<i data-lucide="alert-triangle" class="w-4 h-4"></i>
{% trans "Key Concerns" %}
</h3>
<ul class="space-y-1 text-sm">
{% for concern in ai_concerns|slice:":5" %}
<li class="flex items-start gap-2">
<i data-lucide="alert-circle" class="w-4 h-4 flex-shrink-0 mt-0.5"></i>
<span>{{ concern }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% else %}
<div class="bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl p-6 mb-8 text-center shadow-lg">
<i data-lucide="sparkles" class="w-12 h-12 mx-auto mb-4 text-white/80"></i>
<h2 class="text-xl font-bold text-white mb-2">{% trans "No AI Report Available" %}</h2>
<p class="text-white/90 mb-4">
{% trans "Generate your first executive summary to get AI-powered insights" %}
</p>
<form method="post" action="{% url 'executive:generate_report' %}" class="inline">
{% csrf_token %}
<input type="hidden" name="report_type" value="weekly">
<button type="submit" class="bg-white text-indigo-600 px-6 py-2 rounded-lg font-semibold hover:bg-gray-100 transition">
{% trans "Generate Report" %}
</button>
</form>
</div>
{% endif %}
<!-- Risk Alerts -->
{% if has_risk_alerts %}
<div class="mb-8">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-gray-900 flex items-center gap-2">
<i data-lucide="alert-triangle" class="w-5 h-5 text-red-500"></i>
{% trans "Risk Alerts" %}
<span class="ml-2 bg-red-100 text-red-800 text-xs font-semibold px-2.5 py-0.5 rounded-full">
{{ risk_alerts|length }}
</span>
</h2>
<a href="{% url 'executive:predictive_insights' %}" class="text-sm text-indigo-600 hover:text-indigo-700">
{% trans "View All Insights" %} →
</a>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
{% for insight in risk_alerts %}
<div class="risk-alert {{ insight.severity }} bg-white rounded-lg p-4 shadow-sm">
<div class="flex items-start justify-between gap-3">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-0.5 text-xs font-semibold rounded-full
{% if insight.severity == 'critical' %}bg-red-100 text-red-800
{% elif insight.severity == 'high' %}bg-orange-100 text-orange-800
{% else %}bg-yellow-100 text-yellow-800{% endif %}">
{{ insight.get_severity_display }}
</span>
<span class="text-xs text-gray-500">{{ insight.get_insight_type_display }}</span>
</div>
<h3 class="font-semibold text-gray-900 mb-1">{{ insight.title_en }}</h3>
<p class="text-sm text-gray-600 line-clamp-2">{{ insight.description_en }}</p>
{% if insight.hospital %}
<p class="text-xs text-gray-500 mt-2">
<i data-lucide="building" class="w-3 h-3 inline"></i>
{{ insight.hospital.name }}
</p>
{% endif %}
</div>
<form method="post" action="{% url 'executive:acknowledge_insight' insight.id %}"
class="flex-shrink-0">
{% csrf_token %}
<button type="submit"
class="px-3 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded transition">
{% trans "Acknowledge" %}
</button>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- KPI Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Complaints -->
<div class="kpi-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<div class="p-2 bg-red-100 rounded-lg">
<i data-lucide="message-square" class="w-6 h-6 text-red-600"></i>
</div>
{% include "executive/partials/variance_badge.html" with variance=kpi_cards.complaints.variance %}
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ kpi_cards.complaints.total }}</h3>
<p class="text-sm text-gray-600">{% trans "Total Complaints" %}</p>
<div class="mt-3 pt-3 border-t border-gray-100 grid grid-cols-2 gap-2 text-xs">
<div>
<span class="text-gray-500">{% trans "Critical" %}:</span>
<span class="font-semibold text-red-600 ml-1">{{ kpi_cards.complaints.critical }}</span>
</div>
<div>
<span class="text-gray-500">{% trans "Overdue" %}:</span>
<span class="font-semibold text-orange-600 ml-1">{{ kpi_cards.complaints.overdue }}</span>
</div>
<div class="col-span-2">
<span class="text-gray-500">{% trans "Avg Resolution" %}:</span>
<span class="font-semibold ml-1">{{ kpi_cards.complaints.resolution_time }}h</span>
</div>
</div>
</div>
<!-- Surveys -->
<div class="kpi-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<div class="p-2 bg-green-100 rounded-lg">
<i data-lucide="clipboard-check" class="w-6 h-6 text-green-600"></i>
</div>
{% include "executive/partials/variance_badge.html" with variance=kpi_cards.surveys.variance %}
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ kpi_cards.surveys.satisfaction }}%</h3>
<p class="text-sm text-gray-600">{% trans "Satisfaction Rate" %}</p>
<div class="mt-3 pt-3 border-t border-gray-100 grid grid-cols-2 gap-2 text-xs">
<div>
<span class="text-gray-500">{% trans "NPS" %}:</span>
<span class="font-semibold text-green-600 ml-1">{{ kpi_cards.surveys.nps }}</span>
</div>
<div>
<span class="text-gray-500">{% trans "Total" %}:</span>
<span class="font-semibold ml-1">{{ kpi_cards.surveys.total }}</span>
</div>
</div>
</div>
<!-- Actions -->
<div class="kpi-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<div class="p-2 bg-blue-100 rounded-lg">
<i data-lucide="activity" class="w-6 h-6 text-blue-600"></i>
</div>
{% include "executive/partials/variance_badge.html" with variance=kpi_cards.actions.variance %}
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ kpi_cards.actions.total }}</h3>
<p class="text-sm text-gray-600">{% trans "Total Actions" %}</p>
<div class="mt-3 pt-3 border-t border-gray-100 grid grid-cols-3 gap-2 text-xs">
<div>
<span class="text-gray-500">{% trans "Open" %}:</span>
<span class="font-semibold text-blue-600 ml-1">{{ kpi_cards.actions.open }}</span>
</div>
<div>
<span class="text-gray-500">{% trans "Closed" %}:</span>
<span class="font-semibold text-green-600 ml-1">{{ kpi_cards.actions.closed }}</span>
</div>
<div>
<span class="text-gray-500">{% trans "Overdue" %}:</span>
<span class="font-semibold text-red-600 ml-1">{{ kpi_cards.actions.overdue }}</span>
</div>
</div>
</div>
<!-- Observations -->
<div class="kpi-card bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-4">
<div class="p-2 bg-yellow-100 rounded-lg">
<i data-lucide="eye" class="w-6 h-6 text-yellow-600"></i>
</div>
{% include "executive/partials/variance_badge.html" with variance=kpi_cards.observations.variance %}
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ kpi_cards.observations.total }}</h3>
<p class="text-sm text-gray-600">{% trans "Observations" %}</p>
<div class="mt-3 pt-3 border-t border-gray-100 grid grid-cols-2 gap-2 text-xs">
<div>
<span class="text-gray-500">{% trans "Critical" %}:</span>
<span class="font-semibold text-red-600 ml-1">{{ kpi_cards.observations.critical }}</span>
</div>
</div>
</div>
</div>
<!-- Charts & Leaderboard -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- Complaints Trend Chart -->
<div class="lg:col-span-2 bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-bold text-gray-900 mb-4">{% trans "Complaints Trend" %}</h3>
<div id="complaintsChart"></div>
</div>
<!-- Hospital Leaderboard -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
<i data-lucide="trophy" class="w-5 h-5 text-yellow-500"></i>
{% trans "Hospital Leaderboard" %}
</h3>
<div class="space-y-3">
{% for hospital in hospital_leaderboard|slice:":5" %}
<div class="leaderboard-item flex items-center justify-between p-3 rounded-lg {% if forloop.counter == 1 %}bg-yellow-50 border border-yellow-200{% elif forloop.counter == 2 %}bg-gray-50 border border-gray-200{% elif forloop.counter == 3 %}bg-orange-50 border border-orange-200{% else %}bg-gray-50{% endif %}">
<div class="flex items-center gap-3">
<span class="w-8 h-8 flex items-center justify-center rounded-full text-sm font-bold
{% if forloop.counter == 1 %}bg-yellow-500 text-white
{% elif forloop.counter == 2 %}bg-gray-400 text-white
{% elif forloop.counter == 3 %}bg-orange-400 text-white
{% else %}bg-gray-300 text-gray-700{% endif %}">
{{ forloop.counter }}
</span>
<div>
<p class="font-semibold text-gray-900 text-sm">{{ hospital.name }}</p>
<p class="text-xs text-gray-500">{% trans "Score" %}: {{ hospital.composite_score|floatformat:1 }}</p>
</div>
</div>
<div class="text-right">
<p class="text-sm font-bold text-green-600">{{ hospital.satisfaction_rate|floatformat:0 }}%</p>
<p class="text-xs text-gray-500">{% trans "satisfaction" %}</p>
</div>
</div>
{% empty %}
<p class="text-sm text-gray-500 text-center py-4">{% trans "No hospital data available" %}</p>
{% endfor %}
</div>
</div>
</div>
<!-- Satisfaction Trend & AI Recommendations -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- Satisfaction Trend -->
<div class="lg:col-span-2 bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-bold text-gray-900 mb-4">{% trans "Satisfaction Trend" %}</h3>
<div id="satisfactionChart"></div>
</div>
<!-- AI Recommendations -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
<i data-lucide="lightbulb" class="w-5 h-5 text-amber-500"></i>
{% trans "AI Recommendations" %}
</h3>
<div class="space-y-3">
{% for rec in ai_recommendations %}
<div class="p-3 rounded-lg border-l-4 {% if rec.priority == 'urgent' %}border-red-500 bg-red-50{% elif rec.priority == 'high' %}border-orange-500 bg-orange-50{% elif rec.priority == 'medium' %}border-yellow-500 bg-yellow-50{% else %}border-blue-500 bg-blue-50{% endif %}">
<h4 class="font-semibold text-gray-900 text-sm mb-1">{{ rec.title_en }}</h4>
<p class="text-xs text-gray-600 line-clamp-2">{{ rec.description_en }}</p>
{% if rec.hospital %}
<p class="text-xs text-gray-500 mt-2">
<i data-lucide="building" class="w-3 h-3 inline"></i>
{{ rec.hospital.name }}
</p>
{% endif %}
</div>
{% empty %}
<p class="text-sm text-gray-500 text-center py-4">{% trans "No recommendations available" %}</p>
{% endfor %}
</div>
<a href="#" class="mt-3 text-sm text-indigo-600 hover:text-indigo-700 inline-block">
{% trans "View All Recommendations" %} →
</a>
</div>
</div>
<!-- Quick Actions -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
<h3 class="text-lg font-bold text-gray-900 mb-4">{% trans "Quick Actions" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<a href="{% url 'executive:predictive_insights' %}"
class="flex items-center gap-3 p-4 rounded-lg border-2 border-gray-200 hover:border-indigo-300 hover:bg-indigo-50 transition">
<i data-lucide="trending-up" class="w-6 h-6 text-indigo-600"></i>
<div>
<p class="font-semibold text-gray-900">{% trans "Predictive Insights" %}</p>
<p class="text-xs text-gray-600">{% trans "View AI-detected patterns and risks" %}</p>
</div>
</a>
<a href="{% url 'executive:executive_qa' %}"
class="flex items-center gap-3 p-4 rounded-lg border-2 border-gray-200 hover:border-purple-300 hover:bg-purple-50 transition">
<i data-lucide="message-circle" class="w-6 h-6 text-purple-600"></i>
<div>
<p class="font-semibold text-gray-900">{% trans "Ask AI" %}</p>
<p class="text-xs text-gray-600">{% trans "Ask questions about your data" %}</p>
</div>
</a>
<form method="post" action="{% url 'executive:generate_report' %}" class="flex items-center gap-3 p-4 rounded-lg border-2 border-gray-200 hover:border-green-300 hover:bg-green-50 transition cursor-pointer">
{% csrf_token %}
<input type="hidden" name="report_type" value="weekly">
<i data-lucide="file-text" class="w-6 h-6 text-green-600"></i>
<div>
<p class="font-semibold text-gray-900">{% trans "Generate Report" %}</p>
<p class="text-xs text-gray-600">{% trans "Create AI executive summary" %}</p>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Complaints Trend Chart
const complaintsData = JSON.parse('{{ chart_data.complaints_trend|escapejs }}');
if (complaintsData && complaintsData.length > 0) {
const complaintsOptions = {
series: [{
name: '{% trans "Complaints" %}',
data: complaintsData.map(d => d.value)
}],
chart: {
type: 'area',
height: 300,
toolbar: { show: false }
},
colors: ['#ef4444'],
stroke: { curve: 'smooth', width: 2 },
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.4,
opacityTo: 0.1,
}
},
xaxis: {
categories: complaintsData.map(d => d.date),
labels: { rotate: -45 }
},
yaxis: {
labels: { formatter: function(val) { return Math.round(val); } }
},
tooltip: {
x: { format: 'dd MMM yyyy' }
}
};
const complaintsChart = new ApexCharts(document.querySelector("#complaintsChart"), complaintsOptions);
complaintsChart.render();
}
// Satisfaction Trend Chart
const satisfactionData = JSON.parse('{{ chart_data.surveys_satisfaction_trend|escapejs }}');
if (satisfactionData && satisfactionData.length > 0) {
const satisfactionOptions = {
series: [{
name: '{% trans "Satisfaction %" %}',
data: satisfactionData.map(d => d.value)
}],
chart: {
type: 'line',
height: 300,
toolbar: { show: false }
},
colors: ['#10b981'],
stroke: { curve: 'smooth', width: 2 },
xaxis: {
categories: satisfactionData.map(d => d.date),
labels: { rotate: -45 }
},
yaxis: {
min: 0,
max: 100,
labels: { formatter: function(val) { return val.toFixed(0) + '%'; } }
},
tooltip: {
x: { format: 'dd MMM yyyy' }
}
};
const satisfactionChart = new ApexCharts(document.querySelector("#satisfactionChart"), satisfactionOptions);
satisfactionChart.render();
}
// Initialize Lucide icons
lucide.createIcons();
});
</script>
{% endblock %}
```
## Assistant
The dashboard template references a variance badge partial template, so I need to create that next.
## Assistant
Now let me create the variance badge partial template:
### Tool: Shell: mkdir -p /home/ismail/projects/HH/apps/executive_summary/templates/executive/partials (Create partials directory for executive templates)
**Status**: completed
**Input:**
```json
{
"command": "mkdir -p /home/ismail/projects/HH/apps/executive_summary/templates/executive/partials",
"description": "Create partials directory for executive templates"
}
```
**Output:**
```
Command: mkdir -p /home/ismail/projects/HH/apps/executive_summary/templates/executive/partials
Directory: (root)
Output: (empty)
Error: (none)
Exit Code: 0
Signal: 0
Process Group PGID: 293586
```
## Assistant
Now I need to create the variance badge partial template that was referenced in the dashboard template.
### Tool: WriteFile: Writing to apps/executive_summary/templates/executive/partials/variance_badge.html
**Status**: completed
**Input:**
```json
{
"content": "{% if variance %}\n<span class=\"inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold\n {% if variance.direction == 'up' and variance.value > 0 %}\n {% if variance.positive_up %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}\n {% elif variance.direction == 'down' and variance.value < 0 %}\n {% if variance.positive_down %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}\n {% else %}\n bg-gray-100 text-gray-800\n {% endif %}\">\n {% if variance.direction == 'up' %}\n ↑\n {% elif variance.direction == 'down' %}\n ↓\n {% else %}\n →\n {% endif %}\n {{ variance.value|floatformat:1 }}%\n</span>\n{% endif %}\n",
"file_path": "/home/ismail/projects/HH/apps/executive_summary/templates/executive/partials/variance_badge.html"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/templates/executive/partials/variance_badge.html`
**Output:**
*Diff for `variance_badge.html`:*
```diff
{% if variance %}
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold
{% if variance.direction == 'up' and variance.value > 0 %}
{% if variance.positive_up %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}
{% elif variance.direction == 'down' and variance.value < 0 %}
{% if variance.positive_down %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}
{% else %}
bg-gray-100 text-gray-800
{% endif %}">
{% if variance.direction == 'up' %}
{% elif variance.direction == 'down' %}
{% else %}
{% endif %}
{{ variance.value|floatformat:1 }}%
</span>
{% endif %}
```
## Assistant
Now I need to create the predictive insights, Q&A templates, admin configuration, and register the URLs in the main config.
## Assistant
Now let me create the predictive insights and Q&A templates, admin, tasks, and register the URLs. I'll do these in parallel:
### Tool: WriteFile: Writing to apps/executive_summary/templates/executive/insights.html
**Status**: completed
**Input:**
```json
{
"content": "{% extends \"layouts/base.html\" %}\n{% load i18n %}\n{% load static %}\n\n{% block title %}{% trans \"Predictive Insights\" %} | PX360{% endblock %}\n\n{% block content %}\n<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8\">\n <!-- Page Header -->\n <div class=\"flex flex-col md:flex-row md:items-center md:justify-between mb-8\">\n <div>\n <h1 class=\"text-3xl font-bold text-gray-900\">{% trans \"Predictive Insights\" %}</h1>\n <p class=\"mt-2 text-sm text-gray-600\">{% trans \"AI-detected patterns, anomalies, and early warnings\" %}</p>\n </div>\n \n <!-- Summary Stats -->\n <div class=\"mt-4 md:mt-0 flex items-center gap-4\">\n <div class=\"text-center\">\n <p class=\"text-2xl font-bold text-gray-900\">{{ total_insights }}</p>\n <p class=\"text-xs text-gray-600\">{% trans \"Total\" %}</p>\n </div>\n <div class=\"text-center\">\n <p class=\"text-2xl font-bold text-red-600\">{{ critical_insights }}</p>\n <p class=\"text-xs text-gray-600\">{% trans \"Critical\" %}</p>\n </div>\n <div class=\"text-center\">\n <p class=\"text-2xl font-bold text-orange-600\">{{ high_insights }}</p>\n <p class=\"text-xs text-gray-600\">{% trans \"High\" %}</p>\n </div>\n <div class=\"text-center\">\n <p class=\"text-2xl font-bold text-blue-600\">{{ new_insights }}</p>\n <p class=\"text-xs text-gray-600\">{% trans \"New\" %}</p>\n </div>\n </div>\n </div>\n\n <!-- Filters -->\n <div class=\"bg-white rounded-lg p-4 mb-6 shadow-sm border border-gray-200\">\n <form method=\"get\" class=\"grid grid-cols-1 md:grid-cols-4 gap-4\">\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">{% trans \"Severity\" %}</label>\n <select name=\"severity\" onchange=\"this.form.submit()\" class=\"form-select text-sm\">\n <option value=\"\">{% trans \"All Severities\" %}</option>\n {% for value, label in severity_choices %}\n <option value=\"{{ value }}\" {% if current_filters.severity == value %}selected{% endif %}>\n {{ label }}\n </option>\n {% endfor %}\n </select>\n </div>\n \n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">{% trans \"Status\" %}</label>\n <select name=\"status\" onchange=\"this.form.submit()\" class=\"form-select text-sm\">\n <option value=\"\">{% trans \"All Statuses\" %}</option>\n {% for value, label in status_choices %}\n <option value=\"{{ value }}\" {% if current_filters.status == value %}selected{% endif %}>\n {{ label }}\n </option>\n {% endfor %}\n </select>\n </div>\n \n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">{% trans \"Type\" %}</label>\n <select name=\"insight_type\" onchange=\"this.form.submit()\" class=\"form-select text-sm\">\n <option value=\"\">{% trans \"All Types\" %}</option>\n {% for value, label in insight_type_choices %}\n <option value=\"{{ value }}\" {% if current_filters.insight_type == value %}selected{% endif %}>\n {{ label }}\n </option>\n {% endfor %}\n </select>\n </div>\n \n {% if available_hospitals %}\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">{% trans \"Hospital\" %}</label>\n <select name=\"hospital\" onchange=\"this.form.submit()\" class=\"form-select text-sm\">\n <option value=\"\">{% trans \"All Hospitals\" %}</option>\n {% for hospital in available_hospitals %}\n <option value=\"{{ hospital.id }}\" {% if current_filters.hospital == hospital.id|stringformat:\"s\" %}selected{% endif %}>\n {{ hospital.name }}\n </option>\n {% endfor %}\n </select>\n </div>\n {% endif %}\n </form>\n </div>\n\n <!-- Insights List -->\n <div class=\"space-y-4\">\n {% for insight in insights %}\n <div class=\"bg-white rounded-lg p-6 shadow-sm border border-gray-200 border-l-4 \n {% if insight.severity == 'critical' %}border-l-red-500\n {% elif insight.severity == 'high' %}border-l-orange-500\n {% elif insight.severity == 'medium' %}border-l-yellow-500\n {% else %}border-l-blue-500{% endif %}\">\n \n <div class=\"flex items-start justify-between gap-4\">\n <div class=\"flex-1\">\n <div class=\"flex items-center gap-3 mb-3\">\n <span class=\"px-3 py-1 text-xs font-semibold rounded-full\n {% if insight.severity == 'critical' %}bg-red-100 text-red-800\n {% elif insight.severity == 'high' %}bg-orange-100 text-orange-800\n {% elif insight.severity == 'medium' %}bg-yellow-100 text-yellow-800\n {% else %}bg-blue-100 text-blue-800{% endif %}\">\n {{ insight.get_severity_display }}\n </span>\n <span class=\"text-xs text-gray-500\">{{ insight.get_insight_type_display }}</span>\n <span class=\"text-xs text-gray-500\">•</span>\n <span class=\"text-xs text-gray-500\">{{ insight.created_at|date:\"M d, Y H:i\" }}</span>\n \n {% if insight.status == 'acknowledged' %}\n <span class=\"px-2 py-0.5 text-xs bg-green-100 text-green-800 rounded-full\">\n {% trans \"Acknowledged\" %}\n </span>\n {% endif %}\n </div>\n \n <h3 class=\"text-lg font-semibold text-gray-900 mb-2\">{{ insight.title_en }}</h3>\n <p class=\"text-sm text-gray-600 mb-3\">{{ insight.description_en }}</p>\n \n {% if insight.recommendation_en %}\n <div class=\"bg-blue-50 rounded-lg p-3 mb-3\">\n <p class=\"text-sm font-medium text-blue-900 mb-1 flex items-center gap-2\">\n <i data-lucide=\"lightbulb\" class=\"w-4 h-4\"></i>\n {% trans \"AI Recommendation\" %}\n </p>\n <p class=\"text-sm text-blue-800\">{{ insight.recommendation_en }}</p>\n </div>\n {% endif %}\n \n <div class=\"flex items-center gap-4 text-sm text-gray-500\">\n {% if insight.hospital %}\n <span class=\"flex items-center gap-1\">\n <i data-lucide=\"building\" class=\"w-4 h-4\"></i>\n {{ insight.hospital.name }}\n </span>\n {% endif %}\n {% if insight.department %}\n <span class=\"flex items-center gap-1\">\n <i data-lucide=\"layers\" class=\"w-4 h-4\"></i>\n {{ insight.department.name }}\n </span>\n {% endif %}\n {% if insight.confidence_score %}\n <span>{% trans \"Confidence\" %}: {{ insight.confidence_score|floatformat:0 }}%</span>\n {% endif %}\n {% if insight.predicted_date %}\n <span>{% trans \"Predicted\" %}: {{ insight.predicted_date|date:\"M d, Y\" }}</span>\n {% endif %}\n </div>\n </div>\n \n {% if insight.status == 'new' %}\n <form method=\"post\" action=\"{% url 'executive:acknowledge_insight' insight.id %}\" class=\"flex-shrink-0\">\n {% csrf_token %}\n <button type=\"submit\" \n class=\"px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition\">\n {% trans \"Acknowledge\" %}\n </button>\n </form>\n {% endif %}\n </div>\n </div>\n {% empty %}\n <div class=\"bg-white rounded-lg p-12 text-center shadow-sm border border-gray-200\">\n <i data-lucide=\"check-circle\" class=\"w-16 h-16 mx-auto text-green-500 mb-4\"></i>\n <h3 class=\"text-xl font-semibold text-gray-900 mb-2\">{% trans \"No Insights Available\" %}</h3>\n <p class=\"text-gray-600\">{% trans \"No predictive insights match your current filters\" %}</p>\n </div>\n {% endfor %}\n </div>\n\n <!-- Pagination -->\n {% if page_obj.has_other_pages %}\n <div class=\"mt-8 flex items-center justify-center gap-2\">\n {% if page_obj.has_previous %}\n <a href=\"?page=1{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}\" \n class=\"px-3 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50\">\n {% trans \"First\" %}\n </a>\n <a href=\"?page={{ page_obj.previous_page_number }}{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}\" \n class=\"px-3 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50\">\n {% trans \"Previous\" %}\n </a>\n {% endif %}\n \n <span class=\"px-4 py-2 text-sm text-gray-700\">\n {% trans \"Page\" %} {{ page_obj.number }} {% trans \"of\" %} {{ page_obj.paginator.num_pages }}\n </span>\n \n {% if page_obj.has_next %}\n <a href=\"?page={{ page_obj.next_page_number }}{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}\" \n class=\"px-3 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50\">\n {% trans \"Next\" %}\n </a>\n <a href=\"?page={{ page_obj.paginator.num_pages }}{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}\" \n class=\"px-3 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50\">\n {% trans \"Last\" %}\n </a>\n {% endif %}\n </div>\n {% endif %}\n</div>\n{% endblock %}\n\n{% block extra_js %}\n<script>\ndocument.addEventListener('DOMContentLoaded', function() {\n lucide.createIcons();\n});\n</script>\n{% endblock %}\n",
"file_path": "/home/ismail/projects/HH/apps/executive_summary/templates/executive/insights.html"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/templates/executive/insights.html`
**Output:**
*Diff for `insights.html`:*
```diff
{% extends "layouts/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Predictive Insights" %} | PX360{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Page Header -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8">
<div>
<h1 class="text-3xl font-bold text-gray-900">{% trans "Predictive Insights" %}</h1>
<p class="mt-2 text-sm text-gray-600">{% trans "AI-detected patterns, anomalies, and early warnings" %}</p>
</div>
<!-- Summary Stats -->
<div class="mt-4 md:mt-0 flex items-center gap-4">
<div class="text-center">
<p class="text-2xl font-bold text-gray-900">{{ total_insights }}</p>
<p class="text-xs text-gray-600">{% trans "Total" %}</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold text-red-600">{{ critical_insights }}</p>
<p class="text-xs text-gray-600">{% trans "Critical" %}</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold text-orange-600">{{ high_insights }}</p>
<p class="text-xs text-gray-600">{% trans "High" %}</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold text-blue-600">{{ new_insights }}</p>
<p class="text-xs text-gray-600">{% trans "New" %}</p>
</div>
</div>
</div>
<!-- Filters -->
<div class="bg-white rounded-lg p-4 mb-6 shadow-sm border border-gray-200">
<form method="get" class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{% trans "Severity" %}</label>
<select name="severity" onchange="this.form.submit()" class="form-select text-sm">
<option value="">{% trans "All Severities" %}</option>
{% for value, label in severity_choices %}
<option value="{{ value }}" {% if current_filters.severity == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{% trans "Status" %}</label>
<select name="status" onchange="this.form.submit()" class="form-select text-sm">
<option value="">{% trans "All Statuses" %}</option>
{% for value, label in status_choices %}
<option value="{{ value }}" {% if current_filters.status == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{% trans "Type" %}</label>
<select name="insight_type" onchange="this.form.submit()" class="form-select text-sm">
<option value="">{% trans "All Types" %}</option>
{% for value, label in insight_type_choices %}
<option value="{{ value }}" {% if current_filters.insight_type == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
{% if available_hospitals %}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{% trans "Hospital" %}</label>
<select name="hospital" onchange="this.form.submit()" class="form-select text-sm">
<option value="">{% trans "All Hospitals" %}</option>
{% for hospital in available_hospitals %}
<option value="{{ hospital.id }}" {% if current_filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
{% endif %}
</form>
</div>
<!-- Insights List -->
<div class="space-y-4">
{% for insight in insights %}
<div class="bg-white rounded-lg p-6 shadow-sm border border-gray-200 border-l-4
{% if insight.severity == 'critical' %}border-l-red-500
{% elif insight.severity == 'high' %}border-l-orange-500
{% elif insight.severity == 'medium' %}border-l-yellow-500
{% else %}border-l-blue-500{% endif %}">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<div class="flex items-center gap-3 mb-3">
<span class="px-3 py-1 text-xs font-semibold rounded-full
{% if insight.severity == 'critical' %}bg-red-100 text-red-800
{% elif insight.severity == 'high' %}bg-orange-100 text-orange-800
{% elif insight.severity == 'medium' %}bg-yellow-100 text-yellow-800
{% else %}bg-blue-100 text-blue-800{% endif %}">
{{ insight.get_severity_display }}
</span>
<span class="text-xs text-gray-500">{{ insight.get_insight_type_display }}</span>
<span class="text-xs text-gray-500">•</span>
<span class="text-xs text-gray-500">{{ insight.created_at|date:"M d, Y H:i" }}</span>
{% if insight.status == 'acknowledged' %}
<span class="px-2 py-0.5 text-xs bg-green-100 text-green-800 rounded-full">
{% trans "Acknowledged" %}
</span>
{% endif %}
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">{{ insight.title_en }}</h3>
<p class="text-sm text-gray-600 mb-3">{{ insight.description_en }}</p>
{% if insight.recommendation_en %}
<div class="bg-blue-50 rounded-lg p-3 mb-3">
<p class="text-sm font-medium text-blue-900 mb-1 flex items-center gap-2">
<i data-lucide="lightbulb" class="w-4 h-4"></i>
{% trans "AI Recommendation" %}
</p>
<p class="text-sm text-blue-800">{{ insight.recommendation_en }}</p>
</div>
{% endif %}
<div class="flex items-center gap-4 text-sm text-gray-500">
{% if insight.hospital %}
<span class="flex items-center gap-1">
<i data-lucide="building" class="w-4 h-4"></i>
{{ insight.hospital.name }}
</span>
{% endif %}
{% if insight.department %}
<span class="flex items-center gap-1">
<i data-lucide="layers" class="w-4 h-4"></i>
{{ insight.department.name }}
</span>
{% endif %}
{% if insight.confidence_score %}
<span>{% trans "Confidence" %}: {{ insight.confidence_score|floatformat:0 }}%</span>
{% endif %}
{% if insight.predicted_date %}
<span>{% trans "Predicted" %}: {{ insight.predicted_date|date:"M d, Y" }}</span>
{% endif %}
</div>
</div>
{% if insight.status == 'new' %}
<form method="post" action="{% url 'executive:acknowledge_insight' insight.id %}" class="flex-shrink-0">
{% csrf_token %}
<button type="submit"
class="px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition">
{% trans "Acknowledge" %}
</button>
</form>
{% endif %}
</div>
</div>
{% empty %}
<div class="bg-white rounded-lg p-12 text-center shadow-sm border border-gray-200">
<i data-lucide="check-circle" class="w-16 h-16 mx-auto text-green-500 mb-4"></i>
<h3 class="text-xl font-semibold text-gray-900 mb-2">{% trans "No Insights Available" %}</h3>
<p class="text-gray-600">{% trans "No predictive insights match your current filters" %}</p>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<div class="mt-8 flex items-center justify-center gap-2">
{% if page_obj.has_previous %}
<a href="?page=1{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}"
class="px-3 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
{% trans "First" %}
</a>
<a href="?page={{ page_obj.previous_page_number }}{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}"
class="px-3 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
{% trans "Previous" %}
</a>
{% endif %}
<span class="px-4 py-2 text-sm text-gray-700">
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}"
class="px-3 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
{% trans "Next" %}
</a>
<a href="?page={{ page_obj.paginator.num_pages }}{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}"
class="px-3 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
{% trans "Last" %}
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
{% endblock %}
```
### Tool: WriteFile: Writing to apps/executive_summary/templates/executive/qa.html
**Status**: completed
**Input:**
```json
{
"content": "{% extends \"layouts/base.html\" %}\n{% load i18n %}\n{% load static %}\n\n{% block title %}{% trans \"Ask AI\" %} | PX360{% endblock %}\n\n{% block content %}\n<div class=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8\">\n <!-- Page Header -->\n <div class=\"text-center mb-8\">\n <h1 class=\"text-3xl font-bold text-gray-900 mb-2\">{% trans \"Ask AI About Your Data\" %}</h1>\n <p class=\"text-gray-600\">{% trans \"Get instant answers from your PX360 data using AI\" %}</p>\n </div>\n\n <!-- Suggested Questions -->\n <div class=\"mb-6\">\n <p class=\"text-sm font-medium text-gray-700 mb-3\">{% trans \"Suggested Questions\" %}:</p>\n <div class=\"flex flex-wrap gap-2\">\n {% for question in suggested_questions %}\n <button onclick=\"document.getElementById('questionInput').value='{{ question|escapejs }}'; document.getElementById('questionForm').submit();\" \n class=\"px-4 py-2 bg-indigo-50 text-indigo-700 text-sm rounded-full hover:bg-indigo-100 transition\">\n {{ question }}\n </button>\n {% endfor %}\n </div>\n </div>\n\n <!-- Question Input -->\n <form id=\"questionForm\" method=\"post\" action=\"{% url 'executive:executive_qa_api' %}\" \n class=\"bg-white rounded-xl p-6 shadow-lg border border-gray-200 mb-8\">\n {% csrf_token %}\n <div class=\"flex gap-3\">\n <input type=\"text\" id=\"questionInput\" name=\"question\" \n placeholder=\"{% trans 'Type your question...' %}\" \n class=\"flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500\"\n required>\n <button type=\"submit\" \n id=\"submitBtn\"\n class=\"px-6 py-3 bg-indigo-600 text-white font-semibold rounded-lg hover:bg-indigo-700 transition flex items-center gap-2\">\n <i data-lucide=\"send\" class=\"w-5 h-5\"></i>\n {% trans \"Ask\" %}\n </button>\n </div>\n </form>\n\n <!-- Loading Indicator -->\n <div id=\"loadingIndicator\" class=\"hidden text-center py-8\">\n <div class=\"inline-flex items-center gap-3\">\n <div class=\"animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600\"></div>\n <span class=\"text-gray-600\">{% trans \"AI is thinking...\" %}</span>\n </div>\n </div>\n\n <!-- Answer Display -->\n <div id=\"answerContainer\" class=\"hidden\">\n <div class=\"bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl p-6 text-white shadow-lg\">\n <div class=\"flex items-start gap-3 mb-4\">\n <i data-lucide=\"sparkles\" class=\"w-6 h-6 flex-shrink-0\"></i>\n <div class=\"flex-1\">\n <h3 class=\"text-lg font-bold\">{% trans \"AI Answer\" %}</h3>\n <p id=\"questionText\" class=\"text-sm text-white/80 mt-1\"></p>\n </div>\n </div>\n <div class=\"bg-white/10 backdrop-blur rounded-lg p-4\">\n <p id=\"answerText\" class=\"text-base leading-relaxed\"></p>\n </div>\n <div id=\"supportingData\" class=\"mt-4 flex flex-wrap gap-2\"></div>\n <div id=\"confidenceBadge\" class=\"mt-3 inline-flex items-center gap-2 px-3 py-1 bg-white/20 rounded-full text-sm\"></div>\n </div>\n </div>\n\n <!-- Recent Questions History -->\n {% if recent_questions %}\n <div class=\"mt-8\">\n <h2 class=\"text-xl font-bold text-gray-900 mb-4\">{% trans \"Recent Questions\" %}</h2>\n <div class=\"space-y-4\">\n {% for q in recent_questions|slice:\":5\" %}\n <div class=\"bg-white rounded-lg p-6 shadow-sm border border-gray-200\">\n <p class=\"font-semibold text-gray-900 mb-2 flex items-center gap-2\">\n <i data-lucide=\"help-circle\" class=\"w-4 h-4 text-indigo-600\"></i>\n {{ q.question }}\n </p>\n <p class=\"text-sm text-gray-600 ml-6\">{{ q.answer }}</p>\n <p class=\"text-xs text-gray-400 mt-2 ml-6\">{{ q.timestamp }}</p>\n </div>\n {% endfor %}\n </div>\n </div>\n {% endif %}\n</div>\n{% endblock %}\n\n{% block extra_js %}\n<script>\ndocument.getElementById('questionForm').addEventListener('submit', async function(e) {\n e.preventDefault();\n \n const question = document.getElementById('questionInput').value;\n const submitBtn = document.getElementById('submitBtn');\n const loadingIndicator = document.getElementById('loadingIndicator');\n const answerContainer = document.getElementById('answerContainer');\n \n // Show loading\n submitBtn.disabled = true;\n submitBtn.innerHTML = '<div class=\"animate-spin rounded-full h-5 w-5 border-b-2 border-white\"></div>';\n loadingIndicator.classList.remove('hidden');\n answerContainer.classList.add('hidden');\n \n try {\n const formData = new FormData();\n formData.append('question', question);\n formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);\n \n const response = await fetch('{% url \"executive:executive_qa_api\" %}', {\n method: 'POST',\n headers: {'X-Requested-With': 'XMLHttpRequest'},\n body: formData\n });\n \n const data = await response.json();\n \n if (data.status === 'success') {\n // Display answer\n document.getElementById('questionText').textContent = question;\n document.getElementById('answerText').textContent = data.answer;\n \n // Supporting data\n const supportingDataEl = document.getElementById('supportingData');\n supportingDataEl.innerHTML = '';\n if (data.supporting_data && data.supporting_data.length > 0) {\n data.supporting_data.forEach(item => {\n const badge = document.createElement('span');\n badge.className = 'px-2 py-1 bg-white/20 rounded text-xs';\n badge.textContent = item;\n supportingDataEl.appendChild(badge);\n });\n }\n \n // Confidence badge\n const confidenceBadge = document.getElementById('confidenceBadge');\n const confidence = data.confidence || 'medium';\n confidenceBadge.innerHTML = `<i data-lucide=\"shield\" class=\"w-4 h-4\"></i> Confidence: ${confidence}`;\n \n answerContainer.classList.remove('hidden');\n lucide.createIcons();\n } else {\n alert(data.error || '{% trans \"An error occurred\" %}');\n }\n } catch (error) {\n console.error('Error:', error);\n alert('{% trans \"Failed to get answer. Please try again.\" %}');\n } finally {\n submitBtn.disabled = false;\n submitBtn.innerHTML = '<i data-lucide=\"send\" class=\"w-5 h-5\"></i> {% trans \"Ask\" %}';\n loadingIndicator.classList.add('hidden');\n lucide.createIcons();\n }\n});\n</script>\n{% endblock %}\n",
"file_path": "/home/ismail/projects/HH/apps/executive_summary/templates/executive/qa.html"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/templates/executive/qa.html`
**Output:**
*Diff for `qa.html`:*
```diff
{% extends "layouts/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Ask AI" %} | PX360{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Page Header -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">{% trans "Ask AI About Your Data" %}</h1>
<p class="text-gray-600">{% trans "Get instant answers from your PX360 data using AI" %}</p>
</div>
<!-- Suggested Questions -->
<div class="mb-6">
<p class="text-sm font-medium text-gray-700 mb-3">{% trans "Suggested Questions" %}:</p>
<div class="flex flex-wrap gap-2">
{% for question in suggested_questions %}
<button onclick="document.getElementById('questionInput').value='{{ question|escapejs }}'; document.getElementById('questionForm').submit();"
class="px-4 py-2 bg-indigo-50 text-indigo-700 text-sm rounded-full hover:bg-indigo-100 transition">
{{ question }}
</button>
{% endfor %}
</div>
</div>
<!-- Question Input -->
<form id="questionForm" method="post" action="{% url 'executive:executive_qa_api' %}"
class="bg-white rounded-xl p-6 shadow-lg border border-gray-200 mb-8">
{% csrf_token %}
<div class="flex gap-3">
<input type="text" id="questionInput" name="question"
placeholder="{% trans 'Type your question...' %}"
class="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required>
<button type="submit"
id="submitBtn"
class="px-6 py-3 bg-indigo-600 text-white font-semibold rounded-lg hover:bg-indigo-700 transition flex items-center gap-2">
<i data-lucide="send" class="w-5 h-5"></i>
{% trans "Ask" %}
</button>
</div>
</form>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="hidden text-center py-8">
<div class="inline-flex items-center gap-3">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
<span class="text-gray-600">{% trans "AI is thinking..." %}</span>
</div>
</div>
<!-- Answer Display -->
<div id="answerContainer" class="hidden">
<div class="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl p-6 text-white shadow-lg">
<div class="flex items-start gap-3 mb-4">
<i data-lucide="sparkles" class="w-6 h-6 flex-shrink-0"></i>
<div class="flex-1">
<h3 class="text-lg font-bold">{% trans "AI Answer" %}</h3>
<p id="questionText" class="text-sm text-white/80 mt-1"></p>
</div>
</div>
<div class="bg-white/10 backdrop-blur rounded-lg p-4">
<p id="answerText" class="text-base leading-relaxed"></p>
</div>
<div id="supportingData" class="mt-4 flex flex-wrap gap-2"></div>
<div id="confidenceBadge" class="mt-3 inline-flex items-center gap-2 px-3 py-1 bg-white/20 rounded-full text-sm"></div>
</div>
</div>
<!-- Recent Questions History -->
{% if recent_questions %}
<div class="mt-8">
<h2 class="text-xl font-bold text-gray-900 mb-4">{% trans "Recent Questions" %}</h2>
<div class="space-y-4">
{% for q in recent_questions|slice:":5" %}
<div class="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
<p class="font-semibold text-gray-900 mb-2 flex items-center gap-2">
<i data-lucide="help-circle" class="w-4 h-4 text-indigo-600"></i>
{{ q.question }}
</p>
<p class="text-sm text-gray-600 ml-6">{{ q.answer }}</p>
<p class="text-xs text-gray-400 mt-2 ml-6">{{ q.timestamp }}</p>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script>
document.getElementById('questionForm').addEventListener('submit', async function(e) {
e.preventDefault();
const question = document.getElementById('questionInput').value;
const submitBtn = document.getElementById('submitBtn');
const loadingIndicator = document.getElementById('loadingIndicator');
const answerContainer = document.getElementById('answerContainer');
// Show loading
submitBtn.disabled = true;
submitBtn.innerHTML = '<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>';
loadingIndicator.classList.remove('hidden');
answerContainer.classList.add('hidden');
try {
const formData = new FormData();
formData.append('question', question);
formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);
const response = await fetch('{% url "executive:executive_qa_api" %}', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: formData
});
const data = await response.json();
if (data.status === 'success') {
// Display answer
document.getElementById('questionText').textContent = question;
document.getElementById('answerText').textContent = data.answer;
// Supporting data
const supportingDataEl = document.getElementById('supportingData');
supportingDataEl.innerHTML = '';
if (data.supporting_data && data.supporting_data.length > 0) {
data.supporting_data.forEach(item => {
const badge = document.createElement('span');
badge.className = 'px-2 py-1 bg-white/20 rounded text-xs';
badge.textContent = item;
supportingDataEl.appendChild(badge);
});
}
// Confidence badge
const confidenceBadge = document.getElementById('confidenceBadge');
const confidence = data.confidence || 'medium';
confidenceBadge.innerHTML = `<i data-lucide="shield" class="w-4 h-4"></i> Confidence: ${confidence}`;
answerContainer.classList.remove('hidden');
lucide.createIcons();
} else {
alert(data.error || '{% trans "An error occurred" %}');
}
} catch (error) {
console.error('Error:', error);
alert('{% trans "Failed to get answer. Please try again." %}');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i data-lucide="send" class="w-5 h-5"></i> {% trans "Ask" %}';
loadingIndicator.classList.add('hidden');
lucide.createIcons();
}
});
</script>
{% endblock %}
```
## Assistant
Now I need to create the admin.py to enable Django admin management of the executive summary models and register the executive app URLs in the main config.
## Assistant
Now let me create the admin.py, tasks.py, and register the URLs in the main config:
### Tool: WriteFile: Writing to apps/executive_summary/admin.py
**Status**: completed
**Input:**
```json
{
"content": "\"\"\"\nExecutive Summary Admin - Django admin interface for executive summary models\n\"\"\"\nfrom django.contrib import admin\nfrom django.utils.html import format_html\nfrom django.urls import reverse\nfrom django.utils import timezone\n\nfrom .models import ExecutiveMetric, ExecutiveReport, PredictiveInsight, AIRecommendation\n\n\n@admin.register(ExecutiveMetric)\nclass ExecutiveMetricAdmin(admin.ModelAdmin):\n list_display = [\n 'metric_type', 'metric_value', 'variance_display', \n 'metric_date', 'hospital', 'created_at'\n ]\n list_filter = ['metric_type', 'metric_date', 'hospital']\n date_hierarchy = 'metric_date'\n search_fields = ['metric_type', 'hospital__name']\n ordering = ['-metric_date']\n \n def variance_display(self, obj):\n if obj.variance is None:\n return \"-\"\n arrow = \"↑\" if obj.variance_direction == \"up\" else \"↓\" if obj.variance_direction == \"down\" else \"→\"\n color = \"#10b981\" if obj.variance_direction == \"up\" else \"#ef4444\" if obj.variance_direction == \"down\" else \"#6b7280\"\n return format_html(\n '<span style=\"color: {}; font-weight: bold;\">{} {}%</span>',\n color, arrow, obj.variance\n )\n variance_display.short_description = \"Variance\"\n\n\n@admin.register(ExecutiveReport)\nclass ExecutiveReportAdmin(admin.ModelAdmin):\n list_display = [\n 'report_type', 'status', 'start_date', 'end_date', \n 'generation_time_display', 'created_at', 'pdf_link'\n ]\n list_filter = ['report_type', 'status', 'created_at']\n date_hierarchy = 'created_at'\n readonly_fields = ['created_at', 'updated_at']\n ordering = ['-created_at']\n \n def generation_time_display(self, obj):\n if obj.generation_time_ms:\n return f\"{obj.generation_time_ms / 1000:.2f}s\"\n return \"-\"\n generation_time_display.short_description = \"Gen Time\"\n \n def pdf_link(self, obj):\n if obj.pdf_file:\n return format_html('<a href=\"{}\" target=\"_blank\">Download PDF</a>', obj.pdf_file.url)\n return \"-\"\n pdf_link.short_description = \"PDF\"\n\n\n@admin.register(PredictiveInsight)\nclass PredictiveInsightAdmin(admin.ModelAdmin):\n list_display = [\n 'title_en', 'severity_badge', 'insight_type', \n 'status', 'hospital', 'created_at', 'acknowledged_by_display'\n ]\n list_filter = ['severity', 'status', 'insight_type', 'hospital', 'created_at']\n search_fields = ['title_en', 'description_en', 'hospital__name']\n date_hierarchy = 'created_at'\n ordering = ['-severity', '-created_at']\n readonly_fields = ['created_at', 'updated_at', 'acknowledged_at']\n \n def severity_badge(self, obj):\n colors = {\n 'low': '#3b82f6',\n 'medium': '#f59e0b',\n 'high': '#f97316',\n 'critical': '#ef4444',\n }\n color = colors.get(obj.severity, '#6b7280')\n return format_html(\n '<span style=\"background-color: {}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold;\">{}</span>',\n color, obj.get_severity_display()\n )\n severity_badge.short_description = \"Severity\"\n \n def acknowledged_by_display(self, obj):\n if obj.acknowledged_by:\n return obj.acknowledged_by.get_full_name() or obj.acknowledged_by.email\n return \"-\"\n acknowledged_by_display.short_description = \"Acknowledged By\"\n\n\n@admin.register(AIRecommendation)\nclass AIRecommendationAdmin(admin.ModelAdmin):\n list_display = [\n 'title_en', 'category', 'priority_badge', \n 'status', 'hospital', 'created_at'\n ]\n list_filter = ['category', 'priority', 'status', 'hospital', 'created_at']\n search_fields = ['title_en', 'description_en', 'hospital__name']\n date_hierarchy = 'created_at'\n ordering = ['-priority', '-created_at']\n readonly_fields = ['created_at', 'updated_at']\n \n def priority_badge(self, obj):\n colors = {\n 'low': '#3b82f6',\n 'medium': '#f59e0b',\n 'high': '#f97316',\n 'urgent': '#ef4444',\n }\n color = colors.get(obj.priority, '#6b7280')\n return format_html(\n '<span style=\"background-color: {}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold;\">{}</span>',\n color, obj.get_priority_display()\n )\n priority_badge.short_description = \"Priority\"\n",
"file_path": "/home/ismail/projects/HH/apps/executive_summary/admin.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/admin.py`
**Output:**
*Diff for `admin.py`:*
```diff
"""
Executive Summary Admin - Django admin interface for executive summary models
"""
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils import timezone
from .models import ExecutiveMetric, ExecutiveReport, PredictiveInsight, AIRecommendation
@admin.register(ExecutiveMetric)
class ExecutiveMetricAdmin(admin.ModelAdmin):
list_display = [
'metric_type', 'metric_value', 'variance_display',
'metric_date', 'hospital', 'created_at'
]
list_filter = ['metric_type', 'metric_date', 'hospital']
date_hierarchy = 'metric_date'
search_fields = ['metric_type', 'hospital__name']
ordering = ['-metric_date']
def variance_display(self, obj):
if obj.variance is None:
return "-"
arrow = "↑" if obj.variance_direction == "up" else "↓" if obj.variance_direction == "down" else "→"
color = "#10b981" if obj.variance_direction == "up" else "#ef4444" if obj.variance_direction == "down" else "#6b7280"
return format_html(
'<span style="color: {}; font-weight: bold;">{} {}%</span>',
color, arrow, obj.variance
)
variance_display.short_description = "Variance"
@admin.register(ExecutiveReport)
class ExecutiveReportAdmin(admin.ModelAdmin):
list_display = [
'report_type', 'status', 'start_date', 'end_date',
'generation_time_display', 'created_at', 'pdf_link'
]
list_filter = ['report_type', 'status', 'created_at']
date_hierarchy = 'created_at'
readonly_fields = ['created_at', 'updated_at']
ordering = ['-created_at']
def generation_time_display(self, obj):
if obj.generation_time_ms:
return f"{obj.generation_time_ms / 1000:.2f}s"
return "-"
generation_time_display.short_description = "Gen Time"
def pdf_link(self, obj):
if obj.pdf_file:
return format_html('<a href="{}" target="_blank">Download PDF</a>', obj.pdf_file.url)
return "-"
pdf_link.short_description = "PDF"
@admin.register(PredictiveInsight)
class PredictiveInsightAdmin(admin.ModelAdmin):
list_display = [
'title_en', 'severity_badge', 'insight_type',
'status', 'hospital', 'created_at', 'acknowledged_by_display'
]
list_filter = ['severity', 'status', 'insight_type', 'hospital', 'created_at']
search_fields = ['title_en', 'description_en', 'hospital__name']
date_hierarchy = 'created_at'
ordering = ['-severity', '-created_at']
readonly_fields = ['created_at', 'updated_at', 'acknowledged_at']
def severity_badge(self, obj):
colors = {
'low': '#3b82f6',
'medium': '#f59e0b',
'high': '#f97316',
'critical': '#ef4444',
}
color = colors.get(obj.severity, '#6b7280')
return format_html(
'<span style="background-color: {}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold;">{}</span>',
color, obj.get_severity_display()
)
severity_badge.short_description = "Severity"
def acknowledged_by_display(self, obj):
if obj.acknowledged_by:
return obj.acknowledged_by.get_full_name() or obj.acknowledged_by.email
return "-"
acknowledged_by_display.short_description = "Acknowledged By"
@admin.register(AIRecommendation)
class AIRecommendationAdmin(admin.ModelAdmin):
list_display = [
'title_en', 'category', 'priority_badge',
'status', 'hospital', 'created_at'
]
list_filter = ['category', 'priority', 'status', 'hospital', 'created_at']
search_fields = ['title_en', 'description_en', 'hospital__name']
date_hierarchy = 'created_at'
ordering = ['-priority', '-created_at']
readonly_fields = ['created_at', 'updated_at']
def priority_badge(self, obj):
colors = {
'low': '#3b82f6',
'medium': '#f59e0b',
'high': '#f97316',
'urgent': '#ef4444',
}
color = colors.get(obj.priority, '#6b7280')
return format_html(
'<span style="background-color: {}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold;">{}</span>',
color, obj.get_priority_display()
)
priority_badge.short_description = "Priority"
```
## Assistant
Now let me create the Celery tasks file and register the URLs in the main config.
## Assistant
Now let me create the Celery tasks file and register the URLs in the main config:
### Tool: WriteFile: Writing to apps/executive_summary/tasks.py
**Status**: completed
**Input:**
```json
{
"content": "\"\"\"\nExecutive Summary Celery Tasks\n\nScheduled tasks for:\n- Generating weekly/monthly executive reports\n- Calculating daily metrics\n- Generating predictive insights\n- Creating AI recommendations\n- Sending PDF reports via email\n\"\"\"\nfrom celery import shared_task\nfrom django.utils import timezone\nfrom datetime import timedelta, datetime\nimport logging\nimport time\n\nlogger = logging.getLogger(__name__)\n\n\n@shared_task\ndef generate_weekly_executive_summary():\n \"\"\"\n Generate weekly executive summary report.\n \n Runs every Monday at 6 AM.\n Creates an ExecutiveReport with AI-generated narrative for the past week.\n \"\"\"\n from .models import ExecutiveReport\n from .services import AINarrativeService\n \n try:\n logger.info(\"Starting weekly executive summary generation\")\n \n end_date = timezone.now().date()\n start_date = end_date - timedelta(days=7)\n \n # Check if report already exists\n existing = ExecutiveReport.objects.filter(\n report_type=\"weekly\",\n start_date=start_date,\n end_date=end_date,\n ).first()\n \n if existing:\n logger.info(f\"Weekly report already exists for {start_date} to {end_date}\")\n return {\"status\": \"skipped\", \"report_id\": str(existing.id)}\n \n # Generate narratives using AI\n narrative_service = AINarrativeService()\n \n # Generate English narrative\n logger.info(\"Generating English narrative...\")\n en_result = narrative_service.generate_weekly_narrative(\n start_date=start_date,\n end_date=end_date,\n )\n \n # Generate Arabic narrative\n logger.info(\"Generating Arabic narrative...\")\n ar_result = narrative_service.generate_arabic_narrative(\n start_date=start_date,\n end_date=end_date,\n )\n \n # Create report\n report = ExecutiveReport.objects.create(\n report_type=\"weekly\",\n status=en_result.get(\"status\", \"completed\"),\n start_date=start_date,\n end_date=end_date,\n narrative_en=en_result.get(\"narrative_en\", \"\"),\n narrative_ar=ar_result.get(\"narrative_ar\", \"\"),\n highlights_en=en_result.get(\"highlights_en\", []),\n highlights_ar=ar_result.get(\"highlights_ar\", []),\n concerns_en=en_result.get(\"concerns_en\", []),\n concerns_ar=ar_result.get(\"concerns_ar\", []),\n ai_model=en_result.get(\"ai_model\", \"\"),\n generation_time_ms=en_result.get(\"generation_time_ms\"),\n error_message=en_result.get(\"error\", \"\") or ar_result.get(\"error\", \"\"),\n )\n \n logger.info(f\"Weekly executive summary generated: {report.id}\")\n return {\n \"status\": \"success\",\n \"report_id\": str(report.id),\n \"report_type\": \"weekly\",\n }\n \n except Exception as e:\n logger.error(f\"Failed to generate weekly executive summary: {str(e)}\", exc_info=True)\n return {\"status\": \"error\", \"error\": str(e)}\n\n\n@shared_task\ndef generate_monthly_executive_summary():\n \"\"\"\n Generate monthly executive summary report.\n \n Runs on 1st of each month at 7 AM.\n Creates an ExecutiveReport with AI-generated narrative for the past month.\n \"\"\"\n from .models import ExecutiveReport\n from .services import AINarrativeService\n \n try:\n logger.info(\"Starting monthly executive summary generation\")\n \n end_date = timezone.now().date()\n start_date = end_date - timedelta(days=30)\n \n # Check if report already exists\n existing = ExecutiveReport.objects.filter(\n report_type=\"monthly\",\n start_date=start_date,\n end_date=end_date,\n ).first()\n \n if existing:\n logger.info(f\"Monthly report already exists for {start_date} to {end_date}\")\n return {\"status\": \"skipped\", \"report_id\": str(existing.id)}\n \n # Generate narratives using AI\n narrative_service = AINarrativeService()\n \n # Generate English narrative\n logger.info(\"Generating English narrative...\")\n en_result = narrative_service.generate_weekly_narrative(\n start_date=start_date,\n end_date=end_date,\n )\n \n # Generate Arabic narrative\n logger.info(\"Generating Arabic narrative...\")\n ar_result = narrative_service.generate_arabic_narrative(\n start_date=start_date,\n end_date=end_date,\n )\n \n # Create report\n report = ExecutiveReport.objects.create(\n report_type=\"monthly\",\n status=en_result.get(\"status\", \"completed\"),\n start_date=start_date,\n end_date=end_date,\n narrative_en=en_result.get(\"narrative_en\", \"\"),\n narrative_ar=ar_result.get(\"narrative_ar\", \"\"),\n highlights_en=en_result.get(\"highlights_en\", []),\n highlights_ar=ar_result.get(\"highlights_ar\", []),\n concerns_en=en_result.get(\"concerns_en\", []),\n concerns_ar=ar_result.get(\"concerns_ar\", []),\n ai_model=en_result.get(\"ai_model\", \"\"),\n generation_time_ms=en_result.get(\"generation_time_ms\"),\n error_message=en_result.get(\"error\", \"\") or ar_result.get(\"error\", \"\"),\n )\n \n logger.info(f\"Monthly executive summary generated: {report.id}\")\n return {\n \"status\": \"success\",\n \"report_id\": str(report.id),\n \"report_type\": \"monthly\",\n }\n \n except Exception as e:\n logger.error(f\"Failed to generate monthly executive summary: {str(e)}\", exc_info=True)\n return {\"status\": \"error\", \"error\": str(e)}\n\n\n@shared_task\ndef calculate_daily_metrics():\n \"\"\"\n Calculate and store daily metrics for all hospitals.\n \n Runs daily at 1 AM.\n Aggregates metrics from complaints, surveys, actions, observations, etc.\n \"\"\"\n from .models import ExecutiveMetric\n from .services import ExecutiveSummaryService\n from apps.organizations.models import Hospital\n \n try:\n logger.info(\"Starting daily metrics calculation\")\n \n target_date = (timezone.now() - timedelta(days=1)).date()\n \n # Calculate system-wide metrics\n logger.info(f\"Calculating system-wide metrics for {target_date}\")\n summary_service = ExecutiveSummaryService()\n metrics = summary_service.aggregate_daily_metrics(target_date, hospital=None)\n \n # Calculate per-hospital metrics\n hospitals = Hospital.objects.filter(status=\"active\")\n for hospital in hospitals:\n logger.info(f\"Calculating metrics for {hospital.name}\")\n metrics = summary_service.aggregate_daily_metrics(target_date, hospital=hospital)\n \n logger.info(f\"Daily metrics calculated for {target_date}\")\n return {\n \"status\": \"success\",\n \"date\": target_date.isoformat(),\n \"hospitals_processed\": hospitals.count(),\n }\n \n except Exception as e:\n logger.error(f\"Failed to calculate daily metrics: {str(e)}\", exc_info=True)\n return {\"status\": \"error\", \"error\": str(e)}\n\n\n@shared_task\ndef generate_predictive_insights():\n \"\"\"\n Generate predictive insights from data analysis.\n \n Runs every 6 hours.\n Analyzes trends, anomalies, SLA breach risks, and creates PredictiveInsight objects.\n \"\"\"\n from .services import PredictiveAnalyticsService\n \n try:\n logger.info(\"Starting predictive insights generation\")\n \n predictive_service = PredictiveAnalyticsService()\n result = predictive_service.generate_predictive_insights()\n \n logger.info(f\"Predictive insights generated: {result.get('insights_created', 0)} insights created\")\n return {\n \"status\": \"success\",\n \"insights_created\": result.get(\"insights_created\", 0),\n \"insights_updated\": result.get(\"insights_updated\", 0),\n }\n \n except Exception as e:\n logger.error(f\"Failed to generate predictive insights: {str(e)}\", exc_info=True)\n return {\"status\": \"error\", \"error\": str(e)}\n\n\n@shared_task\ndef generate_ai_recommendations():\n \"\"\"\n Generate AI recommendations from predictive insights.\n \n Runs daily at 3 AM.\n Creates AIRecommendation objects based on unhandled PredictiveInsights.\n \"\"\"\n from .services import RecommendationService\n \n try:\n logger.info(\"Starting AI recommendations generation\")\n \n recommendation_service = RecommendationService()\n result = recommendation_service.generate_recommendations_from_insights()\n \n logger.info(f\"AI recommendations generated: {result.get('recommendations_created', 0)}\")\n return {\n \"status\": \"success\",\n \"recommendations_created\": result.get(\"recommendations_created\", 0),\n }\n \n except Exception as e:\n logger.error(f\"Failed to generate AI recommendations: {str(e)}\", exc_info=True)\n return {\"status\": \"error\", \"error\": str(e)}\n\n\n@shared_task\ndef send_executive_pdf_report():\n \"\"\"\n Send PDF reports to executives via email.\n \n Runs weekly and monthly after report generation.\n Emails the latest executive PDF report to all executive users.\n \"\"\"\n from django.core.mail import EmailMessage\n from django.conf import settings\n from .models import ExecutiveReport\n from apps.accounts.models import User\n \n try:\n logger.info(\"Starting executive PDF report email delivery\")\n \n # Get latest completed report\n latest_report = ExecutiveReport.objects.filter(\n status=\"completed\",\n pdf_file__isnull=False\n ).order_by(\"-created_at\").first()\n \n if not latest_report:\n logger.info(\"No completed PDF report found to send\")\n return {\"status\": \"skipped\", \"reason\": \"no_pdf_report\"}\n \n # Get executive users\n executives = User.objects.filter(\n groups__name=\"Executive\",\n is_active=True,\n ).distinct()\n \n if not executives.exists():\n logger.info(\"No executive users found to send report to\")\n return {\"status\": \"skipped\", \"reason\": \"no_executives\"}\n \n # Prepare email\n subject = f\"Executive Summary Report - {latest_report.get_report_type_display()}\"\n body = f\"\"\"Dear Executive,\n\nPlease find attached the {latest_report.get_report_type_display().lower()} summary report for the period {latest_report.start_date} to {latest_report.end_date}.\n\nKey Highlights:\n{chr(10).join(f\"• {highlight}\" for highlight in latest_report.highlights_en[:5])}\n\nKey Concerns:\n{chr(10).join(f\"• {concern}\" for concern in latest_report.concerns_en[:5])}\n\nThis report was generated automatically by the PX360 AI system.\n\nBest regards,\nPX360 Team\n\"\"\"\n \n # Send emails\n emails_sent = 0\n for executive in executives:\n if executive.email:\n try:\n email = EmailMessage(\n subject=subject,\n body=body,\n to=[executive.email],\n )\n email.attach_file(latest_report.pdf_file.path)\n email.send()\n emails_sent += 1\n logger.info(f\"Report sent to {executive.email}\")\n except Exception as e:\n logger.error(f\"Failed to send report to {executive.email}: {str(e)}\")\n \n logger.info(f\"PDF reports sent to {emails_sent} executives\")\n return {\n \"status\": \"success\",\n \"emails_sent\": emails_sent,\n \"report_id\": str(latest_report.id),\n }\n \n except Exception as e:\n logger.error(f\"Failed to send executive PDF reports: {str(e)}\", exc_info=True)\n return {\"status\": \"error\", \"error\": str(e)}\n",
"file_path": "/home/ismail/projects/HH/apps/executive_summary/tasks.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/tasks.py`
**Output:**
*Diff for `tasks.py`:*
```diff
"""
Executive Summary Celery Tasks
Scheduled tasks for:
- Generating weekly/monthly executive reports
- Calculating daily metrics
- Generating predictive insights
- Creating AI recommendations
- Sending PDF reports via email
"""
from celery import shared_task
from django.utils import timezone
from datetime import timedelta, datetime
import logging
import time
logger = logging.getLogger(__name__)
@shared_task
def generate_weekly_executive_summary():
"""
Generate weekly executive summary report.
Runs every Monday at 6 AM.
Creates an ExecutiveReport with AI-generated narrative for the past week.
"""
from .models import ExecutiveReport
from .services import AINarrativeService
try:
logger.info("Starting weekly executive summary generation")
end_date = timezone.now().date()
start_date = end_date - timedelta(days=7)
# Check if report already exists
existing = ExecutiveReport.objects.filter(
report_type="weekly",
start_date=start_date,
end_date=end_date,
).first()
if existing:
logger.info(f"Weekly report already exists for {start_date} to {end_date}")
return {"status": "skipped", "report_id": str(existing.id)}
# Generate narratives using AI
narrative_service = AINarrativeService()
# Generate English narrative
logger.info("Generating English narrative...")
en_result = narrative_service.generate_weekly_narrative(
start_date=start_date,
end_date=end_date,
)
# Generate Arabic narrative
logger.info("Generating Arabic narrative...")
ar_result = narrative_service.generate_arabic_narrative(
start_date=start_date,
end_date=end_date,
)
# Create report
report = ExecutiveReport.objects.create(
report_type="weekly",
status=en_result.get("status", "completed"),
start_date=start_date,
end_date=end_date,
narrative_en=en_result.get("narrative_en", ""),
narrative_ar=ar_result.get("narrative_ar", ""),
highlights_en=en_result.get("highlights_en", []),
highlights_ar=ar_result.get("highlights_ar", []),
concerns_en=en_result.get("concerns_en", []),
concerns_ar=ar_result.get("concerns_ar", []),
ai_model=en_result.get("ai_model", ""),
generation_time_ms=en_result.get("generation_time_ms"),
error_message=en_result.get("error", "") or ar_result.get("error", ""),
)
logger.info(f"Weekly executive summary generated: {report.id}")
return {
"status": "success",
"report_id": str(report.id),
"report_type": "weekly",
}
except Exception as e:
logger.error(f"Failed to generate weekly executive summary: {str(e)}", exc_info=True)
return {"status": "error", "error": str(e)}
@shared_task
def generate_monthly_executive_summary():
"""
Generate monthly executive summary report.
Runs on 1st of each month at 7 AM.
Creates an ExecutiveReport with AI-generated narrative for the past month.
"""
from .models import ExecutiveReport
from .services import AINarrativeService
try:
logger.info("Starting monthly executive summary generation")
end_date = timezone.now().date()
start_date = end_date - timedelta(days=30)
# Check if report already exists
existing = ExecutiveReport.objects.filter(
report_type="monthly",
start_date=start_date,
end_date=end_date,
).first()
if existing:
logger.info(f"Monthly report already exists for {start_date} to {end_date}")
return {"status": "skipped", "report_id": str(existing.id)}
# Generate narratives using AI
narrative_service = AINarrativeService()
# Generate English narrative
logger.info("Generating English narrative...")
en_result = narrative_service.generate_weekly_narrative(
start_date=start_date,
end_date=end_date,
)
# Generate Arabic narrative
logger.info("Generating Arabic narrative...")
ar_result = narrative_service.generate_arabic_narrative(
start_date=start_date,
end_date=end_date,
)
# Create report
report = ExecutiveReport.objects.create(
report_type="monthly",
status=en_result.get("status", "completed"),
start_date=start_date,
end_date=end_date,
narrative_en=en_result.get("narrative_en", ""),
narrative_ar=ar_result.get("narrative_ar", ""),
highlights_en=en_result.get("highlights_en", []),
highlights_ar=ar_result.get("highlights_ar", []),
concerns_en=en_result.get("concerns_en", []),
concerns_ar=ar_result.get("concerns_ar", []),
ai_model=en_result.get("ai_model", ""),
generation_time_ms=en_result.get("generation_time_ms"),
error_message=en_result.get("error", "") or ar_result.get("error", ""),
)
logger.info(f"Monthly executive summary generated: {report.id}")
return {
"status": "success",
"report_id": str(report.id),
"report_type": "monthly",
}
except Exception as e:
logger.error(f"Failed to generate monthly executive summary: {str(e)}", exc_info=True)
return {"status": "error", "error": str(e)}
@shared_task
def calculate_daily_metrics():
"""
Calculate and store daily metrics for all hospitals.
Runs daily at 1 AM.
Aggregates metrics from complaints, surveys, actions, observations, etc.
"""
from .models import ExecutiveMetric
from .services import ExecutiveSummaryService
from apps.organizations.models import Hospital
try:
logger.info("Starting daily metrics calculation")
target_date = (timezone.now() - timedelta(days=1)).date()
# Calculate system-wide metrics
logger.info(f"Calculating system-wide metrics for {target_date}")
summary_service = ExecutiveSummaryService()
metrics = summary_service.aggregate_daily_metrics(target_date, hospital=None)
# Calculate per-hospital metrics
hospitals = Hospital.objects.filter(status="active")
for hospital in hospitals:
logger.info(f"Calculating metrics for {hospital.name}")
metrics = summary_service.aggregate_daily_metrics(target_date, hospital=hospital)
logger.info(f"Daily metrics calculated for {target_date}")
return {
"status": "success",
"date": target_date.isoformat(),
"hospitals_processed": hospitals.count(),
}
except Exception as e:
logger.error(f"Failed to calculate daily metrics: {str(e)}", exc_info=True)
return {"status": "error", "error": str(e)}
@shared_task
def generate_predictive_insights():
"""
Generate predictive insights from data analysis.
Runs every 6 hours.
Analyzes trends, anomalies, SLA breach risks, and creates PredictiveInsight objects.
"""
from .services import PredictiveAnalyticsService
try:
logger.info("Starting predictive insights generation")
predictive_service = PredictiveAnalyticsService()
result = predictive_service.generate_predictive_insights()
logger.info(f"Predictive insights generated: {result.get('insights_created', 0)} insights created")
return {
"status": "success",
"insights_created": result.get("insights_created", 0),
"insights_updated": result.get("insights_updated", 0),
}
except Exception as e:
logger.error(f"Failed to generate predictive insights: {str(e)}", exc_info=True)
return {"status": "error", "error": str(e)}
@shared_task
def generate_ai_recommendations():
"""
Generate AI recommendations from predictive insights.
Runs daily at 3 AM.
Creates AIRecommendation objects based on unhandled PredictiveInsights.
"""
from .services import RecommendationService
try:
logger.info("Starting AI recommendations generation")
recommendation_service = RecommendationService()
result = recommendation_service.generate_recommendations_from_insights()
logger.info(f"AI recommendations generated: {result.get('recommendations_created', 0)}")
return {
"status": "success",
"recommendations_created": result.get("recommendations_created", 0),
}
except Exception as e:
logger.error(f"Failed to generate AI recommendations: {str(e)}", exc_info=True)
return {"status": "error", "error": str(e)}
@shared_task
def send_executive_pdf_report():
"""
Send PDF reports to executives via email.
Runs weekly and monthly after report generation.
Emails the latest executive PDF report to all executive users.
"""
from django.core.mail import EmailMessage
from django.conf import settings
from .models import ExecutiveReport
from apps.accounts.models import User
try:
logger.info("Starting executive PDF report email delivery")
# Get latest completed report
latest_report = ExecutiveReport.objects.filter(
status="completed",
pdf_file__isnull=False
).order_by("-created_at").first()
if not latest_report:
logger.info("No completed PDF report found to send")
return {"status": "skipped", "reason": "no_pdf_report"}
# Get executive users
executives = User.objects.filter(
groups__name="Executive",
is_active=True,
).distinct()
if not executives.exists():
logger.info("No executive users found to send report to")
return {"status": "skipped", "reason": "no_executives"}
# Prepare email
subject = f"Executive Summary Report - {latest_report.get_report_type_display()}"
body = f"""Dear Executive,
Please find attached the {latest_report.get_report_type_display().lower()} summary report for the period {latest_report.start_date} to {latest_report.end_date}.
Key Highlights:
{chr(10).join(f"• {highlight}" for highlight in latest_report.highlights_en[:5])}
Key Concerns:
{chr(10).join(f"• {concern}" for concern in latest_report.concerns_en[:5])}
This report was generated automatically by the PX360 AI system.
Best regards,
PX360 Team
"""
# Send emails
emails_sent = 0
for executive in executives:
if executive.email:
try:
email = EmailMessage(
subject=subject,
body=body,
to=[executive.email],
)
email.attach_file(latest_report.pdf_file.path)
email.send()
emails_sent += 1
logger.info(f"Report sent to {executive.email}")
except Exception as e:
logger.error(f"Failed to send report to {executive.email}: {str(e)}")
logger.info(f"PDF reports sent to {emails_sent} executives")
return {
"status": "success",
"emails_sent": emails_sent,
"report_id": str(latest_report.id),
}
except Exception as e:
logger.error(f"Failed to send executive PDF reports: {str(e)}", exc_info=True)
return {"status": "error", "error": str(e)}
```
## Assistant
Now let me register the executive_summary URLs in the main config and add the Celery beat schedule.
## Assistant
Now let me register the executive_summary URLs in the main config and add the Celery beat schedule:
### Tool: Edit: config/urls.py: from django.contrib import adm... => from django.contrib import adm...
**Status**: failed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/config/urls.py",
"new_string": "from django.contrib import admin\nfrom django.urls import path, include\nfrom django.conf import settings\nfrom django.conf.urls.static import static\nfrom drf_spectacular.views import (\n SpectacularAPIView,\n SpectacularRedocView,\n SpectacularSwaggerView,\n)\nfrom apps.dashboard.views import CommandCenterView\nfrom rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView\n\nurlpatterns = [\n path(\"admin/\", admin.site.urls),\n path(\"\", CommandCenterView.as_view(), name=\"home\"), # Root URL now shows Command Center\n path(\"dashboard/\", include(\"apps.dashboard.urls\")),\n path(\"accounts/\", include(\"apps.accounts.urls\")),\n path(\"surveys/\", include(\"apps.surveys.urls\")),\n path(\"complaints/\", include(\"apps.complaints.urls\")),\n path(\"physicians/\", include(\"apps.physicians.urls\")),\n path(\"journeys/\", include(\"apps.journeys.urls\")),\n path(\"call-center/\", include(\"apps.callcenter.urls\")),\n path(\"integrations/\", include(\"apps.integrations.urls\")),\n path(\"notifications/\", include(\"apps.notifications.urls\")),\n path(\"social/\", include(\"apps.social.urls\")),\n path(\"projects/\", include(\"apps.projects.urls\")),\n path(\"actions/\", include(\"apps.px_action_center.urls\")),\n path(\"executive/\", include(\"apps.executive_summary.urls\")), # Executive Summary URLs\n path(\"organizations/\", include(\"apps.organizations.urls\")),\n path(\"feedback/\", include(\"apps.feedback.urls\")),\n path(\"analytics/\", include(\"apps.analytics.urls\")),\n path(\"api/auth/token/\", TokenObtainPairView.as_view(), name=\"token_obtain_pair\"),\n path(\"api/auth/token/refresh/\", TokenRefreshView.as_view(), name=\"token_refresh\"),\n path(\"api/schema/\", SpectacularAPIView.as_view(), name=\"schema\"),\n path(\"api/docs/\", SpectacularSwaggerView.as_view(url_name=\"schema\"), name=\"swagger-ui\"),\n path(\"api/redoc/\", SpectacularRedocView.as_view(url_name=\"schema\"), name=\"redoc\"),\n path(\"ai-engine/\", include(\"apps.ai_engine.urls\")),\n path(\"appreciations/\", include(\"apps.appreciation.urls\")),\n path(\"observations/\", include(\"apps.observations.urls\")),\n path(\"px-sources/\", include(\"apps.px_sources.urls\")),\n path(\"references/\", include(\"apps.references.urls\")),\n path(\"standards/\", include(\"apps.standards.urls\")),\n path(\"simulator/\", include(\"apps.simulator.urls\")),\n path(\"reports/\", include(\"apps.reports.urls\")),\n path(\"rca/\", include(\"apps.rca.urls\")),\n path(\"health/\", include(\"apps.core.urls\")),\n]\n\nif settings.DEBUG:\n urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)\n urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)\n urlpatterns.append(path(\"__debug__/\", include(\"debug_toolbar.urls\")))",
"old_string": "from django.contrib import admin\nfrom django.urls import path, include\nfrom django.conf import settings\nfrom django.conf.urls.static import static\nfrom drf_spectacular.views import (\n SpectacularAPIView,\n SpectacularRedocView,\n SpectacularSwaggerView,\n)\nfrom apps.dashboard.views import CommandCenterView\nfrom rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView\n\nurlpatterns = [\n path(\"admin/\", admin.site.urls),\n path(\"\", CommandCenterView.as_view(), name=\"home\"), # Root URL now shows Command Center\n path(\"dashboard/\", include(\"apps.dashboard.urls\")),\n path(\"accounts/\", include(\"apps.accounts.urls\")),\n path(\"surveys/\", include(\"apps.surveys.urls\")),\n path(\"complaints/\", include(\"apps.complaints.urls\")),\n path(\"physicians/\", include(\"apps.physicians.urls\")),\n path(\"journeys/\", include(\"apps.journeys.urls\")),\n path(\"call-center/\", include(\"apps.callcenter.urls\")),\n path(\"integrations/\", include(\"apps.integrations.urls\")),\n path(\"notifications/\", include(\"apps.notifications.urls\")),\n path(\"social/\", include(\"apps.social.urls\")),\n path(\"projects/\", include(\"apps.projects.urls\")),\n path(\"actions/\", include(\"apps.px_action_center.urls\")),\n path(\"organizations/\", include(\"apps.organizations.urls\")),\n path(\"feedback/\", include(\"apps.feedback.urls\")),\n path(\"analytics/\", include(\"apps.analytics.urls\")),\n path(\"api/auth/token/\", TokenObtainPairView.as_view(), name=\"token_obtain_pair\"),\n path(\"api/auth/token/refresh/\", TokenRefreshView.as_view(), name=\"token_refresh\"),\n path(\"api/schema/\", SpectacularAPIView.as_view(), name=\"schema\"),\n path(\"api/docs/\", SpectacularSwaggerView.as_view(url_name=\"schema\"), name=\"swagger-ui\"),\n path(\"api/redoc/\", SpectacularRedocView.as_view(url_name=\"schema\"), name=\"redoc\"),\n path(\"ai-engine/\", include(\"apps.ai_engine.urls\")),\n path(\"appreciations/\", include(\"apps.appreciation.urls\")),\n path(\"observations/\", include(\"apps.observations.urls\")),\n path(\"px-sources/\", include(\"apps.px_sources.urls\")),\n path(\"references/\", include(\"apps.references.urls\")),\n path(\"standards/\", include(\"apps.standards.urls\")),\n path(\"simulator/\", include(\"apps.simulator.urls\")),\n path(\"reports/\", include(\"apps.reports.urls\")),\n path(\"rca/\", include(\"apps.rca.urls\")),\n path(\"health/\", include(\"apps.core.urls\")),\n]\n\nif settings.DEBUG:\n urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)\n urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)\n urlpatterns.append(path(\"__debug__/\", include(\"debug_toolbar.urls\")))"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/config/urls.py`
**Output:**
```python
Failed to edit, 0 occurrences found for old_string in /home/ismail/projects/HH/config/urls.py. No edits made. The exact text in old_string was not found. Ensure you're not escaping content incorrectly and check whitespace, indentation, and context. Use read_file tool to verify.
```
## Assistant
The edit failed because the old_string didn't match the actual file content, so I need to read the file first to see its exact current content.
### Tool: ReadFile: config/urls.py (first 50 lines)
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/config/urls.py",
"limit": 50
}
```
**Affected Files:**
- `/home/ismail/projects/HH/config/urls.py`
**Output:**
```python
Showing lines 1-50 of 72 total lines.
---
"""
URL configuration for PX360 project.
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from django.conf.urls.i18n import i18n_patterns
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from apps.organizations.views import api_patient_search, api_staff_search
urlpatterns = [
# Language switching
path("i18n/", include("django.conf.urls.i18n")),
# Admin
path("admin/", admin.site.urls),
# Dashboard
path("", include("apps.dashboard.urls")),
# Health check endpoint
path("health/", include("apps.core.urls", namespace="health")),
# Core pages (public submissions, hospital selection)
path("core/", include("apps.core.urls", namespace="core")),
# UI Pages
path("complaints/", include("apps.complaints.urls")),
path("inquiries/", include("apps.complaints.urls_inquiries")),
path("physicians/", include("apps.physicians.urls", namespace="physicians")),
path("feedback/", include("apps.feedback.urls")),
path("actions/", include("apps.px_action_center.urls")),
path("accounts/", include("apps.accounts.urls", namespace="accounts")),
path("journeys/", include("apps.journeys.urls")),
path("surveys/", include("apps.surveys.urls")),
path("social/", include("apps.social.urls")),
path("callcenter/", include("apps.callcenter.urls")),
path("analytics/", include("apps.analytics.urls")),
path("organizations/", include("apps.organizations.urls")),
path("projects/", include("apps.projects.urls")),
path("config/", include("apps.core.config_urls")),
path("ai-engine/", include("apps.ai_engine.urls")),
path("appreciation/", include("apps.appreciation.urls", namespace="appreciation")),
path("notifications/", include("apps.notifications.urls", namespace="notifications")),
path("observations/", include("apps.observations.urls")),
path("px-sources/", include("apps.px_sources.urls")),
path("references/", include("apps.references.urls", namespace="references")),
path("standards/", include("apps.standards.urls")),
path("reports/", include("apps.reports.urls", namespace="reports")),
path("rca/", include("apps.rca.urls", namespace="rca")),
# API endpoints
path("api/auth/", include("apps.accounts.urls", namespace="api_auth")),
path("api/physicians/", include("apps.physicians.urls", namespace="api_physicians")),
```
## Assistant
Now I can see the actual URL structure - I need to insert the executive_summary URLs in the right place among the other app URL includes.
### Tool: Edit: config/urls.py: path("actions/", include("... => path("actions/", include("...
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/config/urls.py",
"new_string": " path(\"actions/\", include(\"apps.px_action_center.urls\")),\n path(\"executive/\", include(\"apps.executive_summary.urls\")),\n path(\"accounts/\", include(\"apps.accounts.urls\", namespace=\"accounts\")),",
"old_string": " path(\"actions/\", include(\"apps.px_action_center.urls\")),\n path(\"accounts/\", include(\"apps.accounts.urls\", namespace=\"accounts\")),"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/config/urls.py`
**Output:**
*Diff for `urls.py`:*
```diff
"""
URL configuration for PX360 project.
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from django.conf.urls.i18n import i18n_patterns
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from apps.organizations.views import api_patient_search, api_staff_search
urlpatterns = [
# Language switching
path("i18n/", include("django.conf.urls.i18n")),
# Admin
path("admin/", admin.site.urls),
# Dashboard
path("", include("apps.dashboard.urls")),
# Health check endpoint
path("health/", include("apps.core.urls", namespace="health")),
# Core pages (public submissions, hospital selection)
path("core/", include("apps.core.urls", namespace="core")),
# UI Pages
path("complaints/", include("apps.complaints.urls")),
path("inquiries/", include("apps.complaints.urls_inquiries")),
path("physicians/", include("apps.physicians.urls", namespace="physicians")),
path("feedback/", include("apps.feedback.urls")),
path("actions/", include("apps.px_action_center.urls")),
path("executive/", include("apps.executive_summary.urls")),
path("accounts/", include("apps.accounts.urls", namespace="accounts")),
path("journeys/", include("apps.journeys.urls")),
path("surveys/", include("apps.surveys.urls")),
path("social/", include("apps.social.urls")),
path("callcenter/", include("apps.callcenter.urls")),
path("analytics/", include("apps.analytics.urls")),
path("organizations/", include("apps.organizations.urls")),
path("projects/", include("apps.projects.urls")),
path("config/", include("apps.core.config_urls")),
path("ai-engine/", include("apps.ai_engine.urls")),
path("appreciation/", include("apps.appreciation.urls", namespace="appreciation")),
path("notifications/", include("apps.notifications.urls", namespace="notifications")),
path("observations/", include("apps.observations.urls")),
path("px-sources/", include("apps.px_sources.urls")),
path("references/", include("apps.references.urls", namespace="references")),
path("standards/", include("apps.standards.urls")),
path("reports/", include("apps.reports.urls", namespace="reports")),
path("rca/", include("apps.rca.urls", namespace="rca")),
# API endpoints
path("api/auth/", include("apps.accounts.urls", namespace="api_auth")),
path("api/physicians/", include("apps.physicians.urls", namespace="api_physicians")),
path("api/integrations/", include("apps.integrations.urls")),
path("api/notifications/", include("apps.notifications.urls", namespace="api_notifications")),
path("api/v1/appreciation/", include("apps.appreciation.urls", namespace="api_appreciation")),
path("api/simulator/", include("apps.simulator.urls", namespace="api_simulator")),
path("api/patients/search/", api_patient_search, name="api_patient_search"),
path("api/staffs/search/", api_staff_search, name="api_staff_search"),
# OpenAPI/Swagger documentation
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# Customize admin site
admin.site.site_header = "PX360 Administration"
admin.site.site_title = "PX360 Admin Portal"
admin.site.index_title = "Welcome to PX360 Administration"
```
## Assistant
After registering the URLs, I need to add the Celery beat schedule for the executive summary tasks, so I should first find where the celery beat schedule is configured.
## Assistant
Perfect! Now let me check the celery beat schedule configuration and add the executive summary tasks:
### Tool: Grep: 'CELERY_BEAT_SCHEDULE|celery.*schedule'
**Status**: completed
**Input:**
```json
{
"pattern": "CELERY_BEAT_SCHEDULE|celery.*schedule"
}
```
**Output:**
```
Found 25 matches for pattern "CELERY_BEAT_SCHEDULE|celery.*schedule" in the workspace directory:
---
/home/ismail/projects/HH/docker-compose.yml:75: command: celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
/home/ismail/projects/HH/PX360/settings.py:142:# Django Celery Beat Scheduler
/home/ismail/projects/HH/PX360/settings.py:143:CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
/home/ismail/projects/HH/README.md:97:- Celery beat scheduler
/home/ismail/projects/HH/config/celery_scheduler.py:2:Custom Celery Beat scheduler to fix Python 3.12 zoneinfo compatibility issue.
/home/ismail/projects/HH/config/celery_scheduler.py:57: This class is referenced in CELERY_BEAT_SCHEDULER setting and lazily imports
/home/ismail/projects/HH/config/celery_scheduler.py:69: from django_celery_beat.schedulers import DatabaseScheduler
/home/ismail/projects/HH/config/settings/base.py:259:# Celery Beat Schedule
/home/ismail/projects/HH/config/settings/base.py:260:CELERY_BEAT_SCHEDULER = "config.celery_scheduler:PatchedDatabaseScheduler"
/home/ismail/projects/HH/config/celery.py:8:from celery.schedules import crontab
/home/ismail/projects/HH/config/celery.py:15:from config.celery_scheduler import apply_tzcrontab_patch
/home/ismail/projects/HH/config/celery.py:30:# Celery Beat schedule for periodic tasks
/home/ismail/projects/HH/docs/SURVEY_TRACKING_IMPLEMENTATION.md:283:# Via Celery (scheduled)
/home/ismail/projects/HH/docs/SURVEY_TRACKING_IMPLEMENTATION.md:295:**Celery Beat Schedule:**
/home/ismail/projects/HH/docs/SETUP_GUIDE.md:247:# Start Celery beat (for scheduled tasks)
/home/ismail/projects/HH/docs/SLA_TESTING_PLAN.md:405:1. Configure Celery Beat to run tasks on schedule
/home/ismail/projects/HH/docs/SLA_TESTING_PLAN.md:432:- [ ] Celery Beat scheduler running
/home/ismail/projects/HH/docs/SLA_TESTING_PLAN.md:505:- Check: Celery Beat scheduler running
/home/ismail/projects/HH/docs/SLA_SYSTEM_SETUP_AND_TESTING_GUIDE.md:463:celery -A config inspect scheduled
/home/ismail/projects/HH/docs/SLA_SYSTEM_SETUP_AND_TESTING_GUIDE.md:519:from celery.schedules import crontab
/home/ismail/projects/HH/docs/SLA_SYSTEM_SETUP_AND_TESTING_GUIDE.md:539:# Start Celery Beat scheduler
/home/ismail/projects/HH/docs/SLA_SYSTEM_SETUP_AND_TESTING_GUIDE.md:540:celery -A config beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler
/home/ismail/projects/HH/docs/ARCHITECTURE.md:381:- celery-beat (Scheduler)
/home/ismail/projects/HH/docs/SLA_TESTING_GUIDE.md:227:### Celery Beat Schedule
/home/ismail/projects/HH/docs/SLA_TESTING_GUIDE.md:233:from celery.schedules import crontab
```
## Assistant
Let me check the Celery configuration file to understand how scheduled tasks are defined so I can add the executive summary tasks.
### Tool: ReadFile: config/celery.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/config/celery.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/config/celery.py`
**Output:**
```python
"""
Celery configuration for PX360 project.
"""
import os
from celery import Celery
from celery.schedules import crontab
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev")
# Apply zoneinfo compatibility patch for django-celery-beat with Python 3.12+
# This must be done before Celery app is created
from config.celery_scheduler import apply_tzcrontab_patch
apply_tzcrontab_patch()
app = Celery("px360")
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
# Celery Beat schedule for periodic tasks
app.conf.beat_schedule = {
# Fetch patient data from HIS every 25 minutes
"fetch-his-surveys": {
"task": "apps.integrations.tasks.fetch_his_surveys",
"schedule": crontab(minute="*/25"),
"options": {
"expires": 1500,
},
},
# TEST TASK - Fetch from JSON file (uncomment for testing, remove when done)
# 'test-fetch-his-surveys-from-json': {
# 'task': 'apps.integrations.tasks.test_fetch_his_surveys_from_json',
# 'schedule': crontab(minute='*/5'), # Every 5 minutes
# },
# Send pending scheduled surveys every 10 minutes
# "send-pending-scheduled-surveys": {
# "task": "apps.surveys.tasks.send_pending_scheduled_surveys",
# "schedule": crontab(minute="*/10"), # Every 10 minutes
# },
# Check for overdue complaints every 15 minutes
"check-overdue-complaints": {
"task": "apps.complaints.tasks.check_overdue_complaints",
"schedule": crontab(minute="*/15"),
},
# Check for overdue actions every 15 minutes
"check-overdue-actions": {
"task": "apps.px_action_center.tasks.check_overdue_actions",
"schedule": crontab(minute="*/15"),
},
# Send SLA reminders every hour
"send-sla-reminders": {
"task": "apps.complaints.tasks.send_sla_reminders",
"schedule": crontab(minute=0), # Every hour at minute 0
},
# Check for overdue explanation requests every 15 minutes
"check-overdue-explanation-requests": {
"task": "apps.complaints.tasks.check_overdue_explanation_requests",
"schedule": crontab(minute="*/15"),
},
# Send explanation reminders every hour
"send-explanation-reminders": {
"task": "apps.complaints.tasks.send_explanation_reminders",
"schedule": crontab(minute=0), # Every hour at minute 0
},
# Check for overdue inquiries every 15 minutes
"check-overdue-inquiries": {
"task": "apps.complaints.tasks.check_overdue_inquiries",
"schedule": crontab(minute="*/15"),
},
# Send inquiry SLA reminders every hour
"send-inquiry-sla-reminders": {
"task": "apps.complaints.tasks.send_inquiry_sla_reminders",
"schedule": crontab(minute=0), # Every hour at minute 0
},
# Check for overdue observations every 15 minutes
"check-overdue-observations": {
"task": "apps.observations.tasks.check_overdue_observations",
"schedule": crontab(minute="*/15"),
},
# Send observation SLA reminders every hour
"send-observation-sla-reminders": {
"task": "apps.observations.tasks.send_observation_sla_reminders",
"schedule": crontab(minute=0), # Every hour at minute 0
},
# Send onboarding reminders every hour
"send-onboarding-reminders": {
"task": "apps.accounts.tasks.send_onboarding_reminders",
"schedule": crontab(minute=30), # Every hour at minute 30
},
# Clean up expired invitations daily at midnight
"cleanup-expired-invitations": {
"task": "apps.accounts.tasks.cleanup_expired_invitations",
"schedule": crontab(hour=0, minute=0), # Daily at midnight
},
# Calculate daily KPIs at 1 AM
"calculate-daily-kpis": {
"task": "apps.analytics.tasks.calculate_daily_kpis",
"schedule": crontab(hour=1, minute=0),
},
# Fetch doctor ratings from HIS on the 1st of each month at 1 AM (before aggregation)
"fetch-his-doctor-ratings-monthly": {
"task": "apps.physicians.tasks.fetch_his_doctor_ratings_monthly",
"schedule": crontab(hour=1, minute=0, day_of_month=1),
},
# Fetch doctor ratings from HIS daily at 1:30 AM (yesterday's ratings)
"fetch-his-doctor-ratings-daily": {
"task": "apps.physicians.tasks.fetch_his_doctor_ratings_daily",
"schedule": crontab(hour=1, minute=30),
},
# Calculate physician monthly ratings on the 1st of each month at 2 AM
"calculate-physician-ratings": {
"task": "apps.physicians.tasks.calculate_monthly_ratings",
"schedule": crontab(hour=2, minute=0, day_of_month=1),
},
# Scraping schedules
"scrape-youtube-hourly": {
"task": "social.tasks.scrape_youtube_comments",
"schedule": 3600.0, # Every hour (in seconds)
},
"scrape-facebook-every-6-hours": {
"task": "social.tasks.scrape_facebook_comments",
"schedule": 6 * 3600.0, # Every 6 hours
},
"scrape-instagram-daily": {
"task": "social.tasks.scrape_instagram_comments",
"schedule": crontab(hour=8, minute=0), # Daily at 8:00 AM
},
"scrape-twitter-every-2-hours": {
"task": "social.tasks.scrape_twitter_comments",
"schedule": 2 * 3600.0, # Every 2 hours
},
"scrape-linkedin-daily": {
"task": "social.tasks.scrape_linkedin_comments",
"schedule": crontab(hour=9, minute=0), # Daily at 9:00 AM
},
"scrape-google-reviews-daily": {
"task": "social.tasks.scrape_google_reviews",
"schedule": crontab(hour=10, minute=0), # Daily at 10:00 AM
},
# Commented out - individual platform tasks provide sufficient coverage
# 'scrape-all-platforms-daily': {
# 'task': 'social.tasks.scrape_all_platforms',
# 'schedule': crontab(hour=2, minute=0), # Daily at 2:00 AM
# },
# Analysis schedules
"analyze-comments-fallback": {
"task": "social.tasks.analyze_pending_comments",
"schedule": 30 * 60.0, # Every 30 minutes
"kwargs": {"limit": 100},
},
# Process scheduled reports every 15 minutes
"process-scheduled-reports": {
"task": "apps.reports.tasks.process_scheduled_reports",
"schedule": crontab(minute="*/15"),
},
# Schedule observation monthly follow-ups daily at 6 AM
"schedule-observation-monthly-followups": {
"task": "apps.observations.tasks.schedule_monthly_followups",
"schedule": crontab(hour=6, minute=0),
},
# Process observation monthly follow-ups daily at 7 AM
"process-observation-monthly-followups": {
"task": "apps.observations.tasks.process_monthly_followups",
"schedule": crontab(hour=7, minute=0),
},
# Analyze feedback sentiment every 30 minutes
"analyze-feedback-sentiment": {
"task": "apps.feedback.tasks.process_pending_sentiment_analysis",
"schedule": crontab(minute="*/30"),
},
# Analyze survey text responses every 30 minutes
"analyze-survey-text-responses": {
"task": "apps.surveys.tasks.process_survey_text_analysis",
"schedule": crontab(minute="*/30"),
},
# Pre-compute analytics dashboard cache daily at 3 AM
"precompute-analytics-cache": {
"task": "apps.analytics.tasks.precompute_dashboard_cache_task",
"schedule": crontab(hour=3, minute=0),
},
# Generate AI executive summary daily at 6 AM
"generate-daily-executive-summary": {
"task": "apps.analytics.tasks.generate_executive_summary_task",
"schedule": crontab(hour=6, minute=0),
},
# Generate AI action recommendations daily at 6:30 AM
"generate-daily-action-recommendations": {
"task": "apps.analytics.tasks.generate_action_recommendations_task",
"schedule": crontab(hour=6, minute=30),
},
# Send weekly PX digest email every Monday at 8 AM
"send-weekly-px-digest": {
"task": "apps.analytics.tasks_digest.send_weekly_digest_task",
"schedule": crontab(hour=8, minute=0, day_of_week=1), # Monday
},
# Send monthly PX digest email on 1st of each month at 8 AM
"send-monthly-px-digest": {
"task": "apps.analytics.tasks_digest.send_monthly_digest_task",
"schedule": crontab(hour=8, minute=0, day_of_month=1),
},
}
@app.task(bind=True, ignore_result=True)
def debug_task(self):
"""Debug task to test Celery is working."""
print(f"Request: {self.request!r}")
```
## Assistant
Now I need to add the executive summary Celery tasks to the beat schedule configuration.
## Assistant
Perfect! Now let me add the executive summary tasks to the Celery beat schedule:
### Tool: Edit: config/celery.py: # Send monthly PX digest e... => # Send monthly PX digest e...
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/config/celery.py",
"new_string": " # Send monthly PX digest email on 1st of each month at 8 AM\n \"send-monthly-px-digest\": {\n \"task\": \"apps.analytics.tasks_digest.send_monthly_digest_task\",\n \"schedule\": crontab(hour=8, minute=0, day_of_month=1),\n },\n # Executive Summary Tasks\n # Calculate daily metrics at 1 AM\n \"calculate-daily-executive-metrics\": {\n \"task\": \"apps.executive_summary.tasks.calculate_daily_metrics\",\n \"schedule\": crontab(hour=1, minute=0),\n },\n # Generate predictive insights every 6 hours\n \"generate-predictive-insights\": {\n \"task\": \"apps.executive_summary.tasks.generate_predictive_insights\",\n \"schedule\": crontab(hour=\"*/6\"), # Every 6 hours\n },\n # Generate AI recommendations daily at 3 AM\n \"generate-ai-recommendations\": {\n \"task\": \"apps.executive_summary.tasks.generate_ai_recommendations\",\n \"schedule\": crontab(hour=3, minute=0),\n },\n # Generate weekly executive summary every Monday at 6 AM\n \"generate-weekly-executive-summary\": {\n \"task\": \"apps.executive_summary.tasks.generate_weekly_executive_summary\",\n \"schedule\": crontab(hour=6, minute=0, day_of_week=1), # Monday\n },\n # Generate monthly executive summary on 1st at 7 AM\n \"generate-monthly-executive-summary\": {\n \"task\": \"apps.executive_summary.tasks.generate_monthly_executive_summary\",\n \"schedule\": crontab(hour=7, minute=0, day_of_month=1),\n },\n # Send executive PDF reports every Monday at 9 AM\n \"send-executive-pdf-report\": {\n \"task\": \"apps.executive_summary.tasks.send_executive_pdf_report\",\n \"schedule\": crontab(hour=9, minute=0, day_of_week=1), # Monday\n },\n}",
"old_string": " # Send monthly PX digest email on 1st of each month at 8 AM\n \"send-monthly-px-digest\": {\n \"task\": \"apps.analytics.tasks_digest.send_monthly_digest_task\",\n \"schedule\": crontab(hour=8, minute=0, day_of_month=1),\n },\n}"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/config/celery.py`
**Output:**
*Diff for `celery.py`:*
```diff
"""
Celery configuration for PX360 project.
"""
import os
from celery import Celery
from celery.schedules import crontab
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev")
# Apply zoneinfo compatibility patch for django-celery-beat with Python 3.12+
# This must be done before Celery app is created
from config.celery_scheduler import apply_tzcrontab_patch
apply_tzcrontab_patch()
app = Celery("px360")
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
# Celery Beat schedule for periodic tasks
app.conf.beat_schedule = {
# Fetch patient data from HIS every 25 minutes
"fetch-his-surveys": {
"task": "apps.integrations.tasks.fetch_his_surveys",
"schedule": crontab(minute="*/25"),
"options": {
"expires": 1500,
},
},
# TEST TASK - Fetch from JSON file (uncomment for testing, remove when done)
# 'test-fetch-his-surveys-from-json': {
# 'task': 'apps.integrations.tasks.test_fetch_his_surveys_from_json',
# 'schedule': crontab(minute='*/5'), # Every 5 minutes
# },
# Send pending scheduled surveys every 10 minutes
# "send-pending-scheduled-surveys": {
# "task": "apps.surveys.tasks.send_pending_scheduled_surveys",
# "schedule": crontab(minute="*/10"), # Every 10 minutes
# },
# Check for overdue complaints every 15 minutes
"check-overdue-complaints": {
"task": "apps.complaints.tasks.check_overdue_complaints",
"schedule": crontab(minute="*/15"),
},
# Check for overdue actions every 15 minutes
"check-overdue-actions": {
"task": "apps.px_action_center.tasks.check_overdue_actions",
"schedule": crontab(minute="*/15"),
},
# Send SLA reminders every hour
"send-sla-reminders": {
"task": "apps.complaints.tasks.send_sla_reminders",
"schedule": crontab(minute=0), # Every hour at minute 0
},
# Check for overdue explanation requests every 15 minutes
"check-overdue-explanation-requests": {
"task": "apps.complaints.tasks.check_overdue_explanation_requests",
"schedule": crontab(minute="*/15"),
},
# Send explanation reminders every hour
"send-explanation-reminders": {
"task": "apps.complaints.tasks.send_explanation_reminders",
"schedule": crontab(minute=0), # Every hour at minute 0
},
# Check for overdue inquiries every 15 minutes
"check-overdue-inquiries": {
"task": "apps.complaints.tasks.check_overdue_inquiries",
"schedule": crontab(minute="*/15"),
},
# Send inquiry SLA reminders every hour
"send-inquiry-sla-reminders": {
"task": "apps.complaints.tasks.send_inquiry_sla_reminders",
"schedule": crontab(minute=0), # Every hour at minute 0
},
# Check for overdue observations every 15 minutes
"check-overdue-observations": {
"task": "apps.observations.tasks.check_overdue_observations",
"schedule": crontab(minute="*/15"),
},
# Send observation SLA reminders every hour
"send-observation-sla-reminders": {
"task": "apps.observations.tasks.send_observation_sla_reminders",
"schedule": crontab(minute=0), # Every hour at minute 0
},
# Send onboarding reminders every hour
"send-onboarding-reminders": {
"task": "apps.accounts.tasks.send_onboarding_reminders",
"schedule": crontab(minute=30), # Every hour at minute 30
},
# Clean up expired invitations daily at midnight
"cleanup-expired-invitations": {
"task": "apps.accounts.tasks.cleanup_expired_invitations",
"schedule": crontab(hour=0, minute=0), # Daily at midnight
},
# Calculate daily KPIs at 1 AM
"calculate-daily-kpis": {
"task": "apps.analytics.tasks.calculate_daily_kpis",
"schedule": crontab(hour=1, minute=0),
},
# Fetch doctor ratings from HIS on the 1st of each month at 1 AM (before aggregation)
"fetch-his-doctor-ratings-monthly": {
"task": "apps.physicians.tasks.fetch_his_doctor_ratings_monthly",
"schedule": crontab(hour=1, minute=0, day_of_month=1),
},
# Fetch doctor ratings from HIS daily at 1:30 AM (yesterday's ratings)
"fetch-his-doctor-ratings-daily": {
"task": "apps.physicians.tasks.fetch_his_doctor_ratings_daily",
"schedule": crontab(hour=1, minute=30),
},
# Calculate physician monthly ratings on the 1st of each month at 2 AM
"calculate-physician-ratings": {
"task": "apps.physicians.tasks.calculate_monthly_ratings",
"schedule": crontab(hour=2, minute=0, day_of_month=1),
},
# Scraping schedules
"scrape-youtube-hourly": {
"task": "social.tasks.scrape_youtube_comments",
"schedule": 3600.0, # Every hour (in seconds)
},
"scrape-facebook-every-6-hours": {
"task": "social.tasks.scrape_facebook_comments",
"schedule": 6 * 3600.0, # Every 6 hours
},
"scrape-instagram-daily": {
"task": "social.tasks.scrape_instagram_comments",
"schedule": crontab(hour=8, minute=0), # Daily at 8:00 AM
},
"scrape-twitter-every-2-hours": {
"task": "social.tasks.scrape_twitter_comments",
"schedule": 2 * 3600.0, # Every 2 hours
},
"scrape-linkedin-daily": {
"task": "social.tasks.scrape_linkedin_comments",
"schedule": crontab(hour=9, minute=0), # Daily at 9:00 AM
},
"scrape-google-reviews-daily": {
"task": "social.tasks.scrape_google_reviews",
"schedule": crontab(hour=10, minute=0), # Daily at 10:00 AM
},
# Commented out - individual platform tasks provide sufficient coverage
# 'scrape-all-platforms-daily': {
# 'task': 'social.tasks.scrape_all_platforms',
# 'schedule': crontab(hour=2, minute=0), # Daily at 2:00 AM
# },
# Analysis schedules
"analyze-comments-fallback": {
"task": "social.tasks.analyze_pending_comments",
"schedule": 30 * 60.0, # Every 30 minutes
"kwargs": {"limit": 100},
},
# Process scheduled reports every 15 minutes
"process-scheduled-reports": {
"task": "apps.reports.tasks.process_scheduled_reports",
"schedule": crontab(minute="*/15"),
},
# Schedule observation monthly follow-ups daily at 6 AM
"schedule-observation-monthly-followups": {
"task": "apps.observations.tasks.schedule_monthly_followups",
"schedule": crontab(hour=6, minute=0),
},
# Process observation monthly follow-ups daily at 7 AM
"process-observation-monthly-followups": {
"task": "apps.observations.tasks.process_monthly_followups",
"schedule": crontab(hour=7, minute=0),
},
# Analyze feedback sentiment every 30 minutes
"analyze-feedback-sentiment": {
"task": "apps.feedback.tasks.process_pending_sentiment_analysis",
"schedule": crontab(minute="*/30"),
},
# Analyze survey text responses every 30 minutes
"analyze-survey-text-responses": {
"task": "apps.surveys.tasks.process_survey_text_analysis",
"schedule": crontab(minute="*/30"),
},
# Pre-compute analytics dashboard cache daily at 3 AM
"precompute-analytics-cache": {
"task": "apps.analytics.tasks.precompute_dashboard_cache_task",
"schedule": crontab(hour=3, minute=0),
},
# Generate AI executive summary daily at 6 AM
"generate-daily-executive-summary": {
"task": "apps.analytics.tasks.generate_executive_summary_task",
"schedule": crontab(hour=6, minute=0),
},
# Generate AI action recommendations daily at 6:30 AM
"generate-daily-action-recommendations": {
"task": "apps.analytics.tasks.generate_action_recommendations_task",
"schedule": crontab(hour=6, minute=30),
},
# Send weekly PX digest email every Monday at 8 AM
"send-weekly-px-digest": {
"task": "apps.analytics.tasks_digest.send_weekly_digest_task",
"schedule": crontab(hour=8, minute=0, day_of_week=1), # Monday
},
# Send monthly PX digest email on 1st of each month at 8 AM
"send-monthly-px-digest": {
"task": "apps.analytics.tasks_digest.send_monthly_digest_task",
"schedule": crontab(hour=8, minute=0, day_of_month=1),
},
# Executive Summary Tasks
# Calculate daily metrics at 1 AM
"calculate-daily-executive-metrics": {
"task": "apps.executive_summary.tasks.calculate_daily_metrics",
"schedule": crontab(hour=1, minute=0),
},
# Generate predictive insights every 6 hours
"generate-predictive-insights": {
"task": "apps.executive_summary.tasks.generate_predictive_insights",
"schedule": crontab(hour="*/6"), # Every 6 hours
},
# Generate AI recommendations daily at 3 AM
"generate-ai-recommendations": {
"task": "apps.executive_summary.tasks.generate_ai_recommendations",
"schedule": crontab(hour=3, minute=0),
},
# Generate weekly executive summary every Monday at 6 AM
"generate-weekly-executive-summary": {
"task": "apps.executive_summary.tasks.generate_weekly_executive_summary",
"schedule": crontab(hour=6, minute=0, day_of_week=1), # Monday
},
# Generate monthly executive summary on 1st at 7 AM
"generate-monthly-executive-summary": {
"task": "apps.executive_summary.tasks.generate_monthly_executive_summary",
"schedule": crontab(hour=7, minute=0, day_of_month=1),
},
# Send executive PDF reports every Monday at 9 AM
"send-executive-pdf-report": {
"task": "apps.executive_summary.tasks.send_executive_pdf_report",
"schedule": crontab(hour=9, minute=0, day_of_week=1), # Monday
},
}
@app.task(bind=True, ignore_result=True)
def debug_task(self):
"""Debug task to test Celery is working."""
print(f"Request: {self.request!r}")
```
## Assistant
After adding all the core files, I need to create database migrations and verify the code compiles correctly.
## Assistant
Excellent! Now let me create the database migrations and run them. Let me also update the todo list:
### Tool: TodoWrite
**Status**: completed
**Output:**
```
- [x] Create Executive role and permissions
- [x] Create executive_summary Django app structure (models, views, services, tasks)
- [x] Build ExecutiveMetric and ExecutiveReport models
- [x] Create ExecutiveSummaryService for data aggregation
- [x] Build Executive Dashboard view with KPI cards and filters
- [x] Create Executive Dashboard template with ApexCharts
- [x] Add URL routing and sidebar navigation
- [x] Implement AI Narrative Service using LiteLLM
- [x] Build Predictive Analytics Service (trend analysis, risk detection)
- [x] Create Recommendation Engine
- [x] Integrate AI-generated briefs into dashboard
- [x] Implement Natural Language Q&A interface
- [x] Build Predictive Insights view and risk scoring
- [x] Create Hospital comparison and leaderboard views
- [x] Implement PDF report generation with WeasyPrint
- [x] Create Celery scheduled tasks for automated reports
- [-] Build mobile-optimized responsive view
- [x] Add email delivery system for scheduled reports
- [ ] Run migrations and verify setup
- [ ] Create initial data migration for Executive role
```
## Assistant
Need to run migrations to create the database tables for the new executive_summary models and verify the setup works.
## Assistant
Now let me run migrations and create the initial data migration for the Executive role:
### Tool: Shell: cd /home/ismail/projects/HH &amp;&amp; uv run python manage.py makemigrations executive_summary (Create migrations for executive_summary app)
**Status**: completed
**Input:**
```json
{
"command": "cd /home/ismail/projects/HH && uv run python manage.py makemigrations executive_summary",
"description": "Create migrations for executive_summary app"
}
```
**Output:**
```
Command: cd /home/ismail/projects/HH && uv run python manage.py makemigrations executive_summary
Directory: (root)
Output: Traceback (most recent call last):
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/apps/config.py", line 210, in create
app_module = import_module(app_name)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1324, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'executive_summary'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/ismail/projects/HH/manage.py", line 22, in <module>
main()
File "/home/ismail/projects/HH/manage.py", line 18, in main
execute_from_command_line(sys.argv)
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/core/management/__init__.py", line 443, in execute_from_command_line
utility.execute()
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/core/management/__init__.py", line 417, in execute
django.setup()
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/__init__.py", line 24, in setup
apps.populate(settings.INSTALLED_APPS)
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/apps/registry.py", line 91, in populate
app_config = AppConfig.create(entry)
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/apps/config.py", line 212, in create
raise ImproperlyConfigured(
django.core.exceptions.ImproperlyConfigured: Cannot import 'executive_summary'. Check that 'apps.executive_summary.apps.ExecutiveSummaryConfig.name' is correct.
Error: (none)
Exit Code: 1
Signal: 0
Process Group PGID: 297873
```
## Assistant
The migration command failed because the app config name needs to be verified and corrected.
## Assistant
I need to fix the app configuration. Let me check the apps.py file:
### Tool: ReadFile: apps/executive_summary/apps.py
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/executive_summary/apps.py"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/apps.py`
**Output:**
```python
from django.apps import AppConfig
class ExecutiveSummaryConfig(AppConfig):
name = 'executive_summary'
```
## Assistant
The app config name needs to be corrected to 'apps.executive_summary' to match the INSTALLED_APPS entry.
### Tool: Edit: apps/executive_summary/apps.py: from django.apps import AppCon... => from django.apps import AppCon...
**Status**: completed
**Input:**
```json
{
"file_path": "/home/ismail/projects/HH/apps/executive_summary/apps.py",
"new_string": "from django.apps import AppConfig\n\n\nclass ExecutiveSummaryConfig(AppConfig):\n default_auto_field = 'django.db.models.BigAutoField'\n name = 'apps.executive_summary'\n verbose_name = 'Executive Summary'",
"old_string": "from django.apps import AppConfig\n\n\nclass ExecutiveSummaryConfig(AppConfig):\n name = 'executive_summary'"
}
```
**Affected Files:**
- `/home/ismail/projects/HH/apps/executive_summary/apps.py`
**Output:**
*Diff for `apps.py`:*
```diff
from django.apps import AppConfig
class ExecutiveSummaryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.executive_summary'
verbose_name = 'Executive Summary'
```
## Assistant
After fixing the app configuration, I need to retry the migration command to verify the fix works.
## Assistant
Now let me run migrations again:
### Tool: Shell: cd /home/ismail/projects/HH &amp;&amp; uv run python manage.py makemigrations executive_summary (Create migrations for executive_summary app)
**Status**: completed
**Input:**
```json
{
"command": "cd /home/ismail/projects/HH && uv run python manage.py makemigrations executive_summary",
"description": "Create migrations for executive_summary app"
}
```
**Output:**
```
Command: cd /home/ismail/projects/HH && uv run python manage.py makemigrations executive_summary
Directory: (root)
Output: Migrations for 'executive_summary':
apps/executive_summary/migrations/0001_initial.py
+ Create model ExecutiveReport
+ Create model PredictiveInsight
+ Create model AIRecommendation
+ Create model ExecutiveMetric
+ Create index executive_s_status_35974a_idx on field(s) status, severity, -created_at of model predictiveinsight
+ Create index executive_s_hospita_18595e_idx on field(s) hospital, status of model predictiveinsight
+ Create index executive_s_status_f2a1e6_idx on field(s) status, priority, -created_at of model airecommendation
+ Create index executive_s_hospita_322e48_idx on field(s) hospital, status of model airecommendation
Error: (none)
Exit Code: 0
Signal: 0
Process Group PGID: 298316
```
## Assistant
Migrations created successfully, now need to apply them to the database to create the tables.
## Assistant
Perfect! Now let me apply the migrations:
### Tool: Shell: cd /home/ismail/projects/HH &amp;&amp; uv run python manage.py migrate executive_summary (Apply executive_summary migrations)
**Status**: completed
**Input:**
```json
{
"command": "cd /home/ismail/projects/HH && uv run python manage.py migrate executive_summary",
"description": "Apply executive_summary migrations"
}
```
**Output:**
```
Command: cd /home/ismail/projects/HH && uv run python manage.py migrate executive_summary
Directory: (root)
Output: Operations to perform:
Apply all migrations: executive_summary
Running migrations:
Applying executive_summary.0001_initial... OK
Traceback (most recent call last):
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 105, in _execute
return self.cursor.execute(sql, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "django_content_type_pkey"
DETAIL: Key (id)=(156) already exists.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/ismail/projects/HH/manage.py", line 22, in <module>
main()
File "/home/ismail/projects/HH/manage.py", line 18, in main
execute_from_command_line(sys.argv)
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/core/management/__init__.py", line 443, in execute_from_command_line
utility.execute()
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/core/management/__init__.py", line 437, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/core/management/base.py", line 420, in run_from_argv
self.execute(*args, **cmd_options)
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/core/management/base.py", line 464, in execute
output = self.handle(*args, **options)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/core/management/base.py", line 111, in wrapper
res = handle_func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/core/management/commands/migrate.py", line 381, in handle
emit_post_migrate_signal(
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/core/management/sql.py", line 52, in emit_post_migrate_signal
models.signals.post_migrate.send(
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/dispatch/dispatcher.py", line 209, in send
response = receiver(signal=self, sender=sender, **named)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/contrib/auth/management/__init__.py", line 59, in create_permissions
create_contenttypes(
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/contrib/contenttypes/management/__init__.py", line 132, in create_contenttypes
ContentType.objects.using(using).bulk_create(cts)
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 833, in bulk_create
returned_columns = self._batched_insert(
^^^^^^^^^^^^^^^^^^^^^
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 1956, in _batched_insert
self._insert(
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 1918, in _insert
return query.get_compiler(using=using).execute_sql(returning_fields)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1925, in execute_sql
cursor.execute(sql, params)
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 122, in execute
return super().execute(sql, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 79, in execute
return self._execute_with_wrappers(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 92, in _execute_with_wrappers
return executor(sql, params, many, context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 100, in _execute
with self.db.wrap_database_errors:
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/utils.py", line 94, in __exit__
raise dj_exc_value.with_traceback(traceback) from exc_value
File "/home/ismail/projects/HH/.venv/lib/python3.12/site-packages/django/db/backends/utils.py", line 105, in _execute
return self.cursor.execute(sql, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
django.db.utils.IntegrityError: duplicate key value violates unique constraint "django_content_type_pkey"
DETAIL: Key (id)=(156) already exists.
Error: (none)
Exit Code: 1
Signal: 0
Process Group PGID: 298666
```