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="row">
|
||||||
<div class="col-xl-8">
|
<div class="col-xl-8">
|
||||||
<!-- BEGIN panel -->
|
<!-- BEGIN panel -->
|
||||||
<div class="panel panel-inverse">
|
<div class="panel panel-inverse" data-sortable-id="encounter-1">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">Encounter Information</h4>
|
<h4 class="panel-title">Encounter Information</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
@ -204,7 +204,7 @@
|
|||||||
<!-- END panel -->
|
<!-- END panel -->
|
||||||
|
|
||||||
<!-- BEGIN panel with tabs -->
|
<!-- BEGIN panel with tabs -->
|
||||||
<div class="panel panel-inverse">
|
<div class="panel panel-inverse" data-sortable-id="encounter-2">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">Encounter Details</h4>
|
<h4 class="panel-title">Encounter Details</h4>
|
||||||
<div class="panel-heading-btn">
|
<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>
|
<span class="d-sm-block d-none"><i class="fas fa-x-ray me-1"></i> Radiology</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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">
|
<li class="nav-item">
|
||||||
<a href="#billing-tab" data-bs-toggle="tab" class="nav-link">
|
<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>
|
<span class="d-sm-none"><i class="fas fa-file-invoice-dollar"></i></span>
|
||||||
@ -659,6 +671,164 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- END radiology-tab -->
|
<!-- 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 -->
|
<!-- BEGIN billing-tab -->
|
||||||
<div class="tab-pane fade" id="billing-tab">
|
<div class="tab-pane fade" id="billing-tab">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
@ -725,7 +895,7 @@
|
|||||||
|
|
||||||
<div class="col-xl-4">
|
<div class="col-xl-4">
|
||||||
<!-- BEGIN panel -->
|
<!-- BEGIN panel -->
|
||||||
<div class="panel panel-inverse">
|
<div class="panel panel-inverse" data-sortable-id="encounter-3">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">Quick Actions</h4>
|
<h4 class="panel-title">Quick Actions</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
@ -767,6 +937,22 @@
|
|||||||
<i class="fa fa-file-medical me-2"></i>Add Clinical Note
|
<i class="fa fa-file-medical me-2"></i>Add Clinical Note
|
||||||
</a>
|
</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()">
|
<button class="btn btn-outline-info" onclick="printEncounter()">
|
||||||
<i class="fa fa-print me-2"></i>Print Encounter
|
<i class="fa fa-print me-2"></i>Print Encounter
|
||||||
</button>
|
</button>
|
||||||
@ -776,7 +962,7 @@
|
|||||||
<!-- END panel -->
|
<!-- END panel -->
|
||||||
|
|
||||||
<!-- BEGIN panel -->
|
<!-- BEGIN panel -->
|
||||||
<div class="panel panel-inverse">
|
<div class="panel panel-inverse" data-sortable-id="encounter-4">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">Patient Information</h4>
|
<h4 class="panel-title">Patient Information</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
@ -830,7 +1016,7 @@
|
|||||||
<!-- END panel -->
|
<!-- END panel -->
|
||||||
|
|
||||||
<!-- BEGIN panel -->
|
<!-- BEGIN panel -->
|
||||||
<div class="panel panel-inverse">
|
<div class="panel panel-inverse" data-sortable-id="encounter-5">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">Related Information</h4>
|
<h4 class="panel-title">Related Information</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
@ -878,7 +1064,7 @@
|
|||||||
<!-- END panel -->
|
<!-- END panel -->
|
||||||
|
|
||||||
<!-- BEGIN panel -->
|
<!-- BEGIN panel -->
|
||||||
<div class="panel panel-inverse">
|
<div class="panel panel-inverse" data-sortable-id="encounter-6">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">Encounter Timeline</h4>
|
<h4 class="panel-title">Encounter Timeline</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
@ -938,6 +1124,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
@ -945,6 +1149,8 @@
|
|||||||
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
<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/select2/dist/js/select2.min.js' %}"></script>
|
||||||
<script src="{% static 'plugins/lity/dist/lity.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>
|
<script>
|
||||||
function updateStatus(newStatus) {
|
function updateStatus(newStatus) {
|
||||||
if (confirm('Are you sure you want to update the encounter status?')) {
|
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
|
# Actions
|
||||||
# path('record-create/', views.RecordCreateView.as_view(), name='record_create'),
|
# 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('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('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('note/<int:note_id>/sign/', views.sign_note, name='sign_note'),
|
||||||
path('problem/<int:problem_id>/resolve/', views.resolve_problem, name='resolve_problem'),
|
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(
|
).prefetch_related(
|
||||||
'vital_signs__measured_by',
|
'vital_signs__measured_by',
|
||||||
'clinical_notes__author',
|
'clinical_notes__author',
|
||||||
'problems_identified__diagnosing_provider'
|
'problems_identified__diagnosing_provider',
|
||||||
|
'problems_identified__care_plans',
|
||||||
|
'problems_identified',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -1863,52 +1865,127 @@ def add_vital_signs(request, pk):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@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':
|
if request.method == 'POST':
|
||||||
patient = get_object_or_404(
|
form = ProblemListForm(request.POST, tenant=tenant)
|
||||||
PatientProfile,
|
if form.is_valid():
|
||||||
id=patient_id,
|
problem = form.save(commit=False)
|
||||||
tenant=request.user.tenant
|
problem.tenant = tenant
|
||||||
)
|
problem.patient = encounter.patient
|
||||||
|
problem.diagnosing_provider = request.user
|
||||||
problem_data = {
|
problem.related_encounter = encounter
|
||||||
'tenant': request.user.tenant,
|
problem.save()
|
||||||
'patient': patient,
|
|
||||||
'problem_name': request.POST.get('problem_name'),
|
# Log the action
|
||||||
'problem_type': request.POST.get('problem_type', 'DIAGNOSIS'),
|
try:
|
||||||
'severity': request.POST.get('severity'),
|
AuditLogEntry.objects.create(
|
||||||
'priority': request.POST.get('priority', 'MEDIUM'),
|
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,
|
'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'):
|
@login_required
|
||||||
problem_data['onset_date'] = request.POST.get('onset_date')
|
def add_care_plan(request, encounter_id):
|
||||||
|
"""
|
||||||
# Create problem
|
HTMX endpoint for adding a care plan to an encounter.
|
||||||
problem = ProblemList.objects.create(**problem_data)
|
"""
|
||||||
|
tenant = request.user.tenant
|
||||||
# Log the action
|
encounter = get_object_or_404(Encounter, id=encounter_id, tenant=tenant)
|
||||||
AuditLogger.log_event(
|
|
||||||
user=request.user,
|
if request.method == 'POST':
|
||||||
action='PROBLEM_ADDED',
|
form = CarePlanForm(request.POST, tenant=tenant)
|
||||||
model='ProblemList',
|
if form.is_valid():
|
||||||
object_id=problem.id,
|
care_plan = form.save(commit=False)
|
||||||
details=f"Problem added for {patient.get_full_name()}: {problem.problem_name}"
|
care_plan.tenant = tenant
|
||||||
)
|
care_plan.patient = encounter.patient
|
||||||
|
care_plan.primary_provider = request.user
|
||||||
messages.success(request, f'Problem "{problem.problem_name}" added successfully')
|
care_plan.save()
|
||||||
return JsonResponse({'success': True, 'problem_id': problem.id})
|
form.save_m2m() # Save many-to-many relationships
|
||||||
|
|
||||||
return JsonResponse({'error': 'Invalid request'}, status=400)
|
# 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
|
@login_required
|
||||||
@ -2012,7 +2089,6 @@ def get_status_class(status):
|
|||||||
return status_classes.get(status, 'secondary')
|
return status_classes.get(status, 'secondary')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _norm_code(s: str) -> str:
|
def _norm_code(s: str) -> str:
|
||||||
return (s or "").upper().replace(" ", "")
|
return (s or "").upper().replace(" ", "")
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@ -21,7 +21,6 @@ class BuildingForm(forms.ModelForm):
|
|||||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'building_type': forms.Select(attrs={'class': 'form-select'}),
|
'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}),
|
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
'floor_count': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
|
'floor_count': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
|
||||||
'total_area_sqm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
'total_area_sqm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
||||||
@ -185,8 +184,9 @@ class MaintenanceRequestForm(forms.ModelForm):
|
|||||||
model = MaintenanceRequest
|
model = MaintenanceRequest
|
||||||
fields = [
|
fields = [
|
||||||
'title', 'description', 'maintenance_type', 'building',
|
'title', 'description', 'maintenance_type', 'building',
|
||||||
'floor', 'room', 'asset', 'priority', 'scheduled_date', 'actual_cost',
|
'floor', 'room', 'asset', 'priority', 'scheduled_date',
|
||||||
'estimated_cost', 'notes', 'assigned_to', 'scheduled_date', 'estimated_hours', 'status',
|
'estimated_cost', 'actual_cost', 'notes', 'assigned_to',
|
||||||
|
'estimated_hours', 'status', 'completion_notes'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
@ -199,11 +199,12 @@ class MaintenanceRequestForm(forms.ModelForm):
|
|||||||
'priority': forms.Select(attrs={'class': 'form-select'}),
|
'priority': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'scheduled_date': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
'scheduled_date': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
||||||
'estimated_cost': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
'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}),
|
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
'assigned_to': forms.Select(attrs={'class': 'form-select'}),
|
'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'}),
|
'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):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -428,4 +429,3 @@ class MaintenanceFilterForm(forms.Form):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['assigned_to'].queryset = User.objects.filter(is_active=True)
|
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>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.building_type.id_for_label }}" class="form-label">Building Type <span class="text-danger">*</span></label>
|
<label for="{{ form.building_type.id_for_label }}" class="form-label">Building Type <span class="text-danger">*</span></label>
|
||||||
{{ form.building_type }}
|
{{ form.building_type }}
|
||||||
@ -59,16 +59,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -256,4 +246,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -144,6 +144,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 -->
|
<!-- Status and Cost -->
|
||||||
<hr>
|
<hr>
|
||||||
@ -179,30 +202,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Completion Details -->
|
<!-- Completion Details -->
|
||||||
{% if object and object.status in 'completed,cancelled' %}
|
|
||||||
<hr>
|
<hr>
|
||||||
<h6 class="fw-bold mb-3">Completion Details</h6>
|
<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">
|
<div class="mb-3">
|
||||||
<label for="{{ form.completion_notes.id_for_label }}" class="form-label">Completion Notes</label>
|
<label for="{{ form.completion_notes.id_for_label }}" class="form-label">Completion Notes</label>
|
||||||
{{ form.completion_notes }}
|
{{ form.completion_notes }}
|
||||||
@ -211,7 +212,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<small class="form-text text-muted">Work performed, parts used, recommendations</small>
|
<small class="form-text text-muted">Work performed, parts used, recommendations</small>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Additional Notes -->
|
<!-- Additional Notes -->
|
||||||
<hr>
|
<hr>
|
||||||
@ -298,4 +298,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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',
|
'integration',
|
||||||
'quality',
|
'quality',
|
||||||
'facility_management',
|
'facility_management',
|
||||||
|
'insurance_approvals.apps.InsuranceApprovalsConfig',
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
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.utils.html import format_html
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.db.models import Count, Avg, Sum
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import date
|
from datetime import date, timedelta
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# INLINE ADMIN CLASSES
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
class ScheduleInline(admin.TabularInline):
|
class ScheduleInline(admin.TabularInline):
|
||||||
"""
|
"""
|
||||||
Inline admin for employee schedules.
|
Inline admin for employee schedules.
|
||||||
@ -46,7 +51,7 @@ class PerformanceReviewInline(admin.TabularInline):
|
|||||||
fields = [
|
fields = [
|
||||||
'review_date', 'review_type', 'overall_rating', 'status'
|
'review_date', 'review_type', 'overall_rating', 'status'
|
||||||
]
|
]
|
||||||
|
readonly_fields = ['review_id']
|
||||||
|
|
||||||
|
|
||||||
class TrainingRecordInline(admin.TabularInline):
|
class TrainingRecordInline(admin.TabularInline):
|
||||||
@ -56,12 +61,67 @@ class TrainingRecordInline(admin.TabularInline):
|
|||||||
model = TrainingRecord
|
model = TrainingRecord
|
||||||
extra = 0
|
extra = 0
|
||||||
fields = [
|
fields = [
|
||||||
'completion_date',
|
'program', 'session', 'completion_date',
|
||||||
'status', 'passed'
|
'status', 'passed'
|
||||||
]
|
]
|
||||||
readonly_fields = ['record_id']
|
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)
|
@admin.register(Employee)
|
||||||
class EmployeeAdmin(admin.ModelAdmin):
|
class EmployeeAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
@ -741,121 +801,581 @@ class PerformanceReviewAdmin(admin.ModelAdmin):
|
|||||||
return qs.select_related('employee', 'reviewer')
|
return qs.select_related('employee', 'reviewer')
|
||||||
|
|
||||||
|
|
||||||
# @admin.register(TrainingRecord)
|
# ============================================================================
|
||||||
# class TrainingRecordAdmin(admin.ModelAdmin):
|
# TRAINING ADMIN CLASSES
|
||||||
# """
|
# ============================================================================
|
||||||
# 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')
|
|
||||||
|
|
||||||
|
@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
|
# Customize admin site
|
||||||
admin.site.site_header = "Hospital Management System - HR"
|
admin.site.site_header = "Hospital Management System - HR"
|
||||||
admin.site.site_title = "HR Admin"
|
admin.site.site_title = "HR Admin"
|
||||||
admin.site.index_title = "Human Resources Administration"
|
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 -->
|
<!-- Pagination for Card View -->
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<div class="col-12 mt-3">
|
{% include 'partial/pagination.html' %}
|
||||||
<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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -494,7 +456,7 @@
|
|||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<!-- DataTables 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-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/js/dataTables.responsive.min.js' %}"></script>
|
||||||
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.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 panel-inverse" data-sortable-id="index-1">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<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>
|
</h4>
|
||||||
<div class="panel-heading-btn">
|
<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-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
@ -204,63 +198,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="timeline" id="activity-timeline">
|
{% include 'hr/departments/department_tree.html' %}
|
||||||
<!-- Recent Hires -->
|
{# <div class="timeline" id="activity-timeline">#}
|
||||||
{% for employee in recent_hires %}
|
{# <!-- Recent Hires -->#}
|
||||||
<div class="timeline-item">
|
{# {% for employee in recent_hires %}#}
|
||||||
<div class="timeline-marker bg-success"></div>
|
{# <div class="timeline-item">#}
|
||||||
<div class="timeline-content">
|
{# <div class="timeline-marker bg-success"></div>#}
|
||||||
<h6 class="timeline-title">New Employee Hired</h6>
|
{# <div class="timeline-content">#}
|
||||||
<p class="timeline-text">
|
{# <h6 class="timeline-title">New Employee Hired</h6>#}
|
||||||
<strong>{{ employee.get_full_name }}</strong> joined
|
{# <p class="timeline-text">#}
|
||||||
{% if employee.department %}{{ employee.department.name }}{% else %}the organization{% endif %}
|
{# <strong>{{ employee.get_full_name }}</strong> joined #}
|
||||||
as {{ employee.job_title|default:"Employee" }}
|
{# {% if employee.department %}{{ employee.department.name }}{% else %}the organization{% endif %}#}
|
||||||
</p>
|
{# as {{ employee.job_title|default:"Employee" }}#}
|
||||||
<small class="text-muted">{{ employee.hire_date|timesince }} ago</small>
|
{# </p>#}
|
||||||
</div>
|
{# <small class="text-muted">{{ employee.hire_date|timesince }} ago</small>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
{% endfor %}
|
{# </div>#}
|
||||||
|
{# {% endfor %}#}
|
||||||
<!-- Recent Reviews -->
|
{##}
|
||||||
{% for review in recent_reviews %}
|
{# <!-- Recent Reviews -->#}
|
||||||
<div class="timeline-item">
|
{# {% for review in recent_reviews %}#}
|
||||||
<div class="timeline-marker bg-info"></div>
|
{# <div class="timeline-item">#}
|
||||||
<div class="timeline-content">
|
{# <div class="timeline-marker bg-info"></div>#}
|
||||||
<h6 class="timeline-title">Performance Review Completed</h6>
|
{# <div class="timeline-content">#}
|
||||||
<p class="timeline-text">
|
{# <h6 class="timeline-title">Performance Review Completed</h6>#}
|
||||||
<strong>{{ review.employee.get_full_name }}</strong> received a performance review
|
{# <p class="timeline-text">#}
|
||||||
{% if review.overall_rating %}
|
{# <strong>{{ review.employee.get_full_name }}</strong> received a performance review#}
|
||||||
with rating: {{ review.get_overall_rating_display }}
|
{# {% if review.overall_rating %}#}
|
||||||
{% endif %}
|
{# with rating: {{ review.get_overall_rating_display }}#}
|
||||||
</p>
|
{# {% endif %}#}
|
||||||
<small class="text-muted">{{ review.review_date|timesince }} ago</small>
|
{# </p>#}
|
||||||
</div>
|
{# <small class="text-muted">{{ review.review_date|timesince }} ago</small>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
{% endfor %}
|
{# </div>#}
|
||||||
|
{# {% endfor %}#}
|
||||||
<!-- Recent Training -->
|
{##}
|
||||||
{% for training in recent_training %}
|
{# <!-- Recent Training -->#}
|
||||||
<div class="timeline-item">
|
{# {% for training in recent_training %}#}
|
||||||
<div class="timeline-marker bg-warning"></div>
|
{# <div class="timeline-item">#}
|
||||||
<div class="timeline-content">
|
{# <div class="timeline-marker bg-warning"></div>#}
|
||||||
<h6 class="timeline-title">Training Completed</h6>
|
{# <div class="timeline-content">#}
|
||||||
<p class="timeline-text">
|
{# <h6 class="timeline-title">Training Completed</h6>#}
|
||||||
<strong>{{ training.employee.get_full_name }}</strong> completed
|
{# <p class="timeline-text">#}
|
||||||
"{{ training.training_name }}"
|
{# <strong>{{ training.employee.get_full_name }}</strong> completed #}
|
||||||
</p>
|
{# "{{ training.training_name }}"#}
|
||||||
<small class="text-muted">{{ training.completion_date|timesince }} ago</small>
|
{# </p>#}
|
||||||
</div>
|
{# <small class="text-muted">{{ training.completion_date|timesince }} ago</small>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
{% endfor %}
|
{# </div>#}
|
||||||
|
{# {% endfor %}#}
|
||||||
{% if not recent_hires and not recent_reviews and not recent_training %}
|
{##}
|
||||||
<div class="text-center py-4">
|
{# {% if not recent_hires and not recent_reviews and not recent_training %}#}
|
||||||
<i class="fas fa-clock fa-3x text-muted mb-3"></i>
|
{# <div class="text-center py-4">#}
|
||||||
<h6 class="text-muted">No recent activity</h6>
|
{# <i class="fas fa-clock fa-3x text-muted mb-3"></i>#}
|
||||||
<p class="text-muted">Recent HR activities will appear here</p>
|
{# <h6 class="text-muted">No recent activity</h6>#}
|
||||||
</div>
|
{# <p class="text-muted">Recent HR activities will appear here</p>#}
|
||||||
{% endif %}
|
{# </div>#}
|
||||||
</div>
|
{# {% endif %}#}
|
||||||
|
{# </div>#}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,63 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Assign Department Head - {{ department.name }}{% endblock %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@ -15,7 +71,7 @@
|
|||||||
<h6>{{ department.code }} - {{ department.name }}</h6>
|
<h6>{{ department.code }} - {{ department.name }}</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="page-btn">
|
<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
|
<i class="fas fa-arrow-left me-1"></i>Back to Department
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -140,7 +196,7 @@
|
|||||||
<!-- Selected User Preview -->
|
<!-- Selected User Preview -->
|
||||||
<div class="row" id="userPreview" style="display: none;">
|
<div class="row" id="userPreview" style="display: none;">
|
||||||
<div class="col-12">
|
<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>
|
<h6><i class="fas fa-info-circle me-2"></i>Selected User Information</h6>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -160,7 +216,7 @@
|
|||||||
{% if current_head %}
|
{% if current_head %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<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>
|
<h6><i class="fas fa-exclamation-triangle me-2"></i>Current Department Head</h6>
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
<strong>{{ current_head.get_full_name }}</strong> is currently the head of this department.
|
<strong>{{ current_head.get_full_name }}</strong> is currently the head of this department.
|
||||||
@ -175,7 +231,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-group text-end">
|
<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
|
<i class="fas fa-times me-1"></i>Cancel
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@ -260,7 +316,7 @@
|
|||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
There are no active staff members available to assign as department head.
|
There are no active staff members available to assign as department head.
|
||||||
</p>
|
</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
|
<i class="fas fa-arrow-left me-1"></i>Back to Department
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -270,7 +326,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block js %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const userSelect = document.getElementById('user_id');
|
const userSelect = document.getElementById('user_id');
|
||||||
@ -371,60 +428,6 @@ function removeDepartmentHead() {
|
|||||||
}
|
}
|
||||||
</script>
|
</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 %}
|
{% 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/js/dataTables.responsive.min.js' %}"></script>
|
||||||
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||||
<!-- Chart.js -->
|
<!-- Chart.js -->
|
||||||
<script src="{% static 'plugins/chart.js/dist/chart.js' %}"></script>
|
<script src="{% static 'plugins/chart.js/dist/chart.umd.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Initialize DataTable
|
// Initialize DataTable
|
||||||
$('#employees-table').DataTable({
|
$('#employees-table').DataTable({
|
||||||
responsive: true,
|
responsive: false,
|
||||||
lengthMenu: [5, 10, 25, 50],
|
lengthMenu: [5, 10, 25, 50],
|
||||||
pageLength: 5
|
pageLength: 5
|
||||||
});
|
});
|
||||||
@ -448,7 +448,7 @@
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: false,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
legend: {
|
legend: {
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
{% block css %}
|
{% block css %}
|
||||||
<!-- Select2 CSS -->
|
<!-- Select2 CSS -->
|
||||||
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'plugins/select2-bootstrap5-theme/select2-bootstrap5.min.css' %}" rel="stylesheet" />
|
|
||||||
<style>
|
<style>
|
||||||
.form-floating > .form-control:focus ~ label,
|
.form-floating > .form-control:focus ~ label,
|
||||||
.form-floating > .form-control:not(:placeholder-shown) ~ label {
|
.form-floating > .form-control:not(:placeholder-shown) ~ label {
|
||||||
@ -116,15 +115,15 @@
|
|||||||
|
|
||||||
<!-- Department Code -->
|
<!-- Department Code -->
|
||||||
<div class="form-floating mb-3 required-field">
|
<div class="form-floating mb-3 required-field">
|
||||||
{{ form.department_code }}
|
{{ form.code }}
|
||||||
<label for="{{ form.department_code.id_for_label }}">Department Code</label>
|
<label for="{{ form.code.id_for_label }}">Department Code</label>
|
||||||
{% if form.department_code.errors %}
|
{% if form.code.errors %}
|
||||||
<div class="invalid-feedback d-block">
|
<div class="invalid-feedback d-block">
|
||||||
{{ form.department_code.errors }}
|
{{ form.code.errors }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if form.department_code.help_text %}
|
{% if form.code.help_text %}
|
||||||
<div class="help-text mt-1">{{ form.department_code.help_text }}</div>
|
<div class="help-text mt-1">{{ form.code.help_text }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -347,11 +346,11 @@
|
|||||||
$('#{{ form.name.id_for_label }}').removeClass('is-invalid');
|
$('#{{ form.name.id_for_label }}').removeClass('is-invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($('#{{ form.department_code.id_for_label }}').val() === '') {
|
if ($('#{{ form.code.id_for_label }}').val() === '') {
|
||||||
$('#{{ form.department_code.id_for_label }}').addClass('is-invalid');
|
$('#{{ form.code.id_for_label }}').addClass('is-invalid');
|
||||||
isValid = false;
|
isValid = false;
|
||||||
} else {
|
} else {
|
||||||
$('#{{ form.department_code.id_for_label }}').removeClass('is-invalid');
|
$('#{{ form.code.id_for_label }}').removeClass('is-invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
@ -375,4 +374,3 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% 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 %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Delete Employee - {{ employee.get_full_name }} - {{ block.super }}{% endblock %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
@ -238,7 +293,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Handle data handling radio buttons
|
// Handle data handling radio buttons
|
||||||
@ -327,59 +384,6 @@ window.addEventListener('beforeunload', function(e) {
|
|||||||
});
|
});
|
||||||
</script>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -51,15 +51,7 @@ Delete Performance Review | HR Management
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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 -->
|
<!-- begin page-header -->
|
||||||
<h1 class="page-header">
|
<h1 class="page-header">
|
||||||
|
|||||||
@ -13,17 +13,7 @@
|
|||||||
<!-- Summernote CSS -->
|
<!-- Summernote CSS -->
|
||||||
<link href="{% static 'plugins/summernote/dist/summernote-lite.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/summernote/dist/summernote-lite.css' %}" rel="stylesheet" />
|
||||||
<style>
|
<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 {
|
.category-card {
|
||||||
border: 1px solid #dee2e6;
|
border: 1px solid #dee2e6;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@ -44,10 +34,7 @@
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.remove-goal {
|
|
||||||
color: #dc3545;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.help-sidebar {
|
.help-sidebar {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
@ -69,16 +56,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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 -->
|
<!-- begin page-header -->
|
||||||
<h1 class="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-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-close"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- end panel-heading -->
|
<!-- end panel-heading -->
|
||||||
@ -128,8 +107,9 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<!-- Basic Information Section -->
|
<!-- Basic Information Section -->
|
||||||
<div class="form-section">
|
<div class="card mb-3">
|
||||||
<h5 class="form-section-title">Basic Information</h5>
|
<h6 class="card-header">Basic Information</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="{{ form.employee.id_for_label }}" class="form-label">Employee <span class="text-danger">*</span></label>
|
<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">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="review_period" class="form-label">Review Period <span class="text-danger">*</span></label>
|
<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="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:'' }}">
|
{{ form.period_start }}
|
||||||
<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_end }}
|
||||||
|
<!-- Hidden model fields -->
|
||||||
|
{{ form.review_period_start }}
|
||||||
|
{{ form.review_period_end }}
|
||||||
{% if form.period_start.errors or form.period_end.errors %}
|
{% if form.period_start.errors or form.period_end.errors %}
|
||||||
<div class="invalid-feedback d-block">
|
<div class="invalid-feedback d-block">
|
||||||
{{ form.period_start.errors }}
|
{{ form.period_start.errors }}
|
||||||
@ -174,16 +157,19 @@
|
|||||||
<div class="col-md-6 mb-3">
|
<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>
|
<label for="{{ form.due_date.id_for_label }}" class="form-label">Due Date <span class="text-danger">*</span></label>
|
||||||
{{ form.due_date }}
|
{{ form.due_date }}
|
||||||
|
{{ form.review_date }}
|
||||||
{% if form.due_date.errors %}
|
{% if form.due_date.errors %}
|
||||||
<div class="invalid-feedback d-block">{{ form.due_date.errors }}</div>
|
<div class="invalid-feedback d-block">{{ form.due_date.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Performance Categories Section -->
|
<!-- Performance Categories Section -->
|
||||||
<div class="form-section">
|
<div class="card mb-3">
|
||||||
<h5 class="form-section-title">Performance Categories</h5>
|
<h6 class="card-header">Performance Categories</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div id="categories-container">
|
<div id="categories-container">
|
||||||
<!-- Categories will be added here dynamically -->
|
<!-- Categories will be added here dynamically -->
|
||||||
{% if categories %}
|
{% if categories %}
|
||||||
@ -226,10 +212,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Goals and Achievements Section -->
|
<!-- Goals and Achievements Section -->
|
||||||
<div class="form-section">
|
<div class="card mb-3">
|
||||||
<h5 class="form-section-title">Goals and Achievements</h5>
|
<h6 class="card-header">Goals and Achievements</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div id="goals-container">
|
<div id="goals-container">
|
||||||
<!-- Goals will be added here dynamically -->
|
<!-- Goals will be added here dynamically -->
|
||||||
{% if goals %}
|
{% if goals %}
|
||||||
@ -278,10 +266,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Strengths and Areas for Improvement Section -->
|
<!-- Strengths and Areas for Improvement Section -->
|
||||||
<div class="form-section">
|
<div class="card mb-3">
|
||||||
<h5 class="form-section-title">Strengths and Areas for Improvement</h5>
|
<h6 class="card-header">Strengths and Areas for Improvement</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="{{ form.strengths.id_for_label }}" class="form-label">Strengths</label>
|
<label for="{{ form.strengths.id_for_label }}" class="form-label">Strengths</label>
|
||||||
@ -301,10 +291,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Development Plan Section -->
|
<!-- Development Plan Section -->
|
||||||
<div class="form-section">
|
<div class="card mb-3">
|
||||||
<h5 class="form-section-title">Development Plan</h5>
|
<h6 class="card-header">Development Plan</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12 mb-3">
|
<div class="col-md-12 mb-3">
|
||||||
<label for="{{ form.development_plan.id_for_label }}" class="form-label">Development Plan</label>
|
<label for="{{ form.development_plan.id_for_label }}" class="form-label">Development Plan</label>
|
||||||
@ -315,14 +307,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Comments Section -->
|
<!-- Comments Section -->
|
||||||
<div class="form-section">
|
<div class="card mb-3">
|
||||||
<h5 class="form-section-title">Comments</h5>
|
<h6 class="card-header">Comments</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="{{ form.reviewer_comments.id_for_label }}" class="form-label">Reviewer Comments</label>
|
<label for="{{ form.reviewer_comments.id_for_label }}" class="form-label">Reviewer Comments</label>
|
||||||
{{ form.reviewer_comments }}
|
{{ form.reviewer_comments }}
|
||||||
|
{{ form.notes }}
|
||||||
{% if form.reviewer_comments.errors %}
|
{% if form.reviewer_comments.errors %}
|
||||||
<div class="invalid-feedback d-block">{{ form.reviewer_comments.errors }}</div>
|
<div class="invalid-feedback d-block">{{ form.reviewer_comments.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -336,10 +331,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Overall Score Section -->
|
<!-- Overall Score Section -->
|
||||||
<div class="form-section">
|
<div class="card mb-3">
|
||||||
<h5 class="form-section-title">Overall Score</h5>
|
<h6 class="card-header">Overall Score</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="{{ form.overall_score.id_for_label }}" class="form-label">Overall Score</label>
|
<label for="{{ form.overall_score.id_for_label }}" class="form-label">Overall Score</label>
|
||||||
@ -347,6 +344,8 @@
|
|||||||
{{ form.overall_score }}
|
{{ form.overall_score }}
|
||||||
<span class="input-group-text">/5</span>
|
<span class="input-group-text">/5</span>
|
||||||
</div>
|
</div>
|
||||||
|
{{ form.overall_rating }}
|
||||||
|
{{ form.competency_ratings }}
|
||||||
<div class="form-text">Leave blank to calculate automatically from categories</div>
|
<div class="form-text">Leave blank to calculate automatically from categories</div>
|
||||||
{% if form.overall_score.errors %}
|
{% if form.overall_score.errors %}
|
||||||
<div class="invalid-feedback d-block">{{ form.overall_score.errors }}</div>
|
<div class="invalid-feedback d-block">{{ form.overall_score.errors }}</div>
|
||||||
@ -365,10 +364,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Attachments Section -->
|
<!-- Attachments Section -->
|
||||||
<div class="form-section">
|
<div class="card mb-3">
|
||||||
<h5 class="form-section-title">Attachments</h5>
|
<h6 class="card-header">Attachments</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12 mb-3">
|
<div class="col-md-12 mb-3">
|
||||||
<label for="{{ form.attachments.id_for_label }}" class="form-label">Attachments</label>
|
<label for="{{ form.attachments.id_for_label }}" class="form-label">Attachments</label>
|
||||||
@ -401,6 +402,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Include hidden model fields for goals -->
|
||||||
|
{{ form.goals_achieved }}
|
||||||
|
{{ form.goals_not_achieved }}
|
||||||
|
{{ form.future_goals }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form Actions -->
|
<!-- Form Actions -->
|
||||||
@ -429,11 +436,20 @@
|
|||||||
<div class="col-xl-3">
|
<div class="col-xl-3">
|
||||||
<div class="help-sidebar">
|
<div class="help-sidebar">
|
||||||
<!-- Help Card -->
|
<!-- Help Card -->
|
||||||
<div class="card help-card mb-4">
|
<div class="panel panel-inverse">
|
||||||
<div class="card-header">
|
<!-- begin panel-heading -->
|
||||||
<h5 class="card-title mb-0">Help & Guidelines</h5>
|
<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>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<h6>Performance Review Process</h6>
|
<h6>Performance Review Process</h6>
|
||||||
<p>Follow these steps to complete a performance review:</p>
|
<p>Follow these steps to complete a performance review:</p>
|
||||||
<ol>
|
<ol>
|
||||||
@ -466,11 +482,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Card -->
|
<!-- Status Card -->
|
||||||
<div class="card mb-4">
|
<!-- begin panel-heading -->
|
||||||
<div class="card-header">
|
<div class="panel panel-inverse">
|
||||||
<h5 class="card-title mb-0">Review Status</h5>
|
<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>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<p>The review status determines the workflow:</p>
|
<p>The review status determines the workflow:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Draft</strong> - Initial creation, not visible to employee</li>
|
<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'));
|
$(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_start.id_for_label }}').val(picker.startDate.format('YYYY-MM-DD'));
|
||||||
$('#{{ form.period_end.id_for_label }}').val(picker.endDate.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
|
// Handle date range picker cancel event
|
||||||
@ -534,6 +562,8 @@
|
|||||||
$(this).val('');
|
$(this).val('');
|
||||||
$('#{{ form.period_start.id_for_label }}').val('');
|
$('#{{ form.period_start.id_for_label }}').val('');
|
||||||
$('#{{ form.period_end.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
|
// Initialize DatePicker for due date
|
||||||
@ -555,6 +585,8 @@
|
|||||||
// Handle due date picker apply event
|
// Handle due date picker apply event
|
||||||
$('#{{ form.due_date.id_for_label }}').on('apply.daterangepicker', function(ev, picker) {
|
$('#{{ form.due_date.id_for_label }}').on('apply.daterangepicker', function(ev, picker) {
|
||||||
$(this).val(picker.startDate.format('YYYY-MM-DD'));
|
$(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
|
// 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
|
// Handle star ratings
|
||||||
$('.rating-stars').each(function() {
|
$('.rating-stars').each(function() {
|
||||||
var $stars = $(this);
|
var $stars = $(this);
|
||||||
@ -729,4 +767,3 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -67,14 +67,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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 -->
|
<!-- begin page-header -->
|
||||||
<h1 class="page-header">{% if form.instance.id %}Edit{% else %}Create{% endif %} Schedule</h1>
|
<h1 class="page-header">{% if form.instance.id %}Edit{% else %}Create{% endif %} Schedule</h1>
|
||||||
@ -437,4 +429,3 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -211,43 +211,7 @@
|
|||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<nav aria-label="Page navigation">
|
{% include 'partial/pagination.html' %}
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page=1{% 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,10 @@
|
|||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<!-- DateTimePicker 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" />
|
<link href="{% static 'plugins/bootstrap-timepicker/css/bootstrap-timepicker.min.css' %}" rel="stylesheet" />
|
||||||
<!-- Select2 CSS -->
|
<!-- Select2 CSS -->
|
||||||
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'plugins/select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme.min.css' %}" rel="stylesheet" />
|
|
||||||
<style>
|
<style>
|
||||||
.help-sidebar {
|
.help-sidebar {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
@ -363,7 +362,7 @@
|
|||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<!-- DateTimePicker 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>
|
<script src="{% static 'plugins/bootstrap-timepicker/js/bootstrap-timepicker.min.js' %}"></script>
|
||||||
<!-- Select2 JS -->
|
<!-- Select2 JS -->
|
||||||
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||||
|
|||||||
@ -29,15 +29,26 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 class="h3 mb-0">
|
<h1 class="h3 mb-0">
|
||||||
{% if object %}Edit Training Session{% else %}Schedule Training Session{% endif %}
|
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="card">
|
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||||
<div class="card-body">
|
<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>
|
<form method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
@ -203,7 +214,7 @@
|
|||||||
Cost Override
|
Cost Override
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group">
|
<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 }}
|
{{ form.cost_override }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text">Override program cost for this session</div>
|
<div class="form-text">Override program cost for this session</div>
|
||||||
|
|||||||
@ -6,15 +6,9 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
<div id="content" class="app-content">
|
||||||
<div class="container-fluid">
|
<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="row align-items-center mb-3">
|
||||||
<div class="col">
|
<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>
|
<p class="text-muted">Employee training records and certification tracking</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@ -25,71 +19,77 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Overview -->
|
<!-- Overview -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-3">
|
<div class="col-lg-3 col-sm-6">
|
||||||
<div class="card bg-primary text-white">
|
<div class="widget widget-stats bg-primary mb-7px">
|
||||||
<div class="card-body d-flex align-items-center">
|
<div class="stats-icon stats-icon-lg"><i class="fa fa-database fa-fw"></i></div>
|
||||||
<div class="flex-grow-1">
|
<div class="stats-content">
|
||||||
<h4 class="mb-0">{{ total_records }}</h4>
|
<div class="stats-title">Total Records</div>
|
||||||
<p class="mb-0">Total Records</p>
|
<div class="stats-number">{{ total_records }}</div>
|
||||||
</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>
|
</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 -->
|
<!-- Tabs -->
|
||||||
<div class="card">
|
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<ul class="nav nav-tabs card-header-tabs" id="trainingTabs" role="tablist">
|
<h4 class="panel-title">
|
||||||
<li class="nav-item" role="presentation">
|
<i class="fas fa-graduation-cap me-2"></i>Training<span class="fw-light">Records</span>
|
||||||
<button class="nav-link active" id="records-tab" data-bs-toggle="tab" data-bs-target="#records" type="button" role="tab">
|
</h4>
|
||||||
<i class="fa fa-list me-2"></i>Training Records
|
<div class="panel-heading-btn">
|
||||||
</button>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
</li>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<li class="nav-item" role="presentation">
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
<button class="nav-link" id="compliance-tab" data-bs-toggle="tab" data-bs-target="#compliance" type="button" role="tab">
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
<i class="fa fa-shield-alt me-2"></i>Compliance
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</li>
|
<div class="panel-body">
|
||||||
</ul>
|
<div class="card mb-4" >
|
||||||
</div>
|
<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">
|
<div class="tab-content" id="trainingTabContent">
|
||||||
|
|
||||||
<!-- Training Records -->
|
<!-- Training Records -->
|
||||||
@ -323,6 +323,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -85,16 +85,6 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div>
|
<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">
|
<h1 class="h3 mb-0">
|
||||||
<i class="fas fa-graduation-cap text-primary me-2"></i>
|
<i class="fas fa-graduation-cap text-primary me-2"></i>
|
||||||
{% if form.instance.id %}Edit{% else %}Create{% endif %} Training Record
|
{% if form.instance.id %}Edit{% else %}Create{% endif %} Training Record
|
||||||
@ -130,72 +120,87 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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>
|
<form method="post" id="trainingRecordForm" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<!-- Basic Information Section -->
|
<!-- Basic Information Section -->
|
||||||
<div class="form-section">
|
<div class="card border border-primary mb-3">
|
||||||
<h5 class="form-section-title">
|
<h6 class="card-header bg-primary text-white">
|
||||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||||
</h5>
|
</h6>
|
||||||
<div class="row">
|
<div class="card-body">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="row">
|
||||||
<label for="{{ form.employee.id_for_label }}" class="form-label">
|
<div class="col-md-6 mb-3">
|
||||||
Employee <span class="required-field">*</span>
|
<label for="{{ form.employee.id_for_label }}" class="form-label">
|
||||||
</label>
|
Employee <span class="required-field">*</span>
|
||||||
{{ form.employee }}
|
</label>
|
||||||
{% if form.employee.help_text %}
|
{{ form.employee }}
|
||||||
<div class="field-help">{{ form.employee.help_text }}</div>
|
{% if form.employee.help_text %}
|
||||||
{% endif %}
|
<div class="field-help">{{ form.employee.help_text }}</div>
|
||||||
{% if form.employee.errors %}
|
{% endif %}
|
||||||
<div class="invalid-feedback d-block">{{ form.employee.errors.0 }}</div>
|
{% if form.employee.errors %}
|
||||||
{% endif %}
|
<div class="invalid-feedback d-block">{{ form.employee.errors.0 }}</div>
|
||||||
</div>
|
{% endif %}
|
||||||
<div class="col-md-6 mb-3">
|
</div>
|
||||||
<label for="{{ form.program.id_for_label }}" class="form-label">
|
<div class="col-md-6 mb-3">
|
||||||
Training Program <span class="required-field">*</span>
|
<label for="{{ form.program.id_for_label }}" class="form-label">
|
||||||
</label>
|
Training Program <span class="required-field">*</span>
|
||||||
{{ form.program }}
|
</label>
|
||||||
{% if form.program.help_text %}
|
{{ form.program }}
|
||||||
<div class="field-help">{{ form.program.help_text }}</div>
|
{% if form.program.help_text %}
|
||||||
{% endif %}
|
<div class="field-help">{{ form.program.help_text }}</div>
|
||||||
{% if form.program.errors %}
|
{% endif %}
|
||||||
<div class="invalid-feedback d-block">{{ form.program.errors.0 }}</div>
|
{% if form.program.errors %}
|
||||||
{% endif %}
|
<div class="invalid-feedback d-block">{{ form.program.errors.0 }}</div>
|
||||||
</div>
|
{% endif %}
|
||||||
<div class="col-md-6 mb-3">
|
</div>
|
||||||
<label for="{{ form.session.id_for_label }}" class="form-label">
|
<div class="col-md-6 mb-3">
|
||||||
Training Session
|
<label for="{{ form.session.id_for_label }}" class="form-label">
|
||||||
</label>
|
Training Session
|
||||||
{{ form.session }}
|
</label>
|
||||||
{% if form.session.help_text %}
|
{{ form.session }}
|
||||||
<div class="field-help">{{ form.session.help_text }}</div>
|
{% if form.session.help_text %}
|
||||||
{% endif %}
|
<div class="field-help">{{ form.session.help_text }}</div>
|
||||||
{% if form.session.errors %}
|
{% endif %}
|
||||||
<div class="invalid-feedback d-block">{{ form.session.errors.0 }}</div>
|
{% if form.session.errors %}
|
||||||
{% endif %}
|
<div class="invalid-feedback d-block">{{ form.session.errors.0 }}</div>
|
||||||
</div>
|
{% endif %}
|
||||||
<div class="col-md-6 mb-3">
|
</div>
|
||||||
<label for="{{ form.status.id_for_label }}" class="form-label">
|
<div class="col-md-6 mb-3">
|
||||||
Status <span class="required-field">*</span>
|
<label for="{{ form.status.id_for_label }}" class="form-label">
|
||||||
</label>
|
Status <span class="required-field">*</span>
|
||||||
{{ form.status }}
|
</label>
|
||||||
{% if form.status.help_text %}
|
{{ form.status }}
|
||||||
<div class="field-help">{{ form.status.help_text }}</div>
|
{% if form.status.help_text %}
|
||||||
{% endif %}
|
<div class="field-help">{{ form.status.help_text }}</div>
|
||||||
{% if form.status.errors %}
|
{% endif %}
|
||||||
<div class="invalid-feedback d-block">{{ form.status.errors.0 }}</div>
|
{% if form.status.errors %}
|
||||||
{% endif %}
|
<div class="invalid-feedback d-block">{{ form.status.errors.0 }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Schedule Information Section -->
|
<!-- Schedule Information Section -->
|
||||||
<div class="form-section">
|
<div class="card border border-success mb-3">
|
||||||
<h5 class="form-section-title">
|
<h6 class="card-header bg-success text-white">
|
||||||
<i class="fas fa-calendar-alt me-2"></i>Schedule Information
|
<i class="fas fa-calendar-alt me-2"></i>Schedule Information
|
||||||
</h5>
|
</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="{{ form.started_at.id_for_label }}" class="form-label">
|
<label for="{{ form.started_at.id_for_label }}" class="form-label">
|
||||||
@ -221,12 +226,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Progress & Assessment Section -->
|
<!-- Progress & Assessment Section -->
|
||||||
<div class="form-section">
|
<div class="card border border-info mb-3">
|
||||||
<h5 class="form-section-title">
|
<h6 class="card-header bg-info text-white">
|
||||||
<i class="fas fa-chart-line me-2"></i>Progress & Assessment
|
<i class="fas fa-chart-line me-2"></i>Progress & Assessment
|
||||||
</h5>
|
</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
<label for="{{ form.score.id_for_label }}" class="form-label">
|
<label for="{{ form.score.id_for_label }}" class="form-label">
|
||||||
@ -284,12 +291,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Notes Section -->
|
<!-- Notes Section -->
|
||||||
<div class="form-section">
|
<div class="card border border-warning mb-3">
|
||||||
<h5 class="form-section-title">
|
<h6 class="card-header bg-warning text-white">
|
||||||
<i class="fas fa-sticky-note me-2"></i>Additional Notes
|
<i class="fas fa-sticky-note me-2"></i>Additional Notes
|
||||||
</h5>
|
</h6>
|
||||||
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12 mb-3">
|
<div class="col-md-12 mb-3">
|
||||||
<label for="{{ form.notes.id_for_label }}" class="form-label">
|
<label for="{{ form.notes.id_for_label }}" class="form-label">
|
||||||
@ -305,6 +314,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Form Actions -->
|
<!-- Form Actions -->
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
@ -325,18 +335,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Help Sidebar -->
|
<!-- Help Sidebar -->
|
||||||
<div class="col-xl-4">
|
<div class="col-xl-4">
|
||||||
<div class="help-sidebar">
|
<div class="help-sidebar">
|
||||||
<!-- Help Card -->
|
<!-- Help Card -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="form-stuff-2">
|
||||||
<div class="card-header bg-primary text-white">
|
<div class="panel-heading">
|
||||||
<h5 class="card-title mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-question-circle me-2"></i>Help & Guidelines
|
<i class="fas fa-question-circle me-2"></i>Help & Guidelines
|
||||||
</h5>
|
</h4>
|
||||||
</div>
|
<div class="panel-heading-btn">
|
||||||
<div class="card-body">
|
<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>
|
<h6 class="text-primary">Training Record Guidelines</h6>
|
||||||
<p class="small">Follow these steps to create a comprehensive training record:</p>
|
<p class="small">Follow these steps to create a comprehensive training record:</p>
|
||||||
<ol class="small">
|
<ol class="small">
|
||||||
@ -375,13 +393,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tips Card -->
|
<!-- Tips Card -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="form-stuff-3">
|
||||||
<div class="card-header bg-info text-white">
|
<div class="panel-heading">
|
||||||
<h5 class="card-title mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-lightbulb me-2"></i>Tips
|
<i class="fas fa-lightbulb me-2"></i>Tips
|
||||||
</h5>
|
</h4>
|
||||||
</div>
|
<div class="panel-heading-btn">
|
||||||
<div class="card-body">
|
<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">
|
<ul class="small mb-0">
|
||||||
<li>Select a training session to automatically populate program details</li>
|
<li>Select a training session to automatically populate program details</li>
|
||||||
<li>Completion date is required for completed training</li>
|
<li>Completion date is required for completed training</li>
|
||||||
@ -395,13 +419,19 @@
|
|||||||
|
|
||||||
<!-- Program Info Card -->
|
<!-- Program Info Card -->
|
||||||
{% if form.instance.program %}
|
{% if form.instance.program %}
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="form-stuff-4">
|
||||||
<div class="card-header bg-success text-white">
|
<div class="panel-heading">
|
||||||
<h5 class="card-title mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-info-circle me-2"></i>Program Information
|
<i class="fas fa-info-circle me-2"></i>Program Information
|
||||||
</h5>
|
</h4>
|
||||||
</div>
|
<div class="panel-heading-btn">
|
||||||
<div class="card-body">
|
<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>
|
<h6>{{ form.instance.program.name }}</h6>
|
||||||
<p class="small text-muted">{{ form.instance.program.description|truncatewords:20 }}</p>
|
<p class="small text-muted">{{ form.instance.program.description|truncatewords:20 }}</p>
|
||||||
<div class="row small">
|
<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/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('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('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'),
|
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/create/', views.ScheduleCreateView.as_view(), name='schedule_create'),
|
||||||
path('schedules/<int:pk>/', views.ScheduleDetailView.as_view(), name='schedule_detail'),
|
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>/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
|
# 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/', views.ScheduleAssignmentListView.as_view(), name='schedule_assignment_list'),
|
||||||
path('assignments/create/', views.ScheduleAssignmentCreateView.as_view(), name='schedule_assignment_create'),
|
path('assignments/create/', views.ScheduleAssignmentCreateView.as_view(), name='schedule_assignment_create'),
|
||||||
path('assignments/<int:pk>/update/', views.ScheduleAssignmentUpdateView.as_view(), name='schedule_assignment_update'),
|
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)
|
# 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-sessions/', views.get_program_sessions, name='get_program_sessions'),
|
||||||
path('ajax/get-program-details/', views.get_program_details, name='get_program_details'),
|
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
|
# 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
|
# 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 os
|
||||||
import django
|
import django
|
||||||
@ -16,6 +26,7 @@ import random
|
|||||||
from datetime import datetime, timedelta, date, time
|
from datetime import datetime, timedelta, date, time
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.utils import timezone as django_timezone
|
from django.utils import timezone as django_timezone
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from hr.models import (
|
from hr.models import (
|
||||||
Employee, Department, Schedule, ScheduleAssignment, TimeEntry, PerformanceReview,
|
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
|
# Create more users if we need more employees
|
||||||
to_create = max(0, employees_per_tenant - num_existing)
|
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):
|
for i in range(to_create):
|
||||||
gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
|
try:
|
||||||
first = random.choice(SAUDI_FIRST_NAMES_MALE if gender == Employee.Gender.MALE else SAUDI_FIRST_NAMES_FEMALE)
|
gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
|
||||||
father = random.choice(SAUDI_FIRST_NAMES_MALE)
|
first = random.choice(SAUDI_FIRST_NAMES_MALE if gender == Employee.Gender.MALE else SAUDI_FIRST_NAMES_FEMALE)
|
||||||
grandfather = random.choice(SAUDI_FIRST_NAMES_MALE)
|
father = random.choice(SAUDI_FIRST_NAMES_MALE)
|
||||||
last = random.choice(SAUDI_LAST_NAMES)
|
grandfather = random.choice(SAUDI_FIRST_NAMES_MALE)
|
||||||
base_username = f"{first.lower()}.{last.lower().replace('-', '').replace('al', '')}"
|
last = random.choice(SAUDI_LAST_NAMES)
|
||||||
username = tenant_scoped_unique_username(tenant, base_username)
|
base_username = f"{first.lower()}.{last.lower().replace('-', '').replace('al', '')}"
|
||||||
email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa"
|
username = tenant_scoped_unique_username(tenant, base_username)
|
||||||
|
email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa"
|
||||||
|
|
||||||
u = User.objects.create(
|
u = User.objects.create(
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
first_name=first,
|
first_name=first,
|
||||||
# father_name=father,
|
last_name=last,
|
||||||
# grandfather_name=grandfather,
|
is_active=True,
|
||||||
last_name=last,
|
)
|
||||||
is_active=True,
|
u.set_password('Hospital@123')
|
||||||
)
|
u.save()
|
||||||
u.set_password('Hospital@123')
|
tenant_users.append(u) # signal creates Employee
|
||||||
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}")
|
||||||
# Now (re)populate employee HR data
|
|
||||||
for u in tenant_users:
|
except Exception as e:
|
||||||
emp = getattr(u, 'employee_profile', None)
|
print(f"Error creating user {i+1} for {tenant.name}: {e}")
|
||||||
if not emp:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Basic personal
|
# Now (re)populate employee HR data
|
||||||
if not emp.first_name:
|
print(f"Updating employee profiles for {tenant.name}...")
|
||||||
emp.first_name = u.first_name or emp.first_name
|
for i, u in enumerate(tenant_users):
|
||||||
if not emp.last_name:
|
try:
|
||||||
emp.last_name = u.last_name or emp.last_name
|
emp = getattr(u, 'employee_profile', None)
|
||||||
if not emp.email:
|
if not emp:
|
||||||
emp.email = u.email
|
continue
|
||||||
|
|
||||||
# Demographics
|
# Basic personal
|
||||||
if not emp.gender:
|
if not emp.first_name:
|
||||||
emp.gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE, Employee.Gender.OTHER])
|
emp.first_name = u.first_name or emp.first_name
|
||||||
if not emp.marital_status:
|
if not emp.last_name:
|
||||||
emp.marital_status = random.choice([
|
emp.last_name = u.last_name or emp.last_name
|
||||||
Employee.MaritalStatus.SINGLE, Employee.MaritalStatus.MARRIED,
|
if not emp.email:
|
||||||
Employee.MaritalStatus.DIVORCED, Employee.MaritalStatus.WIDOWED,
|
emp.email = u.email
|
||||||
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)))
|
|
||||||
|
|
||||||
# Contact E.164 (both are mobiles by model design)
|
# Add father and grandfather names
|
||||||
if not emp.phone:
|
if not emp.father_name:
|
||||||
emp.phone = e164_ksa_mobile()
|
emp.father_name = random.choice(SAUDI_FIRST_NAMES_MALE)
|
||||||
if not emp.mobile_phone:
|
if not emp.grandfather_name:
|
||||||
emp.mobile_phone = e164_ksa_mobile()
|
emp.grandfather_name = random.choice(SAUDI_FIRST_NAMES_MALE)
|
||||||
|
|
||||||
# Address
|
# ID information
|
||||||
if not emp.address_line_1:
|
if not emp.identification_number:
|
||||||
emp.address_line_1 = f"{random.randint(1, 999)} {random.choice(['King Fahd Rd', 'Prince Sultan St', 'Olaya St'])}"
|
emp.identification_number = f"{random.randint(1000000000, 9999999999)}"
|
||||||
if not emp.city:
|
emp.id_type = random.choice([Employee.IdNumberTypes.NATIONAL_ID, Employee.IdNumberTypes.IQAMA])
|
||||||
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'
|
|
||||||
|
|
||||||
# Org
|
# Demographics
|
||||||
if not emp.department:
|
if not emp.gender:
|
||||||
emp.department = random.choice(depts)
|
emp.gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE])
|
||||||
if not emp.job_title:
|
if not emp.marital_status:
|
||||||
emp.job_title = pick_job_title_for_department(emp.department)
|
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)
|
# Contact E.164 (both are mobiles by model design)
|
||||||
if not emp.role or emp.role == Employee.Role.GUEST:
|
if not emp.phone:
|
||||||
emp.role = infer_role_from_title(emp.job_title)
|
emp.phone = e164_ksa_mobile()
|
||||||
|
if not emp.mobile_phone:
|
||||||
|
emp.mobile_phone = e164_ksa_mobile()
|
||||||
|
|
||||||
# Employment
|
# Address
|
||||||
if not emp.employment_type:
|
if not emp.address_line_1:
|
||||||
emp.employment_type = random.choice([
|
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'])}"
|
||||||
Employee.EmploymentType.FULL_TIME, Employee.EmploymentType.PART_TIME,
|
if not emp.address_line_2:
|
||||||
Employee.EmploymentType.CONTRACT, Employee.EmploymentType.TEMPORARY,
|
if random.choice([True, False]):
|
||||||
Employee.EmploymentType.INTERN, Employee.EmploymentType.VOLUNTEER,
|
emp.address_line_2 = f"Apt {random.randint(1, 50)}"
|
||||||
Employee.EmploymentType.PER_DIEM, Employee.EmploymentType.CONSULTANT
|
if not emp.city:
|
||||||
])
|
emp.city = random.choice(SAUDI_CITIES)
|
||||||
if not emp.hire_date:
|
if not emp.postal_code:
|
||||||
emp.hire_date = django_timezone.now().date() - timedelta(days=random.randint(30, 2000))
|
emp.postal_code = f"{random.randint(10000, 99999)}"
|
||||||
if not emp.employment_status:
|
if not emp.country:
|
||||||
emp.employment_status = random.choices(
|
emp.country = 'Saudi Arabia'
|
||||||
[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))
|
|
||||||
|
|
||||||
# Licensure (optional by role/title)
|
# Org
|
||||||
jt_lower = (emp.job_title or '').lower()
|
if not emp.department:
|
||||||
if not emp.license_number and any(k in jt_lower for k in ['physician', 'nurse', 'pharmacist', 'radiolog']):
|
emp.department = random.choice(depts)
|
||||||
emp.license_number = f"LIC-{random.randint(100000, 999999)}"
|
if not emp.job_title:
|
||||||
emp.license_expiry_date = django_timezone.now().date() + timedelta(days=random.randint(180, 1095))
|
emp.job_title = pick_job_title_for_department(emp.department)
|
||||||
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)}"
|
|
||||||
|
|
||||||
# Preferences
|
# Role (derive from job title if not set)
|
||||||
emp.user_timezone = 'Asia/Riyadh'
|
if not emp.role or emp.role == Employee.Role.GUEST:
|
||||||
if not emp.language:
|
emp.role = infer_role_from_title(emp.job_title)
|
||||||
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])
|
|
||||||
|
|
||||||
emp.save()
|
# Employment
|
||||||
all_employees.append(emp)
|
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])}")
|
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
|
# Select signer from same tenant
|
||||||
tenant_signers = [s for s in potential_signers if s.tenant == record.employee.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:
|
try:
|
||||||
certificate = TrainingCertificates.objects.create(
|
certificate = TrainingCertificates.objects.create(
|
||||||
@ -1159,13 +1235,17 @@ def create_training_certificates(training_records, employees):
|
|||||||
certificate_number=certificate_number,
|
certificate_number=certificate_number,
|
||||||
certification_body=certification_body,
|
certification_body=certification_body,
|
||||||
expiry_date=expiry_date,
|
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)),
|
created_at=django_timezone.now() - timedelta(days=random.randint(0, 7)),
|
||||||
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 3))
|
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 3))
|
||||||
)
|
)
|
||||||
certificates.append(certificate)
|
certificates.append(certificate)
|
||||||
|
print(f"Created certificate {certificate_number} for {record.employee.get_full_name()}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error creating certificate for {record.employee.get_full_name()}: {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")
|
print(f"Created {len(certificates)} training certificates")
|
||||||
return 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 class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="h2">
|
<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>
|
</h1>
|
||||||
<p class="text-muted">Comprehensive discharge planning and coordination.</p>
|
<p class="text-muted">Comprehensive discharge planning and coordination.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -78,10 +78,11 @@ urlpatterns = [
|
|||||||
path('surgery/create/', views.SurgeryScheduleCreateView.as_view(), name='surgery_create'),
|
path('surgery/create/', views.SurgeryScheduleCreateView.as_view(), name='surgery_create'),
|
||||||
path('surgery/calendar/', views.surgery_calendar, name='surgery_calendar'),
|
path('surgery/calendar/', views.surgery_calendar, name='surgery_calendar'),
|
||||||
path('surgery/<int:pk>/cancel/', views.cancel_surgery, name='cancel_surgery'),
|
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>/confirm/', views.confirm_surgery, name='confirm_surgery'),
|
||||||
path('surgery/<int:pk>/prep/', views.prep_surgery, name='prep_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>/postpone/', views.postpone_surgery, name='postpone_surgery'),
|
||||||
|
path('surgery/<int:pk>/start/', views.start_surgery, name='start_surgery'),
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,7 @@ class InpatientDashboardView(LoginRequiredMixin, ListView):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class WardListView(LoginRequiredMixin, ListView):
|
class WardListView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List view for wards.
|
List view for wards.
|
||||||
@ -970,10 +971,10 @@ class SurgeryScheduleListView(LoginRequiredMixin, ListView):
|
|||||||
tenant = self.request.user.tenant
|
tenant = self.request.user.tenant
|
||||||
|
|
||||||
# Get statuses for filter dropdown
|
# Get statuses for filter dropdown
|
||||||
context['statuses'] = SurgerySchedule.STATUS_CHOICES
|
context['statuses'] = SurgerySchedule.SurgeryStatus.choices
|
||||||
|
|
||||||
# Get priorities for filter dropdown
|
# Get priorities for filter dropdown
|
||||||
context['priorities'] = SurgerySchedule.PRIORITY_CHOICES
|
context['priorities'] = SurgerySchedule.SurgeryPriority.choices
|
||||||
|
|
||||||
# Get surgeons for filter dropdown
|
# Get surgeons for filter dropdown
|
||||||
context['surgeons'] = User.objects.filter(
|
context['surgeons'] = User.objects.filter(
|
||||||
@ -2167,7 +2168,7 @@ def clean_bed(request, pk):
|
|||||||
|
|
||||||
return render(request, 'inpatients/clean_bed.html', {
|
return render(request, 'inpatients/clean_bed.html', {
|
||||||
'bed': bed,
|
'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}))
|
'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
|
tenant = self.request.user.tenant
|
||||||
|
|
||||||
# Get statuses for filter dropdown
|
# Get statuses for filter dropdown
|
||||||
context['statuses'] = Transfer.STATUS_CHOICES
|
context['statuses'] = Transfer.TransferStatus.choices
|
||||||
|
|
||||||
# Get transfer types for filter dropdown
|
# Get transfer types for filter dropdown
|
||||||
context['transfer_types'] = Transfer.TRANSFER_TYPE_CHOICES
|
context['transfer_types'] = Transfer.TransferType.choices
|
||||||
|
|
||||||
# Get priorities for filter dropdown
|
# Get priorities for filter dropdown
|
||||||
context['priorities'] = Transfer.PRIORITY_CHOICES
|
context['priorities'] = Transfer.TransferPriority.choices
|
||||||
|
|
||||||
# Get wards for filter dropdown
|
# Get wards for filter dropdown
|
||||||
context['wards'] = Ward.objects.filter(
|
context['wards'] = Ward.objects.filter(
|
||||||
@ -3301,9 +3302,6 @@ class TransferUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
return reverse('inpatients:transfer_detail', kwargs={'pk': self.object.pk})
|
return reverse('inpatients:transfer_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
# @permission_required('inpatients.change_surgeryschedule')
|
# @permission_required('inpatients.change_surgeryschedule')
|
||||||
def mark_surgery_completed(request, pk):
|
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
|
@login_required
|
||||||
def inpatient_stats(request):
|
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