before changing surgery
This commit is contained in:
parent
ab2c4a36c5
commit
6b3916abaf
588
INSURANCE_APPROVAL_INTEGRATION.md
Normal file
588
INSURANCE_APPROVAL_INTEGRATION.md
Normal file
@ -0,0 +1,588 @@
|
||||
# Insurance Approval Integration Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Insurance Approval Request module has been successfully integrated with all relevant clinical order modules in the hospital management system. This document outlines the integration points and usage patterns.
|
||||
|
||||
---
|
||||
|
||||
## Integrated Modules
|
||||
|
||||
### 1. Laboratory Module ✅
|
||||
**Model:** `LabOrder`
|
||||
**File:** `laboratory/models.py`
|
||||
|
||||
#### Integration Details:
|
||||
- Added `GenericRelation` to link lab orders with approval requests
|
||||
- Added helper methods for approval status checking
|
||||
- Supports multi-tenant approval tracking
|
||||
|
||||
#### Helper Methods Added:
|
||||
```python
|
||||
# Check if order has valid approval
|
||||
lab_order.has_valid_approval() # Returns True/False
|
||||
|
||||
# Get active approval
|
||||
lab_order.get_active_approval() # Returns InsuranceApprovalRequest or None
|
||||
|
||||
# Check if approval is required
|
||||
lab_order.requires_approval() # Returns True/False
|
||||
|
||||
# Get approval status for display
|
||||
lab_order.approval_status # Returns status string
|
||||
```
|
||||
|
||||
#### Usage Example:
|
||||
```python
|
||||
from laboratory.models import LabOrder
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
|
||||
# Create a lab order
|
||||
lab_order = LabOrder.objects.get(order_number='LAB-1-000001')
|
||||
|
||||
# Check if approval is needed
|
||||
if lab_order.requires_approval():
|
||||
# Create approval request
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=lab_order.tenant,
|
||||
patient=lab_order.patient,
|
||||
insurance_info=lab_order.patient.insurance_info.first(),
|
||||
request_type='LABORATORY',
|
||||
content_object=lab_order, # Links to lab order
|
||||
service_description=f"Lab tests: {', '.join([t.test_name for t in lab_order.tests.all()])}",
|
||||
procedure_codes=', '.join([t.cpt_code for t in lab_order.tests.all() if t.cpt_code]),
|
||||
diagnosis_codes=lab_order.diagnosis_code or '',
|
||||
clinical_justification=lab_order.clinical_indication,
|
||||
requesting_provider=lab_order.ordering_provider,
|
||||
service_start_date=lab_order.collection_datetime.date() if lab_order.collection_datetime else timezone.now().date(),
|
||||
priority='ROUTINE',
|
||||
status='PENDING_SUBMISSION',
|
||||
)
|
||||
|
||||
# Check approval status
|
||||
if lab_order.has_valid_approval():
|
||||
print("Order has valid approval - proceed with processing")
|
||||
else:
|
||||
print(f"Approval status: {lab_order.approval_status}")
|
||||
|
||||
# Get all approval requests for this order
|
||||
approvals = lab_order.approval_requests.all()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Radiology Module ✅
|
||||
**Model:** `ImagingOrder`
|
||||
**File:** `radiology/models.py`
|
||||
|
||||
#### Integration Details:
|
||||
- Added `GenericRelation` to link imaging orders with approval requests
|
||||
- Added identical helper methods as laboratory module
|
||||
- Supports all imaging modalities (CT, MRI, X-Ray, etc.)
|
||||
|
||||
#### Helper Methods Added:
|
||||
```python
|
||||
# Check if order has valid approval
|
||||
imaging_order.has_valid_approval() # Returns True/False
|
||||
|
||||
# Get active approval
|
||||
imaging_order.get_active_approval() # Returns InsuranceApprovalRequest or None
|
||||
|
||||
# Check if approval is required
|
||||
imaging_order.requires_approval() # Returns True/False
|
||||
|
||||
# Get approval status for display
|
||||
imaging_order.approval_status # Returns status string
|
||||
```
|
||||
|
||||
#### Usage Example:
|
||||
```python
|
||||
from radiology.models import ImagingOrder
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
|
||||
# Create an imaging order
|
||||
imaging_order = ImagingOrder.objects.get(order_number='IMG-1-000001')
|
||||
|
||||
# Check if approval is needed
|
||||
if imaging_order.requires_approval():
|
||||
# Create approval request
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=imaging_order.tenant,
|
||||
patient=imaging_order.patient,
|
||||
insurance_info=imaging_order.patient.insurance_info.first(),
|
||||
request_type='RADIOLOGY',
|
||||
content_object=imaging_order, # Links to imaging order
|
||||
service_description=imaging_order.study_description,
|
||||
procedure_codes=imaging_order.modality, # Could be enhanced with CPT codes
|
||||
diagnosis_codes=imaging_order.diagnosis_code or '',
|
||||
clinical_justification=imaging_order.clinical_indication,
|
||||
requesting_provider=imaging_order.ordering_provider,
|
||||
service_start_date=imaging_order.requested_datetime.date() if imaging_order.requested_datetime else timezone.now().date(),
|
||||
priority='URGENT' if imaging_order.is_stat else 'ROUTINE',
|
||||
status='PENDING_SUBMISSION',
|
||||
)
|
||||
|
||||
# Access approvals
|
||||
active_approval = imaging_order.get_active_approval()
|
||||
if active_approval:
|
||||
print(f"Authorization Number: {active_approval.authorization_number}")
|
||||
print(f"Expires: {active_approval.expiration_date}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Integration Patterns
|
||||
|
||||
### Pattern 1: Pre-Order Approval Check
|
||||
```python
|
||||
def create_order_with_approval_check(patient, order_type, **order_data):
|
||||
"""
|
||||
Create an order and check if insurance approval is required.
|
||||
"""
|
||||
# Create the order
|
||||
if order_type == 'LAB':
|
||||
order = LabOrder.objects.create(patient=patient, **order_data)
|
||||
elif order_type == 'IMAGING':
|
||||
order = ImagingOrder.objects.create(patient=patient, **order_data)
|
||||
|
||||
# Check if approval is required
|
||||
if order.requires_approval():
|
||||
# Redirect to approval request creation
|
||||
return {
|
||||
'order': order,
|
||||
'requires_approval': True,
|
||||
'redirect_url': f'/insurance-approvals/create/?order_type={order_type}&order_id={order.id}'
|
||||
}
|
||||
|
||||
return {
|
||||
'order': order,
|
||||
'requires_approval': False
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Approval Status Display
|
||||
```python
|
||||
def get_order_status_with_approval(order):
|
||||
"""
|
||||
Get comprehensive order status including approval information.
|
||||
"""
|
||||
status = {
|
||||
'order_status': order.status,
|
||||
'has_insurance': order.patient.insurance_info.exists(),
|
||||
'requires_approval': order.requires_approval(),
|
||||
'approval_status': order.approval_status,
|
||||
'can_proceed': False
|
||||
}
|
||||
|
||||
if not status['has_insurance']:
|
||||
status['can_proceed'] = True # No insurance, no approval needed
|
||||
elif order.has_valid_approval():
|
||||
status['can_proceed'] = True
|
||||
status['approval'] = order.get_active_approval()
|
||||
|
||||
return status
|
||||
```
|
||||
|
||||
### Pattern 3: Bulk Approval Status Check
|
||||
```python
|
||||
def get_orders_needing_approval(tenant, order_type='LAB'):
|
||||
"""
|
||||
Get all orders that need insurance approval.
|
||||
"""
|
||||
if order_type == 'LAB':
|
||||
Model = LabOrder
|
||||
elif order_type == 'IMAGING':
|
||||
Model = ImagingOrder
|
||||
|
||||
orders = Model.objects.filter(
|
||||
tenant=tenant,
|
||||
status__in=['PENDING', 'SCHEDULED']
|
||||
)
|
||||
|
||||
orders_needing_approval = []
|
||||
for order in orders:
|
||||
if order.requires_approval():
|
||||
orders_needing_approval.append({
|
||||
'order': order,
|
||||
'patient': order.patient,
|
||||
'insurance': order.patient.insurance_info.first(),
|
||||
'approval_status': order.approval_status
|
||||
})
|
||||
|
||||
return orders_needing_approval
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Approval Request Types
|
||||
|
||||
The system supports the following request types:
|
||||
|
||||
1. **LABORATORY** - Lab tests and panels
|
||||
2. **RADIOLOGY** - Imaging studies (CT, MRI, X-Ray, etc.)
|
||||
3. **PHARMACY** - Medications (to be integrated)
|
||||
4. **PROCEDURE** - Medical procedures (to be integrated)
|
||||
5. **SURGERY** - Surgical procedures (to be integrated)
|
||||
6. **THERAPY** - Physical/Occupational therapy
|
||||
7. **DME** - Durable Medical Equipment
|
||||
8. **HOME_HEALTH** - Home health services
|
||||
9. **HOSPICE** - Hospice care
|
||||
10. **TRANSPORTATION** - Medical transportation
|
||||
11. **OTHER** - Other services
|
||||
|
||||
---
|
||||
|
||||
## Approval Workflow States
|
||||
|
||||
```
|
||||
DRAFT → PENDING_SUBMISSION → SUBMITTED → UNDER_REVIEW
|
||||
↓
|
||||
MORE_INFO_REQUIRED (can loop back to SUBMITTED)
|
||||
↓
|
||||
APPROVED / PARTIALLY_APPROVED / DENIED
|
||||
↓
|
||||
APPEAL_SUBMITTED → APPEAL_APPROVED / APPEAL_DENIED
|
||||
↓
|
||||
EXPIRED (if expiration_date passes)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Relationships
|
||||
|
||||
### GenericForeignKey Pattern
|
||||
```python
|
||||
# In InsuranceApprovalRequest model
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
# In LabOrder/ImagingOrder models
|
||||
approval_requests = GenericRelation(
|
||||
'insurance_approvals.InsuranceApprovalRequest',
|
||||
content_type_field='content_type',
|
||||
object_id_field='object_id',
|
||||
related_query_name='lab_order' # or 'imaging_order'
|
||||
)
|
||||
```
|
||||
|
||||
### Querying Across Relationships
|
||||
```python
|
||||
# Get all lab orders with approvals
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from laboratory.models import LabOrder
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
|
||||
lab_order_ct = ContentType.objects.get_for_model(LabOrder)
|
||||
approvals_for_lab_orders = InsuranceApprovalRequest.objects.filter(
|
||||
content_type=lab_order_ct
|
||||
)
|
||||
|
||||
# Get specific lab order's approvals
|
||||
lab_order = LabOrder.objects.get(pk=1)
|
||||
approvals = lab_order.approval_requests.all()
|
||||
|
||||
# Reverse query - get order from approval
|
||||
approval = InsuranceApprovalRequest.objects.get(pk=1)
|
||||
order = approval.content_object # Returns LabOrder or ImagingOrder instance
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template Integration Examples
|
||||
|
||||
### Display Approval Status in Order List
|
||||
```django
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td>{{ order.order_number }}</td>
|
||||
<td>{{ order.patient.get_full_name }}</td>
|
||||
<td>
|
||||
{% if order.approval_status == 'APPROVED' %}
|
||||
<span class="badge bg-success">Approved</span>
|
||||
{% elif order.approval_status == 'APPROVAL_REQUIRED' %}
|
||||
<span class="badge bg-warning">Approval Required</span>
|
||||
<a href="{% url 'insurance_approvals:create' %}?order_id={{ order.id }}"
|
||||
class="btn btn-sm btn-primary">Request Approval</a>
|
||||
{% elif order.approval_status == 'NO_INSURANCE' %}
|
||||
<span class="badge bg-secondary">No Insurance</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info">{{ order.get_approval_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Display Approval Details in Order Detail
|
||||
```django
|
||||
{% if order.has_valid_approval %}
|
||||
<div class="alert alert-success">
|
||||
<h6><i class="fa fa-check-circle"></i> Insurance Approval Active</h6>
|
||||
{% with approval=order.get_active_approval %}
|
||||
<p><strong>Authorization Number:</strong> {{ approval.authorization_number }}</p>
|
||||
<p><strong>Expires:</strong> {{ approval.expiration_date|date:"M d, Y" }}</p>
|
||||
<p><strong>Approved Quantity:</strong> {{ approval.approved_quantity }}</p>
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% elif order.requires_approval %}
|
||||
<div class="alert alert-warning">
|
||||
<h6><i class="fa fa-exclamation-triangle"></i> Insurance Approval Required</h6>
|
||||
<p>This order requires insurance approval before processing.</p>
|
||||
<a href="{% url 'insurance_approvals:create' %}?order_id={{ order.id }}"
|
||||
class="btn btn-primary">Request Approval</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Integration (DRF)
|
||||
|
||||
### Serializer Example
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
from laboratory.models import LabOrder
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
|
||||
class LabOrderSerializer(serializers.ModelSerializer):
|
||||
approval_status = serializers.CharField(read_only=True)
|
||||
has_valid_approval = serializers.BooleanField(read_only=True)
|
||||
requires_approval = serializers.SerializerMethodField()
|
||||
active_approval = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = LabOrder
|
||||
fields = '__all__'
|
||||
|
||||
def get_requires_approval(self, obj):
|
||||
return obj.requires_approval()
|
||||
|
||||
def get_active_approval(self, obj):
|
||||
approval = obj.get_active_approval()
|
||||
if approval:
|
||||
return {
|
||||
'id': approval.id,
|
||||
'approval_number': approval.approval_number,
|
||||
'authorization_number': approval.authorization_number,
|
||||
'status': approval.status,
|
||||
'expiration_date': approval.expiration_date
|
||||
}
|
||||
return None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Integration Points
|
||||
|
||||
### Pharmacy Module (Pending)
|
||||
- **Model:** `PharmacyOrder` or `Prescription`
|
||||
- **Request Type:** `PHARMACY`
|
||||
- Same integration pattern as Lab and Radiology
|
||||
|
||||
### Operating Theatre Module (Pending)
|
||||
- **Model:** `SurgerySchedule` or `OperativeCase`
|
||||
- **Request Type:** `SURGERY` or `PROCEDURE`
|
||||
- Same integration pattern
|
||||
|
||||
### Other Potential Integrations:
|
||||
- Home Health Orders
|
||||
- DME Orders
|
||||
- Physical Therapy Orders
|
||||
- Hospice Care Orders
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
**IMPORTANT:** Migrations are NOT included in this integration. The user will handle migrations separately.
|
||||
|
||||
### To apply changes:
|
||||
1. Create migrations: `python manage.py makemigrations laboratory radiology`
|
||||
2. Review migrations carefully
|
||||
3. Apply migrations: `python manage.py migrate`
|
||||
|
||||
---
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Unit Tests
|
||||
```python
|
||||
from django.test import TestCase
|
||||
from laboratory.models import LabOrder
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
|
||||
class LabOrderApprovalIntegrationTest(TestCase):
|
||||
def test_order_requires_approval_with_insurance(self):
|
||||
# Create patient with insurance
|
||||
patient = self.create_patient_with_insurance()
|
||||
order = LabOrder.objects.create(patient=patient, ...)
|
||||
|
||||
self.assertTrue(order.requires_approval())
|
||||
|
||||
def test_order_has_valid_approval(self):
|
||||
order = self.create_lab_order()
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
content_object=order,
|
||||
status='APPROVED',
|
||||
expiration_date=timezone.now().date() + timedelta(days=30),
|
||||
...
|
||||
)
|
||||
|
||||
self.assertTrue(order.has_valid_approval())
|
||||
self.assertEqual(order.get_active_approval(), approval)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Pharmacy Module ✅
|
||||
**Model:** `Prescription`
|
||||
**File:** `pharmacy/models.py`
|
||||
|
||||
#### Integration Details:
|
||||
- Added `GenericRelation` to link prescriptions with approval requests
|
||||
- Added helper methods for approval status checking
|
||||
- Enhanced with prior authorization support
|
||||
- Checks medication formulary status for approval requirements
|
||||
|
||||
#### Helper Methods Added:
|
||||
```python
|
||||
# Check if prescription has valid approval
|
||||
prescription.has_valid_approval() # Returns True/False
|
||||
|
||||
# Get active approval
|
||||
prescription.get_active_approval() # Returns InsuranceApprovalRequest or None
|
||||
|
||||
# Check if approval is required
|
||||
prescription.requires_approval() # Returns True/False
|
||||
|
||||
# Get approval status for display
|
||||
prescription.approval_status # Returns status string
|
||||
```
|
||||
|
||||
#### Special Features:
|
||||
- **Prior Authorization Integration**: Checks both approval requests and prior authorization fields
|
||||
- **Formulary Status**: Automatically requires approval for RESTRICTED or PRIOR_AUTH medications
|
||||
- **Enhanced Status**: Returns specific statuses like 'PRIOR_AUTH_REQUIRED', 'PRIOR_AUTH_EXPIRED', etc.
|
||||
|
||||
#### Usage Example:
|
||||
```python
|
||||
from pharmacy.models import Prescription
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
|
||||
# Create a prescription
|
||||
prescription = Prescription.objects.get(prescription_number='RX-1-000001')
|
||||
|
||||
# Check if approval is needed (considers formulary status)
|
||||
if prescription.requires_approval():
|
||||
# Create approval request
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=prescription.tenant,
|
||||
patient=prescription.patient,
|
||||
insurance_info=prescription.patient.insurance_info.first(),
|
||||
request_type='PHARMACY',
|
||||
content_object=prescription, # Links to prescription
|
||||
service_description=f"{prescription.medication.display_name} - {prescription.quantity_prescribed} {prescription.quantity_unit}",
|
||||
procedure_codes=prescription.medication.ndc_number or '',
|
||||
diagnosis_codes=prescription.diagnosis_code or '',
|
||||
clinical_justification=prescription.indication or 'As prescribed',
|
||||
requesting_provider=prescription.prescriber,
|
||||
service_start_date=prescription.date_written,
|
||||
priority='URGENT' if prescription.medication.is_controlled_substance else 'ROUTINE',
|
||||
status='PENDING_SUBMISSION',
|
||||
requested_quantity=prescription.quantity_prescribed,
|
||||
)
|
||||
|
||||
# Check prior authorization
|
||||
if prescription.prior_authorization_required:
|
||||
if not prescription.prior_authorization_number:
|
||||
print("Prior authorization required but not obtained")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Operating Theatre Module ✅
|
||||
**Model:** `SurgicalCase`
|
||||
**File:** `operating_theatre/models.py`
|
||||
|
||||
#### Integration Details:
|
||||
- Added `GenericRelation` to link surgical cases with approval requests
|
||||
- Added helper methods for approval status checking
|
||||
- Enhanced with emergency case handling
|
||||
- Supports elective and emergency approval workflows
|
||||
|
||||
#### Helper Methods Added:
|
||||
```python
|
||||
# Check if surgical case has valid approval
|
||||
surgical_case.has_valid_approval() # Returns True/False
|
||||
|
||||
# Get active approval
|
||||
surgical_case.get_active_approval() # Returns InsuranceApprovalRequest or None
|
||||
|
||||
# Check if approval is required
|
||||
surgical_case.requires_approval() # Returns True/False
|
||||
|
||||
# Get approval status for display
|
||||
surgical_case.approval_status # Returns status string
|
||||
```
|
||||
|
||||
#### Special Features:
|
||||
- **Emergency Case Handling**: Different approval requirements for emergency vs elective cases
|
||||
- **Enhanced Status**: Returns 'EMERGENCY_APPROVAL_REQUIRED' for emergency cases
|
||||
- **Procedure Code Support**: Integrates with procedure_codes JSON field
|
||||
|
||||
#### Usage Example:
|
||||
```python
|
||||
from operating_theatre.models import SurgicalCase
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
|
||||
# Create a surgical case
|
||||
surgical_case = SurgicalCase.objects.get(case_number='SURG-20250103-0001')
|
||||
|
||||
# Check if approval is needed
|
||||
if surgical_case.requires_approval():
|
||||
# Determine priority based on case type
|
||||
if surgical_case.is_emergency:
|
||||
priority = 'STAT'
|
||||
is_expedited = True
|
||||
else:
|
||||
priority = 'ROUTINE'
|
||||
is_expedited = False
|
||||
|
||||
# Create approval request
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=surgical_case.tenant,
|
||||
patient=surgical_case.patient,
|
||||
insurance_info=surgical_case.patient.insurance_info.first(),
|
||||
request_type='SURGERY',
|
||||
content_object=surgical_case, # Links to surgical case
|
||||
service_description=surgical_case.primary_procedure,
|
||||
procedure_codes=', '.join(surgical_case.procedure_codes) if surgical_case.procedure_codes else '',
|
||||
diagnosis_codes=', '.join(surgical_case.diagnosis_codes) if surgical_case.diagnosis_codes else surgical_case.diagnosis,
|
||||
clinical_justification=surgical_case.clinical_notes or f"Surgical intervention required for {surgical_case.diagnosis}",
|
||||
requesting_provider=surgical_case.primary_surgeon,
|
||||
service_start_date=surgical_case.scheduled_start.date(),
|
||||
priority=priority,
|
||||
is_expedited=is_expedited,
|
||||
status='PENDING_SUBMISSION',
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
✅ **Laboratory Module** - Fully integrated
|
||||
✅ **Radiology Module** - Fully integrated
|
||||
✅ **Pharmacy Module** - Fully integrated (with prior auth support)
|
||||
✅ **Operating Theatre Module** - Fully integrated (with emergency handling)
|
||||
|
||||
All integrated modules now support:
|
||||
- Automatic approval requirement detection
|
||||
- Approval status tracking
|
||||
- Multi-tenant approval management
|
||||
- Complete audit trail
|
||||
- Flexible workflow states
|
||||
- Module-specific enhancements (prior auth, emergency cases, etc.)
|
||||
|
||||
The integration is backward compatible and does not affect existing functionality.
|
||||
504
INSURANCE_APPROVAL_MIGRATION_GUIDE.md
Normal file
504
INSURANCE_APPROVAL_MIGRATION_GUIDE.md
Normal file
@ -0,0 +1,504 @@
|
||||
# Insurance Approval Integration - Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide provides step-by-step instructions for applying the insurance approval integration to your hospital management system.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting the migration:
|
||||
|
||||
1. ✅ Backup your database
|
||||
2. ✅ Ensure all existing migrations are applied
|
||||
3. ✅ Review the integration documentation (`INSURANCE_APPROVAL_INTEGRATION.md`)
|
||||
4. ✅ Test in a development environment first
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Verify Current State
|
||||
|
||||
### Check Existing Migrations
|
||||
|
||||
```bash
|
||||
# Check migration status
|
||||
python manage.py showmigrations
|
||||
|
||||
# Look for these apps:
|
||||
# - insurance_approvals
|
||||
# - laboratory
|
||||
# - radiology
|
||||
# - pharmacy
|
||||
# - operating_theatre
|
||||
```
|
||||
|
||||
### Verify Database Backup
|
||||
|
||||
```bash
|
||||
# For SQLite (development)
|
||||
cp db.sqlite3 db.sqlite3.backup
|
||||
|
||||
# For PostgreSQL (production)
|
||||
pg_dump -U username -d database_name > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# For MySQL (production)
|
||||
mysqldump -u username -p database_name > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create Migrations
|
||||
|
||||
### Generate Migrations for All Modified Apps
|
||||
|
||||
```bash
|
||||
# Create migrations for all integrated modules
|
||||
python manage.py makemigrations laboratory radiology pharmacy operating_theatre
|
||||
|
||||
# Expected output:
|
||||
# Migrations for 'laboratory':
|
||||
# laboratory/migrations/0XXX_add_approval_integration.py
|
||||
# - Add field approval_requests to laborder
|
||||
# Migrations for 'radiology':
|
||||
# radiology/migrations/0XXX_add_approval_integration.py
|
||||
# - Add field approval_requests to imagingorder
|
||||
# Migrations for 'pharmacy':
|
||||
# pharmacy/migrations/0XXX_add_approval_integration.py
|
||||
# - Add field approval_requests to prescription
|
||||
# Migrations for 'operating_theatre':
|
||||
# operating_theatre/migrations/0XXX_add_approval_integration.py
|
||||
# - Add field approval_requests to surgicalcase
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Review Generated Migrations
|
||||
|
||||
### Check Migration Files
|
||||
|
||||
Review each generated migration file to ensure it only adds the GenericRelation field:
|
||||
|
||||
```python
|
||||
# Example: laboratory/migrations/0XXX_add_approval_integration.py
|
||||
|
||||
from django.db import migrations
|
||||
import django.contrib.contenttypes.fields
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('laboratory', '0XXX_previous_migration'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('insurance_approvals', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Note: GenericRelation doesn't create database fields
|
||||
# This migration may be empty or just update model metadata
|
||||
]
|
||||
```
|
||||
|
||||
**Important:** GenericRelation fields don't create actual database columns. The migrations might be empty or only update model metadata.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Apply Migrations
|
||||
|
||||
### Run Migrations
|
||||
|
||||
```bash
|
||||
# Apply all pending migrations
|
||||
python manage.py migrate
|
||||
|
||||
# Expected output:
|
||||
# Running migrations:
|
||||
# Applying laboratory.0XXX_add_approval_integration... OK
|
||||
# Applying radiology.0XXX_add_approval_integration... OK
|
||||
# Applying pharmacy.0XXX_add_approval_integration... OK
|
||||
# Applying operating_theatre.0XXX_add_approval_integration... OK
|
||||
```
|
||||
|
||||
### Verify Migration Success
|
||||
|
||||
```bash
|
||||
# Check migration status again
|
||||
python manage.py showmigrations
|
||||
|
||||
# All migrations should show [X] (applied)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Test Integration
|
||||
|
||||
### Test in Django Shell
|
||||
|
||||
```python
|
||||
python manage.py shell
|
||||
|
||||
# Test Laboratory Integration
|
||||
from laboratory.models import LabOrder
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
|
||||
# Get a lab order
|
||||
order = LabOrder.objects.first()
|
||||
|
||||
# Test helper methods
|
||||
print(f"Requires approval: {order.requires_approval()}")
|
||||
print(f"Has valid approval: {order.has_valid_approval()}")
|
||||
print(f"Approval status: {order.approval_status}")
|
||||
|
||||
# Test creating an approval
|
||||
if order.patient.insurance_info.exists():
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=order.tenant,
|
||||
patient=order.patient,
|
||||
insurance_info=order.patient.insurance_info.first(),
|
||||
request_type='LABORATORY',
|
||||
content_object=order,
|
||||
service_description='Test lab order',
|
||||
requesting_provider=order.ordering_provider,
|
||||
service_start_date=order.created_at.date(),
|
||||
status='PENDING_SUBMISSION',
|
||||
)
|
||||
print(f"Created approval: {approval.approval_number}")
|
||||
|
||||
# Verify relationship
|
||||
print(f"Order approvals: {order.approval_requests.count()}")
|
||||
print(f"Approval content object: {approval.content_object}")
|
||||
```
|
||||
|
||||
### Test Each Module
|
||||
|
||||
Repeat the above test for:
|
||||
- ✅ Radiology (ImagingOrder)
|
||||
- ✅ Pharmacy (Prescription)
|
||||
- ✅ Operating Theatre (SurgicalCase)
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Update Application Code
|
||||
|
||||
### Add Approval Checks to Views
|
||||
|
||||
#### Example: Laboratory Order Creation
|
||||
|
||||
```python
|
||||
# laboratory/views.py
|
||||
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import redirect
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
|
||||
class LabOrderCreateView(LoginRequiredMixin, CreateView):
|
||||
model = LabOrder
|
||||
form_class = LabOrderForm
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
order = self.object
|
||||
|
||||
# Check if approval is required
|
||||
if order.requires_approval():
|
||||
messages.warning(
|
||||
self.request,
|
||||
f"Insurance approval required for order {order.order_number}. "
|
||||
f"Please submit an approval request."
|
||||
)
|
||||
# Optionally redirect to approval creation
|
||||
# return redirect('insurance_approvals:create', order_id=order.id)
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
### Add Approval Status to Templates
|
||||
|
||||
#### Example: Order List Template
|
||||
|
||||
```django
|
||||
<!-- laboratory/templates/laboratory/order_list.html -->
|
||||
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td>{{ order.order_number }}</td>
|
||||
<td>{{ order.patient.get_full_name }}</td>
|
||||
<td>
|
||||
{% if order.approval_status == 'APPROVED' %}
|
||||
<span class="badge bg-success">
|
||||
<i class="fa fa-check"></i> Approved
|
||||
</span>
|
||||
{% elif order.approval_status == 'APPROVAL_REQUIRED' %}
|
||||
<span class="badge bg-warning">
|
||||
<i class="fa fa-exclamation-triangle"></i> Approval Required
|
||||
</span>
|
||||
<a href="{% url 'insurance_approvals:create' %}?order_type=LAB&order_id={{ order.id }}"
|
||||
class="btn btn-sm btn-primary ms-2">
|
||||
Request Approval
|
||||
</a>
|
||||
{% elif order.approval_status == 'NO_INSURANCE' %}
|
||||
<span class="badge bg-secondary">No Insurance</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info">{{ order.approval_status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Generate Sample Data (Optional)
|
||||
|
||||
### Run Data Generation Script
|
||||
|
||||
```bash
|
||||
# Generate sample approval data
|
||||
python insurance_approvals_data.py
|
||||
|
||||
# Expected output:
|
||||
# 🏥 Creating Insurance Approval Data
|
||||
# 📋 Found X tenants
|
||||
# 👥 Found X patients
|
||||
#
|
||||
# 1️⃣ Creating Approval Templates...
|
||||
# Successfully created X approval templates.
|
||||
#
|
||||
# 2️⃣ Creating Approval Requests...
|
||||
# Successfully created X approval requests.
|
||||
#
|
||||
# 3️⃣ Creating Status History...
|
||||
# Successfully created X status history records.
|
||||
#
|
||||
# 4️⃣ Creating Communication Logs...
|
||||
# Successfully created X communication logs.
|
||||
#
|
||||
# 🎉 Insurance Approval Data Creation Complete!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Configure URLs (If Not Already Done)
|
||||
|
||||
### Add Insurance Approvals URLs
|
||||
|
||||
```python
|
||||
# hospital_management/urls.py
|
||||
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
# ... existing patterns ...
|
||||
path('insurance-approvals/', include('insurance_approvals.urls')),
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 9: Test End-to-End Workflow
|
||||
|
||||
### Complete Workflow Test
|
||||
|
||||
1. **Create an Order**
|
||||
```python
|
||||
# Create a lab order for a patient with insurance
|
||||
order = LabOrder.objects.create(
|
||||
tenant=tenant,
|
||||
patient=patient_with_insurance,
|
||||
ordering_provider=provider,
|
||||
# ... other fields
|
||||
)
|
||||
```
|
||||
|
||||
2. **Check Approval Requirement**
|
||||
```python
|
||||
if order.requires_approval():
|
||||
print("✓ Approval requirement detected")
|
||||
```
|
||||
|
||||
3. **Create Approval Request**
|
||||
```python
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=order.tenant,
|
||||
patient=order.patient,
|
||||
insurance_info=order.patient.insurance_info.first(),
|
||||
request_type='LABORATORY',
|
||||
content_object=order,
|
||||
# ... other fields
|
||||
)
|
||||
```
|
||||
|
||||
4. **Update Approval Status**
|
||||
```python
|
||||
approval.status = 'APPROVED'
|
||||
approval.authorization_number = 'AUTH123456'
|
||||
approval.expiration_date = timezone.now().date() + timedelta(days=90)
|
||||
approval.save()
|
||||
```
|
||||
|
||||
5. **Verify Approval**
|
||||
```python
|
||||
assert order.has_valid_approval()
|
||||
assert order.approval_status == 'APPROVED'
|
||||
print("✓ Approval workflow complete")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Issue 1: Migration Conflicts
|
||||
|
||||
**Problem:** Migration conflicts with existing migrations
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Reset migrations (development only!)
|
||||
python manage.py migrate laboratory zero
|
||||
python manage.py migrate radiology zero
|
||||
python manage.py migrate pharmacy zero
|
||||
python manage.py migrate operating_theatre zero
|
||||
|
||||
# Delete migration files
|
||||
rm laboratory/migrations/0XXX_*.py
|
||||
rm radiology/migrations/0XXX_*.py
|
||||
rm pharmacy/migrations/0XXX_*.py
|
||||
rm operating_theatre/migrations/0XXX_*.py
|
||||
|
||||
# Recreate migrations
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
#### Issue 2: ContentType Not Found
|
||||
|
||||
**Problem:** `ContentType matching query does not exist`
|
||||
|
||||
**Solution:**
|
||||
```python
|
||||
# Run in Django shell
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from laboratory.models import LabOrder
|
||||
|
||||
# Ensure ContentType exists
|
||||
ct = ContentType.objects.get_for_model(LabOrder)
|
||||
print(f"ContentType created: {ct}")
|
||||
```
|
||||
|
||||
#### Issue 3: Circular Import
|
||||
|
||||
**Problem:** Circular import when accessing approval_requests
|
||||
|
||||
**Solution:**
|
||||
```python
|
||||
# In helper methods, import inside the function
|
||||
def has_valid_approval(self):
|
||||
from django.utils import timezone # Import inside method
|
||||
return self.approval_requests.filter(
|
||||
status__in=['APPROVED', 'PARTIALLY_APPROVED'],
|
||||
expiration_date__gte=timezone.now().date()
|
||||
).exists()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
### If You Need to Rollback
|
||||
|
||||
```bash
|
||||
# 1. Rollback migrations
|
||||
python manage.py migrate laboratory 0XXX # Previous migration number
|
||||
python manage.py migrate radiology 0XXX
|
||||
python manage.py migrate pharmacy 0XXX
|
||||
python manage.py migrate operating_theatre 0XXX
|
||||
|
||||
# 2. Restore database backup
|
||||
# For SQLite
|
||||
cp db.sqlite3.backup db.sqlite3
|
||||
|
||||
# For PostgreSQL
|
||||
psql -U username -d database_name < backup_file.sql
|
||||
|
||||
# For MySQL
|
||||
mysql -u username -p database_name < backup_file.sql
|
||||
|
||||
# 3. Remove migration files
|
||||
rm laboratory/migrations/0XXX_add_approval_integration.py
|
||||
rm radiology/migrations/0XXX_add_approval_integration.py
|
||||
rm pharmacy/migrations/0XXX_add_approval_integration.py
|
||||
rm operating_theatre/migrations/0XXX_add_approval_integration.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Post-Migration Checklist
|
||||
|
||||
- [ ] All migrations applied successfully
|
||||
- [ ] Helper methods work correctly
|
||||
- [ ] Approval requests can be created
|
||||
- [ ] Approval status displays correctly
|
||||
- [ ] No errors in application logs
|
||||
- [ ] Templates updated with approval status
|
||||
- [ ] Views updated with approval checks
|
||||
- [ ] Sample data generated (if needed)
|
||||
- [ ] End-to-end workflow tested
|
||||
- [ ] Documentation reviewed
|
||||
- [ ] Team trained on new features
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Database Indexes
|
||||
|
||||
The integration uses GenericForeignKey which queries ContentType. Ensure these indexes exist:
|
||||
|
||||
```sql
|
||||
-- These should already exist from insurance_approvals migrations
|
||||
CREATE INDEX idx_approval_content_type ON insurance_approvals_insuranceapprovalrequest(content_type_id);
|
||||
CREATE INDEX idx_approval_object_id ON insurance_approvals_insuranceapprovalrequest(object_id);
|
||||
CREATE INDEX idx_approval_status ON insurance_approvals_insuranceapprovalrequest(status);
|
||||
CREATE INDEX idx_approval_expiration ON insurance_approvals_insuranceapprovalrequest(expiration_date);
|
||||
```
|
||||
|
||||
### Query Optimization
|
||||
|
||||
```python
|
||||
# Use select_related for better performance
|
||||
orders = LabOrder.objects.select_related(
|
||||
'patient',
|
||||
'patient__insurance_info'
|
||||
).prefetch_related(
|
||||
'approval_requests'
|
||||
)
|
||||
|
||||
# Check approval status efficiently
|
||||
for order in orders:
|
||||
if order.requires_approval():
|
||||
# Process...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Review `INSURANCE_APPROVAL_INTEGRATION.md`
|
||||
2. Check this migration guide
|
||||
3. Review the insurance_approvals README.md
|
||||
4. Check Django logs for errors
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
✅ **Migration Complete When:**
|
||||
- All migrations applied
|
||||
- Helper methods working
|
||||
- Approval workflow tested
|
||||
- Templates updated
|
||||
- No errors in logs
|
||||
|
||||
🎉 **You're ready to use the insurance approval integration!**
|
||||
460
INSURANCE_APPROVAL_QUICK_REFERENCE.md
Normal file
460
INSURANCE_APPROVAL_QUICK_REFERENCE.md
Normal file
@ -0,0 +1,460 @@
|
||||
# Insurance Approval Integration - Quick Reference Card
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Check if Order Needs Approval
|
||||
```python
|
||||
if order.requires_approval():
|
||||
# Create approval request
|
||||
```
|
||||
|
||||
### Get Approval Status
|
||||
```python
|
||||
status = order.approval_status
|
||||
# Returns: 'APPROVED', 'APPROVAL_REQUIRED', 'NO_INSURANCE', etc.
|
||||
```
|
||||
|
||||
### Check if Has Valid Approval
|
||||
```python
|
||||
if order.has_valid_approval():
|
||||
# Proceed with order
|
||||
```
|
||||
|
||||
### Get Active Approval
|
||||
```python
|
||||
approval = order.get_active_approval()
|
||||
if approval:
|
||||
print(f"Auth #: {approval.authorization_number}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Integrated Models
|
||||
|
||||
| Module | Model | Request Type |
|
||||
|--------|-------|--------------|
|
||||
| Laboratory | `LabOrder` | `LABORATORY` |
|
||||
| Radiology | `ImagingOrder` | `RADIOLOGY` |
|
||||
| Pharmacy | `Prescription` | `PHARMACY` |
|
||||
| Operating Theatre | `SurgicalCase` | `SURGERY` |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Helper Methods (All Models)
|
||||
|
||||
### `requires_approval()` → bool
|
||||
Returns `True` if order requires insurance approval
|
||||
|
||||
```python
|
||||
if lab_order.requires_approval():
|
||||
# Patient has insurance but no valid approval
|
||||
```
|
||||
|
||||
### `has_valid_approval()` → bool
|
||||
Returns `True` if order has a valid, non-expired approval
|
||||
|
||||
```python
|
||||
if imaging_order.has_valid_approval():
|
||||
# Can proceed with imaging
|
||||
```
|
||||
|
||||
### `get_active_approval()` → InsuranceApprovalRequest | None
|
||||
Returns the active approval or None
|
||||
|
||||
```python
|
||||
approval = prescription.get_active_approval()
|
||||
if approval:
|
||||
auth_number = approval.authorization_number
|
||||
```
|
||||
|
||||
### `approval_status` → str (property)
|
||||
Returns current approval status as string
|
||||
|
||||
```python
|
||||
status = surgical_case.approval_status
|
||||
# 'APPROVED', 'APPROVAL_REQUIRED', 'NO_INSURANCE', etc.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Creating Approval Requests
|
||||
|
||||
### Laboratory Order
|
||||
```python
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=lab_order.tenant,
|
||||
patient=lab_order.patient,
|
||||
insurance_info=lab_order.patient.insurance_info.first(),
|
||||
request_type='LABORATORY',
|
||||
content_object=lab_order,
|
||||
service_description=f"Lab tests: {', '.join([t.test_name for t in lab_order.tests.all()])}",
|
||||
procedure_codes=', '.join([t.cpt_code for t in lab_order.tests.all() if t.cpt_code]),
|
||||
diagnosis_codes=lab_order.diagnosis_code or '',
|
||||
clinical_justification=lab_order.clinical_indication,
|
||||
requesting_provider=lab_order.ordering_provider,
|
||||
service_start_date=lab_order.collection_datetime.date(),
|
||||
priority='ROUTINE',
|
||||
status='PENDING_SUBMISSION',
|
||||
)
|
||||
```
|
||||
|
||||
### Radiology Order
|
||||
```python
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=imaging_order.tenant,
|
||||
patient=imaging_order.patient,
|
||||
insurance_info=imaging_order.patient.insurance_info.first(),
|
||||
request_type='RADIOLOGY',
|
||||
content_object=imaging_order,
|
||||
service_description=imaging_order.study_description,
|
||||
procedure_codes=imaging_order.modality,
|
||||
diagnosis_codes=imaging_order.diagnosis_code or '',
|
||||
clinical_justification=imaging_order.clinical_indication,
|
||||
requesting_provider=imaging_order.ordering_provider,
|
||||
service_start_date=imaging_order.requested_datetime.date(),
|
||||
priority='URGENT' if imaging_order.is_stat else 'ROUTINE',
|
||||
status='PENDING_SUBMISSION',
|
||||
)
|
||||
```
|
||||
|
||||
### Pharmacy Prescription
|
||||
```python
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=prescription.tenant,
|
||||
patient=prescription.patient,
|
||||
insurance_info=prescription.patient.insurance_info.first(),
|
||||
request_type='PHARMACY',
|
||||
content_object=prescription,
|
||||
service_description=f"{prescription.medication.display_name} - {prescription.quantity_prescribed} {prescription.quantity_unit}",
|
||||
procedure_codes=prescription.medication.ndc_number or '',
|
||||
diagnosis_codes=prescription.diagnosis_code or '',
|
||||
clinical_justification=prescription.indication or 'As prescribed',
|
||||
requesting_provider=prescription.prescriber,
|
||||
service_start_date=prescription.date_written,
|
||||
priority='URGENT' if prescription.medication.is_controlled_substance else 'ROUTINE',
|
||||
requested_quantity=prescription.quantity_prescribed,
|
||||
status='PENDING_SUBMISSION',
|
||||
)
|
||||
```
|
||||
|
||||
### Surgical Case
|
||||
```python
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=surgical_case.tenant,
|
||||
patient=surgical_case.patient,
|
||||
insurance_info=surgical_case.patient.insurance_info.first(),
|
||||
request_type='SURGERY',
|
||||
content_object=surgical_case,
|
||||
service_description=surgical_case.primary_procedure,
|
||||
procedure_codes=', '.join(surgical_case.procedure_codes),
|
||||
diagnosis_codes=', '.join(surgical_case.diagnosis_codes),
|
||||
clinical_justification=surgical_case.clinical_notes,
|
||||
requesting_provider=surgical_case.primary_surgeon,
|
||||
service_start_date=surgical_case.scheduled_start.date(),
|
||||
priority='STAT' if surgical_case.is_emergency else 'ROUTINE',
|
||||
is_expedited=surgical_case.is_emergency,
|
||||
status='PENDING_SUBMISSION',
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Template Examples
|
||||
|
||||
### Display Approval Status Badge
|
||||
```django
|
||||
{% if order.approval_status == 'APPROVED' %}
|
||||
<span class="badge bg-success">
|
||||
<i class="fa fa-check"></i> Approved
|
||||
</span>
|
||||
{% elif order.approval_status == 'APPROVAL_REQUIRED' %}
|
||||
<span class="badge bg-warning">
|
||||
<i class="fa fa-exclamation-triangle"></i> Approval Required
|
||||
</span>
|
||||
{% elif order.approval_status == 'NO_INSURANCE' %}
|
||||
<span class="badge bg-secondary">No Insurance</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info">{{ order.approval_status }}</span>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Show Approval Details
|
||||
```django
|
||||
{% if order.has_valid_approval %}
|
||||
{% with approval=order.get_active_approval %}
|
||||
<div class="alert alert-success">
|
||||
<strong>Authorization:</strong> {{ approval.authorization_number }}<br>
|
||||
<strong>Expires:</strong> {{ approval.expiration_date|date:"M d, Y" }}<br>
|
||||
<strong>Approved Quantity:</strong> {{ approval.approved_quantity }}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Request Approval Button
|
||||
```django
|
||||
{% if order.requires_approval %}
|
||||
<a href="{% url 'insurance_approvals:create' %}?order_id={{ order.id }}"
|
||||
class="btn btn-primary">
|
||||
<i class="fa fa-file-medical"></i> Request Approval
|
||||
</a>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Approval Status Values
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| `NO_INSURANCE` | Patient has no insurance |
|
||||
| `APPROVAL_REQUIRED` | Needs approval but none exists |
|
||||
| `APPROVED` | Has valid, active approval |
|
||||
| `PENDING_SUBMISSION` | Request created but not submitted |
|
||||
| `SUBMITTED` | Request submitted to insurance |
|
||||
| `UNDER_REVIEW` | Being reviewed by insurance |
|
||||
| `MORE_INFO_REQUIRED` | Insurance needs more information |
|
||||
| `PARTIALLY_APPROVED` | Partially approved |
|
||||
| `DENIED` | Approval denied |
|
||||
| `EXPIRED` | Approval has expired |
|
||||
| `PRIOR_AUTH_REQUIRED` | Prior authorization required (Pharmacy) |
|
||||
| `PRIOR_AUTH_APPROVED` | Prior authorization approved (Pharmacy) |
|
||||
| `PRIOR_AUTH_EXPIRED` | Prior authorization expired (Pharmacy) |
|
||||
| `EMERGENCY_APPROVAL_REQUIRED` | Emergency approval needed (Surgery) |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Querying Approvals
|
||||
|
||||
### Get All Approvals for an Order
|
||||
```python
|
||||
approvals = order.approval_requests.all()
|
||||
```
|
||||
|
||||
### Get Active Approvals
|
||||
```python
|
||||
from django.utils import timezone
|
||||
|
||||
active_approvals = order.approval_requests.filter(
|
||||
status__in=['APPROVED', 'PARTIALLY_APPROVED'],
|
||||
expiration_date__gte=timezone.now().date()
|
||||
)
|
||||
```
|
||||
|
||||
### Get Latest Approval
|
||||
```python
|
||||
latest = order.approval_requests.order_by('-created_at').first()
|
||||
```
|
||||
|
||||
### Get Approvals by Status
|
||||
```python
|
||||
pending = order.approval_requests.filter(status='PENDING_SUBMISSION')
|
||||
approved = order.approval_requests.filter(status='APPROVED')
|
||||
denied = order.approval_requests.filter(status='DENIED')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Reverse Queries
|
||||
|
||||
### Get Order from Approval
|
||||
```python
|
||||
approval = InsuranceApprovalRequest.objects.get(pk=1)
|
||||
order = approval.content_object # Returns LabOrder, ImagingOrder, etc.
|
||||
```
|
||||
|
||||
### Get All Lab Orders with Approvals
|
||||
```python
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from laboratory.models import LabOrder
|
||||
|
||||
lab_order_ct = ContentType.objects.get_for_model(LabOrder)
|
||||
approvals = InsuranceApprovalRequest.objects.filter(
|
||||
content_type=lab_order_ct
|
||||
)
|
||||
```
|
||||
|
||||
### Filter Orders by Approval Status
|
||||
```python
|
||||
# Orders needing approval
|
||||
orders_needing_approval = [
|
||||
order for order in LabOrder.objects.all()
|
||||
if order.requires_approval()
|
||||
]
|
||||
|
||||
# Orders with valid approval
|
||||
orders_with_approval = [
|
||||
order for order in LabOrder.objects.all()
|
||||
if order.has_valid_approval()
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Performance Tips
|
||||
|
||||
### Use select_related and prefetch_related
|
||||
```python
|
||||
orders = LabOrder.objects.select_related(
|
||||
'patient',
|
||||
'patient__insurance_info'
|
||||
).prefetch_related(
|
||||
'approval_requests'
|
||||
)
|
||||
```
|
||||
|
||||
### Bulk Check Approval Status
|
||||
```python
|
||||
from django.db.models import Prefetch
|
||||
|
||||
orders = LabOrder.objects.prefetch_related(
|
||||
Prefetch(
|
||||
'approval_requests',
|
||||
queryset=InsuranceApprovalRequest.objects.filter(
|
||||
status__in=['APPROVED', 'PARTIALLY_APPROVED']
|
||||
)
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Common Patterns
|
||||
|
||||
### Pattern 1: Create Order with Approval Check
|
||||
```python
|
||||
def create_order_with_approval(patient, **order_data):
|
||||
order = LabOrder.objects.create(patient=patient, **order_data)
|
||||
|
||||
if order.requires_approval():
|
||||
return {
|
||||
'order': order,
|
||||
'needs_approval': True,
|
||||
'redirect': f'/insurance-approvals/create/?order_id={order.id}'
|
||||
}
|
||||
|
||||
return {'order': order, 'needs_approval': False}
|
||||
```
|
||||
|
||||
### Pattern 2: Validate Before Processing
|
||||
```python
|
||||
def process_order(order):
|
||||
if order.requires_approval() and not order.has_valid_approval():
|
||||
raise ValidationError("Order requires valid insurance approval")
|
||||
|
||||
# Process order...
|
||||
```
|
||||
|
||||
### Pattern 3: Auto-Create Approval Request
|
||||
```python
|
||||
def auto_request_approval(order):
|
||||
if not order.requires_approval():
|
||||
return None
|
||||
|
||||
return InsuranceApprovalRequest.objects.create(
|
||||
tenant=order.tenant,
|
||||
patient=order.patient,
|
||||
insurance_info=order.patient.insurance_info.first(),
|
||||
request_type='LABORATORY',
|
||||
content_object=order,
|
||||
# ... other fields
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Error Handling
|
||||
|
||||
### Check for Insurance
|
||||
```python
|
||||
if not order.patient.insurance_info.exists():
|
||||
# No insurance - no approval needed
|
||||
pass
|
||||
```
|
||||
|
||||
### Handle Missing Approval
|
||||
```python
|
||||
try:
|
||||
approval = order.get_active_approval()
|
||||
if not approval:
|
||||
# Create new approval request
|
||||
approval = create_approval_request(order)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting approval: {e}")
|
||||
```
|
||||
|
||||
### Validate Expiration
|
||||
```python
|
||||
from django.utils import timezone
|
||||
|
||||
approval = order.get_active_approval()
|
||||
if approval and approval.expiration_date < timezone.now().date():
|
||||
# Approval expired - need new one
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 API/DRF Integration
|
||||
|
||||
### Serializer with Approval Status
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
|
||||
class LabOrderSerializer(serializers.ModelSerializer):
|
||||
approval_status = serializers.CharField(read_only=True)
|
||||
requires_approval = serializers.SerializerMethodField()
|
||||
active_approval = serializers.SerializerMethodField()
|
||||
|
||||
def get_requires_approval(self, obj):
|
||||
return obj.requires_approval()
|
||||
|
||||
def get_active_approval(self, obj):
|
||||
approval = obj.get_active_approval()
|
||||
return {
|
||||
'id': approval.id,
|
||||
'authorization_number': approval.authorization_number,
|
||||
'expiration_date': approval.expiration_date
|
||||
} if approval else None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **Full Integration Guide:** `INSURANCE_APPROVAL_INTEGRATION.md`
|
||||
- **Migration Guide:** `INSURANCE_APPROVAL_MIGRATION_GUIDE.md`
|
||||
- **Module README:** `insurance_approvals/README.md`
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips
|
||||
|
||||
1. **Always check `requires_approval()` before processing orders**
|
||||
2. **Use `has_valid_approval()` to verify approval is still valid**
|
||||
3. **Prefetch approval_requests for better performance**
|
||||
4. **Handle emergency cases differently (Surgery module)**
|
||||
5. **Check formulary status for medications (Pharmacy module)**
|
||||
6. **Use approval templates for consistency**
|
||||
7. **Track all status changes with ApprovalStatusHistory**
|
||||
8. **Log communications with insurance companies**
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Quick Checklist
|
||||
|
||||
- [ ] Order created
|
||||
- [ ] Check if approval required
|
||||
- [ ] Create approval request if needed
|
||||
- [ ] Submit to insurance
|
||||
- [ ] Track status changes
|
||||
- [ ] Update when approved
|
||||
- [ ] Verify before processing
|
||||
- [ ] Handle expiration
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-01-03
|
||||
**Version:** 1.0
|
||||
Binary file not shown.
@ -1,472 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -1,116 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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"
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@ -1,623 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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",
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,309 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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"
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,526 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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"
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,110 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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"),
|
||||
("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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,328 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("billing", "0002_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="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"),
|
||||
),
|
||||
]
|
||||
@ -1,54 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-28 13:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("billing", "0003_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="billlineitem",
|
||||
name="place_of_service",
|
||||
field=models.IntegerField(
|
||||
choices=[
|
||||
(11, "Office"),
|
||||
(12, "Home"),
|
||||
(21, "Inpatient Hospital"),
|
||||
(22, "Outpatient Hospital"),
|
||||
(23, "Emergency Room"),
|
||||
(24, "Ambulatory Surgical Center"),
|
||||
(25, "Birthing Center"),
|
||||
(26, "Military Treatment Facility"),
|
||||
(31, "Skilled Nursing Facility"),
|
||||
(32, "Nursing Facility"),
|
||||
(33, "Custodial Care Facility"),
|
||||
(34, "Hospice"),
|
||||
(41, "Ambulance - Land"),
|
||||
(42, "Ambulance - Air or Water"),
|
||||
(49, "Independent Clinic"),
|
||||
(50, "Federally Qualified Health Center"),
|
||||
(51, "Inpatient Psychiatric Facility"),
|
||||
(52, "Psychiatric Facility-Partial Hospitalization"),
|
||||
(53, "Community Mental Health Center"),
|
||||
(54, "Intermediate Care Facility/Mentally Retarded"),
|
||||
(55, "Residential Substance Abuse Treatment Facility"),
|
||||
(56, "Psychiatric Residential Treatment Center"),
|
||||
(57, "Non-residential Substance Abuse Treatment Facility"),
|
||||
(60, "Mass Immunization Center"),
|
||||
(61, "Comprehensive Inpatient Rehabilitation Facility"),
|
||||
(62, "Comprehensive Outpatient Rehabilitation Facility"),
|
||||
(65, "End-Stage Renal Disease Treatment Facility"),
|
||||
(71, "Public Health Clinic"),
|
||||
(72, "Rural Health Clinic"),
|
||||
(81, "Independent Laboratory"),
|
||||
(99, "Other Place of Service"),
|
||||
],
|
||||
default=22,
|
||||
help_text="Place of service code",
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,673 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,297 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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")},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@ -1,968 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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",
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,375 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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.
@ -1,921 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
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",
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
213
create_insurance_tables.py
Normal file
213
create_insurance_tables.py
Normal file
@ -0,0 +1,213 @@
|
||||
"""
|
||||
Directly create insurance_approvals tables using SQL.
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
db_path = 'db.sqlite3'
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("Creating insurance_approvals tables directly...")
|
||||
|
||||
# Disable foreign key checks
|
||||
cursor.execute("PRAGMA foreign_keys = OFF;")
|
||||
|
||||
# Create tables from migration SQL
|
||||
tables_sql = [
|
||||
# InsuranceApprovalRequest table
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "insurance_approvals_request" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"approval_id" char(32) NOT NULL UNIQUE,
|
||||
"approval_number" varchar(30) NOT NULL UNIQUE,
|
||||
"request_type" varchar(20) NOT NULL,
|
||||
"service_description" varchar(500) NOT NULL,
|
||||
"procedure_codes" text NOT NULL,
|
||||
"diagnosis_codes" text NOT NULL,
|
||||
"clinical_justification" text NOT NULL,
|
||||
"medical_necessity" text NULL,
|
||||
"alternative_treatments_tried" text NULL,
|
||||
"requested_quantity" integer unsigned NOT NULL CHECK ("requested_quantity" >= 0),
|
||||
"requested_visits" integer unsigned NULL CHECK ("requested_visits" >= 0),
|
||||
"requested_units" integer unsigned NULL CHECK ("requested_units" >= 0),
|
||||
"service_start_date" date NOT NULL,
|
||||
"service_end_date" date NULL,
|
||||
"status" varchar(30) NOT NULL,
|
||||
"priority" varchar(20) NOT NULL,
|
||||
"submission_method" varchar(20) NULL,
|
||||
"submitted_date" datetime NULL,
|
||||
"decision_date" datetime NULL,
|
||||
"authorization_number" varchar(100) NULL,
|
||||
"reference_number" varchar(100) NULL,
|
||||
"approved_quantity" integer unsigned NULL CHECK ("approved_quantity" >= 0),
|
||||
"approved_visits" integer unsigned NULL CHECK ("approved_visits" >= 0),
|
||||
"approved_units" integer unsigned NULL CHECK ("approved_units" >= 0),
|
||||
"approved_amount" decimal NULL,
|
||||
"effective_date" date NULL,
|
||||
"expiration_date" date NULL,
|
||||
"denial_reason" text NULL,
|
||||
"denial_code" varchar(50) NULL,
|
||||
"appeal_date" datetime NULL,
|
||||
"appeal_reason" text NULL,
|
||||
"appeal_deadline" date NULL,
|
||||
"last_contact_date" datetime NULL,
|
||||
"last_contact_method" varchar(20) NULL,
|
||||
"last_contact_notes" text NULL,
|
||||
"is_urgent" bool NOT NULL,
|
||||
"is_expedited" bool NOT NULL,
|
||||
"requires_peer_review" bool NOT NULL,
|
||||
"internal_notes" text NULL,
|
||||
"insurance_notes" text NULL,
|
||||
"created_at" datetime NOT NULL,
|
||||
"updated_at" datetime NOT NULL,
|
||||
"tenant_id" bigint NOT NULL REFERENCES "core_tenant" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"patient_id" bigint NOT NULL REFERENCES "patients_patient_profile" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"insurance_info_id" bigint NOT NULL REFERENCES "patients_insurance_info" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"content_type_id" integer NOT NULL REFERENCES "django_content_type" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"object_id" integer unsigned NOT NULL CHECK ("object_id" >= 0),
|
||||
"submitted_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"assigned_to_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"requesting_provider_id" integer NOT NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"created_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
""",
|
||||
|
||||
# ApprovalDocument table
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "insurance_approvals_document" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"document_id" char(32) NOT NULL UNIQUE,
|
||||
"document_type" varchar(30) NOT NULL,
|
||||
"title" varchar(200) NOT NULL,
|
||||
"description" text NULL,
|
||||
"file" varchar(100) NOT NULL,
|
||||
"file_size" integer unsigned NOT NULL CHECK ("file_size" >= 0),
|
||||
"mime_type" varchar(100) NOT NULL,
|
||||
"uploaded_at" datetime NOT NULL,
|
||||
"approval_request_id" bigint NOT NULL REFERENCES "insurance_approvals_request" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"uploaded_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
""",
|
||||
|
||||
# ApprovalStatusHistory table
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "insurance_approvals_status_history" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"from_status" varchar(30) NULL,
|
||||
"to_status" varchar(30) NOT NULL,
|
||||
"reason" text NULL,
|
||||
"notes" text NULL,
|
||||
"changed_at" datetime NOT NULL,
|
||||
"approval_request_id" bigint NOT NULL REFERENCES "insurance_approvals_request" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"changed_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
""",
|
||||
|
||||
# ApprovalCommunicationLog table
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "insurance_approvals_communication_log" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"communication_id" char(32) NOT NULL UNIQUE,
|
||||
"communication_type" varchar(20) NOT NULL,
|
||||
"contact_person" varchar(200) NULL,
|
||||
"contact_number" varchar(50) NULL,
|
||||
"subject" varchar(200) NOT NULL,
|
||||
"message" text NOT NULL,
|
||||
"response" text NULL,
|
||||
"outcome" varchar(200) NULL,
|
||||
"follow_up_required" bool NOT NULL,
|
||||
"follow_up_date" date NULL,
|
||||
"communicated_at" datetime NOT NULL,
|
||||
"approval_request_id" bigint NOT NULL REFERENCES "insurance_approvals_request" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"communicated_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
""",
|
||||
|
||||
# ApprovalTemplate table
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "insurance_approvals_template" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"template_id" char(32) NOT NULL UNIQUE,
|
||||
"name" varchar(200) NOT NULL,
|
||||
"description" text NULL,
|
||||
"request_type" varchar(20) NOT NULL,
|
||||
"insurance_company" varchar(200) NULL,
|
||||
"clinical_justification_template" text NOT NULL,
|
||||
"medical_necessity_template" text NULL,
|
||||
"required_documents" text NOT NULL,
|
||||
"required_codes" text NOT NULL,
|
||||
"is_active" bool NOT NULL,
|
||||
"usage_count" integer unsigned NOT NULL CHECK ("usage_count" >= 0),
|
||||
"created_at" datetime NOT NULL,
|
||||
"updated_at" datetime NOT NULL,
|
||||
"tenant_id" bigint NOT NULL REFERENCES "core_tenant" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
"created_by_id" integer NULL REFERENCES "accounts_user" ("id") DEFERRABLE INITIALLY DEFERRED,
|
||||
CONSTRAINT "insurance_approvals_template_tenant_id_name_uniq" UNIQUE ("tenant_id", "name")
|
||||
);
|
||||
"""
|
||||
]
|
||||
|
||||
# Create tables
|
||||
for i, sql in enumerate(tables_sql, 1):
|
||||
try:
|
||||
cursor.execute(sql)
|
||||
print(f"✓ Created table {i}/5")
|
||||
except sqlite3.OperationalError as e:
|
||||
if "already exists" in str(e):
|
||||
print(f"✓ Table {i}/5 already exists")
|
||||
else:
|
||||
print(f"✗ Error creating table {i}/5: {e}")
|
||||
|
||||
# Create indexes
|
||||
indexes_sql = [
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_tenant__d6763e_idx" ON "insurance_approvals_request" ("tenant_id", "status");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_patient_00ddbd_idx" ON "insurance_approvals_request" ("patient_id", "status");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_insuran_438196_idx" ON "insurance_approvals_request" ("insurance_info_id", "status");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_approva_cefd6a_idx" ON "insurance_approvals_request" ("approval_number");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_authori_f05618_idx" ON "insurance_approvals_request" ("authorization_number");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_expirat_ef068e_idx" ON "insurance_approvals_request" ("expiration_date");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_assigne_19b007_idx" ON "insurance_approvals_request" ("assigned_to_id", "status");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_content_de7404_idx" ON "insurance_approvals_request" ("content_type_id", "object_id");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_priorit_0abcbd_idx" ON "insurance_approvals_request" ("priority", "status");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_approva_a5298b_idx" ON "insurance_approvals_status_history" ("approval_request_id", "changed_at");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_approva_eae9e0_idx" ON "insurance_approvals_document" ("approval_request_id", "document_type");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_approva_fa779d_idx" ON "insurance_approvals_communication_log" ("approval_request_id", "communicated_at");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_follow__2a3b0a_idx" ON "insurance_approvals_communication_log" ("follow_up_required", "follow_up_date");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_tenant__8f9c3a_idx" ON "insurance_approvals_template" ("tenant_id", "is_active");',
|
||||
'CREATE INDEX IF NOT EXISTS "insurance_a_request_4b3e2f_idx" ON "insurance_approvals_template" ("request_type");',
|
||||
]
|
||||
|
||||
print("\nCreating indexes...")
|
||||
for i, sql in enumerate(indexes_sql, 1):
|
||||
try:
|
||||
cursor.execute(sql)
|
||||
except sqlite3.OperationalError as e:
|
||||
print(f" Index {i}: {e}")
|
||||
|
||||
print(f"✓ Created {len(indexes_sql)} indexes")
|
||||
|
||||
# Mark migration as applied
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO django_migrations (app, name, applied)
|
||||
VALUES ('insurance_approvals', '0001_initial', datetime('now'))
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Re-enable foreign key checks
|
||||
cursor.execute("PRAGMA foreign_keys = ON;")
|
||||
|
||||
conn.close()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✓ Insurance Approvals tables created successfully!")
|
||||
print("="*60)
|
||||
print("\nTables created:")
|
||||
print(" 1. insurance_approvals_request")
|
||||
print(" 2. insurance_approvals_document")
|
||||
print(" 3. insurance_approvals_status_history")
|
||||
print(" 4. insurance_approvals_communication_log")
|
||||
print(" 5. insurance_approvals_template")
|
||||
print("\nModule is ready to use!")
|
||||
print("Access admin at: http://127.0.0.1:8000/admin/insurance_approvals/")
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,657 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("appointments", "0002_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="encounter",
|
||||
name="admission",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Related admission",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="encounters",
|
||||
to="inpatients.admission",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="encounter",
|
||||
name="appointment",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Related appointment",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="encounters",
|
||||
to="appointments.appointmentrequest",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="encounter",
|
||||
name="created_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="User who created the encounter",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="created_encounters",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="encounter",
|
||||
name="patient",
|
||||
field=models.ForeignKey(
|
||||
help_text="Patient for this encounter",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="encounters",
|
||||
to="patients.patientprofile",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="encounter",
|
||||
name="provider",
|
||||
field=models.ForeignKey(
|
||||
help_text="Primary provider for this encounter",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="encounters",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="encounter",
|
||||
name="signed_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Provider who signed off",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="signed_encounters",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="encounter",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="encounters",
|
||||
to="core.tenant",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="criticalalert",
|
||||
name="related_encounter",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Related encounter",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="critical_alerts",
|
||||
to="emr.encounter",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="clinicalrecommendation",
|
||||
name="related_encounter",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Related encounter",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="recommendations",
|
||||
to="emr.encounter",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="clinicalnote",
|
||||
name="encounter",
|
||||
field=models.ForeignKey(
|
||||
help_text="Associated encounter",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="clinical_notes",
|
||||
to="emr.encounter",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="icd10",
|
||||
name="parent",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="children",
|
||||
to="emr.icd10",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="notetemplate",
|
||||
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_note_templates",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="notetemplate",
|
||||
name="previous_version",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Previous version of this template",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="newer_versions",
|
||||
to="emr.notetemplate",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="notetemplate",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="note_templates",
|
||||
to="core.tenant",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="clinicalnote",
|
||||
name="template",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Template used for this note",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="notes",
|
||||
to="emr.notetemplate",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="problemlist",
|
||||
name="created_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="User who created the problem",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="created_problems",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="problemlist",
|
||||
name="diagnosing_provider",
|
||||
field=models.ForeignKey(
|
||||
help_text="Provider who diagnosed the problem",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="diagnosed_problems",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="problemlist",
|
||||
name="managing_provider",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Provider managing the problem",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="managed_problems",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="problemlist",
|
||||
name="patient",
|
||||
field=models.ForeignKey(
|
||||
help_text="Patient",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="problems",
|
||||
to="patients.patientprofile",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="problemlist",
|
||||
name="related_encounter",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Encounter where problem was identified",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="problems_identified",
|
||||
to="emr.encounter",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="problemlist",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="problem_lists",
|
||||
to="core.tenant",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="problemlist",
|
||||
name="verified_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Provider who verified the problem",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="verified_problems",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="clinicalrecommendation",
|
||||
name="related_problems",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Related problems",
|
||||
related_name="recommendations",
|
||||
to="emr.problemlist",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="clinicalnote",
|
||||
name="related_problems",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Related problems",
|
||||
related_name="related_clinical_notes",
|
||||
to="emr.problemlist",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="careplan",
|
||||
name="related_problems",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Related problems addressed by this plan",
|
||||
related_name="care_plans",
|
||||
to="emr.problemlist",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="treatmentprotocol",
|
||||
name="created_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="User who created the protocol",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="created_protocols",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="treatmentprotocol",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="treatment_protocols",
|
||||
to="core.tenant",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vitalsigns",
|
||||
name="encounter",
|
||||
field=models.ForeignKey(
|
||||
help_text="Associated encounter",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="vital_signs",
|
||||
to="emr.encounter",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vitalsigns",
|
||||
name="measured_by",
|
||||
field=models.ForeignKey(
|
||||
help_text="Staff member who took measurements",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="vital_signs_measurements",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vitalsigns",
|
||||
name="patient",
|
||||
field=models.ForeignKey(
|
||||
help_text="Patient",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="vital_signs",
|
||||
to="patients.patientprofile",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vitalsigns",
|
||||
name="verified_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Staff member who verified measurements",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="verified_vital_signs",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="allergyalert",
|
||||
index=models.Index(
|
||||
fields=["tenant", "resolved"], name="emr_allergy_tenant__da9219_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="allergyalert",
|
||||
index=models.Index(
|
||||
fields=["patient", "resolved"], name="emr_allergy_patient_674c53_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="allergyalert",
|
||||
index=models.Index(
|
||||
fields=["severity"], name="emr_allergy_severit_38d8dd_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="allergyalert",
|
||||
index=models.Index(
|
||||
fields=["detected_at"], name="emr_allergy_detecte_97c184_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalguideline",
|
||||
index=models.Index(
|
||||
fields=["tenant", "is_active"], name="emr_clinica_tenant__08a1f7_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalguideline",
|
||||
index=models.Index(
|
||||
fields=["organization"], name="emr_clinica_organiz_107f8d_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalguideline",
|
||||
index=models.Index(
|
||||
fields=["publication_date"], name="emr_clinica_publica_4e35d0_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="diagnosticsuggestion",
|
||||
index=models.Index(
|
||||
fields=["tenant", "status"], name="emr_diagnos_tenant__77e6f8_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="diagnosticsuggestion",
|
||||
index=models.Index(
|
||||
fields=["patient", "status"], name="emr_diagnos_patient_8c5470_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="diagnosticsuggestion",
|
||||
index=models.Index(
|
||||
fields=["confidence"], name="emr_diagnos_confide_f7447d_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="diagnosticsuggestion",
|
||||
index=models.Index(
|
||||
fields=["created_at"], name="emr_diagnos_created_34cc79_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="encounter",
|
||||
index=models.Index(
|
||||
fields=["tenant", "status"], name="emr_encount_tenant__6734f5_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="encounter",
|
||||
index=models.Index(
|
||||
fields=["patient", "start_datetime"],
|
||||
name="emr_encount_patient_c0f443_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="encounter",
|
||||
index=models.Index(
|
||||
fields=["provider"], name="emr_encount_provide_5e06b3_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="encounter",
|
||||
index=models.Index(
|
||||
fields=["encounter_type"], name="emr_encount_encount_019f80_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="encounter",
|
||||
index=models.Index(
|
||||
fields=["start_datetime"], name="emr_encount_start_d_a01018_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="criticalalert",
|
||||
index=models.Index(
|
||||
fields=["tenant", "acknowledged"], name="emr_critica_tenant__a7de09_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="criticalalert",
|
||||
index=models.Index(
|
||||
fields=["patient", "acknowledged"],
|
||||
name="emr_critica_patient_3f3d88_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="criticalalert",
|
||||
index=models.Index(
|
||||
fields=["priority"], name="emr_critica_priorit_06ad08_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="criticalalert",
|
||||
index=models.Index(
|
||||
fields=["created_at"], name="emr_critica_created_3acbe1_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="notetemplate",
|
||||
index=models.Index(
|
||||
fields=["tenant", "is_active"], name="emr_note_te_tenant__caa5d3_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="notetemplate",
|
||||
index=models.Index(
|
||||
fields=["note_type", "specialty"], name="emr_note_te_note_ty_d18594_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="notetemplate",
|
||||
index=models.Index(
|
||||
fields=["is_default"], name="emr_note_te_is_defa_7b5223_idx"
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="notetemplate",
|
||||
unique_together={("tenant", "note_type", "specialty", "is_default")},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="problemlist",
|
||||
index=models.Index(
|
||||
fields=["tenant", "status"], name="emr_problem_tenant__bb0abf_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="problemlist",
|
||||
index=models.Index(
|
||||
fields=["patient", "status"], name="emr_problem_patient_d732d2_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="problemlist",
|
||||
index=models.Index(
|
||||
fields=["problem_type"], name="emr_problem_problem_90c9f8_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="problemlist",
|
||||
index=models.Index(
|
||||
fields=["priority"], name="emr_problem_priorit_327dd3_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="problemlist",
|
||||
index=models.Index(
|
||||
fields=["onset_date"], name="emr_problem_onset_d_de94bd_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalrecommendation",
|
||||
index=models.Index(
|
||||
fields=["tenant", "status"], name="emr_clinica_tenant__9ac4a3_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalrecommendation",
|
||||
index=models.Index(
|
||||
fields=["patient", "status"], name="emr_clinica_patient_6a41b7_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalrecommendation",
|
||||
index=models.Index(
|
||||
fields=["category"], name="emr_clinica_categor_44bb6e_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalrecommendation",
|
||||
index=models.Index(
|
||||
fields=["priority"], name="emr_clinica_priorit_d52001_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalrecommendation",
|
||||
index=models.Index(
|
||||
fields=["created_at"], name="emr_clinica_created_d816a2_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalnote",
|
||||
index=models.Index(
|
||||
fields=["patient", "note_datetime"],
|
||||
name="emr_clinica_patient_442718_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalnote",
|
||||
index=models.Index(
|
||||
fields=["encounter"], name="emr_clinica_encount_751749_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalnote",
|
||||
index=models.Index(
|
||||
fields=["author"], name="emr_clinica_author__85ec13_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalnote",
|
||||
index=models.Index(
|
||||
fields=["note_type"], name="emr_clinica_note_ty_e6c13c_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="clinicalnote",
|
||||
index=models.Index(fields=["status"], name="emr_clinica_status_0ba513_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="careplan",
|
||||
index=models.Index(
|
||||
fields=["tenant", "status"], name="emr_care_pl_tenant__46659b_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="careplan",
|
||||
index=models.Index(
|
||||
fields=["patient", "status"], name="emr_care_pl_patient_a85a8d_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="careplan",
|
||||
index=models.Index(
|
||||
fields=["primary_provider"], name="emr_care_pl_primary_7b0b7d_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="careplan",
|
||||
index=models.Index(
|
||||
fields=["start_date", "end_date"], name="emr_care_pl_start_d_1183e0_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="careplan",
|
||||
index=models.Index(
|
||||
fields=["priority"], name="emr_care_pl_priorit_0a41d3_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="treatmentprotocol",
|
||||
index=models.Index(
|
||||
fields=["tenant", "is_active"], name="emr_treatme_tenant__1f3aaa_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="treatmentprotocol",
|
||||
index=models.Index(
|
||||
fields=["success_rate"], name="emr_treatme_success_d8024a_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="vitalsigns",
|
||||
index=models.Index(
|
||||
fields=["patient", "measured_datetime"],
|
||||
name="emr_vital_s_patient_0fc206_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="vitalsigns",
|
||||
index=models.Index(
|
||||
fields=["encounter"], name="emr_vital_s_encount_6b9829_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="vitalsigns",
|
||||
index=models.Index(
|
||||
fields=["measured_datetime"], name="emr_vital_s_measure_8badac_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,36 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-28 13:55
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0001_initial"),
|
||||
("emr", "0002_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="icd10",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
default=1,
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="icd10_codes",
|
||||
to="core.tenant",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="icd10",
|
||||
name="code",
|
||||
field=models.CharField(db_index=True, max_length=10),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="icd10",
|
||||
unique_together={("tenant", "code")},
|
||||
),
|
||||
]
|
||||
@ -1,33 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-29 13:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("emr", "0003_icd10_tenant_alter_icd10_code_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="encounter",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("PLANNED", "Planned"),
|
||||
("ARRIVED", "Arrived"),
|
||||
("TRIAGED", "Triaged"),
|
||||
("IN_PROGRESS", "In Progress"),
|
||||
("ON_HOLD", "On Hold"),
|
||||
("COMPLETED", "Completed"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
("ENTERED_IN_ERROR", "Entered in Error"),
|
||||
("UNKNOWN", "Unknown"),
|
||||
],
|
||||
default="PLANNED",
|
||||
help_text="Current encounter status",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -29,7 +29,7 @@
|
||||
<div class="row">
|
||||
<div class="col-xl-8">
|
||||
<!-- BEGIN panel -->
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel panel-inverse" data-sortable-id="encounter-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Encounter Information</h4>
|
||||
<div class="panel-heading-btn">
|
||||
@ -204,7 +204,7 @@
|
||||
<!-- END panel -->
|
||||
|
||||
<!-- BEGIN panel with tabs -->
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel panel-inverse" data-sortable-id="encounter-2">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Encounter Details</h4>
|
||||
<div class="panel-heading-btn">
|
||||
@ -247,6 +247,18 @@
|
||||
<span class="d-sm-block d-none"><i class="fas fa-x-ray me-1"></i> Radiology</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#problems-tab" data-bs-toggle="tab" class="nav-link">
|
||||
<span class="d-sm-none"><i class="fas fa-list"></i></span>
|
||||
<span class="d-sm-block d-none"><i class="fas fa-list me-1"></i> Problems</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#care-plans-tab" data-bs-toggle="tab" class="nav-link">
|
||||
<span class="d-sm-none"><i class="fas fa-clipboard-list"></i></span>
|
||||
<span class="d-sm-block d-none"><i class="fas fa-clipboard-list me-1"></i> Care Plans</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#billing-tab" data-bs-toggle="tab" class="nav-link">
|
||||
<span class="d-sm-none"><i class="fas fa-file-invoice-dollar"></i></span>
|
||||
@ -659,6 +671,164 @@
|
||||
</div>
|
||||
<!-- END radiology-tab -->
|
||||
|
||||
<!-- BEGIN problems-tab -->
|
||||
<div class="tab-pane fade" id="problems-tab">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="m-0">Problems Identified</h5>
|
||||
<button class="btn btn-sm btn-primary"
|
||||
hx-get="{% url 'emr:add_problem' object.id %}"
|
||||
hx-target="#problem-modal .modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#problem-modal">
|
||||
<i class="fa fa-plus me-1"></i> Add Problem
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if problems %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Problem</th>
|
||||
<th>Type</th>
|
||||
<th>Priority</th>
|
||||
<th>Status</th>
|
||||
<th>Onset Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for problem in problems %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ problem.problem_name }}</strong>
|
||||
{% if problem.problem_code %}
|
||||
<br><small class="text-muted">{{ problem.problem_code }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ problem.get_problem_type_display }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if problem.priority == 'HIGH' or problem.priority == 'URGENT' %}danger{% elif problem.priority == 'MEDIUM' %}warning{% else %}info{% endif %} fs-10px">
|
||||
{{ problem.get_priority_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if problem.status == 'ACTIVE' %}success{% elif problem.status == 'RESOLVED' %}secondary{% else %}info{% endif %} fs-10px">
|
||||
{{ problem.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if problem.onset_date %}
|
||||
{{ problem.onset_date|date:"M d, Y" }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not specified</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'emr:problem_detail' problem.pk %}" class="btn btn-xs btn-outline-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4 text-muted">
|
||||
<i class="fa fa-list fa-3x mb-3"></i>
|
||||
<p>No problems identified for this encounter.</p>
|
||||
<button class="btn btn-primary"
|
||||
hx-get="{% url 'emr:add_problem' object.id %}"
|
||||
hx-target="#problem-modal .modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#problem-modal">
|
||||
<i class="fa fa-plus me-2"></i>Add Problem
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- END problems-tab -->
|
||||
|
||||
<!-- BEGIN care-plans-tab -->
|
||||
<div class="tab-pane fade" id="care-plans-tab">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="m-0">Care Plans</h5>
|
||||
<button class="btn btn-sm btn-primary"
|
||||
hx-get="{% url 'emr:add_care_plan' object.id %}"
|
||||
hx-target="#care-plan-modal .modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#care-plan-modal">
|
||||
<i class="fa fa-plus me-1"></i> Add Care Plan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if object.patient.care_plans.exists %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
<th>Start Date</th>
|
||||
<th>Progress</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for care_plan in object.patient.care_plans.all %}
|
||||
<tr>
|
||||
<td><strong>{{ care_plan.title }}</strong></td>
|
||||
<td>{{ care_plan.get_plan_type_display }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if care_plan.status == 'ACTIVE' %}success{% elif care_plan.status == 'COMPLETED' %}primary{% elif care_plan.status == 'DRAFT' %}warning{% else %}secondary{% endif %} fs-10px">
|
||||
{{ care_plan.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if care_plan.priority == 'STAT' or care_plan.priority == 'URGENT' %}danger{% elif care_plan.priority == 'ROUTINE' %}info{% else %}secondary{% endif %} fs-10px">
|
||||
{{ care_plan.get_priority_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ care_plan.start_date|date:"M d, Y" }}</td>
|
||||
<td>
|
||||
<div class="progress" style="height: 20px;">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
style="width: {{ care_plan.completion_percentage }}%;"
|
||||
aria-valuenow="{{ care_plan.completion_percentage }}"
|
||||
aria-valuemin="0" aria-valuemax="100">
|
||||
{{ care_plan.completion_percentage }}%
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'emr:care_plan_detail' care_plan.pk %}" class="btn btn-xs btn-outline-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4 text-muted">
|
||||
<i class="fa fa-clipboard-list fa-3x mb-3"></i>
|
||||
<p>No care plans for this patient.</p>
|
||||
<button class="btn btn-primary"
|
||||
hx-get="{% url 'emr:add_care_plan' object.id %}"
|
||||
hx-target="#care-plan-modal .modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#care-plan-modal">
|
||||
<i class="fa fa-plus me-2"></i>Add Care Plan
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- END care-plans-tab -->
|
||||
|
||||
<!-- BEGIN billing-tab -->
|
||||
<div class="tab-pane fade" id="billing-tab">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
@ -725,7 +895,7 @@
|
||||
|
||||
<div class="col-xl-4">
|
||||
<!-- BEGIN panel -->
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel panel-inverse" data-sortable-id="encounter-3">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Quick Actions</h4>
|
||||
<div class="panel-heading-btn">
|
||||
@ -767,6 +937,22 @@
|
||||
<i class="fa fa-file-medical me-2"></i>Add Clinical Note
|
||||
</a>
|
||||
|
||||
<button class="btn btn-outline-success"
|
||||
hx-get="{% url 'emr:add_problem' object.id %}"
|
||||
hx-target="#problem-modal .modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#problem-modal">
|
||||
<i class="fa fa-plus me-2"></i>Add Problem
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-info"
|
||||
hx-get="{% url 'emr:add_care_plan' object.id %}"
|
||||
hx-target="#care-plan-modal .modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#care-plan-modal">
|
||||
<i class="fa fa-clipboard-list me-2"></i>Add Care Plan
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-info" onclick="printEncounter()">
|
||||
<i class="fa fa-print me-2"></i>Print Encounter
|
||||
</button>
|
||||
@ -776,7 +962,7 @@
|
||||
<!-- END panel -->
|
||||
|
||||
<!-- BEGIN panel -->
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel panel-inverse" data-sortable-id="encounter-4">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Patient Information</h4>
|
||||
<div class="panel-heading-btn">
|
||||
@ -830,7 +1016,7 @@
|
||||
<!-- END panel -->
|
||||
|
||||
<!-- BEGIN panel -->
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel panel-inverse" data-sortable-id="encounter-5">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Related Information</h4>
|
||||
<div class="panel-heading-btn">
|
||||
@ -878,7 +1064,7 @@
|
||||
<!-- END panel -->
|
||||
|
||||
<!-- BEGIN panel -->
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel panel-inverse" data-sortable-id="encounter-6">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Encounter Timeline</h4>
|
||||
<div class="panel-heading-btn">
|
||||
@ -938,6 +1124,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Problem Modal -->
|
||||
<div class="modal fade" id="problem-modal" tabindex="-1" aria-labelledby="problemModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<!-- Content loaded via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Care Plan Modal -->
|
||||
<div class="modal fade" id="care-plan-modal" tabindex="-1" aria-labelledby="carePlanModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<!-- Content loaded via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
@ -945,6 +1149,8 @@
|
||||
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/lity/dist/lity.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/moment/moment.js' %}"></script>
|
||||
<script src="{% static 'plugins/toastr/toastr.min.js' %}"
|
||||
<script>
|
||||
function updateStatus(newStatus) {
|
||||
if (confirm('Are you sure you want to update the encounter status?')) {
|
||||
|
||||
163
emr/templates/emr/partials/care_plan_form_modal.html
Normal file
163
emr/templates/emr/partials/care_plan_form_modal.html
Normal file
@ -0,0 +1,163 @@
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-clipboard-list me-2"></i>Add Care Plan</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'emr:add_care_plan' encounter.id %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong>Please correct the following errors:</strong>
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}
|
||||
<li>{{ field|title }}: {{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<strong>Patient:</strong> {{ patient.get_full_name }} (MRN: {{ patient.mrn }})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label for="{{ form.title.id_for_label }}" class="form-label">Care Plan Title <span class="text-danger">*</span></label>
|
||||
{{ form.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plan Type and Category -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.plan_type.id_for_label }}" class="form-label">Plan Type <span class="text-danger">*</span></label>
|
||||
{{ form.plan_type }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.category.id_for_label }}" class="form-label">Category <span class="text-danger">*</span></label>
|
||||
{{ form.category }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status and Priority -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.status.id_for_label }}" class="form-label">Status <span class="text-danger">*</span></label>
|
||||
{{ form.status }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.priority.id_for_label }}" class="form-label">Priority <span class="text-danger">*</span></label>
|
||||
{{ form.priority }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dates -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.start_date.id_for_label }}" class="form-label">Start Date <span class="text-danger">*</span></label>
|
||||
{{ form.start_date }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.end_date.id_for_label }}" class="form-label">End Date</label>
|
||||
{{ form.end_date }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.target_completion_date.id_for_label }}" class="form-label">Target Completion</label>
|
||||
{{ form.target_completion_date }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">Description <span class="text-danger">*</span></label>
|
||||
{{ form.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Goals -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label for="{{ form.goals.id_for_label }}" class="form-label">Goals</label>
|
||||
{{ form.goals }}
|
||||
<small class="form-text text-muted">Enter goals as JSON array, e.g., ["Goal 1", "Goal 2"]</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interventions -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label for="{{ form.interventions.id_for_label }}" class="form-label">Interventions</label>
|
||||
{{ form.interventions }}
|
||||
<small class="form-text text-muted">Enter interventions as JSON array</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monitoring Parameters -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label for="{{ form.monitoring_parameters.id_for_label }}" class="form-label">Monitoring Parameters</label>
|
||||
{{ form.monitoring_parameters }}
|
||||
<small class="form-text text-muted">Enter monitoring parameters as JSON array</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Patient Goals and Preferences -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.patient_goals.id_for_label }}" class="form-label">Patient Goals</label>
|
||||
{{ form.patient_goals }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.patient_preferences.id_for_label }}" class="form-label">Patient Preferences</label>
|
||||
{{ form.patient_preferences }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related Problems -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label for="{{ form.related_problems.id_for_label }}" class="form-label">Related Problems</label>
|
||||
{{ form.related_problems }}
|
||||
<small class="form-text text-muted">Hold Ctrl/Cmd to select multiple problems</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Fields -->
|
||||
{{ form.patient.as_hidden }}
|
||||
{{ form.primary_provider.as_hidden }}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>Create Care Plan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize JSON fields with empty arrays if they're empty
|
||||
const jsonFields = ['{{ form.goals.id_for_label }}', '{{ form.interventions.id_for_label }}', '{{ form.monitoring_parameters.id_for_label }}'];
|
||||
|
||||
jsonFields.forEach(fieldId => {
|
||||
const field = document.getElementById(fieldId);
|
||||
if (field && !field.value.trim()) {
|
||||
field.value = '[]';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
183
emr/templates/emr/partials/problem_form_modal.html
Normal file
183
emr/templates/emr/partials/problem_form_modal.html
Normal file
@ -0,0 +1,183 @@
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-plus-circle me-2"></i>Add Problem</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'emr:add_problem' encounter.id %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="modal-body">
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong>Please correct the following errors:</strong>
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}
|
||||
<li>{{ field|title }}: {{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<strong>Patient:</strong> {{ patient.get_full_name }} (MRN: {{ patient.mrn }})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ICD-10 Search -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label for="icd10-search" class="form-label">Search ICD-10 Diagnosis</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="icd10-search"
|
||||
placeholder="Type to search ICD-10 codes..."
|
||||
autocomplete="off">
|
||||
<div id="icd10-results" class="list-group mt-2" style="max-height: 200px; overflow-y: auto; display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Problem Name and Code -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-8">
|
||||
<label for="{{ form.problem_name.id_for_label }}" class="form-label">Problem Name <span class="text-danger">*</span></label>
|
||||
{{ form.problem_name }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.problem_code.id_for_label }}" class="form-label">ICD-10 Code</label>
|
||||
{{ form.problem_code }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Problem Type and Status -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.problem_type.id_for_label }}" class="form-label">Problem Type <span class="text-danger">*</span></label>
|
||||
{{ form.problem_type }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.status.id_for_label }}" class="form-label">Status <span class="text-danger">*</span></label>
|
||||
{{ form.status }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Severity and Priority -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.severity.id_for_label }}" class="form-label">Severity</label>
|
||||
{{ form.severity }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.priority.id_for_label }}" class="form-label">Priority <span class="text-danger">*</span></label>
|
||||
{{ form.priority }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Onset Date and Body Site -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.onset_date.id_for_label }}" class="form-label">Onset Date</label>
|
||||
{{ form.onset_date }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.body_site.id_for_label }}" class="form-label">Body Site</label>
|
||||
{{ form.body_site }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clinical Notes -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label for="{{ form.clinical_notes.id_for_label }}" class="form-label">Clinical Notes</label>
|
||||
{{ form.clinical_notes }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Fields -->
|
||||
{{ form.patient.as_hidden }}
|
||||
{{ form.diagnosing_provider.as_hidden }}
|
||||
{{ form.related_encounter.as_hidden }}
|
||||
{{ form.coding_system.as_hidden }}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>Add Problem
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('icd10-search');
|
||||
const resultsDiv = document.getElementById('icd10-results');
|
||||
const problemNameInput = document.getElementById('{{ form.problem_name.id_for_label }}');
|
||||
const problemCodeInput = document.getElementById('{{ form.problem_code.id_for_label }}');
|
||||
const codingSystemInput = document.getElementById('{{ form.coding_system.id_for_label }}');
|
||||
|
||||
let searchTimeout;
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
const query = this.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
resultsDiv.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(function() {
|
||||
fetch(`/emr/api/icd10-search/?q=${encodeURIComponent(query)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.results && data.results.length > 0) {
|
||||
resultsDiv.innerHTML = '';
|
||||
data.results.forEach(result => {
|
||||
const item = document.createElement('a');
|
||||
item.href = '#';
|
||||
item.className = 'list-group-item list-group-item-action';
|
||||
item.innerHTML = `<strong>${result.code}</strong> - ${result.description}`;
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
problemNameInput.value = result.description;
|
||||
problemCodeInput.value = result.code;
|
||||
if (codingSystemInput) {
|
||||
codingSystemInput.value = 'ICD10';
|
||||
}
|
||||
searchInput.value = result.display;
|
||||
resultsDiv.style.display = 'none';
|
||||
});
|
||||
resultsDiv.appendChild(item);
|
||||
});
|
||||
resultsDiv.style.display = 'block';
|
||||
} else {
|
||||
resultsDiv.innerHTML = '<div class="list-group-item">No results found</div>';
|
||||
resultsDiv.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error searching ICD-10:', error);
|
||||
resultsDiv.innerHTML = '<div class="list-group-item text-danger">Error searching</div>';
|
||||
resultsDiv.style.display = 'block';
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Hide results when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!searchInput.contains(e.target) && !resultsDiv.contains(e.target)) {
|
||||
resultsDiv.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -56,7 +56,8 @@ urlpatterns = [
|
||||
# Actions
|
||||
# path('record-create/', views.RecordCreateView.as_view(), name='record_create'),
|
||||
path('encounter/<uuid:encounter_id>/vitals/add/', views.add_vital_signs, name='add_vital_signs'),
|
||||
path('patient/<int:patient_id>/problem/add/', views.add_problem, name='add_problem'),
|
||||
path('encounter/<int:encounter_id>/problem/add/', views.add_problem, name='add_problem'),
|
||||
path('encounter/<int:encounter_id>/care-plan/add/', views.add_care_plan, name='add_care_plan'),
|
||||
path('encounter/<int:encounter_id>/status/', views.update_encounter_status, name='update_encounter_status'),
|
||||
path('note/<int:note_id>/sign/', views.sign_note, name='sign_note'),
|
||||
path('problem/<int:problem_id>/resolve/', views.resolve_problem, name='resolve_problem'),
|
||||
|
||||
160
emr/views.py
160
emr/views.py
@ -229,7 +229,9 @@ class EncounterDetailView(LoginRequiredMixin, DetailView):
|
||||
).prefetch_related(
|
||||
'vital_signs__measured_by',
|
||||
'clinical_notes__author',
|
||||
'problems_identified__diagnosing_provider'
|
||||
'problems_identified__diagnosing_provider',
|
||||
'problems_identified__care_plans',
|
||||
'problems_identified',
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -1863,52 +1865,127 @@ def add_vital_signs(request, pk):
|
||||
|
||||
|
||||
@login_required
|
||||
def add_problem(request, patient_id):
|
||||
def add_problem(request, encounter_id):
|
||||
"""
|
||||
HTMX endpoint for adding a problem.
|
||||
HTMX endpoint for adding a problem to an encounter.
|
||||
"""
|
||||
tenant = request.user.tenant
|
||||
encounter = get_object_or_404(Encounter, id=encounter_id, tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
patient = get_object_or_404(
|
||||
PatientProfile,
|
||||
id=patient_id,
|
||||
tenant=request.user.tenant
|
||||
)
|
||||
|
||||
problem_data = {
|
||||
'tenant': request.user.tenant,
|
||||
'patient': patient,
|
||||
'problem_name': request.POST.get('problem_name'),
|
||||
'problem_type': request.POST.get('problem_type', 'DIAGNOSIS'),
|
||||
'severity': request.POST.get('severity'),
|
||||
'priority': request.POST.get('priority', 'MEDIUM'),
|
||||
form = ProblemListForm(request.POST, tenant=tenant)
|
||||
if form.is_valid():
|
||||
problem = form.save(commit=False)
|
||||
problem.tenant = tenant
|
||||
problem.patient = encounter.patient
|
||||
problem.diagnosing_provider = request.user
|
||||
problem.related_encounter = encounter
|
||||
problem.save()
|
||||
|
||||
# Log the action
|
||||
try:
|
||||
AuditLogEntry.objects.create(
|
||||
tenant=tenant,
|
||||
user=request.user,
|
||||
event_type='CREATE',
|
||||
event_category='CLINICAL_DATA',
|
||||
action='Add Problem',
|
||||
description=f'Problem added: {problem.problem_name}',
|
||||
content_type=ContentType.objects.get_for_model(ProblemList),
|
||||
object_id=problem.pk,
|
||||
object_repr=str(problem),
|
||||
patient_id=str(encounter.patient.patient_id),
|
||||
patient_mrn=encounter.patient.mrn,
|
||||
changes={'problem_type': problem.problem_type, 'status': problem.status}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Audit logging failed: {e}")
|
||||
|
||||
messages.success(request, f'Problem "{problem.problem_name}" added successfully')
|
||||
return redirect('emr:encounter_detail', pk=encounter_id)
|
||||
else:
|
||||
# Return form with errors
|
||||
return render(request, 'emr/partials/problem_form_modal.html', {
|
||||
'form': form,
|
||||
'encounter': encounter,
|
||||
'patient': encounter.patient
|
||||
})
|
||||
else:
|
||||
# GET request - show form
|
||||
form = ProblemListForm(tenant=tenant, initial={
|
||||
'patient': encounter.patient,
|
||||
'diagnosing_provider': request.user,
|
||||
'clinical_notes': request.POST.get('clinical_notes', ''),
|
||||
}
|
||||
'related_encounter': encounter,
|
||||
'status': 'ACTIVE',
|
||||
'priority': 'MEDIUM'
|
||||
})
|
||||
return render(request, 'emr/partials/problem_form_modal.html', {
|
||||
'form': form,
|
||||
'encounter': encounter,
|
||||
'patient': encounter.patient
|
||||
})
|
||||
|
||||
# Add optional fields
|
||||
if request.POST.get('problem_code'):
|
||||
problem_data['problem_code'] = request.POST.get('problem_code')
|
||||
problem_data['coding_system'] = request.POST.get('coding_system', 'ICD10')
|
||||
|
||||
if request.POST.get('onset_date'):
|
||||
problem_data['onset_date'] = request.POST.get('onset_date')
|
||||
|
||||
# Create problem
|
||||
problem = ProblemList.objects.create(**problem_data)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='PROBLEM_ADDED',
|
||||
model='ProblemList',
|
||||
object_id=problem.id,
|
||||
details=f"Problem added for {patient.get_full_name()}: {problem.problem_name}"
|
||||
)
|
||||
|
||||
messages.success(request, f'Problem "{problem.problem_name}" added successfully')
|
||||
return JsonResponse({'success': True, 'problem_id': problem.id})
|
||||
|
||||
return JsonResponse({'error': 'Invalid request'}, status=400)
|
||||
@login_required
|
||||
def add_care_plan(request, encounter_id):
|
||||
"""
|
||||
HTMX endpoint for adding a care plan to an encounter.
|
||||
"""
|
||||
tenant = request.user.tenant
|
||||
encounter = get_object_or_404(Encounter, id=encounter_id, tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CarePlanForm(request.POST, tenant=tenant)
|
||||
if form.is_valid():
|
||||
care_plan = form.save(commit=False)
|
||||
care_plan.tenant = tenant
|
||||
care_plan.patient = encounter.patient
|
||||
care_plan.primary_provider = request.user
|
||||
care_plan.save()
|
||||
form.save_m2m() # Save many-to-many relationships
|
||||
|
||||
# Log the action
|
||||
try:
|
||||
AuditLogEntry.objects.create(
|
||||
tenant=tenant,
|
||||
user=request.user,
|
||||
event_type='CREATE',
|
||||
event_category='CARE_PLAN',
|
||||
action='Add Care Plan',
|
||||
description=f'Care plan created: {care_plan.title}',
|
||||
content_type=ContentType.objects.get_for_model(CarePlan),
|
||||
object_id=care_plan.pk,
|
||||
object_repr=str(care_plan),
|
||||
patient_id=str(encounter.patient.patient_id),
|
||||
patient_mrn=encounter.patient.mrn,
|
||||
changes={'plan_type': care_plan.plan_type, 'status': care_plan.status}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Audit logging failed: {e}")
|
||||
|
||||
messages.success(request, f'Care plan "{care_plan.title}" created successfully')
|
||||
return redirect('emr:encounter_detail', pk=encounter_id)
|
||||
else:
|
||||
# Return form with errors
|
||||
return render(request, 'emr/partials/care_plan_form_modal.html', {
|
||||
'form': form,
|
||||
'encounter': encounter,
|
||||
'patient': encounter.patient
|
||||
})
|
||||
else:
|
||||
# GET request - show form
|
||||
form = CarePlanForm(tenant=tenant, initial={
|
||||
'patient': encounter.patient,
|
||||
'primary_provider': request.user,
|
||||
'start_date': timezone.now().date(),
|
||||
'status': 'DRAFT',
|
||||
'priority': 'ROUTINE'
|
||||
})
|
||||
return render(request, 'emr/partials/care_plan_form_modal.html', {
|
||||
'form': form,
|
||||
'encounter': encounter,
|
||||
'patient': encounter.patient
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@ -2012,7 +2089,6 @@ def get_status_class(status):
|
||||
return status_classes.get(status, 'secondary')
|
||||
|
||||
|
||||
|
||||
def _norm_code(s: str) -> str:
|
||||
return (s or "").upper().replace(" ", "")
|
||||
|
||||
|
||||
Binary file not shown.
@ -21,7 +21,6 @@ class BuildingForm(forms.ModelForm):
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'building_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'airport_code': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'floor_count': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
|
||||
'total_area_sqm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
||||
@ -185,8 +184,9 @@ class MaintenanceRequestForm(forms.ModelForm):
|
||||
model = MaintenanceRequest
|
||||
fields = [
|
||||
'title', 'description', 'maintenance_type', 'building',
|
||||
'floor', 'room', 'asset', 'priority', 'scheduled_date', 'actual_cost',
|
||||
'estimated_cost', 'notes', 'assigned_to', 'scheduled_date', 'estimated_hours', 'status',
|
||||
'floor', 'room', 'asset', 'priority', 'scheduled_date',
|
||||
'estimated_cost', 'actual_cost', 'notes', 'assigned_to',
|
||||
'estimated_hours', 'status', 'completion_notes'
|
||||
]
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
@ -199,11 +199,12 @@ class MaintenanceRequestForm(forms.ModelForm):
|
||||
'priority': forms.Select(attrs={'class': 'form-select'}),
|
||||
'scheduled_date': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
||||
'estimated_cost': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
||||
'actual_cost': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
||||
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'assigned_to': forms.Select(attrs={'class': 'form-select'}),
|
||||
'estimated_hours': forms.NumberInput(attrs={'class': 'form-control', 'type': 'number'}),
|
||||
'estimated_hours': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.5', 'min': '0'}),
|
||||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||||
'actual_cost': forms.NumberInput(attrs={'class': 'form-control', 'type': 'number'}),
|
||||
'completion_notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -428,4 +429,3 @@ class MaintenanceFilterForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['assigned_to'].queryset = User.objects.filter(is_active=True)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -50,7 +50,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.building_type.id_for_label }}" class="form-label">Building Type <span class="text-danger">*</span></label>
|
||||
{{ form.building_type }}
|
||||
@ -59,16 +59,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.airport_code.id_for_label }}" class="form-label">Airport Code <span class="text-danger">*</span></label>
|
||||
{{ form.airport_code }}
|
||||
{% if form.airport_code.errors %}
|
||||
<div class="text-danger small mt-1">{{ form.airport_code.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<small class="form-text text-muted">IATA airport code</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@ -256,4 +246,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -144,6 +144,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.started_date.id_for_label }}" class="form-label">Started Date</label>
|
||||
{{ form.started_date }}
|
||||
{% if form.started_date.errors %}
|
||||
<div class="text-danger small mt-1">{{ form.started_date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.completed_date.id_for_label }}" class="form-label">Completed Date</label>
|
||||
{{ form.completed_date }}
|
||||
{% if form.completed_date.errors %}
|
||||
<div class="text-danger small mt-1">{{ form.completed_date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Status and Cost -->
|
||||
<hr>
|
||||
@ -179,30 +202,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Completion Details -->
|
||||
{% if object and object.status in 'completed,cancelled' %}
|
||||
<hr>
|
||||
<h6 class="fw-bold mb-3">Completion Details</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.completed_date.id_for_label }}" class="form-label">Completed Date</label>
|
||||
{{ form.completed_date }}
|
||||
{% if form.completed_date.errors %}
|
||||
<div class="text-danger small mt-1">{{ form.completed_date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.actual_hours.id_for_label }}" class="form-label">Actual Hours</label>
|
||||
{{ form.actual_hours }}
|
||||
{% if form.actual_hours.errors %}
|
||||
<div class="text-danger small mt-1">{{ form.actual_hours.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.completion_notes.id_for_label }}" class="form-label">Completion Notes</label>
|
||||
{{ form.completion_notes }}
|
||||
@ -211,7 +212,6 @@
|
||||
{% endif %}
|
||||
<small class="form-text text-muted">Work performed, parts used, recommendations</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Additional Notes -->
|
||||
<hr>
|
||||
@ -298,4 +298,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
58
fix_all_constraints.py
Normal file
58
fix_all_constraints.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""
|
||||
Comprehensive fix for all database foreign key constraints.
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
# Connect to SQLite database
|
||||
db_path = 'db.sqlite3'
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("Fixing all database constraints...")
|
||||
|
||||
# Disable foreign key checks
|
||||
cursor.execute("PRAGMA foreign_keys = OFF;")
|
||||
|
||||
# Get all tables with tenant_id that have violations
|
||||
tables_with_violations = [
|
||||
'radiology_imaging_study',
|
||||
'accounts_user',
|
||||
'hr_department',
|
||||
'hr_training_program',
|
||||
'operating_theatre_operating_room',
|
||||
'patients_patient_profile',
|
||||
]
|
||||
|
||||
for table in tables_with_violations:
|
||||
try:
|
||||
# Delete rows with invalid tenant_id
|
||||
cursor.execute(f"""
|
||||
DELETE FROM {table}
|
||||
WHERE tenant_id NOT IN (SELECT id FROM core_tenant)
|
||||
""")
|
||||
affected = cursor.rowcount
|
||||
if affected > 0:
|
||||
print(f" Deleted {affected} rows from {table} with invalid tenant_id")
|
||||
except sqlite3.OperationalError as e:
|
||||
print(f" Error fixing {table}: {e}")
|
||||
|
||||
# Commit changes
|
||||
conn.commit()
|
||||
|
||||
# Re-enable foreign key checks
|
||||
cursor.execute("PRAGMA foreign_keys = ON;")
|
||||
|
||||
# Verify constraints
|
||||
cursor.execute("PRAGMA foreign_key_check;")
|
||||
violations = cursor.fetchall()
|
||||
|
||||
if violations:
|
||||
print(f"\n⚠ Still have {len(violations)} foreign key violations")
|
||||
print("First 10 violations:")
|
||||
for violation in violations[:10]:
|
||||
print(f" {violation}")
|
||||
else:
|
||||
print("\n✓ All foreign key constraints are now valid!")
|
||||
|
||||
conn.close()
|
||||
print("\nDatabase fix completed!")
|
||||
70
fix_db_constraints.py
Normal file
70
fix_db_constraints.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""
|
||||
Fix database foreign key constraints before running new migrations.
|
||||
"""
|
||||
import os
|
||||
import django
|
||||
import sqlite3
|
||||
|
||||
# Setup Django
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
|
||||
django.setup()
|
||||
|
||||
# Connect to SQLite database
|
||||
db_path = 'db.sqlite3'
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("Fixing database constraints...")
|
||||
|
||||
# Disable foreign key checks temporarily
|
||||
cursor.execute("PRAGMA foreign_keys = OFF;")
|
||||
|
||||
# Fix operating_theatre_surgical_note_template with invalid tenant_id
|
||||
print("Fixing operating_theatre_surgical_note_template...")
|
||||
cursor.execute("""
|
||||
DELETE FROM operating_theatre_surgical_note_template
|
||||
WHERE tenant_id NOT IN (SELECT id FROM core_tenant)
|
||||
""")
|
||||
affected = cursor.rowcount
|
||||
print(f" Deleted {affected} rows with invalid tenant_id")
|
||||
|
||||
# Check for other tables with similar issues
|
||||
tables_to_check = [
|
||||
'facility_management_maintenancerequest',
|
||||
'facility_management_workorder',
|
||||
'facility_management_asset',
|
||||
]
|
||||
|
||||
for table in tables_to_check:
|
||||
try:
|
||||
cursor.execute(f"""
|
||||
UPDATE {table}
|
||||
SET tenant_id = 1
|
||||
WHERE tenant_id NOT IN (SELECT id FROM core_tenant)
|
||||
""")
|
||||
affected = cursor.rowcount
|
||||
if affected > 0:
|
||||
print(f" Fixed {affected} rows in {table}")
|
||||
except sqlite3.OperationalError as e:
|
||||
# Table might not exist yet
|
||||
print(f" Skipping {table}: {e}")
|
||||
|
||||
# Commit changes
|
||||
conn.commit()
|
||||
|
||||
# Re-enable foreign key checks
|
||||
cursor.execute("PRAGMA foreign_keys = ON;")
|
||||
|
||||
# Verify constraints
|
||||
cursor.execute("PRAGMA foreign_key_check;")
|
||||
violations = cursor.fetchall()
|
||||
|
||||
if violations:
|
||||
print("\nRemaining foreign key violations:")
|
||||
for violation in violations:
|
||||
print(f" {violation}")
|
||||
else:
|
||||
print("\n✓ All foreign key constraints are now valid!")
|
||||
|
||||
conn.close()
|
||||
print("\nDatabase constraints fixed successfully!")
|
||||
Binary file not shown.
@ -72,6 +72,7 @@ LOCAL_APPS = [
|
||||
'integration',
|
||||
'quality',
|
||||
'facility_management',
|
||||
'insurance_approvals.apps.InsuranceApprovalsConfig',
|
||||
]
|
||||
|
||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
750
hr/admin.py
750
hr/admin.py
@ -6,11 +6,16 @@ from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.db.models import Count, Avg, Sum
|
||||
from decimal import Decimal
|
||||
from datetime import date
|
||||
from datetime import date, timedelta
|
||||
from .models import *
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# INLINE ADMIN CLASSES
|
||||
# ============================================================================
|
||||
|
||||
class ScheduleInline(admin.TabularInline):
|
||||
"""
|
||||
Inline admin for employee schedules.
|
||||
@ -46,7 +51,7 @@ class PerformanceReviewInline(admin.TabularInline):
|
||||
fields = [
|
||||
'review_date', 'review_type', 'overall_rating', 'status'
|
||||
]
|
||||
|
||||
readonly_fields = ['review_id']
|
||||
|
||||
|
||||
class TrainingRecordInline(admin.TabularInline):
|
||||
@ -56,12 +61,67 @@ class TrainingRecordInline(admin.TabularInline):
|
||||
model = TrainingRecord
|
||||
extra = 0
|
||||
fields = [
|
||||
'completion_date',
|
||||
'program', 'session', 'completion_date',
|
||||
'status', 'passed'
|
||||
]
|
||||
readonly_fields = ['record_id']
|
||||
|
||||
|
||||
class ProgramModuleInline(admin.TabularInline):
|
||||
"""
|
||||
Inline admin for training program modules.
|
||||
"""
|
||||
model = ProgramModule
|
||||
extra = 0
|
||||
fields = ['order', 'title', 'hours']
|
||||
ordering = ['order']
|
||||
|
||||
|
||||
class ProgramPrerequisiteInline(admin.TabularInline):
|
||||
"""
|
||||
Inline admin for training program prerequisites.
|
||||
"""
|
||||
model = ProgramPrerequisite
|
||||
extra = 0
|
||||
fk_name = 'program'
|
||||
fields = ['required_program']
|
||||
|
||||
|
||||
class TrainingSessionInline(admin.TabularInline):
|
||||
"""
|
||||
Inline admin for training sessions.
|
||||
"""
|
||||
model = TrainingSession
|
||||
extra = 0
|
||||
fields = [
|
||||
'title', 'start_at', 'end_at', 'instructor',
|
||||
'delivery_method', 'capacity'
|
||||
]
|
||||
readonly_fields = ['session_id']
|
||||
|
||||
|
||||
class TrainingAttendanceInline(admin.TabularInline):
|
||||
"""
|
||||
Inline admin for training attendance.
|
||||
"""
|
||||
model = TrainingAttendance
|
||||
extra = 0
|
||||
fields = [
|
||||
'checked_in_at', 'checked_out_at', 'status', 'notes'
|
||||
]
|
||||
|
||||
|
||||
class TrainingAssessmentInline(admin.TabularInline):
|
||||
"""
|
||||
Inline admin for training assessments.
|
||||
"""
|
||||
model = TrainingAssessment
|
||||
extra = 0
|
||||
fields = [
|
||||
'name', 'max_score', 'score', 'passed', 'taken_at'
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Employee)
|
||||
class EmployeeAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
@ -741,121 +801,581 @@ class PerformanceReviewAdmin(admin.ModelAdmin):
|
||||
return qs.select_related('employee', 'reviewer')
|
||||
|
||||
|
||||
# @admin.register(TrainingRecord)
|
||||
# class TrainingRecordAdmin(admin.ModelAdmin):
|
||||
# """
|
||||
# Admin interface for training records.
|
||||
# """
|
||||
# list_display = [
|
||||
# 'employee_name', 'completion_date', 'status',
|
||||
# 'passed', 'expiry_status_display'
|
||||
# ]
|
||||
#
|
||||
# search_fields = [
|
||||
# 'employee__first_name', 'employee__last_name',
|
||||
# 'employee__employee_id', 'training_name'
|
||||
# ]
|
||||
# readonly_fields = [
|
||||
# 'record_id', 'created_at', 'updated_at'
|
||||
# ]
|
||||
# fieldsets = [
|
||||
# ('Training Information', {
|
||||
# 'fields': [
|
||||
# 'record_id', 'employee', 'training_description'
|
||||
# ]
|
||||
# }),
|
||||
#
|
||||
# ('Training Provider', {
|
||||
# 'fields': [
|
||||
# 'training_provider', 'instructor'
|
||||
# ]
|
||||
# }),
|
||||
# ('Training Dates', {
|
||||
# 'fields': [
|
||||
# 'completion_date', 'expiry_date'
|
||||
# ]
|
||||
# }),
|
||||
# ('Training Details', {
|
||||
# 'fields': [
|
||||
# 'duration_hours', 'credits_earned'
|
||||
# ]
|
||||
# }),
|
||||
# ('Training Status', {
|
||||
# 'fields': [
|
||||
# 'status'
|
||||
# ]
|
||||
# }),
|
||||
# ('Results', {
|
||||
# 'fields': [
|
||||
# 'score', 'passed'
|
||||
# ]
|
||||
# }),
|
||||
# ('Certification Information', {
|
||||
# 'fields': [
|
||||
# 'certificate_number', 'certification_body'
|
||||
# ]
|
||||
# }),
|
||||
# ('Cost Information', {
|
||||
# 'fields': [
|
||||
# 'training_cost'
|
||||
# ]
|
||||
# }),
|
||||
# ('Expiry Information', {
|
||||
# 'fields': [
|
||||
# 'is_expired', 'days_to_expiry', 'is_due_for_renewal'
|
||||
# ],
|
||||
# 'classes': ['collapse']
|
||||
# }),
|
||||
# ('Notes', {
|
||||
# 'fields': [
|
||||
# 'notes'
|
||||
# ],
|
||||
# 'classes': ['collapse']
|
||||
# }),
|
||||
# ('Related Information', {
|
||||
# 'fields': [
|
||||
# 'employee__tenant'
|
||||
# ],
|
||||
# 'classes': ['collapse']
|
||||
# }),
|
||||
# ('Metadata', {
|
||||
# 'fields': [
|
||||
# 'created_at', 'updated_at', 'created_by'
|
||||
# ],
|
||||
# 'classes': ['collapse']
|
||||
# })
|
||||
# ]
|
||||
#
|
||||
#
|
||||
# def employee_name(self, obj):
|
||||
# """Display employee name."""
|
||||
# return obj.employee.get_full_name()
|
||||
# employee_name.short_description = 'Employee'
|
||||
#
|
||||
# def expiry_status_display(self, obj):
|
||||
# """Display expiry status with color coding."""
|
||||
# if not obj.expiry_date:
|
||||
# return format_html('<span style="color: gray;">No Expiry</span>')
|
||||
#
|
||||
# if obj.is_expired:
|
||||
# return format_html('<span style="color: red;">⚠️ Expired</span>')
|
||||
#
|
||||
# if obj.is_due_for_renewal:
|
||||
# return format_html('<span style="color: orange;">⚠️ Due Soon</span>')
|
||||
#
|
||||
# return format_html('<span style="color: green;">✓ Valid</span>')
|
||||
# expiry_status_display.short_description = 'Expiry Status'
|
||||
#
|
||||
# def get_queryset(self, request):
|
||||
# """Filter by user's tenant."""
|
||||
# qs = super().get_queryset(request)
|
||||
# if hasattr(request.user, 'tenant'):
|
||||
# qs = qs.filter(employee__tenant=request.user.tenant)
|
||||
# return qs.select_related('employee', 'created_by')
|
||||
# ============================================================================
|
||||
# TRAINING ADMIN CLASSES
|
||||
# ============================================================================
|
||||
|
||||
@admin.register(TrainingPrograms)
|
||||
class TrainingProgramsAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin interface for training programs.
|
||||
"""
|
||||
list_display = [
|
||||
'name', 'program_type', 'program_provider', 'instructor',
|
||||
'duration_hours', 'cost', 'is_certified', 'validity_display'
|
||||
]
|
||||
list_filter = [
|
||||
'tenant', 'program_type', 'is_certified', 'program_provider'
|
||||
]
|
||||
search_fields = [
|
||||
'name', 'description', 'program_provider'
|
||||
]
|
||||
readonly_fields = [
|
||||
'program_id', 'created_at', 'updated_at'
|
||||
]
|
||||
fieldsets = [
|
||||
('Program Information', {
|
||||
'fields': [
|
||||
'program_id', 'tenant', 'name', 'description'
|
||||
]
|
||||
}),
|
||||
('Program Details', {
|
||||
'fields': [
|
||||
'program_type', 'program_provider', 'instructor'
|
||||
]
|
||||
}),
|
||||
('Schedule Information', {
|
||||
'fields': [
|
||||
'start_date', 'end_date', 'duration_hours'
|
||||
]
|
||||
}),
|
||||
('Cost Information', {
|
||||
'fields': [
|
||||
'cost'
|
||||
]
|
||||
}),
|
||||
('Certification Information', {
|
||||
'fields': [
|
||||
'is_certified', 'validity_days', 'notify_before_days'
|
||||
]
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': [
|
||||
'created_at', 'updated_at', 'created_by'
|
||||
],
|
||||
'classes': ['collapse']
|
||||
})
|
||||
]
|
||||
inlines = [ProgramModuleInline, ProgramPrerequisiteInline, TrainingSessionInline]
|
||||
date_hierarchy = 'start_date'
|
||||
|
||||
def validity_display(self, obj):
|
||||
"""Display validity information."""
|
||||
if obj.is_certified and obj.validity_days:
|
||||
return f"{obj.validity_days} days"
|
||||
return "No expiry"
|
||||
validity_display.short_description = 'Validity'
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Filter by user's tenant."""
|
||||
qs = super().get_queryset(request)
|
||||
if hasattr(request.user, 'tenant'):
|
||||
qs = qs.filter(tenant=request.user.tenant)
|
||||
return qs.select_related('instructor', 'created_by')
|
||||
|
||||
|
||||
@admin.register(ProgramModule)
|
||||
class ProgramModuleAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin interface for program modules.
|
||||
"""
|
||||
list_display = [
|
||||
'program', 'order', 'title', 'hours'
|
||||
]
|
||||
list_filter = [
|
||||
'program__tenant', 'program'
|
||||
]
|
||||
search_fields = [
|
||||
'title', 'program__name'
|
||||
]
|
||||
ordering = ['program', 'order']
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Filter by user's tenant."""
|
||||
qs = super().get_queryset(request)
|
||||
if hasattr(request.user, 'tenant'):
|
||||
qs = qs.filter(program__tenant=request.user.tenant)
|
||||
return qs.select_related('program')
|
||||
|
||||
|
||||
@admin.register(ProgramPrerequisite)
|
||||
class ProgramPrerequisiteAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin interface for program prerequisites.
|
||||
"""
|
||||
list_display = [
|
||||
'program', 'required_program'
|
||||
]
|
||||
list_filter = [
|
||||
'program__tenant', 'program'
|
||||
]
|
||||
search_fields = [
|
||||
'program__name', 'required_program__name'
|
||||
]
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Filter by user's tenant."""
|
||||
qs = super().get_queryset(request)
|
||||
if hasattr(request.user, 'tenant'):
|
||||
qs = qs.filter(program__tenant=request.user.tenant)
|
||||
return qs.select_related('program', 'required_program')
|
||||
|
||||
|
||||
@admin.register(TrainingSession)
|
||||
class TrainingSessionAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin interface for training sessions.
|
||||
"""
|
||||
list_display = [
|
||||
'program', 'title_display', 'instructor', 'start_at',
|
||||
'end_at', 'delivery_method', 'capacity', 'enrollment_count'
|
||||
]
|
||||
list_filter = [
|
||||
'program__tenant', 'delivery_method', 'start_at'
|
||||
]
|
||||
search_fields = [
|
||||
'title', 'program__name', 'instructor__first_name', 'instructor__last_name'
|
||||
]
|
||||
readonly_fields = [
|
||||
'session_id', 'enrollment_count', 'created_at'
|
||||
]
|
||||
fieldsets = [
|
||||
('Session Information', {
|
||||
'fields': [
|
||||
'session_id', 'program', 'title'
|
||||
]
|
||||
}),
|
||||
('Instructor Information', {
|
||||
'fields': [
|
||||
'instructor'
|
||||
]
|
||||
}),
|
||||
('Schedule Information', {
|
||||
'fields': [
|
||||
'start_at', 'end_at'
|
||||
]
|
||||
}),
|
||||
('Delivery Information', {
|
||||
'fields': [
|
||||
'delivery_method', 'location'
|
||||
]
|
||||
}),
|
||||
('Capacity Information', {
|
||||
'fields': [
|
||||
'capacity', 'enrollment_count'
|
||||
]
|
||||
}),
|
||||
('Cost Override', {
|
||||
'fields': [
|
||||
'cost_override', 'hours_override'
|
||||
]
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': [
|
||||
'created_at', 'created_by'
|
||||
],
|
||||
'classes': ['collapse']
|
||||
})
|
||||
]
|
||||
date_hierarchy = 'start_at'
|
||||
|
||||
def title_display(self, obj):
|
||||
"""Display session title or program name."""
|
||||
return obj.title or obj.program.name
|
||||
title_display.short_description = 'Title'
|
||||
|
||||
def enrollment_count(self, obj):
|
||||
"""Display enrollment count."""
|
||||
return obj.enrollments.count()
|
||||
enrollment_count.short_description = 'Enrollments'
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Filter by user's tenant."""
|
||||
qs = super().get_queryset(request)
|
||||
if hasattr(request.user, 'tenant'):
|
||||
qs = qs.filter(program__tenant=request.user.tenant)
|
||||
return qs.select_related('program', 'instructor', 'created_by')
|
||||
|
||||
|
||||
@admin.register(TrainingRecord)
|
||||
class TrainingRecordAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin interface for training records (enrollments).
|
||||
"""
|
||||
list_display = [
|
||||
'employee_name', 'program', 'session', 'enrolled_at',
|
||||
'completion_date', 'status', 'passed', 'expiry_status_display'
|
||||
]
|
||||
list_filter = [
|
||||
'employee__tenant', 'status', 'passed', 'program__program_type',
|
||||
'enrolled_at', 'completion_date'
|
||||
]
|
||||
search_fields = [
|
||||
'employee__first_name', 'employee__last_name',
|
||||
'employee__employee_id', 'program__name'
|
||||
]
|
||||
readonly_fields = [
|
||||
'record_id', 'enrolled_at', 'hours', 'effective_cost',
|
||||
'eligible_for_certificate', 'completion_percentage',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
fieldsets = [
|
||||
('Enrollment Information', {
|
||||
'fields': [
|
||||
'record_id', 'employee', 'program', 'session'
|
||||
]
|
||||
}),
|
||||
('Enrollment Dates', {
|
||||
'fields': [
|
||||
'enrolled_at', 'started_at', 'completion_date', 'expiry_date'
|
||||
]
|
||||
}),
|
||||
('Status Information', {
|
||||
'fields': [
|
||||
'status', 'passed'
|
||||
]
|
||||
}),
|
||||
('Performance Information', {
|
||||
'fields': [
|
||||
'credits_earned', 'score'
|
||||
]
|
||||
}),
|
||||
('Cost Information', {
|
||||
'fields': [
|
||||
'cost_paid', 'effective_cost'
|
||||
]
|
||||
}),
|
||||
('Calculated Information', {
|
||||
'fields': [
|
||||
'hours', 'eligible_for_certificate', 'completion_percentage'
|
||||
],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Notes', {
|
||||
'fields': [
|
||||
'notes'
|
||||
],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': [
|
||||
'created_at', 'updated_at', 'created_by'
|
||||
],
|
||||
'classes': ['collapse']
|
||||
})
|
||||
]
|
||||
inlines = [TrainingAttendanceInline, TrainingAssessmentInline]
|
||||
date_hierarchy = 'enrolled_at'
|
||||
|
||||
def employee_name(self, obj):
|
||||
"""Display employee name."""
|
||||
return obj.employee.get_full_name()
|
||||
employee_name.short_description = 'Employee'
|
||||
|
||||
def expiry_status_display(self, obj):
|
||||
"""Display expiry status with color coding."""
|
||||
if not obj.expiry_date:
|
||||
return format_html('<span style="color: gray;">No Expiry</span>')
|
||||
|
||||
days_to_expiry = (obj.expiry_date - date.today()).days
|
||||
if days_to_expiry < 0:
|
||||
return format_html('<span style="color: red;">⚠️ Expired</span>')
|
||||
elif days_to_expiry <= 30:
|
||||
return format_html('<span style="color: orange;">⚠️ Expires Soon</span>')
|
||||
else:
|
||||
return format_html('<span style="color: green;">✓ Valid</span>')
|
||||
expiry_status_display.short_description = 'Expiry Status'
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Filter by user's tenant."""
|
||||
qs = super().get_queryset(request)
|
||||
if hasattr(request.user, 'tenant'):
|
||||
qs = qs.filter(employee__tenant=request.user.tenant)
|
||||
return qs.select_related('employee', 'program', 'session', 'created_by')
|
||||
|
||||
|
||||
@admin.register(TrainingAttendance)
|
||||
class TrainingAttendanceAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin interface for training attendance.
|
||||
"""
|
||||
list_display = [
|
||||
'employee_name', 'program_name', 'checked_in_at',
|
||||
'checked_out_at', 'status', 'duration_display'
|
||||
]
|
||||
list_filter = [
|
||||
'enrollment__employee__tenant', 'status', 'checked_in_at'
|
||||
]
|
||||
search_fields = [
|
||||
'enrollment__employee__first_name', 'enrollment__employee__last_name',
|
||||
'enrollment__program__name'
|
||||
]
|
||||
readonly_fields = [
|
||||
'duration_display'
|
||||
]
|
||||
fieldsets = [
|
||||
('Attendance Information', {
|
||||
'fields': [
|
||||
'enrollment'
|
||||
]
|
||||
}),
|
||||
('Check-in/out Times', {
|
||||
'fields': [
|
||||
'checked_in_at', 'checked_out_at'
|
||||
]
|
||||
}),
|
||||
('Status Information', {
|
||||
'fields': [
|
||||
'status'
|
||||
]
|
||||
}),
|
||||
('Duration Information', {
|
||||
'fields': [
|
||||
'duration_display'
|
||||
],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Notes', {
|
||||
'fields': [
|
||||
'notes'
|
||||
],
|
||||
'classes': ['collapse']
|
||||
})
|
||||
]
|
||||
date_hierarchy = 'checked_in_at'
|
||||
|
||||
def employee_name(self, obj):
|
||||
"""Display employee name."""
|
||||
return obj.enrollment.employee.get_full_name()
|
||||
employee_name.short_description = 'Employee'
|
||||
|
||||
def program_name(self, obj):
|
||||
"""Display program name."""
|
||||
return obj.enrollment.program.name
|
||||
program_name.short_description = 'Program'
|
||||
|
||||
def duration_display(self, obj):
|
||||
"""Display attendance duration."""
|
||||
if obj.checked_in_at and obj.checked_out_at:
|
||||
duration = obj.checked_out_at - obj.checked_in_at
|
||||
hours = duration.total_seconds() / 3600
|
||||
return f"{hours:.2f} hours"
|
||||
return "Incomplete"
|
||||
duration_display.short_description = 'Duration'
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Filter by user's tenant."""
|
||||
qs = super().get_queryset(request)
|
||||
if hasattr(request.user, 'tenant'):
|
||||
qs = qs.filter(enrollment__employee__tenant=request.user.tenant)
|
||||
return qs.select_related('enrollment__employee', 'enrollment__program')
|
||||
|
||||
|
||||
@admin.register(TrainingAssessment)
|
||||
class TrainingAssessmentAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin interface for training assessments.
|
||||
"""
|
||||
list_display = [
|
||||
'employee_name', 'program_name', 'name',
|
||||
'score', 'max_score', 'percentage_display', 'passed', 'taken_at'
|
||||
]
|
||||
list_filter = [
|
||||
'enrollment__employee__tenant', 'passed', 'taken_at'
|
||||
]
|
||||
search_fields = [
|
||||
'enrollment__employee__first_name', 'enrollment__employee__last_name',
|
||||
'enrollment__program__name', 'name'
|
||||
]
|
||||
readonly_fields = [
|
||||
'percentage_display'
|
||||
]
|
||||
fieldsets = [
|
||||
('Assessment Information', {
|
||||
'fields': [
|
||||
'enrollment', 'name'
|
||||
]
|
||||
}),
|
||||
('Score Information', {
|
||||
'fields': [
|
||||
'max_score', 'score', 'percentage_display', 'passed'
|
||||
]
|
||||
}),
|
||||
('Date Information', {
|
||||
'fields': [
|
||||
'taken_at'
|
||||
]
|
||||
}),
|
||||
('Notes', {
|
||||
'fields': [
|
||||
'notes'
|
||||
],
|
||||
'classes': ['collapse']
|
||||
})
|
||||
]
|
||||
date_hierarchy = 'taken_at'
|
||||
|
||||
def employee_name(self, obj):
|
||||
"""Display employee name."""
|
||||
return obj.enrollment.employee.get_full_name()
|
||||
employee_name.short_description = 'Employee'
|
||||
|
||||
def program_name(self, obj):
|
||||
"""Display program name."""
|
||||
return obj.enrollment.program.name
|
||||
program_name.short_description = 'Program'
|
||||
|
||||
def percentage_display(self, obj):
|
||||
"""Display score percentage."""
|
||||
if obj.score is not None and obj.max_score > 0:
|
||||
percentage = (obj.score / obj.max_score) * 100
|
||||
color = 'green' if percentage >= 70 else 'orange' if percentage >= 50 else 'red'
|
||||
return format_html(
|
||||
'<span style="color: {};">{:.1f}%</span>',
|
||||
color, percentage
|
||||
)
|
||||
return "N/A"
|
||||
percentage_display.short_description = 'Percentage'
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Filter by user's tenant."""
|
||||
qs = super().get_queryset(request)
|
||||
if hasattr(request.user, 'tenant'):
|
||||
qs = qs.filter(enrollment__employee__tenant=request.user.tenant)
|
||||
return qs.select_related('enrollment__employee', 'enrollment__program')
|
||||
|
||||
|
||||
@admin.register(TrainingCertificates)
|
||||
class TrainingCertificatesAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin interface for training certificates.
|
||||
"""
|
||||
list_display = [
|
||||
'employee_name', 'certificate_name', 'program',
|
||||
'issued_date', 'expiry_date', 'expiry_status_display',
|
||||
'certificate_number'
|
||||
]
|
||||
list_filter = [
|
||||
'employee__tenant', 'program', 'issued_date', 'expiry_date'
|
||||
]
|
||||
search_fields = [
|
||||
'employee__first_name', 'employee__last_name',
|
||||
'certificate_name', 'certificate_number', 'program__name'
|
||||
]
|
||||
readonly_fields = [
|
||||
'certificate_id', 'is_expired', 'days_to_expiry',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
fieldsets = [
|
||||
('Certificate Information', {
|
||||
'fields': [
|
||||
'certificate_id', 'employee', 'program', 'enrollment'
|
||||
]
|
||||
}),
|
||||
('Certificate Details', {
|
||||
'fields': [
|
||||
'certificate_name', 'certificate_number', 'certification_body'
|
||||
]
|
||||
}),
|
||||
('Date Information', {
|
||||
'fields': [
|
||||
'issued_date', 'expiry_date'
|
||||
]
|
||||
}),
|
||||
('File Information', {
|
||||
'fields': [
|
||||
'file'
|
||||
]
|
||||
}),
|
||||
('Signature Information', {
|
||||
'fields': [
|
||||
'created_by', 'signed_by'
|
||||
]
|
||||
}),
|
||||
('Status Information', {
|
||||
'fields': [
|
||||
'is_expired', 'days_to_expiry'
|
||||
],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': [
|
||||
'created_at', 'updated_at'
|
||||
],
|
||||
'classes': ['collapse']
|
||||
})
|
||||
]
|
||||
date_hierarchy = 'issued_date'
|
||||
|
||||
def employee_name(self, obj):
|
||||
"""Display employee name."""
|
||||
return obj.employee.get_full_name()
|
||||
employee_name.short_description = 'Employee'
|
||||
|
||||
def expiry_status_display(self, obj):
|
||||
"""Display expiry status with color coding."""
|
||||
if not obj.expiry_date:
|
||||
return format_html('<span style="color: gray;">No Expiry</span>')
|
||||
|
||||
if obj.is_expired:
|
||||
return format_html('<span style="color: red;">⚠️ Expired</span>')
|
||||
|
||||
if obj.days_to_expiry is not None and obj.days_to_expiry <= 30:
|
||||
return format_html('<span style="color: orange;">⚠️ Expires Soon</span>')
|
||||
|
||||
return format_html('<span style="color: green;">✓ Valid</span>')
|
||||
expiry_status_display.short_description = 'Expiry Status'
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Filter by user's tenant."""
|
||||
qs = super().get_queryset(request)
|
||||
if hasattr(request.user, 'tenant'):
|
||||
qs = qs.filter(employee__tenant=request.user.tenant)
|
||||
return qs.select_related('employee', 'program', 'enrollment', 'created_by', 'signed_by')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CUSTOM ADMIN ACTIONS
|
||||
# ============================================================================
|
||||
|
||||
def mark_training_completed(modeladmin, request, queryset):
|
||||
"""Mark selected training records as completed."""
|
||||
updated = queryset.update(status='COMPLETED', completion_date=date.today())
|
||||
modeladmin.message_user(request, f'{updated} training records marked as completed.')
|
||||
mark_training_completed.short_description = "Mark selected training as completed"
|
||||
|
||||
def mark_training_passed(modeladmin, request, queryset):
|
||||
"""Mark selected training records as passed."""
|
||||
updated = queryset.update(passed=True)
|
||||
modeladmin.message_user(request, f'{updated} training records marked as passed.')
|
||||
mark_training_passed.short_description = "Mark selected training as passed"
|
||||
|
||||
def generate_certificates(modeladmin, request, queryset):
|
||||
"""Generate certificates for completed training."""
|
||||
count = 0
|
||||
for record in queryset.filter(status='COMPLETED', passed=True):
|
||||
if record.program.is_certified and not hasattr(record, 'certificate'):
|
||||
TrainingCertificates.objects.create(
|
||||
program=record.program,
|
||||
employee=record.employee,
|
||||
enrollment=record,
|
||||
certificate_name=f"{record.program.name} Certificate",
|
||||
expiry_date=TrainingCertificates.compute_expiry(record.program, date.today()),
|
||||
created_by=request.user
|
||||
)
|
||||
count += 1
|
||||
modeladmin.message_user(request, f'{count} certificates generated.')
|
||||
generate_certificates.short_description = "Generate certificates for completed training"
|
||||
|
||||
# Add actions to TrainingRecord admin
|
||||
TrainingRecordAdmin.actions = [mark_training_completed, mark_training_passed, generate_certificates]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ADMIN SITE CUSTOMIZATION
|
||||
# ============================================================================
|
||||
|
||||
# Customize admin site
|
||||
admin.site.site_header = "Hospital Management System - HR"
|
||||
admin.site.site_title = "HR Admin"
|
||||
admin.site.index_title = "Human Resources Administration"
|
||||
|
||||
|
||||
802
hr/forms.py
802
hr/forms.py
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -441,45 +441,7 @@
|
||||
|
||||
<!-- Pagination for Card View -->
|
||||
{% if is_paginated %}
|
||||
<div class="col-12 mt-3">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% if search %}&search={{ search }}{% endif %}{% if selected_schedule %}&schedule={{ selected_schedule }}{% endif %}{% if selected_employee %}&employee={{ selected_employee }}{% endif %}{% if selected_department %}&department={{ selected_department }}{% endif %}{% if selected_shift_type %}&shift_type={{ selected_shift_type }}{% endif %}{% if date_range %}&date_range={{ date_range }}{% endif %}" aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search %}&search={{ search }}{% endif %}{% if selected_schedule %}&schedule={{ selected_schedule }}{% endif %}{% if selected_employee %}&employee={{ selected_employee }}{% endif %}{% if selected_department %}&department={{ selected_department }}{% endif %}{% if selected_shift_type %}&shift_type={{ selected_shift_type }}{% endif %}{% if date_range %}&date_range={{ date_range }}{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active"><a class="page-link" href="#">{{ num }}</a></li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item"><a class="page-link" href="?page={{ num }}{% if search %}&search={{ search }}{% endif %}{% if selected_schedule %}&schedule={{ selected_schedule }}{% endif %}{% if selected_employee %}&employee={{ selected_employee }}{% endif %}{% if selected_department %}&department={{ selected_department }}{% endif %}{% if selected_shift_type %}&shift_type={{ selected_shift_type }}{% endif %}{% if date_range %}&date_range={{ date_range }}{% endif %}">{{ num }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search %}&search={{ search }}{% endif %}{% if selected_schedule %}&schedule={{ selected_schedule }}{% endif %}{% if selected_employee %}&employee={{ selected_employee }}{% endif %}{% if selected_department %}&department={{ selected_department }}{% endif %}{% if selected_shift_type %}&shift_type={{ selected_shift_type }}{% endif %}{% if date_range %}&date_range={{ date_range }}{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search %}&search={{ search }}{% endif %}{% if selected_schedule %}&schedule={{ selected_schedule }}{% endif %}{% if selected_employee %}&employee={{ selected_employee }}{% endif %}{% if selected_department %}&department={{ selected_department }}{% endif %}{% if selected_shift_type %}&shift_type={{ selected_shift_type }}{% endif %}{% if date_range %}&date_range={{ date_range }}{% endif %}" aria-label="Last">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -494,7 +456,7 @@
|
||||
|
||||
{% block js %}
|
||||
<!-- DataTables JS -->
|
||||
<script src="{% static 'plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||
|
||||
@ -188,15 +188,9 @@
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-clock me-2"></i>Recent Activity
|
||||
<i class="fas fa-timeline me-2"></i>Departments Tree
|
||||
</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<button type="button" class="btn btn-xs btn-outline-secondary me-2" onclick="refreshActivity()">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<a href="#" class="btn btn-xs btn-outline-primary me-2">
|
||||
<i class="fas fa-list me-1"></i>View All
|
||||
</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-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||
@ -204,63 +198,64 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="timeline" id="activity-timeline">
|
||||
<!-- Recent Hires -->
|
||||
{% for employee in recent_hires %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker bg-success"></div>
|
||||
<div class="timeline-content">
|
||||
<h6 class="timeline-title">New Employee Hired</h6>
|
||||
<p class="timeline-text">
|
||||
<strong>{{ employee.get_full_name }}</strong> joined
|
||||
{% if employee.department %}{{ employee.department.name }}{% else %}the organization{% endif %}
|
||||
as {{ employee.job_title|default:"Employee" }}
|
||||
</p>
|
||||
<small class="text-muted">{{ employee.hire_date|timesince }} ago</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Recent Reviews -->
|
||||
{% for review in recent_reviews %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker bg-info"></div>
|
||||
<div class="timeline-content">
|
||||
<h6 class="timeline-title">Performance Review Completed</h6>
|
||||
<p class="timeline-text">
|
||||
<strong>{{ review.employee.get_full_name }}</strong> received a performance review
|
||||
{% if review.overall_rating %}
|
||||
with rating: {{ review.get_overall_rating_display }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<small class="text-muted">{{ review.review_date|timesince }} ago</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Recent Training -->
|
||||
{% for training in recent_training %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker bg-warning"></div>
|
||||
<div class="timeline-content">
|
||||
<h6 class="timeline-title">Training Completed</h6>
|
||||
<p class="timeline-text">
|
||||
<strong>{{ training.employee.get_full_name }}</strong> completed
|
||||
"{{ training.training_name }}"
|
||||
</p>
|
||||
<small class="text-muted">{{ training.completion_date|timesince }} ago</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not recent_hires and not recent_reviews and not recent_training %}
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-clock fa-3x text-muted mb-3"></i>
|
||||
<h6 class="text-muted">No recent activity</h6>
|
||||
<p class="text-muted">Recent HR activities will appear here</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'hr/departments/department_tree.html' %}
|
||||
{# <div class="timeline" id="activity-timeline">#}
|
||||
{# <!-- Recent Hires -->#}
|
||||
{# {% for employee in recent_hires %}#}
|
||||
{# <div class="timeline-item">#}
|
||||
{# <div class="timeline-marker bg-success"></div>#}
|
||||
{# <div class="timeline-content">#}
|
||||
{# <h6 class="timeline-title">New Employee Hired</h6>#}
|
||||
{# <p class="timeline-text">#}
|
||||
{# <strong>{{ employee.get_full_name }}</strong> joined #}
|
||||
{# {% if employee.department %}{{ employee.department.name }}{% else %}the organization{% endif %}#}
|
||||
{# as {{ employee.job_title|default:"Employee" }}#}
|
||||
{# </p>#}
|
||||
{# <small class="text-muted">{{ employee.hire_date|timesince }} ago</small>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# {% endfor %}#}
|
||||
{##}
|
||||
{# <!-- Recent Reviews -->#}
|
||||
{# {% for review in recent_reviews %}#}
|
||||
{# <div class="timeline-item">#}
|
||||
{# <div class="timeline-marker bg-info"></div>#}
|
||||
{# <div class="timeline-content">#}
|
||||
{# <h6 class="timeline-title">Performance Review Completed</h6>#}
|
||||
{# <p class="timeline-text">#}
|
||||
{# <strong>{{ review.employee.get_full_name }}</strong> received a performance review#}
|
||||
{# {% if review.overall_rating %}#}
|
||||
{# with rating: {{ review.get_overall_rating_display }}#}
|
||||
{# {% endif %}#}
|
||||
{# </p>#}
|
||||
{# <small class="text-muted">{{ review.review_date|timesince }} ago</small>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# {% endfor %}#}
|
||||
{##}
|
||||
{# <!-- Recent Training -->#}
|
||||
{# {% for training in recent_training %}#}
|
||||
{# <div class="timeline-item">#}
|
||||
{# <div class="timeline-marker bg-warning"></div>#}
|
||||
{# <div class="timeline-content">#}
|
||||
{# <h6 class="timeline-title">Training Completed</h6>#}
|
||||
{# <p class="timeline-text">#}
|
||||
{# <strong>{{ training.employee.get_full_name }}</strong> completed #}
|
||||
{# "{{ training.training_name }}"#}
|
||||
{# </p>#}
|
||||
{# <small class="text-muted">{{ training.completion_date|timesince }} ago</small>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# {% endfor %}#}
|
||||
{##}
|
||||
{# {% if not recent_hires and not recent_reviews and not recent_training %}#}
|
||||
{# <div class="text-center py-4">#}
|
||||
{# <i class="fas fa-clock fa-3x text-muted mb-3"></i>#}
|
||||
{# <h6 class="text-muted">No recent activity</h6>#}
|
||||
{# <p class="text-muted">Recent HR activities will appear here</p>#}
|
||||
{# </div>#}
|
||||
{# {% endif %}#}
|
||||
{# </div>#}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -2,7 +2,63 @@
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Assign Department Head - {{ department.name }}{% endblock %}
|
||||
{% block css %}
|
||||
<style>
|
||||
.avatar-sm {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.avatar-title {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
#userPreview {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.table-responsive {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.avatar-sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.avatar-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
@ -15,7 +71,7 @@
|
||||
<h6>{{ department.code }} - {{ department.name }}</h6>
|
||||
</div>
|
||||
<div class="page-btn">
|
||||
<a href="{% url 'core:department_detail' department.pk %}" class="btn btn-secondary">
|
||||
<a href="{% url 'hr:department_detail' department.pk %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Department
|
||||
</a>
|
||||
</div>
|
||||
@ -140,7 +196,7 @@
|
||||
<!-- Selected User Preview -->
|
||||
<div class="row" id="userPreview" style="display: none;">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<div class="note alert-info p-4">
|
||||
<h6><i class="fas fa-info-circle me-2"></i>Selected User Information</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@ -160,7 +216,7 @@
|
||||
{% if current_head %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
<div class="note alert-warning p-4">
|
||||
<h6><i class="fas fa-exclamation-triangle me-2"></i>Current Department Head</h6>
|
||||
<p class="mb-0">
|
||||
<strong>{{ current_head.get_full_name }}</strong> is currently the head of this department.
|
||||
@ -175,7 +231,7 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group text-end">
|
||||
<a href="{% url 'core:department_detail' department.pk %}" class="btn btn-secondary me-2">
|
||||
<a href="{% url 'hr:department_detail' department.pk %}" class="btn btn-secondary me-2">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
|
||||
@ -260,7 +316,7 @@
|
||||
<p class="text-muted">
|
||||
There are no active staff members available to assign as department head.
|
||||
</p>
|
||||
<a href="{% url 'core:department_detail' department.pk %}" class="btn btn-secondary">
|
||||
<a href="{% url 'hr:department_detail' department.pk %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Department
|
||||
</a>
|
||||
</div>
|
||||
@ -270,7 +326,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const userSelect = document.getElementById('user_id');
|
||||
@ -371,60 +428,6 @@ function removeDepartmentHead() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.avatar-sm {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.avatar-title {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
#userPreview {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.table-responsive {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.avatar-sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.avatar-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -408,13 +408,13 @@
|
||||
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||
<!-- Chart.js -->
|
||||
<script src="{% static 'plugins/chart.js/dist/chart.js' %}"></script>
|
||||
<script src="{% static 'plugins/chart.js/dist/chart.umd.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable
|
||||
$('#employees-table').DataTable({
|
||||
responsive: true,
|
||||
responsive: false,
|
||||
lengthMenu: [5, 10, 25, 50],
|
||||
pageLength: 5
|
||||
});
|
||||
@ -448,7 +448,7 @@
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
responsive: false,
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
{% block css %}
|
||||
<!-- Select2 CSS -->
|
||||
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/select2-bootstrap5-theme/select2-bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<style>
|
||||
.form-floating > .form-control:focus ~ label,
|
||||
.form-floating > .form-control:not(:placeholder-shown) ~ label {
|
||||
@ -116,15 +115,15 @@
|
||||
|
||||
<!-- Department Code -->
|
||||
<div class="form-floating mb-3 required-field">
|
||||
{{ form.department_code }}
|
||||
<label for="{{ form.department_code.id_for_label }}">Department Code</label>
|
||||
{% if form.department_code.errors %}
|
||||
{{ form.code }}
|
||||
<label for="{{ form.code.id_for_label }}">Department Code</label>
|
||||
{% if form.code.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.department_code.errors }}
|
||||
{{ form.code.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.department_code.help_text %}
|
||||
<div class="help-text mt-1">{{ form.department_code.help_text }}</div>
|
||||
{% if form.code.help_text %}
|
||||
<div class="help-text mt-1">{{ form.code.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -347,11 +346,11 @@
|
||||
$('#{{ form.name.id_for_label }}').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
if ($('#{{ form.department_code.id_for_label }}').val() === '') {
|
||||
$('#{{ form.department_code.id_for_label }}').addClass('is-invalid');
|
||||
if ($('#{{ form.code.id_for_label }}').val() === '') {
|
||||
$('#{{ form.code.id_for_label }}').addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
$('#{{ form.department_code.id_for_label }}').removeClass('is-invalid');
|
||||
$('#{{ form.code.id_for_label }}').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
@ -375,4 +374,3 @@
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
366
hr/templates/hr/departments/department_tree.html
Normal file
366
hr/templates/hr/departments/department_tree.html
Normal file
@ -0,0 +1,366 @@
|
||||
{% load static %}
|
||||
|
||||
<div class="department-tree">
|
||||
{% for department in departments %}
|
||||
{% include 'hr/departments/department_tree_node.html' with department=department level=0 %}
|
||||
{% empty %}
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-building fa-3x mb-3"></i>
|
||||
<p class="mb-0">No departments found</p>
|
||||
<small>Create your first department to get started</small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Department tree node template -->
|
||||
{% verbatim %}
|
||||
<script type="text/template" id="department-node-template">
|
||||
<div class="department-node" data-department-id="{{id}}">
|
||||
<div class="department-item d-flex align-items-center py-2 px-3 border-bottom">
|
||||
<div class="department-toggle me-2" style="width: 20px;">
|
||||
{{#if hasChildren}}
|
||||
<i class="fas fa-chevron-right toggle-icon" data-bs-toggle="collapse"
|
||||
data-bs-target="#dept-{{id}}-children" aria-expanded="false"></i>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="department-icon me-3">
|
||||
<i class="fas {{icon}} text-{{color}}"></i>
|
||||
</div>
|
||||
|
||||
<div class="department-info flex-grow-1">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-0 department-name">
|
||||
<a href="/hr/departments/{{id}}/"
|
||||
class="text-decoration-none">{{name}}</a>
|
||||
{{#unless is_active}}
|
||||
<span class="badge badge-secondary ms-2">Inactive</span>
|
||||
{{/unless}}
|
||||
</h6>
|
||||
<small class="text-muted">
|
||||
{{department_type}} • {{employee_count}} employees
|
||||
{{#if department_head}}
|
||||
• Head: {{department_head}}
|
||||
{{/if}}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="department-actions">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="/hr/departments/{{id}}/"
|
||||
class="btn btn-outline-primary btn-sm" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="/hr/departments/{{id}}/edit/"
|
||||
class="btn btn-outline-secondary btn-sm" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{{#if is_active}}
|
||||
<button class="btn btn-outline-warning btn-sm"
|
||||
onclick="deactivateDepartment('{{id}}')" title="Deactivate">
|
||||
<i class="fas fa-pause"></i>
|
||||
</button>
|
||||
{{else}}
|
||||
<button class="btn btn-outline-success btn-sm"
|
||||
onclick="activateDepartment('{{id}}')" title="Activate">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if hasChildren}}
|
||||
<div class="collapse" id="dept-{{id}}-children">
|
||||
<div class="department-children ps-4">
|
||||
<!-- Children will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</script>
|
||||
{% endverbatim %}
|
||||
|
||||
<style>
|
||||
.department-tree {
|
||||
background: #fff;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.department-node {
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.department-node:hover {
|
||||
background-color: #f8f9fa;
|
||||
border-left-color: #007bff;
|
||||
}
|
||||
|
||||
.department-node.active {
|
||||
background-color: #e3f2fd;
|
||||
border-left-color: #2196f3;
|
||||
}
|
||||
|
||||
.department-item {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.department-toggle {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
transition: transform 0.2s ease;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.toggle-icon:hover {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.toggle-icon.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.department-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.department-info .department-name a {
|
||||
color: #212529;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.department-info .department-name a:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.department-actions {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.department-node:hover .department-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.department-children {
|
||||
border-left: 1px dashed #dee2e6;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.department-children .department-node {
|
||||
border-left-width: 2px;
|
||||
}
|
||||
|
||||
.department-children .department-children .department-node {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
/* Department type colors */
|
||||
.department-clinical { color: #dc3545; }
|
||||
.department-administrative { color: #6f42c1; }
|
||||
.department-support { color: #fd7e14; }
|
||||
.department-ancillary { color: #20c997; }
|
||||
.department-executive { color: #0d6efd; }
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.department-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.department-actions .btn-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.department-info small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.department-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.department-loading .spinner-border {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.department-tree-empty {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.department-tree-empty .fa-building {
|
||||
color: #dee2e6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize department tree functionality
|
||||
initializeDepartmentTree();
|
||||
});
|
||||
|
||||
function initializeDepartmentTree() {
|
||||
// Handle toggle clicks
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('toggle-icon')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const icon = e.target;
|
||||
const targetId = icon.getAttribute('data-bs-target');
|
||||
const target = document.querySelector(targetId);
|
||||
|
||||
if (target) {
|
||||
// Toggle icon rotation
|
||||
icon.classList.toggle('expanded');
|
||||
|
||||
// Load children if not already loaded
|
||||
if (!target.hasAttribute('data-loaded')) {
|
||||
loadDepartmentChildren(target, icon.getAttribute('data-department-id'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle department node clicks
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.department-item') && !e.target.closest('.department-actions')) {
|
||||
const departmentItem = e.target.closest('.department-item');
|
||||
const departmentNode = departmentItem.closest('.department-node');
|
||||
|
||||
// Remove active class from all nodes
|
||||
document.querySelectorAll('.department-node.active').forEach(node => {
|
||||
node.classList.remove('active');
|
||||
});
|
||||
|
||||
// Add active class to clicked node
|
||||
departmentNode.classList.add('active');
|
||||
|
||||
// Emit custom event for other components
|
||||
const departmentId = departmentNode.getAttribute('data-department-id');
|
||||
document.dispatchEvent(new CustomEvent('departmentSelected', {
|
||||
detail: { departmentId: departmentId }
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadDepartmentChildren(container, departmentId) {
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="department-loading"><div class="spinner-border spinner-border-sm" role="status"></div>Loading...</div>';
|
||||
|
||||
// Make HTMX request to load children
|
||||
htmx.ajax('GET', `/hr/api/departments/${departmentId}/children/`, {
|
||||
target: container,
|
||||
swap: 'innerHTML'
|
||||
}).then(() => {
|
||||
container.setAttribute('data-loaded', 'true');
|
||||
}).catch(() => {
|
||||
container.innerHTML = '<div class="text-danger p-2"><i class="fas fa-exclamation-triangle"></i> Failed to load children</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function activateDepartment(departmentId) {
|
||||
if (confirm('Are you sure you want to activate this department?')) {
|
||||
htmx.ajax('POST', `/hr/departments/${departmentId}/activate/`, {
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
}).then(() => {
|
||||
// Refresh the tree
|
||||
refreshDepartmentTree();
|
||||
showToast('Department activated successfully', 'success');
|
||||
}).catch(() => {
|
||||
showToast('Failed to activate department', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function deactivateDepartment(departmentId) {
|
||||
if (confirm('Are you sure you want to deactivate this department? This will affect all associated employees and schedules.')) {
|
||||
htmx.ajax('POST', `/hr/departments/${departmentId}/deactivate/`, {
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
}).then(() => {
|
||||
// Refresh the tree
|
||||
refreshDepartmentTree();
|
||||
showToast('Department deactivated successfully', 'warning');
|
||||
}).catch(() => {
|
||||
showToast('Failed to deactivate department', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDepartmentTree() {
|
||||
htmx.ajax('GET', '/hr/departments/tree/', {
|
||||
target: '.department-tree',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
|
||||
function expandAllDepartments() {
|
||||
document.querySelectorAll('.toggle-icon').forEach(icon => {
|
||||
if (!icon.classList.contains('expanded')) {
|
||||
icon.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function collapseAllDepartments() {
|
||||
document.querySelectorAll('.toggle-icon.expanded').forEach(icon => {
|
||||
icon.click();
|
||||
});
|
||||
}
|
||||
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
// Use your existing toast notification system
|
||||
if (typeof HospitalApp !== 'undefined' && HospitalApp.utils && HospitalApp.utils.showToast) {
|
||||
HospitalApp.utils.showToast(message, type);
|
||||
} else {
|
||||
// Fallback to basic alert
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions for external use
|
||||
window.DepartmentTree = {
|
||||
refresh: refreshDepartmentTree,
|
||||
expandAll: expandAllDepartments,
|
||||
collapseAll: collapseAllDepartments,
|
||||
activate: activateDepartment,
|
||||
deactivate: deactivateDepartment
|
||||
};
|
||||
</script>
|
||||
301
hr/templates/hr/departments/department_tree_node.html
Normal file
301
hr/templates/hr/departments/department_tree_node.html
Normal file
@ -0,0 +1,301 @@
|
||||
{% load static %}
|
||||
|
||||
<div class="department-node" data-department-id="{{ department.department_id }}"
|
||||
style="margin-left: {{ level|add:0 }}rem;">
|
||||
<div class="department-item d-flex align-items-center py-2 px-3 border-bottom">
|
||||
<div class="department-toggle me-2" style="width: 20px;">
|
||||
{% if department.sub_departments.exists %}
|
||||
<i class="fas fa-chevron-right toggle-icon"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#dept-{{ department.department_id }}-children"
|
||||
data-department-id="{{ department.department_id }}"
|
||||
aria-expanded="false"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="department-icon me-3">
|
||||
<i class="fas
|
||||
{% if department.department_type == 'CLINICAL' %}fa-stethoscope text-danger
|
||||
{% elif department.department_type == 'ADMINISTRATIVE' %}fa-users-cog text-primary
|
||||
{% elif department.department_type == 'SUPPORT' %}fa-tools text-warning
|
||||
{% elif department.department_type == 'ANCILLARY' %}fa-microscope text-info
|
||||
{% elif department.department_type == 'EXECUTIVE' %}fa-crown text-success
|
||||
{% else %}fa-building text-secondary
|
||||
{% endif %}"></i>
|
||||
</div>
|
||||
|
||||
<div class="department-info flex-grow-1">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-0 department-name">
|
||||
<a href="{% url 'hr:department_detail' pk=department.pk %}"
|
||||
class="text-decoration-none">{{ department.name }}</a>
|
||||
{% if not department.is_active %}
|
||||
<span class="badge bg-secondary ms-2">Inactive</span>
|
||||
{% endif %}
|
||||
{% if department.department_head %}
|
||||
<span class="badge bg-info ms-1" title="Department Head">
|
||||
<i class="fas fa-user-tie"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</h6>
|
||||
<small class="text-muted">
|
||||
<span class="department-type">{{ department.get_department_type_display }}</span>
|
||||
•
|
||||
<span class="employee-count">{{ department.employee_count }} employee{{ department.employee_count|pluralize }}</span>
|
||||
{% if department.department_head %}
|
||||
• Head: {{ department.department_head.get_full_name }}
|
||||
{% endif %}
|
||||
{% if department.cost_center %}
|
||||
• Cost Center: {{ department.cost_center }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="department-actions">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'hr:department_detail' pk=department.pk %}"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="View Details"
|
||||
data-bs-toggle="tooltip">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'hr:department_update' pk=department.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
title="Edit Department"
|
||||
data-bs-toggle="tooltip">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{% if department.is_active %}
|
||||
<button class="btn btn-outline-warning btn-sm"
|
||||
onclick="deactivateDepartment('{{ department.department_id }}')"
|
||||
title="Deactivate Department"
|
||||
data-bs-toggle="tooltip">
|
||||
<i class="fas fa-pause"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-outline-success btn-sm"
|
||||
onclick="activateDepartment('{{ department.department_id }}')"
|
||||
title="Activate Department"
|
||||
data-bs-toggle="tooltip">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<!-- Additional actions dropdown -->
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||||
data-bs-toggle="dropdown" aria-expanded="false" title="More Actions">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'hr:assign_department_head' pk=department.pk %}">
|
||||
<i class="fas fa-user-tie me-2"></i>Assign Head
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'hr:employee_list' %}?department={{ department.pk }}">
|
||||
<i class="fas fa-users me-2"></i>View Employees
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'hr:department_create' %}?parent={{ department.pk }}">
|
||||
<i class="fas fa-plus me-2"></i>Add Sub-Department
|
||||
</a>
|
||||
</li>
|
||||
{% if department.employee_count == 0 and not department.sub_departments.exists %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item text-danger"
|
||||
href="{% url 'hr:department_delete' pk=department.pk %}"
|
||||
onclick="return confirm('Are you sure you want to delete this department?')">
|
||||
<i class="fas fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Department Statistics Bar -->
|
||||
{% if department.authorized_positions > 0 %}
|
||||
<div class="mt-2">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<small class="text-muted">Staffing Level</small>
|
||||
<small class="text-muted">{{ department.employee_count }}/{{ department.authorized_positions }}</small>
|
||||
</div>
|
||||
<div class="progress" style="height: 4px;">
|
||||
{% with staffing_percentage=department.staffing_percentage %}
|
||||
<div class="progress-bar
|
||||
{% if staffing_percentage >= 90 %}bg-success
|
||||
{% elif staffing_percentage >= 70 %}bg-warning
|
||||
{% else %}bg-danger
|
||||
{% endif %}"
|
||||
role="progressbar"
|
||||
style="width: {{ staffing_percentage|floatformat:0 }}%"
|
||||
aria-valuenow="{{ staffing_percentage|floatformat:0 }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Children Container -->
|
||||
{% if department.sub_departments.exists %}
|
||||
<div class="collapse" id="dept-{{ department.department_id }}-children">
|
||||
<div class="department-children ps-4">
|
||||
{% for child_department in department.sub_departments.all %}
|
||||
{% include 'hr/departments/department_tree_node.html' with department=child_department level=level|add:1 %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Add some specific styles for this node -->
|
||||
<style>
|
||||
.department-node[data-department-id="{{ department.department_id }}"] {
|
||||
{% if not department.is_active %}
|
||||
opacity: 0.7;
|
||||
background-color: #f8f9fa;
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
.department-node[data-department-id="{{ department.department_id }}"] .department-icon {
|
||||
{% if department.department_type == 'CLINICAL' %}
|
||||
background-color: #ffeaea;
|
||||
border-color: #ffcdd2;
|
||||
{% elif department.department_type == 'ADMINISTRATIVE' %}
|
||||
background-color: #e8f2ff;
|
||||
border-color: #bbdefb;
|
||||
{% elif department.department_type == 'SUPPORT' %}
|
||||
background-color: #fff8e1;
|
||||
border-color: #ffe082;
|
||||
{% elif department.department_type == 'ANCILLARY' %}
|
||||
background-color: #e0f7fa;
|
||||
border-color: #80deea;
|
||||
{% elif department.department_type == 'EXECUTIVE' %}
|
||||
background-color: #e8f5e8;
|
||||
border-color: #a5d6a7;
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
/* Depth-based left border styling */
|
||||
.department-node[data-department-id="{{ department.department_id }}"] {
|
||||
{% if level == 0 %}
|
||||
border-left-width: 4px;
|
||||
{% elif level == 1 %}
|
||||
border-left-width: 3px;
|
||||
{% elif level == 2 %}
|
||||
border-left-width: 2px;
|
||||
{% else %}
|
||||
border-left-width: 1px;
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
/* Department level specific styling */
|
||||
{% if level > 0 %}
|
||||
.department-node[data-department-id="{{ department.department_id }}"] .department-item {
|
||||
padding-left: {{ level|add:1 }}rem;
|
||||
}
|
||||
|
||||
.department-node[data-department-id="{{ department.department_id }}"] .department-icon {
|
||||
width: {% if level == 1 %}35px{% elif level == 2 %}30px{% else %}25px{% endif %};
|
||||
height: {% if level == 1 %}35px{% elif level == 2 %}30px{% else %}25px{% endif %};
|
||||
}
|
||||
|
||||
.department-node[data-department-id="{{ department.department_id }}"] .department-name {
|
||||
font-size: {% if level == 1 %}0.95rem{% elif level == 2 %}0.9rem{% else %}0.85rem{% endif %};
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Initialize tooltips for this specific department node
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const departmentNode = document.querySelector('[data-department-id="{{ department.department_id }}"]');
|
||||
if (departmentNode) {
|
||||
// Initialize Bootstrap tooltips
|
||||
const tooltipTriggerList = departmentNode.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
|
||||
// Add click handlers for this specific node
|
||||
const toggleIcon = departmentNode.querySelector('.toggle-icon');
|
||||
if (toggleIcon) {
|
||||
toggleIcon.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const target = document.querySelector(this.getAttribute('data-bs-target'));
|
||||
if (target) {
|
||||
// Toggle Bootstrap collapse
|
||||
const bsCollapse = new bootstrap.Collapse(target, {
|
||||
toggle: true
|
||||
});
|
||||
|
||||
// Rotate icon
|
||||
this.classList.toggle('expanded');
|
||||
|
||||
// Track expansion state
|
||||
const isExpanded = target.classList.contains('show') || this.classList.contains('expanded');
|
||||
this.setAttribute('aria-expanded', isExpanded);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Department-specific utility functions
|
||||
window.Department_{{ department.department_id|escapejs }} = {
|
||||
activate: function() {
|
||||
activateDepartment('{{ department.department_id|escapejs }}');
|
||||
},
|
||||
deactivate: function() {
|
||||
deactivateDepartment('{{ department.department_id|escapejs }}');
|
||||
},
|
||||
expand: function() {
|
||||
const node = document.querySelector('[data-department-id="{{ department.department_id|escapejs }}"]');
|
||||
const toggleIcon = node?.querySelector('.toggle-icon');
|
||||
if (toggleIcon && !toggleIcon.classList.contains('expanded')) {
|
||||
toggleIcon.click();
|
||||
}
|
||||
},
|
||||
collapse: function() {
|
||||
const node = document.querySelector('[data-department-id="{{ department.department_id|escapejs }}"]');
|
||||
const toggleIcon = node?.querySelector('.toggle-icon');
|
||||
if (toggleIcon && toggleIcon.classList.contains('expanded')) {
|
||||
toggleIcon.click();
|
||||
}
|
||||
},
|
||||
select: function() {
|
||||
const node = document.querySelector('[data-department-id="{{ department.department_id|escapejs }}"]');
|
||||
if (node) {
|
||||
// Remove active class from all nodes
|
||||
document.querySelectorAll('.department-node.active').forEach(n => n.classList.remove('active'));
|
||||
|
||||
// Add active class to this node
|
||||
node.classList.add('active');
|
||||
|
||||
// Scroll into view if needed
|
||||
node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
|
||||
// Emit selection event
|
||||
document.dispatchEvent(new CustomEvent('departmentSelected', {
|
||||
detail: {
|
||||
departmentId: '{{ department.department_id|escapejs }}',
|
||||
departmentName: '{{ department.name|escapejs }}',
|
||||
departmentType: '{{ department.department_type|escapejs }}'
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -2,7 +2,62 @@
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete Employee - {{ employee.get_full_name }} - {{ block.super }}{% endblock %}
|
||||
{% block css %}
|
||||
<style>
|
||||
.font-monospace {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border: none;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.alert-heading {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
dl.row dt {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
dl.row dd {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.card.border-danger {
|
||||
border-color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.card.border-warning {
|
||||
border-color: #ffc107 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.d-flex.justify-content-between {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
@ -238,7 +293,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Handle data handling radio buttons
|
||||
@ -327,59 +384,6 @@ window.addEventListener('beforeunload', function(e) {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.font-monospace {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border: none;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.alert-heading {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
dl.row dt {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
dl.row dd {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.card.border-danger {
|
||||
border-color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.card.border-warning {
|
||||
border-color: #ffc107 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.d-flex.justify-content-between {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -51,15 +51,7 @@ Delete Performance Review | HR Management
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- begin breadcrumb -->
|
||||
<ol class="breadcrumb float-xl-end">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:performance_review_list' %}">Performance Reviews</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:performance_review_detail' review.id %}">{{ review.employee.get_full_name }}</a></li>
|
||||
<li class="breadcrumb-item active">Delete</li>
|
||||
</ol>
|
||||
<!-- end breadcrumb -->
|
||||
|
||||
|
||||
<!-- begin page-header -->
|
||||
<h1 class="page-header">
|
||||
|
||||
@ -13,17 +13,7 @@
|
||||
<!-- Summernote CSS -->
|
||||
<link href="{% static 'plugins/summernote/dist/summernote-lite.css' %}" rel="stylesheet" />
|
||||
<style>
|
||||
.form-section {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-section-title {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 5px;
|
||||
@ -44,10 +34,7 @@
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.remove-goal {
|
||||
color: #dc3545;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.help-sidebar {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
@ -69,16 +56,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- begin breadcrumb -->
|
||||
<ol class="breadcrumb float-xl-end">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:performance_review_list' %}">Performance Reviews</a></li>
|
||||
<li class="breadcrumb-item active">
|
||||
{% if form.instance.id %}Edit{% else %}Create{% endif %} Review
|
||||
</li>
|
||||
</ol>
|
||||
<!-- end breadcrumb -->
|
||||
|
||||
|
||||
<!-- begin page-header -->
|
||||
<h1 class="page-header">
|
||||
@ -101,6 +79,7 @@
|
||||
<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-close"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end panel-heading -->
|
||||
@ -128,8 +107,9 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Basic Information Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">Basic Information</h5>
|
||||
<div class="card mb-3">
|
||||
<h6 class="card-header">Basic Information</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.employee.id_for_label }}" class="form-label">Employee <span class="text-danger">*</span></label>
|
||||
@ -162,8 +142,11 @@
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="review_period" class="form-label">Review Period <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="review_period" name="review_period">
|
||||
<input type="hidden" id="{{ form.period_start.id_for_label }}" name="{{ form.period_start.html_name }}" value="{{ form.period_start.value|date:'Y-m-d'|default:'' }}">
|
||||
<input type="hidden" id="{{ form.period_end.id_for_label }}" name="{{ form.period_end.html_name }}" value="{{ form.period_end.value|date:'Y-m-d'|default:'' }}">
|
||||
{{ form.period_start }}
|
||||
{{ form.period_end }}
|
||||
<!-- Hidden model fields -->
|
||||
{{ form.review_period_start }}
|
||||
{{ form.review_period_end }}
|
||||
{% if form.period_start.errors or form.period_end.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.period_start.errors }}
|
||||
@ -174,16 +157,19 @@
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.due_date.id_for_label }}" class="form-label">Due Date <span class="text-danger">*</span></label>
|
||||
{{ form.due_date }}
|
||||
{{ form.review_date }}
|
||||
{% if form.due_date.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.due_date.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance Categories Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">Performance Categories</h5>
|
||||
<div class="card mb-3">
|
||||
<h6 class="card-header">Performance Categories</h6>
|
||||
<div class="card-body">
|
||||
<div id="categories-container">
|
||||
<!-- Categories will be added here dynamically -->
|
||||
{% if categories %}
|
||||
@ -226,10 +212,12 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Goals and Achievements Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">Goals and Achievements</h5>
|
||||
<div class="card mb-3">
|
||||
<h6 class="card-header">Goals and Achievements</h6>
|
||||
<div class="card-body">
|
||||
<div id="goals-container">
|
||||
<!-- Goals will be added here dynamically -->
|
||||
{% if goals %}
|
||||
@ -278,10 +266,12 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Strengths and Areas for Improvement Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">Strengths and Areas for Improvement</h5>
|
||||
<div class="card mb-3">
|
||||
<h6 class="card-header">Strengths and Areas for Improvement</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.strengths.id_for_label }}" class="form-label">Strengths</label>
|
||||
@ -301,10 +291,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Development Plan Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">Development Plan</h5>
|
||||
<div class="card mb-3">
|
||||
<h6 class="card-header">Development Plan</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label for="{{ form.development_plan.id_for_label }}" class="form-label">Development Plan</label>
|
||||
@ -315,14 +307,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">Comments</h5>
|
||||
<div class="card mb-3">
|
||||
<h6 class="card-header">Comments</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.reviewer_comments.id_for_label }}" class="form-label">Reviewer Comments</label>
|
||||
{{ form.reviewer_comments }}
|
||||
{{ form.notes }}
|
||||
{% if form.reviewer_comments.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.reviewer_comments.errors }}</div>
|
||||
{% endif %}
|
||||
@ -336,10 +331,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overall Score Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">Overall Score</h5>
|
||||
<div class="card mb-3">
|
||||
<h6 class="card-header">Overall Score</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.overall_score.id_for_label }}" class="form-label">Overall Score</label>
|
||||
@ -347,6 +344,8 @@
|
||||
{{ form.overall_score }}
|
||||
<span class="input-group-text">/5</span>
|
||||
</div>
|
||||
{{ form.overall_rating }}
|
||||
{{ form.competency_ratings }}
|
||||
<div class="form-text">Leave blank to calculate automatically from categories</div>
|
||||
{% if form.overall_score.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.overall_score.errors }}</div>
|
||||
@ -365,10 +364,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attachments Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">Attachments</h5>
|
||||
<div class="card mb-3">
|
||||
<h6 class="card-header">Attachments</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label for="{{ form.attachments.id_for_label }}" class="form-label">Attachments</label>
|
||||
@ -401,6 +402,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Include hidden model fields for goals -->
|
||||
{{ form.goals_achieved }}
|
||||
{{ form.goals_not_achieved }}
|
||||
{{ form.future_goals }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
@ -429,11 +436,20 @@
|
||||
<div class="col-xl-3">
|
||||
<div class="help-sidebar">
|
||||
<!-- Help Card -->
|
||||
<div class="card help-card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Help & Guidelines</h5>
|
||||
<div class="panel panel-inverse">
|
||||
<!-- begin panel-heading -->
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
Help & Guidelines
|
||||
</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-close"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<h6>Performance Review Process</h6>
|
||||
<p>Follow these steps to complete a performance review:</p>
|
||||
<ol>
|
||||
@ -466,11 +482,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Status Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Review Status</h5>
|
||||
<!-- begin panel-heading -->
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
Review Status
|
||||
</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-close"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>The review status determines the workflow:</p>
|
||||
<ul>
|
||||
<li><strong>Draft</strong> - Initial creation, not visible to employee</li>
|
||||
@ -527,6 +552,9 @@
|
||||
$(this).val(picker.startDate.format('YYYY-MM-DD') + ' - ' + picker.endDate.format('YYYY-MM-DD'));
|
||||
$('#{{ form.period_start.id_for_label }}').val(picker.startDate.format('YYYY-MM-DD'));
|
||||
$('#{{ form.period_end.id_for_label }}').val(picker.endDate.format('YYYY-MM-DD'));
|
||||
// Also update the hidden model fields
|
||||
$('#{{ form.review_period_start.id_for_label }}').val(picker.startDate.format('YYYY-MM-DD'));
|
||||
$('#{{ form.review_period_end.id_for_label }}').val(picker.endDate.format('YYYY-MM-DD'));
|
||||
});
|
||||
|
||||
// Handle date range picker cancel event
|
||||
@ -534,6 +562,8 @@
|
||||
$(this).val('');
|
||||
$('#{{ form.period_start.id_for_label }}').val('');
|
||||
$('#{{ form.period_end.id_for_label }}').val('');
|
||||
$('#{{ form.review_period_start.id_for_label }}').val('');
|
||||
$('#{{ form.review_period_end.id_for_label }}').val('');
|
||||
});
|
||||
|
||||
// Initialize DatePicker for due date
|
||||
@ -555,6 +585,8 @@
|
||||
// Handle due date picker apply event
|
||||
$('#{{ form.due_date.id_for_label }}').on('apply.daterangepicker', function(ev, picker) {
|
||||
$(this).val(picker.startDate.format('YYYY-MM-DD'));
|
||||
// Also update the hidden model field
|
||||
$('#{{ form.review_date.id_for_label }}').val(picker.startDate.format('YYYY-MM-DD'));
|
||||
});
|
||||
|
||||
// Initialize Summernote for rich text editors
|
||||
@ -569,6 +601,12 @@
|
||||
]
|
||||
});
|
||||
|
||||
// Update overall score when overall_score field changes
|
||||
$('#{{ form.overall_score.id_for_label }}').on('change', function() {
|
||||
var score = $(this).val();
|
||||
$('#{{ form.overall_rating.id_for_label }}').val(score);
|
||||
});
|
||||
|
||||
// Handle star ratings
|
||||
$('.rating-stars').each(function() {
|
||||
var $stars = $(this);
|
||||
@ -729,4 +767,3 @@
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -67,14 +67,6 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- begin breadcrumb -->
|
||||
<ol class="breadcrumb float-xl-end">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:schedule_list' %}">Schedules</a></li>
|
||||
<li class="breadcrumb-item active">{% if form.instance.id %}Edit{% else %}Create{% endif %} Schedule</li>
|
||||
</ol>
|
||||
<!-- end breadcrumb -->
|
||||
|
||||
<!-- begin page-header -->
|
||||
<h1 class="page-header">{% if form.instance.id %}Edit{% else %}Create{% endif %} Schedule</h1>
|
||||
@ -437,4 +429,3 @@
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -211,43 +211,7 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}{% if department %}&department={{ department }}{% endif %}" aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}{% if department %}&department={{ department }}{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active"><a class="page-link" href="#">{{ num }}</a></li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item"><a class="page-link" href="?page={{ num }}{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}{% if department %}&department={{ department }}{% endif %}">{{ num }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}{% if department %}&department={{ department }}{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search %}&search={{ search }}{% endif %}{% if status %}&status={{ status }}{% endif %}{% if department %}&department={{ department }}{% endif %}" aria-label="Last">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@ -7,11 +7,10 @@
|
||||
|
||||
{% block css %}
|
||||
<!-- DateTimePicker CSS -->
|
||||
<link href="{% static 'plugins/bootstrap-datepicker/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/bootstrap-timepicker/css/bootstrap-timepicker.min.css' %}" rel="stylesheet" />
|
||||
<!-- Select2 CSS -->
|
||||
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme.min.css' %}" rel="stylesheet" />
|
||||
<style>
|
||||
.help-sidebar {
|
||||
background-color: #f8f9fa;
|
||||
@ -363,7 +362,7 @@
|
||||
|
||||
{% block js %}
|
||||
<!-- DateTimePicker JS -->
|
||||
<script src="{% static 'plugins/bootstrap-datepicker/js/bootstrap-datepicker.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/bootstrap-datepicker/js/bootstrap-datepicker.js' %}"></script>
|
||||
<script src="{% static 'plugins/bootstrap-timepicker/js/bootstrap-timepicker.min.js' %}"></script>
|
||||
<!-- Select2 JS -->
|
||||
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||
|
||||
@ -29,15 +29,26 @@
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="h3 mb-0">
|
||||
{% if object %}Edit Training Session{% else %}Schedule Training Session{% endif %}
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-file-edit me-2"></i>{% if object %}Edit Training Session{% else %}Schedule Training Session{% 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>
|
||||
<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 method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
@ -203,7 +214,7 @@
|
||||
Cost Override
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<span class="input-group-text py-1 px-1"><span class="symbol m-0">ê</span></span>
|
||||
{{ form.cost_override }}
|
||||
</div>
|
||||
<div class="form-text">Override program cost for this session</div>
|
||||
|
||||
@ -6,15 +6,9 @@
|
||||
{% block content %}
|
||||
<div id="content" class="app-content">
|
||||
<div class="container-fluid">
|
||||
<ul class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
||||
<li class="breadcrumb-item active">Training Management</li>
|
||||
</ul>
|
||||
|
||||
<div class="row align-items-center mb-3">
|
||||
<div class="col">
|
||||
<h1 class="page-header">Training Management</h1>
|
||||
<h1 class="page-header"><i class="fas fa-graduation-cap me-2"></i>Training<span class="fw-light">Management</span></h1>
|
||||
<p class="text-muted">Employee training records and certification tracking</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
@ -25,71 +19,77 @@
|
||||
</div>
|
||||
|
||||
<!-- Overview -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="mb-0">{{ total_records }}</h4>
|
||||
<p class="mb-0">Total Records</p>
|
||||
</div>
|
||||
<i class="fa fa-database fa-2x ms-3"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="mb-0">{{ completed_trainings }}</h4>
|
||||
<p class="mb-0">Completed</p>
|
||||
</div>
|
||||
<i class="fa fa-check-circle fa-2x ms-3"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-warning text-white">
|
||||
<div class="card-body d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="mb-0">{{ pending_trainings }}</h4>
|
||||
<p class="mb-0">Scheduled / In Progress</p>
|
||||
</div>
|
||||
<i class="fa fa-clock fa-2x ms-3"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-danger text-white">
|
||||
<div class="card-body d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="mb-0">{{ overdue_trainings }}</h4>
|
||||
<p class="mb-0">Expired / Overdue</p>
|
||||
</div>
|
||||
<i class="fa fa-exclamation-triangle fa-2x ms-3"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-3 col-sm-6">
|
||||
<div class="widget widget-stats bg-primary mb-7px">
|
||||
<div class="stats-icon stats-icon-lg"><i class="fa fa-database fa-fw"></i></div>
|
||||
<div class="stats-content">
|
||||
<div class="stats-title">Total Records</div>
|
||||
<div class="stats-number">{{ total_records }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6">
|
||||
<div class="widget widget-stats bg-success mb-7px">
|
||||
<div class="stats-icon stats-icon-lg"><i class="fa fa-check-circle fa-fw"></i></div>
|
||||
<div class="stats-content">
|
||||
<div class="stats-title">Completed</div>
|
||||
<div class="stats-number">{{ completed_trainings }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6">
|
||||
<div class="widget widget-stats bg-warning mb-7px">
|
||||
<div class="stats-icon stats-icon-lg"><i class="fa fa-clock fa-fw"></i></div>
|
||||
<div class="stats-content">
|
||||
<div class="stats-title">Scheduled / In Progress</div>
|
||||
<div class="stats-number">{{ pending_trainings }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6">
|
||||
<div class="widget widget-stats bg-danger mb-7px">
|
||||
<div class="stats-icon stats-icon-lg"><i class="fa fa-exclamation-triangle fa-fw"></i></div>
|
||||
<div class="stats-content">
|
||||
<div class="stats-title">Expired / Overdue</div>
|
||||
<div class="stats-number">{{ overdue_trainings }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs" id="trainingTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="records-tab" data-bs-toggle="tab" data-bs-target="#records" type="button" role="tab">
|
||||
<i class="fa fa-list me-2"></i>Training Records
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="compliance-tab" data-bs-toggle="tab" data-bs-target="#compliance" type="button" role="tab">
|
||||
<i class="fa fa-shield-alt me-2"></i>Compliance
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-graduation-cap me-2"></i>Training<span class="fw-light">Records</span>
|
||||
</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 class="card mb-4" >
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs" id="trainingTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="records-tab" data-bs-toggle="tab" data-bs-target="#records" type="button" role="tab">
|
||||
<i class="fa fa-list me-2"></i>Training Records
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="compliance-tab" data-bs-toggle="tab" data-bs-target="#compliance" type="button" role="tab">
|
||||
<i class="fa fa-shield-alt me-2"></i>Compliance
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="card-body">
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content" id="trainingTabContent">
|
||||
|
||||
<!-- Training Records -->
|
||||
@ -323,6 +323,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -85,16 +85,6 @@
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:training_record_list' %}">Training Records</a></li>
|
||||
<li class="breadcrumb-item active">
|
||||
{% if form.instance.id %}Edit{% else %}Create{% endif %} Record
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="h3 mb-0">
|
||||
<i class="fas fa-graduation-cap text-primary me-2"></i>
|
||||
{% if form.instance.id %}Edit{% else %}Create{% endif %} Training Record
|
||||
@ -130,72 +120,87 @@
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="panel panel-inverse" data-sortable-id="form-stuff-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-graduation-cap me-2"></i>{% if form.instance.id %}Edit{% else %}Create{% endif %} Training Record
|
||||
</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 method="post" id="trainingRecordForm" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Basic Information Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">
|
||||
<div class="card border border-primary mb-3">
|
||||
<h6 class="card-header bg-primary text-white">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.employee.id_for_label }}" class="form-label">
|
||||
Employee <span class="required-field">*</span>
|
||||
</label>
|
||||
{{ form.employee }}
|
||||
{% if form.employee.help_text %}
|
||||
<div class="field-help">{{ form.employee.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.employee.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.employee.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.program.id_for_label }}" class="form-label">
|
||||
Training Program <span class="required-field">*</span>
|
||||
</label>
|
||||
{{ form.program }}
|
||||
{% if form.program.help_text %}
|
||||
<div class="field-help">{{ form.program.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.program.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.program.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.session.id_for_label }}" class="form-label">
|
||||
Training Session
|
||||
</label>
|
||||
{{ form.session }}
|
||||
{% if form.session.help_text %}
|
||||
<div class="field-help">{{ form.session.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.session.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.session.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.status.id_for_label }}" class="form-label">
|
||||
Status <span class="required-field">*</span>
|
||||
</label>
|
||||
{{ form.status }}
|
||||
{% if form.status.help_text %}
|
||||
<div class="field-help">{{ form.status.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.status.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.status.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.employee.id_for_label }}" class="form-label">
|
||||
Employee <span class="required-field">*</span>
|
||||
</label>
|
||||
{{ form.employee }}
|
||||
{% if form.employee.help_text %}
|
||||
<div class="field-help">{{ form.employee.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.employee.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.employee.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.program.id_for_label }}" class="form-label">
|
||||
Training Program <span class="required-field">*</span>
|
||||
</label>
|
||||
{{ form.program }}
|
||||
{% if form.program.help_text %}
|
||||
<div class="field-help">{{ form.program.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.program.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.program.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.session.id_for_label }}" class="form-label">
|
||||
Training Session
|
||||
</label>
|
||||
{{ form.session }}
|
||||
{% if form.session.help_text %}
|
||||
<div class="field-help">{{ form.session.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.session.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.session.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.status.id_for_label }}" class="form-label">
|
||||
Status <span class="required-field">*</span>
|
||||
</label>
|
||||
{{ form.status }}
|
||||
{% if form.status.help_text %}
|
||||
<div class="field-help">{{ form.status.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.status.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.status.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Information Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">
|
||||
<div class="card border border-success mb-3">
|
||||
<h6 class="card-header bg-success text-white">
|
||||
<i class="fas fa-calendar-alt me-2"></i>Schedule Information
|
||||
</h5>
|
||||
</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.started_at.id_for_label }}" class="form-label">
|
||||
@ -221,12 +226,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress & Assessment Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">
|
||||
<div class="card border border-info mb-3">
|
||||
<h6 class="card-header bg-info text-white">
|
||||
<i class="fas fa-chart-line me-2"></i>Progress & Assessment
|
||||
</h5>
|
||||
</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.score.id_for_label }}" class="form-label">
|
||||
@ -284,12 +291,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Section -->
|
||||
<div class="form-section">
|
||||
<h5 class="form-section-title">
|
||||
<div class="card border border-warning mb-3">
|
||||
<h6 class="card-header bg-warning text-white">
|
||||
<i class="fas fa-sticky-note me-2"></i>Additional Notes
|
||||
</h5>
|
||||
</h6>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label for="{{ form.notes.id_for_label }}" class="form-label">
|
||||
@ -305,6 +314,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
@ -325,18 +335,26 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Sidebar -->
|
||||
<div class="col-xl-4">
|
||||
<div class="help-sidebar">
|
||||
<!-- Help Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-question-circle me-2"></i>Help & Guidelines
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel panel-inverse" data-sortable-id="form-stuff-2">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-question-circle me-2"></i>Help & Guidelines
|
||||
</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">
|
||||
<h6 class="text-primary">Training Record Guidelines</h6>
|
||||
<p class="small">Follow these steps to create a comprehensive training record:</p>
|
||||
<ol class="small">
|
||||
@ -375,13 +393,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Tips Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-lightbulb me-2"></i>Tips
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel panel-inverse" data-sortable-id="form-stuff-3">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-lightbulb me-2"></i>Tips
|
||||
</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">
|
||||
<ul class="small mb-0">
|
||||
<li>Select a training session to automatically populate program details</li>
|
||||
<li>Completion date is required for completed training</li>
|
||||
@ -395,13 +419,19 @@
|
||||
|
||||
<!-- Program Info Card -->
|
||||
{% if form.instance.program %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Program Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel panel-inverse" data-sortable-id="form-stuff-4">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-info-circle me-2"></i>Program Information
|
||||
</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">
|
||||
<h6>{{ form.instance.program.name }}</h6>
|
||||
<p class="small text-muted">{{ form.instance.program.description|truncatewords:20 }}</p>
|
||||
<div class="row small">
|
||||
|
||||
11
hr/urls.py
11
hr/urls.py
@ -36,7 +36,8 @@ urlpatterns = [
|
||||
path('departments/bulk-deactivate/', views.bulk_deactivate_departments, name='bulk_deactivate_departments'),
|
||||
path('departments/<int:pk>/assign-head/', views.assign_department_head, name='assign_department_head'),
|
||||
path('api/department-hierarchy/', views.get_department_hierarchy, name='get_department_hierarchy'),
|
||||
path('htmx/department-tree/', views.department_tree, name='department_tree'),
|
||||
path('api/departments/<uuid:department_id>/children/', views.department_children, name='department_children'),
|
||||
path('departments/tree/', views.department_tree, name='department_tree'),
|
||||
path('search/departments/', views.department_search, name='department_search'),
|
||||
|
||||
# ============================================================================
|
||||
@ -46,6 +47,7 @@ urlpatterns = [
|
||||
path('schedules/create/', views.ScheduleCreateView.as_view(), name='schedule_create'),
|
||||
path('schedules/<int:pk>/', views.ScheduleDetailView.as_view(), name='schedule_detail'),
|
||||
path('schedules/<int:pk>/update/', views.ScheduleUpdateView.as_view(), name='schedule_update'),
|
||||
path('schedules/<int:pk>/delete/', views.ScheduleAssignmentDeleteView.as_view(), name='schedule_assignment_delete'),
|
||||
# Note: No delete view for schedules - use status updates instead
|
||||
|
||||
# ============================================================================
|
||||
@ -54,7 +56,9 @@ urlpatterns = [
|
||||
path('assignments/', views.ScheduleAssignmentListView.as_view(), name='schedule_assignment_list'),
|
||||
path('assignments/create/', views.ScheduleAssignmentCreateView.as_view(), name='schedule_assignment_create'),
|
||||
path('assignments/<int:pk>/update/', views.ScheduleAssignmentUpdateView.as_view(), name='schedule_assignment_update'),
|
||||
# Note: No detail/delete views for assignments - managed via schedules
|
||||
path('assignments/<int:pk>/delete/', views.ScheduleAssignmentDeleteView.as_view(), name='schedule_assignment_delete'),
|
||||
path('assignments/export/', views.export_assignments, name='export_assignments'),
|
||||
# Note: No detail views for assignments - managed via schedules
|
||||
|
||||
# ============================================================================
|
||||
# TIME ENTRY URLS (RESTRICTED CRUD - Operational Data)
|
||||
@ -142,6 +146,9 @@ urlpatterns = [
|
||||
# ============================================================================
|
||||
path('ajax/get-program-sessions/', views.get_program_sessions, name='get_program_sessions'),
|
||||
path('ajax/get-program-details/', views.get_program_details, name='get_program_details'),
|
||||
path('ajax/check-time-entry-conflicts/', views.check_time_entry_conflicts, name='check_time_entry_conflicts'),
|
||||
path('ajax/get-employee-schedule-assignments/', views.get_employee_schedule_assignments, name='get_employee_schedule_assignments'),
|
||||
path('ajax/get-schedule-assignment-details/', views.get_schedule_assignment_details, name='get_schedule_assignment_details'),
|
||||
|
||||
# ============================================================================
|
||||
# API ENDPOINTS
|
||||
|
||||
2376
hr/views.py
2376
hr/views.py
File diff suppressed because it is too large
Load Diff
290
hr_data.py
290
hr_data.py
@ -1,4 +1,14 @@
|
||||
# scripts/hr_data_generator.py
|
||||
"""
|
||||
Comprehensive HR Data Generator for Hospital Management System
|
||||
Generates realistic Saudi healthcare HR data including:
|
||||
- Employees with complete profiles
|
||||
- Departments with organizational structure
|
||||
- Work schedules and assignments
|
||||
- Time tracking entries
|
||||
- Performance reviews
|
||||
- Complete training management system
|
||||
"""
|
||||
|
||||
import os
|
||||
import django
|
||||
@ -16,6 +26,7 @@ import random
|
||||
from datetime import datetime, timedelta, date, time
|
||||
from decimal import Decimal
|
||||
from django.utils import timezone as django_timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from hr.models import (
|
||||
Employee, Department, Schedule, ScheduleAssignment, TimeEntry, PerformanceReview,
|
||||
@ -215,122 +226,186 @@ def create_or_update_saudi_employees(tenants, departments_by_tenant, employees_p
|
||||
|
||||
# Create more users if we need more employees
|
||||
to_create = max(0, employees_per_tenant - num_existing)
|
||||
print(f"Creating {to_create} new employees for {tenant.name} (existing: {num_existing})")
|
||||
|
||||
for _ in range(to_create):
|
||||
gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
|
||||
first = random.choice(SAUDI_FIRST_NAMES_MALE if gender == Employee.Gender.MALE else SAUDI_FIRST_NAMES_FEMALE)
|
||||
father = random.choice(SAUDI_FIRST_NAMES_MALE)
|
||||
grandfather = random.choice(SAUDI_FIRST_NAMES_MALE)
|
||||
last = random.choice(SAUDI_LAST_NAMES)
|
||||
base_username = f"{first.lower()}.{last.lower().replace('-', '').replace('al', '')}"
|
||||
username = tenant_scoped_unique_username(tenant, base_username)
|
||||
email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa"
|
||||
for i in range(to_create):
|
||||
try:
|
||||
gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
|
||||
first = random.choice(SAUDI_FIRST_NAMES_MALE if gender == Employee.Gender.MALE else SAUDI_FIRST_NAMES_FEMALE)
|
||||
father = random.choice(SAUDI_FIRST_NAMES_MALE)
|
||||
grandfather = random.choice(SAUDI_FIRST_NAMES_MALE)
|
||||
last = random.choice(SAUDI_LAST_NAMES)
|
||||
base_username = f"{first.lower()}.{last.lower().replace('-', '').replace('al', '')}"
|
||||
username = tenant_scoped_unique_username(tenant, base_username)
|
||||
email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa"
|
||||
|
||||
u = User.objects.create(
|
||||
tenant=tenant,
|
||||
username=username,
|
||||
email=email,
|
||||
first_name=first,
|
||||
# father_name=father,
|
||||
# grandfather_name=grandfather,
|
||||
last_name=last,
|
||||
is_active=True,
|
||||
)
|
||||
u.set_password('Hospital@123')
|
||||
u.save()
|
||||
tenant_users.append(u) # signal creates Employee
|
||||
|
||||
# Now (re)populate employee HR data
|
||||
for u in tenant_users:
|
||||
emp = getattr(u, 'employee_profile', None)
|
||||
if not emp:
|
||||
u = User.objects.create(
|
||||
tenant=tenant,
|
||||
username=username,
|
||||
email=email,
|
||||
first_name=first,
|
||||
last_name=last,
|
||||
is_active=True,
|
||||
)
|
||||
u.set_password('Hospital@123')
|
||||
u.save()
|
||||
tenant_users.append(u) # signal creates Employee
|
||||
|
||||
if (i + 1) % 50 == 0:
|
||||
print(f" Created {i + 1}/{to_create} users for {tenant.name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating user {i+1} for {tenant.name}: {e}")
|
||||
continue
|
||||
|
||||
# Basic personal
|
||||
if not emp.first_name:
|
||||
emp.first_name = u.first_name or emp.first_name
|
||||
if not emp.last_name:
|
||||
emp.last_name = u.last_name or emp.last_name
|
||||
if not emp.email:
|
||||
emp.email = u.email
|
||||
# Now (re)populate employee HR data
|
||||
print(f"Updating employee profiles for {tenant.name}...")
|
||||
for i, u in enumerate(tenant_users):
|
||||
try:
|
||||
emp = getattr(u, 'employee_profile', None)
|
||||
if not emp:
|
||||
continue
|
||||
|
||||
# Demographics
|
||||
if not emp.gender:
|
||||
emp.gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE, Employee.Gender.OTHER])
|
||||
if not emp.marital_status:
|
||||
emp.marital_status = random.choice([
|
||||
Employee.MaritalStatus.SINGLE, Employee.MaritalStatus.MARRIED,
|
||||
Employee.MaritalStatus.DIVORCED, Employee.MaritalStatus.WIDOWED,
|
||||
Employee.MaritalStatus.SEPARATED, Employee.MaritalStatus.OTHER
|
||||
])
|
||||
if not emp.date_of_birth:
|
||||
# 22–55 years old
|
||||
emp.date_of_birth = (django_timezone.now().date()
|
||||
- timedelta(days=random.randint(22*365, 55*365)))
|
||||
# Basic personal
|
||||
if not emp.first_name:
|
||||
emp.first_name = u.first_name or emp.first_name
|
||||
if not emp.last_name:
|
||||
emp.last_name = u.last_name or emp.last_name
|
||||
if not emp.email:
|
||||
emp.email = u.email
|
||||
|
||||
# Contact E.164 (both are mobiles by model design)
|
||||
if not emp.phone:
|
||||
emp.phone = e164_ksa_mobile()
|
||||
if not emp.mobile_phone:
|
||||
emp.mobile_phone = e164_ksa_mobile()
|
||||
# Add father and grandfather names
|
||||
if not emp.father_name:
|
||||
emp.father_name = random.choice(SAUDI_FIRST_NAMES_MALE)
|
||||
if not emp.grandfather_name:
|
||||
emp.grandfather_name = random.choice(SAUDI_FIRST_NAMES_MALE)
|
||||
|
||||
# Address
|
||||
if not emp.address_line_1:
|
||||
emp.address_line_1 = f"{random.randint(1, 999)} {random.choice(['King Fahd Rd', 'Prince Sultan St', 'Olaya St'])}"
|
||||
if not emp.city:
|
||||
emp.city = random.choice(SAUDI_CITIES)
|
||||
if not emp.postal_code:
|
||||
emp.postal_code = f"{random.randint(10000, 99999)}"
|
||||
if not emp.country:
|
||||
emp.country = 'Saudi Arabia'
|
||||
# ID information
|
||||
if not emp.identification_number:
|
||||
emp.identification_number = f"{random.randint(1000000000, 9999999999)}"
|
||||
emp.id_type = random.choice([Employee.IdNumberTypes.NATIONAL_ID, Employee.IdNumberTypes.IQAMA])
|
||||
|
||||
# Org
|
||||
if not emp.department:
|
||||
emp.department = random.choice(depts)
|
||||
if not emp.job_title:
|
||||
emp.job_title = pick_job_title_for_department(emp.department)
|
||||
# Demographics
|
||||
if not emp.gender:
|
||||
emp.gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
|
||||
if not emp.marital_status:
|
||||
emp.marital_status = random.choice([
|
||||
Employee.MaritalStatus.SINGLE, Employee.MaritalStatus.MARRIED,
|
||||
Employee.MaritalStatus.DIVORCED, Employee.MaritalStatus.WIDOWED,
|
||||
Employee.MaritalStatus.SEPARATED
|
||||
])
|
||||
if not emp.date_of_birth:
|
||||
# 22–55 years old
|
||||
emp.date_of_birth = (django_timezone.now().date()
|
||||
- timedelta(days=random.randint(22*365, 55*365)))
|
||||
|
||||
# Role (derive from job title if not set)
|
||||
if not emp.role or emp.role == Employee.Role.GUEST:
|
||||
emp.role = infer_role_from_title(emp.job_title)
|
||||
# Contact E.164 (both are mobiles by model design)
|
||||
if not emp.phone:
|
||||
emp.phone = e164_ksa_mobile()
|
||||
if not emp.mobile_phone:
|
||||
emp.mobile_phone = e164_ksa_mobile()
|
||||
|
||||
# Employment
|
||||
if not emp.employment_type:
|
||||
emp.employment_type = random.choice([
|
||||
Employee.EmploymentType.FULL_TIME, Employee.EmploymentType.PART_TIME,
|
||||
Employee.EmploymentType.CONTRACT, Employee.EmploymentType.TEMPORARY,
|
||||
Employee.EmploymentType.INTERN, Employee.EmploymentType.VOLUNTEER,
|
||||
Employee.EmploymentType.PER_DIEM, Employee.EmploymentType.CONSULTANT
|
||||
])
|
||||
if not emp.hire_date:
|
||||
emp.hire_date = django_timezone.now().date() - timedelta(days=random.randint(30, 2000))
|
||||
if not emp.employment_status:
|
||||
emp.employment_status = random.choices(
|
||||
[Employee.EmploymentStatus.ACTIVE, Employee.EmploymentStatus.INACTIVE, Employee.EmploymentStatus.LEAVE],
|
||||
weights=[85, 10, 5]
|
||||
)[0]
|
||||
# If terminated, set a termination_date after hire_date
|
||||
if emp.employment_status == Employee.EmploymentStatus.TERMINATED and not emp.termination_date:
|
||||
emp.termination_date = emp.hire_date + timedelta(days=random.randint(30, 1000))
|
||||
# Address
|
||||
if not emp.address_line_1:
|
||||
emp.address_line_1 = f"{random.randint(1, 999)} {random.choice(['King Fahd Rd', 'Prince Sultan St', 'Olaya St', 'King Abdul Aziz Rd', 'Tahlia St'])}"
|
||||
if not emp.address_line_2:
|
||||
if random.choice([True, False]):
|
||||
emp.address_line_2 = f"Apt {random.randint(1, 50)}"
|
||||
if not emp.city:
|
||||
emp.city = random.choice(SAUDI_CITIES)
|
||||
if not emp.postal_code:
|
||||
emp.postal_code = f"{random.randint(10000, 99999)}"
|
||||
if not emp.country:
|
||||
emp.country = 'Saudi Arabia'
|
||||
|
||||
# Licensure (optional by role/title)
|
||||
jt_lower = (emp.job_title or '').lower()
|
||||
if not emp.license_number and any(k in jt_lower for k in ['physician', 'nurse', 'pharmacist', 'radiolog']):
|
||||
emp.license_number = f"LIC-{random.randint(100000, 999999)}"
|
||||
emp.license_expiry_date = django_timezone.now().date() + timedelta(days=random.randint(180, 1095))
|
||||
emp.license_state = random.choice(['Riyadh Province', 'Makkah Province', 'Eastern Province', 'Asir Province'])
|
||||
if 'physician' in jt_lower and not emp.npi_number:
|
||||
emp.npi_number = f"SA{random.randint(1000000, 9999999)}"
|
||||
# Org
|
||||
if not emp.department:
|
||||
emp.department = random.choice(depts)
|
||||
if not emp.job_title:
|
||||
emp.job_title = pick_job_title_for_department(emp.department)
|
||||
|
||||
# Preferences
|
||||
emp.user_timezone = 'Asia/Riyadh'
|
||||
if not emp.language:
|
||||
emp.language = random.choice(['ar', 'en', 'ar_SA'])
|
||||
if not emp.theme:
|
||||
emp.theme = random.choice([Employee.Theme.LIGHT, Employee.Theme.DARK, Employee.Theme.AUTO])
|
||||
# Role (derive from job title if not set)
|
||||
if not emp.role or emp.role == Employee.Role.GUEST:
|
||||
emp.role = infer_role_from_title(emp.job_title)
|
||||
|
||||
emp.save()
|
||||
all_employees.append(emp)
|
||||
# Employment
|
||||
if not emp.employment_type:
|
||||
emp.employment_type = random.choices([
|
||||
Employee.EmploymentType.FULL_TIME, Employee.EmploymentType.PART_TIME,
|
||||
Employee.EmploymentType.CONTRACT, Employee.EmploymentType.TEMPORARY,
|
||||
Employee.EmploymentType.INTERN, Employee.EmploymentType.CONSULTANT
|
||||
], weights=[70, 15, 8, 4, 2, 1])[0]
|
||||
|
||||
if not emp.hire_date:
|
||||
emp.hire_date = django_timezone.now().date() - timedelta(days=random.randint(30, 2000))
|
||||
|
||||
if not emp.employment_status:
|
||||
emp.employment_status = random.choices(
|
||||
[Employee.EmploymentStatus.ACTIVE, Employee.EmploymentStatus.INACTIVE, Employee.EmploymentStatus.LEAVE],
|
||||
weights=[85, 10, 5]
|
||||
)[0]
|
||||
|
||||
# If terminated, set a termination_date after hire_date
|
||||
if emp.employment_status == Employee.EmploymentStatus.TERMINATED and not emp.termination_date:
|
||||
emp.termination_date = emp.hire_date + timedelta(days=random.randint(30, 1000))
|
||||
|
||||
# Compensation
|
||||
if not emp.hourly_rate and emp.employment_type in [Employee.EmploymentType.PART_TIME, Employee.EmploymentType.PER_DIEM]:
|
||||
emp.hourly_rate = Decimal(str(random.randint(50, 300)))
|
||||
if not emp.annual_salary and emp.employment_type == Employee.EmploymentType.FULL_TIME:
|
||||
emp.annual_salary = Decimal(str(random.randint(60000, 500000)))
|
||||
if not emp.fte_percentage:
|
||||
if emp.employment_type == Employee.EmploymentType.PART_TIME:
|
||||
emp.fte_percentage = Decimal(str(random.choice([25, 50, 75])))
|
||||
else:
|
||||
emp.fte_percentage = Decimal('100.00')
|
||||
|
||||
# Licensure (optional by role/title)
|
||||
jt_lower = (emp.job_title or '').lower()
|
||||
if not emp.license_number and any(k in jt_lower for k in ['physician', 'nurse', 'pharmacist', 'radiolog']):
|
||||
emp.license_number = f"LIC-{random.randint(100000, 999999)}"
|
||||
emp.license_expiry_date = django_timezone.now().date() + timedelta(days=random.randint(180, 1095))
|
||||
emp.license_state = random.choice(['Riyadh Province', 'Makkah Province', 'Eastern Province', 'Asir Province'])
|
||||
if 'physician' in jt_lower and not emp.npi_number:
|
||||
emp.npi_number = f"SA{random.randint(1000000, 9999999)}"
|
||||
|
||||
# Emergency contact
|
||||
if not emp.emergency_contact_name:
|
||||
contact_gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
|
||||
contact_first = random.choice(SAUDI_FIRST_NAMES_MALE if contact_gender == Employee.Gender.MALE else SAUDI_FIRST_NAMES_FEMALE)
|
||||
contact_last = random.choice(SAUDI_LAST_NAMES)
|
||||
emp.emergency_contact_name = f"{contact_first} {contact_last}"
|
||||
emp.emergency_contact_relationship = random.choice(['Spouse', 'Parent', 'Sibling', 'Child', 'Friend'])
|
||||
emp.emergency_contact_phone = e164_ksa_mobile()
|
||||
|
||||
# Bio for senior staff
|
||||
if any(k in jt_lower for k in ['chief', 'director', 'manager', 'senior', 'consultant']):
|
||||
if not emp.bio:
|
||||
years_exp = random.randint(5, 25)
|
||||
emp.bio = f"Experienced healthcare professional with {years_exp} years in {emp.department.name if emp.department else 'healthcare'}. Specialized in {random.choice(['patient care', 'clinical excellence', 'team leadership', 'quality improvement'])}."
|
||||
|
||||
# Preferences
|
||||
emp.user_timezone = 'Asia/Riyadh'
|
||||
if not emp.language:
|
||||
emp.language = random.choices(['ar', 'en'], weights=[70, 30])[0]
|
||||
if not emp.theme:
|
||||
emp.theme = random.choice([Employee.Theme.LIGHT, Employee.Theme.DARK, Employee.Theme.AUTO])
|
||||
|
||||
# Approval status for active employees
|
||||
if emp.employment_status == Employee.EmploymentStatus.ACTIVE:
|
||||
emp.is_verified = True
|
||||
emp.is_approved = True
|
||||
emp.approval_date = emp.hire_date + timedelta(days=random.randint(1, 30))
|
||||
|
||||
emp.save()
|
||||
all_employees.append(emp)
|
||||
|
||||
if (i + 1) % 100 == 0:
|
||||
print(f" Updated {i + 1}/{len(tenant_users)} employee profiles for {tenant.name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating employee profile for user {u.username}: {e}")
|
||||
continue
|
||||
|
||||
print(f"Employees ready for {tenant.name}: {len([e for e in all_employees if e.tenant == tenant])}")
|
||||
|
||||
@ -1148,7 +1223,8 @@ def create_training_certificates(training_records, employees):
|
||||
|
||||
# Select signer from same tenant
|
||||
tenant_signers = [s for s in potential_signers if s.tenant == record.employee.tenant]
|
||||
signer = random.choice(tenant_signers) if tenant_signers else None
|
||||
signer_employee = random.choice(tenant_signers) if tenant_signers else None
|
||||
signer_user = signer_employee.user if signer_employee else None
|
||||
|
||||
try:
|
||||
certificate = TrainingCertificates.objects.create(
|
||||
@ -1159,13 +1235,17 @@ def create_training_certificates(training_records, employees):
|
||||
certificate_number=certificate_number,
|
||||
certification_body=certification_body,
|
||||
expiry_date=expiry_date,
|
||||
signed_by=signer,
|
||||
signed_by=signer_user,
|
||||
created_by=signer_user,
|
||||
created_at=django_timezone.now() - timedelta(days=random.randint(0, 7)),
|
||||
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 3))
|
||||
)
|
||||
certificates.append(certificate)
|
||||
print(f"Created certificate {certificate_number} for {record.employee.get_full_name()}")
|
||||
except Exception as e:
|
||||
print(f"Error creating certificate for {record.employee.get_full_name()}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print(f"Created {len(certificates)} training certificates")
|
||||
return certificates
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,429 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-26 18:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("core", "0001_initial"),
|
||||
("facility_management", "0001_initial"),
|
||||
("inpatients", "0001_initial"),
|
||||
("operating_theatre", "0001_initial"),
|
||||
("patients", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="surgeryschedule",
|
||||
name="operating_room",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Operating room assignment",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="surgery_operations",
|
||||
to="operating_theatre.operatingroom",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="surgeryschedule",
|
||||
name="patient",
|
||||
field=models.ForeignKey(
|
||||
help_text="Patient undergoing surgery",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="surgeries",
|
||||
to="patients.patientprofile",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="surgeryschedule",
|
||||
name="primary_surgeon",
|
||||
field=models.ForeignKey(
|
||||
help_text="Primary surgeon",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="primary_surgeries",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="surgeryschedule",
|
||||
name="scrub_nurse",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Scrub nurse",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="inpatient_scrub_cases",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="surgeryschedule",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="surgery_schedules",
|
||||
to="core.tenant",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transfer",
|
||||
name="admission",
|
||||
field=models.ForeignKey(
|
||||
help_text="Associated admission",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="transfers",
|
||||
to="inpatients.admission",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transfer",
|
||||
name="approved_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Staff member who approved transfer",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="approved_transfers",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transfer",
|
||||
name="completed_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Staff member who completed transfer",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="completed_transfers",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transfer",
|
||||
name="from_bed",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Source bed",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="transfers_from",
|
||||
to="inpatients.bed",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transfer",
|
||||
name="patient",
|
||||
field=models.ForeignKey(
|
||||
help_text="Patient being transferred",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="transfers",
|
||||
to="patients.patientprofile",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transfer",
|
||||
name="requested_by",
|
||||
field=models.ForeignKey(
|
||||
help_text="Staff member who requested transfer",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="requested_transfers",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transfer",
|
||||
name="to_bed",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Destination bed",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="transfers_to",
|
||||
to="inpatients.bed",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transfer",
|
||||
name="transport_team",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Transport team members",
|
||||
related_name="transport_assignments",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ward",
|
||||
name="attending_physicians",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Attending physicians for this ward",
|
||||
related_name="attending_wards",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ward",
|
||||
name="building",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wards",
|
||||
to="facility_management.building",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ward",
|
||||
name="created_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="User who created the ward",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="created_wards",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ward",
|
||||
name="floor",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wards_floor",
|
||||
to="facility_management.floor",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ward",
|
||||
name="nurse_manager",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Nurse manager for this ward",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="managed_wards",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ward",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wards",
|
||||
to="core.tenant",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transfer",
|
||||
name="from_ward",
|
||||
field=models.ForeignKey(
|
||||
help_text="Source ward",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="transfers_from",
|
||||
to="inpatients.ward",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transfer",
|
||||
name="to_ward",
|
||||
field=models.ForeignKey(
|
||||
help_text="Destination ward",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="transfers_to",
|
||||
to="inpatients.ward",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="bed",
|
||||
name="ward",
|
||||
field=models.ForeignKey(
|
||||
help_text="Ward containing this bed",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="beds",
|
||||
to="inpatients.ward",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="admission",
|
||||
name="current_ward",
|
||||
field=models.ForeignKey(
|
||||
help_text="Current ward assignment",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="current_admissions",
|
||||
to="inpatients.ward",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="dischargesummary",
|
||||
index=models.Index(
|
||||
fields=["admission"], name="inpatients__admissi_0ccfc4_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="dischargesummary",
|
||||
index=models.Index(
|
||||
fields=["discharge_date"], name="inpatients__dischar_275061_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="dischargesummary",
|
||||
index=models.Index(
|
||||
fields=["discharging_physician"], name="inpatients__dischar_8244b8_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="surgeryschedule",
|
||||
index=models.Index(
|
||||
fields=["tenant", "status"], name="inpatients__tenant__ba70e0_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="surgeryschedule",
|
||||
index=models.Index(
|
||||
fields=["patient"], name="inpatients__patient_7254b1_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="surgeryschedule",
|
||||
index=models.Index(
|
||||
fields=["admission"], name="inpatients__admissi_cb9ef2_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="surgeryschedule",
|
||||
index=models.Index(
|
||||
fields=["scheduled_date"], name="inpatients__schedul_43b664_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="surgeryschedule",
|
||||
index=models.Index(
|
||||
fields=["primary_surgeon"], name="inpatients__primary_5238a2_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="surgeryschedule",
|
||||
index=models.Index(
|
||||
fields=["operating_room"], name="inpatients__operati_dd9f2e_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="ward",
|
||||
index=models.Index(
|
||||
fields=["tenant", "ward_type"], name="inpatients__tenant__338f37_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="ward",
|
||||
index=models.Index(
|
||||
fields=["specialty"], name="inpatients__special_149fd4_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="ward",
|
||||
index=models.Index(
|
||||
fields=["is_active", "is_accepting_admissions"],
|
||||
name="inpatients__is_acti_7dc57f_idx",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="ward",
|
||||
unique_together={("tenant", "ward_id")},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="transfer",
|
||||
index=models.Index(
|
||||
fields=["admission", "status"], name="inpatients__admissi_08e4ca_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="transfer",
|
||||
index=models.Index(
|
||||
fields=["patient"], name="inpatients__patient_9e7d37_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="transfer",
|
||||
index=models.Index(
|
||||
fields=["from_ward", "to_ward"], name="inpatients__from_wa_466f5f_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="transfer",
|
||||
index=models.Index(
|
||||
fields=["requested_datetime"], name="inpatients__request_2b3b18_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="transfer",
|
||||
index=models.Index(
|
||||
fields=["priority"], name="inpatients__priorit_4b9e10_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="bed",
|
||||
index=models.Index(
|
||||
fields=["ward", "status"], name="inpatients__ward_id_87bbcc_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="bed",
|
||||
index=models.Index(
|
||||
fields=["current_admission"], name="inpatients__current_46ec0b_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="bed",
|
||||
index=models.Index(
|
||||
fields=["bed_type", "room_type"], name="inpatients__bed_typ_7caad7_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="bed",
|
||||
index=models.Index(fields=["status"], name="inpatients__status_e98d75_idx"),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="bed",
|
||||
unique_together={("ward", "bed_number")},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="admission",
|
||||
index=models.Index(
|
||||
fields=["tenant", "status"], name="inpatients__tenant__71213b_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="admission",
|
||||
index=models.Index(
|
||||
fields=["patient", "status"], name="inpatients__patient_87f767_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="admission",
|
||||
index=models.Index(
|
||||
fields=["current_ward"], name="inpatients__current_8e363a_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="admission",
|
||||
index=models.Index(
|
||||
fields=["admission_datetime"], name="inpatients__admissi_632f0b_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="admission",
|
||||
index=models.Index(
|
||||
fields=["attending_physician"], name="inpatients__attendi_19fb85_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@ -7,7 +7,7 @@
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<div>
|
||||
<h1 class="h2">
|
||||
<i class="fas fa-procedures me-2"></i></i>Surgeries<span class="fw-light">Schedule</span>
|
||||
<i class="fas fa-procedures me-2"></i>Surgeries<span class="fw-light">Schedule</span>
|
||||
</h1>
|
||||
<p class="text-muted">Comprehensive discharge planning and coordination.</p>
|
||||
</div>
|
||||
|
||||
@ -78,10 +78,11 @@ urlpatterns = [
|
||||
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.mark_surgery_completed, name='complete_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
|
||||
|
||||
|
||||
@ -85,6 +85,7 @@ class InpatientDashboardView(LoginRequiredMixin, ListView):
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class WardListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
List view for wards.
|
||||
@ -970,10 +971,10 @@ class SurgeryScheduleListView(LoginRequiredMixin, ListView):
|
||||
tenant = self.request.user.tenant
|
||||
|
||||
# Get statuses for filter dropdown
|
||||
context['statuses'] = SurgerySchedule.STATUS_CHOICES
|
||||
context['statuses'] = SurgerySchedule.SurgeryStatus.choices
|
||||
|
||||
# Get priorities for filter dropdown
|
||||
context['priorities'] = SurgerySchedule.PRIORITY_CHOICES
|
||||
context['priorities'] = SurgerySchedule.SurgeryPriority.choices
|
||||
|
||||
# Get surgeons for filter dropdown
|
||||
context['surgeons'] = User.objects.filter(
|
||||
@ -2167,7 +2168,7 @@ def clean_bed(request, pk):
|
||||
|
||||
return render(request, 'inpatients/clean_bed.html', {
|
||||
'bed': bed,
|
||||
'cleaning_levels': Bed.CLEANING_LEVEL_CHOICES,
|
||||
'cleaning_levels': Bed.CleaningLevel.choices,
|
||||
'next': request.GET.get('next', reverse('inpatients:bed_detail', kwargs={'pk': bed.pk}))
|
||||
})
|
||||
|
||||
@ -3178,13 +3179,13 @@ class TransferListView(LoginRequiredMixin, ListView):
|
||||
tenant = self.request.user.tenant
|
||||
|
||||
# Get statuses for filter dropdown
|
||||
context['statuses'] = Transfer.STATUS_CHOICES
|
||||
context['statuses'] = Transfer.TransferStatus.choices
|
||||
|
||||
# Get transfer types for filter dropdown
|
||||
context['transfer_types'] = Transfer.TRANSFER_TYPE_CHOICES
|
||||
context['transfer_types'] = Transfer.TransferType.choices
|
||||
|
||||
# Get priorities for filter dropdown
|
||||
context['priorities'] = Transfer.PRIORITY_CHOICES
|
||||
context['priorities'] = Transfer.TransferPriority.choices
|
||||
|
||||
# Get wards for filter dropdown
|
||||
context['wards'] = Ward.objects.filter(
|
||||
@ -3301,9 +3302,6 @@ class TransferUpdateView(LoginRequiredMixin, UpdateView):
|
||||
return reverse('inpatients:transfer_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
# @permission_required('inpatients.change_surgeryschedule')
|
||||
def mark_surgery_completed(request, pk):
|
||||
@ -3508,6 +3506,95 @@ def postpone_surgery(request, pk):
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
# @permission_required('inpatients.change_surgeryschedule')
|
||||
def start_surgery(request, pk):
|
||||
"""
|
||||
Start a surgery (change status to IN_PROGRESS).
|
||||
"""
|
||||
surgery = get_object_or_404(
|
||||
SurgerySchedule,
|
||||
pk=pk,
|
||||
admission__tenant=request.user.tenant
|
||||
)
|
||||
|
||||
# Only surgeries in PREP status can be started
|
||||
if surgery.status != 'PREP':
|
||||
messages.error(request, _('Only surgeries in prep status can be started'))
|
||||
return redirect('inpatients:surgery_detail', pk=surgery.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
surgery.status = 'IN_PROGRESS'
|
||||
surgery.actual_start_time = timezone.now()
|
||||
surgery.save()
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_event(
|
||||
actor=request.user,
|
||||
action='SURGERY_STARTED',
|
||||
target=surgery,
|
||||
target_repr=str(surgery),
|
||||
description=f"Surgery started for {surgery.patient.get_full_name()}"
|
||||
)
|
||||
|
||||
messages.success(request, _('Surgery started successfully'))
|
||||
return redirect('inpatients:surgery_schedule')
|
||||
|
||||
return render(request, 'inpatients/surgeries/surgery_schedule.html', {
|
||||
'surgery': surgery
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
# @permission_required('inpatients.change_surgeryschedule')
|
||||
def complete_surgery(request, pk):
|
||||
"""
|
||||
Complete a surgery.
|
||||
"""
|
||||
surgery = get_object_or_404(
|
||||
SurgerySchedule,
|
||||
pk=pk,
|
||||
admission__tenant=request.user.tenant
|
||||
)
|
||||
|
||||
# Only surgeries in IN_PROGRESS status can be completed
|
||||
if surgery.status != 'IN_PROGRESS':
|
||||
messages.error(request, _('Only surgeries in progress can be completed'))
|
||||
return redirect('inpatients:surgery_detail', pk=surgery.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
notes = request.POST.get('notes', '')
|
||||
|
||||
surgery.status = 'COMPLETED'
|
||||
surgery.actual_end_time = timezone.now()
|
||||
|
||||
if notes:
|
||||
surgery.notes = (surgery.notes or "") + f"\n\nCompletion Notes ({timezone.now().strftime('%Y-%m-%d %H:%M')}):\n{notes}"
|
||||
|
||||
# Calculate actual duration
|
||||
if surgery.actual_start_time:
|
||||
duration = surgery.actual_end_time - surgery.actual_start_time
|
||||
surgery.actual_duration_minutes = int(duration.total_seconds() / 60)
|
||||
|
||||
surgery.save()
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_event(
|
||||
actor=request.user,
|
||||
action='SURGERY_COMPLETED',
|
||||
target=surgery,
|
||||
target_repr=str(surgery),
|
||||
description=f"Surgery completed for {surgery.patient.get_full_name()}"
|
||||
)
|
||||
|
||||
messages.success(request, _('Surgery completed successfully'))
|
||||
return redirect('inpatients:surgery_schedule')
|
||||
|
||||
return render(request, 'inpatients/surgeries/surgery_schedule.html', {
|
||||
'surgery': surgery
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def inpatient_stats(request):
|
||||
"""
|
||||
|
||||
394
insurance_approvals/README.md
Normal file
394
insurance_approvals/README.md
Normal file
@ -0,0 +1,394 @@
|
||||
# Insurance Approval Request Module
|
||||
|
||||
## Overview
|
||||
|
||||
The Insurance Approval Request Module is a comprehensive system for managing insurance pre-authorization and approval requests for any type of medical order in the hospital management system. It provides a universal approval workflow that works with Lab Orders, Radiology Orders, Prescriptions, and any other order type.
|
||||
|
||||
## Features
|
||||
|
||||
### ✅ Core Functionality
|
||||
|
||||
- **Universal Order Support**: Works with any order type using Django's GenericForeignKey
|
||||
- **Complete Workflow**: 13 status states from Draft to Approved/Denied/Appealed
|
||||
- **Priority Management**: Routine, Urgent, STAT, Emergency levels
|
||||
- **Document Management**: Upload and track supporting documents
|
||||
- **Communication Tracking**: Log all interactions with insurance companies
|
||||
- **Template System**: Reusable templates for common approval requests
|
||||
- **Audit Trail**: Complete history of all status changes
|
||||
- **Multi-tenant**: Full tenant isolation and scoping
|
||||
- **Expiration Tracking**: Automatic warnings for expiring approvals
|
||||
|
||||
### 📊 Status Workflow
|
||||
|
||||
```
|
||||
DRAFT → PENDING_SUBMISSION → SUBMITTED → UNDER_REVIEW
|
||||
↓
|
||||
┌─────────┴─────────┐
|
||||
↓ ↓
|
||||
APPROVED DENIED
|
||||
↓ ↓
|
||||
PARTIALLY_APPROVED APPEAL_SUBMITTED
|
||||
↓ ↓
|
||||
EXPIRED APPEAL_APPROVED
|
||||
↓
|
||||
APPEAL_DENIED
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. App is Already Configured
|
||||
|
||||
The app is already added to `INSTALLED_APPS` in `hospital_management/settings.py`:
|
||||
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
# ... other apps ...
|
||||
'insurance_approvals',
|
||||
]
|
||||
```
|
||||
|
||||
### 2. Create Database Tables
|
||||
|
||||
Run the table creation script:
|
||||
|
||||
```bash
|
||||
python3 create_insurance_tables.py
|
||||
```
|
||||
|
||||
This will create 5 tables:
|
||||
- `insurance_approvals_request`
|
||||
- `insurance_approvals_document`
|
||||
- `insurance_approvals_status_history`
|
||||
- `insurance_approvals_communication_log`
|
||||
- `insurance_approvals_template`
|
||||
|
||||
### 3. Add URLs to Main Project
|
||||
|
||||
Add to `hospital_management/urls.py`:
|
||||
|
||||
```python
|
||||
urlpatterns = [
|
||||
# ... other patterns ...
|
||||
path('insurance-approvals/', include('insurance_approvals.urls')),
|
||||
]
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating an Approval Request from a Lab Order
|
||||
|
||||
```python
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
# Get the lab order
|
||||
lab_order = LabOrder.objects.get(pk=order_id)
|
||||
|
||||
# Create approval request
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=lab_order.tenant,
|
||||
patient=lab_order.patient,
|
||||
insurance_info=lab_order.patient.insurance_info.first(),
|
||||
content_type=ContentType.objects.get_for_model(lab_order),
|
||||
object_id=lab_order.id,
|
||||
request_type='LABORATORY',
|
||||
service_description=f"Lab Tests: {', '.join([t.test_name for t in lab_order.tests.all()])}",
|
||||
procedure_codes=lab_order.get_procedure_codes(),
|
||||
diagnosis_codes=lab_order.get_diagnosis_codes(),
|
||||
clinical_justification="Patient presents with symptoms requiring diagnostic testing...",
|
||||
requesting_provider=request.user,
|
||||
service_start_date=lab_order.order_datetime.date(),
|
||||
priority='ROUTINE',
|
||||
requested_quantity=lab_order.tests.count()
|
||||
)
|
||||
```
|
||||
|
||||
### Creating from a Radiology Order
|
||||
|
||||
```python
|
||||
# Get the imaging order
|
||||
imaging_order = ImagingStudy.objects.get(pk=order_id)
|
||||
|
||||
# Create approval request
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=imaging_order.tenant,
|
||||
patient=imaging_order.patient,
|
||||
insurance_info=imaging_order.patient.insurance_info.first(),
|
||||
content_type=ContentType.objects.get_for_model(imaging_order),
|
||||
object_id=imaging_order.id,
|
||||
request_type='RADIOLOGY',
|
||||
service_description=imaging_order.study_description,
|
||||
procedure_codes=imaging_order.procedure_code,
|
||||
diagnosis_codes=imaging_order.get_diagnosis_codes(),
|
||||
clinical_justification=imaging_order.clinical_indication,
|
||||
requesting_provider=imaging_order.ordering_provider,
|
||||
service_start_date=imaging_order.requested_datetime.date(),
|
||||
priority=imaging_order.priority,
|
||||
requested_quantity=1
|
||||
)
|
||||
```
|
||||
|
||||
### Creating from a Prescription
|
||||
|
||||
```python
|
||||
# Get the prescription
|
||||
prescription = Prescription.objects.get(pk=prescription_id)
|
||||
|
||||
# Create approval request
|
||||
approval = InsuranceApprovalRequest.objects.create(
|
||||
tenant=prescription.tenant,
|
||||
patient=prescription.patient,
|
||||
insurance_info=prescription.patient.insurance_info.first(),
|
||||
content_type=ContentType.objects.get_for_model(prescription),
|
||||
object_id=prescription.id,
|
||||
request_type='PHARMACY',
|
||||
service_description=f"{prescription.medication.display_name} - {prescription.dosage_instructions}",
|
||||
procedure_codes=prescription.medication.ndc_code,
|
||||
diagnosis_codes=prescription.get_diagnosis_codes(),
|
||||
clinical_justification=prescription.indication or "As prescribed for patient condition",
|
||||
medical_necessity="Medication required for treatment of diagnosed condition",
|
||||
requesting_provider=prescription.prescriber,
|
||||
service_start_date=prescription.date_written,
|
||||
requested_quantity=prescription.quantity,
|
||||
requested_units=prescription.quantity,
|
||||
priority='ROUTINE'
|
||||
)
|
||||
```
|
||||
|
||||
### Updating Status
|
||||
|
||||
```python
|
||||
# Approve a request
|
||||
approval.status = 'APPROVED'
|
||||
approval.authorization_number = 'AUTH123456'
|
||||
approval.approved_quantity = approval.requested_quantity
|
||||
approval.effective_date = timezone.now().date()
|
||||
approval.expiration_date = timezone.now().date() + timedelta(days=90)
|
||||
approval.save()
|
||||
|
||||
# Deny a request
|
||||
approval.status = 'DENIED'
|
||||
approval.denial_reason = "Medical necessity not established"
|
||||
approval.denial_code = "D001"
|
||||
approval.save()
|
||||
```
|
||||
|
||||
### Uploading Documents
|
||||
|
||||
```python
|
||||
from insurance_approvals.models import ApprovalDocument
|
||||
|
||||
document = ApprovalDocument.objects.create(
|
||||
approval_request=approval,
|
||||
document_type='MEDICAL_RECORDS',
|
||||
title="Patient Medical History",
|
||||
description="Complete medical history for past 6 months",
|
||||
file=uploaded_file,
|
||||
uploaded_by=request.user
|
||||
)
|
||||
```
|
||||
|
||||
### Logging Communications
|
||||
|
||||
```python
|
||||
from insurance_approvals.models import ApprovalCommunicationLog
|
||||
|
||||
communication = ApprovalCommunicationLog.objects.create(
|
||||
approval_request=approval,
|
||||
communication_type='PHONE',
|
||||
contact_person="Jane Smith, Insurance Rep",
|
||||
contact_number="1-800-555-0123",
|
||||
subject="Status inquiry for authorization",
|
||||
message="Called to check on status of pending authorization",
|
||||
response="Authorization is under review, expect decision within 48 hours",
|
||||
outcome="Pending - follow up in 2 days",
|
||||
follow_up_required=True,
|
||||
follow_up_date=timezone.now().date() + timedelta(days=2),
|
||||
communicated_by=request.user
|
||||
)
|
||||
```
|
||||
|
||||
## Integration with Existing Order Models
|
||||
|
||||
Add these methods to your order models (LabOrder, ImagingStudy, Prescription, etc.):
|
||||
|
||||
```python
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
|
||||
class LabOrder(models.Model):
|
||||
# ... existing fields ...
|
||||
|
||||
# Add this field
|
||||
approval_requests = GenericRelation(
|
||||
'insurance_approvals.InsuranceApprovalRequest',
|
||||
content_type_field='content_type',
|
||||
object_id_field='object_id',
|
||||
related_query_name='lab_order'
|
||||
)
|
||||
|
||||
def requires_insurance_approval(self):
|
||||
"""Check if this order requires insurance approval."""
|
||||
return True # All orders require approval per requirements
|
||||
|
||||
def get_active_approval(self):
|
||||
"""Get the active approval for this order."""
|
||||
from django.utils import timezone
|
||||
return self.approval_requests.filter(
|
||||
status__in=['APPROVED', 'PARTIALLY_APPROVED', 'APPEAL_APPROVED'],
|
||||
expiration_date__gte=timezone.now().date()
|
||||
).first()
|
||||
|
||||
def has_valid_approval(self):
|
||||
"""Check if order has a valid, non-expired approval."""
|
||||
approval = self.get_active_approval()
|
||||
return approval is not None and not approval.is_expired
|
||||
|
||||
def create_approval_request(self, user):
|
||||
"""Helper method to create an approval request for this order."""
|
||||
from insurance_approvals.models import InsuranceApprovalRequest
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
return InsuranceApprovalRequest.objects.create(
|
||||
tenant=self.tenant,
|
||||
patient=self.patient,
|
||||
insurance_info=self.patient.insurance_info.first(),
|
||||
content_type=ContentType.objects.get_for_model(self),
|
||||
object_id=self.id,
|
||||
request_type='LABORATORY',
|
||||
service_description=self.get_service_description(),
|
||||
procedure_codes=self.get_procedure_codes(),
|
||||
diagnosis_codes=self.get_diagnosis_codes(),
|
||||
clinical_justification=self.get_clinical_justification(),
|
||||
requesting_provider=user,
|
||||
service_start_date=self.order_datetime.date(),
|
||||
priority=self.priority or 'ROUTINE'
|
||||
)
|
||||
```
|
||||
|
||||
## URL Patterns
|
||||
|
||||
The module provides these URL patterns:
|
||||
|
||||
```
|
||||
/insurance-approvals/ # Dashboard
|
||||
/insurance-approvals/list/ # List all approvals
|
||||
/insurance-approvals/<id>/ # Detail view
|
||||
/insurance-approvals/create/ # Create new approval
|
||||
/insurance-approvals/<id>/edit/ # Edit approval
|
||||
/insurance-approvals/<id>/submit/ # Submit for approval
|
||||
/insurance-approvals/create-from-order/<ct_id>/<obj_id>/ # Create from order
|
||||
/insurance-approvals/htmx/<id>/update-status/ # HTMX: Update status
|
||||
/insurance-approvals/htmx/<id>/upload-document/ # HTMX: Upload document
|
||||
/insurance-approvals/htmx/<id>/log-communication/ # HTMX: Log communication
|
||||
/insurance-approvals/htmx/dashboard-stats/ # HTMX: Dashboard stats
|
||||
```
|
||||
|
||||
## Admin Interface
|
||||
|
||||
Access the admin interface at:
|
||||
```
|
||||
http://127.0.0.1:8000/admin/insurance_approvals/
|
||||
```
|
||||
|
||||
Features:
|
||||
- Color-coded status badges
|
||||
- Priority indicators
|
||||
- Expiration warnings
|
||||
- Inline document management
|
||||
- Status history tracking
|
||||
- Communication logs
|
||||
- Advanced filtering and search
|
||||
|
||||
## Models
|
||||
|
||||
### InsuranceApprovalRequest
|
||||
Main model for approval requests with:
|
||||
- Patient and insurance information
|
||||
- Service details and codes
|
||||
- Clinical justification
|
||||
- Status workflow
|
||||
- Approval/denial details
|
||||
- Expiration tracking
|
||||
|
||||
### ApprovalDocument
|
||||
Supporting documents with:
|
||||
- Document type classification
|
||||
- File upload
|
||||
- Metadata tracking
|
||||
|
||||
### ApprovalStatusHistory
|
||||
Audit trail with:
|
||||
- Status transitions
|
||||
- Change reasons
|
||||
- User tracking
|
||||
- Timestamps
|
||||
|
||||
### ApprovalCommunicationLog
|
||||
Communication tracking with:
|
||||
- Communication type
|
||||
- Contact information
|
||||
- Message and response
|
||||
- Follow-up management
|
||||
|
||||
### ApprovalTemplate
|
||||
Reusable templates with:
|
||||
- Insurance company specific
|
||||
- Procedure specific
|
||||
- Required documentation
|
||||
- Usage tracking
|
||||
|
||||
## API Integration (Future)
|
||||
|
||||
The module is designed to support REST API integration:
|
||||
|
||||
```python
|
||||
# Example API endpoint structure
|
||||
GET /api/insurance-approvals/ # List approvals
|
||||
POST /api/insurance-approvals/ # Create approval
|
||||
GET /api/insurance-approvals/{id}/ # Get approval details
|
||||
PUT /api/insurance-approvals/{id}/ # Update approval
|
||||
PATCH /api/insurance-approvals/{id}/ # Partial update
|
||||
DELETE /api/insurance-approvals/{id}/ # Delete approval
|
||||
POST /api/insurance-approvals/{id}/submit/ # Submit approval
|
||||
POST /api/insurance-approvals/{id}/documents/ # Upload document
|
||||
GET /api/insurance-approvals/{id}/history/ # Get status history
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always create approval requests before processing orders**
|
||||
2. **Upload all required documents before submission**
|
||||
3. **Log all communications with insurance companies**
|
||||
4. **Set appropriate priorities for urgent cases**
|
||||
5. **Monitor expiration dates and renew before expiry**
|
||||
6. **Use templates for common approval types**
|
||||
7. **Keep clinical justifications detailed and specific**
|
||||
8. **Track denial reasons for appeal preparation**
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue**: Approval request not showing in list
|
||||
- **Solution**: Check tenant filtering, ensure user has correct tenant access
|
||||
|
||||
**Issue**: Cannot upload documents
|
||||
- **Solution**: Check MEDIA_ROOT and MEDIA_URL settings, ensure directory permissions
|
||||
|
||||
**Issue**: Status not updating
|
||||
- **Solution**: Check user permissions, ensure status transition is valid
|
||||
|
||||
**Issue**: Related order not showing
|
||||
- **Solution**: Ensure GenericRelation is added to order model
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check this README
|
||||
2. Review the admin interface
|
||||
3. Check model documentation in `models.py`
|
||||
4. Review view logic in `views.py`
|
||||
|
||||
## License
|
||||
|
||||
Part of the Hospital Management System v4 project.
|
||||
0
insurance_approvals/__init__.py
Normal file
0
insurance_approvals/__init__.py
Normal file
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