added gitignore file

This commit is contained in:
Marwan Alwali 2025-10-06 15:25:37 +03:00
parent 6b3916abaf
commit 6e643e1cac
157 changed files with 145726 additions and 4987 deletions

BIN
.DS_Store vendored

Binary file not shown.

172
.gitignore vendored Normal file
View File

@ -0,0 +1,172 @@
# Django #
*.log
*.pot
*.pyc
__pycache__
**/*__pycache__
**/*/__pycache__
hospital_management_system_v4/__pycache__
db.sqlite
dbtest.sqlite3
db.sqlite3
db.sqlite3.backup
db.sqlite*
new.sqlite3
*.sqlite3
media
#car*.json
hospital_management/settings.py
hospital_management/__pycache__
scripts/dsrpipe.py
dev_venv
# Backup files #
*.bak
play.sh
git-sync.sh
deploy*
git-sync.sh
update.sh
rollback.sh
# If you are using PyCharm #
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
Makefile
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
**/migrations/**
**haikalbot/migrations/**
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# File-based project format
*.iws
# IntelliJ
out/
# JIRA plugin
atlassian-ide-plugin.xml
# Python #
*.py[cod]
*$py.class
# Distribution / packaging
.Python build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.whl
*.egg-info/
.installed.cfg
*.egg
*.manifest
*.spec
inventory/management/commands/run.py
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
.pytest_cache/
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery
celerybeat-schedule.*
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
dev_venv/
ENV/
env.bak/
venv.bak/
# mkdocs documentation
/site
# mypy
.mypy_cache/
# Sublime Text #
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
*.sublime-workspace
*.sublime-project
# sftp configuration file
sftp-config.json
# Package control specific files Package
Control.last-run
Control.ca-list
Control.ca-bundle
Control.system-ca-bundle
GitHub.sublime-settings
# Visual Studio Code #
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history
static-copy
static
static/*
staticfiles
media
tmp
logs
static/testdir

View File

@ -0,0 +1,349 @@
# EMR Data Generator - Comprehensive Guide
This document explains how to use the unified `emr_data.py` script that combines sample EMR data generation with full ICD-10-CM XML import capabilities.
## Overview
The `emr_data.py` script provides three modes of operation:
1. **Standard Mode**: Generate sample EMR data with ~35 sample ICD-10 codes
2. **Full ICD-10 Import Mode**: Generate sample EMR data + import complete ICD-10-CM from XML
3. **ICD-10 Only Mode**: Import only ICD-10-CM codes, skip other EMR data
## Prerequisites
### Required
- Python 3.8+
- Django project properly configured
- Existing tenants in the database (run `core_data.py` first)
- Existing patients in the database (run `patients_data.py` first)
### Optional (for ICD-10 XML import)
- `xmlschema` library: `pip install xmlschema`
- ICD-10-CM XML files (download from CDC/CMS)
## Usage Examples
### 1. Standard Mode (Default)
Generate sample EMR data with sample ICD-10 codes:
```bash
python3 emr_data.py
```
**What it creates:**
- Note templates (5 templates)
- Patient encounters (~900-1000)
- Vital signs (~1800-2000 records)
- Problem lists (~140-150 entries)
- Care plans (~70-80 plans)
- Clinical notes (~800-900 notes)
- **Sample ICD-10 codes (~35 codes)**
- Clinical recommendations (~100-110)
- Allergy alerts (~20-30)
- Treatment protocols (5 protocols)
- Clinical guidelines (10 guidelines)
- Critical alerts (~5-10)
- Diagnostic suggestions (~10-20)
---
### 2. Full ICD-10 Import Mode
Generate sample EMR data + import complete ICD-10-CM from XML:
```bash
python3 emr_data.py \
--import-icd10 \
--xsd /path/to/icd10cm-tabular-2026.xsd \
--xml /path/to/icd10cm-tabular-2026.xml
```
**What it creates:**
- All standard EMR data (as above)
- **Complete ICD-10-CM codes (~70,000+ codes)** instead of sample codes
**Optional: Truncate existing codes first**
```bash
python3 emr_data.py \
--import-icd10 \
--xsd /path/to/icd10cm-tabular-2026.xsd \
--xml /path/to/icd10cm-tabular-2026.xml \
--truncate
```
---
### 3. ICD-10 Only Mode
Import only ICD-10-CM codes, skip all other EMR data generation:
```bash
python3 emr_data.py \
--icd10-only \
--xsd /path/to/icd10cm-tabular-2026.xsd \
--xml /path/to/icd10cm-tabular-2026.xml
```
**What it creates:**
- **Only ICD-10-CM codes (~70,000+ codes)**
- Skips all other EMR data generation
**With truncate:**
```bash
python3 emr_data.py \
--icd10-only \
--xsd /path/to/icd10cm-tabular-2026.xsd \
--xml /path/to/icd10cm-tabular-2026.xml \
--truncate
```
---
## Command-Line Arguments
| Argument | Description | Required | Default |
|----------|-------------|----------|---------|
| `--import-icd10` | Import full ICD-10 codes from XML files | No | False |
| `--xsd` | Path to ICD-10 XSD schema file | Yes (with --import-icd10) | None |
| `--xml` | Path to ICD-10 XML data file | Yes (with --import-icd10) | None |
| `--icd10-only` | Only import ICD-10, skip other EMR data | No | False |
| `--truncate` | Delete existing ICD-10 codes before importing | No | False |
---
## Obtaining ICD-10-CM XML Files
### Official Sources
1. **CDC (Centers for Disease Control and Prevention)**
- URL: https://www.cdc.gov/nchs/icd/icd-10-cm.htm
- Download the "ICD-10-CM Tabular List" XML files
2. **CMS (Centers for Medicare & Medicaid Services)**
- URL: https://www.cms.gov/medicare/coding-billing/icd-10-codes
- Download the complete ICD-10-CM code set
### Required Files
You need two files:
- `icd10cm-tabular-YYYY.xsd` (Schema definition)
- `icd10cm-tabular-YYYY.xml` (Actual codes)
Where `YYYY` is the year (e.g., 2026)
---
## Multi-Tenant Support
The script automatically creates ICD-10 codes for **all tenants** in your database:
```python
# Codes are created for each tenant
for tenant in tenants:
# Creates ICD-10 codes with tenant relationship
Icd10.objects.create(
tenant=tenant,
code='E11.9',
description='Type 2 diabetes mellitus without complications',
...
)
```
---
## Performance Considerations
### Sample Mode (Default)
- **Runtime**: ~30-60 seconds
- **Database Impact**: Minimal (~2,000 total records)
- **Recommended for**: Development, testing, demos
### Full ICD-10 Import Mode
- **Runtime**: ~5-15 minutes (depending on system)
- **Database Impact**: Significant (~70,000+ ICD-10 codes + ~2,000 other records)
- **Recommended for**: Production, staging, comprehensive testing
### ICD-10 Only Mode
- **Runtime**: ~3-10 minutes
- **Database Impact**: ~70,000+ ICD-10 codes only
- **Recommended for**: Updating ICD-10 codes without regenerating other data
---
## Error Handling
### Missing xmlschema Library
```
❌ Error: xmlschema library not installed.
Install it with: pip install xmlschema
```
**Solution**: `pip install xmlschema`
### Missing XML/XSD Files
```
❌ Error: --xsd and --xml are required when using --import-icd10
```
**Solution**: Provide both `--xsd` and `--xml` arguments
### No Tenants Found
```
❌ No tenants found. Please run the core data generator first.
```
**Solution**: Run `python3 core_data.py` first
### XML Parsing Errors
```
❌ Failed to parse XML: [error details]
```
**Solution**: Verify XML file integrity, ensure correct file paths
---
## Data Generated
### Sample ICD-10 Codes (Default Mode)
The script creates ~35 sample codes covering:
- Infectious diseases (A00-A04)
- Neoplasms (C00-C04)
- Circulatory diseases (I00-I06)
- Respiratory diseases (J00-J04)
- Digestive diseases (K00-K04)
- Genitourinary diseases (N00-N04)
- Symptoms and signs (R00-R04)
### Full ICD-10-CM Import
Complete ICD-10-CM code set including:
- All chapters (1-22)
- All sections
- All diagnosis codes
- Parent-child relationships
- Code descriptions
- Chapter and section names
---
## Integration with Other Data Generators
### Recommended Execution Order
1. **Core Data** (Required first)
```bash
python3 core_data.py
```
2. **Patients Data** (Required before EMR)
```bash
python3 patients_data.py
```
3. **EMR Data** (This script)
```bash
python3 emr_data.py
# or with full ICD-10 import
python3 emr_data.py --import-icd10 --xsd path/to/file.xsd --xml path/to/file.xml
```
4. **Other Modules** (Optional, any order)
```bash
python3 appointments_data.py
python3 billing_data.py
python3 pharmacy_data.py
# etc.
```
---
## Troubleshooting
### Issue: Duplicate ICD-10 Codes
**Symptom**: UNIQUE constraint failed errors
**Solution**: Use `--truncate` flag to delete existing codes first
### Issue: Slow Performance
**Symptom**: Script takes very long to complete
**Solution**:
- Ensure database indexes are created
- Use SSD storage
- Consider using PostgreSQL instead of SQLite for large datasets
### Issue: Memory Errors
**Symptom**: Out of memory errors during import
**Solution**:
- The script uses batch processing (1000 records at a time)
- Increase available system memory
- Close other applications
---
## Advanced Usage
### Custom Days Back for Encounters
Modify the script to change the number of days of encounter history:
```python
# In main() function, change:
encounters = create_encounters(tenants, days_back=30)
# To:
encounters = create_encounters(tenants, days_back=90) # 90 days of history
```
### Adjusting Data Volume
Modify the random ranges in each function:
```python
# Example: More encounters per day
daily_encounters = random.randint(20, 50) # Default
daily_encounters = random.randint(50, 100) # More data
```
---
## Migration from Old System
If you were using the separate `emr/management/commands/import_icd10.py` Django management command:
### Old Way (Django Management Command)
```bash
python manage.py import_icd10 \
--xsd path/to/file.xsd \
--xml path/to/file.xml \
--truncate
```
### New Way (Unified Script)
```bash
python3 emr_data.py \
--icd10-only \
--xsd path/to/file.xsd \
--xml path/to/file.xml \
--truncate
```
**Benefits of new approach:**
- No need for Django management command infrastructure
- Consistent with other data generators
- Can combine with EMR data generation
- Simpler command-line interface
---
## Support
For issues or questions:
1. Check this README
2. Review error messages carefully
3. Verify prerequisites are met
4. Check file paths are correct
5. Ensure database is accessible
---
## Version History
- **v2.0** (Current): Merged ICD-10 XML import functionality
- **v1.0**: Original EMR data generator with sample ICD-10 codes
---
## License
This script is part of the Hospital Management System v4 project.

View File

@ -0,0 +1,246 @@
# Surgery Management Consolidation - Summary Report
**Date:** 2025-10-03
**Task:** Remove duplicate SurgerySchedule from inpatients app and consolidate all surgical management in operating_theatre app
---
## ✅ Completed Actions
### 1. **Removed SurgerySchedule Model from Inpatients App**
**File:** `inpatients/models.py`
- ❌ Removed entire `SurgerySchedule` class (~300 lines)
- ✅ Kept: Ward, Bed, Admission, Transfer, DischargeSummary models
- ✅ Admission model retains relationship to SurgicalCase via reverse FK
### 2. **Removed SurgerySchedule Admin Configuration**
**File:** `inpatients/admin.py`
- ❌ Removed `SurgeryScheduleAdmin` class
- ❌ Removed admin registration for SurgerySchedule
- ❌ Removed import statement
### 3. **Removed SurgerySchedule Form**
**File:** `inpatients/forms.py`
- ❌ Removed `SurgeryScheduleForm` class (~100 lines)
- ❌ Removed import statement
- ✅ Kept all other forms intact
### 4. **Removed All Surgery Views**
**File:** `inpatients/views.py`
- ❌ Removed 6 class-based views:
- `SurgeryScheduleView`
- `SurgeryDetailView`
- `SurgeryScheduleListView`
- `SurgeryScheduleDetailView`
- `SurgeryScheduleCreateView`
- `SurgeryScheduleUpdateView`
- ❌ Removed 9 function-based views:
- `surgery_calendar`
- `mark_surgery_completed`
- `reschedule_surgery`
- `cancel_surgery`
- `confirm_surgery`
- `prep_surgery`
- `postpone_surgery`
- `start_surgery`
- `complete_surgery`
### 5. **Removed Surgery URL Patterns**
**File:** `inpatients/urls.py`
- ❌ Removed 12 surgery-related URL patterns
- ✅ All other URL patterns remain functional
---
## 📊 Impact Analysis
### What Was Removed:
- **Total Lines Removed:** ~800+ lines of code
- **Models:** 1 (SurgerySchedule)
- **Forms:** 1 (SurgeryScheduleForm)
- **Views:** 15 (6 CBVs + 9 FBVs)
- **URL Patterns:** 12
- **Admin Classes:** 1
### What Remains in Inpatients:
- ✅ Ward management
- ✅ Bed allocation and tracking
- ✅ Patient admissions
- ✅ Inter-ward transfers
- ✅ Discharge summaries
- ✅ All HTMX endpoints for bed management
---
## 🔗 Integration Points
### How Inpatients Connects to Operating Theatre:
1. **Admission → SurgicalCase**
```python
# In operating_theatre/models.py - SurgicalCase
admission = models.ForeignKey(
'inpatients.Admission',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='surgical_cases'
)
```
2. **Usage Example:**
```python
# Get all surgeries for an admission
admission = Admission.objects.get(pk=1)
surgeries = admission.surgical_cases.all()
# Check if surgery needs insurance approval
for surgery in surgeries:
if surgery.requires_approval():
# Create approval request
pass
```
3. **Operating Room Access:**
```python
# Get all cases in an OR
or_room = OperatingRoom.objects.get(room_number='OR-1')
today_cases = or_room.surgical_cases.filter(
scheduled_start__date=timezone.now().date()
)
```
---
## 🎯 Next Steps (Recommended)
### Phase 1: Enhance Operating Theatre (Priority)
1. **Add Missing Status Choices to SurgicalCase:**
- Add `CONFIRMED` status
- Add `PREP` status
2. **Add Workflow Views:**
- `confirm_case(request, pk)` - Confirm scheduled case
- `prep_case(request, pk)` - Mark case in prep
- `cancel_case(request, pk)` - Cancel case
- `postpone_case(request, pk)` - Postpone case
- `reschedule_case(request, pk)` - Reschedule case
3. **Add URL Patterns:**
```python
path('cases/<int:pk>/confirm/', views.confirm_case, name='confirm_case'),
path('cases/<int:pk>/prep/', views.prep_case, name='prep_case'),
path('cases/<int:pk>/cancel/', views.cancel_case, name='cancel_case'),
path('cases/<int:pk>/postpone/', views.postpone_case, name='postpone_case'),
path('cases/<int:pk>/reschedule/', views.reschedule_case, name='reschedule_case'),
```
4. **Create Reschedule Template:**
- `operating_theatre/templates/operating_theatre/case_reschedule.html`
### Phase 2: Update Data Generation
1. **Remove from `inpatients_data.py`:**
- Remove SurgerySchedule data generation
2. **Verify `or_data.py`:**
- Ensure SurgicalCase generation includes all statuses
- Verify insurance approval integration
### Phase 3: Database Migration
1. **Create migrations:**
```bash
python manage.py makemigrations
python manage.py migrate
```
2. **Verify no broken references**
---
## 🎨 Surgical Workflow (Operating Theatre)
### Complete Status Flow:
```
SCHEDULED → CONFIRMED → PREP → IN_PROGRESS → COMPLETED
↓ ↓ ↓ ↓
POSTPONED CANCELLED DELAYED CANCELLED
SCHEDULED (via reschedule)
```
### Available Actions:
- ✅ Create Case (SurgicalCaseCreateView) - Already exists
- ✅ Start Case (StartCaseView) - Already exists
- ✅ Complete Case (CompleteCaseView) - Already exists
- 🔄 Confirm Case - Need to add
- 🔄 Prep Case - Need to add
- 🔄 Cancel Case - Need to add
- 🔄 Postpone Case - Need to add
- 🔄 Reschedule Case - Need to add
---
## ✅ Benefits Achieved
1. **Single Source of Truth**
- All surgical management in one app (operating_theatre)
- No duplicate functionality
2. **Better Architecture**
- Clear separation of concerns
- Inpatients focuses on ward/bed management
- Operating theatre handles all surgical workflows
3. **Insurance Integration**
- SurgicalCase already has GenericRelation to insurance approvals
- No changes needed for insurance approval workflow
4. **Scalability**
- SurgicalCase supports both inpatient AND outpatient surgeries
- More comprehensive data model
- Better OR scheduling with ORBlock concept
5. **Data Integrity**
- No risk of conflicting surgery records
- Cleaner database schema
---
## 📝 Files Modified
1. ✅ `inpatients/models.py` - Removed SurgerySchedule model
2. ✅ `inpatients/admin.py` - Removed SurgeryScheduleAdmin
3. ✅ `inpatients/forms.py` - Removed SurgeryScheduleForm
4. ✅ `inpatients/views.py` - Removed 15 surgery views
5. ✅ `inpatients/urls.py` - Removed 12 surgery URL patterns
---
## 🚀 Ready for Production
The inpatients app is now clean and focused on its core responsibilities:
- ✅ Ward management
- ✅ Bed allocation
- ✅ Patient admissions
- ✅ Transfers
- ✅ Discharge planning
All surgical management is now centralized in the operating_theatre app with:
- ✅ Comprehensive SurgicalCase model
- ✅ OR scheduling with ORBlock
- ✅ Insurance approval integration
- ✅ Equipment tracking
- ✅ Surgical notes
- ✅ Team management
---
## 📞 Support
For questions or issues related to this consolidation:
1. Review the operating_theatre app documentation
2. Check the SurgicalCase model for available fields and methods
3. Refer to insurance_approvals integration documentation
**Status:** ✅ CONSOLIDATION COMPLETE

View File

@ -0,0 +1,328 @@
# Operating Theatre Workflow Enhancement - Complete ✅
**Date:** 2025-10-03
**Task:** Add missing workflow functions to operating_theatre app
---
## ✅ Completed Enhancements
### 1. **Added New Status Choices to SurgicalCase Model**
**File:** `operating_theatre/models.py`
Added two new statuses to the `CaseStatus` enum:
```python
class CaseStatus(models.TextChoices):
SCHEDULED = 'SCHEDULED', 'Scheduled'
CONFIRMED = 'CONFIRMED', 'Confirmed' # ✅ NEW
PREP = 'PREP', 'Pre-operative Prep' # ✅ NEW
DELAYED = 'DELAYED', 'Delayed'
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
COMPLETED = 'COMPLETED', 'Completed'
CANCELLED = 'CANCELLED', 'Cancelled'
POSTPONED = 'POSTPONED', 'Postponed'
```
### 2. **Added 5 New Workflow Functions**
**File:** `operating_theatre/views.py`
#### New Functions Added:
1. **`confirm_case(request, pk)`**
- Confirms a scheduled surgical case
- Changes status from SCHEDULED → CONFIRMED
- POST-only endpoint
2. **`prep_case(request, pk)`**
- Marks case as in pre-operative prep
- Changes status from SCHEDULED/CONFIRMED → PREP
- POST-only endpoint
3. **`cancel_case(request, pk)`**
- Cancels a surgical case
- Cannot cancel COMPLETED or IN_PROGRESS cases
- Adds cancellation reason to clinical notes
- POST-only endpoint
4. **`postpone_case(request, pk)`**
- Postpones a surgical case
- Cannot postpone COMPLETED or IN_PROGRESS cases
- Adds postponement reason to clinical notes
- POST-only endpoint
5. **`reschedule_case(request, pk)`**
- Reschedules a surgical case to new OR block and time
- Cannot reschedule COMPLETED or IN_PROGRESS cases
- Provides form to select new OR block and start time
- Adds rescheduling note to clinical notes
- GET (form) and POST (submit) endpoint
### 3. **Added URL Patterns**
**File:** `operating_theatre/urls.py`
Added 5 new URL patterns:
```python
path('cases/<int:pk>/confirm/', views.confirm_case, name='confirm_case'),
path('cases/<int:pk>/prep/', views.prep_case, name='prep_case'),
path('cases/<int:pk>/cancel/', views.cancel_case, name='cancel_case'),
path('cases/<int:pk>/postpone/', views.postpone_case, name='postpone_case'),
path('cases/<int:pk>/reschedule/', views.reschedule_case, name='reschedule_case'),
```
### 4. **Created Reschedule Template**
**File:** `operating_theatre/templates/operating_theatre/case_reschedule.html`
Features:
- Displays current case information
- Form to select new OR block
- Input for new scheduled start time
- Validation and user-friendly interface
---
## 🎨 Complete Surgical Workflow
### Status Flow Diagram:
```
SCHEDULED → CONFIRMED → PREP → IN_PROGRESS → COMPLETED
↓ ↓ ↓ ↓
POSTPONED CANCELLED DELAYED CANCELLED
SCHEDULED (via reschedule)
```
### Available Actions by Status:
| Current Status | Available Actions |
|---------------|-------------------|
| SCHEDULED | Confirm, Prep, Cancel, Postpone, Reschedule |
| CONFIRMED | Prep, Cancel, Postpone, Reschedule |
| PREP | Start, Cancel, Postpone, Reschedule |
| DELAYED | Start, Cancel, Postpone, Reschedule |
| IN_PROGRESS | Complete |
| COMPLETED | *(No actions - final state)* |
| CANCELLED | Reschedule |
| POSTPONED | Reschedule |
---
## 📋 Usage Examples
### 1. Confirm a Case
```python
# POST to /operating-theatre/cases/123/confirm/
# Status changes: SCHEDULED → CONFIRMED
```
### 2. Move to Prep
```python
# POST to /operating-theatre/cases/123/prep/
# Status changes: SCHEDULED/CONFIRMED → PREP
```
### 3. Start a Case
```python
# POST to /operating-theatre/cases/123/start/
# Status changes: PREP/CONFIRMED → IN_PROGRESS
# Sets actual_start timestamp
```
### 4. Complete a Case
```python
# POST to /operating-theatre/cases/123/complete/
# Status changes: IN_PROGRESS → COMPLETED
# Sets actual_end timestamp
# Calculates actual_duration
```
### 5. Cancel a Case
```python
# POST to /operating-theatre/cases/123/cancel/
# With POST data: reason="Patient condition improved"
# Status changes: Any (except COMPLETED/IN_PROGRESS) → CANCELLED
# Adds reason to clinical_notes
```
### 6. Postpone a Case
```python
# POST to /operating-theatre/cases/123/postpone/
# With POST data: reason="Equipment unavailable"
# Status changes: Any (except COMPLETED/IN_PROGRESS) → POSTPONED
# Adds reason to clinical_notes
```
### 7. Reschedule a Case
```python
# GET /operating-theatre/cases/123/reschedule/
# Displays form with available OR blocks
# POST /operating-theatre/cases/123/reschedule/
# With POST data: or_block=456, scheduled_start="2025-10-05 08:00"
# Status changes: POSTPONED/CANCELLED → SCHEDULED
# Updates or_block and scheduled_start
# Adds rescheduling note to clinical_notes
```
---
## 🔗 Integration with Existing Features
### Insurance Approval Integration
All workflow functions work seamlessly with the existing insurance approval system:
```python
# Check if case requires approval
if case.requires_approval():
# Create approval request
approval = InsuranceApprovalRequest.objects.create(
content_object=case,
request_type='SURGERY',
# ...
)
# Check approval status
if case.has_valid_approval():
# Proceed with surgery
case.status = 'CONFIRMED'
case.save()
```
### Admission Integration
Cases remain linked to admissions:
```python
# Get all surgeries for an admission
admission = Admission.objects.get(pk=1)
surgeries = admission.surgical_cases.all()
# Each surgery has full workflow capabilities
for surgery in surgeries:
if surgery.status == 'SCHEDULED':
# Can confirm, prep, cancel, postpone, or reschedule
```
---
## 🎯 Benefits Achieved
1. **Complete Workflow Coverage**
- All surgical case lifecycle stages covered
- Clear status transitions
- Proper validation at each step
2. **Audit Trail**
- All status changes logged
- Reasons captured for cancellations and postponements
- Rescheduling history maintained in clinical notes
3. **User-Friendly**
- Simple POST endpoints for quick actions
- Dedicated reschedule form for complex changes
- Clear error messages for invalid transitions
4. **Flexible**
- Supports emergency and elective workflows
- Handles postponements and rescheduling
- Maintains data integrity
5. **Production-Ready**
- Proper permission checks
- Tenant isolation
- Error handling
- User feedback via messages
---
## 📝 Files Modified/Created
### Modified Files:
1. ✅ `operating_theatre/models.py` - Added CONFIRMED and PREP statuses
2. ✅ `operating_theatre/views.py` - Added 5 workflow functions
3. ✅ `operating_theatre/urls.py` - Added 5 URL patterns
### Created Files:
1. ✅ `operating_theatre/templates/operating_theatre/case_reschedule.html` - Reschedule form template
2. ✅ `WORKFLOW_ENHANCEMENT_COMPLETE.md` - This documentation
---
## 🚀 Next Steps
### Database Migration:
```bash
python manage.py makemigrations operating_theatre
python manage.py migrate
```
### Testing Checklist:
- [ ] Test confirm_case endpoint
- [ ] Test prep_case endpoint
- [ ] Test cancel_case with reason
- [ ] Test postpone_case with reason
- [ ] Test reschedule_case form and submission
- [ ] Verify status transitions
- [ ] Check clinical notes updates
- [ ] Test permission checks
- [ ] Verify tenant isolation
### Optional Enhancements:
- Add HTMX endpoints for real-time status updates
- Create dashboard widgets for workflow statistics
- Add email notifications for status changes
- Implement approval workflows for cancellations
- Add bulk operations for multiple cases
---
## 📞 API Reference
### Confirm Case
```
POST /operating-theatre/cases/<pk>/confirm/
Response: Redirect to case detail
Messages: Success message
```
### Prep Case
```
POST /operating-theatre/cases/<pk>/prep/
Response: Redirect to case detail
Messages: Success message
```
### Cancel Case
```
POST /operating-theatre/cases/<pk>/cancel/
POST Data: reason (optional)
Response: Redirect to case list
Messages: Warning message
```
### Postpone Case
```
POST /operating-theatre/cases/<pk>/postpone/
POST Data: reason (optional)
Response: Redirect to case detail
Messages: Info message
```
### Reschedule Case
```
GET /operating-theatre/cases/<pk>/reschedule/
Response: Reschedule form
POST /operating-theatre/cases/<pk>/reschedule/
POST Data: or_block (required), scheduled_start (required)
Response: Redirect to case detail
Messages: Success or error message
```
---
## ✅ Summary
**Status:** ✅ WORKFLOW ENHANCEMENT COMPLETE
All missing workflow functions have been successfully added to the operating_theatre app. The surgical case management system now has complete lifecycle support from scheduling through completion, with proper handling of cancellations, postponements, and rescheduling.
The system is ready for database migration and testing!

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,149 @@
{
"name_collisions": [
{
"name": "IntegrationLog",
"occurrences": [
{
"app": "core",
"file": "core/models.py",
"lineno": 710
},
{
"app": "integration",
"file": "integration/models.py",
"lineno": 681
}
]
},
{
"name": "InventoryLocation",
"occurrences": [
{
"app": "blood_bank",
"file": "blood_bank/models.py",
"lineno": 437
},
{
"app": "inventory",
"file": "inventory/models.py",
"lineno": 562
}
]
},
{
"name": "QualityControl",
"occurrences": [
{
"app": "blood_bank",
"file": "blood_bank/models.py",
"lineno": 469
},
{
"app": "laboratory",
"file": "laboratory/models.py",
"lineno": 1060
}
]
},
{
"name": "InsuranceClaim",
"occurrences": [
{
"app": "patients",
"file": "patients/models.py",
"lineno": 784
},
{
"app": "billing",
"file": "billing/models.py",
"lineno": 606
}
]
}
],
"fieldset_similar": [
{
"a": {
"app": "patients",
"name": "ClaimStatusHistory",
"file": "patients/models.py"
},
"b": {
"app": "insurance_approvals",
"name": "ApprovalStatusHistory",
"file": "insurance_approvals/models.py"
},
"similarity": 0.64,
"shared": [
"changed_at",
"changed_by",
"from_status",
"notes",
"reason",
"settings.AUTH_USER_MODEL",
"to_status"
]
}
],
"inventory_leaks": [
{
"app": "blood_bank",
"model": "InventoryLocation",
"file": "blood_bank/models.py",
"tokens": [
"stock"
]
},
{
"app": "pharmacy",
"model": "Prescription",
"file": "pharmacy/models.py",
"tokens": [
"quantity"
]
},
{
"app": "pharmacy",
"model": "MedicationInventoryItem",
"file": "pharmacy/models.py",
"tokens": [
"quantity"
]
},
{
"app": "pharmacy",
"model": "DispenseRecord",
"file": "pharmacy/models.py",
"tokens": [
"quantity",
"stock"
]
},
{
"app": "operating_theatre",
"model": "EquipmentUsage",
"file": "operating_theatre/models.py",
"tokens": [
"quantity"
]
},
{
"app": "insurance_approvals",
"model": "InsuranceApprovalRequest",
"file": "insurance_approvals/models.py",
"tokens": [
"quantity"
]
}
],
"canonical_conflicts": [
{
"model": "Encounter",
"app": "emr",
"expected": [
"core"
],
"file": "emr/models.py"
}
]
}

Binary file not shown.

44
accounts/adapter.py Normal file
View File

@ -0,0 +1,44 @@
"""
Custom allauth adapter for the hospital management system.
"""
from allauth.account.adapter import DefaultAccountAdapter
from django.conf import settings
class CustomAccountAdapter(DefaultAccountAdapter):
"""
Custom account adapter to handle edge cases in allauth.
"""
def is_safe_url(self, url):
"""
Override is_safe_url to handle cases where request context is None.
This fixes the AttributeError: 'NoneType' object has no attribute 'get_host'
that occurs when context.request is None in certain scenarios.
"""
if not url:
return False
# Try to get the request from the adapter's stash
request = getattr(self, 'request', None)
if request:
# If we have a request, use the standard validation
try:
allowed_hosts = {request.get_host()} | set(settings.ALLOWED_HOSTS)
except AttributeError:
# Fallback if request doesn't have get_host
allowed_hosts = set(settings.ALLOWED_HOSTS)
else:
# If no request available, just use ALLOWED_HOSTS
allowed_hosts = set(settings.ALLOWED_HOSTS)
# Use Django's url_has_allowed_host_and_scheme for validation
from django.utils.http import url_has_allowed_host_and_scheme
return url_has_allowed_host_and_scheme(
url=url,
allowed_hosts=allowed_hosts,
require_https=request.is_secure() if request else False
)

View File

@ -0,0 +1,472 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name="PasswordHistory",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"password_hash",
models.CharField(help_text="Hashed password", max_length=128),
),
("created_at", models.DateTimeField(auto_now_add=True)),
],
options={
"verbose_name": "Password History",
"verbose_name_plural": "Password History",
"db_table": "accounts_password_history",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="SocialAccount",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"provider",
models.CharField(
choices=[
("GOOGLE", "Google"),
("MICROSOFT", "Microsoft"),
("APPLE", "Apple"),
("FACEBOOK", "Facebook"),
("LINKEDIN", "LinkedIn"),
("GITHUB", "GitHub"),
("OKTA", "Okta"),
("SAML", "SAML"),
("LDAP", "LDAP"),
],
max_length=50,
),
),
(
"provider_id",
models.CharField(help_text="Provider user ID", max_length=200),
),
(
"provider_email",
models.EmailField(
blank=True,
help_text="Email from provider",
max_length=254,
null=True,
),
),
(
"display_name",
models.CharField(
blank=True,
help_text="Display name from provider",
max_length=200,
null=True,
),
),
(
"profile_url",
models.URLField(
blank=True, help_text="Profile URL from provider", null=True
),
),
(
"avatar_url",
models.URLField(
blank=True, help_text="Avatar URL from provider", null=True
),
),
(
"access_token",
models.TextField(
blank=True, help_text="Access token from provider", null=True
),
),
(
"refresh_token",
models.TextField(
blank=True, help_text="Refresh token from provider", null=True
),
),
(
"token_expires_at",
models.DateTimeField(
blank=True, help_text="Token expiration date", null=True
),
),
(
"is_active",
models.BooleanField(
default=True, help_text="Social account is active"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"last_login_at",
models.DateTimeField(
blank=True,
help_text="Last login using this social account",
null=True,
),
),
],
options={
"verbose_name": "Social Account",
"verbose_name_plural": "Social Accounts",
"db_table": "accounts_social_account",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="TwoFactorDevice",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"device_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique device identifier",
unique=True,
),
),
("name", models.CharField(help_text="Device name", max_length=100)),
(
"device_type",
models.CharField(
choices=[
("TOTP", "Time-based OTP (Authenticator App)"),
("SMS", "SMS"),
("EMAIL", "Email"),
("HARDWARE", "Hardware Token"),
("BACKUP", "Backup Codes"),
],
max_length=20,
),
),
(
"secret_key",
models.CharField(
blank=True,
help_text="Secret key for TOTP devices",
max_length=200,
null=True,
),
),
(
"phone_number",
models.CharField(
blank=True,
help_text="Phone number for SMS devices",
max_length=20,
null=True,
),
),
(
"email_address",
models.EmailField(
blank=True,
help_text="Email address for email devices",
max_length=254,
null=True,
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Device is active"),
),
(
"is_verified",
models.BooleanField(default=False, help_text="Device is verified"),
),
(
"verified_at",
models.DateTimeField(
blank=True, help_text="Device verification date", null=True
),
),
(
"last_used_at",
models.DateTimeField(
blank=True, help_text="Last time device was used", null=True
),
),
(
"usage_count",
models.PositiveIntegerField(
default=0, help_text="Number of times device was used"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"verbose_name": "Two Factor Device",
"verbose_name_plural": "Two Factor Devices",
"db_table": "accounts_two_factor_device",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="UserSession",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"session_key",
models.CharField(
help_text="Django session key", max_length=40, unique=True
),
),
(
"session_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique session identifier",
unique=True,
),
),
("ip_address", models.GenericIPAddressField(help_text="IP address")),
("user_agent", models.TextField(help_text="User agent string")),
(
"device_type",
models.CharField(
choices=[
("DESKTOP", "Desktop"),
("MOBILE", "Mobile"),
("TABLET", "Tablet"),
("UNKNOWN", "Unknown"),
],
default="UNKNOWN",
max_length=20,
),
),
(
"browser",
models.CharField(
blank=True,
help_text="Browser name and version",
max_length=100,
null=True,
),
),
(
"operating_system",
models.CharField(
blank=True,
help_text="Operating system",
max_length=100,
null=True,
),
),
(
"country",
models.CharField(
blank=True, help_text="Country", max_length=100, null=True
),
),
(
"region",
models.CharField(
blank=True, help_text="Region/State", max_length=100, null=True
),
),
(
"city",
models.CharField(
blank=True, help_text="City", max_length=100, null=True
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Session is active"),
),
(
"login_method",
models.CharField(
choices=[
("PASSWORD", "Password"),
("TWO_FACTOR", "Two Factor"),
("SOCIAL", "Social Login"),
("SSO", "Single Sign-On"),
("API_KEY", "API Key"),
],
default="PASSWORD",
max_length=20,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("last_activity_at", models.DateTimeField(auto_now=True)),
(
"expires_at",
models.DateTimeField(help_text="Session expiration time"),
),
(
"ended_at",
models.DateTimeField(
blank=True, help_text="Session end time", null=True
),
),
],
options={
"verbose_name": "User Session",
"verbose_name_plural": "User Sessions",
"db_table": "accounts_user_session",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
(
"user_id",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
("email", models.EmailField(max_length=254, unique=True)),
("force_password_change", models.BooleanField(default=False)),
("password_expires_at", models.DateTimeField(blank=True, null=True)),
("failed_login_attempts", models.PositiveIntegerField(default=0)),
("locked_until", models.DateTimeField(blank=True, null=True)),
("two_factor_enabled", models.BooleanField(default=False)),
("max_concurrent_sessions", models.PositiveIntegerField(default=3)),
("session_timeout_minutes", models.PositiveIntegerField(default=30)),
("last_password_change", models.DateTimeField(blank=True, null=True)),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
],
options={
"db_table": "accounts_user",
"ordering": ["last_name", "first_name"],
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
]

View File

@ -0,0 +1,116 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("accounts", "0001_initial"),
("auth", "0012_alter_user_first_name_max_length"),
("core", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="user",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="users",
to="core.tenant",
),
),
migrations.AddField(
model_name="user",
name="user_permissions",
field=models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
migrations.AddField(
model_name="passwordhistory",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="password_history",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="socialaccount",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="social_accounts",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="twofactordevice",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="two_factor_devices",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="usersession",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="user_sessions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["tenant", "email"], name="accounts_us_tenant__162cd2_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["tenant", "username"], name="accounts_us_tenant__d92906_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["tenant", "is_active"], name="accounts_us_tenant__78e6c9_idx"
),
),
migrations.AlterUniqueTogether(
name="socialaccount",
unique_together={("provider", "provider_id")},
),
migrations.AddIndex(
model_name="usersession",
index=models.Index(
fields=["user", "is_active"], name="accounts_us_user_id_f3bc3f_idx"
),
),
migrations.AddIndex(
model_name="usersession",
index=models.Index(
fields=["session_key"], name="accounts_us_session_5ce38e_idx"
),
),
migrations.AddIndex(
model_name="usersession",
index=models.Index(
fields=["ip_address"], name="accounts_us_ip_addr_f7885b_idx"
),
),
]

View File

@ -11,9 +11,9 @@ app_name = 'accounts'
urlpatterns = [ urlpatterns = [
# Main views # Main views
path('login/', LoginView.as_view(), name='login'), # path('login/', LoginView.as_view(),name='login'),
path('logout/', LogoutView.as_view(), name='logout'), # path('logout/', LogoutView.as_view(), name='logout'),
path('signup/', SignupView.as_view(), name='account_signup'), # path('signup/', SignupView.as_view(), name='account_signup'),
path('users/', views.UserListView.as_view(), name='user_list'), path('users/', views.UserListView.as_view(), name='user_list'),
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user_detail'), path('users/<int:pk>/', views.UserDetailView.as_view(), name='user_detail'),

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,623 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.core.validators
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="DashboardWidget",
fields=[
(
"widget_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"widget_type",
models.CharField(
choices=[
("CHART", "Chart Widget"),
("TABLE", "Table Widget"),
("METRIC", "Metric Widget"),
("GAUGE", "Gauge Widget"),
("MAP", "Map Widget"),
("TEXT", "Text Widget"),
("IMAGE", "Image Widget"),
("IFRAME", "IFrame Widget"),
("CUSTOM", "Custom Widget"),
],
max_length=20,
),
),
(
"chart_type",
models.CharField(
blank=True,
choices=[
("LINE", "Line Chart"),
("BAR", "Bar Chart"),
("PIE", "Pie Chart"),
("DOUGHNUT", "Doughnut Chart"),
("AREA", "Area Chart"),
("SCATTER", "Scatter Plot"),
("HISTOGRAM", "Histogram"),
("HEATMAP", "Heat Map"),
("TREEMAP", "Tree Map"),
("FUNNEL", "Funnel Chart"),
],
max_length=20,
),
),
(
"query_config",
models.JSONField(
default=dict, help_text="Query configuration for data source"
),
),
("position_x", models.PositiveIntegerField(default=0)),
("position_y", models.PositiveIntegerField(default=0)),
(
"width",
models.PositiveIntegerField(
default=4,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(12),
],
),
),
(
"height",
models.PositiveIntegerField(
default=4,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(12),
],
),
),
(
"display_config",
models.JSONField(
default=dict, help_text="Widget display configuration"
),
),
("color_scheme", models.CharField(default="default", max_length=50)),
("auto_refresh", models.BooleanField(default=True)),
(
"refresh_interval",
models.PositiveIntegerField(
default=300, help_text="Refresh interval in seconds"
),
),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_dashboard_widget",
"ordering": ["position_y", "position_x"],
},
),
migrations.CreateModel(
name="DataSource",
fields=[
(
"source_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"source_type",
models.CharField(
choices=[
("DATABASE", "Database Query"),
("API", "API Endpoint"),
("FILE", "File Upload"),
("STREAM", "Real-time Stream"),
("WEBHOOK", "Webhook"),
("CUSTOM", "Custom Source"),
],
max_length=20,
),
),
(
"connection_type",
models.CharField(
choices=[
("POSTGRESQL", "PostgreSQL"),
("MYSQL", "MySQL"),
("SQLITE", "SQLite"),
("MONGODB", "MongoDB"),
("REDIS", "Redis"),
("REST_API", "REST API"),
("GRAPHQL", "GraphQL"),
("WEBSOCKET", "WebSocket"),
("CSV", "CSV File"),
("JSON", "JSON File"),
("XML", "XML File"),
],
max_length=20,
),
),
(
"connection_config",
models.JSONField(
default=dict, help_text="Connection configuration"
),
),
(
"authentication_config",
models.JSONField(
default=dict, help_text="Authentication configuration"
),
),
(
"query_template",
models.TextField(
blank=True, help_text="SQL query or API endpoint template"
),
),
(
"parameters",
models.JSONField(default=dict, help_text="Query parameters"),
),
(
"data_transformation",
models.JSONField(
default=dict, help_text="Data transformation rules"
),
),
(
"cache_duration",
models.PositiveIntegerField(
default=300, help_text="Cache duration in seconds"
),
),
("is_healthy", models.BooleanField(default=True)),
("last_health_check", models.DateTimeField(blank=True, null=True)),
(
"health_check_interval",
models.PositiveIntegerField(
default=300, help_text="Health check interval in seconds"
),
),
("is_active", models.BooleanField(default=True)),
(
"last_test_status",
models.CharField(
choices=[
("PENDING", "Pending"),
("RUNNING", "Running"),
("SUCCESS", "Success"),
("FAILURE", "Failure"),
],
default="PENDING",
max_length=20,
),
),
("last_test_start_at", models.DateTimeField(blank=True, null=True)),
("last_test_end_at", models.DateTimeField(blank=True, null=True)),
(
"last_test_duration_seconds",
models.PositiveIntegerField(blank=True, null=True),
),
("last_test_error_message", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_data_source",
},
),
migrations.CreateModel(
name="MetricDefinition",
fields=[
(
"metric_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"metric_type",
models.CharField(
choices=[
("COUNT", "Count"),
("SUM", "Sum"),
("AVERAGE", "Average"),
("PERCENTAGE", "Percentage"),
("RATIO", "Ratio"),
("RATE", "Rate"),
("DURATION", "Duration"),
("CUSTOM", "Custom Calculation"),
],
max_length=20,
),
),
(
"calculation_config",
models.JSONField(
default=dict, help_text="Metric calculation configuration"
),
),
(
"aggregation_period",
models.CharField(
choices=[
("REAL_TIME", "Real-time"),
("HOURLY", "Hourly"),
("DAILY", "Daily"),
("WEEKLY", "Weekly"),
("MONTHLY", "Monthly"),
("QUARTERLY", "Quarterly"),
("YEARLY", "Yearly"),
],
max_length=20,
),
),
(
"aggregation_config",
models.JSONField(
default=dict, help_text="Aggregation configuration"
),
),
(
"target_value",
models.DecimalField(
blank=True, decimal_places=4, max_digits=15, null=True
),
),
(
"warning_threshold",
models.DecimalField(
blank=True, decimal_places=4, max_digits=15, null=True
),
),
(
"critical_threshold",
models.DecimalField(
blank=True, decimal_places=4, max_digits=15, null=True
),
),
("unit_of_measure", models.CharField(blank=True, max_length=50)),
(
"decimal_places",
models.PositiveIntegerField(
default=2,
validators=[django.core.validators.MaxValueValidator(10)],
),
),
("display_format", models.CharField(default="number", max_length=50)),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_metric_definition",
},
),
migrations.CreateModel(
name="MetricValue",
fields=[
(
"value_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("value", models.DecimalField(decimal_places=4, max_digits=15)),
("period_start", models.DateTimeField()),
("period_end", models.DateTimeField()),
(
"dimensions",
models.JSONField(
default=dict,
help_text="Metric dimensions (e.g., department, provider)",
),
),
(
"metadata",
models.JSONField(default=dict, help_text="Additional metadata"),
),
(
"data_quality_score",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=5,
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(100),
],
),
),
(
"confidence_level",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=5,
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(100),
],
),
),
("calculation_timestamp", models.DateTimeField(auto_now_add=True)),
(
"calculation_duration_ms",
models.PositiveIntegerField(blank=True, null=True),
),
],
options={
"db_table": "analytics_metric_value",
"ordering": ["-period_start"],
},
),
migrations.CreateModel(
name="Report",
fields=[
(
"report_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"report_type",
models.CharField(
choices=[
("OPERATIONAL", "Operational Report"),
("FINANCIAL", "Financial Report"),
("CLINICAL", "Clinical Report"),
("QUALITY", "Quality Report"),
("COMPLIANCE", "Compliance Report"),
("PERFORMANCE", "Performance Report"),
("CUSTOM", "Custom Report"),
],
max_length=20,
),
),
(
"query_config",
models.JSONField(
default=dict, help_text="Query configuration for report"
),
),
(
"output_format",
models.CharField(
choices=[
("PDF", "PDF Document"),
("EXCEL", "Excel Spreadsheet"),
("CSV", "CSV File"),
("JSON", "JSON Data"),
("HTML", "HTML Page"),
("EMAIL", "Email Report"),
],
max_length=20,
),
),
(
"template_config",
models.JSONField(
default=dict, help_text="Report template configuration"
),
),
(
"schedule_type",
models.CharField(
choices=[
("MANUAL", "Manual Execution"),
("DAILY", "Daily"),
("WEEKLY", "Weekly"),
("MONTHLY", "Monthly"),
("QUARTERLY", "Quarterly"),
("YEARLY", "Yearly"),
("CUSTOM", "Custom Schedule"),
],
default="MANUAL",
max_length=20,
),
),
(
"schedule_config",
models.JSONField(default=dict, help_text="Schedule configuration"),
),
("next_execution", models.DateTimeField(blank=True, null=True)),
(
"recipients",
models.JSONField(default=list, help_text="Report recipients"),
),
(
"distribution_config",
models.JSONField(
default=dict, help_text="Distribution configuration"
),
),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "analytics_report",
},
),
migrations.CreateModel(
name="ReportExecution",
fields=[
(
"execution_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"execution_type",
models.CharField(
choices=[
("MANUAL", "Manual"),
("SCHEDULED", "Scheduled"),
("API", "API Triggered"),
],
default="MANUAL",
max_length=20,
),
),
("started_at", models.DateTimeField(auto_now_add=True)),
("completed_at", models.DateTimeField(blank=True, null=True)),
(
"duration_seconds",
models.PositiveIntegerField(blank=True, null=True),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("RUNNING", "Running"),
("COMPLETED", "Completed"),
("FAILED", "Failed"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
max_length=20,
),
),
("error_message", models.TextField(blank=True)),
("output_file_path", models.CharField(blank=True, max_length=500)),
(
"output_size_bytes",
models.PositiveBigIntegerField(blank=True, null=True),
),
("record_count", models.PositiveIntegerField(blank=True, null=True)),
(
"execution_parameters",
models.JSONField(default=dict, help_text="Execution parameters"),
),
],
options={
"db_table": "analytics_report_execution",
"ordering": ["-started_at"],
},
),
migrations.CreateModel(
name="Dashboard",
fields=[
(
"dashboard_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
(
"dashboard_type",
models.CharField(
choices=[
("EXECUTIVE", "Executive Dashboard"),
("CLINICAL", "Clinical Dashboard"),
("OPERATIONAL", "Operational Dashboard"),
("FINANCIAL", "Financial Dashboard"),
("QUALITY", "Quality Dashboard"),
("PATIENT", "Patient Dashboard"),
("PROVIDER", "Provider Dashboard"),
("DEPARTMENT", "Department Dashboard"),
("CUSTOM", "Custom Dashboard"),
],
max_length=20,
),
),
(
"layout_config",
models.JSONField(
default=dict, help_text="Dashboard layout configuration"
),
),
(
"refresh_interval",
models.PositiveIntegerField(
default=300, help_text="Refresh interval in seconds"
),
),
("is_public", models.BooleanField(default=False)),
(
"allowed_roles",
models.JSONField(
default=list, help_text="List of allowed user roles"
),
),
("is_active", models.BooleanField(default=True)),
("is_default", models.BooleanField(default=False)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"allowed_users",
models.ManyToManyField(
blank=True,
related_name="accessible_dashboards",
to=settings.AUTH_USER_MODEL,
),
),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "analytics_dashboard",
},
),
]

View File

@ -0,0 +1,309 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("analytics", "0001_initial"),
("core", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="dashboard",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="dashboards",
to="core.tenant",
),
),
migrations.AddField(
model_name="dashboardwidget",
name="dashboard",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="widgets",
to="analytics.dashboard",
),
),
migrations.AddField(
model_name="datasource",
name="created_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="datasource",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="data_sources",
to="core.tenant",
),
),
migrations.AddField(
model_name="dashboardwidget",
name="data_source",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="widgets",
to="analytics.datasource",
),
),
migrations.AddField(
model_name="metricdefinition",
name="created_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="metricdefinition",
name="data_source",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="metrics",
to="analytics.datasource",
),
),
migrations.AddField(
model_name="metricdefinition",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="metric_definitions",
to="core.tenant",
),
),
migrations.AddField(
model_name="metricvalue",
name="metric_definition",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="values",
to="analytics.metricdefinition",
),
),
migrations.AddField(
model_name="report",
name="created_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="report",
name="data_source",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reports",
to="analytics.datasource",
),
),
migrations.AddField(
model_name="report",
name="tenant",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reports",
to="core.tenant",
),
),
migrations.AddField(
model_name="reportexecution",
name="executed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="reportexecution",
name="report",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="executions",
to="analytics.report",
),
),
migrations.AddIndex(
model_name="dashboard",
index=models.Index(
fields=["tenant", "dashboard_type"],
name="analytics_d_tenant__6c4962_idx",
),
),
migrations.AddIndex(
model_name="dashboard",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_d_tenant__c68e4a_idx"
),
),
migrations.AddIndex(
model_name="dashboard",
index=models.Index(
fields=["tenant", "is_default"], name="analytics_d_tenant__f167b1_idx"
),
),
migrations.AlterUniqueTogether(
name="dashboard",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="datasource",
index=models.Index(
fields=["tenant", "source_type"], name="analytics_d_tenant__1f790a_idx"
),
),
migrations.AddIndex(
model_name="datasource",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_d_tenant__a566a2_idx"
),
),
migrations.AddIndex(
model_name="datasource",
index=models.Index(
fields=["tenant", "is_healthy"], name="analytics_d_tenant__442319_idx"
),
),
migrations.AlterUniqueTogether(
name="datasource",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="dashboardwidget",
index=models.Index(
fields=["dashboard", "is_active"], name="analytics_d_dashboa_6a4da0_idx"
),
),
migrations.AddIndex(
model_name="dashboardwidget",
index=models.Index(
fields=["dashboard", "position_x", "position_y"],
name="analytics_d_dashboa_4ce236_idx",
),
),
migrations.AddIndex(
model_name="metricdefinition",
index=models.Index(
fields=["tenant", "metric_type"], name="analytics_m_tenant__74f857_idx"
),
),
migrations.AddIndex(
model_name="metricdefinition",
index=models.Index(
fields=["tenant", "aggregation_period"],
name="analytics_m_tenant__95594d_idx",
),
),
migrations.AddIndex(
model_name="metricdefinition",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_m_tenant__fed8ae_idx"
),
),
migrations.AlterUniqueTogether(
name="metricdefinition",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["metric_definition", "period_start"],
name="analytics_m_metric__20f4a3_idx",
),
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["metric_definition", "period_end"],
name="analytics_m_metric__eca5ed_idx",
),
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["period_start", "period_end"],
name="analytics_m_period__286467_idx",
),
),
migrations.AddIndex(
model_name="metricvalue",
index=models.Index(
fields=["calculation_timestamp"], name="analytics_m_calcula_c2ca26_idx"
),
),
migrations.AlterUniqueTogether(
name="metricvalue",
unique_together={("metric_definition", "period_start", "period_end")},
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "report_type"], name="analytics_r_tenant__9818e0_idx"
),
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "schedule_type"],
name="analytics_r_tenant__6d4012_idx",
),
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "next_execution"],
name="analytics_r_tenant__832dfb_idx",
),
),
migrations.AddIndex(
model_name="report",
index=models.Index(
fields=["tenant", "is_active"], name="analytics_r_tenant__88f6f3_idx"
),
),
migrations.AlterUniqueTogether(
name="report",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="reportexecution",
index=models.Index(
fields=["report", "status"], name="analytics_r_report__db5768_idx"
),
),
migrations.AddIndex(
model_name="reportexecution",
index=models.Index(
fields=["report", "started_at"], name="analytics_r_report__be32b5_idx"
),
),
migrations.AddIndex(
model_name="reportexecution",
index=models.Index(
fields=["status", "started_at"], name="analytics_r_status_294e23_idx"
),
),
]

View File

@ -125,21 +125,71 @@ class WaitingQueueForm(forms.ModelForm):
""" """
Form for waiting queue management. Form for waiting queue management.
""" """
# Additional fields for providers (many-to-many)
providers = forms.ModelMultipleChoiceField(
queryset=User.objects.none(),
required=False,
widget=forms.SelectMultiple(attrs={
'class': 'form-select',
'multiple': 'multiple',
'data-placeholder': 'Select providers...'
}),
help_text='Select providers assigned to this queue'
)
# Additional field for accepting patients status
is_accepting_patients = forms.BooleanField(
required=False,
initial=True,
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}),
help_text='Queue is accepting new patients'
)
class Meta: class Meta:
model = WaitingQueue model = WaitingQueue
fields = [ fields = [
'name', 'description', 'queue_type', 'location', 'specialty', 'name', 'description', 'queue_type', 'location', 'specialty',
'max_queue_size', 'average_service_time_minutes', 'is_active' 'max_queue_size', 'average_service_time_minutes', 'is_active',
'is_accepting_patients'
] ]
widgets = { widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}), 'name': forms.TextInput(attrs={
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 'class': 'form-control',
'queue_type': forms.Select(attrs={'class': 'form-select'}), 'placeholder': 'Enter queue name',
'location': forms.TextInput(attrs={'class': 'form-control'}), 'required': True
'specialty': forms.TextInput(attrs={'class': 'form-control'}), }),
'max_queue_size': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}), 'description': forms.Textarea(attrs={
'average_service_time_minutes': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}), 'class': 'form-control',
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'rows': 3,
'placeholder': 'Describe the purpose and scope of this queue'
}),
'queue_type': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'location': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Building A, Floor 2, Room 201'
}),
'specialty': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Cardiology, Pediatrics'
}),
'max_queue_size': forms.NumberInput(attrs={
'class': 'form-control',
'min': '1',
'placeholder': '50',
'required': True
}),
'average_service_time_minutes': forms.NumberInput(attrs={
'class': 'form-control',
'min': '1',
'placeholder': '30',
'required': True
}),
'is_active': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -147,13 +197,62 @@ class WaitingQueueForm(forms.ModelForm):
tenant = kwargs.pop('tenant', None) tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if user and tenant: # Set up providers queryset
self.fields['provider'].queryset = User.objects.filter( if user and hasattr(user, 'tenant'):
tenant = user.tenant
if tenant:
self.fields['providers'].queryset = User.objects.filter(
tenant=tenant, tenant=tenant,
is_active=True, is_active=True,
role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT'] employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'SURGEON', 'PHARMACIST', 'LAB_TECH', 'RADIOLOGIST', 'THERAPIST']
).order_by('last_name', 'first_name') ).order_by('last_name', 'first_name')
# If editing existing queue, set initial providers
if self.instance and self.instance.pk:
self.fields['providers'].initial = self.instance.providers.all()
# Load existing operating hours and priority weights
if self.instance.operating_hours:
self.initial_operating_hours = self.instance.operating_hours
if self.instance.priority_weights:
self.initial_priority_weights = self.instance.priority_weights
def clean(self):
cleaned_data = super().clean()
# Validate max queue size
max_queue_size = cleaned_data.get('max_queue_size')
if max_queue_size and max_queue_size < 1:
raise ValidationError('Maximum queue size must be at least 1.')
# Validate average service time
avg_service_time = cleaned_data.get('average_service_time_minutes')
if avg_service_time and avg_service_time < 1:
raise ValidationError('Average service time must be at least 1 minute.')
return cleaned_data
def save(self, commit=True):
instance = super().save(commit=False)
# Handle operating hours from POST data
if hasattr(self, 'operating_hours_data'):
instance.operating_hours = self.operating_hours_data
# Handle priority weights from POST data
if hasattr(self, 'priority_weights_data'):
instance.priority_weights = self.priority_weights_data
if commit:
instance.save()
# Save many-to-many relationships
if 'providers' in self.cleaned_data:
instance.providers.set(self.cleaned_data['providers'])
self.save_m2m()
return instance
class QueueEntryForm(forms.ModelForm): class QueueEntryForm(forms.ModelForm):
""" """

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,526 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("appointments", "0001_initial"),
("core", "0001_initial"),
("hr", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="appointmentrequest",
name="patient",
field=models.ForeignKey(
help_text="Patient requesting appointment",
on_delete=django.db.models.deletion.CASCADE,
related_name="appointment_requests",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="appointmentrequest",
name="provider",
field=models.ForeignKey(
help_text="Healthcare provider",
on_delete=django.db.models.deletion.CASCADE,
related_name="provider_appointments",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="appointmentrequest",
name="rescheduled_from",
field=models.ForeignKey(
blank=True,
help_text="Original appointment if rescheduled",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="rescheduled_appointments",
to="appointments.appointmentrequest",
),
),
migrations.AddField(
model_name="appointmentrequest",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="appointment_requests",
to="core.tenant",
),
),
migrations.AddField(
model_name="appointmenttemplate",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the template",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_appointment_templates",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="appointmenttemplate",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="appointment_templates",
to="core.tenant",
),
),
migrations.AddField(
model_name="queueentry",
name="appointment",
field=models.ForeignKey(
help_text="Associated appointment",
on_delete=django.db.models.deletion.CASCADE,
related_name="queue_entries",
to="appointments.appointmentrequest",
),
),
migrations.AddField(
model_name="queueentry",
name="assigned_provider",
field=models.ForeignKey(
blank=True,
help_text="Assigned provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="assigned_queue_entries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="queueentry",
name="patient",
field=models.ForeignKey(
help_text="Patient in queue",
on_delete=django.db.models.deletion.CASCADE,
related_name="queue_entries",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="queueentry",
name="updated_by",
field=models.ForeignKey(
blank=True,
help_text="User who last updated entry",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="updated_queue_entries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="slotavailability",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the slot",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_availability_slots",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="slotavailability",
name="provider",
field=models.ForeignKey(
help_text="Healthcare provider",
on_delete=django.db.models.deletion.CASCADE,
related_name="availability_slots",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="slotavailability",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="availability_slots",
to="core.tenant",
),
),
migrations.AddField(
model_name="telemedicinesession",
name="appointment",
field=models.OneToOneField(
help_text="Associated appointment",
on_delete=django.db.models.deletion.CASCADE,
related_name="telemedicine_session",
to="appointments.appointmentrequest",
),
),
migrations.AddField(
model_name="telemedicinesession",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the session",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_telemedicine_sessions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the waiting list entry",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_waiting_list_entries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="department",
field=models.ForeignKey(
help_text="Department for appointment",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="hr.department",
),
),
migrations.AddField(
model_name="waitinglist",
name="patient",
field=models.ForeignKey(
help_text="Patient on waiting list",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="waitinglist",
name="provider",
field=models.ForeignKey(
blank=True,
help_text="Preferred healthcare provider",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="provider_waiting_list",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="removed_by",
field=models.ForeignKey(
blank=True,
help_text="User who removed entry from waiting list",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="removed_waiting_list_entries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="scheduled_appointment",
field=models.ForeignKey(
blank=True,
help_text="Scheduled appointment from waiting list",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="waiting_list_entry",
to="appointments.appointmentrequest",
),
),
migrations.AddField(
model_name="waitinglist",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="core.tenant",
),
),
migrations.AddField(
model_name="waitinglistcontactlog",
name="contacted_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who made contact",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglistcontactlog",
name="waiting_list_entry",
field=models.ForeignKey(
help_text="Associated waiting list entry",
on_delete=django.db.models.deletion.CASCADE,
related_name="contact_logs",
to="appointments.waitinglist",
),
),
migrations.AddField(
model_name="waitingqueue",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the queue",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_waiting_queues",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitingqueue",
name="providers",
field=models.ManyToManyField(
blank=True,
help_text="Providers associated with this queue",
related_name="waiting_queues",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitingqueue",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_queues",
to="core.tenant",
),
),
migrations.AddField(
model_name="queueentry",
name="queue",
field=models.ForeignKey(
help_text="Waiting queue",
on_delete=django.db.models.deletion.CASCADE,
related_name="queue_entries",
to="appointments.waitingqueue",
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["tenant", "status"], name="appointment_tenant__979096_idx"
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["patient", "status"], name="appointment_patient_803ab4_idx"
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["provider", "scheduled_datetime"],
name="appointment_provide_ef6955_idx",
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["scheduled_datetime"], name="appointment_schedul_8f6c0e_idx"
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["priority", "urgency_score"],
name="appointment_priorit_cdad1a_idx",
),
),
migrations.AddIndex(
model_name="appointmentrequest",
index=models.Index(
fields=["appointment_type", "specialty"],
name="appointment_appoint_49fcc4_idx",
),
),
migrations.AddIndex(
model_name="appointmenttemplate",
index=models.Index(
fields=["tenant", "specialty"], name="appointment_tenant__8f5ab7_idx"
),
),
migrations.AddIndex(
model_name="appointmenttemplate",
index=models.Index(
fields=["appointment_type"], name="appointment_appoint_da9846_idx"
),
),
migrations.AddIndex(
model_name="appointmenttemplate",
index=models.Index(
fields=["is_active"], name="appointment_is_acti_953e67_idx"
),
),
migrations.AddIndex(
model_name="slotavailability",
index=models.Index(
fields=["tenant", "provider", "date"],
name="appointment_tenant__d41564_idx",
),
),
migrations.AddIndex(
model_name="slotavailability",
index=models.Index(
fields=["date", "start_time"], name="appointment_date_e6d843_idx"
),
),
migrations.AddIndex(
model_name="slotavailability",
index=models.Index(
fields=["specialty"], name="appointment_special_158174_idx"
),
),
migrations.AddIndex(
model_name="slotavailability",
index=models.Index(
fields=["is_active", "is_blocked"],
name="appointment_is_acti_4bd0a5_idx",
),
),
migrations.AlterUniqueTogether(
name="slotavailability",
unique_together={("provider", "date", "start_time")},
),
migrations.AddIndex(
model_name="telemedicinesession",
index=models.Index(
fields=["appointment"], name="appointment_appoint_34f472_idx"
),
),
migrations.AddIndex(
model_name="telemedicinesession",
index=models.Index(fields=["status"], name="appointment_status_f49676_idx"),
),
migrations.AddIndex(
model_name="telemedicinesession",
index=models.Index(
fields=["scheduled_start"], name="appointment_schedul_8a4e8e_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["tenant", "status"], name="appointment_tenant__a558da_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["patient", "status"], name="appointment_patient_73f03d_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["department", "specialty", "status"],
name="appointment_departm_78fd70_idx",
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["priority", "urgency_score"],
name="appointment_priorit_30fb90_idx",
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["status", "created_at"], name="appointment_status_cfe551_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["provider", "status"], name="appointment_provide_dd6c2b_idx"
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["waiting_list_entry", "contact_date"],
name="appointment_waiting_50d8ac_idx",
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["contact_outcome"], name="appointment_contact_ad9c45_idx"
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["next_contact_date"], name="appointment_next_co_b29984_idx"
),
),
migrations.AddIndex(
model_name="waitingqueue",
index=models.Index(
fields=["tenant", "queue_type"], name="appointment_tenant__e21f2a_idx"
),
),
migrations.AddIndex(
model_name="waitingqueue",
index=models.Index(
fields=["specialty"], name="appointment_special_b50647_idx"
),
),
migrations.AddIndex(
model_name="waitingqueue",
index=models.Index(
fields=["is_active"], name="appointment_is_acti_e2f2a7_idx"
),
),
migrations.AddIndex(
model_name="queueentry",
index=models.Index(
fields=["queue", "status"], name="appointment_queue_i_d69f8c_idx"
),
),
migrations.AddIndex(
model_name="queueentry",
index=models.Index(
fields=["patient"], name="appointment_patient_beccf1_idx"
),
),
migrations.AddIndex(
model_name="queueentry",
index=models.Index(
fields=["priority_score"], name="appointment_priorit_48b785_idx"
),
),
migrations.AddIndex(
model_name="queueentry",
index=models.Index(
fields=["joined_at"], name="appointment_joined__709843_idx"
),
),
]

View File

@ -23,24 +23,62 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-9"> <div class="col-lg-9">
<div class="card"> <div class="panel panel-inverse mb-4" data-sortable-id="index-3">
<div class="card-body"> <div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-calendar me-2"></i>Calender
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div id="calendar" class="calendar-wrapper"></div> <div id="calendar" class="calendar-wrapper"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-3"> <div class="col-lg-3">
<div class="card"> <div class="panel panel-inverse mb-4" data-sortable-id="index-2">
<div class="card-header fw-bold">Appointment Details</div> <div class="panel-heading">
<div class="card-body" id="appt-details"> <h4 class="panel-title">
<i class="fas fa-calendar-alt me-2"></i>Appointment Details
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body" id="appt-details">
<div class="text-muted small">Click an event to see details.</div> <div class="text-muted small">Click an event to see details.</div>
</div> </div>
</div> </div>
<div class="card mt-3">
<div class="card-header fw-bold">Filters</div> <div class="panel panel-inverse mb-4" data-sortable-id="index-3">
<div class="card-body"> <div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-filter me-2"></i>Filters
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<form id="calendarFilters"> <form id="calendarFilters">
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Status</label> <label class="form-label">Status</label>
@ -66,30 +104,16 @@
</div> </div>
</div> </div>
{# Optional: Bootstrap modal for full details #}
<div class="modal fade" id="apptModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title">Appointment</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="apptModalBody">
<!-- HTMX fills here -->
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{% static 'plugins/moment/min/moment.min.js' %}"></script> <script src="{% static 'plugins/moment/min/moment.min.js' %}"></script>
<script src="{% static 'plugins/@fullcalendar/core/index.global.js' %}"></script> <script src="{% static 'plugins/@fullcalendar/core/index.global.js' %}"></script>
<script src="{% static 'plugins/@fullcalendar/daygrid/index.global.js' %}"></script> <script src="{% static 'plugins/@fullcalendar/daygrid/index.global.js' %}"></script>
<script src="{% static 'plugins/@fullcalendar/timegrid/index.global.js' %}"></script> <script src="{% static 'plugins/@fullcalendar/timegrid/index.global.js' %}"></script>
<script src="{% static 'plugins/@fullcalendar/interaction/index.global.js' %}"></script> <script src="{% static 'plugins/@fullcalendar/interaction/index.global.js' %}"></script>
<script src="{% static 'plugins/@fullcalendar/list/index.global.js' %}"></script> <script src="{% static 'plugins/@fullcalendar/list/index.global.js' %}"></script>
<script src="{% static 'plugins/@fullcalendar/bootstrap/index.global.js' %}"></script> <script src="{% static 'plugins/@fullcalendar/bootstrap/index.global.js' %}"></script>
<script> <script>
(function(){ (function(){
const calEl = document.getElementById('calendar'); const calEl = document.getElementById('calendar');
@ -138,13 +162,6 @@
fetch("{% url 'appointments:appointment_detail_card' 0 %}".replace('0', info.event.id), {credentials:'same-origin'}) fetch("{% url 'appointments:appointment_detail_card' 0 %}".replace('0', info.event.id), {credentials:'same-origin'})
.then(r => r.text()) .then(r => r.text())
.then(html => { detailsEl.innerHTML = html; }); .then(html => { detailsEl.innerHTML = html; });
// Also open modal
{#const modalBody = document.getElementById('apptModalBody');#}
{#modalBody.innerHTML = '<div class="text-center text-muted py-3">Loading...</div>';#}
{#fetch("{% url 'appointments:appointment_detail_card' 0 %}".replace('0', info.event.id), {credentials:'same-origin'})#}
{# .then(r => r.text())#}
{# .then(html => { modalBody.innerHTML = html; new bootstrap.Modal('#apptModal').show(); });#}
}, },
eventDrop: function(info){ sendReschedule(info); }, eventDrop: function(info){ sendReschedule(info); },
eventResize: function(info){ sendReschedule(info); } eventResize: function(info){ sendReschedule(info); }

View File

@ -0,0 +1,316 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Queue Display - {{ queue.name }}{% endblock %}
{% block css %}
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
overflow: hidden;
}
.queue-display-container {
min-height: 100vh;
padding: 20px;
}
.display-header {
background: white;
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.queue-title {
font-size: 3rem;
font-weight: 700;
color: #2d3748;
margin: 0;
text-align: center;
}
.queue-subtitle {
font-size: 1.5rem;
color: #718096;
text-align: center;
margin-top: 10px;
}
.current-serving {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
padding: 40px;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
text-align: center;
}
.serving-label {
font-size: 1.8rem;
color: rgba(255,255,255,0.9);
margin-bottom: 20px;
text-transform: uppercase;
letter-spacing: 2px;
}
.serving-number {
font-size: 8rem;
font-weight: 900;
color: white;
line-height: 1;
text-shadow: 0 5px 15px rgba(0,0,0,0.3);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.waiting-list {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.waiting-header {
font-size: 2rem;
font-weight: 700;
color: #2d3748;
margin-bottom: 30px;
text-align: center;
border-bottom: 3px solid #667eea;
padding-bottom: 15px;
}
.waiting-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 20px;
}
.waiting-item {
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
border: 2px solid #e2e8f0;
border-radius: 10px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
}
.waiting-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.waiting-item.priority-high {
border-color: #f56565;
background: linear-gradient(135deg, #fff5f5 0%, #fed7d7 100%);
}
.waiting-item.priority-urgent {
border-color: #ed8936;
background: linear-gradient(135deg, #fffaf0 0%, #feebc8 100%);
}
.waiting-number {
font-size: 3rem;
font-weight: 900;
color: #2d3748;
margin-bottom: 10px;
}
.waiting-position {
font-size: 1rem;
color: #718096;
text-transform: uppercase;
letter-spacing: 1px;
}
.stats-bar {
background: white;
border-radius: 15px;
padding: 20px;
margin-top: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 20px;
}
.stat-item {
text-align: center;
flex: 1;
min-width: 150px;
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
color: #667eea;
}
.stat-label {
font-size: 1rem;
color: #718096;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 5px;
}
.no-patients {
text-align: center;
padding: 60px 20px;
color: #718096;
}
.no-patients i {
font-size: 5rem;
margin-bottom: 20px;
opacity: 0.3;
}
.no-patients h3 {
font-size: 2rem;
margin-bottom: 10px;
}
.clock {
position: fixed;
top: 20px;
right: 20px;
background: white;
padding: 15px 25px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
font-size: 1.5rem;
font-weight: 600;
color: #2d3748;
}
@media (max-width: 768px) {
.queue-title {
font-size: 2rem;
}
.serving-number {
font-size: 5rem;
}
.waiting-grid {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
.waiting-number {
font-size: 2rem;
}
}
</style>
{% endblock %}
{% block content %}
<div class="queue-display-container">
<!-- Clock -->
<div class="clock" id="clock"></div>
<!-- Header -->
<div class="display-header">
<h1 class="queue-title">{{ queue.name }}</h1>
<p class="queue-subtitle">{{ queue.get_queue_type_display }} Queue</p>
</div>
<!-- Current Serving -->
<div class="current-serving">
<div class="serving-label">Now Serving</div>
{% if current_patient %}
<div class="serving-number">{{ current_patient.queue_position|stringformat:"03d" }}</div>
{% else %}
<div class="serving-number">---</div>
{% endif %}
</div>
<!-- Waiting List -->
<div class="waiting-list">
<h2 class="waiting-header">
<i class="fas fa-users me-2"></i>Waiting Patients
</h2>
{% if waiting_patients %}
<div class="waiting-grid">
{% for entry in waiting_patients %}
<div class="waiting-item {% if entry.priority == 'EMERGENCY' or entry.priority == 'STAT' %}priority-high{% elif entry.priority == 'URGENT' %}priority-urgent{% endif %}">
<div class="waiting-number">{{ entry.queue_position|stringformat:"03d" }}</div>
<div class="waiting-position">
{% if entry.priority == 'EMERGENCY' or entry.priority == 'STAT' %}
<i class="fas fa-exclamation-triangle text-danger"></i>
{% elif entry.priority == 'URGENT' %}
<i class="fas fa-exclamation-circle text-warning"></i>
{% endif %}
Position {{ forloop.counter }}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="no-patients">
<i class="fas fa-check-circle"></i>
<h3>No Patients Waiting</h3>
<p>Queue is currently empty</p>
</div>
{% endif %}
</div>
<!-- Statistics Bar -->
<div class="stats-bar">
<div class="stat-item">
<div class="stat-value">{{ stats.total_waiting }}</div>
<div class="stat-label">Waiting</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ stats.avg_wait_time }}</div>
<div class="stat-label">Avg Wait (min)</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ stats.served_today }}</div>
<div class="stat-label">Served Today</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ queue.max_queue_size }}</div>
<div class="stat-label">Max Capacity</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script>
$(document).ready(function() {
// Update clock
function updateClock() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
$('#clock').text(`${hours}:${minutes}:${seconds}`);
}
updateClock();
setInterval(updateClock, 1000);
// Auto-refresh every 10 seconds
setInterval(function() {
location.reload();
}, 10000);
// Play sound when new patient is called
const currentServing = '{{ current_patient.queue_position|default:"" }}';
const lastServing = localStorage.getItem('lastServing_{{ queue.id }}');
if (currentServing && currentServing !== lastServing) {
// Play notification sound (you can add an audio element)
localStorage.setItem('lastServing_{{ queue.id }}', currentServing);
}
});
</script>
{% endblock %}

View File

@ -219,9 +219,9 @@
<button class="btn btn-xs btn-outline-danger me-2" onclick="clearQueue()"> <button class="btn btn-xs btn-outline-danger me-2" onclick="clearQueue()">
<i class="fas fa-broom me-2"></i>Clear Queue <i class="fas fa-broom me-2"></i>Clear Queue
</button> </button>
<a href="{% url 'appointments:call_next_patient' queue.id %}" class="btn btn-xs btn-outline-primary me-2"> <button onclick="callNextPatient('{{ queue.id }}')" class="btn btn-xs btn-outline-primary me-2">
<i class="fas fa-phone me-1"></i>Call Next <i class="fas fa-phone me-1"></i>Call Next
</a> </button>
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a> <a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a> <a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
@ -366,45 +366,45 @@ function getCookie(name) {
return cookieValue; return cookieValue;
} }
{#function callNextPatient(queueId) {#} function callNextPatient(queueId) {
{# Swal.fire({#} swal({
{# title: "Call the next patient?",#} title: "Call the next patient?",
{# text: "This will notify the next patient in the queue.",#} text: "This will notify the next patient in the queue.",
{# icon: "warning",#} icon: "warning",
{# showCancelButton: true,#} showCancelButton: true,
{# confirmButtonText: "Yes",#} confirmButtonText: "Yes",
{# cancelButtonText: "Cancel"#} cancelButtonText: "Cancel"
{# }).then((result) => {#} }).then((result) => {
{# if (result.isConfirmed) {#} if (result.isConfirmed) {
{# $.ajax({#} $.ajax({
{# url: "{% url 'appointments:call_next_patient' 0 %}".replace("0", queueId),#} url: "{% url 'appointments:call_next_patient' 0 %}".replace('0', queueId),
{# type: "POST",#} type: "POST",
{# headers: { "X-CSRFToken": getCookie("csrftoken") },#} headers: { "X-CSRFToken": getCookie("csrftoken") },
{# success: function(response) {#} success: function(response) {
{# if (response.success) {#} if (response.success) {
{# Swal.fire({#} Swal.fire({
{# icon: "success",#} icon: "success",
{# title: "Next patient called",#} title: "Next patient called",
{# showConfirmButton: false,#} showConfirmButton: false,
{# timer: 1200#} timer: 1200
{# }).then(() => {#} }).then(() => {
{# location.reload();#} location.reload();
{# });#} });
{# } else {#} } else {
{# Swal.fire("Error", response.message || "Failed to call next patient", "error");#} swal("Error", response.message || "Failed to call next patient", "error");
{# }#} }
{# },#} },
{# error: function() {#} error: function() {
{# Swal.fire("Error", "Failed to call next patient", "error");#} swal("Error", "Failed to call next patient", "error");
{# }#} }
{# });#} });
{# }#} }
{# });#} });
{#}#} }
{#function callPatient(entryId) {#} {#function callPatient(entryId) {#}
{# if (confirm('Call this patient?')) {#} {# if (confirm('Call this patient?')) {#}
{# $.post('{% url "appointments:call_patient" %}', {#} {# $.post('{% url "appointments:call_patient" 0 %}'.replace('0', entryId), {#}
{# entry_id: entryId,#} {# entry_id: entryId,#}
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#} {# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
{# }).done(function(response) {#} {# }).done(function(response) {#}

View File

@ -1,86 +1,41 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% block title %}{% if form.instance.pk %}Edit{% else %}Create{% endif %} Waiting Queue{% endblock %} {% block title %}
{% if object %}Edit Waiting Queue{% else %}Create Waiting Queue{% endif %}
{% endblock %}
{% block css %} {% block css %}
<!-- Select2 CSS -->
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" /> <link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<style> <style>
.form-section { .help-sidebar {
background: white; background-color: #f8f9fa;
border: 1px solid #dee2e6; border-radius: 5px;
border-radius: 0.5rem; padding: 15px;
padding: 1.5rem; }
margin-bottom: 1.5rem; .operating-hours-grid {
}
.section-header {
border-bottom: 2px solid #f8f9fa;
padding-bottom: 0.75rem;
margin-bottom: 1.5rem;
}
.section-title {
color: #495057;
font-weight: 600;
margin: 0;
}
.section-description {
color: #6c757d;
font-size: 0.875rem;
margin: 0.25rem 0 0 0;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
}
.form-text {
color: #6c757d;
font-size: 0.875rem;
}
.queue-type-preview {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
margin-top: 0.5rem;
}
.operating-hours-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; gap: 1rem;
margin-top: 1rem; margin-top: 1rem;
} }
.day-schedule {
.day-schedule {
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
border-radius: 0.375rem; border-radius: 0.375rem;
padding: 1rem; padding: 1rem;
} }
.day-header {
.day-header {
font-weight: 600; font-weight: 600;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
color: #495057; color: #495057;
} }
.time-inputs {
.time-inputs {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
align-items: center; align-items: center;
} }
.priority-weight-item {
.priority-weight-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
@ -88,188 +43,163 @@
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
border-radius: 0.375rem; border-radius: 0.375rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.weight-label {
.weight-label {
flex: 1; flex: 1;
font-weight: 500; font-weight: 500;
} }
.weight-input {
.weight-input {
width: 100px; width: 100px;
} }
@media (max-width: 768px) {
@media (max-width: 768px) {
.operating-hours-grid { .operating-hours-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.time-inputs { .time-inputs {
flex-direction: column; flex-direction: column;
gap: 0.25rem; gap: 0.25rem;
} }
.priority-weight-item { .priority-weight-item {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: 0.5rem; gap: 0.5rem;
} }
.weight-input { .weight-input {
width: 100%; width: 100%;
} }
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <!-- begin breadcrumb -->
<!-- Page Header --> <ol class="breadcrumb float-xl-end">
<div class="d-flex align-items-center mb-3"> <li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<div>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li> <li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_list' %}">Waiting Queues</a></li> <li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_list' %}">Waiting Queues</a></li>
<li class="breadcrumb-item active"> <li class="breadcrumb-item active">
{% if form.instance.pk %}Edit Queue{% else %}Create Queue{% endif %} {% if object %}Edit Queue{% else %}Create Queue{% endif %}
</li> </li>
</ol> </ol>
<h1 class="page-header mb-0"> <!-- end breadcrumb -->
<i class="fas fa-users me-2"></i>
{% if form.instance.pk %}Edit Waiting Queue{% else %}Create Waiting Queue{% endif %}
</h1>
</div>
<div class="ms-auto">
<a href="{% url 'appointments:waiting_queue_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to List
</a>
</div>
</div>
<!-- begin page-header -->
<h1 class="page-header">
{% if object %}Edit Waiting Queue{% else %}Create Waiting Queue{% endif %}
</h1>
<!-- end page-header -->
<!-- begin row -->
<div class="row">
<!-- begin col-8 -->
<div class="col-xl-8">
<!-- begin panel -->
<div class="panel panel-inverse">
<!-- begin panel-heading -->
<div class="panel-heading">
<h4 class="panel-title">
{% if object %}Edit Queue: {{ object.name }}{% else %}Create New Waiting Queue{% endif %}
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<!-- end panel-heading -->
<!-- begin panel-body -->
<div class="panel-body">
<form method="post" id="queueForm"> <form method="post" id="queueForm">
{% csrf_token %} {% csrf_token %}
<!-- Form Errors -->
{% if form.errors %}
<div class="alert alert-danger">
<h5><i class="fas fa-exclamation-circle"></i> Please correct the errors below:</h5>
<ul class="mb-0">
{% for field in form %}
{% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Basic Information --> <!-- Basic Information -->
<div class="form-section"> <div class="card mb-4">
<div class="section-header"> <div class="card-header">
<h4 class="section-title"> <h5 class="card-title mb-0"><i class="fas fa-info-circle me-2"></i>Basic Information</h5>
<i class="fas fa-info-circle me-2"></i>Basic Information
</h4>
<p class="section-description">Configure the basic details of the waiting queue</p>
</div> </div>
<div class="card-body">
<div class="row"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <label for="{{ form.name.id_for_label }}" class="form-label">Queue Name <span class="text-danger">*</span></label>
<label for="{{ form.name.id_for_label }}" class="form-label">
Queue Name <span class="text-danger">*</span>
</label>
{{ form.name }} {{ form.name }}
{% if form.name.help_text %}
<div class="form-text">{{ form.name.help_text }}</div>
{% endif %}
{% if form.name.errors %} {% if form.name.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">{{ form.name.errors }}</div>
{% for error in form.name.errors %}{{ error }}{% endfor %}
</div>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <label for="{{ form.queue_type.id_for_label }}" class="form-label">Queue Type <span class="text-danger">*</span></label>
<label for="{{ form.queue_type.id_for_label }}" class="form-label">
Queue Type <span class="text-danger">*</span>
</label>
{{ form.queue_type }} {{ form.queue_type }}
{% if form.queue_type.help_text %}
<div class="form-text">{{ form.queue_type.help_text }}</div>
{% endif %}
{% if form.queue_type.errors %} {% if form.queue_type.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">{{ form.queue_type.errors }}</div>
{% for error in form.queue_type.errors %}{{ error }}{% endfor %}
</div>
{% endif %} {% endif %}
<div class="queue-type-preview" id="type-preview" style="display: none;">
<div class="d-flex align-items-center">
<i class="fas fa-info-circle text-primary me-2"></i>
<span id="type-description"></span>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">Description</label> <label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
{{ form.description }} {{ form.description }}
{% if form.description.help_text %}
<div class="form-text">{{ form.description.help_text }}</div>
{% endif %}
{% if form.description.errors %} {% if form.description.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">{{ form.description.errors }}</div>
{% for error in form.description.errors %}{{ error }}{% endfor %}
</div>
{% endif %} {% endif %}
<div class="form-text">Provide a detailed description of this queue's purpose</div>
</div>
</div> </div>
</div> </div>
<!-- Queue Configuration --> <!-- Queue Configuration -->
<div class="form-section"> <div class="card mb-4">
<div class="section-header"> <div class="card-header">
<h4 class="section-title"> <h5 class="card-title mb-0"><i class="fas fa-cogs me-2"></i>Queue Configuration</h5>
<i class="fas fa-cogs me-2"></i>Queue Configuration
</h4>
<p class="section-description">Set capacity limits and service time estimates</p>
</div> </div>
<div class="card-body">
<div class="row"> <div class="row mb-3">
<div class="col-md-4"> <div class="col-md-6">
<div class="form-group"> <label for="{{ form.max_queue_size.id_for_label }}" class="form-label">Maximum Queue Size <span class="text-danger">*</span></label>
<label for="{{ form.max_queue_size.id_for_label }}" class="form-label">
Maximum Queue Size <span class="text-danger">*</span>
</label>
{{ form.max_queue_size }} {{ form.max_queue_size }}
{% if form.max_queue_size.help_text %}
<div class="form-text">{{ form.max_queue_size.help_text }}</div>
{% endif %}
{% if form.max_queue_size.errors %} {% if form.max_queue_size.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">{{ form.max_queue_size.errors }}</div>
{% for error in form.max_queue_size.errors %}{{ error }}{% endfor %}
</div>
{% endif %} {% endif %}
<div class="form-text">Maximum number of patients allowed in queue</div>
</div> </div>
</div> <div class="col-md-6">
<label for="{{ form.average_service_time_minutes.id_for_label }}" class="form-label">Average Service Time (minutes) <span class="text-danger">*</span></label>
<div class="col-md-4">
<div class="form-group">
<label for="{{ form.average_service_time_minutes.id_for_label }}" class="form-label">
Average Service Time (minutes) <span class="text-danger">*</span>
</label>
{{ form.average_service_time_minutes }} {{ form.average_service_time_minutes }}
{% if form.average_service_time_minutes.help_text %}
<div class="form-text">{{ form.average_service_time_minutes.help_text }}</div>
{% endif %}
{% if form.average_service_time_minutes.errors %} {% if form.average_service_time_minutes.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">{{ form.average_service_time_minutes.errors }}</div>
{% for error in form.average_service_time_minutes.errors %}{{ error }}{% endfor %}
</div>
{% endif %} {% endif %}
<div class="form-text">Average time to serve each patient</div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="row mb-3">
<div class="form-group"> <div class="col-md-6">
<label class="form-label">Status</label> <div class="form-check mb-2">
<div class="form-check">
{{ form.is_active }} {{ form.is_active }}
<label class="form-check-label" for="{{ form.is_active.id_for_label }}"> <label class="form-check-label" for="{{ form.is_active.id_for_label }}">
Queue is active Queue is active
</label> </label>
</div> </div>
<div class="form-check"> </div>
<div class="col-md-6">
<div class="form-check mb-2">
{{ form.is_accepting_patients }} {{ form.is_accepting_patients }}
<label class="form-check-label" for="{{ form.is_accepting_patients.id_for_label }}"> <label class="form-check-label" for="{{ form.is_accepting_patients.id_for_label }}">
Accepting new patients Accepting new patients
@ -281,71 +211,45 @@
</div> </div>
<!-- Queue Associations --> <!-- Queue Associations -->
<div class="form-section"> <div class="card mb-4">
<div class="section-header"> <div class="card-header">
<h4 class="section-title"> <h5 class="card-title mb-0"><i class="fas fa-link me-2"></i>Queue Associations</h5>
<i class="fas fa-link me-2"></i>Queue Associations
</h4>
<p class="section-description">Associate the queue with providers, specialties, and locations</p>
</div> </div>
<div class="card-body">
<div class="row"> <div class="row mb-3">
<div class="col-md-4"> <div class="col-md-6">
<div class="form-group">
<label for="{{ form.specialty.id_for_label }}" class="form-label">Medical Specialty</label> <label for="{{ form.specialty.id_for_label }}" class="form-label">Medical Specialty</label>
{{ form.specialty }} {{ form.specialty }}
{% if form.specialty.help_text %}
<div class="form-text">{{ form.specialty.help_text }}</div>
{% endif %}
{% if form.specialty.errors %} {% if form.specialty.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">{{ form.specialty.errors }}</div>
{% for error in form.specialty.errors %}{{ error }}{% endfor %}
</div>
{% endif %} {% endif %}
</div> </div>
</div> <div class="col-md-6">
<div class="col-md-4">
<div class="form-group">
<label for="{{ form.location.id_for_label }}" class="form-label">Location</label> <label for="{{ form.location.id_for_label }}" class="form-label">Location</label>
{{ form.location }} {{ form.location }}
{% if form.location.help_text %}
<div class="form-text">{{ form.location.help_text }}</div>
{% endif %}
{% if form.location.errors %} {% if form.location.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">{{ form.location.errors }}</div>
{% for error in form.location.errors %}{{ error }}{% endfor %}
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="mb-3">
<div class="form-group">
<label for="{{ form.providers.id_for_label }}" class="form-label">Assigned Providers</label> <label for="{{ form.providers.id_for_label }}" class="form-label">Assigned Providers</label>
{{ form.providers }} {{ form.providers }}
{% if form.providers.help_text %}
<div class="form-text">{{ form.providers.help_text }}</div>
{% endif %}
{% if form.providers.errors %} {% if form.providers.errors %}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">{{ form.providers.errors }}</div>
{% for error in form.providers.errors %}{{ error }}{% endfor %}
</div>
{% endif %} {% endif %}
</div> <div class="form-text">Select one or more providers for this queue</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Operating Hours --> <!-- Operating Hours -->
<div class="form-section"> <div class="card mb-4">
<div class="section-header"> <div class="card-header">
<h4 class="section-title"> <h5 class="card-title mb-0"><i class="fas fa-clock me-2"></i>Operating Hours</h5>
<i class="fas fa-clock me-2"></i>Operating Hours
</h4>
<p class="section-description">Set the operating hours for each day of the week</p>
</div> </div>
<div class="card-body">
<div class="operating-hours-grid"> <div class="operating-hours-grid">
{% for day in days_of_week %} {% for day in days_of_week %}
<div class="day-schedule"> <div class="day-schedule">
@ -356,7 +260,7 @@
name="operating_hours_{{ day.value }}_enabled" name="operating_hours_{{ day.value }}_enabled"
{% if day.enabled %}checked{% endif %}> {% if day.enabled %}checked{% endif %}>
<label class="form-check-label" for="day_{{ day.value }}_enabled"> <label class="form-check-label" for="day_{{ day.value }}_enabled">
Open on {{ day.name }} Open
</label> </label>
</div> </div>
<div class="time-inputs" id="times_{{ day.value }}" <div class="time-inputs" id="times_{{ day.value }}"
@ -373,15 +277,15 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div>
<!-- Priority Configuration --> <!-- Priority Configuration -->
<div class="form-section"> <div class="card mb-4">
<div class="section-header"> <div class="card-header">
<h4 class="section-title"> <h5 class="card-title mb-0"><i class="fas fa-sort-amount-up me-2"></i>Priority Configuration</h5>
<i class="fas fa-sort-amount-up me-2"></i>Priority Configuration
</h4>
<p class="section-description">Configure priority weights for queue ordering (higher values = higher priority)</p>
</div> </div>
<div class="card-body">
<p class="text-muted mb-3">Configure priority weights for queue ordering (higher values = higher priority)</p>
<div class="priority-weight-item"> <div class="priority-weight-item">
<span class="weight-label">Emergency Cases</span> <span class="weight-label">Emergency Cases</span>
@ -431,68 +335,94 @@
min="0" max="100" step="0.1"> min="0" max="100" step="0.1">
</div> </div>
</div> </div>
</div>
<!-- Form Actions --> <!-- Form Actions -->
<div class="form-section"> <div class="d-flex justify-content-between mt-4">
<div class="d-flex justify-content-between align-items-center"> <a href="{% url 'appointments:waiting_queue_list' %}" class="btn btn-secondary">
<div> <i class="fas fa-times"></i> Cancel
{% if form.instance.pk %}
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
Last updated: {{ form.instance.updated_at|date:"M d, Y g:i A" }}
</small>
{% endif %}
</div>
<div>
<a href="{% url 'appointments:waiting_queue_list' %}" class="btn btn-outline-secondary me-2">
<i class="fas fa-times me-1"></i>Cancel
</a> </a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i> <i class="fas fa-save"></i> {% if object %}Update Queue{% else %}Create Queue{% endif %}
{% if form.instance.pk %}Update Queue{% else %}Create Queue{% endif %}
</button> </button>
</div> </div>
</div>
</div>
</form> </form>
</div>
<!-- end panel-body -->
</div>
<!-- end panel -->
</div>
<!-- end col-8 -->
<!-- begin col-4 -->
<div class="col-xl-4">
<!-- Help Sidebar -->
<div class="help-sidebar">
<h5><i class="fas fa-info-circle"></i> Help & Guidelines</h5>
<div class="card mb-3">
<div class="card-header">
<h6 class="card-title mb-0">Queue Setup Tips</h6>
</div>
<div class="card-body">
<ul class="mb-0">
<li>Choose a descriptive queue name</li>
<li>Set realistic maximum queue size</li>
<li>Estimate average service time accurately</li>
<li>Assign appropriate providers</li>
<li>Configure operating hours for each day</li>
</ul>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h6 class="card-title mb-0">Queue Types</h6>
</div>
<div class="card-body">
<ul class="mb-0">
<li><strong>Provider:</strong> Queue managed by specific providers</li>
<li><strong>Specialty:</strong> Queue for specific medical specialty</li>
<li><strong>Location:</strong> Queue for specific location/department</li>
<li><strong>Procedure:</strong> Queue for specific procedures</li>
<li><strong>Emergency:</strong> Priority queue for emergencies</li>
</ul>
</div>
</div>
<div class="card">
<div class="card-header">
<h6 class="card-title mb-0">Priority Weights</h6>
</div>
<div class="card-body">
<p class="mb-2">Priority weights determine patient order in queue:</p>
<ul class="mb-0">
<li>Higher values = higher priority</li>
<li>Emergency cases should have highest weight</li>
<li>Regular appointments have lowest weight</li>
<li>Adjust based on your facility's needs</li>
</ul>
</div>
</div>
</div>
</div>
<!-- end col-4 -->
</div> </div>
<!-- end row -->
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<!-- Select2 JS -->
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script> <script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
// Initialize Select2 // Initialize Select2
$('.form-select').select2({ $('#{{ form.providers.id_for_label }}').select2({
theme: 'bootstrap-5', theme: 'bootstrap-5',
placeholder: 'Select providers...', placeholder: 'Select providers...',
allowClear: true allowClear: true
}); });
// Queue type descriptions
const typeDescriptions = {
'PROVIDER': 'Queue managed by specific healthcare providers',
'SPECIALTY': 'Queue for patients requiring specific medical specialty',
'LOCATION': 'Queue for patients at a specific location or department',
'PROCEDURE': 'Queue for patients requiring specific procedures',
'EMERGENCY': 'Priority queue for emergency cases'
};
// Show queue type description
$('#id_queue_type').on('change', function() {
const selectedType = $(this).val();
if (selectedType && typeDescriptions[selectedType]) {
$('#type-description').text(typeDescriptions[selectedType]);
$('#type-preview').show();
} else {
$('#type-preview').hide();
}
});
// Trigger on page load
$('#id_queue_type').trigger('change');
// Operating hours toggle // Operating hours toggle
$('[id^="day_"][id$="_enabled"]').on('change', function() { $('[id^="day_"][id$="_enabled"]').on('change', function() {
const dayValue = $(this).attr('id').replace('day_', '').replace('_enabled', ''); const dayValue = $(this).attr('id').replace('day_', '').replace('_enabled', '');
@ -511,23 +441,23 @@ $(document).ready(function() {
const errors = []; const errors = [];
// Validate required fields // Validate required fields
if (!$('#id_name').val().trim()) { if (!$('#{{ form.name.id_for_label }}').val().trim()) {
errors.push('Queue name is required'); errors.push('Queue name is required');
isValid = false; isValid = false;
} }
if (!$('#id_queue_type').val()) { if (!$('#{{ form.queue_type.id_for_label }}').val()) {
errors.push('Queue type is required'); errors.push('Queue type is required');
isValid = false; isValid = false;
} }
const maxSize = parseInt($('#id_max_queue_size').val()); const maxSize = parseInt($('#{{ form.max_queue_size.id_for_label }}').val());
if (!maxSize || maxSize < 1) { if (!maxSize || maxSize < 1) {
errors.push('Maximum queue size must be at least 1'); errors.push('Maximum queue size must be at least 1');
isValid = false; isValid = false;
} }
const serviceTime = parseInt($('#id_average_service_time_minutes').val()); const serviceTime = parseInt($('#{{ form.average_service_time_minutes.id_for_label }}').val());
if (!serviceTime || serviceTime < 1) { if (!serviceTime || serviceTime < 1) {
errors.push('Average service time must be at least 1 minute'); errors.push('Average service time must be at least 1 minute');
isValid = false; isValid = false;
@ -561,41 +491,9 @@ $(document).ready(function() {
if (!isValid) { if (!isValid) {
e.preventDefault(); e.preventDefault();
showAlert('error', errors.join('<br>')); alert(errors.join('\n'));
} }
}); });
// Auto-save draft functionality
let autoSaveTimer;
$('input, select, textarea').on('change input', function() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
// Could implement auto-save here
console.log('Auto-save triggered');
}, 5000);
});
}); });
function showAlert(type, message) {
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
const alertHtml = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
// Remove existing alerts
$('.alert').remove();
// Add new alert at the top of content
$('#content').prepend(alertHtml);
// Auto-dismiss after 8 seconds
setTimeout(function() {
$('.alert').fadeOut();
}, 8000);
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -53,6 +53,7 @@ urlpatterns = [
path('queue/entry/<int:pk>/update/', views.QueueEntryUpdateView.as_view(), name='queue_entry_update'), path('queue/entry/<int:pk>/update/', views.QueueEntryUpdateView.as_view(), name='queue_entry_update'),
path('queue/<int:queue_id>/call-next/', views.next_in_queue, name='call_next_patient'), path('queue/<int:queue_id>/call-next/', views.next_in_queue, name='call_next_patient'),
path('queue/<int:queue_id>/status/', views.queue_status, name='queue_status'), path('queue/<int:queue_id>/status/', views.queue_status, name='queue_status'),
path('queue/<int:pk>/display/', views.queue_display_view, name='queue_display'),
@ -84,4 +85,3 @@ urlpatterns = [
# API endpoints # API endpoints
path('api/', include('appointments.api.urls')), path('api/', include('appointments.api.urls')),
] ]

File diff suppressed because it is too large Load Diff

View File

@ -557,7 +557,15 @@ def create_appointment_requests(tenants, slots, days_back=14, appointments_per_d
for _ in range(daily_appointments): for _ in range(daily_appointments):
patient = random.choice(patients) patient = random.choice(patients)
provider = random.choice([p for p in providers if p.tenant == patient.tenant])
# Get providers for this patient's tenant, with fallback
tenant_providers = [p for p in providers if p.tenant == patient.tenant]
if not tenant_providers:
# If no providers for this tenant, skip this appointment
print(f"Warning: No providers found for tenant {patient.tenant.name}, skipping appointment for {patient.get_full_name()}")
continue
provider = random.choice(tenant_providers)
# Select appointment type and specialty # Select appointment type and specialty
appointment_type = random.choices( appointment_type = random.choices(

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,418 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("billing", "0001_initial"),
("core", "0001_initial"),
("emr", "0001_initial"),
("inpatients", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="billingconfiguration",
name="tenant",
field=models.OneToOneField(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="billing_configuration",
to="core.tenant",
),
),
migrations.AddField(
model_name="billlineitem",
name="rendering_provider",
field=models.ForeignKey(
blank=True,
help_text="Rendering provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="rendered_line_items",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="billlineitem",
name="supervising_provider",
field=models.ForeignKey(
blank=True,
help_text="Supervising provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="supervised_line_items",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="claimstatusupdate",
name="updated_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who made the update",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="claim_status_updates",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="insuranceclaim",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the claim",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_insurance_claims",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="insuranceclaim",
name="insurance_info",
field=models.ForeignKey(
help_text="Insurance information",
on_delete=django.db.models.deletion.CASCADE,
related_name="insurance_claims",
to="patients.insuranceinfo",
),
),
migrations.AddField(
model_name="insuranceclaim",
name="original_claim",
field=models.ForeignKey(
blank=True,
help_text="Original claim if this is a resubmission",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="resubmissions",
to="billing.insuranceclaim",
),
),
migrations.AddField(
model_name="claimstatusupdate",
name="insurance_claim",
field=models.ForeignKey(
help_text="Related insurance claim",
on_delete=django.db.models.deletion.CASCADE,
related_name="status_updates",
to="billing.insuranceclaim",
),
),
migrations.AddField(
model_name="medicalbill",
name="admission",
field=models.ForeignKey(
blank=True,
help_text="Related admission",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="medical_bills",
to="inpatients.admission",
),
),
migrations.AddField(
model_name="medicalbill",
name="attending_provider",
field=models.ForeignKey(
blank=True,
help_text="Attending provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="attending_bills",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="medicalbill",
name="billing_provider",
field=models.ForeignKey(
blank=True,
help_text="Billing provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="billing_provider_bills",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="medicalbill",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the bill",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_medical_bills",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="medicalbill",
name="encounter",
field=models.ForeignKey(
blank=True,
help_text="Related encounter",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="medical_bills",
to="emr.encounter",
),
),
migrations.AddField(
model_name="medicalbill",
name="patient",
field=models.ForeignKey(
help_text="Patient",
on_delete=django.db.models.deletion.CASCADE,
related_name="medical_bills",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="medicalbill",
name="primary_insurance",
field=models.ForeignKey(
blank=True,
help_text="Primary insurance",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="primary_bills",
to="patients.insuranceinfo",
),
),
migrations.AddField(
model_name="medicalbill",
name="secondary_insurance",
field=models.ForeignKey(
blank=True,
help_text="Secondary insurance",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="secondary_bills",
to="patients.insuranceinfo",
),
),
migrations.AddField(
model_name="medicalbill",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="medical_bills",
to="core.tenant",
),
),
migrations.AddField(
model_name="insuranceclaim",
name="medical_bill",
field=models.ForeignKey(
help_text="Related medical bill",
on_delete=django.db.models.deletion.CASCADE,
related_name="insurance_claims",
to="billing.medicalbill",
),
),
migrations.AddField(
model_name="billlineitem",
name="medical_bill",
field=models.ForeignKey(
help_text="Medical bill",
on_delete=django.db.models.deletion.CASCADE,
related_name="line_items",
to="billing.medicalbill",
),
),
migrations.AddField(
model_name="payment",
name="insurance_claim",
field=models.ForeignKey(
blank=True,
help_text="Related insurance claim",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="payments",
to="billing.insuranceclaim",
),
),
migrations.AddField(
model_name="payment",
name="medical_bill",
field=models.ForeignKey(
help_text="Related medical bill",
on_delete=django.db.models.deletion.CASCADE,
related_name="payments",
to="billing.medicalbill",
),
),
migrations.AddField(
model_name="payment",
name="processed_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who processed payment",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="processed_payments",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="payment",
name="received_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who received payment",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="received_payments",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddIndex(
model_name="claimstatusupdate",
index=models.Index(
fields=["insurance_claim"], name="billing_cla_insuran_7c9e49_idx"
),
),
migrations.AddIndex(
model_name="claimstatusupdate",
index=models.Index(
fields=["status_date"], name="billing_cla_status__49d81f_idx"
),
),
migrations.AddIndex(
model_name="claimstatusupdate",
index=models.Index(
fields=["new_status"], name="billing_cla_new_sta_9c28d3_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["tenant", "status"], name="billing_med_tenant__fe8d14_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["patient", "bill_date"], name="billing_med_patient_8f1a85_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["bill_number"], name="billing_med_bill_nu_f01dfa_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["status", "due_date"], name="billing_med_status_cde77f_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["collection_status"], name="billing_med_collect_6d0faf_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["medical_bill"], name="billing_ins_medical_1fec52_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["insurance_info"], name="billing_ins_insuran_e54611_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["claim_number"], name="billing_ins_claim_n_becaa3_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["status", "submission_date"],
name="billing_ins_status_921ea7_idx",
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["response_date"], name="billing_ins_respons_fc4f3d_idx"
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["medical_bill", "line_number"],
name="billing_bil_medical_37a377_idx",
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["service_code"], name="billing_bil_service_b88f5b_idx"
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["service_date"], name="billing_bil_service_658c36_idx"
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["rendering_provider"], name="billing_bil_renderi_2740ad_idx"
),
),
migrations.AlterUniqueTogether(
name="billlineitem",
unique_together={("medical_bill", "line_number")},
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["medical_bill"], name="billing_pay_medical_e4e348_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["payment_number"], name="billing_pay_payment_0825d6_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["payment_date"], name="billing_pay_payment_bed741_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["payment_method"], name="billing_pay_payment_715e62_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(fields=["status"], name="billing_pay_status_b7739e_idx"),
),
]

View File

@ -213,7 +213,7 @@
<td class="fw-bold">Related Encounter:</td> <td class="fw-bold">Related Encounter:</td>
<td> <td>
{% if object.encounter %} {% if object.encounter %}
<a href="{% url 'emr:encounter_detail' object.encounter.encounter_id %}" class="text-decoration-none"> <a href="{% url 'emr:encounter_detail' object.encounter.id %}" class="text-decoration-none">
{{ object.encounter.encounter_number }} {{ object.encounter.encounter_number }}
</a> </a>
{% else %} {% else %}

View File

@ -22,9 +22,9 @@
<i class="fas fa-arrow-left me-2"></i>Back to List <i class="fas fa-arrow-left me-2"></i>Back to List
</a> </a>
{% if object.status == 'PENDING' %} {% if object.status == 'PENDING' %}
<a href="{% url 'billing:payment_update' object.payment_id %}" class="btn btn-primary"> {# <a href="{% url 'billing:payment_update' object.payment_id %}" class="btn btn-primary">#}
<i class="fas fa-edit me-2"></i>Edit Payment {# <i class="fas fa-edit me-2"></i>Edit Payment#}
</a> {# </a>#}
{% endif %} {% endif %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"> <button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
@ -42,17 +42,17 @@
</a></li> </a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% if object.status == 'PENDING' %} {% if object.status == 'PENDING' %}
<li><a class="dropdown-item text-success" href="{% url 'billing:payment_process' object.payment_id %}"> <li><a class="dropdown-item text-success" href="">
<i class="fas fa-check me-2"></i>Process Payment <i class="fas fa-check me-2"></i>Process Payment
</a></li> </a></li>
{% elif object.status == 'COMPLETED' %} {% elif object.status == 'COMPLETED' %}
<li><a class="dropdown-item text-warning" href="{% url 'billing:payment_refund' object.payment_id %}"> <li><a class="dropdown-item text-warning" href="">
<i class="fas fa-undo me-2"></i>Process Refund <i class="fas fa-undo me-2"></i>Process Refund
</a></li> </a></li>
{% endif %} {% endif %}
{% if object.status == 'PENDING' %} {% if object.status == 'PENDING' %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="{% url 'billing:payment_delete' object.payment_id %}"> <li><a class="dropdown-item text-danger" href="">
<i class="fas fa-trash me-2"></i>Delete Payment <i class="fas fa-trash me-2"></i>Delete Payment
</a></li> </a></li>
{% endif %} {% endif %}
@ -466,10 +466,10 @@
<i class="fas fa-file-invoice me-2"></i>View Bill <i class="fas fa-file-invoice me-2"></i>View Bill
</a> </a>
{% if object.status == 'PENDING' %} {% if object.status == 'PENDING' %}
<a href="{% url 'billing:payment_update' object.payment_id %}" class="btn btn-outline-warning"> {# <a href="{% url 'billing:payment_update' object.payment_id %}" class="btn btn-outline-warning">#}
<i class="fas fa-edit me-2"></i>Edit Payment {# <i class="fas fa-edit me-2"></i>Edit Payment#}
</a> {# </a>#}
<a href="{% url 'billing:payment_process' object.payment_id %}" class="btn btn-success"> <a href="" class="btn btn-success">
<i class="fas fa-check me-2"></i>Process Payment <i class="fas fa-check me-2"></i>Process Payment
</a> </a>
{% elif object.status == 'COMPLETED' %} {% elif object.status == 'COMPLETED' %}

View File

@ -0,0 +1,673 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="BloodComponent",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
choices=[
("WHOLE_BLOOD", "Whole Blood"),
("PACKED_RBC", "Packed Red Blood Cells"),
("FRESH_FROZEN_PLASMA", "Fresh Frozen Plasma"),
("PLATELETS", "Platelets"),
("CRYOPRECIPITATE", "Cryoprecipitate"),
("GRANULOCYTES", "Granulocytes"),
],
max_length=50,
unique=True,
),
),
("description", models.TextField()),
("shelf_life_days", models.PositiveIntegerField()),
("storage_temperature", models.CharField(max_length=50)),
("volume_ml", models.PositiveIntegerField()),
("is_active", models.BooleanField(default=True)),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="BloodTest",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"test_type",
models.CharField(
choices=[
("ABO_RH", "ABO/Rh Typing"),
("ANTIBODY_SCREEN", "Antibody Screening"),
("HIV", "HIV"),
("HBV", "Hepatitis B"),
("HCV", "Hepatitis C"),
("SYPHILIS", "Syphilis"),
("HTLV", "HTLV"),
("CMV", "CMV"),
("MALARIA", "Malaria"),
],
max_length=20,
),
),
(
"result",
models.CharField(
choices=[
("POSITIVE", "Positive"),
("NEGATIVE", "Negative"),
("INDETERMINATE", "Indeterminate"),
("PENDING", "Pending"),
],
default="PENDING",
max_length=15,
),
),
("test_date", models.DateTimeField()),
("equipment_used", models.CharField(blank=True, max_length=100)),
("lot_number", models.CharField(blank=True, max_length=50)),
("notes", models.TextField(blank=True)),
("verified_at", models.DateTimeField(blank=True, null=True)),
],
options={
"ordering": ["-test_date"],
},
),
migrations.CreateModel(
name="BloodUnit",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("unit_number", models.CharField(max_length=20, unique=True)),
("collection_date", models.DateTimeField()),
("expiry_date", models.DateTimeField()),
("volume_ml", models.PositiveIntegerField()),
(
"status",
models.CharField(
choices=[
("COLLECTED", "Collected"),
("TESTING", "Testing"),
("QUARANTINE", "Quarantine"),
("AVAILABLE", "Available"),
("RESERVED", "Reserved"),
("ISSUED", "Issued"),
("TRANSFUSED", "Transfused"),
("EXPIRED", "Expired"),
("DISCARDED", "Discarded"),
],
default="COLLECTED",
max_length=20,
),
),
("location", models.CharField(max_length=100)),
("bag_type", models.CharField(max_length=50)),
("anticoagulant", models.CharField(default="CPDA-1", max_length=50)),
("collection_site", models.CharField(max_length=100)),
("notes", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["-collection_date"],
},
),
migrations.CreateModel(
name="CrossMatch",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"test_type",
models.CharField(
choices=[
("MAJOR", "Major Crossmatch"),
("MINOR", "Minor Crossmatch"),
("IMMEDIATE_SPIN", "Immediate Spin"),
("ANTIGLOBULIN", "Antiglobulin Test"),
],
max_length=20,
),
),
(
"compatibility",
models.CharField(
choices=[
("COMPATIBLE", "Compatible"),
("INCOMPATIBLE", "Incompatible"),
("PENDING", "Pending"),
],
default="PENDING",
max_length=15,
),
),
("test_date", models.DateTimeField()),
("temperature", models.CharField(default="37°C", max_length=20)),
("incubation_time", models.PositiveIntegerField(default=15)),
("notes", models.TextField(blank=True)),
("verified_at", models.DateTimeField(blank=True, null=True)),
],
options={
"ordering": ["-test_date"],
},
),
migrations.CreateModel(
name="Donor",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("donor_id", models.CharField(max_length=20, unique=True)),
("first_name", models.CharField(max_length=100)),
("last_name", models.CharField(max_length=100)),
("date_of_birth", models.DateField()),
(
"gender",
models.CharField(
choices=[
("MALE", "Male"),
("FEMALE", "Female"),
("OTHER", "Other"),
],
max_length=10,
),
),
("national_id", models.CharField(max_length=10, unique=True)),
("phone", models.CharField(max_length=20)),
("email", models.EmailField(blank=True, max_length=254)),
("address", models.TextField()),
("emergency_contact_name", models.CharField(max_length=100)),
("emergency_contact_phone", models.CharField(max_length=20)),
(
"donor_type",
models.CharField(
choices=[
("VOLUNTARY", "Voluntary"),
("REPLACEMENT", "Replacement"),
("AUTOLOGOUS", "Autologous"),
("DIRECTED", "Directed"),
],
default="VOLUNTARY",
max_length=20,
),
),
(
"status",
models.CharField(
choices=[
("ACTIVE", "Active"),
("DEFERRED", "Deferred"),
("PERMANENTLY_DEFERRED", "Permanently Deferred"),
("INACTIVE", "Inactive"),
],
default="ACTIVE",
max_length=20,
),
),
("registration_date", models.DateTimeField(auto_now_add=True)),
("last_donation_date", models.DateTimeField(blank=True, null=True)),
("total_donations", models.PositiveIntegerField(default=0)),
(
"weight",
models.FloatField(
validators=[django.core.validators.MinValueValidator(45.0)]
),
),
(
"height",
models.FloatField(
validators=[django.core.validators.MinValueValidator(140.0)]
),
),
("notes", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["-registration_date"],
},
),
migrations.CreateModel(
name="InventoryLocation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100, unique=True)),
(
"location_type",
models.CharField(
choices=[
("REFRIGERATOR", "Refrigerator"),
("FREEZER", "Freezer"),
("PLATELET_AGITATOR", "Platelet Agitator"),
("QUARANTINE", "Quarantine"),
("TESTING", "Testing Area"),
],
max_length=20,
),
),
("temperature_range", models.CharField(max_length=50)),
("temperature", models.FloatField(blank=True, null=True)),
("capacity", models.PositiveIntegerField()),
("current_stock", models.PositiveIntegerField(default=0)),
("is_active", models.BooleanField(default=True)),
("notes", models.TextField(blank=True)),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="QualityControl",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"test_type",
models.CharField(
choices=[
("TEMPERATURE_MONITORING", "Temperature Monitoring"),
("EQUIPMENT_CALIBRATION", "Equipment Calibration"),
("REAGENT_TESTING", "Reagent Testing"),
("PROFICIENCY_TESTING", "Proficiency Testing"),
("PROCESS_VALIDATION", "Process Validation"),
],
max_length=30,
),
),
("test_date", models.DateTimeField()),
("equipment_tested", models.CharField(blank=True, max_length=100)),
("parameters_tested", models.TextField()),
("expected_results", models.TextField()),
("actual_results", models.TextField()),
(
"status",
models.CharField(
choices=[
("PASS", "Pass"),
("FAIL", "Fail"),
("PENDING", "Pending"),
],
max_length=10,
),
),
("review_date", models.DateTimeField(blank=True, null=True)),
("review_notes", models.TextField(blank=True)),
("corrective_action", models.TextField(blank=True)),
("next_test_date", models.DateTimeField(blank=True, null=True)),
("capa_initiated", models.BooleanField(default=False)),
("capa_number", models.CharField(blank=True, max_length=50)),
(
"capa_priority",
models.CharField(
blank=True,
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
],
max_length=10,
),
),
("capa_date", models.DateTimeField(blank=True, null=True)),
("capa_assessment", models.TextField(blank=True)),
(
"capa_status",
models.CharField(
blank=True,
choices=[
("OPEN", "Open"),
("IN_PROGRESS", "In Progress"),
("CLOSED", "Closed"),
],
max_length=20,
),
),
],
options={
"ordering": ["-test_date"],
},
),
migrations.CreateModel(
name="Transfusion",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("start_time", models.DateTimeField()),
("end_time", models.DateTimeField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[
("STARTED", "Started"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("STOPPED", "Stopped"),
("ADVERSE_REACTION", "Adverse Reaction"),
],
default="STARTED",
max_length=20,
),
),
(
"volume_transfused",
models.PositiveIntegerField(blank=True, null=True),
),
("transfusion_rate", models.CharField(blank=True, max_length=50)),
("pre_transfusion_vitals", models.JSONField(default=dict)),
("post_transfusion_vitals", models.JSONField(default=dict)),
("vital_signs_history", models.JSONField(default=list)),
("current_blood_pressure", models.CharField(blank=True, max_length=20)),
("current_heart_rate", models.IntegerField(blank=True, null=True)),
("current_temperature", models.FloatField(blank=True, null=True)),
(
"current_respiratory_rate",
models.IntegerField(blank=True, null=True),
),
(
"current_oxygen_saturation",
models.IntegerField(blank=True, null=True),
),
("last_vitals_check", models.DateTimeField(blank=True, null=True)),
("patient_consent", models.BooleanField(default=False)),
("consent_date", models.DateTimeField(blank=True, null=True)),
("notes", models.TextField(blank=True)),
("stop_reason", models.TextField(blank=True)),
("completion_notes", models.TextField(blank=True)),
],
options={
"ordering": ["-start_time"],
},
),
migrations.CreateModel(
name="AdverseReaction",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"reaction_type",
models.CharField(
choices=[
("FEBRILE", "Febrile Non-Hemolytic"),
("ALLERGIC", "Allergic"),
("HEMOLYTIC_ACUTE", "Acute Hemolytic"),
("HEMOLYTIC_DELAYED", "Delayed Hemolytic"),
("ANAPHYLACTIC", "Anaphylactic"),
("SEPTIC", "Septic"),
("CIRCULATORY_OVERLOAD", "Circulatory Overload"),
("LUNG_INJURY", "Transfusion-Related Acute Lung Injury"),
("OTHER", "Other"),
],
max_length=30,
),
),
(
"severity",
models.CharField(
choices=[
("MILD", "Mild"),
("MODERATE", "Moderate"),
("SEVERE", "Severe"),
("LIFE_THREATENING", "Life Threatening"),
],
max_length=20,
),
),
("onset_time", models.DateTimeField()),
("symptoms", models.TextField()),
("treatment_given", models.TextField()),
("outcome", models.TextField()),
("investigation_notes", models.TextField(blank=True)),
("regulatory_reported", models.BooleanField(default=False)),
("report_date", models.DateTimeField(blank=True, null=True)),
(
"investigated_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="investigated_reactions",
to=settings.AUTH_USER_MODEL,
),
),
(
"reported_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="reported_reactions",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-onset_time"],
},
),
migrations.CreateModel(
name="BloodGroup",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"abo_type",
models.CharField(
choices=[("A", "A"), ("B", "B"), ("AB", "AB"), ("O", "O")],
max_length=2,
),
),
(
"rh_factor",
models.CharField(
choices=[("POS", "Positive"), ("NEG", "Negative")], max_length=8
),
),
],
options={
"ordering": ["abo_type", "rh_factor"],
"unique_together": {("abo_type", "rh_factor")},
},
),
migrations.CreateModel(
name="BloodIssue",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("issue_date", models.DateTimeField(auto_now_add=True)),
("expiry_time", models.DateTimeField()),
("returned", models.BooleanField(default=False)),
("return_date", models.DateTimeField(blank=True, null=True)),
("return_reason", models.TextField(blank=True)),
("notes", models.TextField(blank=True)),
(
"issued_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="issued_units",
to=settings.AUTH_USER_MODEL,
),
),
(
"issued_to",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="received_units",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-issue_date"],
},
),
migrations.CreateModel(
name="BloodRequest",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("request_number", models.CharField(max_length=20, unique=True)),
(
"units_requested",
models.PositiveIntegerField(
validators=[django.core.validators.MinValueValidator(1)]
),
),
(
"urgency",
models.CharField(
choices=[
("ROUTINE", "Routine"),
("URGENT", "Urgent"),
("EMERGENCY", "Emergency"),
],
default="ROUTINE",
max_length=10,
),
),
("indication", models.TextField()),
("special_requirements", models.TextField(blank=True)),
("hemoglobin_level", models.FloatField(blank=True, null=True)),
("platelet_count", models.IntegerField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("PROCESSING", "Processing"),
("READY", "Ready"),
("ISSUED", "Issued"),
("COMPLETED", "Completed"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
max_length=15,
),
),
("request_date", models.DateTimeField(auto_now_add=True)),
("required_by", models.DateTimeField()),
("processed_at", models.DateTimeField(blank=True, null=True)),
("notes", models.TextField(blank=True)),
("cancellation_reason", models.TextField(blank=True)),
("cancellation_date", models.DateTimeField(blank=True, null=True)),
(
"cancelled_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="cancelled_requests",
to=settings.AUTH_USER_MODEL,
),
),
(
"component_requested",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.bloodcomponent",
),
),
],
options={
"ordering": ["-request_date"],
},
),
]

View File

@ -0,0 +1,297 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("blood_bank", "0001_initial"),
("hr", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="bloodrequest",
name="patient",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_requests",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="bloodrequest",
name="patient_blood_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="blood_bank.bloodgroup"
),
),
migrations.AddField(
model_name="bloodrequest",
name="processed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="processed_requests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodrequest",
name="requesting_department",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="hr.department"
),
),
migrations.AddField(
model_name="bloodrequest",
name="requesting_physician",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_requests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodissue",
name="blood_request",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="issues",
to="blood_bank.bloodrequest",
),
),
migrations.AddField(
model_name="bloodtest",
name="tested_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
),
),
migrations.AddField(
model_name="bloodtest",
name="verified_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="verified_tests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodunit",
name="blood_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="blood_bank.bloodgroup"
),
),
migrations.AddField(
model_name="bloodunit",
name="collected_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="collected_units",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodunit",
name="component",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.bloodcomponent",
),
),
migrations.AddField(
model_name="bloodtest",
name="blood_unit",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tests",
to="blood_bank.bloodunit",
),
),
migrations.AddField(
model_name="bloodissue",
name="blood_unit",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="issue",
to="blood_bank.bloodunit",
),
),
migrations.AddField(
model_name="crossmatch",
name="blood_unit",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="crossmatches",
to="blood_bank.bloodunit",
),
),
migrations.AddField(
model_name="crossmatch",
name="recipient",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="crossmatch",
name="tested_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
),
),
migrations.AddField(
model_name="crossmatch",
name="verified_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="verified_crossmatches",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodissue",
name="crossmatch",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.crossmatch",
),
),
migrations.AddField(
model_name="donor",
name="blood_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="blood_bank.bloodgroup"
),
),
migrations.AddField(
model_name="donor",
name="created_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="created_donors",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodunit",
name="donor",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_units",
to="blood_bank.donor",
),
),
migrations.AddField(
model_name="qualitycontrol",
name="capa_initiated_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="initiated_capas",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="qualitycontrol",
name="performed_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="qc_tests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="qualitycontrol",
name="reviewed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="reviewed_qc_tests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="administered_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="administered_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="blood_issue",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="transfusion",
to="blood_bank.bloodissue",
),
),
migrations.AddField(
model_name="transfusion",
name="completed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="completed_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="stopped_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="stopped_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="witnessed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="witnessed_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="adversereaction",
name="transfusion",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="adverse_reactions",
to="blood_bank.transfusion",
),
),
migrations.AlterUniqueTogether(
name="bloodtest",
unique_together={("blood_unit", "test_type")},
),
]

View File

@ -0,0 +1,968 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="CommunicationChannel",
fields=[
(
"channel_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the channel",
primary_key=True,
serialize=False,
),
),
("name", models.CharField(help_text="Channel name", max_length=255)),
(
"description",
models.TextField(
blank=True, help_text="Channel description", null=True
),
),
(
"channel_type",
models.CharField(
choices=[
("EMAIL", "Email"),
("SMS", "SMS"),
("PUSH", "Push Notification"),
("SLACK", "Slack"),
("TEAMS", "Microsoft Teams"),
("WEBHOOK", "Webhook"),
("PHONE", "Phone Call"),
("FAX", "Fax"),
("PAGER", "Pager"),
],
help_text="Type of communication channel",
max_length=20,
),
),
(
"provider_type",
models.CharField(
choices=[
("SMTP", "SMTP Email"),
("SENDGRID", "SendGrid"),
("MAILGUN", "Mailgun"),
("TWILIO", "Twilio SMS"),
("AWS_SNS", "AWS SNS"),
("FIREBASE", "Firebase"),
("SLACK_API", "Slack API"),
("TEAMS_API", "Teams API"),
("WEBHOOK", "Webhook"),
("CUSTOM", "Custom Provider"),
],
help_text="Provider type",
max_length=20,
),
),
(
"configuration",
models.JSONField(
default=dict, help_text="Channel configuration settings"
),
),
(
"authentication_config",
models.JSONField(
default=dict, help_text="Authentication configuration"
),
),
(
"rate_limits",
models.JSONField(
default=dict, help_text="Rate limiting configuration"
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Channel is active"),
),
(
"is_healthy",
models.BooleanField(
default=True, help_text="Channel health status"
),
),
(
"last_health_check",
models.DateTimeField(
blank=True, help_text="Last health check timestamp", null=True
),
),
(
"health_check_interval",
models.PositiveIntegerField(
default=300, help_text="Health check interval in seconds"
),
),
(
"message_count",
models.PositiveIntegerField(
default=0, help_text="Total messages sent through channel"
),
),
(
"success_count",
models.PositiveIntegerField(
default=0, help_text="Successful message deliveries"
),
),
(
"failure_count",
models.PositiveIntegerField(
default=0, help_text="Failed message deliveries"
),
),
(
"last_used_at",
models.DateTimeField(
blank=True, help_text="Last usage timestamp", null=True
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, help_text="Channel creation timestamp"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, help_text="Last update timestamp"
),
),
],
options={
"db_table": "communications_communication_channel",
},
),
migrations.CreateModel(
name="DeliveryLog",
fields=[
(
"log_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the delivery log",
primary_key=True,
serialize=False,
),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("PROCESSING", "Processing"),
("SENT", "Sent"),
("DELIVERED", "Delivered"),
("FAILED", "Failed"),
("BOUNCED", "Bounced"),
("REJECTED", "Rejected"),
("TIMEOUT", "Timeout"),
],
default="PENDING",
help_text="Delivery status",
max_length=20,
),
),
(
"attempt_number",
models.PositiveIntegerField(
default=1, help_text="Delivery attempt number"
),
),
(
"started_at",
models.DateTimeField(
auto_now_add=True, help_text="Delivery start timestamp"
),
),
(
"completed_at",
models.DateTimeField(
blank=True, help_text="Delivery completion timestamp", null=True
),
),
(
"external_id",
models.CharField(
blank=True,
help_text="External delivery ID",
max_length=255,
null=True,
),
),
(
"response_code",
models.CharField(
blank=True,
help_text="Response code from provider",
max_length=50,
null=True,
),
),
(
"response_message",
models.TextField(
blank=True,
help_text="Response message from provider",
null=True,
),
),
(
"error_details",
models.JSONField(
default=dict, help_text="Detailed error information"
),
),
(
"processing_time_ms",
models.PositiveIntegerField(
blank=True,
help_text="Processing time in milliseconds",
null=True,
),
),
(
"payload_size_bytes",
models.PositiveIntegerField(
blank=True, help_text="Payload size in bytes", null=True
),
),
(
"metadata",
models.JSONField(
default=dict, help_text="Additional delivery metadata"
),
),
],
options={
"db_table": "communications_delivery_log",
"ordering": ["-started_at"],
},
),
migrations.CreateModel(
name="Message",
fields=[
(
"message_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the message",
primary_key=True,
serialize=False,
),
),
(
"subject",
models.CharField(help_text="Message subject line", max_length=255),
),
("content", models.TextField(help_text="Message content/body")),
(
"message_type",
models.CharField(
choices=[
("INTERNAL", "Internal Message"),
("EMAIL", "Email"),
("SMS", "SMS"),
("PUSH", "Push Notification"),
("SLACK", "Slack Message"),
("TEAMS", "Microsoft Teams"),
("WEBHOOK", "Webhook"),
("SYSTEM", "System Message"),
("ALERT", "Alert Message"),
],
default="INTERNAL",
help_text="Type of message",
max_length=20,
),
),
(
"priority",
models.CharField(
choices=[
("LOW", "Low"),
("NORMAL", "Normal"),
("HIGH", "High"),
("URGENT", "Urgent"),
("CRITICAL", "Critical"),
],
default="NORMAL",
help_text="Message priority level",
max_length=20,
),
),
(
"status",
models.CharField(
choices=[
("DRAFT", "Draft"),
("PENDING", "Pending"),
("SENDING", "Sending"),
("SENT", "Sent"),
("DELIVERED", "Delivered"),
("READ", "Read"),
("FAILED", "Failed"),
("CANCELLED", "Cancelled"),
],
default="DRAFT",
help_text="Message status",
max_length=20,
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, help_text="Message creation timestamp"
),
),
(
"scheduled_at",
models.DateTimeField(
blank=True, help_text="Scheduled send time", null=True
),
),
(
"sent_at",
models.DateTimeField(
blank=True, help_text="Actual send timestamp", null=True
),
),
(
"expires_at",
models.DateTimeField(
blank=True, help_text="Message expiration time", null=True
),
),
(
"is_urgent",
models.BooleanField(default=False, help_text="Urgent message flag"),
),
(
"requires_acknowledgment",
models.BooleanField(
default=False, help_text="Requires recipient acknowledgment"
),
),
(
"is_confidential",
models.BooleanField(
default=False, help_text="Confidential message flag"
),
),
(
"delivery_attempts",
models.PositiveIntegerField(
default=0, help_text="Number of delivery attempts"
),
),
(
"max_delivery_attempts",
models.PositiveIntegerField(
default=3, help_text="Maximum delivery attempts"
),
),
(
"message_thread_id",
models.UUIDField(
blank=True,
help_text="Thread ID for message grouping",
null=True,
),
),
(
"external_message_id",
models.CharField(
blank=True,
help_text="External system message ID",
max_length=255,
null=True,
),
),
(
"has_attachments",
models.BooleanField(
default=False, help_text="Message has attachments"
),
),
(
"content_type",
models.CharField(
default="text/plain",
help_text="Content MIME type",
max_length=50,
),
),
],
options={
"db_table": "communications_message",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="MessageRecipient",
fields=[
(
"recipient_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the recipient",
primary_key=True,
serialize=False,
),
),
(
"recipient_type",
models.CharField(
choices=[
("USER", "User"),
("EMAIL", "Email Address"),
("PHONE", "Phone Number"),
("ROLE", "User Role"),
("DEPARTMENT", "Department"),
("GROUP", "User Group"),
],
help_text="Type of recipient",
max_length=20,
),
),
(
"email_address",
models.EmailField(
blank=True,
help_text="Email address recipient",
max_length=254,
null=True,
),
),
(
"phone_number",
models.CharField(
blank=True,
help_text="Phone number recipient",
max_length=20,
null=True,
),
),
(
"role_name",
models.CharField(
blank=True,
help_text="Role name for role-based recipients",
max_length=100,
null=True,
),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("SENT", "Sent"),
("DELIVERED", "Delivered"),
("READ", "Read"),
("ACKNOWLEDGED", "Acknowledged"),
("FAILED", "Failed"),
("BOUNCED", "Bounced"),
("UNSUBSCRIBED", "Unsubscribed"),
],
default="PENDING",
help_text="Delivery status",
max_length=20,
),
),
(
"sent_at",
models.DateTimeField(
blank=True, help_text="Sent timestamp", null=True
),
),
(
"delivered_at",
models.DateTimeField(
blank=True, help_text="Delivered timestamp", null=True
),
),
(
"read_at",
models.DateTimeField(
blank=True, help_text="Read timestamp", null=True
),
),
(
"acknowledged_at",
models.DateTimeField(
blank=True, help_text="Acknowledged timestamp", null=True
),
),
(
"delivery_attempts",
models.PositiveIntegerField(
default=0, help_text="Number of delivery attempts"
),
),
(
"last_attempt_at",
models.DateTimeField(
blank=True,
help_text="Last delivery attempt timestamp",
null=True,
),
),
(
"error_message",
models.TextField(
blank=True, help_text="Last delivery error message", null=True
),
),
(
"external_delivery_id",
models.CharField(
blank=True,
help_text="External delivery tracking ID",
max_length=255,
null=True,
),
),
],
options={
"db_table": "communications_message_recipient",
},
),
migrations.CreateModel(
name="NotificationTemplate",
fields=[
(
"template_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the template",
primary_key=True,
serialize=False,
),
),
("name", models.CharField(help_text="Template name", max_length=255)),
(
"description",
models.TextField(
blank=True, help_text="Template description", null=True
),
),
(
"template_type",
models.CharField(
choices=[
("EMAIL", "Email Template"),
("SMS", "SMS Template"),
("PUSH", "Push Notification Template"),
("SLACK", "Slack Template"),
("TEAMS", "Teams Template"),
("WEBHOOK", "Webhook Template"),
("SYSTEM", "System Notification Template"),
],
help_text="Type of template",
max_length=20,
),
),
(
"category",
models.CharField(
choices=[
("APPOINTMENT", "Appointment Notifications"),
("MEDICATION", "Medication Reminders"),
("LAB_RESULTS", "Lab Results"),
("BILLING", "Billing Notifications"),
("EMERGENCY", "Emergency Alerts"),
("SYSTEM", "System Notifications"),
("MARKETING", "Marketing Communications"),
("CLINICAL", "Clinical Notifications"),
("ADMINISTRATIVE", "Administrative Messages"),
("QUALITY", "Quality Alerts"),
],
help_text="Template category",
max_length=30,
),
),
(
"subject_template",
models.CharField(
blank=True,
help_text="Subject line template",
max_length=255,
null=True,
),
),
(
"content_template",
models.TextField(help_text="Message content template"),
),
(
"variables",
models.JSONField(
default=dict, help_text="Available template variables"
),
),
(
"default_values",
models.JSONField(default=dict, help_text="Default variable values"),
),
(
"formatting_rules",
models.JSONField(
default=dict, help_text="Content formatting rules"
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Template is active"),
),
(
"is_system_template",
models.BooleanField(
default=False, help_text="System-defined template"
),
),
(
"requires_approval",
models.BooleanField(
default=False, help_text="Requires approval before use"
),
),
(
"usage_count",
models.PositiveIntegerField(
default=0, help_text="Number of times template has been used"
),
),
(
"last_used_at",
models.DateTimeField(
blank=True, help_text="Last usage timestamp", null=True
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, help_text="Template creation timestamp"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, help_text="Last update timestamp"
),
),
],
options={
"db_table": "communications_notification_template",
},
),
migrations.CreateModel(
name="AlertInstance",
fields=[
(
"alert_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the alert instance",
primary_key=True,
serialize=False,
),
),
("title", models.CharField(help_text="Alert title", max_length=255)),
("description", models.TextField(help_text="Alert description")),
(
"severity",
models.CharField(
choices=[
("INFO", "Information"),
("WARNING", "Warning"),
("ERROR", "Error"),
("CRITICAL", "Critical"),
("EMERGENCY", "Emergency"),
],
help_text="Alert severity level",
max_length=20,
),
),
(
"trigger_data",
models.JSONField(
default=dict, help_text="Data that triggered the alert"
),
),
(
"context_data",
models.JSONField(default=dict, help_text="Additional context data"),
),
(
"status",
models.CharField(
choices=[
("ACTIVE", "Active"),
("ACKNOWLEDGED", "Acknowledged"),
("RESOLVED", "Resolved"),
("SUPPRESSED", "Suppressed"),
("ESCALATED", "Escalated"),
("EXPIRED", "Expired"),
],
default="ACTIVE",
help_text="Alert status",
max_length=20,
),
),
(
"triggered_at",
models.DateTimeField(
auto_now_add=True, help_text="Alert trigger timestamp"
),
),
(
"acknowledged_at",
models.DateTimeField(
blank=True, help_text="Acknowledgment timestamp", null=True
),
),
(
"resolved_at",
models.DateTimeField(
blank=True, help_text="Resolution timestamp", null=True
),
),
(
"expires_at",
models.DateTimeField(
blank=True, help_text="Alert expiration time", null=True
),
),
(
"resolution_notes",
models.TextField(
blank=True, help_text="Resolution notes", null=True
),
),
(
"escalation_level",
models.PositiveIntegerField(
default=0, help_text="Current escalation level"
),
),
(
"escalated_at",
models.DateTimeField(
blank=True, help_text="Last escalation timestamp", null=True
),
),
(
"notifications_sent",
models.PositiveIntegerField(
default=0, help_text="Number of notifications sent"
),
),
(
"last_notification_at",
models.DateTimeField(
blank=True, help_text="Last notification timestamp", null=True
),
),
(
"acknowledged_by",
models.ForeignKey(
blank=True,
help_text="User who acknowledged the alert",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="acknowledged_alerts",
to=settings.AUTH_USER_MODEL,
),
),
(
"resolved_by",
models.ForeignKey(
blank=True,
help_text="User who resolved the alert",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="resolved_alerts",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "communications_alert_instance",
"ordering": ["-triggered_at"],
},
),
migrations.CreateModel(
name="AlertRule",
fields=[
(
"rule_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique identifier for the alert rule",
primary_key=True,
serialize=False,
),
),
("name", models.CharField(help_text="Alert rule name", max_length=255)),
(
"description",
models.TextField(
blank=True, help_text="Alert rule description", null=True
),
),
(
"trigger_type",
models.CharField(
choices=[
("THRESHOLD", "Threshold Alert"),
("PATTERN", "Pattern Alert"),
("SCHEDULE", "Scheduled Alert"),
("EVENT", "Event-based Alert"),
("ANOMALY", "Anomaly Detection"),
("SYSTEM", "System Alert"),
("CLINICAL", "Clinical Alert"),
("OPERATIONAL", "Operational Alert"),
],
help_text="Type of alert trigger",
max_length=20,
),
),
(
"severity",
models.CharField(
choices=[
("INFO", "Information"),
("WARNING", "Warning"),
("ERROR", "Error"),
("CRITICAL", "Critical"),
("EMERGENCY", "Emergency"),
],
default="WARNING",
help_text="Alert severity level",
max_length=20,
),
),
(
"trigger_conditions",
models.JSONField(
default=dict, help_text="Conditions that trigger the alert"
),
),
(
"evaluation_frequency",
models.PositiveIntegerField(
default=300, help_text="Evaluation frequency in seconds"
),
),
(
"cooldown_period",
models.PositiveIntegerField(
default=3600,
help_text="Cooldown period between alerts in seconds",
),
),
(
"notification_channels",
models.JSONField(
default=list, help_text="Notification channels to use"
),
),
(
"escalation_rules",
models.JSONField(
default=dict, help_text="Escalation configuration"
),
),
(
"recipient_roles",
models.JSONField(default=list, help_text="Recipient roles"),
),
(
"is_active",
models.BooleanField(default=True, help_text="Alert rule is active"),
),
(
"is_system_rule",
models.BooleanField(default=False, help_text="System-defined rule"),
),
(
"trigger_count",
models.PositiveIntegerField(
default=0, help_text="Number of times rule has triggered"
),
),
(
"last_triggered_at",
models.DateTimeField(
blank=True, help_text="Last trigger timestamp", null=True
),
),
(
"last_evaluated_at",
models.DateTimeField(
blank=True, help_text="Last evaluation timestamp", null=True
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, help_text="Rule creation timestamp"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, help_text="Last update timestamp"
),
),
(
"created_by",
models.ForeignKey(
blank=True,
help_text="Rule creator",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_alert_rules",
to=settings.AUTH_USER_MODEL,
),
),
(
"default_recipients",
models.ManyToManyField(
blank=True,
help_text="Default alert recipients",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "communications_alert_rule",
},
),
]

View File

@ -0,0 +1,375 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("communications", "0001_initial"),
("core", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="alertrule",
name="tenant",
field=models.ForeignKey(
help_text="Tenant organization",
on_delete=django.db.models.deletion.CASCADE,
to="core.tenant",
),
),
migrations.AddField(
model_name="alertinstance",
name="alert_rule",
field=models.ForeignKey(
help_text="Associated alert rule",
on_delete=django.db.models.deletion.CASCADE,
related_name="instances",
to="communications.alertrule",
),
),
migrations.AddField(
model_name="communicationchannel",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="Channel creator",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="communicationchannel",
name="tenant",
field=models.ForeignKey(
help_text="Tenant organization",
on_delete=django.db.models.deletion.CASCADE,
to="core.tenant",
),
),
migrations.AddField(
model_name="deliverylog",
name="channel",
field=models.ForeignKey(
help_text="Communication channel used",
on_delete=django.db.models.deletion.CASCADE,
related_name="delivery_logs",
to="communications.communicationchannel",
),
),
migrations.AddField(
model_name="message",
name="reply_to_message",
field=models.ForeignKey(
blank=True,
help_text="Original message if this is a reply",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="communications.message",
),
),
migrations.AddField(
model_name="message",
name="sender",
field=models.ForeignKey(
help_text="Message sender",
on_delete=django.db.models.deletion.CASCADE,
related_name="sent_messages",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="message",
name="tenant",
field=models.ForeignKey(
help_text="Tenant organization",
on_delete=django.db.models.deletion.CASCADE,
to="core.tenant",
),
),
migrations.AddField(
model_name="deliverylog",
name="message",
field=models.ForeignKey(
help_text="Associated message",
on_delete=django.db.models.deletion.CASCADE,
related_name="delivery_logs",
to="communications.message",
),
),
migrations.AddField(
model_name="messagerecipient",
name="message",
field=models.ForeignKey(
help_text="Associated message",
on_delete=django.db.models.deletion.CASCADE,
related_name="recipients",
to="communications.message",
),
),
migrations.AddField(
model_name="messagerecipient",
name="user",
field=models.ForeignKey(
blank=True,
help_text="User recipient",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="deliverylog",
name="recipient",
field=models.ForeignKey(
help_text="Associated recipient",
on_delete=django.db.models.deletion.CASCADE,
related_name="delivery_logs",
to="communications.messagerecipient",
),
),
migrations.AddField(
model_name="notificationtemplate",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="Template creator",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="notificationtemplate",
name="tenant",
field=models.ForeignKey(
help_text="Tenant organization",
on_delete=django.db.models.deletion.CASCADE,
to="core.tenant",
),
),
migrations.AddField(
model_name="alertrule",
name="notification_template",
field=models.ForeignKey(
blank=True,
help_text="Notification template to use",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="communications.notificationtemplate",
),
),
migrations.AddIndex(
model_name="alertinstance",
index=models.Index(
fields=["alert_rule", "status"], name="communicati_alert_r_69aa1e_idx"
),
),
migrations.AddIndex(
model_name="alertinstance",
index=models.Index(
fields=["severity", "triggered_at"],
name="communicati_severit_65b0c9_idx",
),
),
migrations.AddIndex(
model_name="alertinstance",
index=models.Index(
fields=["status", "triggered_at"], name="communicati_status_402adb_idx"
),
),
migrations.AddIndex(
model_name="alertinstance",
index=models.Index(
fields=["expires_at"], name="communicati_expires_2c10ee_idx"
),
),
migrations.AddIndex(
model_name="communicationchannel",
index=models.Index(
fields=["tenant", "channel_type"], name="communicati_tenant__0326d1_idx"
),
),
migrations.AddIndex(
model_name="communicationchannel",
index=models.Index(
fields=["is_active", "is_healthy"],
name="communicati_is_acti_77d48f_idx",
),
),
migrations.AddIndex(
model_name="communicationchannel",
index=models.Index(
fields=["last_health_check"], name="communicati_last_he_71110c_idx"
),
),
migrations.AddIndex(
model_name="communicationchannel",
index=models.Index(
fields=["provider_type"], name="communicati_provide_583ead_idx"
),
),
migrations.AlterUniqueTogether(
name="communicationchannel",
unique_together={("tenant", "name")},
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["tenant", "status"], name="communicati_tenant__d41606_idx"
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["sender", "created_at"], name="communicati_sender__7da57d_idx"
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["message_type", "priority"],
name="communicati_message_b3baa0_idx",
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["scheduled_at"], name="communicati_schedul_59afe2_idx"
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["message_thread_id"], name="communicati_message_990a68_idx"
),
),
migrations.AddIndex(
model_name="messagerecipient",
index=models.Index(
fields=["message", "status"], name="communicati_message_6c5b5b_idx"
),
),
migrations.AddIndex(
model_name="messagerecipient",
index=models.Index(
fields=["user", "status"], name="communicati_user_id_2e3702_idx"
),
),
migrations.AddIndex(
model_name="messagerecipient",
index=models.Index(
fields=["recipient_type"], name="communicati_recipie_b45f4d_idx"
),
),
migrations.AddIndex(
model_name="messagerecipient",
index=models.Index(
fields=["sent_at"], name="communicati_sent_at_10fda1_idx"
),
),
migrations.AlterUniqueTogether(
name="messagerecipient",
unique_together={
("message", "email_address"),
("message", "phone_number"),
("message", "user"),
},
),
migrations.AddIndex(
model_name="deliverylog",
index=models.Index(
fields=["message", "status"], name="communicati_message_fdf561_idx"
),
),
migrations.AddIndex(
model_name="deliverylog",
index=models.Index(
fields=["recipient", "status"], name="communicati_recipie_8a8767_idx"
),
),
migrations.AddIndex(
model_name="deliverylog",
index=models.Index(
fields=["channel", "started_at"], name="communicati_channel_03e902_idx"
),
),
migrations.AddIndex(
model_name="deliverylog",
index=models.Index(
fields=["status", "started_at"], name="communicati_status_eb46bd_idx"
),
),
migrations.AddIndex(
model_name="deliverylog",
index=models.Index(
fields=["external_id"], name="communicati_externa_64021f_idx"
),
),
migrations.AddIndex(
model_name="notificationtemplate",
index=models.Index(
fields=["tenant", "template_type"],
name="communicati_tenant__c0ae05_idx",
),
),
migrations.AddIndex(
model_name="notificationtemplate",
index=models.Index(
fields=["category", "is_active"], name="communicati_categor_2c7900_idx"
),
),
migrations.AddIndex(
model_name="notificationtemplate",
index=models.Index(
fields=["is_system_template"], name="communicati_is_syst_dae5b7_idx"
),
),
migrations.AddIndex(
model_name="notificationtemplate",
index=models.Index(
fields=["usage_count"], name="communicati_usage_c_d78c30_idx"
),
),
migrations.AlterUniqueTogether(
name="notificationtemplate",
unique_together={("tenant", "name", "template_type")},
),
migrations.AddIndex(
model_name="alertrule",
index=models.Index(
fields=["tenant", "is_active"], name="communicati_tenant__6d58f7_idx"
),
),
migrations.AddIndex(
model_name="alertrule",
index=models.Index(
fields=["trigger_type", "severity"],
name="communicati_trigger_0ab274_idx",
),
),
migrations.AddIndex(
model_name="alertrule",
index=models.Index(
fields=["last_evaluated_at"], name="communicati_last_ev_3529e2_idx"
),
),
migrations.AddIndex(
model_name="alertrule",
index=models.Index(
fields=["is_system_rule"], name="communicati_is_syst_52420d_idx"
),
),
migrations.AlterUniqueTogether(
name="alertrule",
unique_together={("tenant", "name")},
),
]

View File

@ -0,0 +1,921 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Tenant",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"tenant_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique tenant identifier",
unique=True,
),
),
(
"name",
models.CharField(help_text="Organization name", max_length=200),
),
(
"display_name",
models.CharField(
help_text="Display name for the organization", max_length=200
),
),
(
"description",
models.TextField(
blank=True, help_text="Organization description", null=True
),
),
(
"organization_type",
models.CharField(
choices=[
("HOSPITAL", "Hospital"),
("CLINIC", "Clinic"),
("HEALTH_SYSTEM", "Health System"),
("AMBULATORY", "Ambulatory Care"),
("SPECIALTY", "Specialty Practice"),
("URGENT_CARE", "Urgent Care"),
("REHABILITATION", "Rehabilitation Center"),
("LONG_TERM_CARE", "Long-term Care"),
],
default="HOSPITAL",
max_length=50,
),
),
(
"address_line1",
models.CharField(help_text="Address line 1", max_length=200),
),
(
"address_line2",
models.CharField(
blank=True,
help_text="Address line 2",
max_length=200,
null=True,
),
),
("city", models.CharField(help_text="City", max_length=100)),
(
"state",
models.CharField(help_text="State or province", max_length=100),
),
(
"postal_code",
models.CharField(help_text="Postal code", max_length=20),
),
(
"country",
models.CharField(
default="Saudi Arabia", help_text="Country", max_length=100
),
),
(
"phone_number",
models.CharField(
help_text="Primary phone number",
max_length=20,
validators=[
django.core.validators.RegexValidator(
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.',
regex="^\\+?1?\\d{9,15}$",
)
],
),
),
(
"email",
models.EmailField(
help_text="Primary email address", max_length=254
),
),
(
"website",
models.URLField(
blank=True, help_text="Organization website", null=True
),
),
(
"license_number",
models.CharField(
blank=True,
help_text="Healthcare license number",
max_length=100,
null=True,
),
),
(
"accreditation_body",
models.CharField(
blank=True,
help_text="Accreditation body (e.g., Joint Commission)",
max_length=100,
null=True,
),
),
(
"accreditation_number",
models.CharField(
blank=True,
help_text="Accreditation number",
max_length=100,
null=True,
),
),
(
"accreditation_expiry",
models.DateField(
blank=True, help_text="Accreditation expiry date", null=True
),
),
(
"timezone",
models.CharField(
default="UTC", help_text="Organization timezone", max_length=50
),
),
(
"locale",
models.CharField(
default="en-US", help_text="Organization locale", max_length=10
),
),
(
"currency",
models.CharField(
default="SAR",
help_text="Organization currency code",
max_length=3,
),
),
(
"subscription_plan",
models.CharField(
choices=[
("BASIC", "Basic"),
("STANDARD", "Standard"),
("PREMIUM", "Premium"),
("ENTERPRISE", "Enterprise"),
],
default="BASIC",
max_length=50,
),
),
(
"max_users",
models.PositiveIntegerField(
default=50, help_text="Maximum number of users allowed"
),
),
(
"max_patients",
models.PositiveIntegerField(
default=1000, help_text="Maximum number of patients allowed"
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Tenant is active"),
),
(
"is_trial",
models.BooleanField(default=False, help_text="Tenant is on trial"),
),
(
"trial_expires_at",
models.DateTimeField(
blank=True, help_text="Trial expiration date", null=True
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"verbose_name": "Tenant",
"verbose_name_plural": "Tenants",
"db_table": "core_tenant",
"ordering": ["name"],
},
),
migrations.CreateModel(
name="SystemNotification",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"notification_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique notification identifier",
unique=True,
),
),
(
"title",
models.CharField(help_text="Notification title", max_length=200),
),
("message", models.TextField(help_text="Notification message")),
(
"notification_type",
models.CharField(
choices=[
("INFO", "Information"),
("WARNING", "Warning"),
("ERROR", "Error"),
("SUCCESS", "Success"),
("MAINTENANCE", "Maintenance"),
("SECURITY", "Security Alert"),
("FEATURE", "New Feature"),
("UPDATE", "System Update"),
],
max_length=30,
),
),
(
"priority",
models.CharField(
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
("URGENT", "Urgent"),
],
default="MEDIUM",
max_length=20,
),
),
(
"target_audience",
models.CharField(
choices=[
("ALL_USERS", "All Users"),
("ADMINISTRATORS", "Administrators"),
("CLINICAL_STAFF", "Clinical Staff"),
("SUPPORT_STAFF", "Support Staff"),
("SPECIFIC_ROLES", "Specific Roles"),
("SPECIFIC_USERS", "Specific Users"),
],
default="ALL_USERS",
max_length=30,
),
),
(
"target_roles",
models.JSONField(
default=list,
help_text="Target user roles (if target_audience is SPECIFIC_ROLES)",
),
),
(
"is_dismissible",
models.BooleanField(
default=True, help_text="Users can dismiss this notification"
),
),
(
"auto_dismiss_after",
models.PositiveIntegerField(
blank=True, help_text="Auto-dismiss after X seconds", null=True
),
),
(
"show_on_login",
models.BooleanField(
default=False, help_text="Show notification on user login"
),
),
(
"start_date",
models.DateTimeField(
default=django.utils.timezone.now,
help_text="Notification start date",
),
),
(
"end_date",
models.DateTimeField(
blank=True, help_text="Notification end date", null=True
),
),
(
"action_url",
models.URLField(
blank=True,
help_text="Action URL for the notification",
null=True,
),
),
(
"action_text",
models.CharField(
blank=True,
help_text="Action button text",
max_length=100,
null=True,
),
),
(
"is_active",
models.BooleanField(
default=True, help_text="Notification is active"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"target_users",
models.ManyToManyField(
blank=True,
related_name="targeted_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"tenant",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to="core.tenant",
),
),
],
options={
"verbose_name": "System Notification",
"verbose_name_plural": "System Notifications",
"db_table": "core_system_notification",
"ordering": ["-priority", "-created_at"],
},
),
migrations.CreateModel(
name="SystemConfiguration",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"key",
models.CharField(help_text="Configuration key", max_length=200),
),
("value", models.TextField(help_text="Configuration value")),
(
"data_type",
models.CharField(
choices=[
("STRING", "String"),
("INTEGER", "Integer"),
("FLOAT", "Float"),
("BOOLEAN", "Boolean"),
("JSON", "JSON"),
("DATE", "Date"),
("DATETIME", "DateTime"),
],
default="STRING",
max_length=20,
),
),
(
"category",
models.CharField(
help_text="Configuration category", max_length=100
),
),
(
"description",
models.TextField(
blank=True, help_text="Configuration description", null=True
),
),
(
"validation_rules",
models.JSONField(
default=dict,
help_text="Validation rules for the configuration value",
),
),
(
"default_value",
models.TextField(blank=True, help_text="Default value", null=True),
),
(
"is_sensitive",
models.BooleanField(
default=False, help_text="Configuration contains sensitive data"
),
),
(
"is_encrypted",
models.BooleanField(
default=False, help_text="Configuration value is encrypted"
),
),
(
"required_permission",
models.CharField(
blank=True,
help_text="Permission required to modify this configuration",
max_length=100,
null=True,
),
),
(
"is_active",
models.BooleanField(
default=True, help_text="Configuration is active"
),
),
(
"is_readonly",
models.BooleanField(
default=False, help_text="Configuration is read-only"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"updated_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="updated_configurations",
to=settings.AUTH_USER_MODEL,
),
),
(
"tenant",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="configurations",
to="core.tenant",
),
),
],
options={
"verbose_name": "System Configuration",
"verbose_name_plural": "System Configurations",
"db_table": "core_system_configuration",
"ordering": ["category", "key"],
"unique_together": {("tenant", "key")},
},
),
migrations.CreateModel(
name="IntegrationLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"log_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique log identifier",
unique=True,
),
),
(
"integration_type",
models.CharField(
choices=[
("HL7", "HL7 Message"),
("DICOM", "DICOM Communication"),
("API", "API Call"),
("DATABASE", "Database Sync"),
("FILE_TRANSFER", "File Transfer"),
("WEBHOOK", "Webhook"),
("EMAIL", "Email"),
("SMS", "SMS"),
],
max_length=30,
),
),
(
"direction",
models.CharField(
choices=[("INBOUND", "Inbound"), ("OUTBOUND", "Outbound")],
max_length=10,
),
),
(
"external_system",
models.CharField(help_text="External system name", max_length=200),
),
(
"endpoint",
models.CharField(
blank=True,
help_text="Integration endpoint",
max_length=500,
null=True,
),
),
(
"message_type",
models.CharField(
blank=True,
help_text="Message type (e.g., HL7 message type)",
max_length=100,
null=True,
),
),
(
"message_id",
models.CharField(
blank=True,
help_text="Message identifier",
max_length=200,
null=True,
),
),
(
"correlation_id",
models.UUIDField(
blank=True,
help_text="Correlation ID for tracking related messages",
null=True,
),
),
(
"request_data",
models.TextField(
blank=True, help_text="Request data sent", null=True
),
),
(
"response_data",
models.TextField(
blank=True, help_text="Response data received", null=True
),
),
(
"status",
models.CharField(
choices=[
("SUCCESS", "Success"),
("FAILED", "Failed"),
("PENDING", "Pending"),
("TIMEOUT", "Timeout"),
("RETRY", "Retry"),
],
max_length=20,
),
),
(
"error_code",
models.CharField(
blank=True, help_text="Error code", max_length=50, null=True
),
),
(
"error_message",
models.TextField(blank=True, help_text="Error message", null=True),
),
(
"processing_time_ms",
models.PositiveIntegerField(
blank=True,
help_text="Processing time in milliseconds",
null=True,
),
),
(
"timestamp",
models.DateTimeField(
default=django.utils.timezone.now, help_text="Log timestamp"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="integration_logs",
to="core.tenant",
),
),
],
options={
"verbose_name": "Integration Log",
"verbose_name_plural": "Integration Logs",
"db_table": "core_integration_log",
"ordering": ["-timestamp"],
"indexes": [
models.Index(
fields=["tenant", "integration_type", "timestamp"],
name="core_integr_tenant__b44419_idx",
),
models.Index(
fields=["external_system", "status"],
name="core_integr_externa_11a6db_idx",
),
models.Index(
fields=["correlation_id"], name="core_integr_correla_d72107_idx"
),
],
},
),
migrations.CreateModel(
name="AuditLogEntry",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"log_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique log identifier",
unique=True,
),
),
(
"event_type",
models.CharField(
choices=[
("CREATE", "Create"),
("READ", "Read"),
("UPDATE", "Update"),
("DELETE", "Delete"),
("LOGIN", "Login"),
("LOGOUT", "Logout"),
("ACCESS", "Access"),
("EXPORT", "Export"),
("PRINT", "Print"),
("SHARE", "Share"),
("SYSTEM", "System Event"),
("ERROR", "Error"),
("SECURITY", "Security Event"),
],
max_length=50,
),
),
(
"event_category",
models.CharField(
choices=[
("AUTHENTICATION", "Authentication"),
("AUTHORIZATION", "Authorization"),
("DATA_ACCESS", "Data Access"),
("DATA_MODIFICATION", "Data Modification"),
("SYSTEM_ADMINISTRATION", "System Administration"),
("PATIENT_DATA", "Patient Data"),
("CLINICAL_DATA", "Clinical Data"),
("FINANCIAL_DATA", "Financial Data"),
("SECURITY", "Security"),
("INTEGRATION", "Integration"),
("REPORTING", "Reporting"),
],
max_length=50,
),
),
(
"user_email",
models.EmailField(
blank=True,
help_text="User email at time of event",
max_length=254,
null=True,
),
),
(
"user_role",
models.CharField(
blank=True,
help_text="User role at time of event",
max_length=50,
null=True,
),
),
(
"session_key",
models.CharField(
blank=True, help_text="Session key", max_length=40, null=True
),
),
(
"ip_address",
models.GenericIPAddressField(
blank=True, help_text="IP address", null=True
),
),
(
"user_agent",
models.TextField(
blank=True, help_text="User agent string", null=True
),
),
("object_id", models.PositiveIntegerField(blank=True, null=True)),
(
"object_repr",
models.CharField(
blank=True,
help_text="String representation of the object",
max_length=200,
null=True,
),
),
(
"action",
models.CharField(help_text="Action performed", max_length=200),
),
(
"description",
models.TextField(help_text="Detailed description of the event"),
),
(
"changes",
models.JSONField(
default=dict, help_text="Field changes (before/after values)"
),
),
(
"additional_data",
models.JSONField(default=dict, help_text="Additional event data"),
),
(
"patient_id",
models.CharField(
blank=True,
help_text="Patient identifier if applicable",
max_length=50,
null=True,
),
),
(
"patient_mrn",
models.CharField(
blank=True,
help_text="Patient MRN if applicable",
max_length=50,
null=True,
),
),
(
"risk_level",
models.CharField(
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
("CRITICAL", "Critical"),
],
default="LOW",
max_length=20,
),
),
(
"hipaa_relevant",
models.BooleanField(
default=False, help_text="Event is HIPAA relevant"
),
),
(
"gdpr_relevant",
models.BooleanField(
default=False, help_text="Event is GDPR relevant"
),
),
(
"is_successful",
models.BooleanField(default=True, help_text="Event was successful"),
),
(
"error_message",
models.TextField(
blank=True, help_text="Error message if event failed", null=True
),
),
(
"timestamp",
models.DateTimeField(
default=django.utils.timezone.now, help_text="Event timestamp"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"content_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="contenttypes.contenttype",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="audit_logs",
to=settings.AUTH_USER_MODEL,
),
),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="audit_logs",
to="core.tenant",
),
),
],
options={
"verbose_name": "Audit Log Entry",
"verbose_name_plural": "Audit Log Entries",
"db_table": "core_audit_log_entry",
"ordering": ["-timestamp"],
"indexes": [
models.Index(
fields=["tenant", "event_type", "timestamp"],
name="core_audit__tenant__96449c_idx",
),
models.Index(
fields=["user", "timestamp"],
name="core_audit__user_id_4190d3_idx",
),
models.Index(
fields=["patient_mrn", "timestamp"],
name="core_audit__patient_9afecd_idx",
),
models.Index(
fields=["content_type", "object_id"],
name="core_audit__content_4866d0_idx",
),
models.Index(
fields=["risk_level", "timestamp"],
name="core_audit__risk_le_b2cf34_idx",
),
],
},
),
]

View File

@ -502,7 +502,7 @@ class SystemNotificationListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['priority_choices'] = SystemNotification.PRIORITY_CHOICES context['priority_choices'] = SystemNotification.NotificationPriority.choices
return context return context
@ -762,14 +762,22 @@ def system_notifications(request):
""" """
HTMX view for system notifications. HTMX view for system notifications.
""" """
tenant = request.user.tenant tenant = getattr(request.user, 'tenant', None)
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
# Build base query
if tenant:
notifications = SystemNotification.objects.filter( notifications = SystemNotification.objects.filter(
Q(tenant=tenant) | Q(tenant=None), Q(tenant=tenant) | Q(tenant=None),
is_active=True is_active=True
).filter( )
else:
notifications = SystemNotification.objects.filter(
tenant=None,
is_active=True
)
# Filter by date range
notifications = notifications.filter(
start_date__lte=timezone.now() start_date__lte=timezone.now()
).filter( ).filter(
Q(end_date__gte=timezone.now()) | Q(end_date=None) Q(end_date__gte=timezone.now()) | Q(end_date=None)

BIN
db.sqlite3 Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
# Generated by Django 5.2.6 on 2025-10-05 22:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("emr", "0001_initial"),
]
operations = [
migrations.AlterUniqueTogether(
name="icd10",
unique_together=set(),
),
migrations.AlterField(
model_name="icd10",
name="code",
field=models.CharField(db_index=True, max_length=10, unique=True),
),
migrations.RemoveField(
model_name="icd10",
name="tenant",
),
]

View File

@ -1088,6 +1088,19 @@ class ProblemList(models.Model):
} }
return priority_colors.get(self.priority, 'secondary') return priority_colors.get(self.priority, 'secondary')
def get_severity_color(self):
"""
Get Bootstrap color class for priority display.
"""
severity_colors = {
'MILD': 'success',
'MODERATE': 'info',
'SEVERE': 'warning',
'CRITICAL': 'danger',
}
return severity_colors.get(self.severity, 'secondary')
class CarePlan(models.Model): class CarePlan(models.Model):
""" """
@ -1919,14 +1932,14 @@ class Icd10(models.Model):
Handles chapters/sections/diagnoses (with parent-child hierarchy). Handles chapters/sections/diagnoses (with parent-child hierarchy).
""" """
# Tenant relationship # Tenant relationship
tenant = models.ForeignKey( # tenant = models.ForeignKey(
'core.Tenant', # 'core.Tenant',
on_delete=models.CASCADE, # on_delete=models.CASCADE,
related_name='icd10_codes', # related_name='icd10_codes',
help_text='Organization tenant' # help_text='Organization tenant'
) # )
code = models.CharField(max_length=10, db_index=True) code = models.CharField(max_length=10, unique=True, db_index=True)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
chapter_name = models.CharField(max_length=255, blank=True, null=True) chapter_name = models.CharField(max_length=255, blank=True, null=True)
section_name = models.CharField(max_length=255, blank=True, null=True) section_name = models.CharField(max_length=255, blank=True, null=True)
@ -1946,7 +1959,7 @@ class Icd10(models.Model):
verbose_name = 'ICD-10 Code' verbose_name = 'ICD-10 Code'
verbose_name_plural = 'ICD-10 Codes' verbose_name_plural = 'ICD-10 Codes'
ordering = ['code'] ordering = ['code']
unique_together = ['tenant', 'code']
def __str__(self): def __str__(self):
return f"{self.code}{self.description[:80] if self.description else ''}" return f"{self.code}{self.description[:80] if self.description else ''}"

View File

@ -858,15 +858,15 @@
<td>{{ bill.bill_number }}</td> <td>{{ bill.bill_number }}</td>
<td>{{ bill.bill_date|date:"M d, Y" }}</td> <td>{{ bill.bill_date|date:"M d, Y" }}</td>
<td>{{ bill.get_bill_type_display }}</td> <td>{{ bill.get_bill_type_display }}</td>
<td>${{ bill.total_amount|floatformat:2 }}</td> <td><span class="symbol">&#xea;</span>{{ bill.total_amount|floatformat:'2g' }}</td>
<td>${{ bill.balance_amount|floatformat:2 }}</td> <td><span class="symbol">&#xea;</span>{{ bill.balance_amount|floatformat:'2g' }}</td>
<td> <td>
<span class="badge bg-{% if bill.status == 'PAID' %}success{% elif bill.status == 'PARTIAL_PAID' %}warning{% elif bill.status == 'OVERDUE' %}danger{% else %}secondary{% endif %} fs-10px"> <span class="badge bg-{% if bill.status == 'PAID' %}success{% elif bill.status == 'PARTIAL_PAID' %}warning{% elif bill.status == 'OVERDUE' %}danger{% else %}secondary{% endif %} fs-10px">
{{ bill.get_status_display }} {{ bill.get_status_display }}
</span> </span>
</td> </td>
<td> <td>
<a href="{% url 'billing:medical_bill_detail' bill.pk %}" class="btn btn-xs btn-outline-primary"> <a href="{% url 'billing:bill_detail' bill.bill_id %}" class="btn btn-xs btn-outline-primary">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
</td> </td>

View File

@ -7,10 +7,7 @@
<link href="{% static 'plugins/lity/dist/lity.min.css' %}" rel="stylesheet" /> <link href="{% static 'plugins/lity/dist/lity.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/bootstrap-icons/font/bootstrap-icons.css' %}" rel="stylesheet" /> <link href="{% static 'plugins/bootstrap-icons/font/bootstrap-icons.css' %}" rel="stylesheet" />
<style> <style>
.problem-badge {
font-size: 0.85rem;
padding: 0.35em 0.65em;
}
.problem-header { .problem-header {
background: linear-gradient(90deg, var(--bs-black), var(--bs-secondary)); background: linear-gradient(90deg, var(--bs-black), var(--bs-secondary));
color: white; color: white;
@ -18,42 +15,7 @@
border-radius: 0.25rem; border-radius: 0.25rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.problem-status-active {
background-color: var(--bs-success);
color: white;
}
.problem-status-resolved {
background-color: var(--bs-secondary);
color: white;
}
.problem-status-inactive {
background-color: var(--bs-warning);
color: white;
}
.problem-priority-high {
background-color: var(--bs-danger);
color: white;
}
.problem-priority-medium {
background-color: var(--bs-warning);
color: white;
}
.problem-priority-low {
background-color: var(--bs-info);
color: white;
}
.problem-severity-mild {
background-color: var(--bs-info);
color: white;
}
.problem-severity-moderate {
background-color: var(--bs-warning);
color: white;
}
.problem-severity-severe {
background-color: var(--bs-danger);
color: white;
}
.timeline-item { .timeline-item {
padding: 1rem; padding: 1rem;
border-left: 3px solid var(--bs-primary); border-left: 3px solid var(--bs-primary);
@ -109,13 +71,13 @@
</div> </div>
<div class="d-flex"> <div class="d-flex">
<p class="fw-bold me-2">Status: <p class="fw-bold me-2">Status:
<span class="badge problem-status-{{ problem.status|lower }}">{{ problem.get_status_display }}</span> <span class="badge bg-{{ problem.get_status_color }}">{{ problem.get_status_display }}</span>
</p> </p>
<p class="fw-bold me-2">Priority: <p class="fw-bold me-2">Priority:
<span class="badge problem-priority-{{ problem.priority|lower }}">{{ problem.get_priority_display }} Priority</span> <span class="badge bg-{{ problem.get_priority_color }}">{{ problem.get_priority_display }} Priority</span>
</p> </p>
<p class="fw-bold me-2">Severity: <p class="fw-bold me-2">Severity:
<span class="badge problem-severity-{{ problem.severity|lower }}">{{ problem.get_severity_display }}</span> <span class="badge bg-{{ problem.get_severity_color }}">{{ problem.get_severity_display }}</span>
</p> </p>
</div> </div>
</div> </div>
@ -305,7 +267,7 @@
<div class="card related-item h-100"> <div class="card related-item h-100">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">{{ care_plan.title }}</h5> <h5 class="card-title mb-0">{{ care_plan.title }}</h5>
<span class="badge bg-{{ care_plan.status|lower }}">{{ care_plan.get_status_display }}</span> <span class="badge bg-{{ care_plan.get_status_color }}">{{ care_plan.get_status_display }}</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="card-text">{{ care_plan.description|truncatechars:100 }}</p> <p class="card-text">{{ care_plan.description|truncatechars:100 }}</p>
@ -370,7 +332,7 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="alert alert-info"> <div class="note alert-info p-4">
<i class="fa fa-info-circle me-2"></i> No clinical notes are associated with this problem. <i class="fa fa-info-circle me-2"></i> No clinical notes are associated with this problem.
</div> </div>
{% endif %} {% endif %}

View File

@ -1,5 +1,7 @@
import os import os
import sys
import django import django
import argparse
# Set up Django environment # Set up Django environment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
@ -8,6 +10,7 @@ django.setup()
import random import random
from datetime import datetime, date, time, timedelta from datetime import datetime, date, time, timedelta
from django.utils import timezone as django_timezone from django.utils import timezone as django_timezone
from django.db import transaction
from emr.models import Encounter, VitalSigns, ProblemList, CarePlan, ClinicalNote, NoteTemplate, Icd10, ClinicalRecommendation, AllergyAlert, TreatmentProtocol, ClinicalGuideline, CriticalAlert, DiagnosticSuggestion from emr.models import Encounter, VitalSigns, ProblemList, CarePlan, ClinicalNote, NoteTemplate, Icd10, ClinicalRecommendation, AllergyAlert, TreatmentProtocol, ClinicalGuideline, CriticalAlert, DiagnosticSuggestion
from patients.models import PatientProfile from patients.models import PatientProfile
from accounts.models import User from accounts.models import User
@ -18,6 +21,14 @@ from core.models import Tenant
import uuid import uuid
from decimal import Decimal from decimal import Decimal
# Optional import for ICD-10 XML parsing
try:
import xmlschema
XMLSCHEMA_AVAILABLE = True
except ImportError:
XMLSCHEMA_AVAILABLE = False
xmlschema = None
# Saudi-specific clinical data # Saudi-specific clinical data
SAUDI_ENCOUNTER_TYPES = [ SAUDI_ENCOUNTER_TYPES = [
@ -76,8 +87,9 @@ SAUDI_LOCATIONS = [
] ]
SAUDI_PROBLEM_TYPES = [ SAUDI_PROBLEM_TYPES = [
'DIAGNOSIS', 'SYMPTOM', 'RISK_FACTOR', 'ALLERGY', 'DIAGNOSIS', 'SYMPTOM', 'FINDING', 'COMPLAINT',
'MEDICATION_INTOLERANCE', 'FAMILY_HISTORY', 'SOCIAL_HISTORY' 'CONDITION', 'DISORDER', 'SYNDROME', 'INJURY',
'ALLERGY', 'INTOLERANCE', 'RISK_FACTOR'
] ]
SAUDI_COMMON_PROBLEMS = [ SAUDI_COMMON_PROBLEMS = [
@ -99,8 +111,9 @@ SAUDI_COMMON_PROBLEMS = [
] ]
SAUDI_CARE_PLAN_TYPES = [ SAUDI_CARE_PLAN_TYPES = [
'TREATMENT', 'PREVENTION', 'REHABILITATION', 'CHRONIC_CARE', 'COMPREHENSIVE', 'DISEASE_SPECIFIC', 'PREVENTIVE', 'CHRONIC_CARE',
'ACUTE_CARE', 'SURGICAL', 'MEDICATION_MANAGEMENT', 'LIFESTYLE' 'ACUTE_CARE', 'DISCHARGE', 'REHABILITATION', 'PALLIATIVE',
'MENTAL_HEALTH', 'MEDICATION', 'NUTRITION', 'EXERCISE'
] ]
SAUDI_NOTE_TYPES = [ SAUDI_NOTE_TYPES = [
@ -465,7 +478,13 @@ def create_encounters(tenants, days_back=30):
for _ in range(daily_encounters): for _ in range(daily_encounters):
patient = random.choice(patients) patient = random.choice(patients)
provider = random.choice([p for p in providers if p.tenant == patient.tenant])
# Get providers for this patient's tenant
tenant_providers = [p for p in providers if p.tenant == patient.tenant]
if not tenant_providers:
print(f"Warning: No providers found for tenant {patient.tenant.name}. Skipping encounter for {patient.get_full_name()}")
continue
provider = random.choice(tenant_providers)
# Determine encounter type and class # Determine encounter type and class
encounter_type = random.choices( encounter_type = random.choices(
@ -637,7 +656,7 @@ def create_vital_signs(encounters):
diastolic_bp = random.randint(70, 100) diastolic_bp = random.randint(70, 100)
bp_position = random.choice(['SITTING', 'LYING', 'STANDING']) bp_position = random.choice(['SITTING', 'LYING', 'STANDING'])
bp_cuff_size = 'ADULT' if patient_age >= 18 else random.choice(['PEDIATRIC', 'INFANT']) bp_cuff_size = 'REGULAR' if patient_age >= 18 else 'PEDIATRIC'
# Heart rate # Heart rate
if patient_age < 1: if patient_age < 1:
@ -648,8 +667,8 @@ def create_vital_signs(encounters):
heart_rate = random.randint(60, 100) heart_rate = random.randint(60, 100)
heart_rhythm = random.choices( heart_rhythm = random.choices(
['REGULAR', 'IRREGULAR', 'BRADYCARDIA', 'TACHYCARDIA'], ['REGULAR', 'REGULARLY_IRREGULAR', 'IRREGULARLY_IRREGULAR', 'IRREGULAR_UNSPECIFIED'],
weights=[85, 10, 3, 2] weights=[85, 8, 5, 2]
)[0] )[0]
# Respiratory rate # Respiratory rate
@ -767,7 +786,7 @@ def create_problem_lists(patients, providers):
problem_type = random.choices( problem_type = random.choices(
SAUDI_PROBLEM_TYPES, SAUDI_PROBLEM_TYPES,
weights=[60, 20, 10, 5, 2, 2, 1] weights=[60, 20, 10, 5, 2, 2, 1, 1, 1, 1, 1]
)[0] )[0]
# Onset date (random date in the past) # Onset date (random date in the past)
@ -803,9 +822,13 @@ def create_problem_lists(patients, providers):
resolution_notes = f"Problem resolved after treatment. Patient improved significantly." resolution_notes = f"Problem resolved after treatment. Patient improved significantly."
# Provider assignments # Provider assignments
diagnosing_provider = random.choice([p for p in providers if p.tenant == patient.tenant]) tenant_providers = [p for p in providers if p.tenant == patient.tenant]
managing_provider = diagnosing_provider if random.choice([True, False]) else random.choice( if not tenant_providers:
[p for p in providers if p.tenant == patient.tenant]) print(f"Warning: No providers found for tenant {patient.tenant.name}. Skipping problem for {patient.get_full_name()}")
continue
diagnosing_provider = random.choice(tenant_providers)
managing_provider = diagnosing_provider if random.choice([True, False]) else random.choice(tenant_providers)
# Fix timezone issue # Fix timezone issue
verified_date = None verified_date = None
@ -894,8 +917,9 @@ def create_care_plans(patients, providers, problems):
# Care plan category # Care plan category
category = random.choice([ category = random.choice([
'MEDICAL', 'SURGICAL', 'NURSING', 'REHABILITATION', 'ASSESSMENT', 'TREATMENT', 'EDUCATION', 'COORDINATION',
'PREVENTIVE', 'CHRONIC_DISEASE', 'ACUTE_CARE' 'PREVENTION', 'LIFESTYLE', 'MEDICATION', 'FOLLOW_UP',
'EMERGENCY', 'SUPPORT'
]) ])
# Timeline # Timeline
@ -911,15 +935,20 @@ def create_care_plans(patients, providers, problems):
# Priority # Priority
priority = random.choices( priority = random.choices(
['HIGH', 'MEDIUM', 'LOW'], ['LOW', 'ROUTINE', 'URGENT', 'STAT'],
weights=[25, 50, 25] weights=[25, 50, 20, 5]
)[0] )[0]
# Providers # Providers
primary_provider = random.choice([p for p in providers if p.tenant == patient.tenant]) tenant_providers = [p for p in providers if p.tenant == patient.tenant]
if not tenant_providers:
print(f"Warning: No providers found for tenant {patient.tenant.name}. Skipping care plan for {patient.get_full_name()}")
continue
primary_provider = random.choice(tenant_providers)
care_team_list = random.sample( care_team_list = random.sample(
[p for p in providers if p.tenant == patient.tenant], tenant_providers,
min(random.randint(2, 4), len([p for p in providers if p.tenant == patient.tenant])) min(random.randint(2, 4), len(tenant_providers))
) )
# Goals and objectives # Goals and objectives
@ -1025,7 +1054,7 @@ def create_care_plans(patients, providers, problems):
outcomes_achieved=[ outcomes_achieved=[
"Symptom reduction achieved", "Symptom reduction achieved",
"Patient education completed" "Patient education completed"
] if completion_percentage > 50 else [], ] if completion_percentage > 50 else ["No outcomes achieved yet"],
completion_percentage=completion_percentage, completion_percentage=completion_percentage,
approved=approved, approved=approved,
approved_by=primary_provider if approved else None, approved_by=primary_provider if approved else None,
@ -1279,6 +1308,7 @@ def create_icd10_codes(tenants):
try: try:
icd10_code = Icd10.objects.create( icd10_code = Icd10.objects.create(
tenant=tenant,
code=code, code=code,
description=description, description=description,
chapter_name=chapter, chapter_name=chapter,
@ -1400,24 +1430,29 @@ def create_clinical_recommendations(patients, providers, problems, encounters):
)[0] )[0]
# Provider assignments # Provider assignments
created_by_provider = random.choice([p for p in providers if p.tenant == patient.tenant]) tenant_providers = [p for p in providers if p.tenant == patient.tenant]
if not tenant_providers:
print(f"Warning: No providers found for tenant {patient.tenant.name}. Skipping recommendation for {patient.get_full_name()}")
continue
created_by_provider = random.choice(tenant_providers)
accepted_by = None accepted_by = None
accepted_at = None accepted_at = None
if status in ['ACCEPTED', 'COMPLETED']: if status in ['ACCEPTED', 'COMPLETED']:
accepted_by = random.choice([p for p in providers if p.tenant == patient.tenant]) accepted_by = random.choice(tenant_providers)
accepted_at = django_timezone.now() - timedelta(days=random.randint(1, 30)) accepted_at = django_timezone.now() - timedelta(days=random.randint(1, 30))
deferred_by = None deferred_by = None
deferred_at = None deferred_at = None
if status == 'DEFERRED': if status == 'DEFERRED':
deferred_by = random.choice([p for p in providers if p.tenant == patient.tenant]) deferred_by = random.choice(tenant_providers)
deferred_at = django_timezone.now() - timedelta(days=random.randint(1, 14)) deferred_at = django_timezone.now() - timedelta(days=random.randint(1, 14))
dismissed_by = None dismissed_by = None
dismissed_at = None dismissed_at = None
if status == 'DISMISSED': if status == 'DISMISSED':
dismissed_by = random.choice([p for p in providers if p.tenant == patient.tenant]) dismissed_by = random.choice(tenant_providers)
dismissed_at = django_timezone.now() - timedelta(days=random.randint(1, 7)) dismissed_at = django_timezone.now() - timedelta(days=random.randint(1, 7))
# Expiration # Expiration
@ -1536,9 +1571,14 @@ def create_allergy_alerts(patients, providers):
]) ])
# Status # Status
tenant_providers = [p for p in providers if p.tenant == patient.tenant]
if not tenant_providers:
print(f"Warning: No providers found for tenant {patient.tenant.name}. Skipping allergy alert for {patient.get_full_name()}")
continue
resolved = random.choice([True, False]) resolved = random.choice([True, False])
resolved_at = django_timezone.now() - timedelta(days=random.randint(1, 365)) if resolved else None resolved_at = django_timezone.now() - timedelta(days=random.randint(1, 365)) if resolved else None
resolved_by = random.choice([p for p in providers if p.tenant == patient.tenant]) if resolved else None resolved_by = random.choice(tenant_providers) if resolved else None
try: try:
alert = AllergyAlert.objects.create( alert = AllergyAlert.objects.create(
@ -1621,6 +1661,12 @@ def create_treatment_protocols(tenants, providers):
for tenant in tenants: for tenant in tenants:
for template in protocol_templates: for template in protocol_templates:
# Get providers for this tenant
tenant_providers = [p for p in providers if p.tenant == tenant]
if not tenant_providers:
print(f"Warning: No providers found for tenant {tenant.name}. Skipping protocol {template['name']}")
continue
try: try:
protocol = TreatmentProtocol.objects.create( protocol = TreatmentProtocol.objects.create(
tenant=tenant, tenant=tenant,
@ -1637,7 +1683,7 @@ def create_treatment_protocols(tenants, providers):
usage_count=random.randint(10, 200), usage_count=random.randint(10, 200),
created_at=django_timezone.now() - timedelta(days=random.randint(30, 365)), created_at=django_timezone.now() - timedelta(days=random.randint(30, 365)),
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)),
created_by=random.choice([p for p in providers if p.tenant == tenant]) created_by=random.choice(tenant_providers)
) )
protocols.append(protocol) protocols.append(protocol)
except Exception as e: except Exception as e:
@ -1791,8 +1837,13 @@ def create_critical_alerts(patients, providers, encounters):
template = random.choice(alert_templates) template = random.choice(alert_templates)
# Status # Status
tenant_providers = [p for p in providers if p.tenant == patient.tenant]
if not tenant_providers:
print(f"Warning: No providers found for tenant {patient.tenant.name}. Skipping critical alert for {patient.get_full_name()}")
continue
acknowledged = random.choice([True, False]) acknowledged = random.choice([True, False])
acknowledged_by = random.choice([p for p in providers if p.tenant == patient.tenant]) if acknowledged else None acknowledged_by = random.choice(tenant_providers) if acknowledged else None
acknowledged_at = django_timezone.now() - timedelta(hours=random.randint(1, 24)) if acknowledged else None acknowledged_at = django_timezone.now() - timedelta(hours=random.randint(1, 24)) if acknowledged else None
# Expiration # Expiration
@ -1815,7 +1866,7 @@ def create_critical_alerts(patients, providers, encounters):
acknowledged_at=acknowledged_at, acknowledged_at=acknowledged_at,
expires_at=expires_at, expires_at=expires_at,
related_encounter=related_encounter, related_encounter=related_encounter,
created_by=random.choice([p for p in providers if p.tenant == patient.tenant]), created_by=random.choice(tenant_providers),
created_at=django_timezone.now() - timedelta(hours=random.randint(1, 48)), created_at=django_timezone.now() - timedelta(hours=random.randint(1, 48)),
updated_at=django_timezone.now() - timedelta(minutes=random.randint(0, 60)) updated_at=django_timezone.now() - timedelta(minutes=random.randint(0, 60))
) )
@ -1904,10 +1955,15 @@ def create_diagnostic_suggestions(patients, providers, encounters):
)[0] )[0]
# Provider assignments # Provider assignments
tenant_providers = [p for p in providers if p.tenant == patient.tenant]
if not tenant_providers:
print(f"Warning: No providers found for tenant {patient.tenant.name}. Skipping diagnostic suggestion for {patient.get_full_name()}")
continue
ordered_by = None ordered_by = None
ordered_at = None ordered_at = None
if status in ['ORDERED', 'COMPLETED']: if status in ['ORDERED', 'COMPLETED']:
ordered_by = random.choice([p for p in providers if p.tenant == patient.tenant]) ordered_by = random.choice(tenant_providers)
ordered_at = django_timezone.now() - timedelta(days=random.randint(1, 14)) ordered_at = django_timezone.now() - timedelta(days=random.randint(1, 14))
try: try:
@ -1922,7 +1978,7 @@ def create_diagnostic_suggestions(patients, providers, encounters):
status=status, status=status,
ordered_by=ordered_by, ordered_by=ordered_by,
ordered_at=ordered_at, ordered_at=ordered_at,
created_by=random.choice([p for p in providers if p.tenant == patient.tenant]), created_by=random.choice(tenant_providers),
created_at=django_timezone.now() - timedelta(days=random.randint(1, 30)), created_at=django_timezone.now() - timedelta(days=random.randint(1, 30)),
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 7)) updated_at=django_timezone.now() - timedelta(days=random.randint(0, 7))
) )
@ -1936,8 +1992,266 @@ def create_diagnostic_suggestions(patients, providers, encounters):
return suggestions return suggestions
# ============================================================================
# ICD-10 XML IMPORT FUNCTIONS (from import_icd10.py management command)
# ============================================================================
def _as_text(val):
"""Convert value to text string"""
if val is None:
return None
if isinstance(val, dict):
return val.get("#text") or val.get("@value") or val.get("value") or str(val)
return str(val)
def _ensure_list(maybe_list):
"""Ensure value is a list"""
if maybe_list is None:
return []
if isinstance(maybe_list, list):
return maybe_list
return [maybe_list]
def _find_first_with_key(data, key):
"""Depth-first search: return the first dict that directly contains `key`"""
if isinstance(data, dict):
if key in data:
return data
for v in data.values():
found = _find_first_with_key(v, key)
if found is not None:
return found
elif isinstance(data, list):
for item in data:
found = _find_first_with_key(item, key)
if found is not None:
return found
return None
def _collect_icd10_rows(chapters, tenants):
"""
Build Icd10 rows + parent links from chapters dict/list.
Expected structure: chapter -> section? -> diag (recursive)
"""
rows = []
parent_links = []
def import_diag(diag, chapter_name, section_name, parent_code, tenant):
code = _as_text(diag.get("name"))
desc = _as_text(diag.get("desc"))
if not code:
return
children = _ensure_list(diag.get("diag"))
is_header = bool(children) and not (desc and desc.strip())
rows.append(Icd10(
tenant=tenant,
code=code,
description=desc,
chapter_name=_as_text(chapter_name),
section_name=_as_text(section_name),
parent=None, # set later
is_header=is_header,
))
if parent_code:
parent_links.append((code, parent_code, tenant))
for child in children:
import_diag(child, chapter_name, section_name, parent_code=code, tenant=tenant)
# Normalize chapters to a list
chapters = _ensure_list(chapters)
# Process for each tenant
for tenant in tenants:
for ch in chapters:
ch_name = _as_text(ch.get("name"))
# Sections may be missing; diags may be directly under chapter
sections = _ensure_list(ch.get("section"))
if sections:
for sec in sections:
sec_name = _as_text(sec.get("name"))
for d in _ensure_list(sec.get("diag")):
import_diag(d, ch_name, sec_name, parent_code=None, tenant=tenant)
else:
# If no sections, look for diags at chapter level
for d in _ensure_list(ch.get("diag")):
import_diag(d, ch_name, None, parent_code=None, tenant=tenant)
return rows, parent_links
def import_icd10_from_xml(xsd_path, xml_path, tenants, truncate=False):
"""
Import ICD-10-CM codes from XML file.
Args:
xsd_path: Path to XSD schema file
xml_path: Path to XML data file
tenants: List of tenant objects to import codes for
truncate: Whether to delete existing codes first
Returns:
Number of codes imported
"""
if not XMLSCHEMA_AVAILABLE:
print("❌ Error: xmlschema library not installed.")
print(" Install it with: pip install xmlschema")
return 0
print(f"\n📥 Importing ICD-10 codes from XML...")
print(f" XSD: {xsd_path}")
print(f" XML: {xml_path}")
try:
xs = xmlschema.XMLSchema(xsd_path)
except Exception as e:
print(f"❌ Failed to load XSD: {e}")
return 0
try:
data = xs.to_dict(xml_path)
except Exception as e:
print(f"❌ Failed to parse XML: {e}")
return 0
# Unwrap root if single-key dict
if isinstance(data, dict) and len(data) == 1:
root_key, root_val = next(iter(data.items()))
root = root_val
else:
root = data
# Find container with "chapter" key
container_with_chapter = _find_first_with_key(root, "chapter")
if not container_with_chapter:
container_with_chapter = _find_first_with_key(root, "chapters")
if container_with_chapter and isinstance(container_with_chapter.get("chapters"), dict):
if "chapter" in container_with_chapter["chapters"]:
container_with_chapter = container_with_chapter["chapters"]
if not container_with_chapter or ("chapter" not in container_with_chapter):
preview_keys = list(root.keys()) if isinstance(root, dict) else type(root)
print(f"❌ Could not locate 'chapter' in XML. Top-level keys: {preview_keys}")
return 0
chapters = container_with_chapter.get("chapter")
if chapters is None:
print("❌ Found container for chapters, but 'chapter' is empty.")
return 0
# Optionally truncate
if truncate:
print("⚠️ Truncating existing ICD-10 data...")
Icd10.objects.all().delete()
# Collect rows + parent links
print("📊 Collecting ICD-10 rows...")
rows, parent_links = _collect_icd10_rows(chapters, tenants)
print(f"✅ Collected {len(rows)} rows. Inserting...")
BATCH = 1000
with transaction.atomic():
for i in range(0, len(rows), BATCH):
Icd10.objects.bulk_create(rows[i:i+BATCH], ignore_conflicts=True)
# Link parents
if parent_links:
print("🔗 Linking parent relationships...")
code_to_obj = {}
for tenant in tenants:
tenant_codes = {o.code: o for o in Icd10.objects.filter(tenant=tenant).only("id", "code")}
code_to_obj[tenant.id] = tenant_codes
updates = []
for child_code, parent_code, tenant in parent_links:
tenant_codes = code_to_obj.get(tenant.id, {})
child = tenant_codes.get(child_code)
parent = tenant_codes.get(parent_code)
if child and parent and child.parent_id != parent.id:
child.parent_id = parent.id
updates.append(child)
for i in range(0, len(updates), BATCH):
Icd10.objects.bulk_update(updates[i:i+BATCH], ["parent"])
print(f"✅ Linked {len(updates)} parent relations.")
print(f"✅ ICD-10 import completed: {len(rows)} codes imported")
return len(rows)
# ============================================================================
# MAIN DATA GENERATION FUNCTIONS
# ============================================================================
def parse_arguments():
"""Parse command-line arguments"""
parser = argparse.ArgumentParser(
description='Generate EMR sample data and optionally import ICD-10 codes from XML'
)
parser.add_argument(
'--import-icd10',
action='store_true',
help='Import full ICD-10 codes from XML files'
)
parser.add_argument(
'--xsd',
type=str,
help='Path to ICD-10 XSD schema file (required with --import-icd10)'
)
parser.add_argument(
'--xml',
type=str,
help='Path to ICD-10 XML data file (required with --import-icd10)'
)
parser.add_argument(
'--icd10-only',
action='store_true',
help='Only import ICD-10 codes, skip other EMR data generation'
)
parser.add_argument(
'--truncate',
action='store_true',
help='Delete existing ICD-10 codes before importing'
)
return parser.parse_args()
def main(): def main():
"""Main function to generate all EMR data""" """Main function to generate all EMR data"""
# Parse command-line arguments
args = parse_arguments()
# Validate ICD-10 import arguments
if args.import_icd10 or args.icd10_only:
if not args.xsd or not args.xml:
print("❌ Error: --xsd and --xml are required when using --import-icd10 or --icd10-only")
sys.exit(1)
if not XMLSCHEMA_AVAILABLE:
print("❌ Error: xmlschema library not installed.")
print(" Install it with: pip install xmlschema")
sys.exit(1)
# Get existing tenants
tenants = list(Tenant.objects.all())
if not tenants:
print("❌ No tenants found. Please run the core data generator first.")
return
# ICD-10 only mode
if args.icd10_only:
print("🏥 ICD-10 Import Mode (skipping other EMR data)")
import_icd10_from_xml(args.xsd, args.xml, tenants, truncate=args.truncate)
return
# Standard EMR data generation
print("Starting Saudi Healthcare EMR Data Generation...") print("Starting Saudi Healthcare EMR Data Generation...")
# Get existing tenants # Get existing tenants
@ -1974,8 +2288,14 @@ def main():
print("\n6. Creating Clinical Notes...") print("\n6. Creating Clinical Notes...")
clinical_notes = create_clinical_notes(encounters, templates) clinical_notes = create_clinical_notes(encounters, templates)
# Create ICD-10 codes # Create ICD-10 codes (sample or full import)
print("\n7. Creating ICD-10 Codes...") print("\n7. Creating ICD-10 Codes...")
if args.import_icd10:
# Import full ICD-10 from XML
num_imported = import_icd10_from_xml(args.xsd, args.xml, tenants, truncate=args.truncate)
icd10_codes = list(Icd10.objects.filter(tenant__in=tenants))
else:
# Create sample ICD-10 codes
icd10_codes = create_icd10_codes(tenants) icd10_codes = create_icd10_codes(tenants)
# Create clinical recommendations # Create clinical recommendations

83
enhancements.txt Normal file
View File

@ -0,0 +1,83 @@
# documentation/models.py
from django.conf import settings
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
import uuid
class NoteType(models.TextChoices):
ADMISSION = "ADMISSION", "Admission Note"
PROGRESS = "PROGRESS", "Progress Note"
DISCHARGE = "DISCHARGE", "Discharge Summary"
RADIOLOGY_REPORT = "RADIOLOGY_REPORT", "Radiology Report"
LAB_REPORT = "LAB_REPORT", "Laboratory Narrative"
PROCEDURE = "PROCEDURE", "Procedure Note"
ANESTHESIA = "ANESTHESIA", "Anesthesia Note"
OTHER = "OTHER", "Other"
class Document(models.Model):
"""
Canonical clinical document (note/report).
Stores content, metadata, provenance, and links to any domain object.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey("core.Tenant", on_delete=models.CASCADE, related_name="documents")
patient = models.ForeignKey("core.Patient", on_delete=models.CASCADE, related_name="documents")
encounter = models.ForeignKey("core.Encounter", on_delete=models.SET_NULL, null=True, blank=True, related_name="documents")
doc_type = models.CharField(max_length=40, choices=NoteType.choices)
title = models.CharField(max_length=255)
status = models.CharField(max_length=20, default="final") # draft | amended | final | entered-in-error
authored_at = models.DateTimeField(auto_now_add=True)
authored_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="documents_authored")
signed_at = models.DateTimeField(null=True, blank=True)
signed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="documents_signed")
# Content
# Store both rich text and structured JSON to support templates, tokens, smart fields.
body_markdown = models.TextField(blank=True, default="")
body_json = models.JSONField(blank=True, null=True) # optional structured representation
# FHIR mapping hints (optional)
fhir_profile = models.CharField(max_length=120, blank=True, default="") # e.g., Composition/DiagnosticReport
code = models.CharField(max_length=64, blank=True, default="") # LOINC/SNOMED if used
# Full-text search (enable GIN index in migration)
search_vector = models.TextField(blank=True, default="") # or use a dedicated SearchVector field
# Soft flags
is_confidential = models.BooleanField(default=False)
is_amendment = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class DocumentVersion(models.Model):
document = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="versions")
version = models.PositiveIntegerField()
snapshot_markdown = models.TextField(blank=True, default="")
snapshot_json = models.JSONField(blank=True, null=True)
changed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
changed_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = [("document", "version")]
ordering = ["-version"]
class DocumentLink(models.Model):
"""
Generic relation to ANY object (lab order, study, admission, procedure, etc.)
Allows many-to-many linking without embedding foreign keys in every domain app.
"""
document = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="links")
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.CharField(max_length=64)
target = GenericForeignKey("content_type", "object_id")
role = models.CharField(max_length=40, blank=True, default="context") # context | source | result | followup
created_at = models.DateTimeField(auto_now_add=True)
class DocumentAttachment(models.Model):
document = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="attachments")
file = models.FileField(upload_to="documents/%Y/%m/")
title = models.CharField(max_length=255, blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,7 @@ THIRD_PARTY_APPS = [
'django_extensions', 'django_extensions',
'webpack_loader', 'webpack_loader',
'allauth', 'allauth',
'allauth.account',
'viewflow', 'viewflow',
'viewflow.workflow', 'viewflow.workflow',
# 'allauth.socialaccount', # 'allauth.socialaccount',
@ -87,6 +88,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'allauth.account.middleware.AccountMiddleware',
# 'core.middleware.TenantMiddleware', # 'core.middleware.TenantMiddleware',
] ]
@ -178,6 +180,9 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Custom User Model # Custom User Model
AUTH_USER_MODEL = 'accounts.User' AUTH_USER_MODEL = 'accounts.User'
# Allauth Configuration
ACCOUNT_ADAPTER = 'accounts.adapter.CustomAccountAdapter'
# Account login # Account login
# ACCOUNT_LOGIN_METHODS = {'email'} # ACCOUNT_LOGIN_METHODS = {'email'}
# ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*'] # ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']

View File

@ -37,7 +37,7 @@ urlpatterns += i18n_patterns(
path('accounts/', include('allauth.urls')), path('accounts/', include('allauth.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', include('core.urls')), path('', include('core.urls')),
path('accounts/', include('accounts.urls')), path('account/', include('accounts.urls')),
path('blood-bank/', include('blood_bank.urls')), path('blood-bank/', include('blood_bank.urls')),
path('patients/', include('patients.urls')), path('patients/', include('patients.urls')),
path('appointments/', include('appointments.urls')), path('appointments/', include('appointments.urls')),
@ -55,6 +55,7 @@ urlpatterns += i18n_patterns(
path('communications/', include('communications.urls')), path('communications/', include('communications.urls')),
path('integration/', include('integration.urls')), path('integration/', include('integration.urls')),
path('quality/', include('quality.urls')), path('quality/', include('quality.urls')),
path('approvals/', include('insurance_approvals.urls')),
) )
if settings.DEBUG: if settings.DEBUG:

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -6,7 +6,7 @@ from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from .models import Ward, Bed, Admission, Transfer, DischargeSummary, SurgerySchedule from .models import Ward, Bed, Admission, Transfer, DischargeSummary
@admin.register(Ward) @admin.register(Ward)
@ -426,103 +426,5 @@ class DischargeSummaryAdmin(admin.ModelAdmin):
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
@admin.register(SurgerySchedule)
class SurgeryScheduleAdmin(admin.ModelAdmin):
"""
Admin interface for SurgerySchedule model.
"""
list_display = [
'patient', 'procedure_name', 'scheduled_date', 'scheduled_start_time',
'operating_room', 'primary_surgeon', 'status_display', 'surgery_type'
]
list_filter = [
'surgery_type', 'status', 'priority', 'anesthesia_type',
'scheduled_date', 'operating_room', 'consent_obtained'
]
search_fields = [
'patient__first_name', 'patient__last_name', 'patient__mrn',
'procedure_name', 'procedure_code', 'surgery_id'
]
readonly_fields = ['surgery_id', 'created_at', 'updated_at', 'is_today', 'duration_variance']
filter_horizontal = ['assistant_surgeons']
date_hierarchy = 'scheduled_date'
fieldsets = (
('Basic Information', {
'fields': ('tenant', 'surgery_id', 'patient', 'admission')
}),
# ('Operating Room',{
# 'fields': ('operating_room')
# }),
('Surgery Details', {
'fields': ('procedure_name', 'procedure_code', 'surgery_type', 'preop_diagnosis')
}),
('Scheduling', {
'fields': ('scheduled_date', 'scheduled_start_time', 'estimated_duration_minutes',
'operating_room', 'or_block_time')
}),
('Surgical Team', {
'fields': ('primary_surgeon', 'assistant_surgeons', 'anesthesiologist',
'scrub_nurse', 'circulating_nurse')
}),
('Anesthesia', {
'fields': ('anesthesia_type',),
'classes': ('collapse',)
}),
('Pre-operative', {
'fields': ('preop_orders', 'consent_obtained', 'consent_date'),
'classes': ('collapse',)
}),
('Equipment & Supplies', {
'fields': ('special_equipment', 'implants_needed', 'blood_products'),
'classes': ('collapse',)
}),
('Status & Priority', {
'fields': ('status', 'priority')
}),
('Actual Timing', {
'fields': ('actual_start_time', 'actual_end_time', 'actual_duration_minutes'),
'classes': ('collapse',)
}),
('Post-operative', {
'fields': ('postop_diagnosis', 'procedure_performed', 'complications', 'recovery_location'),
'classes': ('collapse',)
}),
('Notes', {
'fields': ('surgery_notes',),
'classes': ('collapse',)
}),
('Metadata', {
'fields': ('created_at', 'updated_at', 'created_by', 'is_today', 'duration_variance'),
'classes': ('collapse',)
})
)
def status_display(self, obj):
"""Display status with color coding."""
colors = {
'SCHEDULED': 'blue',
'CONFIRMED': 'green',
'PREP': 'orange',
'IN_PROGRESS': 'purple',
'COMPLETED': 'green',
'CANCELLED': 'red',
'POSTPONED': 'orange',
'DELAYED': 'orange'
}
color = colors.get(obj.status, 'black')
return format_html(
'<span style="color: {};">{}</span>',
color, obj.get_status_display()
)
status_display.short_description = 'Status'
def save_model(self, request, obj, form, change):
if not change:
obj.created_by = request.user
super().save_model(request, obj, form, change)
# Register Ward with Bed inline # Register Ward with Bed inline
WardAdmin.inlines = [BedInline] WardAdmin.inlines = [BedInline]

View File

@ -6,7 +6,7 @@ from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils import timezone from django.utils import timezone
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from .models import Ward, Bed, Admission, Transfer, DischargeSummary, SurgerySchedule from .models import Ward, Bed, Admission, Transfer, DischargeSummary
from patients.models import PatientProfile from patients.models import PatientProfile
from accounts.models import User from accounts.models import User
@ -347,99 +347,3 @@ class DischargeSummaryForm(forms.ModelForm):
raise ValidationError('A discharging physician must be specified to sign the summary.') raise ValidationError('A discharging physician must be specified to sign the summary.')
return cleaned_data return cleaned_data
class SurgeryScheduleForm(forms.ModelForm):
"""
Form for surgery schedule management.
"""
class Meta:
model = SurgerySchedule
fields = [
'admission', 'patient', 'procedure_name', 'procedure_code', 'surgery_type',
'scheduled_date', 'scheduled_start_time', 'estimated_duration_minutes',
'operating_room', 'primary_surgeon', 'anesthesiologist', 'anesthesia_type',
'preop_diagnosis', 'consent_obtained', 'special_equipment', 'priority', 'status',
'surgery_notes'
]
widgets = {
'admission': forms.Select(attrs={'class': 'form-select'}),
'patient': forms.Select(attrs={'class': 'form-select', 'disabled': 'disabled'}),
'procedure_name': forms.TextInput(attrs={'class': 'form-control'}),
'procedure_code': forms.TextInput(attrs={'class': 'form-control'}),
'surgery_type': forms.Select(attrs={'class': 'form-select'}),
'scheduled_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'scheduled_start_time': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}),
'estimated_duration_minutes': forms.NumberInput(attrs={'class': 'form-control', 'min': '15', 'step': '15'}),
'operating_room': forms.TextInput(attrs={'class': 'form-control'}),
'primary_surgeon': forms.Select(attrs={'class': 'form-select'}),
'anesthesiologist': forms.Select(attrs={'class': 'form-select'}),
'anesthesia_type': forms.Select(attrs={'class': 'form-select'}),
'preop_diagnosis': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'consent_obtained': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'special_equipment': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'priority': forms.Select(attrs={'class': 'form-select'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'surgery_notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
self.fields['admission'].queryset = Admission.objects.filter(
tenant=user.tenant,
status='ADMITTED'
).select_related('patient').order_by('patient__last_name', 'patient__first_name')
self.fields['patient'].queryset = PatientProfile.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('last_name', 'first_name')
self.fields['primary_surgeon'].queryset = User.objects.filter(
tenant=user.tenant,
is_active=True,
employee_profile__role__in=['PHYSICIAN', 'SURGEON', 'SPECIALIST']
).order_by('last_name', 'first_name')
self.fields['anesthesiologist'].queryset = User.objects.filter(
tenant=user.tenant,
is_active=True,
employee_profile__role__in=['PHYSICIAN', 'ANESTHESIOLOGIST']
).order_by('last_name', 'first_name')
def clean_scheduled_date(self):
scheduled_date = self.cleaned_data.get('scheduled_date')
if scheduled_date and scheduled_date < timezone.now().date():
raise ValidationError('Surgery date cannot be in the past.')
if scheduled_date and scheduled_date > timezone.now().date() + timedelta(days=90):
raise ValidationError('Surgery cannot be scheduled more than 90 days in advance.')
return scheduled_date
def clean_estimated_duration_minutes(self):
duration = self.cleaned_data.get('estimated_duration_minutes')
if duration and duration < 15:
raise ValidationError('Surgery duration must be at least 15 minutes.')
if duration and duration > 1440: # 24 hours
raise ValidationError('Surgery duration cannot exceed 24 hours (1440 minutes).')
return duration
def clean(self):
cleaned_data = super().clean()
scheduled_date = cleaned_data.get('scheduled_date')
scheduled_start_time = cleaned_data.get('scheduled_start_time')
consent_obtained = cleaned_data.get('consent_obtained')
status = cleaned_data.get('status')
if scheduled_date and scheduled_start_time:
scheduled_datetime = datetime.combine(scheduled_date, scheduled_start_time)
if scheduled_datetime < timezone.now():
raise ValidationError('Surgery date and time cannot be in the past.')
if status in ['CONFIRMED', 'PREP', 'IN_PROGRESS'] and not consent_obtained:
raise ValidationError('Patient consent must be obtained before surgery can be confirmed or started.')
return cleaned_data

File diff suppressed because it is too large Load Diff

View File

@ -1420,324 +1420,3 @@ class Transfer(models.Model):
if self.actual_datetime and self.requested_datetime: if self.actual_datetime and self.requested_datetime:
return self.actual_datetime - self.requested_datetime return self.actual_datetime - self.requested_datetime
return None return None
class SurgerySchedule(models.Model):
"""
Surgery schedule model for tracking surgical procedures.
"""
class SurgeryType(models.TextChoices):
ELECTIVE = 'ELECTIVE', 'Elective'
URGENT = 'URGENT', 'Urgent'
EMERGENT = 'EMERGENT', 'Emergent'
TRAUMA = 'TRAUMA', 'Trauma'
TRANSPLANT = 'TRANSPLANT', 'Transplant'
CARDIAC = 'CARDIAC', 'Cardiac'
NEUROSURGERY = 'NEUROSURGERY', 'Neurosurgery'
ORTHOPEDIC = 'ORTHOPEDIC', 'Orthopedic'
GENERAL = 'GENERAL', 'General Surgery'
OTHER = 'OTHER', 'Other'
class SurgeryAnesthesiaType(models.TextChoices):
GENERAL = 'GENERAL', 'General Anesthesia'
REGIONAL = 'REGIONAL', 'Regional Anesthesia'
LOCAL = 'LOCAL', 'Local Anesthesia'
SPINAL = 'SPINAL', 'Spinal Anesthesia'
EPIDURAL = 'EPIDURAL', 'Epidural Anesthesia'
MAC = 'MAC', 'Monitored Anesthesia Care'
SEDATION = 'SEDATION', 'Conscious Sedation'
OTHER = 'OTHER', 'Other'
class SurgeryStatus(models.TextChoices):
SCHEDULED = 'SCHEDULED', 'Scheduled'
CONFIRMED = 'CONFIRMED', 'Confirmed'
PREP = 'PREP', 'Pre-operative Prep'
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
COMPLETED = 'COMPLETED', 'Completed'
CANCELLED = 'CANCELLED', 'Cancelled'
POSTPONED = 'POSTPONED', 'Postponed'
DELAYED = 'DELAYED', 'Delayed'
class RecoveryLocation(models.TextChoices):
PACU = 'PACU', 'Post-Anesthesia Care Unit'
ICU = 'ICU', 'Intensive Care Unit'
WARD = 'WARD', 'Regular Ward'
SAME_DAY = 'SAME_DAY', 'Same Day Surgery'
HOME = 'HOME', 'Home'
OTHER = 'OTHER', 'Other'
class SurgeryPriority(models.TextChoices):
ROUTINE = 'ROUTINE', 'Routine'
URGENT = 'URGENT', 'Urgent'
EMERGENT = 'EMERGENT', 'Emergent'
STAT = 'STAT', 'STAT'
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
related_name='surgery_schedules',
help_text='Organization tenant'
)
# Surgery Information
surgery_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique surgery identifier'
)
# Patient and Admission
patient = models.ForeignKey(
'patients.PatientProfile',
on_delete=models.CASCADE,
related_name='surgeries',
help_text='Patient undergoing surgery'
)
admission = models.ForeignKey(
Admission,
on_delete=models.CASCADE,
related_name='surgeries',
help_text='Associated admission'
)
# Surgery Details
procedure_name = models.CharField(
max_length=200,
help_text='Name of surgical procedure'
)
procedure_code = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='CPT or ICD procedure code'
)
surgery_type = models.CharField(
max_length=30,
choices=SurgeryType.choices,
help_text='Type of surgery'
)
# Scheduling Information
scheduled_date = models.DateField(
help_text='Scheduled surgery date'
)
scheduled_start_time = models.TimeField(
help_text='Scheduled start time'
)
estimated_duration_minutes = models.PositiveIntegerField(
help_text='Estimated duration in minutes'
)
operating_room = models.ForeignKey(
'operating_theatre.OperatingRoom',
on_delete=models.SET_NULL,
related_name='surgery_operations',
blank=True,
null=True,
help_text='Operating room assignment',
)
or_block_time = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='OR block time assignment'
)
# Surgical Team
primary_surgeon = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='primary_surgeries',
help_text='Primary surgeon'
)
assistant_surgeons = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='assistant_surgeries',
blank=True,
help_text='Assistant surgeons'
)
anesthesiologist = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='inpatient_anesthesia_cases',
help_text='Anesthesiologist'
)
scrub_nurse = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='inpatient_scrub_cases',
help_text='Scrub nurse'
)
circulating_nurse = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='inpatient_circulating_cases',
help_text='Circulating nurse'
)
# Anesthesia Information
anesthesia_type = models.CharField(
max_length=30,
choices=SurgeryAnesthesiaType.choices,
blank=True,
null=True,
help_text='Type of anesthesia'
)
# Pre-operative Information
preop_diagnosis = models.TextField(
help_text='Pre-operative diagnosis'
)
preop_orders = models.JSONField(
default=list,
blank=True,
help_text='Pre-operative orders'
)
consent_obtained = models.BooleanField(
default=False,
help_text='Surgical consent obtained'
)
consent_date = models.DateTimeField(
blank=True,
null=True,
help_text='Date consent was obtained'
)
# Equipment and Supplies
special_equipment = models.JSONField(
default=list,
blank=True,
help_text='Special equipment needed'
)
implants_needed = models.JSONField(
default=list,
blank=True,
help_text='Implants or devices needed'
)
blood_products = models.JSONField(
default=list,
blank=True,
help_text='Blood products needed'
)
# Surgery Status
status = models.CharField(
max_length=20,
choices=SurgeryStatus.choices,
default=SurgeryStatus.SCHEDULED,
help_text='Surgery status'
)
# Actual Timing
actual_start_time = models.DateTimeField(
blank=True,
null=True,
help_text='Actual start time'
)
actual_end_time = models.DateTimeField(
blank=True,
null=True,
help_text='Actual end time'
)
actual_duration_minutes = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Actual duration in minutes'
)
# Post-operative Information
postop_diagnosis = models.TextField(
blank=True,
null=True,
help_text='Post-operative diagnosis'
)
procedure_performed = models.TextField(
blank=True,
null=True,
help_text='Actual procedure performed'
)
complications = models.TextField(
blank=True,
null=True,
help_text='Intraoperative complications'
)
# Recovery Information
recovery_location = models.CharField(
max_length=50,
choices=RecoveryLocation.choices,
blank=True,
null=True,
help_text='Post-operative recovery location'
)
# Priority and Urgency
priority = models.CharField(
max_length=20,
choices=SurgeryPriority.choices,
default=SurgeryPriority.ROUTINE,
help_text='Surgery priority'
)
# Notes
surgery_notes = models.TextField(
blank=True,
null=True,
help_text='Surgery notes'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_surgeries',
help_text='User who created the surgery schedule'
)
class Meta:
db_table = 'inpatients_surgery_schedule'
verbose_name = 'Surgery Schedule'
verbose_name_plural = 'Surgery Schedules'
ordering = ['scheduled_date', 'scheduled_start_time']
indexes = [
models.Index(fields=['tenant', 'status']),
models.Index(fields=['patient']),
models.Index(fields=['admission']),
models.Index(fields=['scheduled_date']),
models.Index(fields=['primary_surgeon']),
models.Index(fields=['operating_room']),
]
def __str__(self):
return f"{self.procedure_name} - {self.patient.get_full_name()} ({self.scheduled_date})"
@property
def is_today(self):
"""
Check if surgery is scheduled for today.
"""
return self.scheduled_date == timezone.now().date()
@property
def duration_variance(self):
"""
Calculate variance between estimated and actual duration.
"""
if self.actual_duration_minutes:
return self.actual_duration_minutes - self.estimated_duration_minutes
return None

View File

@ -11,12 +11,7 @@
<h1 class="h3 mb-1"> <h1 class="h3 mb-1">
<i class="fas fa-user-injured me-2"></i>Admission Management <i class="fas fa-user-injured me-2"></i>Admission Management
</h1> </h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'inpatients:dashboard' %}">Inpatients</a></li>
<li class="breadcrumb-item active">Admissions</li>
</ol>
</nav>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-outline-secondary" onclick="refreshAdmissions()"> <button type="button" class="btn btn-outline-secondary" onclick="refreshAdmissions()">
@ -116,13 +111,19 @@
</div> </div>
<!-- Filters and Search --> <!-- Filters and Search -->
<div class="card mb-4"> <div class="panel panel-inverse" data-sortable-id="discharge-planning-header">
<div class="card-header"> <div class="panel-heading">
<h5 class="mb-0"> <h4 class="panel-title">
<i class="fas fa-filter me-2"></i>Filters and Search <i class="fas fa-filter me-2"></i>Filters and Search
</h5> </h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div> </div>
<div class="card-body"> </div>
<div class="panel-body">
<form method="get" id="filterForm"> <form method="get" id="filterForm">
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-2">
@ -194,21 +195,21 @@
</div> </div>
<!-- Admissions Table --> <!-- Admissions Table -->
<div class="card">
<div class="card-header"> <div class="panel panel-inverse" data-sortable-id="discharge-planning-header">
<div class="d-flex justify-content-between align-items-center"> <div class="panel-heading">
<h5 class="mb-0"> <h4 class="panel-title">
<i class="fas fa-list me-2"></i>Admissions List <i class="fas fa-list me-2"></i>Admissions List
</h5> </h4>
<div class="btn-group btn-group-sm"> <div class="panel-heading-btn">
<button type="button" class="btn btn-outline-secondary" onclick="selectAll()"> <button type="button" class="btn btn-xs btn-outline-theme me-2" onclick="selectAll()">
<i class="fas fa-check-square me-1"></i>Select All <i class="fas fa-check-square me-1"></i>Select All
</button> </button>
<button type="button" class="btn btn-outline-secondary" onclick="clearSelection()"> <button type="button" class="btn btn-xs btn-outline-danger me-2" onclick="clearSelection()">
<i class="fas fa-square me-1"></i>Clear <i class="fas fa-square me-1"></i>Clear
</button> </button>
<div class="btn-group">
<button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown" id="bulkActions" disabled> <button type="button" class="btn btn-xs btn-outline-primary dropdown-toggle me-2" data-bs-toggle="dropdown" id="bulkActions" disabled>
<i class="fas fa-cogs me-1"></i>Bulk Actions <i class="fas fa-cogs me-1"></i>Bulk Actions
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@ -223,14 +224,17 @@
<i class="fas fa-download me-2"></i>Export Selected <i class="fas fa-download me-2"></i>Export Selected
</a></li> </a></li>
</ul> </ul>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div> </div>
</div> </div>
</div> <div class="panel-body">
</div>
<div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover mb-0"> <table class="table table-sm table-hover">
<thead class="table-light"> <thead>
<tr> <tr>
<th width="40"> <th width="40">
<input type="checkbox" class="form-check-input" id="selectAllCheckbox" onchange="toggleSelectAll()"> <input type="checkbox" class="form-check-input" id="selectAllCheckbox" onchange="toggleSelectAll()">
@ -331,7 +335,8 @@
<i class="fas fa-edit me-2"></i>Edit <i class="fas fa-edit me-2"></i>Edit
</a></li> </a></li>
{% if admission.status == 'ADMITTED' %} {% if admission.status == 'ADMITTED' %}
<li><a class="dropdown-item" href="{% url 'inpatients:transfer_patient' admission.pk %}"> {# href="{% url 'inpatients:transfer_patient' admission.pk %}"#}
<li><a class="dropdown-item" onclick="showTransfer({{ admission.id }})">
<i class="fas fa-exchange-alt me-2"></i>Transfer <i class="fas fa-exchange-alt me-2"></i>Transfer
</a></li> </a></li>
<li><a class="dropdown-item" href="{% url 'inpatients:discharge_patient' admission.pk %}"> <li><a class="dropdown-item" href="{% url 'inpatients:discharge_patient' admission.pk %}">
@ -370,16 +375,30 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
</div>
<!-- Pagination --> <!-- Pagination -->
{% if is_paginated %} {% if is_paginated %}
{% include 'partial/pagination.html'%} {% include 'partial/pagination.html'%}
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
</div>
<div class="modal fade" id="transferModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-exchange-alt me-2"></i>Transfer Patient</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="transferContent">
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script> <script>
// Admission list functionality // Admission list functionality
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
@ -405,6 +424,19 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
function showTransfer(bedId) {
// Load bed details in modal
fetch(`{% url 'inpatients:transfer_patient' 0%}`.replace('0', bedId))
.then(response => response.text())
.then(html => {
document.getElementById('transferContent').innerHTML = html;
new bootstrap.Modal(document.getElementById('transferModal')).show();
})
.catch(error => {
console.error('Error loading transfer details:', error);
});
}
function refreshAdmissions() { function refreshAdmissions() {
location.reload(); location.reload();
} }

View File

@ -188,9 +188,7 @@
<i class="fas fa-calendar-day me-2"></i>Today's Schedule <i class="fas fa-calendar-day me-2"></i>Today's Schedule
</h4> </h4>
<div class="panel-heading-btn"> <div class="panel-heading-btn">
<a href="{% url 'inpatients:surgery_schedule' %}" class="btn btn-xs btn-outline-theme">
<i class="fas fa-calendar me-1"></i>Full Schedule
</a>
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a> <a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload" onclick="refreshSchedule()"><i class="fa fa-redo"></i></a> <a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload" onclick="refreshSchedule()"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a> <a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
@ -264,20 +262,17 @@
<div class="panel-body"> <div class="panel-body">
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<a href="{% url 'inpatients:admission_list' %}?action=new" class="btn btn-primary"> <a href="{% url 'inpatients:admission_list' %}?action=new" class="btn btn-primary">
<i class="fas fa-user-plus me-2"></i>New Admission <i class="fas fa-user-plus me-2"></i>Admission Management
</a> </a>
<a href="{% url 'inpatients:bed_management' %}" class="btn btn-outline-secondary"> <a href="{% url 'inpatients:bed_management' %}" class="btn btn-outline-secondary">
<i class="fas fa-bed me-2"></i>Bed Management <i class="fas fa-bed me-2"></i>Bed Management
</a> </a>
<a href="{% url 'inpatients:transfer_management' %}" class="btn btn-outline-info"> <a href="{% url 'inpatients:transfer_management' %}" class="btn btn-outline-info">
<i class="fas fa-exchange-alt me-2"></i>Transfer Patient <i class="fas fa-exchange-alt me-2"></i>Transfer Management
</a> </a>
<a href="{% url 'inpatients:ward_list' %}" class="btn btn-outline-success"> <a href="{% url 'inpatients:ward_list' %}" class="btn btn-outline-success">
<i class="fas fa-hospital me-2"></i>Ward Management <i class="fas fa-hospital me-2"></i>Ward Management
</a> </a>
<a href="{% url 'inpatients:surgery_schedule' %}" class="btn btn-outline-warning">
<i class="fas fa-calendar-alt me-2"></i>Surgery Schedule
</a>
<button type="button" class="btn btn-outline-secondary" onclick="printDashboard()"> <button type="button" class="btn btn-outline-secondary" onclick="printDashboard()">
<i class="fas fa-print me-2"></i>Print Report <i class="fas fa-print me-2"></i>Print Report
</button> </button>

View File

@ -0,0 +1,193 @@
# Ward Statistics - Dynamic Column Width Implementation
## Current Implementation ✅
The ward statistics cards now use **responsive Bootstrap column classes** for dynamic width adjustment across all screen sizes.
### Column Classes Applied
```html
<div class="col-12 col-sm-6 col-md-4 col-lg mb-3">
```
### Responsive Behavior
| Screen Size | Bootstrap Breakpoint | Columns per Row | Card Width |
|-------------|---------------------|-----------------|------------|
| **Mobile** (< 576px) | `col-12` | 1 column | 100% width |
| **Tablet** (≥ 576px) | `col-sm-6` | 2 columns | 50% width |
| **Medium** (≥ 768px) | `col-md-4` | 3 columns | ~33% width |
| **Large** (≥ 992px) | `col-lg` | 5 columns | Equal width (auto) |
| **Extra Large** (≥ 1200px) | `col-lg` | 5 columns | Equal width (auto) |
### How It Works
1. **Mobile First**: Starts with full width (`col-12`)
2. **Progressive Enhancement**: Adds responsive classes for larger screens
3. **Equal Distribution**: `col-lg` without a number creates equal-width columns
4. **Automatic Wrapping**: Cards wrap to new rows when space is limited
## Alternative Approaches
### Option 1: Fixed Column Distribution (Current - RECOMMENDED)
```html
<div class="col-12 col-sm-6 col-md-4 col-lg mb-3">
```
**Pros:**
- Predictable layout across all screen sizes
- Works perfectly with 5 cards
- No custom CSS needed
- Leverages Bootstrap's grid system
**Cons:**
- Less flexible if card count changes
---
### Option 2: Flexbox Equal Width
```html
<div class="col-12 col-sm-6 col-lg mb-3">
```
**Pros:**
- Simpler class structure
- Automatically adjusts to any number of cards
- Very flexible
**Cons:**
- Less control over medium screen layout
- May not look optimal with 5 cards on medium screens
---
### Option 3: CSS Grid (Custom Implementation)
```html
<!-- In parent container -->
<div class="row mb-4 stats-grid" id="ward-stats">
```
```css
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
@media (min-width: 992px) {
.stats-grid {
grid-template-columns: repeat(5, 1fr);
}
}
```
**Pros:**
- Most modern and flexible
- Perfect equal widths
- Easy to maintain
**Cons:**
- Requires custom CSS
- Overrides Bootstrap grid
- May conflict with existing styles
---
### Option 4: Dynamic Column Calculation (JavaScript)
```javascript
// Calculate columns based on card count
const cardCount = document.querySelectorAll('.widget-stats').length;
const colClass = `col-lg-${Math.floor(12 / cardCount)}`;
```
**Pros:**
- Automatically adapts to any number of cards
- No manual class updates needed
**Cons:**
- Requires JavaScript
- More complex
- Overkill for static card count
---
### Option 5: Percentage-Based Width (Custom CSS)
```css
.stats-card-wrapper {
flex: 1 1 calc(20% - 1rem); /* 5 cards = 20% each */
min-width: 200px;
}
@media (max-width: 991px) {
.stats-card-wrapper {
flex: 1 1 calc(33.333% - 1rem); /* 3 cards */
}
}
@media (max-width: 767px) {
.stats-card-wrapper {
flex: 1 1 calc(50% - 1rem); /* 2 cards */
}
}
@media (max-width: 575px) {
.stats-card-wrapper {
flex: 1 1 100%; /* 1 card */
}
}
```
**Pros:**
- Precise control over widths
- Smooth transitions
**Cons:**
- Requires custom CSS
- More maintenance
- Duplicates Bootstrap functionality
---
## Recommendations
### For Current Use Case (5 Static Cards)
**Use Option 1** (Current Implementation)
- Best balance of simplicity and control
- Leverages Bootstrap's proven grid system
- No custom CSS required
- Predictable behavior
### For Dynamic Card Count
Consider **Option 3** (CSS Grid) if:
- Card count varies based on permissions/features
- Need perfect equal widths always
- Willing to add custom CSS
### For Maximum Flexibility
Consider **Option 2** (Flexbox Equal Width) if:
- Card count may change frequently
- Want simplest possible implementation
- Don't need precise control over medium screens
## Testing Checklist
- [ ] Mobile (< 576px): 1 card per row, full width
- [ ] Tablet (576-767px): 2 cards per row, 50% width each
- [ ] Medium (768-991px): 3 cards per row, ~33% width each
- [ ] Large (≥ 992px): 5 cards per row, equal width
- [ ] Cards wrap properly when resizing
- [ ] No horizontal overflow
- [ ] Consistent spacing between cards
- [ ] HTMX auto-refresh maintains layout
## Browser Compatibility
All approaches work in:
- ✅ Chrome/Edge (latest)
- ✅ Firefox (latest)
- ✅ Safari (latest)
- ✅ Mobile browsers (iOS Safari, Chrome Mobile)
## Performance Notes
- Bootstrap grid classes are highly optimized
- No JavaScript overhead with current implementation
- HTMX partial updates preserve layout
- Minimal CSS footprint

View File

@ -1,6 +1,6 @@
<!-- Ward Statistics Cards --> <!-- Ward Statistics Cards -->
<div class="col-lg-2 col-md-6 mb-3"> <!-- Responsive columns: 1 col on mobile, 2 on tablet, 3 on medium, 5 on large screens -->
<div class="col-12 col-sm-6 col-md-4 col-lg mb-3">
<div class="widget widget-stats bg-primary"> <div class="widget widget-stats bg-primary">
<div class="stats-icon stats-icon-lg"><i class="fas fa-bed fa-fw"></i></div> <div class="stats-icon stats-icon-lg"><i class="fas fa-bed fa-fw"></i></div>
<div class="stats-content"> <div class="stats-content">
@ -11,7 +11,7 @@
</div> </div>
</div> </div>
<div class="col-lg-2 col-md-6 mb-3"> <div class="col-12 col-sm-6 col-md-4 col-lg mb-3">
<div class="widget widget-stats bg-success"> <div class="widget widget-stats bg-success">
<div class="stats-icon stats-icon-lg"><i class="fas fa-user-injured fa-fw"></i></div> <div class="stats-icon stats-icon-lg"><i class="fas fa-user-injured fa-fw"></i></div>
<div class="stats-content"> <div class="stats-content">
@ -22,7 +22,7 @@
</div> </div>
</div> </div>
<div class="col-lg-2 col-md-6 mb-3"> <div class="col-12 col-sm-6 col-md-4 col-lg mb-3">
<div class="widget widget-stats bg-info"> <div class="widget widget-stats bg-info">
<div class="stats-icon stats-icon-lg"><i class="fas fa-bed-pulse fa-fw"></i></div> <div class="stats-icon stats-icon-lg"><i class="fas fa-bed-pulse fa-fw"></i></div>
<div class="stats-content"> <div class="stats-content">
@ -33,7 +33,7 @@
</div> </div>
</div> </div>
<div class="col-lg-2 col-md-6 mb-3"> <div class="col-12 col-sm-6 col-md-4 col-lg mb-3">
<div class="widget widget-stats bg-warning"> <div class="widget widget-stats bg-warning">
<div class="stats-icon stats-icon-lg"><i class="fas fa-exclamation-triangle fa-fw"></i></div> <div class="stats-icon stats-icon-lg"><i class="fas fa-exclamation-triangle fa-fw"></i></div>
<div class="stats-content"> <div class="stats-content">
@ -44,7 +44,7 @@
</div> </div>
</div> </div>
<div class="col-lg-2 col-md-6 mb-3"> <div class="col-12 col-sm-6 col-md-4 col-lg mb-3">
<div class="widget widget-stats bg-danger"> <div class="widget widget-stats bg-danger">
<div class="stats-icon stats-icon-lg"><i class="fas fa-stop-circle fa-fw"></i></div> <div class="stats-icon stats-icon-lg"><i class="fas fa-stop-circle fa-fw"></i></div>
<div class="stats-content"> <div class="stats-content">
@ -54,4 +54,3 @@
</div> </div>
</div> </div>
</div> </div>

View File

@ -106,33 +106,7 @@
<!-- Pagination --> <!-- Pagination -->
{% if is_paginated %} {% if is_paginated %}
<nav aria-label="Ward pagination"> {% include 'partial/pagination.html' %}
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1">First</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -71,19 +71,6 @@ urlpatterns = [
path('htmx/schedule-maintenance/', views.htmx_schedule_maintenance, name='htmx_schedule_maintenance'), path('htmx/schedule-maintenance/', views.htmx_schedule_maintenance, name='htmx_schedule_maintenance'),
path('htmx/view-alerts/', views.htmx_view_alerts, name='htmx_view_alerts'), path('htmx/view-alerts/', views.htmx_view_alerts, name='htmx_view_alerts'),
path('surgery/', views.SurgeryScheduleView.as_view(), name='surgery_schedule'),
path('surgery/list/', views.SurgeryScheduleListView.as_view(), name='surgery_list'),
path('surgery/<int:pk>/', views.SurgeryScheduleDetailView.as_view(), name='surgery_detail'),
path('surgery/<int:pk>/edit/', views.SurgeryScheduleUpdateView.as_view(), name='surgery_update'),
path('surgery/create/', views.SurgeryScheduleCreateView.as_view(), name='surgery_create'),
path('surgery/calendar/', views.surgery_calendar, name='surgery_calendar'),
path('surgery/<int:pk>/cancel/', views.cancel_surgery, name='cancel_surgery'),
path('surgery/<int:pk>/complete/', views.complete_surgery, name='complete_surgery'),
path('surgery/<int:pk>/confirm/', views.confirm_surgery, name='confirm_surgery'),
path('surgery/<int:pk>/prep/', views.prep_surgery, name='prep_surgery'),
path('surgery/<int:pk>/postpone/', views.postpone_surgery, name='postpone_surgery'),
path('surgery/<int:pk>/start/', views.start_surgery, name='start_surgery'),
# Actions # Actions

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ from datetime import datetime, timedelta, date, time as dt_time
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from inpatients.models import Ward, Bed, Admission, Transfer, DischargeSummary, SurgerySchedule from inpatients.models import Ward, Bed, Admission, Transfer, DischargeSummary
from accounts.models import User from accounts.models import User
from patients.models import PatientProfile from patients.models import PatientProfile
from core.models import Tenant from core.models import Tenant
@ -547,96 +547,6 @@ def create_saudi_discharge_summaries(admissions):
print(f"Created {len(summaries)} discharge summaries") print(f"Created {len(summaries)} discharge summaries")
return summaries return summaries
# ------------------------------
# Surgery Schedules (FKs, OR optional)
# ------------------------------
def create_saudi_surgery_schedules(tenants, surgeries_per_tenant=30):
surgeries = []
for tenant in tenants:
admissions = list(Admission.objects.filter(tenant=tenant, status=Admission.AdmissionStatus.ADMITTED))
patients = [a.patient for a in admissions]
surgeons = pick_tenant_users(tenant, roles=['PHYSICIAN'], limit=30)
nurses = pick_tenant_users(tenant, roles=['NURSE', 'NURSE_PRACTITIONER'], limit=50)
anesthes = pick_tenant_users(tenant, roles=['PHYSICIAN'], limit=30) # reuse physician role for demo
if not admissions or not surgeons or not nurses:
print(f"Insufficient OR data for tenant {tenant.name}: admissions={len(admissions)}, surgeons={len(surgeons)}, nurses={len(nurses)}")
continue
# Optional OR pool
ors = list(OperatingRoom.objects.filter(tenant=tenant)) if OperatingRoom else []
for _ in range(min(surgeries_per_tenant, len(admissions))):
admission = random.choice(admissions)
patient = admission.patient
procedure = random.choice(SAUDI_SURGICAL_PROCEDURES)
surgery_date = (timezone.now() + timedelta(days=random.randint(0, 30))).date()
start_time = dt_time(hour=random.randint(7, 16), minute=random.choice([0, 30]))
est_dur = random.randint(60, 480)
status = random.choice([s[0] for s in SurgerySchedule.SurgeryStatus.choices])
actual_start = None
actual_end = None
actual_dur = None
recovery_location = None
if status in ['IN_PROGRESS', 'COMPLETED']:
actual_start = datetime.combine(surgery_date, start_time, tzinfo=timezone.get_current_timezone())
if status == 'COMPLETED':
actual_dur = est_dur + random.randint(-30, 60)
actual_end = actual_start + timedelta(minutes=actual_dur)
recovery_location = random.choice([r[0] for r in SurgerySchedule.RecoveryLocation.choices])
ss = SurgerySchedule.objects.create(
tenant=tenant,
patient=patient,
admission=admission,
procedure_name=procedure,
procedure_code=f"CPT-{random.randint(10000, 99999)}",
surgery_type=random.choice([t[0] for t in SurgerySchedule.SurgeryType.choices]),
scheduled_date=surgery_date,
scheduled_start_time=start_time,
estimated_duration_minutes=est_dur,
operating_room=(random.choice(ors) if ors and random.random() < 0.8 else None),
or_block_time=f"{start_time} - {(datetime.combine(surgery_date, start_time) + timedelta(minutes=est_dur)).time()}",
primary_surgeon=random.choice(surgeons),
anesthesiologist=(random.choice(anesthes) if anesthes else None),
scrub_nurse=random.choice(nurses),
circulating_nurse=random.choice(nurses),
anesthesia_type=random.choice([a[0] for a in SurgerySchedule.SurgeryAnesthesiaType.choices]),
preop_diagnosis=random.choice(SAUDI_COMMON_DIAGNOSES),
preop_orders=['NPO after midnight', 'Pre-op antibiotics', 'Type and crossmatch', 'Pre-op labs', 'Consent signed'],
consent_obtained=True,
consent_date=timezone.now() - timedelta(days=random.randint(0, 7)),
special_equipment=(['Laparoscopic equipment', 'C-arm', 'Microscope'] if random.random() < 0.4 else []),
implants_needed=(['Orthopedic implants', 'Cardiac devices'] if random.random() < 0.3 else []),
blood_products=(['Packed RBCs', 'FFP', 'Platelets'] if random.random() < 0.3 else []),
status=status,
actual_start_time=actual_start,
actual_end_time=actual_end,
actual_duration_minutes=actual_dur,
postop_diagnosis=(random.choice(SAUDI_COMMON_DIAGNOSES) if status == 'COMPLETED' else None),
procedure_performed=(procedure if status == 'COMPLETED' else None),
complications=("None" if status == 'COMPLETED' and random.random() < 0.8 else None),
recovery_location=recovery_location,
priority=random.choice([p[0] for p in SurgerySchedule.SurgeryPriority.choices]),
surgery_notes=("Uneventful case." if status == 'COMPLETED' else None),
created_by=random.choice(surgeons)
)
# assistant surgeons
if len(surgeons) > 1 and random.random() < 0.7:
assistants = [s for s in surgeons if s != ss.primary_surgeon]
ss.assistant_surgeons.set(random.sample(assistants, k=min(len(assistants), random.randint(0, 2))))
surgeries.append(ss)
print(f"Created {min(surgeries_per_tenant, len(admissions))} surgeries for {tenant.name}")
return surgeries
# ------------------------------ # ------------------------------
# Main # Main
# ------------------------------ # ------------------------------
@ -669,10 +579,6 @@ def main():
print("\n5. Creating Discharge Summaries …") print("\n5. Creating Discharge Summaries …")
summaries = create_saudi_discharge_summaries(admissions) summaries = create_saudi_discharge_summaries(admissions)
# 6) Surgery Schedules
print("\n6. Creating Surgery Schedules …")
surgeries = create_saudi_surgery_schedules(tenants, surgeries_per_tenant=25)
print("\n✅ Done.") print("\n✅ Done.")
print(f"📊 Summary:") print(f"📊 Summary:")
print(f" - Wards: {len(wards)}") print(f" - Wards: {len(wards)}")
@ -680,7 +586,6 @@ def main():
print(f" - Admissions: {len(admissions)}") print(f" - Admissions: {len(admissions)}")
print(f" - Transfers: {len(transfers)}") print(f" - Transfers: {len(transfers)}")
print(f" - Discharge Summaries: {len(summaries)}") print(f" - Discharge Summaries: {len(summaries)}")
print(f" - Surgery Schedules: {len(surgeries)}")
# Status distribution (nice labels) # Status distribution (nice labels)
from collections import Counter from collections import Counter
@ -704,8 +609,7 @@ def main():
'beds': beds, 'beds': beds,
'admissions': admissions, 'admissions': admissions,
'transfers': transfers, 'transfers': transfers,
'summaries': summaries, 'summaries': summaries
'surgeries': surgeries
} }
if __name__ == "__main__": if __name__ == "__main__":

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -40,16 +40,23 @@ class InsuranceApprovalRequestForm(forms.ModelForm):
'internal_notes', 'internal_notes',
] ]
widgets = { widgets = {
'service_start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), 'patient': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'service_end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), 'insurance_info': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'clinical_justification': forms.Textarea(attrs={'rows': 4, 'class': 'form-control'}), 'request_type': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'medical_necessity': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}), 'priority': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'alternative_treatments_tried': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}), 'assigned_to': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'internal_notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}), 'procedure_codes': forms.Textarea(attrs={'class': 'form-control form-control-sm'}),
'service_description': forms.TextInput(attrs={'class': 'form-control'}), 'diagnosis_codes': forms.Textarea(attrs={'class': 'form-control form-control-sm'}),
'requested_quantity': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}), 'service_start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control form-control-sm'}),
'requested_visits': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}), 'service_end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control form-control-sm'}),
'requested_units': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}), 'clinical_justification': forms.Textarea(attrs={'rows': 4, 'class': 'form-control form-control-sm'}),
'medical_necessity': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}),
'alternative_treatments_tried': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}),
'internal_notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}),
'service_description': forms.TextInput(attrs={'class': 'form-control form-control-sm'}),
'requested_quantity': forms.NumberInput(attrs={'class': 'form-control form-control-sm', 'min': '1'}),
'requested_visits': forms.NumberInput(attrs={'class': 'form-control form-control-sm', 'min': '1'}),
'requested_units': forms.NumberInput(attrs={'class': 'form-control form-control-sm', 'min': '1'}),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -0,0 +1,897 @@
# Generated by Django 5.2.6 on 2025-10-03 17:38
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("core", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="InsuranceApprovalRequest",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"approval_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique approval identifier",
unique=True,
),
),
(
"approval_number",
models.CharField(
help_text="Approval request number", max_length=30, unique=True
),
),
(
"object_id",
models.PositiveIntegerField(help_text="ID of the related order"),
),
(
"request_type",
models.CharField(
choices=[
("LABORATORY", "Laboratory Test"),
("RADIOLOGY", "Radiology/Imaging"),
("PHARMACY", "Medication/Prescription"),
("PROCEDURE", "Medical Procedure"),
("SURGERY", "Surgical Procedure"),
("THERAPY", "Therapy Services"),
("DME", "Durable Medical Equipment"),
("HOME_HEALTH", "Home Health Services"),
("OTHER", "Other Services"),
],
help_text="Type of approval request",
max_length=20,
),
),
(
"service_description",
models.CharField(
help_text="Description of service/procedure", max_length=500
),
),
(
"procedure_codes",
models.JSONField(
default=list, help_text="CPT/HCPCS procedure codes"
),
),
(
"diagnosis_codes",
models.JSONField(default=list, help_text="ICD-10 diagnosis codes"),
),
(
"clinical_justification",
models.TextField(help_text="Clinical justification for service"),
),
(
"medical_necessity",
models.TextField(
blank=True, help_text="Medical necessity statement", null=True
),
),
(
"alternative_treatments_tried",
models.TextField(
blank=True,
help_text="Alternative treatments attempted",
null=True,
),
),
(
"requested_quantity",
models.PositiveIntegerField(
default=1, help_text="Quantity requested"
),
),
(
"requested_visits",
models.PositiveIntegerField(
blank=True, help_text="Number of visits requested", null=True
),
),
(
"requested_units",
models.PositiveIntegerField(
blank=True, help_text="Number of units requested", null=True
),
),
(
"service_start_date",
models.DateField(help_text="Requested service start date"),
),
(
"service_end_date",
models.DateField(
blank=True, help_text="Requested service end date", null=True
),
),
(
"status",
models.CharField(
choices=[
("DRAFT", "Draft"),
("PENDING_SUBMISSION", "Pending Submission"),
("SUBMITTED", "Submitted to Insurance"),
("UNDER_REVIEW", "Under Review"),
("ADDITIONAL_INFO_REQUESTED", "Additional Info Requested"),
("APPROVED", "Approved"),
("PARTIALLY_APPROVED", "Partially Approved"),
("DENIED", "Denied"),
("APPEALED", "Appealed"),
("APPEAL_APPROVED", "Appeal Approved"),
("APPEAL_DENIED", "Appeal Denied"),
("EXPIRED", "Expired"),
("CANCELLED", "Cancelled"),
],
default="DRAFT",
help_text="Current approval status",
max_length=30,
),
),
(
"priority",
models.CharField(
choices=[
("ROUTINE", "Routine"),
("URGENT", "Urgent"),
("STAT", "STAT"),
("EMERGENCY", "Emergency"),
],
default="ROUTINE",
help_text="Request priority",
max_length=20,
),
),
(
"submission_method",
models.CharField(
blank=True,
choices=[
("FAX", "Fax"),
("PHONE", "Phone"),
("PORTAL", "Insurance Portal"),
("EMAIL", "Email"),
("MAIL", "Mail"),
("EDI", "Electronic Data Interchange"),
],
help_text="Method used to submit request",
max_length=20,
null=True,
),
),
(
"submitted_date",
models.DateTimeField(
blank=True,
help_text="Date and time submitted to insurance",
null=True,
),
),
(
"decision_date",
models.DateTimeField(
blank=True, help_text="Date insurance made decision", null=True
),
),
(
"authorization_number",
models.CharField(
blank=True,
help_text="Insurance authorization number",
max_length=100,
null=True,
),
),
(
"reference_number",
models.CharField(
blank=True,
help_text="Insurance reference number",
max_length=100,
null=True,
),
),
(
"approved_quantity",
models.PositiveIntegerField(
blank=True, help_text="Approved quantity", null=True
),
),
(
"approved_visits",
models.PositiveIntegerField(
blank=True, help_text="Approved number of visits", null=True
),
),
(
"approved_units",
models.PositiveIntegerField(
blank=True, help_text="Approved number of units", null=True
),
),
(
"approved_amount",
models.DecimalField(
blank=True,
decimal_places=2,
help_text="Approved dollar amount",
max_digits=12,
null=True,
),
),
(
"effective_date",
models.DateField(
blank=True, help_text="Authorization effective date", null=True
),
),
(
"expiration_date",
models.DateField(
blank=True, help_text="Authorization expiration date", null=True
),
),
(
"denial_reason",
models.TextField(
blank=True, help_text="Reason for denial", null=True
),
),
(
"denial_code",
models.CharField(
blank=True,
help_text="Insurance denial code",
max_length=50,
null=True,
),
),
(
"appeal_date",
models.DateTimeField(
blank=True, help_text="Date appeal was filed", null=True
),
),
(
"appeal_reason",
models.TextField(
blank=True, help_text="Reason for appeal", null=True
),
),
(
"appeal_deadline",
models.DateField(
blank=True, help_text="Deadline to file appeal", null=True
),
),
(
"last_contact_date",
models.DateTimeField(
blank=True, help_text="Last contact with insurance", null=True
),
),
(
"last_contact_method",
models.CharField(
blank=True,
help_text="Method of last contact",
max_length=20,
null=True,
),
),
(
"last_contact_notes",
models.TextField(
blank=True, help_text="Notes from last contact", null=True
),
),
(
"is_urgent",
models.BooleanField(default=False, help_text="Urgent request"),
),
(
"is_expedited",
models.BooleanField(
default=False, help_text="Expedited processing requested"
),
),
(
"requires_peer_review",
models.BooleanField(
default=False, help_text="Requires peer-to-peer review"
),
),
(
"internal_notes",
models.TextField(
blank=True, help_text="Internal staff notes", null=True
),
),
(
"insurance_notes",
models.TextField(
blank=True, help_text="Notes from insurance company", null=True
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"assigned_to",
models.ForeignKey(
blank=True,
help_text="Staff member assigned to this request",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="assigned_approvals",
to=settings.AUTH_USER_MODEL,
),
),
(
"content_type",
models.ForeignKey(
help_text="Type of order (Lab, Radiology, Pharmacy, etc.)",
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
(
"created_by",
models.ForeignKey(
blank=True,
help_text="User who created the request",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_approvals",
to=settings.AUTH_USER_MODEL,
),
),
(
"insurance_info",
models.ForeignKey(
help_text="Insurance information",
on_delete=django.db.models.deletion.CASCADE,
related_name="approval_requests",
to="patients.insuranceinfo",
),
),
(
"patient",
models.ForeignKey(
help_text="Patient",
on_delete=django.db.models.deletion.CASCADE,
related_name="approval_requests",
to="patients.patientprofile",
),
),
(
"requesting_provider",
models.ForeignKey(
help_text="Provider requesting the approval",
on_delete=django.db.models.deletion.CASCADE,
related_name="requested_approvals",
to=settings.AUTH_USER_MODEL,
),
),
(
"submitted_by",
models.ForeignKey(
blank=True,
help_text="User who submitted the request",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="submitted_approvals",
to=settings.AUTH_USER_MODEL,
),
),
(
"tenant",
models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="approval_requests",
to="core.tenant",
),
),
],
options={
"verbose_name": "Insurance Approval Request",
"verbose_name_plural": "Insurance Approval Requests",
"db_table": "insurance_approvals_request",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="ApprovalStatusHistory",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"from_status",
models.CharField(
blank=True,
help_text="Previous status",
max_length=30,
null=True,
),
),
("to_status", models.CharField(help_text="New status", max_length=30)),
(
"reason",
models.TextField(
blank=True, help_text="Reason for status change", null=True
),
),
(
"notes",
models.TextField(
blank=True, help_text="Additional notes", null=True
),
),
("changed_at", models.DateTimeField(auto_now_add=True)),
(
"changed_by",
models.ForeignKey(
blank=True,
help_text="User who made the change",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="approval_status_changes",
to=settings.AUTH_USER_MODEL,
),
),
(
"approval_request",
models.ForeignKey(
help_text="Related approval request",
on_delete=django.db.models.deletion.CASCADE,
related_name="status_history",
to="insurance_approvals.insuranceapprovalrequest",
),
),
],
options={
"verbose_name": "Approval Status History",
"verbose_name_plural": "Approval Status Histories",
"db_table": "insurance_approvals_status_history",
"ordering": ["-changed_at"],
},
),
migrations.CreateModel(
name="ApprovalDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"document_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique document identifier",
unique=True,
),
),
(
"document_type",
models.CharField(
choices=[
("MEDICAL_RECORDS", "Medical Records"),
("LAB_RESULTS", "Lab Results"),
("IMAGING_REPORTS", "Imaging Reports"),
("CLINICAL_NOTES", "Clinical Notes"),
("PRESCRIPTION", "Prescription"),
(
"LETTER_OF_MEDICAL_NECESSITY",
"Letter of Medical Necessity",
),
("PRIOR_AUTH_FORM", "Prior Authorization Form"),
("INSURANCE_CARD", "Insurance Card"),
("CONSENT_FORM", "Consent Form"),
("APPEAL_LETTER", "Appeal Letter"),
("PEER_REVIEW", "Peer Review Documentation"),
("OTHER", "Other"),
],
help_text="Type of document",
max_length=30,
),
),
("title", models.CharField(help_text="Document title", max_length=200)),
(
"description",
models.TextField(
blank=True, help_text="Document description", null=True
),
),
(
"file",
models.FileField(
help_text="Document file", upload_to="approval_documents/%Y/%m/"
),
),
(
"file_size",
models.PositiveIntegerField(help_text="File size in bytes"),
),
("mime_type", models.CharField(help_text="MIME type", max_length=100)),
("uploaded_at", models.DateTimeField(auto_now_add=True)),
(
"uploaded_by",
models.ForeignKey(
blank=True,
help_text="User who uploaded the document",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="uploaded_approval_documents",
to=settings.AUTH_USER_MODEL,
),
),
(
"approval_request",
models.ForeignKey(
help_text="Related approval request",
on_delete=django.db.models.deletion.CASCADE,
related_name="documents",
to="insurance_approvals.insuranceapprovalrequest",
),
),
],
options={
"verbose_name": "Approval Document",
"verbose_name_plural": "Approval Documents",
"db_table": "insurance_approvals_document",
"ordering": ["-uploaded_at"],
},
),
migrations.CreateModel(
name="ApprovalCommunicationLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"communication_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique communication identifier",
unique=True,
),
),
(
"communication_type",
models.CharField(
choices=[
("PHONE_CALL", "Phone Call"),
("FAX_SENT", "Fax Sent"),
("FAX_RECEIVED", "Fax Received"),
("EMAIL_SENT", "Email Sent"),
("EMAIL_RECEIVED", "Email Received"),
("PORTAL_MESSAGE", "Portal Message"),
("MAIL_SENT", "Mail Sent"),
("MAIL_RECEIVED", "Mail Received"),
("IN_PERSON", "In Person"),
],
help_text="Type of communication",
max_length=20,
),
),
(
"contact_person",
models.CharField(
blank=True,
help_text="Insurance contact person",
max_length=200,
null=True,
),
),
(
"contact_number",
models.CharField(
blank=True,
help_text="Contact phone/fax number",
max_length=50,
null=True,
),
),
(
"subject",
models.CharField(help_text="Communication subject", max_length=200),
),
("message", models.TextField(help_text="Message content")),
(
"response",
models.TextField(
blank=True, help_text="Response received", null=True
),
),
(
"outcome",
models.CharField(
blank=True,
help_text="Communication outcome",
max_length=200,
null=True,
),
),
(
"follow_up_required",
models.BooleanField(default=False, help_text="Follow-up required"),
),
(
"follow_up_date",
models.DateField(blank=True, help_text="Follow-up date", null=True),
),
("communicated_at", models.DateTimeField(auto_now_add=True)),
(
"communicated_by",
models.ForeignKey(
blank=True,
help_text="User who made the communication",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="approval_communications",
to=settings.AUTH_USER_MODEL,
),
),
(
"approval_request",
models.ForeignKey(
help_text="Related approval request",
on_delete=django.db.models.deletion.CASCADE,
related_name="communications",
to="insurance_approvals.insuranceapprovalrequest",
),
),
],
options={
"verbose_name": "Approval Communication Log",
"verbose_name_plural": "Approval Communication Logs",
"db_table": "insurance_approvals_communication_log",
"ordering": ["-communicated_at"],
},
),
migrations.CreateModel(
name="ApprovalTemplate",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"template_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique template identifier",
unique=True,
),
),
("name", models.CharField(help_text="Template name", max_length=200)),
(
"description",
models.TextField(
blank=True, help_text="Template description", null=True
),
),
(
"request_type",
models.CharField(
help_text="Type of request this template is for", max_length=20
),
),
(
"insurance_company",
models.CharField(
blank=True,
help_text="Specific insurance company (optional)",
max_length=200,
null=True,
),
),
(
"clinical_justification_template",
models.TextField(help_text="Template for clinical justification"),
),
(
"medical_necessity_template",
models.TextField(
blank=True,
help_text="Template for medical necessity statement",
null=True,
),
),
(
"required_documents",
models.JSONField(
default=list, help_text="List of required document types"
),
),
(
"required_codes",
models.JSONField(
default=dict, help_text="Required procedure/diagnosis codes"
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Template is active"),
),
(
"usage_count",
models.PositiveIntegerField(
default=0, help_text="Number of times template has been used"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"created_by",
models.ForeignKey(
blank=True,
help_text="User who created the template",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_approval_templates",
to=settings.AUTH_USER_MODEL,
),
),
(
"tenant",
models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="approval_templates",
to="core.tenant",
),
),
],
options={
"verbose_name": "Approval Template",
"verbose_name_plural": "Approval Templates",
"db_table": "insurance_approvals_template",
"ordering": ["name"],
"indexes": [
models.Index(
fields=["tenant", "is_active"],
name="insurance_a_tenant__21856c_idx",
),
models.Index(
fields=["request_type"], name="insurance_a_request_631392_idx"
),
],
"unique_together": {("tenant", "name")},
},
),
migrations.AddIndex(
model_name="insuranceapprovalrequest",
index=models.Index(
fields=["tenant", "status"], name="insurance_a_tenant__d6763e_idx"
),
),
migrations.AddIndex(
model_name="insuranceapprovalrequest",
index=models.Index(
fields=["patient", "status"], name="insurance_a_patient_00ddbd_idx"
),
),
migrations.AddIndex(
model_name="insuranceapprovalrequest",
index=models.Index(
fields=["insurance_info", "status"],
name="insurance_a_insuran_438196_idx",
),
),
migrations.AddIndex(
model_name="insuranceapprovalrequest",
index=models.Index(
fields=["approval_number"], name="insurance_a_approva_cefd6a_idx"
),
),
migrations.AddIndex(
model_name="insuranceapprovalrequest",
index=models.Index(
fields=["authorization_number"], name="insurance_a_authori_f05618_idx"
),
),
migrations.AddIndex(
model_name="insuranceapprovalrequest",
index=models.Index(
fields=["expiration_date"], name="insurance_a_expirat_ef068e_idx"
),
),
migrations.AddIndex(
model_name="insuranceapprovalrequest",
index=models.Index(
fields=["assigned_to", "status"], name="insurance_a_assigne_19b007_idx"
),
),
migrations.AddIndex(
model_name="insuranceapprovalrequest",
index=models.Index(
fields=["content_type", "object_id"],
name="insurance_a_content_de7404_idx",
),
),
migrations.AddIndex(
model_name="insuranceapprovalrequest",
index=models.Index(
fields=["priority", "status"], name="insurance_a_priorit_0abcbd_idx"
),
),
migrations.AddIndex(
model_name="approvalstatushistory",
index=models.Index(
fields=["approval_request", "changed_at"],
name="insurance_a_approva_a5298b_idx",
),
),
migrations.AddIndex(
model_name="approvaldocument",
index=models.Index(
fields=["approval_request", "document_type"],
name="insurance_a_approva_eae9e0_idx",
),
),
migrations.AddIndex(
model_name="approvalcommunicationlog",
index=models.Index(
fields=["approval_request", "communicated_at"],
name="insurance_a_approva_fa779d_idx",
),
),
migrations.AddIndex(
model_name="approvalcommunicationlog",
index=models.Index(
fields=["follow_up_required", "follow_up_date"],
name="insurance_a_follow__2a3b0a_idx",
),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 5.2.3 on 2025-10-04 20:52
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('insurance_approvals', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='insuranceapprovalrequest',
name='content_type',
field=models.ForeignKey(blank=True, help_text='Type of order (Lab, Radiology, Pharmacy, etc.)', null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
),
migrations.AlterField(
model_name='insuranceapprovalrequest',
name='object_id',
field=models.PositiveIntegerField(blank=True, help_text='ID of the related order', null=True),
),
]

Some files were not shown because too many files have changed in this diff Show More