added gitignore file
This commit is contained in:
parent
6b3916abaf
commit
6e643e1cac
172
.gitignore
vendored
Normal file
172
.gitignore
vendored
Normal 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
|
||||||
349
EMR_DATA_GENERATOR_README.md
Normal file
349
EMR_DATA_GENERATOR_README.md
Normal 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.
|
||||||
246
SURGERY_CONSOLIDATION_SUMMARY.md
Normal file
246
SURGERY_CONSOLIDATION_SUMMARY.md
Normal 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
|
||||||
328
WORKFLOW_ENHANCEMENT_COMPLETE.md
Normal file
328
WORKFLOW_ENHANCEMENT_COMPLETE.md
Normal 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!
|
||||||
6261
_refactor_report/model_map.json
Normal file
6261
_refactor_report/model_map.json
Normal file
File diff suppressed because it is too large
Load Diff
5822
_refactor_report/modular_refactoring_report.md
Normal file
5822
_refactor_report/modular_refactoring_report.md
Normal file
File diff suppressed because it is too large
Load Diff
149
_refactor_report/overlaps.json
Normal file
149
_refactor_report/overlaps.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
accounts/__pycache__/adapter.cpython-312.pyc
Normal file
BIN
accounts/__pycache__/adapter.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
44
accounts/adapter.py
Normal file
44
accounts/adapter.py
Normal 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
|
||||||
|
)
|
||||||
472
accounts/migrations/0001_initial.py
Normal file
472
accounts/migrations/0001_initial.py
Normal 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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
116
accounts/migrations/0002_initial.py
Normal file
116
accounts/migrations/0002_initial.py
Normal 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
accounts/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
accounts/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
accounts/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
BIN
accounts/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
Binary file not shown.
@ -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'),
|
||||||
|
|||||||
1205
accounts/views.py
1205
accounts/views.py
File diff suppressed because it is too large
Load Diff
623
analytics/migrations/0001_initial.py
Normal file
623
analytics/migrations/0001_initial.py
Normal 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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
309
analytics/migrations/0002_initial.py
Normal file
309
analytics/migrations/0002_initial.py
Normal 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
analytics/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
analytics/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
analytics/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
BIN
analytics/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
1568
appointments/migrations/0001_initial.py
Normal file
1568
appointments/migrations/0001_initial.py
Normal file
File diff suppressed because it is too large
Load Diff
526
appointments/migrations/0002_initial.py
Normal file
526
appointments/migrations/0002_initial.py
Normal 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
appointments/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
appointments/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
appointments/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
BIN
appointments/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
Binary file not shown.
@ -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); }
|
||||||
|
|||||||
316
appointments/templates/appointments/queue/queue_display.html
Normal file
316
appointments/templates/appointments/queue/queue_display.html
Normal 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 %}
|
||||||
@ -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) {#}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@ -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
@ -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(
|
||||||
|
|||||||
1172
billing/migrations/0001_initial.py
Normal file
1172
billing/migrations/0001_initial.py
Normal file
File diff suppressed because it is too large
Load Diff
418
billing/migrations/0002_initial.py
Normal file
418
billing/migrations/0002_initial.py
Normal 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"),
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
billing/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
billing/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
billing/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
BIN
billing/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
Binary file not shown.
@ -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 %}
|
||||||
|
|||||||
@ -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' %}
|
||||||
|
|||||||
673
blood_bank/migrations/0001_initial.py
Normal file
673
blood_bank/migrations/0001_initial.py
Normal 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"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
297
blood_bank/migrations/0002_initial.py
Normal file
297
blood_bank/migrations/0002_initial.py
Normal 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")},
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
blood_bank/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
blood_bank/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
blood_bank/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
BIN
blood_bank/migrations/__pycache__/0002_initial.cpython-312.pyc
Normal file
Binary file not shown.
968
communications/migrations/0001_initial.py
Normal file
968
communications/migrations/0001_initial.py
Normal 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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
375
communications/migrations/0002_initial.py
Normal file
375
communications/migrations/0002_initial.py
Normal 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")},
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
921
core/migrations/0001_initial.py
Normal file
921
core/migrations/0001_initial.py
Normal 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",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
@ -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
BIN
db.sqlite3
Normal file
Binary file not shown.
Binary file not shown.
2727
emr/migrations/0001_initial.py
Normal file
2727
emr/migrations/0001_initial.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
emr/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
emr/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -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 ''}"
|
||||||
|
|||||||
@ -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">ê</span>{{ bill.total_amount|floatformat:'2g' }}</td>
|
||||||
<td>${{ bill.balance_amount|floatformat:2 }}</td>
|
<td><span class="symbol">ê</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>
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
382
emr_data.py
382
emr_data.py
@ -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
83
enhancements.txt
Normal 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)
|
||||||
1194
facility_management/migrations/0001_initial.py
Normal file
1194
facility_management/migrations/0001_initial.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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*']
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
1921
hr/migrations/0001_initial.py
Normal file
1921
hr/migrations/0001_initial.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
hr/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
hr/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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]
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|
||||||
|
|||||||
1644
inpatients/migrations/0001_initial.py
Normal file
1644
inpatients/migrations/0001_initial.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
inpatients/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
inpatients/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
@ -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
|
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
@ -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__":
|
||||||
|
|||||||
BIN
insurance_approvals/__pycache__/forms.cpython-312.pyc
Normal file
BIN
insurance_approvals/__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
insurance_approvals/__pycache__/urls.cpython-312.pyc
Normal file
BIN
insurance_approvals/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
insurance_approvals/__pycache__/views.cpython-312.pyc
Normal file
BIN
insurance_approvals/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
@ -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):
|
||||||
|
|||||||
897
insurance_approvals/migrations/0001_initial.py
Normal file
897
insurance_approvals/migrations/0001_initial.py
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user