This commit is contained in:
Marwan Alwali 2025-09-20 14:26:19 +03:00
parent 2b4c58aa34
commit 4d06ca4b5e
229 changed files with 741685 additions and 8968 deletions

BIN
.DS_Store vendored

Binary file not shown.

1
.env Normal file
View File

@ -0,0 +1 @@
ENCRYPTION_KEY=super-secret-key-123

20
.idea/dataSources.xml generated Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="db" uuid="0a1c7514-5bfb-4128-82a6-2adb0a4546e2">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db.sqlite3</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>

15461
AR-DRG Index Appendix A Normal file

File diff suppressed because it is too large Load Diff

14304
AR-DRG Index Appendix B Normal file

File diff suppressed because it is too large Load Diff

BIN
Archive.zip Normal file

Binary file not shown.

Binary file not shown.

BIN
Mapping Template.xlsx Normal file

Binary file not shown.

BIN
SBS V2 Alphabetic Index.pdf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
SBS_V2_Code_list.xlsx Normal file

Binary file not shown.

BIN
SBS_V2_Code_list_URLs.xlsx Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,7 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
import uuid
from django.db import migrations, models
@ -386,6 +387,21 @@ class Migration(migrations.Migration):
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(
@ -424,67 +440,15 @@ class Migration(migrations.Migration):
"user_id",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
(
"username",
models.CharField(
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
),
),
("email", models.EmailField(blank=True, max_length=254, null=True)),
(
"force_password_change",
models.BooleanField(
default=False,
help_text="User must change password on next login",
),
),
(
"password_expires_at",
models.DateTimeField(
blank=True, help_text="Password expiration date", null=True
),
),
(
"failed_login_attempts",
models.PositiveIntegerField(
default=0, help_text="Number of failed login attempts"
),
),
(
"locked_until",
models.DateTimeField(
blank=True,
help_text="Account locked until this time",
null=True,
),
),
(
"two_factor_enabled",
models.BooleanField(
default=False, help_text="Two-factor authentication enabled"
),
),
(
"max_concurrent_sessions",
models.PositiveIntegerField(
default=3, help_text="Maximum concurrent sessions allowed"
),
),
(
"session_timeout_minutes",
models.PositiveIntegerField(
default=30, help_text="Session timeout in minutes"
),
),
(
"last_password_change",
models.DateTimeField(
default=django.utils.timezone.now,
help_text="Last password change date",
),
),
("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(

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.db.models.deletion
from django.conf import settings
@ -20,7 +20,6 @@ class Migration(migrations.Migration):
model_name="user",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.PROTECT,
related_name="users",
to="core.tenant",
@ -86,12 +85,6 @@ class Migration(migrations.Migration):
fields=["tenant", "username"], name="accounts_us_tenant__d92906_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["tenant", "user_id"], name="accounts_us_tenant__bd3758_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-15 14:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0002_initial"),
]
operations = [
migrations.AlterField(
model_name="user",
name="is_active",
field=models.BooleanField(default=True, help_text="User account is active"),
),
]

View File

@ -359,71 +359,17 @@ from django.conf import settings
class User(AbstractUser):
"""
Minimal auth user for a multi-tenant app:
- Authentication core (from AbstractUser)
- Tenant link
- Security/session controls (lockout, password expiry, 2FA flag, session caps)
Everything else lives on hr.Employee.
"""
# Stable internal UUID
user_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
# Tenant (PROTECT = safer than cascading deletion)
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.PROTECT,
related_name='users',
help_text='Organization tenant',
)
username = models.CharField(
max_length=150,
unique=True,
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'
)
email = models.EmailField(blank=True, null=True)
# --- Security & session controls kept on User (auth-level concerns) ---
force_password_change = models.BooleanField(
default=False,
help_text='User must change password on next login'
)
password_expires_at = models.DateTimeField(
blank=True,
null=True,
help_text='Password expiration date'
)
failed_login_attempts = models.PositiveIntegerField(
default=0,
help_text='Number of failed login attempts'
)
locked_until = models.DateTimeField(
blank=True,
null=True,
help_text='Account locked until this time'
)
two_factor_enabled = models.BooleanField(
default=False,
help_text='Two-factor authentication enabled'
)
max_concurrent_sessions = models.PositiveIntegerField(
default=3,
help_text='Maximum concurrent sessions allowed'
)
session_timeout_minutes = models.PositiveIntegerField(
default=30,
help_text='Session timeout in minutes'
)
last_password_change = models.DateTimeField(
default=timezone.now,
help_text='Last password change date'
)
is_active = models.BooleanField(
default=True,
help_text='User account is active'
)
user_id = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
email = models.EmailField(unique=True)
tenant = models.ForeignKey('core.Tenant', on_delete=models.PROTECT, related_name='users')
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)
class Meta:
db_table = 'accounts_user'
@ -431,15 +377,15 @@ class User(AbstractUser):
indexes = [
models.Index(fields=['tenant', 'email']),
models.Index(fields=['tenant', 'username']),
models.Index(fields=['tenant', 'user_id']),
models.Index(fields=['tenant', 'is_active']),
]
def __str__(self):
full = super().get_full_name().strip()
return f"{full or self.username} (tenant={self.tenant_id})"
return f"{full or self.username}"
# ---- Security helpers ----
@property
def is_account_locked(self) -> bool:
return bool(self.locked_until and timezone.now() < self.locked_until)
@ -477,13 +423,22 @@ class User(AbstractUser):
self.failed_login_attempts = 0
self.save(update_fields=['failed_login_attempts'])
def set_password(self, raw_password):
super().set_password(raw_password)
self.last_password_change = timezone.now()
class TwoFactorDevice(models.Model):
"""
Two-factor authentication devices for users.
"""
class DeviceType(models.TextChoices):
TOTP = 'TOTP', 'Time-based OTP (Authenticator App)'
SMS = 'SMS', 'SMS'
EMAIL = 'EMAIL', 'Email'
HARDWARE = 'HARDWARE', 'Hardware Token'
BACKUP = 'BACKUP', 'Backup Codes'
# User relationship
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
@ -501,16 +456,7 @@ class TwoFactorDevice(models.Model):
max_length=100,
help_text='Device name'
)
device_type = models.CharField(
max_length=20,
choices=[
('TOTP', 'Time-based OTP (Authenticator App)'),
('SMS', 'SMS'),
('EMAIL', 'Email'),
('HARDWARE', 'Hardware Token'),
('BACKUP', 'Backup Codes'),
]
)
device_type = models.CharField(max_length=20,choices=DeviceType.choices)
# Device Configuration
secret_key = models.CharField(
@ -575,6 +521,16 @@ class SocialAccount(models.Model):
"""
Social authentication accounts linked to users.
"""
class Provider(models.TextChoices):
GOOGLE = 'GOOGLE', 'Google'
MICROSOFT = 'MICROSOFT', 'Microsoft'
APPLE = 'APPLE', 'Apple'
FACEBOOK = 'FACEBOOK', 'Facebook'
LINKEDIN = 'LINKEDIN', 'LinkedIn'
GITHUB = 'GITHUB', 'GitHub'
OKTA = 'OKTA', 'Okta'
SAML = 'SAML', 'SAML'
LDAP = 'LDAP', 'LDAP'
# User relationship
user = models.ForeignKey(
@ -584,20 +540,7 @@ class SocialAccount(models.Model):
)
# Provider Information
provider = models.CharField(
max_length=50,
choices=[
('GOOGLE', 'Google'),
('MICROSOFT', 'Microsoft'),
('APPLE', 'Apple'),
('FACEBOOK', 'Facebook'),
('LINKEDIN', 'LinkedIn'),
('GITHUB', 'GitHub'),
('OKTA', 'Okta'),
('SAML', 'SAML'),
('LDAP', 'LDAP'),
]
)
provider = models.CharField(max_length=50,choices=Provider.choices)
provider_id = models.CharField(
max_length=200,
help_text='Provider user ID'
@ -673,6 +616,20 @@ class UserSession(models.Model):
"""
User session tracking for security and audit purposes.
"""
class DeviceType(models.TextChoices):
DESKTOP = 'DESKTOP', 'Desktop'
MOBILE = 'MOBILE', 'Mobile'
TABLET = 'TABLET', 'Tablet'
UNKNOWN = 'UNKNOWN', 'Unknown'
class LoginMethod(models.TextChoices):
PASSWORD = 'PASSWORD', 'Password'
TWO_FACTOR = 'TWO_FACTOR', 'Two Factor'
SOCIAL = 'SOCIAL', 'Social Login'
SSO = 'SSO', 'Single Sign-On'
API_KEY = 'API_KEY', 'API Key'
# User relationship
user = models.ForeignKey(
@ -703,12 +660,7 @@ class UserSession(models.Model):
)
device_type = models.CharField(
max_length=20,
choices=[
('DESKTOP', 'Desktop'),
('MOBILE', 'Mobile'),
('TABLET', 'Tablet'),
('UNKNOWN', 'Unknown'),
],
choices=DeviceType.choices,
default='UNKNOWN'
)
browser = models.CharField(
@ -751,14 +703,8 @@ class UserSession(models.Model):
)
login_method = models.CharField(
max_length=20,
choices=[
('PASSWORD', 'Password'),
('TWO_FACTOR', 'Two Factor'),
('SOCIAL', 'Social Login'),
('SSO', 'Single Sign-On'),
('API_KEY', 'API Key'),
],
default='PASSWORD'
choices=LoginMethod.choices,
default=LoginMethod.PASSWORD
)
# Timestamps

View File

@ -8,34 +8,28 @@ from hr.models import Employee
from .models import User
@receiver(post_save, sender=User)
def create_employee_for_user(sender, instance: User, created: bool, **kwargs):
"""
Auto-create an Employee profile when a User is created.
Idempotent, tenant-aligned, and makes a unique employee_number per tenant.
"""
@receiver(post_save, sender=User, dispatch_uid="create_employee_on_user_create_simple")
def create_employee_on_user_create(sender, instance: User, created: bool, **kwargs):
if not created:
return
def _make_employee():
# Generate a readable employee_number from username/email, unique within tenant
base = (instance.username or (instance.email or 'user')).split('@')[0]
candidate = slugify(base)[:16] or 'emp'
suffix = 1
emp_no = candidate
while Employee.objects.filter(tenant=instance.tenant, employee_number=emp_no).exists():
suffix += 1
emp_no = f"{candidate}-{suffix}"
def _make():
tenant = getattr(instance, "tenant", None)
email = (instance.email or "").strip()
# If already linked OR same email exists in this tenant, skip
if Employee.objects.filter(user=instance).exists():
return
if email and Employee.objects.filter(tenant=tenant, email__iexact=email).exists():
return
# Create with basic info mirrored from User
Employee.objects.create(
tenant=instance.tenant,
tenant=tenant,
user=instance,
employee_number=emp_no,
first_name=instance.first_name or '',
last_name=instance.last_name or '',
email=instance.email
first_name=instance.first_name or "",
last_name=instance.last_name or "",
email=email,
# employee_id will be auto-generated in Employee.save()
)
# Defer until after outer transaction commits (avoids race conditions in tests/views)
transaction.on_commit(_make_employee)
transaction.on_commit(_make)

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.core.validators
import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.db.models.deletion
from django.conf import settings

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.core.validators
import django.db.models.deletion
@ -700,6 +700,7 @@ class Migration(migrations.Migration):
blank=True,
default=list,
help_text="Acceptable days of week (0=Monday, 6=Sunday)",
null=True,
),
),
(

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.db.models.deletion
from django.conf import settings

View File

@ -1,23 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-16 12:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("appointments", "0002_initial"),
]
operations = [
migrations.AlterField(
model_name="waitinglist",
name="acceptable_days",
field=models.JSONField(
blank=True,
default=list,
help_text="Acceptable days of week (0=Monday, 6=Sunday)",
null=True,
),
),
]

View File

@ -16,65 +16,66 @@ class AppointmentRequest(models.Model):
"""
Appointment request model for scheduling patient appointments.
"""
APPOINTMENT_TYPE_CHOICES = [
('CONSULTATION', 'Consultation'),
('FOLLOW_UP', 'Follow-up'),
('PROCEDURE', 'Procedure'),
('SURGERY', 'Surgery'),
('DIAGNOSTIC', 'Diagnostic'),
('THERAPY', 'Therapy'),
('VACCINATION', 'Vaccination'),
('SCREENING', 'Screening'),
('EMERGENCY', 'Emergency'),
('TELEMEDICINE', 'Telemedicine'),
('OTHER', 'Other'),
]
SPECIALTY_CHOICES = [
('FAMILY_MEDICINE', 'Family Medicine'),
('INTERNAL_MEDICINE', 'Internal Medicine'),
('PEDIATRICS', 'Pediatrics'),
('CARDIOLOGY', 'Cardiology'),
('DERMATOLOGY', 'Dermatology'),
('ENDOCRINOLOGY', 'Endocrinology'),
('GASTROENTEROLOGY', 'Gastroenterology'),
('NEUROLOGY', 'Neurology'),
('ONCOLOGY', 'Oncology'),
('ORTHOPEDICS', 'Orthopedics'),
('PSYCHIATRY', 'Psychiatry'),
('RADIOLOGY', 'Radiology'),
('SURGERY', 'Surgery'),
('UROLOGY', 'Urology'),
('GYNECOLOGY', 'Gynecology'),
('OPHTHALMOLOGY', 'Ophthalmology'),
('ENT', 'Ear, Nose & Throat'),
('EMERGENCY', 'Emergency Medicine'),
('OTHER', 'Other'),
]
PRIORITY_CHOICES=[
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('STAT', 'STAT'),
('EMERGENCY', 'Emergency'),
]
STATUS_CHOICES=[
('PENDING', 'Pending'),
('SCHEDULED', 'Scheduled'),
('CONFIRMED', 'Confirmed'),
('CHECKED_IN', 'Checked In'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
('NO_SHOW', 'No Show'),
('RESCHEDULED', 'Rescheduled'),
]
TELEMEDICINE_PLATFORM_CHOICES=[
('ZOOM', 'Zoom'),
('TEAMS', 'Microsoft Teams'),
('WEBEX', 'Cisco Webex'),
('DOXY', 'Doxy.me'),
('CUSTOM', 'Custom Platform'),
('OTHER', 'Other'),
]
class AppointmentType(models.TextChoices):
CONSULTATION = 'CONSULTATION', 'Consultation'
FOLLOW_UP = 'FOLLOW_UP', 'Follow-up'
PROCEDURE = 'PROCEDURE', 'Procedure'
SURGERY = 'SURGERY', 'Surgery'
DIAGNOSTIC = 'DIAGNOSTIC', 'Diagnostic'
THERAPY = 'THERAPY', 'Therapy'
VACCINATION = 'VACCINATION', 'Vaccination'
SCREENING = 'SCREENING', 'Screening'
EMERGENCY = 'EMERGENCY', 'Emergency'
TELEMEDICINE = 'TELEMEDICINE', 'Telemedicine'
OTHER = 'OTHER', 'Other'
class Specialty(models.TextChoices):
FAMILY_MEDICINE = 'FAMILY_MEDICINE', 'Family Medicine'
INTERNAL_MEDICINE = 'INTERNAL_MEDICINE', 'Internal Medicine'
PEDIATRICS = 'PEDIATRICS', 'Pediatrics'
CARDIOLOGY = 'CARDIOLOGY', 'Cardiology'
DERMATOLOGY = 'DERMATOLOGY', 'Dermatology'
ENDOCRINOLOGY = 'ENDOCRINOLOGY', 'Endocrinology'
GASTROENTEROLOGY = 'GASTROENTEROLOGY', 'Gastroenterology'
NEUROLOGY = 'NEUROLOGY', 'Neurology'
ONCOLOGY = 'ONCOLOGY', 'Oncology'
ORTHOPEDICS = 'ORTHOPEDICS', 'Orthopedics'
PSYCHIATRY = 'PSYCHIATRY', 'Psychiatry'
RADIOLOGY = 'RADIOLOGY', 'Radiology'
SURGERY = 'SURGERY', 'Surgery'
UROLOGY = 'UROLOGY', 'Urology'
GYNECOLOGY = 'GYNECOLOGY', 'Gynecology'
OPHTHALMOLOGY = 'OPHTHALMOLOGY', 'Ophthalmology'
ENT = 'ENT', 'Ear, Nose & Throat'
EMERGENCY = 'EMERGENCY', 'Emergency Medicine'
OTHER = 'OTHER', 'Other'
class Priority(models.TextChoices):
ROUTINE = 'ROUTINE', 'Routine'
URGENT = 'URGENT', 'Urgent'
STAT = 'STAT', 'STAT'
EMERGENCY = 'EMERGENCY', 'Emergency'
class AppointmentStatus(models.TextChoices):
PENDING = 'PENDING', 'Pending'
SCHEDULED = 'SCHEDULED', 'Scheduled'
CONFIRMED = 'CONFIRMED', 'Confirmed'
CHECKED_IN = 'CHECKED_IN', 'Checked In'
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
COMPLETED = 'COMPLETED', 'Completed'
CANCELLED = 'CANCELLED', 'Cancelled'
NO_SHOW = 'NO_SHOW', 'No Show'
RESCHEDULED = 'RESCHEDULED', 'Rescheduled'
class TelemedicinePlatform(models.TextChoices):
ZOOM = 'ZOOM', 'Zoom'
TEAMS = 'TEAMS', 'Microsoft Teams'
WEBEX = 'WEBEX', 'Cisco Webex'
DOXY = 'DOXY', 'Doxy.me'
CUSTOM = 'CUSTOM', 'Custom Platform'
OTHER = 'OTHER', 'Other'
# Basic Identifiers
request_id = models.UUIDField(
default=uuid.uuid4,
@ -110,13 +111,13 @@ class AppointmentRequest(models.Model):
# Appointment Details
appointment_type = models.CharField(
max_length=50,
choices=APPOINTMENT_TYPE_CHOICES,
choices=AppointmentType.choices,
help_text='Type of appointment'
)
specialty = models.CharField(
max_length=100,
choices=SPECIALTY_CHOICES,
choices=Specialty.choices,
help_text='Medical specialty'
)
@ -159,8 +160,8 @@ class AppointmentRequest(models.Model):
# Priority and Urgency
priority = models.CharField(
max_length=20,
choices=PRIORITY_CHOICES,
default='ROUTINE',
choices=Priority.choices,
default=Priority.ROUTINE,
help_text='Appointment priority'
)
urgency_score = models.PositiveIntegerField(
@ -204,8 +205,8 @@ class AppointmentRequest(models.Model):
# Status and Workflow
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='PENDING',
choices=AppointmentStatus.choices,
default=AppointmentStatus.PENDING,
help_text='Appointment status'
)
@ -242,7 +243,7 @@ class AppointmentRequest(models.Model):
)
telemedicine_platform = models.CharField(
max_length=50,
choices=TELEMEDICINE_PLATFORM_CHOICES,
choices=TelemedicinePlatform.choices,
blank=True,
null=True,
help_text='Telemedicine platform'
@ -398,13 +399,14 @@ class SlotAvailability(models.Model):
"""
Provider availability slots for appointment scheduling.
"""
AVAILABILITY_TYPE_CHOICES=[
('REGULAR', 'Regular Hours'),
('EXTENDED', 'Extended Hours'),
('EMERGENCY', 'Emergency'),
('ON_CALL', 'On Call'),
('TELEMEDICINE', 'Telemedicine Only'),
]
class AvailabilityType(models.TextChoices):
REGULAR = 'REGULAR', 'Regular Hours'
EXTENDED = 'EXTENDED', 'Extended Hours'
EMERGENCY = 'EMERGENCY', 'Emergency'
ON_CALL = 'ON_CALL', 'On Call'
TELEMEDICINE = 'TELEMEDICINE', 'Telemedicine Only'
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
@ -446,8 +448,8 @@ class SlotAvailability(models.Model):
# Availability Type
availability_type = models.CharField(
max_length=20,
choices=AVAILABILITY_TYPE_CHOICES,
default='REGULAR',
choices=AvailabilityType.choices,
default=AvailabilityType.REGULAR,
help_text='Type of availability'
)
@ -588,13 +590,14 @@ class WaitingQueue(models.Model):
"""
Waiting queue for managing patient flow.
"""
QUEUE_TYPE_CHOICES=[
('PROVIDER', 'Provider Queue'),
('SPECIALTY', 'Specialty Queue'),
('LOCATION', 'Location Queue'),
('PROCEDURE', 'Procedure Queue'),
('EMERGENCY', 'Emergency Queue'),
]
class QueueType(models.TextChoices):
PROVIDER = 'PROVIDER', 'Provider Queue'
SPECIALTY = 'SPECIALTY', 'Specialty Queue'
LOCATION = 'LOCATION', 'Location Queue'
PROCEDURE = 'PROCEDURE', 'Procedure Queue'
EMERGENCY = 'EMERGENCY', 'Emergency Queue'
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
@ -624,7 +627,7 @@ class WaitingQueue(models.Model):
# Queue Type and Configuration
queue_type = models.CharField(
max_length=20,
choices=QUEUE_TYPE_CHOICES,
choices=QueueType.choices,
help_text='Type of queue'
)
@ -726,14 +729,15 @@ class QueueEntry(models.Model):
"""
Individual entry in a waiting queue.
"""
STATUS_CHOICES=[
('WAITING', 'Waiting'),
('CALLED', 'Called'),
('IN_SERVICE', 'In Service'),
('COMPLETED', 'Completed'),
('LEFT', 'Left Queue'),
('NO_SHOW', 'No Show'),
]
class QueueStatus(models.TextChoices):
WAITING = 'WAITING', 'Waiting'
CALLED = 'CALLED', 'Called'
IN_SERVICE = 'IN_SERVICE', 'In Service'
COMPLETED = 'COMPLETED', 'Completed'
LEFT = 'LEFT', 'Left Queue'
NO_SHOW = 'NO_SHOW', 'No Show'
# Queue and Patient
queue = models.ForeignKey(
WaitingQueue,
@ -795,8 +799,8 @@ class QueueEntry(models.Model):
# Status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='WAITING',
choices=QueueStatus.choices,
default=QueueStatus.WAITING,
help_text='Queue entry status'
)
@ -878,22 +882,23 @@ class TelemedicineSession(models.Model):
"""
Telemedicine session management.
"""
PLATFORM_CHOICES=[
('ZOOM', 'Zoom'),
('TEAMS', 'Microsoft Teams'),
('WEBEX', 'Cisco Webex'),
('DOXY', 'Doxy.me'),
('CUSTOM', 'Custom Platform'),
('OTHER', 'Other'),
]
STATUS_CHOICES=[
('SCHEDULED', 'Scheduled'),
('READY', 'Ready to Start'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
('FAILED', 'Failed'),
]
class Platform(models.TextChoices):
ZOOM = 'ZOOM', 'Zoom'
TEAMS = 'TEAMS', 'Microsoft Teams'
WEBEX = 'WEBEX', 'Cisco Webex'
DOXY = 'DOXY', 'Doxy.me'
CUSTOM = 'CUSTOM', 'Custom Platform'
OTHER = 'OTHER', 'Other'
class SessionStatus(models.TextChoices):
SCHEDULED = 'SCHEDULED', 'Scheduled'
READY = 'READY', 'Ready to Start'
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
COMPLETED = 'COMPLETED', 'Completed'
CANCELLED = 'CANCELLED', 'Cancelled'
FAILED = 'FAILED', 'Failed'
# Session Information
session_id = models.UUIDField(
default=uuid.uuid4,
@ -913,7 +918,7 @@ class TelemedicineSession(models.Model):
# Platform Information
platform = models.CharField(
max_length=50,
choices=PLATFORM_CHOICES,
choices=Platform.choices,
help_text='Telemedicine platform'
)
@ -959,8 +964,8 @@ class TelemedicineSession(models.Model):
# Session Status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='SCHEDULED',
choices=SessionStatus.choices,
default=SessionStatus.SCHEDULED,
help_text='Session status'
)
@ -1182,85 +1187,85 @@ class WaitingList(models.Model):
Patient waiting list for appointment scheduling.
Follows healthcare industry standards for patient queue management.
"""
APPOINTMENT_TYPE_CHOICES = [
('CONSULTATION', 'Consultation'),
('FOLLOW_UP', 'Follow-up'),
('PROCEDURE', 'Procedure'),
('SURGERY', 'Surgery'),
('DIAGNOSTIC', 'Diagnostic'),
('THERAPY', 'Therapy'),
('VACCINATION', 'Vaccination'),
('SCREENING', 'Screening'),
('EMERGENCY', 'Emergency'),
('TELEMEDICINE', 'Telemedicine'),
('OTHER', 'Other'),
]
SPECIALTY_CHOICES = [
('FAMILY_MEDICINE', 'Family Medicine'),
('INTERNAL_MEDICINE', 'Internal Medicine'),
('PEDIATRICS', 'Pediatrics'),
('CARDIOLOGY', 'Cardiology'),
('DERMATOLOGY', 'Dermatology'),
('ENDOCRINOLOGY', 'Endocrinology'),
('GASTROENTEROLOGY', 'Gastroenterology'),
('NEUROLOGY', 'Neurology'),
('ONCOLOGY', 'Oncology'),
('ORTHOPEDICS', 'Orthopedics'),
('PSYCHIATRY', 'Psychiatry'),
('RADIOLOGY', 'Radiology'),
('SURGERY', 'Surgery'),
('UROLOGY', 'Urology'),
('GYNECOLOGY', 'Gynecology'),
('OPHTHALMOLOGY', 'Ophthalmology'),
('ENT', 'Ear, Nose & Throat'),
('EMERGENCY', 'Emergency Medicine'),
('OTHER', 'Other'),
]
PRIORITY_CHOICES = [
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('STAT', 'STAT'),
('EMERGENCY', 'Emergency'),
]
CONTACT_METHOD_CHOICES = [
('PHONE', 'Phone'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
('PORTAL', 'Patient Portal'),
('MAIL', 'Mail'),
]
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('CONTACTED', 'Contacted'),
('OFFERED', 'Appointment Offered'),
('SCHEDULED', 'Scheduled'),
('CANCELLED', 'Cancelled'),
('EXPIRED', 'Expired'),
('TRANSFERRED', 'Transferred'),
]
AUTHORIZATION_STATUS_CHOICES = [
('NOT_REQUIRED', 'Not Required'),
('PENDING', 'Pending'),
('APPROVED', 'Approved'),
('DENIED', 'Denied'),
('EXPIRED', 'Expired'),
]
REFERRAL_URGENCY_CHOICES = [
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('STAT', 'STAT'),
]
REMOVAL_REASON_CHOICES = [
('SCHEDULED', 'Appointment Scheduled'),
('PATIENT_CANCELLED', 'Patient Cancelled'),
('PROVIDER_CANCELLED', 'Provider Cancelled'),
('NO_RESPONSE', 'No Response to Contact'),
('INSURANCE_ISSUE', 'Insurance Issue'),
('TRANSFERRED', 'Transferred to Another Provider'),
('EXPIRED', 'Entry Expired'),
('DUPLICATE', 'Duplicate Entry'),
('OTHER', 'Other'),
]
class AppointmentType(models.TextChoices):
CONSULTATION = 'CONSULTATION', 'Consultation'
FOLLOW_UP = 'FOLLOW_UP', 'Follow-up'
PROCEDURE = 'PROCEDURE', 'Procedure'
SURGERY = 'SURGERY', 'Surgery'
DIAGNOSTIC = 'DIAGNOSTIC', 'Diagnostic'
THERAPY = 'THERAPY', 'Therapy'
VACCINATION = 'VACCINATION', 'Vaccination'
SCREENING = 'SCREENING', 'Screening'
EMERGENCY = 'EMERGENCY', 'Emergency'
TELEMEDICINE = 'TELEMEDICINE', 'Telemedicine'
OTHER = 'OTHER', 'Other'
class Specialty(models.TextChoices):
FAMILY_MEDICINE = 'FAMILY_MEDICINE', 'Family Medicine'
INTERNAL_MEDICINE = 'INTERNAL_MEDICINE', 'Internal Medicine'
PEDIATRICS = 'PEDIATRICS', 'Pediatrics'
CARDIOLOGY = 'CARDIOLOGY', 'Cardiology'
DERMATOLOGY = 'DERMATOLOGY', 'Dermatology'
ENDOCRINOLOGY = 'ENDOCRINOLOGY', 'Endocrinology'
GASTROENTEROLOGY = 'GASTROENTEROLOGY', 'Gastroenterology'
NEUROLOGY = 'NEUROLOGY', 'Neurology'
ONCOLOGY = 'ONCOLOGY', 'Oncology'
ORTHOPEDICS = 'ORTHOPEDICS', 'Orthopedics'
PSYCHIATRY = 'PSYCHIATRY', 'Psychiatry'
RADIOLOGY = 'RADIOLOGY', 'Radiology'
SURGERY = 'SURGERY', 'Surgery'
UROLOGY = 'UROLOGY', 'Urology'
GYNECOLOGY = 'GYNECOLOGY', 'Gynecology'
OPHTHALMOLOGY = 'OPHTHALMOLOGY', 'Ophthalmology'
ENT = 'ENT', 'Ear, Nose & Throat'
EMERGENCY = 'EMERGENCY', 'Emergency Medicine'
OTHER = 'OTHER', 'Other'
class Priority(models.TextChoices):
ROUTINE = 'ROUTINE', 'Routine'
URGENT = 'URGENT', 'Urgent'
STAT = 'STAT', 'STAT'
EMERGENCY = 'EMERGENCY', 'Emergency'
class ContactMethod(models.TextChoices):
PHONE = 'PHONE', 'Phone'
EMAIL = 'EMAIL', 'Email'
SMS = 'SMS', 'SMS'
PORTAL = 'PORTAL', 'Patient Portal'
MAIL = 'MAIL', 'Mail'
class RequestStatus(models.TextChoices):
ACTIVE = 'ACTIVE', 'Active'
CONTACTED = 'CONTACTED', 'Contacted'
OFFERED = 'OFFERED', 'Appointment Offered'
SCHEDULED = 'SCHEDULED', 'Scheduled'
CANCELLED = 'CANCELLED', 'Cancelled'
EXPIRED = 'EXPIRED', 'Expired'
TRANSFERRED = 'TRANSFERRED', 'Transferred'
class AuthorizationStatus(models.TextChoices):
NOT_REQUIRED = 'NOT_REQUIRED', 'Not Required'
PENDING = 'PENDING', 'Pending'
APPROVED = 'APPROVED', 'Approved'
DENIED = 'DENIED', 'Denied'
EXPIRED = 'EXPIRED', 'Expired'
class ReferralUrgency(models.TextChoices):
ROUTINE = 'ROUTINE', 'Routine'
URGENT = 'URGENT', 'Urgent'
STAT = 'STAT', 'STAT'
class RemovalReason(models.TextChoices):
SCHEDULED = 'SCHEDULED', 'Appointment Scheduled'
PATIENT_CANCELLED = 'PATIENT_CANCELLED', 'Patient Cancelled'
PROVIDER_CANCELLED = 'PROVIDER_CANCELLED', 'Provider Cancelled'
NO_RESPONSE = 'NO_RESPONSE', 'No Response to Contact'
INSURANCE_ISSUE = 'INSURANCE_ISSUE', 'Insurance Issue'
TRANSFERRED = 'TRANSFERRED', 'Transferred to Another Provider'
EXPIRED = 'EXPIRED', 'Entry Expired'
DUPLICATE = 'DUPLICATE', 'Duplicate Entry'
OTHER = 'OTHER', 'Other'
# Basic Identifiers
waiting_list_id = models.UUIDField(
@ -1305,21 +1310,21 @@ class WaitingList(models.Model):
appointment_type = models.CharField(
max_length=50,
choices=APPOINTMENT_TYPE_CHOICES,
choices=AppointmentType.choices,
help_text='Type of appointment requested'
)
specialty = models.CharField(
max_length=100,
choices=SPECIALTY_CHOICES,
choices=Specialty.choices,
help_text='Medical specialty required'
)
# Priority and Clinical Information
priority = models.CharField(
max_length=20,
choices=PRIORITY_CHOICES,
default='ROUTINE',
choices=Priority.choices,
default=Priority.ROUTINE,
help_text='Clinical priority level'
)
@ -1385,8 +1390,8 @@ class WaitingList(models.Model):
# Communication Preferences
contact_method = models.CharField(
max_length=20,
choices=CONTACT_METHOD_CHOICES,
default='PHONE',
choices=ContactMethod.choices,
default=ContactMethod.PHONE,
help_text='Preferred contact method'
)
@ -1406,8 +1411,8 @@ class WaitingList(models.Model):
# Status and Workflow
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='ACTIVE',
choices=RequestStatus.choices,
default=RequestStatus.ACTIVE,
help_text='Waiting list status'
)
@ -1495,8 +1500,8 @@ class WaitingList(models.Model):
authorization_status = models.CharField(
max_length=20,
choices=AUTHORIZATION_STATUS_CHOICES,
default='NOT_REQUIRED',
choices=AuthorizationStatus.choices,
default=AuthorizationStatus.NOT_REQUIRED,
help_text='Authorization status'
)
@ -1523,8 +1528,8 @@ class WaitingList(models.Model):
referral_urgency = models.CharField(
max_length=20,
choices=REFERRAL_URGENCY_CHOICES,
default='ROUTINE',
choices=ReferralUrgency.choices,
default=ReferralUrgency.ROUTINE,
help_text='Referral urgency level'
)
@ -1540,7 +1545,7 @@ class WaitingList(models.Model):
removal_reason = models.CharField(
max_length=50,
choices=REMOVAL_REASON_CHOICES,
choices=RemovalReason.choices,
blank=True,
null=True,
help_text='Reason for removal from waiting list'
@ -1701,35 +1706,35 @@ class WaitingListContactLog(models.Model):
Contact log for waiting list entries.
Tracks all communication attempts with patients on waiting list.
"""
CONTACT_METHOD_CHOICES = [
('PHONE', 'Phone Call'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
('PORTAL', 'Patient Portal Message'),
('MAIL', 'Mail'),
('IN_PERSON', 'In Person'),
]
CONTACT_OUTCOME_CHOICES = [
('SUCCESSFUL', 'Successful Contact'),
('NO_ANSWER', 'No Answer'),
('BUSY', 'Line Busy'),
('VOICEMAIL', 'Left Voicemail'),
('EMAIL_SENT', 'Email Sent'),
('EMAIL_BOUNCED', 'Email Bounced'),
('SMS_SENT', 'SMS Sent'),
('SMS_FAILED', 'SMS Failed'),
('WRONG_NUMBER', 'Wrong Number'),
('DECLINED', 'Patient Declined'),
]
PATIENT_RESPONSE_CHOICES = [
('ACCEPTED', 'Accepted Appointment'),
('DECLINED', 'Declined Appointment'),
('REQUESTED_DIFFERENT', 'Requested Different Time'),
('WILL_CALL_BACK', 'Will Call Back'),
('NO_LONGER_NEEDED', 'No Longer Needed'),
('INSURANCE_ISSUE', 'Insurance Issue'),
('NO_RESPONSE', 'No Response'),
]
class ContactMethod(models.TextChoices):
PHONE = 'PHONE', 'Phone Call'
EMAIL = 'EMAIL', 'Email'
SMS = 'SMS', 'SMS'
PORTAL = 'PORTAL', 'Patient Portal Message'
MAIL = 'MAIL', 'Mail'
IN_PERSON = 'IN_PERSON', 'In Person'
class ContactOutcome(models.TextChoices):
SUCCESSFUL = 'SUCCESSFUL', 'Successful Contact'
NO_ANSWER = 'NO_ANSWER', 'No Answer'
BUSY = 'BUSY', 'Line Busy'
VOICEMAIL = 'VOICEMAIL', 'Left Voicemail'
EMAIL_SENT = 'EMAIL_SENT', 'Email Sent'
EMAIL_BOUNCED = 'EMAIL_BOUNCED', 'Email Bounced'
SMS_SENT = 'SMS_SENT', 'SMS Sent'
SMS_FAILED = 'SMS_FAILED', 'SMS Failed'
WRONG_NUMBER = 'WRONG_NUMBER', 'Wrong Number'
DECLINED = 'DECLINED', 'Patient Declined'
class PatientResponse(models.TextChoices):
ACCEPTED = 'ACCEPTED', 'Accepted Appointment'
DECLINED = 'DECLINED', 'Declined Appointment'
REQUESTED_DIFFERENT = 'REQUESTED_DIFFERENT', 'Requested Different Time'
WILL_CALL_BACK = 'WILL_CALL_BACK', 'Will Call Back'
NO_LONGER_NEEDED = 'NO_LONGER_NEEDED', 'No Longer Needed'
INSURANCE_ISSUE = 'INSURANCE_ISSUE', 'Insurance Issue'
NO_RESPONSE = 'NO_RESPONSE', 'No Response'
waiting_list_entry = models.ForeignKey(
WaitingList,
@ -1745,13 +1750,13 @@ class WaitingListContactLog(models.Model):
contact_method = models.CharField(
max_length=20,
choices=CONTACT_METHOD_CHOICES,
choices=ContactMethod.choices,
help_text='Method of contact used'
)
contact_outcome = models.CharField(
max_length=20,
choices=CONTACT_OUTCOME_CHOICES,
choices=ContactOutcome.choices,
help_text='Outcome of contact attempt'
)
@ -1774,7 +1779,7 @@ class WaitingListContactLog(models.Model):
patient_response = models.CharField(
max_length=20,
choices=PATIENT_RESPONSE_CHOICES,
choices=PatientResponse.choices,
blank=True,
null=True,
help_text='Patient response to contact'

View File

@ -3,9 +3,9 @@
{% block title %}Slot Availability - Appointment Management{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/fullcalendar/main.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/select2/css/select2.min.css' %}" rel="stylesheet" />
{% block css %}
<link href="{% static 'plugins/@fullcalendar/main.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/select2/css/select2.min.css' %}" rel="stylesheet" />
<style>
.availability-header {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);

View File

@ -18,7 +18,7 @@ from core.models import Tenant
from hr.models import Employee, Department
import uuid
User = get_user_model()
from accounts.models import User
# Saudi-specific healthcare data
SAUDI_SPECIALTIES = [
@ -134,8 +134,8 @@ def get_providers_for_tenant(tenants):
if not providers:
for tenant in tenants:
tenant_providers = User.objects.filter(
employee_profile__tenant=tenant,
role__in=['PHYSICIAN', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'RADIOLOGIST', 'SURGEON']
tenant=tenant,
employee_profile__role__in=['PHYSICIAN', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'RADIOLOGIST', 'SURGEON']
).select_related('employee_profile')
providers.extend(list(tenant_providers))
@ -145,7 +145,7 @@ def get_providers_for_tenant(tenants):
tenant_providers = User.objects.filter(
tenant=tenant,
is_active=True,
role__icontains='PHYSICIAN'
employee_profile__role__icontains='PHYSICIAN'
)
providers.extend(list(tenant_providers))

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import billing.utils
import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.db.models.deletion
from django.conf import settings
@ -12,8 +12,6 @@ class Migration(migrations.Migration):
dependencies = [
("billing", "0001_initial"),
("core", "0001_initial"),
("emr", "0001_initial"),
("inpatients", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
@ -109,310 +107,4 @@ class Migration(migrations.Migration):
to="billing.insuranceclaim",
),
),
migrations.AddField(
model_name="medicalbill",
name="admission",
field=models.ForeignKey(
blank=True,
help_text="Related admission",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="medical_bills",
to="inpatients.admission",
),
),
migrations.AddField(
model_name="medicalbill",
name="attending_provider",
field=models.ForeignKey(
blank=True,
help_text="Attending provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="attending_bills",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="medicalbill",
name="billing_provider",
field=models.ForeignKey(
blank=True,
help_text="Billing provider",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="billing_provider_bills",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="medicalbill",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the bill",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_medical_bills",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="medicalbill",
name="encounter",
field=models.ForeignKey(
blank=True,
help_text="Related encounter",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="medical_bills",
to="emr.encounter",
),
),
migrations.AddField(
model_name="medicalbill",
name="patient",
field=models.ForeignKey(
help_text="Patient",
on_delete=django.db.models.deletion.CASCADE,
related_name="medical_bills",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="medicalbill",
name="primary_insurance",
field=models.ForeignKey(
blank=True,
help_text="Primary insurance",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="primary_bills",
to="patients.insuranceinfo",
),
),
migrations.AddField(
model_name="medicalbill",
name="secondary_insurance",
field=models.ForeignKey(
blank=True,
help_text="Secondary insurance",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="secondary_bills",
to="patients.insuranceinfo",
),
),
migrations.AddField(
model_name="medicalbill",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="medical_bills",
to="core.tenant",
),
),
migrations.AddField(
model_name="insuranceclaim",
name="medical_bill",
field=models.ForeignKey(
help_text="Related medical bill",
on_delete=django.db.models.deletion.CASCADE,
related_name="insurance_claims",
to="billing.medicalbill",
),
),
migrations.AddField(
model_name="billlineitem",
name="medical_bill",
field=models.ForeignKey(
help_text="Medical bill",
on_delete=django.db.models.deletion.CASCADE,
related_name="line_items",
to="billing.medicalbill",
),
),
migrations.AddField(
model_name="payment",
name="insurance_claim",
field=models.ForeignKey(
blank=True,
help_text="Related insurance claim",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="payments",
to="billing.insuranceclaim",
),
),
migrations.AddField(
model_name="payment",
name="medical_bill",
field=models.ForeignKey(
help_text="Related medical bill",
on_delete=django.db.models.deletion.CASCADE,
related_name="payments",
to="billing.medicalbill",
),
),
migrations.AddField(
model_name="payment",
name="processed_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who processed payment",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="processed_payments",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="payment",
name="received_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who received payment",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="received_payments",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddIndex(
model_name="claimstatusupdate",
index=models.Index(
fields=["insurance_claim"], name="billing_cla_insuran_7c9e49_idx"
),
),
migrations.AddIndex(
model_name="claimstatusupdate",
index=models.Index(
fields=["status_date"], name="billing_cla_status__49d81f_idx"
),
),
migrations.AddIndex(
model_name="claimstatusupdate",
index=models.Index(
fields=["new_status"], name="billing_cla_new_sta_9c28d3_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["tenant", "status"], name="billing_med_tenant__fe8d14_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["patient", "bill_date"], name="billing_med_patient_8f1a85_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["bill_number"], name="billing_med_bill_nu_f01dfa_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["status", "due_date"], name="billing_med_status_cde77f_idx"
),
),
migrations.AddIndex(
model_name="medicalbill",
index=models.Index(
fields=["collection_status"], name="billing_med_collect_6d0faf_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["medical_bill"], name="billing_ins_medical_1fec52_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["insurance_info"], name="billing_ins_insuran_e54611_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["claim_number"], name="billing_ins_claim_n_becaa3_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["status", "submission_date"],
name="billing_ins_status_921ea7_idx",
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["response_date"], name="billing_ins_respons_fc4f3d_idx"
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["medical_bill", "line_number"],
name="billing_bil_medical_37a377_idx",
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["service_code"], name="billing_bil_service_b88f5b_idx"
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["service_date"], name="billing_bil_service_658c36_idx"
),
),
migrations.AddIndex(
model_name="billlineitem",
index=models.Index(
fields=["rendering_provider"], name="billing_bil_renderi_2740ad_idx"
),
),
migrations.AlterUniqueTogether(
name="billlineitem",
unique_together={("medical_bill", "line_number")},
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["medical_bill"], name="billing_pay_medical_e4e348_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["payment_number"], name="billing_pay_payment_0825d6_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["payment_date"], name="billing_pay_payment_bed741_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(
fields=["payment_method"], name="billing_pay_payment_715e62_idx"
),
),
migrations.AddIndex(
model_name="payment",
index=models.Index(fields=["status"], name="billing_pay_status_b7739e_idx"),
),
]

View File

@ -0,0 +1,328 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
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"),
),
]

View File

@ -15,12 +15,10 @@ from core.models import Tenant
from patients.models import PatientProfile, InsuranceInfo
from emr.models import Encounter
from inpatients.models import Admission
from billing.models import (
MedicalBill, BillLineItem, InsuranceClaim, Payment,
ClaimStatusUpdate, BillingConfiguration
)
from billing.models import *
from accounts.models import User
User = get_user_model()
from django.db import IntegrityError, transaction
from django.db.models import DecimalField, CharField, IntegerField

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.core.validators
import django.db.models.deletion
@ -31,12 +31,12 @@ class Migration(migrations.Migration):
"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"),
("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,
@ -68,15 +68,15 @@ class Migration(migrations.Migration):
"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"),
("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,
),
@ -85,12 +85,12 @@ class Migration(migrations.Migration):
"result",
models.CharField(
choices=[
("positive", "Positive"),
("negative", "Negative"),
("indeterminate", "Indeterminate"),
("pending", "Pending"),
("POSITIVE", "Positive"),
("NEGATIVE", "Negative"),
("INDETERMINATE", "Indeterminate"),
("PENDING", "Pending"),
],
default="pending",
default="PENDING",
max_length=15,
),
),
@ -124,17 +124,17 @@ class Migration(migrations.Migration):
"status",
models.CharField(
choices=[
("collected", "Collected"),
("testing", "Testing"),
("quarantine", "Quarantine"),
("available", "Available"),
("reserved", "Reserved"),
("issued", "Issued"),
("transfused", "Transfused"),
("expired", "Expired"),
("discarded", "Discarded"),
("COLLECTED", "Collected"),
("TESTING", "Testing"),
("QUARANTINE", "Quarantine"),
("AVAILABLE", "Available"),
("RESERVED", "Reserved"),
("ISSUED", "Issued"),
("TRANSFUSED", "Transfused"),
("EXPIRED", "Expired"),
("DISCARDED", "Discarded"),
],
default="collected",
default="COLLECTED",
max_length=20,
),
),
@ -166,10 +166,10 @@ class Migration(migrations.Migration):
"test_type",
models.CharField(
choices=[
("major", "Major Crossmatch"),
("minor", "Minor Crossmatch"),
("immediate_spin", "Immediate Spin"),
("antiglobulin", "Antiglobulin Test"),
("MAJOR", "Major Crossmatch"),
("MINOR", "Minor Crossmatch"),
("IMMEDIATE_SPIN", "Immediate Spin"),
("ANTIGLOBULIN", "Antiglobulin Test"),
],
max_length=20,
),
@ -178,11 +178,11 @@ class Migration(migrations.Migration):
"compatibility",
models.CharField(
choices=[
("compatible", "Compatible"),
("incompatible", "Incompatible"),
("pending", "Pending"),
("COMPATIBLE", "Compatible"),
("INCOMPATIBLE", "Incompatible"),
("PENDING", "Pending"),
],
default="pending",
default="PENDING",
max_length=15,
),
),
@ -215,7 +215,11 @@ class Migration(migrations.Migration):
(
"gender",
models.CharField(
choices=[("M", "Male"), ("F", "Female"), ("O", "Other")],
choices=[
("MALE", "Male"),
("FEMALE", "Female"),
("OTHER", "Other"),
],
max_length=10,
),
),
@ -229,12 +233,12 @@ class Migration(migrations.Migration):
"donor_type",
models.CharField(
choices=[
("voluntary", "Voluntary"),
("replacement", "Replacement"),
("autologous", "Autologous"),
("directed", "Directed"),
("VOLUNTARY", "Voluntary"),
("REPLACEMENT", "Replacement"),
("AUTOLOGOUS", "Autologous"),
("DIRECTED", "Directed"),
],
default="voluntary",
default="VOLUNTARY",
max_length=20,
),
),
@ -242,12 +246,12 @@ class Migration(migrations.Migration):
"status",
models.CharField(
choices=[
("active", "Active"),
("deferred", "Deferred"),
("permanently_deferred", "Permanently Deferred"),
("inactive", "Inactive"),
("ACTIVE", "Active"),
("DEFERRED", "Deferred"),
("PERMANENTLY_DEFERRED", "Permanently Deferred"),
("INACTIVE", "Inactive"),
],
default="active",
default="ACTIVE",
max_length=20,
),
),
@ -291,11 +295,11 @@ class Migration(migrations.Migration):
"location_type",
models.CharField(
choices=[
("refrigerator", "Refrigerator"),
("freezer", "Freezer"),
("platelet_agitator", "Platelet Agitator"),
("quarantine", "Quarantine"),
("testing", "Testing Area"),
("REFRIGERATOR", "Refrigerator"),
("FREEZER", "Freezer"),
("PLATELET_AGITATOR", "Platelet Agitator"),
("QUARANTINE", "Quarantine"),
("TESTING", "Testing Area"),
],
max_length=20,
),
@ -327,11 +331,11 @@ class Migration(migrations.Migration):
"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"),
("TEMPERATURE_MONITORING", "Temperature Monitoring"),
("EQUIPMENT_CALIBRATION", "Equipment Calibration"),
("REAGENT_TESTING", "Reagent Testing"),
("PROFICIENCY_TESTING", "Proficiency Testing"),
("PROCESS_VALIDATION", "Process Validation"),
],
max_length=30,
),
@ -345,9 +349,9 @@ class Migration(migrations.Migration):
"status",
models.CharField(
choices=[
("pass", "Pass"),
("fail", "Fail"),
("pending", "Pending"),
("PASS", "Pass"),
("FAIL", "Fail"),
("PENDING", "Pending"),
],
max_length=10,
),
@ -363,9 +367,9 @@ class Migration(migrations.Migration):
models.CharField(
blank=True,
choices=[
("low", "Low"),
("medium", "Medium"),
("high", "High"),
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
],
max_length=10,
),
@ -377,9 +381,9 @@ class Migration(migrations.Migration):
models.CharField(
blank=True,
choices=[
("open", "Open"),
("in_progress", "In Progress"),
("closed", "Closed"),
("OPEN", "Open"),
("IN_PROGRESS", "In Progress"),
("CLOSED", "Closed"),
],
max_length=20,
),
@ -407,13 +411,13 @@ class Migration(migrations.Migration):
"status",
models.CharField(
choices=[
("started", "Started"),
("in_progress", "In Progress"),
("completed", "Completed"),
("stopped", "Stopped"),
("adverse_reaction", "Adverse Reaction"),
("STARTED", "Started"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("STOPPED", "Stopped"),
("ADVERSE_REACTION", "Adverse Reaction"),
],
default="started",
default="STARTED",
max_length=20,
),
),
@ -463,15 +467,15 @@ class Migration(migrations.Migration):
"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"),
("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,
),
@ -480,10 +484,10 @@ class Migration(migrations.Migration):
"severity",
models.CharField(
choices=[
("mild", "Mild"),
("moderate", "Moderate"),
("severe", "Severe"),
("life_threatening", "Life Threatening"),
("MILD", "Mild"),
("MODERATE", "Moderate"),
("SEVERE", "Severe"),
("LIFE_THREATENING", "Life Threatening"),
],
max_length=20,
),
@ -611,11 +615,11 @@ class Migration(migrations.Migration):
"urgency",
models.CharField(
choices=[
("routine", "Routine"),
("urgent", "Urgent"),
("emergency", "Emergency"),
("ROUTINE", "Routine"),
("URGENT", "Urgent"),
("EMERGENCY", "Emergency"),
],
default="routine",
default="ROUTINE",
max_length=10,
),
),
@ -627,14 +631,14 @@ class Migration(migrations.Migration):
"status",
models.CharField(
choices=[
("pending", "Pending"),
("processing", "Processing"),
("ready", "Ready"),
("issued", "Issued"),
("completed", "Completed"),
("cancelled", "Cancelled"),
("PENDING", "Pending"),
("PROCESSING", "Processing"),
("READY", "Ready"),
("ISSUED", "Issued"),
("COMPLETED", "Completed"),
("CANCELLED", "Cancelled"),
],
default="pending",
default="PENDING",
max_length=15,
),
),

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.db.models.deletion
from django.conf import settings

View File

@ -1,275 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-15 16:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("blood_bank", "0002_initial"),
]
operations = [
migrations.AlterField(
model_name="adversereaction",
name="reaction_type",
field=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,
),
),
migrations.AlterField(
model_name="adversereaction",
name="severity",
field=models.CharField(
choices=[
("MILD", "Mild"),
("MODERATE", "Moderate"),
("SEVERE", "Severe"),
("LIFE_THREATENING", "Life Threatening"),
],
max_length=20,
),
),
migrations.AlterField(
model_name="bloodcomponent",
name="name",
field=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,
),
),
migrations.AlterField(
model_name="bloodrequest",
name="status",
field=models.CharField(
choices=[
("PENDING", "Pending"),
("PROCESSING", "Processing"),
("READY", "Ready"),
("ISSUED", "Issued"),
("COMPLETED", "Completed"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
max_length=15,
),
),
migrations.AlterField(
model_name="bloodrequest",
name="urgency",
field=models.CharField(
choices=[
("ROUTINE", "Routine"),
("URGENT", "Urgent"),
("EMERGENCY", "Emergency"),
],
default="ROUTINE",
max_length=10,
),
),
migrations.AlterField(
model_name="bloodtest",
name="result",
field=models.CharField(
choices=[
("POSITIVE", "Positive"),
("NEGATIVE", "Negative"),
("INDETERMINATE", "Indeterminate"),
("PENDING", "Pending"),
],
default="PENDING",
max_length=15,
),
),
migrations.AlterField(
model_name="bloodtest",
name="test_type",
field=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,
),
),
migrations.AlterField(
model_name="bloodunit",
name="status",
field=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,
),
),
migrations.AlterField(
model_name="crossmatch",
name="compatibility",
field=models.CharField(
choices=[
("COMPATIBLE", "Compatible"),
("INCOMPATIBLE", "Incompatible"),
("PENDING", "Pending"),
],
default="PENDING",
max_length=15,
),
),
migrations.AlterField(
model_name="crossmatch",
name="test_type",
field=models.CharField(
choices=[
("MAJOR", "Major Crossmatch"),
("MINOR", "Minor Crossmatch"),
("IMMEDIATE_SPIN", "Immediate Spin"),
("ANTIGLOBULIN", "Antiglobulin Test"),
],
max_length=20,
),
),
migrations.AlterField(
model_name="donor",
name="donor_type",
field=models.CharField(
choices=[
("VOLUNTARY", "Voluntary"),
("REPLACEMENT", "Replacement"),
("AUTOLOGOUS", "Autologous"),
("DIRECTED", "Directed"),
],
default="VOLUNTARY",
max_length=20,
),
),
migrations.AlterField(
model_name="donor",
name="gender",
field=models.CharField(
choices=[("MALE", "Male"), ("FEMALE", "Female"), ("OTHER", "Other")],
max_length=10,
),
),
migrations.AlterField(
model_name="donor",
name="status",
field=models.CharField(
choices=[
("ACTIVE", "Active"),
("DEFERRED", "Deferred"),
("PERMANENTLY_DEFERRED", "Permanently Deferred"),
("INACTIVE", "Inactive"),
],
default="ACTIVE",
max_length=20,
),
),
migrations.AlterField(
model_name="inventorylocation",
name="location_type",
field=models.CharField(
choices=[
("REFRIGERATOR", "Refrigerator"),
("FREEZER", "Freezer"),
("PLATELET_AGITATOR", "Platelet Agitator"),
("QUARANTINE", "Quarantine"),
("TESTING", "Testing Area"),
],
max_length=20,
),
),
migrations.AlterField(
model_name="qualitycontrol",
name="capa_priority",
field=models.CharField(
blank=True,
choices=[("LOW", "Low"), ("MEDIUM", "Medium"), ("HIGH", "High")],
max_length=10,
),
),
migrations.AlterField(
model_name="qualitycontrol",
name="capa_status",
field=models.CharField(
blank=True,
choices=[
("OPEN", "Open"),
("IN_PROGRESS", "In Progress"),
("CLOSED", "Closed"),
],
max_length=20,
),
),
migrations.AlterField(
model_name="qualitycontrol",
name="status",
field=models.CharField(
choices=[("PASS", "Pass"), ("FAIL", "Fail"), ("PENDING", "Pending")],
max_length=10,
),
),
migrations.AlterField(
model_name="qualitycontrol",
name="test_type",
field=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,
),
),
migrations.AlterField(
model_name="transfusion",
name="status",
field=models.CharField(
choices=[
("STARTED", "Started"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("STOPPED", "Stopped"),
("ADVERSE_REACTION", "Adverse Reaction"),
],
default="STARTED",
max_length=20,
),
),
]

View File

@ -1,11 +1,11 @@
from django.db import models
# from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils import timezone
from datetime import timedelta
from hr.models import Department
from patients.models import PatientProfile
from accounts.models import User
from django.conf import settings
class BloodGroup(models.Model):
"""Blood group types (A, B, AB, O) with Rh factor"""
@ -71,7 +71,7 @@ class Donor(models.Model):
weight = models.FloatField(validators=[MinValueValidator(45.0)]) # Minimum weight for donation
height = models.FloatField(validators=[MinValueValidator(140.0)]) # In cm
notes = models.TextField(blank=True)
created_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='created_donors')
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='created_donors')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@ -164,7 +164,7 @@ class BloodUnit(models.Model):
bag_type = models.CharField(max_length=50)
anticoagulant = models.CharField(max_length=50, default='CPDA-1')
collection_site = models.CharField(max_length=100)
collected_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='collected_units')
collected_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='collected_units')
notes = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@ -213,11 +213,11 @@ class BloodTest(models.Model):
test_type = models.CharField(max_length=20, choices=TestType.choices)
result = models.CharField(max_length=15, choices=TestResult.choices, default=TestResult.PENDING)
test_date = models.DateTimeField()
tested_by = models.ForeignKey(User, on_delete=models.PROTECT)
tested_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
equipment_used = models.CharField(max_length=100, blank=True)
lot_number = models.CharField(max_length=50, blank=True)
notes = models.TextField(blank=True)
verified_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='verified_tests', null=True,
verified_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='verified_tests', null=True,
blank=True)
verified_at = models.DateTimeField(null=True, blank=True)
@ -248,11 +248,11 @@ class CrossMatch(models.Model):
test_type = models.CharField(max_length=20, choices=CrossmatchTestType.choices)
compatibility = models.CharField(max_length=15, choices=CompatibilityStatus.choices, default=CompatibilityStatus.PENDING)
test_date = models.DateTimeField()
tested_by = models.ForeignKey(User, on_delete=models.PROTECT)
tested_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
temperature = models.CharField(max_length=20, default='37°C')
incubation_time = models.PositiveIntegerField(default=15) # minutes
notes = models.TextField(blank=True)
verified_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='verified_crossmatches', null=True,
verified_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='verified_crossmatches', null=True,
blank=True)
verified_at = models.DateTimeField(null=True, blank=True)
@ -281,7 +281,7 @@ class BloodRequest(models.Model):
request_number = models.CharField(max_length=20, unique=True)
patient = models.ForeignKey(PatientProfile, on_delete=models.PROTECT, related_name='blood_requests')
requesting_department = models.ForeignKey(Department, on_delete=models.PROTECT)
requesting_physician = models.ForeignKey(User, on_delete=models.PROTECT, related_name='blood_requests')
requesting_physician = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='blood_requests')
component_requested = models.ForeignKey(BloodComponent, on_delete=models.PROTECT)
units_requested = models.PositiveIntegerField(validators=[MinValueValidator(1)])
urgency = models.CharField(max_length=10, choices=UrgencyLevel.choices, default=UrgencyLevel.ROUTINE)
@ -293,14 +293,14 @@ class BloodRequest(models.Model):
status = models.CharField(max_length=15, choices=RequestStatus.choices, default=RequestStatus.PENDING)
request_date = models.DateTimeField(auto_now_add=True)
required_by = models.DateTimeField()
processed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='processed_requests', null=True,
processed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='processed_requests', null=True,
blank=True)
processed_at = models.DateTimeField(null=True, blank=True)
notes = models.TextField(blank=True)
# Cancellation fields
cancellation_reason = models.TextField(blank=True)
cancelled_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='cancelled_requests', null=True,
cancelled_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='cancelled_requests', null=True,
blank=True)
cancellation_date = models.DateTimeField(null=True, blank=True)
@ -320,8 +320,8 @@ class BloodIssue(models.Model):
blood_request = models.ForeignKey(BloodRequest, on_delete=models.PROTECT, related_name='issues')
blood_unit = models.OneToOneField(BloodUnit, on_delete=models.PROTECT, related_name='issue')
crossmatch = models.ForeignKey(CrossMatch, on_delete=models.PROTECT, null=True, blank=True)
issued_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='issued_units')
issued_to = models.ForeignKey(User, on_delete=models.PROTECT, related_name='received_units') # Nurse/physician
issued_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='issued_units')
issued_to = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='received_units') # Nurse/physician
issue_date = models.DateTimeField(auto_now_add=True)
expiry_time = models.DateTimeField() # 4 hours from issue for RBC
returned = models.BooleanField(default=False)
@ -356,8 +356,8 @@ class Transfusion(models.Model):
status = models.CharField(max_length=20, choices=TransfusionStatus.choices, default=TransfusionStatus.STARTED)
volume_transfused = models.PositiveIntegerField(null=True, blank=True) # ml
transfusion_rate = models.CharField(max_length=50, blank=True) # ml/hour
administered_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='administered_transfusions')
witnessed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='witnessed_transfusions', null=True,
administered_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='administered_transfusions')
witnessed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='witnessed_transfusions', null=True,
blank=True)
pre_transfusion_vitals = models.JSONField(default=dict) # BP, HR, Temp, etc.
post_transfusion_vitals = models.JSONField(default=dict)
@ -374,9 +374,9 @@ class Transfusion(models.Model):
# Completion/Stop fields
stop_reason = models.TextField(blank=True)
stopped_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='stopped_transfusions', null=True,
stopped_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='stopped_transfusions', null=True,
blank=True)
completed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='completed_transfusions', null=True,
completed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='completed_transfusions', null=True,
blank=True)
completion_notes = models.TextField(blank=True)
@ -420,8 +420,8 @@ class AdverseReaction(models.Model):
symptoms = models.TextField()
treatment_given = models.TextField()
outcome = models.TextField()
reported_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='reported_reactions')
investigated_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='investigated_reactions',
reported_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='reported_reactions')
investigated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='investigated_reactions',
null=True, blank=True)
investigation_notes = models.TextField(blank=True)
regulatory_reported = models.BooleanField(default=False)
@ -488,8 +488,8 @@ class QualityControl(models.Model):
expected_results = models.TextField()
actual_results = models.TextField()
status = models.CharField(max_length=10, choices=QualityTestStatus.choices)
performed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='qc_tests')
reviewed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='reviewed_qc_tests', null=True,
performed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='qc_tests')
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='reviewed_qc_tests', null=True,
blank=True)
review_date = models.DateTimeField(null=True, blank=True)
review_notes = models.TextField(blank=True)
@ -501,7 +501,7 @@ class QualityControl(models.Model):
capa_number = models.CharField(max_length=50, blank=True)
capa_priority = models.CharField(max_length=10, choices=[('LOW', 'Low'), ('MEDIUM', 'Medium'), ('HIGH', 'High')],
blank=True)
capa_initiated_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='initiated_capas', null=True,
capa_initiated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='initiated_capas', null=True,
blank=True)
capa_date = models.DateTimeField(null=True, blank=True)
capa_assessment = models.TextField(blank=True)

BIN
catherine_perry.pdf Normal file

Binary file not shown.

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.db.models.deletion
import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.db.models.deletion
from django.conf import settings

View File

@ -6,10 +6,10 @@ import uuid
from datetime import timedelta
from django.db import models
from accounts.models import User
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator
from decimal import Decimal
from django.conf import settings
class Message(models.Model):
@ -83,7 +83,7 @@ class Message(models.Model):
# Sender and recipients
sender = models.ForeignKey(
User,
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='sent_messages',
help_text="Message sender"
@ -249,7 +249,7 @@ class MessageRecipient(models.Model):
help_text="Type of recipient"
)
user = models.ForeignKey(
User,
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
blank=True,
@ -472,7 +472,7 @@ class NotificationTemplate(models.Model):
help_text="Last update timestamp"
)
created_by = models.ForeignKey(
User,
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
@ -584,7 +584,7 @@ class AlertRule(models.Model):
# Recipients
default_recipients = models.ManyToManyField(
User,
settings.AUTH_USER_MODEL,
blank=True,
help_text="Default alert recipients"
)
@ -629,7 +629,7 @@ class AlertRule(models.Model):
help_text="Last update timestamp"
)
created_by = models.ForeignKey(
User,
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
@ -743,7 +743,7 @@ class AlertInstance(models.Model):
# Response tracking
acknowledged_by = models.ForeignKey(
User,
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
@ -751,7 +751,7 @@ class AlertInstance(models.Model):
help_text="User who acknowledged the alert"
)
resolved_by = models.ForeignKey(
User,
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
@ -938,7 +938,7 @@ class CommunicationChannel(models.Model):
help_text="Last update timestamp"
)
created_by = models.ForeignKey(
User,
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-15 14:05
# Generated by Django 5.2.6 on 2025-09-19 10:58
import django.core.validators
import django.db.models.deletion

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -8,9 +8,11 @@ from django.urls import reverse
from django.utils.safestring import mark_safe
from .models import (
Encounter, VitalSigns, ProblemList, CarePlan,
ClinicalNote, NoteTemplate
ClinicalNote, NoteTemplate, Icd10
)
admin.site.register(Icd10)
@admin.register(Encounter)
class EncounterAdmin(admin.ModelAdmin):
@ -576,3 +578,5 @@ class ProblemListInline(admin.TabularInline):
# Add inlines to Encounter admin
EncounterAdmin.inlines = [VitalSignsInline, ClinicalNoteInline, ProblemListInline]

View File

@ -210,6 +210,18 @@ class CarePlanForm(forms.ModelForm):
'approved', 'approved_by'
]
widgets = {
'plan_type': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'category': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'patient': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'title': forms.TextInput(attrs={'class': 'form-control form-control-sm'}),
'priority': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'status': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'primary_provider': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'care_team': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'completion_percentage': forms.NumberInput(attrs={'class': 'form-control form-control-sm'}),
'approved': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'approved_by': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'related_problems': forms.SelectMultiple(attrs={'class': 'form-control form-control-sm'}),
'start_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}),
'end_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}),
'target_completion_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}),
@ -270,6 +282,71 @@ class CarePlanForm(forms.ModelForm):
return cleaned_data
class CarePlanProgressForm(forms.ModelForm):
"""
Restricted form used to update progress on an existing care plan
without touching identity/scope fields (patient, goals, etc.).
"""
class Meta:
model = CarePlan
fields = [
'status',
'completion_percentage',
'progress_notes',
'outcomes_achieved',
'evaluation_criteria',
'monitoring_parameters',
'last_reviewed',
'next_review_date',
]
widgets = {
'status': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'completion_percentage': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'min': 0, 'max': 100, 'step': 1,
}),
'progress_notes': forms.Textarea(attrs={'rows': 4, 'class': 'form-control form-control-sm'}),
'outcomes_achieved': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}),
'evaluation_criteria': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}),
'monitoring_parameters': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}),
'last_reviewed': forms.DateInput(attrs={'type': 'date', 'class': 'form-control form-control-sm'}),
'next_review_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control form-control-sm'}),
}
def __init__(self, *args, **kwargs):
# Accept tenant for potential future use; not strictly needed here.
self.tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
# Sensible default: prefill last_reviewed with today if empty
if not self.instance.pk and not self.fields['last_reviewed'].initial:
self.fields['last_reviewed'].initial = timezone.now().date()
def clean(self):
cleaned = super().clean()
status = cleaned.get('status')
pct = cleaned.get('completion_percentage')
# Range check
if pct is not None and not (0 <= pct <= 100):
self.add_error('completion_percentage', 'Completion must be between 0 and 100.')
# Status / % consistency
if status == 'COMPLETED' and (pct is None or pct < 100):
self.add_error('completion_percentage', 'Completion must be 100% when status is COMPLETED.')
if status == 'DRAFT' and pct and pct > 0:
self.add_error('completion_percentage', 'Draft care plans must have 0% completion.')
# Dates sanity: next_review_date should not be before last_reviewed
last_rev = cleaned.get('last_reviewed')
next_rev = cleaned.get('next_review_date')
if last_rev and next_rev and next_rev < last_rev:
self.add_error('next_review_date', 'Next review date must be on/after Last reviewed.')
return cleaned
class ClinicalNoteForm(forms.ModelForm):
"""
Form for creating and updating clinical notes.

View File

Binary file not shown.

View File

View File

@ -0,0 +1,174 @@
# myapp/management/commands/import_icd10.py
import xmlschema
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from emr.models import Icd10
class Command(BaseCommand):
help = "Import ICD-10-CM tabular XML into Icd10 model (auto-detects root)."
def add_arguments(self, parser):
parser.add_argument("--xsd", required=True, help="Path to icd10cm-tabular-2026.xsd")
parser.add_argument("--xml", required=True, help="Path to icd10cm-tabular-2026.xml")
parser.add_argument("--truncate", action="store_true", help="Delete existing Icd10 rows first")
# ------------------------ helpers ------------------------
def _as_text(self, val):
if val is None:
return None
if isinstance(val, dict):
# xmlschema may convert text/attributes into #text/@value/etc.
return val.get("#text") or val.get("@value") or val.get("value") or str(val)
return str(val)
def _ensure_list(self, maybe_list):
if maybe_list is None:
return []
if isinstance(maybe_list, list):
return maybe_list
return [maybe_list]
def _find_first_with_key(self, data, key):
"""Depth-first search: return the first dict that directly contains `key`."""
if isinstance(data, dict):
if key in data:
return data
for v in data.values():
found = self._find_first_with_key(v, key)
if found is not None:
return found
elif isinstance(data, list):
for item in data:
found = self._find_first_with_key(item, key)
if found is not None:
return found
return None
def _collect_rows(self, chapters):
"""
Build Icd10 rows + parent links from a chapters dict/list.
Expected minimal structure:
chapter -> section? -> diag (recursive)
"""
rows = []
parent_links = []
def import_diag(diag, chapter_name, section_name, parent_code=None):
code = self._as_text(diag.get("name"))
desc = self._as_text(diag.get("desc"))
if not code:
return
children = self._ensure_list(diag.get("diag"))
is_header = bool(children) and not (desc and desc.strip())
rows.append(Icd10(
code=code,
description=desc,
chapter_name=self._as_text(chapter_name),
section_name=self._as_text(section_name),
parent=None, # set later
is_header=is_header,
))
if parent_code:
parent_links.append((code, parent_code))
for child in children:
import_diag(child, chapter_name, section_name, parent_code=code)
# Normalize chapters to a list
chapters = self._ensure_list(chapters)
for ch in chapters:
ch_name = self._as_text(ch.get("name"))
# Sections may be missing in some packs; diags may be directly under chapter
sections = self._ensure_list(ch.get("section"))
if sections:
for sec in sections:
sec_name = self._as_text(sec.get("name"))
for d in self._ensure_list(sec.get("diag")):
import_diag(d, ch_name, sec_name, parent_code=None)
else:
# If no sections, look for diags at chapter level
for d in self._ensure_list(ch.get("diag")):
import_diag(d, ch_name, None, parent_code=None)
return rows, parent_links
def handle(self, *args, **opts):
xsd_path = opts["xsd"]
xml_path = opts["xml"]
try:
xs = xmlschema.XMLSchema(xsd_path)
except Exception as e:
raise CommandError(f"Failed to load XSD: {e}")
try:
# to_dict() already flattens namespaces into keys; well auto-detect paths.
data = xs.to_dict(xml_path)
except Exception as e:
raise CommandError(f"Failed to parse XML: {e}")
# If the root is a single-key dict, unwrap while keeping reference
if isinstance(data, dict) and len(data) == 1:
root_key, root_val = next(iter(data.items()))
root = root_val
else:
root = data
# Find the dict that *contains* the "chapter" key (any depth)
container_with_chapter = self._find_first_with_key(root, "chapter")
if not container_with_chapter:
# Fall back: sometimes structure uses "chapters"
container_with_chapter = self._find_first_with_key(root, "chapters")
if container_with_chapter and isinstance(container_with_chapter["chapters"], dict):
# Normalize "chapters" -> "chapter" if its nested like {"chapters": {"chapter": [...]}}
if "chapter" in container_with_chapter["chapters"]:
container_with_chapter = container_with_chapter["chapters"]
if not container_with_chapter or ("chapter" not in container_with_chapter):
# Give user a quick peek at available top-level keys to debug
preview_keys = list(root.keys()) if isinstance(root, dict) else type(root)
raise CommandError(
"Could not locate 'chapter' anywhere in the parsed XML. "
f"Top-level preview: {preview_keys}"
)
chapters = container_with_chapter.get("chapter")
if chapters is None:
raise CommandError("Found container for chapters, but 'chapter' is empty.")
# Optionally truncate
if opts["truncate"]:
self.stdout.write(self.style.WARNING("Truncating existing Icd10 data..."))
Icd10.objects.all().delete()
# Collect rows + parent links
self.stdout.write("Collecting ICD-10 rows...")
rows, parent_links = self._collect_rows(chapters)
self.stdout.write(self.style.SUCCESS(f"Collected {len(rows)} rows. Inserting..."))
BATCH = 1000
with transaction.atomic():
for i in range(0, len(rows), BATCH):
Icd10.objects.bulk_create(rows[i:i+BATCH], ignore_conflicts=True)
# Link parents
if parent_links:
self.stdout.write("Linking parents...")
code_to_obj = {o.code: o for o in Icd10.objects.only("id", "code")}
updates = []
for child_code, parent_code in parent_links:
child = code_to_obj.get(child_code)
parent = code_to_obj.get(parent_code)
if child and parent and child.parent_id != parent.id:
child.parent_id = parent.id
updates.append(child)
for i in range(0, len(updates), BATCH):
Icd10.objects.bulk_update(updates[i:i+BATCH], ["parent"])
self.stdout.write(self.style.SUCCESS(f"Linked {len(updates)} parent relations."))
self.stdout.write(self.style.SUCCESS("ICD-10 import completed successfully."))

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
# Generated by Django 5.2.6 on 2025-09-16 00:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("emr", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="vitalsigns",
name="temperature",
field=models.DecimalField(
blank=True,
decimal_places=1,
help_text="Temperature in Celsius",
max_digits=4,
null=True,
),
),
]

View File

@ -0,0 +1,468 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
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="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="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="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="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="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="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="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"
),
),
]

View File

@ -16,6 +16,52 @@ class Encounter(models.Model):
"""
Clinical encounter model for tracking patient visits and care episodes.
"""
class EncounterType(models.TextChoices):
INPATIENT = 'INPATIENT', 'Inpatient'
OUTPATIENT = 'OUTPATIENT', 'Outpatient'
EMERGENCY = 'EMERGENCY', 'Emergency'
URGENT_CARE = 'URGENT_CARE', 'Urgent Care'
OBSERVATION = 'OBSERVATION', 'Observation'
TELEMEDICINE = 'TELEMEDICINE', 'Telemedicine'
HOME_VISIT = 'HOME_VISIT', 'Home Visit'
CONSULTATION = 'CONSULTATION', 'Consultation'
FOLLOW_UP = 'FOLLOW_UP', 'Follow-up'
PROCEDURE = 'PROCEDURE', 'Procedure'
SURGERY = 'SURGERY', 'Surgery'
DIAGNOSTIC = 'DIAGNOSTIC', 'Diagnostic'
PREVENTIVE = 'PREVENTIVE', 'Preventive Care'
class EncounterClass(models.TextChoices):
AMB = 'AMB', 'Ambulatory'
EMER = 'EMER', 'Emergency'
FLD = 'FLD', 'Field'
HH = 'HH', 'Home Health'
IMP = 'IMP', 'Inpatient'
ACUTE = 'ACUTE', 'Inpatient Acute'
NONAC = 'NONAC', 'Inpatient Non-Acute'
OBSENC = 'OBSENC', 'Observation Encounter'
PRENC = 'PRENC', 'Pre-Admission'
SS = 'SS', 'Short Stay'
VR = 'VR', 'Virtual'
class EncounterStatus(models.TextChoices):
PLANNED = 'PLANNED', 'Planned'
ARRIVED = 'ARRIVED', 'Arrived'
TRIAGED = 'TRIAGED', 'Triaged'
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
ON_HOLD = 'ON_HOLD', 'On Hold'
FINISHED = 'FINISHED', 'Finished'
CANCELLED = 'CANCELLED', 'Cancelled'
ENTERED_IN_ERROR = 'ENTERED_IN_ERROR', 'Entered in Error'
UNKNOWN = 'UNKNOWN', 'Unknown'
class Priority(models.TextChoices):
ROUTINE = 'ROUTINE', 'Routine'
URGENT = 'URGENT', 'Urgent'
STAT = 'STAT', 'STAT'
EMERGENCY = 'EMERGENCY', 'Emergency'
# Tenant relationship
tenant = models.ForeignKey(
@ -50,40 +96,13 @@ class Encounter(models.Model):
# Encounter Details
encounter_type = models.CharField(
max_length=30,
choices=[
('INPATIENT', 'Inpatient'),
('OUTPATIENT', 'Outpatient'),
('EMERGENCY', 'Emergency'),
('URGENT_CARE', 'Urgent Care'),
('OBSERVATION', 'Observation'),
('TELEMEDICINE', 'Telemedicine'),
('HOME_VISIT', 'Home Visit'),
('CONSULTATION', 'Consultation'),
('FOLLOW_UP', 'Follow-up'),
('PROCEDURE', 'Procedure'),
('SURGERY', 'Surgery'),
('DIAGNOSTIC', 'Diagnostic'),
('PREVENTIVE', 'Preventive Care'),
('OTHER', 'Other'),
],
choices=EncounterType.choices,
help_text='Type of encounter'
)
encounter_class = models.CharField(
max_length=20,
choices=[
('AMB', 'Ambulatory'),
('EMER', 'Emergency'),
('FLD', 'Field'),
('HH', 'Home Health'),
('IMP', 'Inpatient'),
('ACUTE', 'Inpatient Acute'),
('NONAC', 'Inpatient Non-Acute'),
('OBSENC', 'Observation Encounter'),
('PRENC', 'Pre-Admission'),
('SS', 'Short Stay'),
('VR', 'Virtual'),
],
choices=EncounterClass.choices,
help_text='Encounter class (HL7 standard)'
)
@ -100,18 +119,8 @@ class Encounter(models.Model):
# Status
status = models.CharField(
max_length=20,
choices=[
('PLANNED', 'Planned'),
('ARRIVED', 'Arrived'),
('TRIAGED', 'Triaged'),
('IN_PROGRESS', 'In Progress'),
('ON_HOLD', 'On Hold'),
('FINISHED', 'Finished'),
('CANCELLED', 'Cancelled'),
('ENTERED_IN_ERROR', 'Entered in Error'),
('UNKNOWN', 'Unknown'),
],
default='PLANNED',
choices=EncounterStatus.choices,
default=EncounterStatus.PLANNED,
help_text='Current encounter status'
)
@ -162,14 +171,8 @@ class Encounter(models.Model):
# Priority and Acuity
priority = models.CharField(
max_length=20,
choices=[
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('ASAP', 'ASAP'),
('STAT', 'STAT'),
('EMERGENCY', 'Emergency'),
],
default='ROUTINE',
choices=Priority.choices,
default=Priority.ROUTINE,
help_text='Encounter priority'
)
acuity_level = models.PositiveIntegerField(
@ -272,6 +275,44 @@ class VitalSigns(models.Model):
Vital signs model for tracking patient vital signs measurements.
"""
class TemperatureMethod(models.TextChoices):
ORAL = 'ORAL', 'Oral'
RECTAL = 'RECTAL', 'Rectal'
AXILLARY = 'AXILLARY', 'Axillary'
TYMPANIC = 'TYMPANIC', 'Tympanic'
TEMPORAL = 'TEMPORAL', 'Temporal'
CORE = 'CORE', 'Core'
class BpPosition(models.TextChoices):
SITTING = 'SITTING', 'Sitting'
STANDING = 'STANDING', 'Standing'
LYING = 'LYING', 'Lying'
SUPINE = 'SUPINE', 'Supine'
class BpCuffSize(models.TextChoices):
SMALL = 'SMALL', 'Small'
REGULAR = 'REGULAR', 'Regular'
LARGE = 'LARGE', 'Large'
EXTRA_LARGE = 'EXTRA_LARGE', 'Extra Large'
PEDIATRIC = 'PEDIATRIC', 'Pediatric'
class HeartRhythm(models.TextChoices):
REGULAR = 'REGULAR', 'Regular'
REGULARLY_IRREGULAR = 'REGULARLY_IRREGULAR', 'Regularly irregular'
IRREGULARLY_IRREGULAR = 'IRREGULARLY_IRREGULAR', 'Irregularly irregular'
IRREGULAR_UNSPECIFIED = 'IRREGULAR_UNSPECIFIED', 'Irregular (unspecified)'
class OxygenDelivery(models.TextChoices):
ROOM_AIR = 'ROOM_AIR', 'Room Air'
NASAL_CANNULA = 'NASAL_CANNULA', 'Nasal Cannula'
SIMPLE_MASK = 'SIMPLE_MASK', 'Simple Mask'
NON_REBREATHER = 'NON_REBREATHER', 'Non-Rebreather Mask'
VENTURI_MASK = 'VENTURI_MASK', 'Venturi Mask'
CPAP = 'CPAP', 'CPAP'
BIPAP = 'BIPAP', 'BiPAP'
MECHANICAL_VENTILATION = 'MECHANICAL_VENTILATION', 'Mechanical Ventilation'
OTHER = 'OTHER', 'Other'
# Encounter relationship
encounter = models.ForeignKey(
Encounter,
@ -312,14 +353,7 @@ class VitalSigns(models.Model):
)
temperature_method = models.CharField(
max_length=20,
choices=[
('ORAL', 'Oral'),
('RECTAL', 'Rectal'),
('AXILLARY', 'Axillary'),
('TYMPANIC', 'Tympanic'),
('TEMPORAL', 'Temporal'),
('CORE', 'Core'),
],
choices=TemperatureMethod.choices,
blank=True,
null=True,
help_text='Temperature measurement method'
@ -340,25 +374,14 @@ class VitalSigns(models.Model):
)
bp_position = models.CharField(
max_length=20,
choices=[
('SITTING', 'Sitting'),
('STANDING', 'Standing'),
('LYING', 'Lying'),
('SUPINE', 'Supine'),
],
choices=BpPosition.choices,
blank=True,
null=True,
help_text='Patient position during BP measurement'
)
bp_cuff_size = models.CharField(
max_length=20,
choices=[
('SMALL', 'Small'),
('REGULAR', 'Regular'),
('LARGE', 'Large'),
('EXTRA_LARGE', 'Extra Large'),
('PEDIATRIC', 'Pediatric'),
],
choices=BpCuffSize.choices,
blank=True,
null=True,
help_text='Blood pressure cuff size'
@ -373,12 +396,7 @@ class VitalSigns(models.Model):
)
heart_rhythm = models.CharField(
max_length=25,
choices=[
('REGULAR', 'Regular'),
('IRREGULAR', 'Irregular'),
('REGULARLY_IRREGULAR', 'Regularly Irregular'),
('IRREGULARLY_IRREGULAR', 'Irregularly Irregular'),
],
choices=HeartRhythm.choices,
blank=True,
null=True,
help_text='Heart rhythm'
@ -401,18 +419,8 @@ class VitalSigns(models.Model):
)
oxygen_delivery = models.CharField(
max_length=30,
choices=[
('ROOM_AIR', 'Room Air'),
('NASAL_CANNULA', 'Nasal Cannula'),
('SIMPLE_MASK', 'Simple Mask'),
('NON_REBREATHER', 'Non-Rebreather Mask'),
('VENTURI_MASK', 'Venturi Mask'),
('CPAP', 'CPAP'),
('BIPAP', 'BiPAP'),
('MECHANICAL_VENTILATION', 'Mechanical Ventilation'),
('OTHER', 'Other'),
],
default='ROOM_AIR',
choices=OxygenDelivery.choices,
default=OxygenDelivery.ROOM_AIR,
help_text='Oxygen delivery method'
)
oxygen_flow_rate = models.DecimalField(
@ -573,6 +581,58 @@ class ProblemList(models.Model):
Problem list model for tracking patient problems and diagnoses.
"""
class CodingSystem(models.TextChoices):
ICD10 = 'ICD10', 'ICD-10'
ICD9 = 'ICD9', 'ICD-9'
SNOMED = 'SNOMED', 'SNOMED CT'
CPT = 'CPT', 'CPT'
LOINC = 'LOINC', 'LOINC'
OTHER = 'OTHER', 'Other'
class ProblemType(models.TextChoices):
DIAGNOSIS = 'DIAGNOSIS', 'Diagnosis'
SYMPTOM = 'SYMPTOM', 'Symptom'
FINDING = 'FINDING', 'Finding'
COMPLAINT = 'COMPLAINT', 'Complaint'
CONDITION = 'CONDITION', 'Condition'
DISORDER = 'DISORDER', 'Disorder'
SYNDROME = 'SYNDROME', 'Syndrome'
INJURY = 'INJURY', 'Injury'
ALLERGY = 'ALLERGY', 'Allergy'
INTOLERANCE = 'INTOLERANCE', 'Intolerance'
RISK_FACTOR = 'RISK_FACTOR', 'Risk Factor'
OTHER = 'OTHER', 'Other'
class Severity(models.TextChoices):
MILD = 'MILD', 'Mild'
MODERATE = 'MODERATE', 'Moderate'
SEVERE = 'SEVERE', 'Severe'
CRITICAL = 'CRITICAL', 'Critical'
UNKNOWN = 'UNKNOWN', 'Unknown'
class Priority(models.TextChoices):
LOW = 'LOW', 'Low'
MEDIUM = 'MEDIUM', 'Medium'
HIGH = 'HIGH', 'High'
URGENT = 'URGENT', 'Urgent'
class ProblemStatus(models.TextChoices):
ACTIVE = 'ACTIVE', 'Active'
INACTIVE = 'INACTIVE', 'Inactive'
RESOLVED = 'RESOLVED', 'Resolved'
REMISSION = 'REMISSION', 'In Remission'
RECURRENCE = 'RECURRENCE', 'Recurrence'
RELAPSE = 'RELAPSE', 'Relapse'
UNKNOWN = 'UNKNOWN', 'Unknown'
OTHER = 'OTHER', 'Other'
class Laterality(models.TextChoices):
LEFT = 'LEFT', 'Left'
RIGHT = 'RIGHT', 'Right'
BILATERAL = 'BILATERAL', 'Bilateral'
UNILATERAL = 'UNILATERAL', 'Unilateral'
NOT_APPLICABLE = 'NOT_APPLICABLE', 'Not Applicable'
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
@ -610,14 +670,7 @@ class ProblemList(models.Model):
)
coding_system = models.CharField(
max_length=20,
choices=[
('ICD10', 'ICD-10'),
('ICD9', 'ICD-9'),
('SNOMED', 'SNOMED CT'),
('CPT', 'CPT'),
('LOINC', 'LOINC'),
('OTHER', 'Other'),
],
choices=CodingSystem.choices,
blank=True,
null=True,
help_text='Coding system used'
@ -626,20 +679,7 @@ class ProblemList(models.Model):
# Problem Classification
problem_type = models.CharField(
max_length=30,
choices=[
('DIAGNOSIS', 'Diagnosis'),
('SYMPTOM', 'Symptom'),
('FINDING', 'Finding'),
('COMPLAINT', 'Complaint'),
('CONDITION', 'Condition'),
('DISORDER', 'Disorder'),
('SYNDROME', 'Syndrome'),
('INJURY', 'Injury'),
('ALLERGY', 'Allergy'),
('INTOLERANCE', 'Intolerance'),
('RISK_FACTOR', 'Risk Factor'),
('OTHER', 'Other'),
],
choices=ProblemType.choices,
help_text='Type of problem'
)
@ -659,42 +699,23 @@ class ProblemList(models.Model):
# Severity and Priority
severity = models.CharField(
max_length=20,
choices=[
('MILD', 'Mild'),
('MODERATE', 'Moderate'),
('SEVERE', 'Severe'),
('CRITICAL', 'Critical'),
('UNKNOWN', 'Unknown'),
],
choices=Severity.choices,
blank=True,
null=True,
help_text='Problem severity'
)
priority = models.CharField(
max_length=20,
choices=[
('LOW', 'Low'),
('MEDIUM', 'Medium'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
],
default='MEDIUM',
choices=Priority.choices,
default=Priority.MEDIUM,
help_text='Problem priority'
)
# Status
status = models.CharField(
max_length=20,
choices=[
('ACTIVE', 'Active'),
('INACTIVE', 'Inactive'),
('RESOLVED', 'Resolved'),
('REMISSION', 'In Remission'),
('RECURRENCE', 'Recurrence'),
('RELAPSE', 'Relapse'),
('UNKNOWN', 'Unknown'),
],
default='ACTIVE',
choices=ProblemStatus.choices,
default=ProblemStatus.ACTIVE,
help_text='Current problem status'
)
@ -745,13 +766,7 @@ class ProblemList(models.Model):
)
laterality = models.CharField(
max_length=20,
choices=[
('LEFT', 'Left'),
('RIGHT', 'Right'),
('BILATERAL', 'Bilateral'),
('UNILATERAL', 'Unilateral'),
('NOT_APPLICABLE', 'Not Applicable'),
],
choices=Laterality.choices,
blank=True,
null=True,
help_text='Laterality'
@ -850,6 +865,48 @@ class CarePlan(models.Model):
"""
Care plan model for managing patient care coordination and goals.
"""
class PlanType(models.TextChoices):
COMPREHENSIVE = 'COMPREHENSIVE', 'Comprehensive Care Plan'
DISEASE_SPECIFIC = 'DISEASE_SPECIFIC', 'Disease-Specific Plan'
PREVENTIVE = 'PREVENTIVE', 'Preventive Care Plan'
CHRONIC_CARE = 'CHRONIC_CARE', 'Chronic Care Management'
ACUTE_CARE = 'ACUTE_CARE', 'Acute Care Plan'
DISCHARGE = 'DISCHARGE', 'Discharge Planning'
REHABILITATION = 'REHABILITATION', 'Rehabilitation Plan'
PALLIATIVE = 'PALLIATIVE', 'Palliative Care Plan'
MENTAL_HEALTH = 'MENTAL_HEALTH', 'Mental Health Plan'
MEDICATION = 'MEDICATION', 'Medication Management'
NUTRITION = 'NUTRITION', 'Nutrition Plan'
EXERCISE = 'EXERCISE', 'Exercise Plan'
OTHER = 'OTHER', 'Other'
class PlanCategory(models.TextChoices):
ASSESSMENT = 'ASSESSMENT', 'Assessment and Monitoring'
TREATMENT = 'TREATMENT', 'Treatment'
EDUCATION = 'EDUCATION', 'Patient Education'
COORDINATION = 'COORDINATION', 'Care Coordination'
PREVENTION = 'PREVENTION', 'Prevention'
LIFESTYLE = 'LIFESTYLE', 'Lifestyle Modification'
MEDICATION = 'MEDICATION', 'Medication Management'
FOLLOW_UP = 'FOLLOW_UP', 'Follow-up Care'
EMERGENCY = 'EMERGENCY', 'Emergency Planning'
SUPPORT = 'SUPPORT', 'Support Services'
class PlanStatus(models.TextChoices):
DRAFT = 'DRAFT', 'Draft'
ACTIVE = 'ACTIVE', 'Active'
ON_HOLD = 'ON_HOLD', 'On Hold'
COMPLETED = 'COMPLETED', 'Completed'
CANCELLED = 'CANCELLED', 'Cancelled'
ENTERED_IN_ERROR = 'ENTERED_IN_ERROR', 'Entered in Error'
UNKNOWN = 'UNKNOWN', 'Unknown'
class PlanPriority(models.TextChoices):
LOW = 'LOW', 'Low'
ROUTINE = 'ROUTINE', 'Routine'
URGENT = 'URGENT', 'Urgent'
STAT = 'STAT', 'STAT'
# Tenant relationship
tenant = models.ForeignKey(
@ -887,38 +944,15 @@ class CarePlan(models.Model):
# Plan Type and Category
plan_type = models.CharField(
max_length=30,
choices=[
('COMPREHENSIVE', 'Comprehensive Care Plan'),
('DISEASE_SPECIFIC', 'Disease-Specific Plan'),
('PREVENTIVE', 'Preventive Care Plan'),
('CHRONIC_CARE', 'Chronic Care Management'),
('ACUTE_CARE', 'Acute Care Plan'),
('DISCHARGE', 'Discharge Planning'),
('REHABILITATION', 'Rehabilitation Plan'),
('PALLIATIVE', 'Palliative Care Plan'),
('MENTAL_HEALTH', 'Mental Health Plan'),
('MEDICATION', 'Medication Management'),
('NUTRITION', 'Nutrition Plan'),
('EXERCISE', 'Exercise Plan'),
('OTHER', 'Other'),
],
choices=PlanType.choices,
help_text='Type of care plan'
)
category = models.CharField(
max_length=50,
choices=[
('ASSESSMENT', 'Assessment and Monitoring'),
('TREATMENT', 'Treatment'),
('EDUCATION', 'Patient Education'),
('COORDINATION', 'Care Coordination'),
('PREVENTION', 'Prevention'),
('LIFESTYLE', 'Lifestyle Modification'),
('MEDICATION', 'Medication Management'),
('FOLLOW_UP', 'Follow-up Care'),
('EMERGENCY', 'Emergency Planning'),
('SUPPORT', 'Support Services'),
],
choices=PlanCategory.choices,
help_text='Care plan category'
)
@ -940,30 +974,16 @@ class CarePlan(models.Model):
# Status
status = models.CharField(
max_length=20,
choices=[
('DRAFT', 'Draft'),
('ACTIVE', 'Active'),
('ON_HOLD', 'On Hold'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
('ENTERED_IN_ERROR', 'Entered in Error'),
('UNKNOWN', 'Unknown'),
],
default='DRAFT',
choices=PlanStatus.choices,
default=PlanStatus.DRAFT,
help_text='Care plan status'
)
# Priority
priority = models.CharField(
max_length=20,
choices=[
('LOW', 'Low'),
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('ASAP', 'ASAP'),
('STAT', 'STAT'),
],
default='ROUTINE',
choices=PlanPriority.choices,
default=PlanPriority.ROUTINE,
help_text='Care plan priority'
)
@ -1143,6 +1163,46 @@ class ClinicalNote(models.Model):
Clinical note model for documenting patient care and observations.
"""
class NoteType(models.TextChoices):
PROGRESS = 'PROGRESS', 'Progress'
ADMISSION = 'ADMISSION', 'Admission Note'
DISCHARGE = 'DISCHARGE', 'Discharge Note'
CONSULTATION = 'CONSULTATION', 'Consultation Note'
PROCEDURE = 'PROCEDURE', 'Procedure Note'
OPERATIVE = 'OPERATIVE', 'Operative Note'
NURSING = 'NURSING', 'Nursing Note'
THERAPY = 'THERAPY', 'Therapy Note'
SOCIAL_WORK = 'SOCIAL_WORK', 'Social Work Note'
PSYCHOLOGY = 'PSYCHOLOGY', 'Psychology Note'
NUTRITION = 'NUTRITION', 'Nutrition Note'
PHARMACY = 'PHARMACY', 'Pharmacy Note'
CASE_MANAGEMENT = 'CASE_MANAGEMENT', 'Case Management Note'
EDUCATION = 'EDUCATION', 'Patient Education Note'
TELEPHONE = 'TELEPHONE', 'Telephone Note'
ADDENDUM = 'ADDENDUM', 'Addendum'
CORRECTION = 'CORRECTION', 'Correction'
OTHER = 'OTHER', 'Other'
class NoteStatus(models.TextChoices):
DRAFT = 'DRAFT', 'Draft'
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
COMPLETED = 'COMPLETED', 'Completed'
SIGNED = 'SIGNED', 'Signed'
AMENDED = 'AMENDED', 'Amended'
CORRECTED = 'CORRECTED', 'Corrected'
CANCELLED = 'CANCELLED', 'Cancelled'
ERROR = 'ERROR', 'Entered in Error'
UNKNOWN = 'UNKNOWN', 'Unknown'
class NoteSignatureMethod(models.TextChoices):
ELECTRONIC = 'ELECTRONIC', 'Electronic'
DIGITAL = 'DIGITAL', 'Digital Signature'
BIOMETRIC = 'BIOMETRIC', 'Biometric Signature'
PASSWORD = 'PASSWORD', 'Password'
TOKEN = 'TOKEN', 'Token Authentication'
OTHER = 'OTHER', 'Other'
# Encounter relationship
encounter = models.ForeignKey(
Encounter,
@ -1170,26 +1230,7 @@ class ClinicalNote(models.Model):
# Note Details
note_type = models.CharField(
max_length=30,
choices=[
('PROGRESS', 'Progress Note'),
('ADMISSION', 'Admission Note'),
('DISCHARGE', 'Discharge Note'),
('CONSULTATION', 'Consultation Note'),
('PROCEDURE', 'Procedure Note'),
('OPERATIVE', 'Operative Note'),
('NURSING', 'Nursing Note'),
('THERAPY', 'Therapy Note'),
('SOCIAL_WORK', 'Social Work Note'),
('PSYCHOLOGY', 'Psychology Note'),
('NUTRITION', 'Nutrition Note'),
('PHARMACY', 'Pharmacy Note'),
('CASE_MANAGEMENT', 'Case Management Note'),
('EDUCATION', 'Patient Education Note'),
('TELEPHONE', 'Telephone Note'),
('ADDENDUM', 'Addendum'),
('CORRECTION', 'Correction'),
('OTHER', 'Other'),
],
choices=NoteType.choices,
help_text='Type of clinical note'
)
@ -1235,16 +1276,8 @@ class ClinicalNote(models.Model):
# Status and Workflow
status = models.CharField(
max_length=20,
choices=[
('DRAFT', 'Draft'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('SIGNED', 'Signed'),
('AMENDED', 'Amended'),
('CORRECTED', 'Corrected'),
('ENTERED_IN_ERROR', 'Entered in Error'),
],
default='DRAFT',
choices=NoteStatus.choices,
default=NoteStatus.DRAFT,
help_text='Note status'
)
@ -1260,13 +1293,7 @@ class ClinicalNote(models.Model):
)
signature_method = models.CharField(
max_length=20,
choices=[
('ELECTRONIC', 'Electronic Signature'),
('DIGITAL', 'Digital Signature'),
('BIOMETRIC', 'Biometric Signature'),
('PASSWORD', 'Password Authentication'),
('TOKEN', 'Token Authentication'),
],
choices=NoteSignatureMethod.choices,
blank=True,
null=True,
help_text='Method of signature'
@ -1375,6 +1402,45 @@ class NoteTemplate(models.Model):
Note template model for standardizing clinical documentation.
"""
class NoteType(models.TextChoices):
PROGRESS = 'PROGRESS', 'Progress Note'
ADMISSION = 'ADMISSION', 'Admission Note'
DISCHARGE = 'DISCHARGE', 'Discharge Note'
CONSULTATION = 'CONSULTATION', 'Consultation Note'
PROCEDURE = 'PROCEDURE', 'Procedure Note'
OPERATIVE = 'OPERATIVE', 'Operative Note'
NURSING = 'NURSING', 'Nursing Note'
THERAPY = 'THERAPY', 'Therapy Note'
SOCIAL_WORK = 'SOCIAL_WORK', 'Social Work Note'
PSYCHOLOGY = 'PSYCHOLOGY', 'Psychology Note'
NUTRITION = 'NUTRITION', 'Nutrition Note'
PHARMACY = 'PHARMACY', 'Pharmacy Note'
CASE_MANAGEMENT = 'CASE_MANAGEMENT', 'Case Management Note'
EDUCATION = 'EDUCATION', 'Patient Education Note'
TELEPHONE = 'TELEPHONE', 'Telephone Note'
OTHER = 'OTHER', 'Other'
class NoteSpecialty(models.TextChoices):
GENERAL_MEDICINE = 'GENERAL_MEDICINE', 'General Medicine'
SURGERY = 'SURGERY', 'Surgery'
CARDIOLOGY = 'CARDIOLOGY', 'Cardiology'
NEUROLOGY = 'NEUROLOGY', 'Neurology'
ONCOLOGY = 'ONCOLOGY', 'Oncology'
PEDIATRICS = 'PEDIATRICS', 'Pediatrics'
OBSTETRICS = 'OBSTETRICS', 'Obstetrics'
GYNECOLOGY = 'GYNECOLOGY', 'Gynecology'
ORTHOPEDICS = 'ORTHOPEDICS', 'Orthopedics'
PSYCHIATRY = 'PSYCHIATRY', 'Psychiatry'
EMERGENCY = 'EMERGENCY', 'Emergency Medicine'
CRITICAL_CARE = 'CRITICAL_CARE', 'Critical Care'
REHABILITATION = 'REHABILITATION', 'Rehabilitation'
NURSING = 'NURSING', 'Nursing'
THERAPY = 'THERAPY', 'Therapy'
SOCIAL_WORK = 'SOCIAL_WORK', 'Social Work'
NUTRITION = 'NUTRITION', 'Nutrition'
PHARMACY = 'PHARMACY', 'Pharmacy'
OTHER = 'OTHER', 'Other'
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
@ -1404,50 +1470,13 @@ class NoteTemplate(models.Model):
# Template Type
note_type = models.CharField(
max_length=30,
choices=[
('PROGRESS', 'Progress Note'),
('ADMISSION', 'Admission Note'),
('DISCHARGE', 'Discharge Note'),
('CONSULTATION', 'Consultation Note'),
('PROCEDURE', 'Procedure Note'),
('OPERATIVE', 'Operative Note'),
('NURSING', 'Nursing Note'),
('THERAPY', 'Therapy Note'),
('SOCIAL_WORK', 'Social Work Note'),
('PSYCHOLOGY', 'Psychology Note'),
('NUTRITION', 'Nutrition Note'),
('PHARMACY', 'Pharmacy Note'),
('CASE_MANAGEMENT', 'Case Management Note'),
('EDUCATION', 'Patient Education Note'),
('TELEPHONE', 'Telephone Note'),
('OTHER', 'Other'),
],
choices=NoteType.choices,
help_text='Type of note this template is for'
)
specialty = models.CharField(
max_length=100,
choices=[
('GENERAL_MEDICINE', 'General Medicine'),
('SURGERY', 'Surgery'),
('CARDIOLOGY', 'Cardiology'),
('NEUROLOGY', 'Neurology'),
('ONCOLOGY', 'Oncology'),
('PEDIATRICS', 'Pediatrics'),
('OBSTETRICS', 'Obstetrics'),
('GYNECOLOGY', 'Gynecology'),
('ORTHOPEDICS', 'Orthopedics'),
('PSYCHIATRY', 'Psychiatry'),
('EMERGENCY', 'Emergency Medicine'),
('CRITICAL_CARE', 'Critical Care'),
('REHABILITATION', 'Rehabilitation'),
('NURSING', 'Nursing'),
('THERAPY', 'Therapy'),
('SOCIAL_WORK', 'Social Work'),
('NUTRITION', 'Nutrition'),
('PHARMACY', 'Pharmacy'),
('OTHER', 'Other'),
],
choices=NoteSpecialty.choices,
blank=True,
null=True,
help_text='Medical specialty'
@ -1543,3 +1572,32 @@ class NoteTemplate(models.Model):
super().save(*args, **kwargs)
class Icd10(models.Model):
"""
ICD-10-CM tabular code entry.
Handles chapters/sections/diagnoses (with parent-child hierarchy).
"""
code = models.CharField(max_length=10, unique=True, db_index=True)
description = models.TextField(blank=True, null=True)
chapter_name = models.CharField(max_length=255, blank=True, null=True)
section_name = models.CharField(max_length=255, blank=True, null=True)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
blank=True,
null=True,
related_name='children'
)
is_header = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'emr_icd10'
verbose_name = 'ICD-10 Code'
verbose_name_plural = 'ICD-10 Codes'
ordering = ['code']
def __str__(self):
return f"{self.code}{self.description[:80] if self.description else ''}"

View File

@ -148,8 +148,14 @@
</div>
</div>
<div class="d-flex">
<span class="badge care-plan-status-{{ care_plan.status|lower }} me-2">{{ care_plan.get_status_display }}</span>
<span class="badge care-plan-priority-{{ care_plan.priority|lower }}">{{ care_plan.get_priority_display }} Priority</span>
<p class="fw-bold me-2">Status:
<span class="badge care-plan-status-{{ care_plan.status|lower }}">{{ care_plan.get_status_display }}</span>
</p>
<p class="fw-bold me-2">Priority:
<span class="badge care-plan-priority-{{ care_plan.priority|lower }}">{{ care_plan.get_priority_display }}</span>
</p>
</div>
</div>
<!-- end care plan header -->
@ -161,18 +167,18 @@
<div class="panel-heading-btn">
<a href="{% url 'patients:patient_detail' care_plan.patient.id %}" class="btn btn-xs btn-outline-secondary me-2">
<i class="fa fa-user me-1"></i> Patient Profile
<i class="fa fa-user me-1"></i>Patient Profile
</a>
{% if not care_plan.approved %}
<a href="#" class="btn btn-xs btn-outline-success me-2" data-bs-toggle="modal" data-bs-target="#approveModal">
<i class="fa fa-check-circle me-1"></i> Approve Plan
<i class="fa fa-check-circle me-1"></i>Approve Plan
</a>
{% endif %}
<a href="{% url 'emr:care_plan_update' care_plan.id %}" class="btn btn-xs btn-outline-warning me-2">
<i class="fa fa-edit me-1"></i> Edit
<i class="fa fa-edit me-1"></i>Edit
</a>
<a href="{% url 'emr:care_plan_delete' care_plan.id %}" class="btn btn-xs btn-outline-danger me-2">
<i class="fa fa-trash me-1"></i> Delete
<i class="fa fa-trash me-1"></i>Delete
</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>
@ -448,7 +454,7 @@
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<div class="note alert-info">
<i class="fa fa-info-circle me-2"></i> No objectives have been defined for this care plan.
</div>
{% endif %}
@ -475,7 +481,7 @@
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<div class="note alert-info">
<i class="fa fa-info-circle me-2"></i> No interventions have been defined for this care plan.
</div>
{% endif %}
@ -497,7 +503,7 @@
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<div class="note alert-info">
<i class="fa fa-info-circle me-2"></i> No activities have been defined for this care plan.
</div>
{% endif %}
@ -549,7 +555,7 @@
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<div class="note alert-info">
<i class="fa fa-info-circle me-2"></i> No evaluation criteria have been defined for this care plan.
</div>
{% endif %}
@ -603,7 +609,7 @@
{% endfor %}
</div>
{% else %}
<div class="alert alert-info mb-4">
<div class="note alert-info mb-4">
<i class="fa fa-info-circle me-2"></i> No resources have been identified for this care plan.
</div>
{% endif %}
@ -627,7 +633,7 @@
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<div class="note alert-info">
<i class="fa fa-info-circle me-2"></i> No support systems have been identified for this care plan.
</div>
{% endif %}
@ -654,7 +660,7 @@
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<div class="note alert-info">
<i class="fa fa-info-circle me-2"></i> No outcomes have been recorded for this care plan.
</div>
{% endif %}
@ -678,6 +684,7 @@
</div>
<div class="panel-body">
{% if care_plan.clinical_notes.all %}
<div class="timeline">
{% for note in care_plan.clinical_notes.all %}
<div class="timeline-item">
@ -702,7 +709,7 @@
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<div class="note alert-info">
<i class="fa fa-info-circle me-2"></i> No clinical notes are associated with this care plan.
</div>
{% endif %}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% load static %}
{% load static custom_filters%}
{% block title %}
{% if form.instance.id %}Edit Care Plan: {{ form.instance.title }}{% else %}New Care Plan{% endif %}
@ -142,19 +142,15 @@
<h5 class="form-section-title">Basic Information</h5>
<div class="row">
<div class="col-md-6 mb-3 required-field">
<div class="form-floating">
<label class="form-label" for="{{ form.patient.id_for_label }}">Patient</label>
{{ form.patient }}
<label for="{{ form.patient.id_for_label }}">Patient</label>
</div>
{% if form.patient.errors %}
<div class="invalid-feedback d-block">{{ form.patient.errors }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3 required-field">
<div class="form-floating">
<label class="form-label" for="{{ form.title.id_for_label }}">Care Plan Title</label>
{{ form.title }}
<label for="{{ form.title.id_for_label }}">Care Plan Title</label>
</div>
{% if form.title.errors %}
<div class="invalid-feedback d-block">{{ form.title.errors }}</div>
{% endif %}
@ -163,19 +159,15 @@
<div class="row">
<div class="col-md-6 mb-3 required-field">
<div class="form-floating">
<label class="form-label" for="{{ form.plan_type.id_for_label }}">Plan Type</label>
{{ form.plan_type }}
<label for="{{ form.plan_type.id_for_label }}">Plan Type</label>
</div>
{% if form.plan_type.errors %}
<div class="invalid-feedback d-block">{{ form.plan_type.errors }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3 required-field">
<div class="form-floating">
<label class="form-label" for="{{ form.category.id_for_label }}">Category</label>
{{ form.category }}
<label for="{{ form.category.id_for_label }}">Category</label>
</div>
{% if form.category.errors %}
<div class="invalid-feedback d-block">{{ form.category.errors }}</div>
{% endif %}
@ -184,7 +176,7 @@
<div class="row">
<div class="col-md-12 mb-3">
<label for="{{ form.description.id_for_label }}">Description</label>
<label class="form-label" for="{{ form.description.id_for_label }}">Description</label>
{{ form.description }}
{% if form.description.errors %}
<div class="invalid-feedback d-block">{{ form.description.errors }}</div>
@ -198,28 +190,22 @@
<h5 class="form-section-title">Timeline</h5>
<div class="row">
<div class="col-md-4 mb-3 required-field">
<div class="form-floating">
<label class="form-label" for="{{ form.start_date.id_for_label }}">Start Date</label>
{{ form.start_date }}
<label for="{{ form.start_date.id_for_label }}">Start Date</label>
</div>
{% if form.start_date.errors %}
<div class="invalid-feedback d-block">{{ form.start_date.errors }}</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
<div class="form-floating">
<label class="form-label" for="{{ form.target_completion_date.id_for_label }}">Target Completion Date</label>
{{ form.target_completion_date }}
<label for="{{ form.target_completion_date.id_for_label }}">Target Completion Date</label>
</div>
{% if form.target_completion_date.errors %}
<div class="invalid-feedback d-block">{{ form.target_completion_date.errors }}</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
<div class="form-floating">
<label class="form-label" for="{{ form.end_date.id_for_label }}">End Date</label>
{{ form.end_date }}
<label for="{{ form.end_date.id_for_label }}">End Date</label>
</div>
{% if form.end_date.errors %}
<div class="invalid-feedback d-block">{{ form.end_date.errors }}</div>
{% endif %}
@ -228,28 +214,22 @@
<div class="row">
<div class="col-md-4 mb-3">
<div class="form-floating">
<label class="form-label" for="{{ form.next_review_date.id_for_label }}">Next Review Date</label>
{{ form.next_review_date }}
<label for="{{ form.next_review_date.id_for_label }}">Next Review Date</label>
</div>
{% if form.next_review_date.errors %}
<div class="invalid-feedback d-block">{{ form.next_review_date.errors }}</div>
{% endif %}
</div>
<div class="col-md-4 mb-3 required-field">
<div class="form-floating">
<label class="form-label" for="{{ form.status.id_for_label }}">Status</label>
{{ form.status }}
<label for="{{ form.status.id_for_label }}">Status</label>
</div>
{% if form.status.errors %}
<div class="invalid-feedback d-block">{{ form.status.errors }}</div>
{% endif %}
</div>
<div class="col-md-4 mb-3 required-field">
<div class="form-floating">
<label class="form-label" for="{{ form.priority.id_for_label }}">Priority</label>
{{ form.priority }}
<label for="{{ form.priority.id_for_label }}">Priority</label>
</div>
{% if form.priority.errors %}
<div class="invalid-feedback d-block">{{ form.priority.errors }}</div>
{% endif %}
@ -258,10 +238,8 @@
<div class="row">
<div class="col-md-6 mb-3 required-field">
<div class="form-floating">
<label class="form-label" for="{{ form.completion_percentage.id_for_label }}">Completion Percentage</label>
{{ form.completion_percentage }}
<label for="{{ form.completion_percentage.id_for_label }}">Completion Percentage</label>
</div>
{% if form.completion_percentage.errors %}
<div class="invalid-feedback d-block">{{ form.completion_percentage.errors }}</div>
{% endif %}
@ -271,8 +249,8 @@
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch mt-4">
{{ form.approved }}
<label class="form-check-label" for="{{ form.approved.id_for_label }}">Approved</label>
{{ form.approved }}
</div>
{% if form.approved.errors %}
<div class="invalid-feedback d-block">{{ form.approved.errors }}</div>
@ -280,7 +258,7 @@
<div class="approved-fields" style="display: none;">
<div class="form-floating mt-2">
{{ form.approved_by }}
<label for="{{ form.approved_by.id_for_label }}">Approved By</label>
<label class="form-label" for="{{ form.approved_by.id_for_label }}">Approved By</label>
</div>
{% if form.approved_by.errors %}
<div class="invalid-feedback d-block">{{ form.approved_by.errors }}</div>
@ -295,17 +273,15 @@
<h5 class="form-section-title">Provider Information</h5>
<div class="row">
<div class="col-md-6 mb-3 required-field">
<div class="form-floating">
<label class="form-label" for="{{ form.primary_provider.id_for_label }}">Primary Provider</label>
{{ form.primary_provider }}
<label for="{{ form.primary_provider.id_for_label }}">Primary Provider</label>
</div>
{% if form.primary_provider.errors %}
<div class="invalid-feedback d-block">{{ form.primary_provider.errors }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="{{ form.care_team.id_for_label }}">Care Team</label>
{{ form.care_team }}
<label class="form-label" for="{{ form.care_team.id_for_label }}">Care Team</label>
{{ form.care_team|attr:'multiple' }}
{% if form.care_team.errors %}
<div class="invalid-feedback d-block">{{ form.care_team.errors }}</div>
{% endif %}
@ -318,8 +294,8 @@
<h5 class="form-section-title">Related Problems</h5>
<div class="row">
<div class="col-md-12 mb-3">
<label for="{{ form.related_problems.id_for_label }}">Related Problems</label>
{{ form.related_problems }}
<label class="form-label" for="{{ form.related_problems.id_for_label }}">Related Problems</label>
{{ form.related_problems|attr:'multiple' }}
{% if form.related_problems.errors %}
<div class="invalid-feedback d-block">{{ form.related_problems.errors }}</div>
{% endif %}
@ -362,15 +338,14 @@
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-floating">
<select class="form-select" name="goal_status[]">
<label>Status</label>
<select class="form-select form-select-sm" name="goal_status[]">
<option value="not_started">Not Started</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
<label>Status</label>
</div>
</div>
</div>
</div>
@ -413,15 +388,13 @@
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-floating">
<label>Status</label>
<select class="form-select" name="objective_status[]">
<option value="not_started">Not Started</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
<label>Status</label>
</div>
</div>
</div>
</div>
@ -468,15 +441,13 @@
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-floating">
<label>Status</label>
<select class="form-select" name="intervention_status[]">
<option value="not_started">Not Started</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
<label>Status</label>
</div>
</div>
</div>
</div>

View File

@ -63,9 +63,10 @@
</div>
<!-- Care Plans Table -->
<div class="card">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-sm table-hover mb-0 align-middle">
<thead class="table-primary">
<tr>
<th>Patient</th>
<th>Care Plan</th>
@ -145,16 +146,17 @@
<i class="fas fa-eye"></i>
</a>
{% if plan.status == 'ACTIVE' %}
<button class="btn btn-outline-info" title="Update Progress">
<a href="{% url 'emr:care_plan_progress_update' plan.id %}" class="btn btn-outline-info" title="Update Progress">
<i class="fas fa-chart-line"></i>
</a>
<button class="btn btn-outline-success"
title="Mark Complete"
hx-post="{% url 'emr:complete_care_plan' plan.id %}"
hx-confirm="Mark this care plan as complete?"
hx-swap="none"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<i class="fas fa-check"></i>
</button>
{# <button class="btn btn-outline-success" #}
{# title="Mark Complete"#}
{# hx-post="{% url 'emr:complete_care_plan' plan.id %}"#}
{# hx-confirm="Mark this care plan as complete?"#}
{# hx-swap="none">#}
{# <i class="fas fa-check"></i>#}
{# </button>#}
{% endif %}
<button class="btn btn-outline-secondary" title="Notes">
<i class="fas fa-sticky-note"></i>
@ -180,6 +182,7 @@
{% include 'partial/pagination.html' %}
{% endif %}
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,153 @@
{% extends "base.html" %}
{% load static %}
{% block css %}
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/bootstrap-icons/font/bootstrap-icons.css' %}" rel="stylesheet" />
<style>
.form-floating > .form-control:focus ~ label,
.form-floating > .form-control:not(:placeholder-shown) ~ label,
.form-floating > .form-select ~ label {
opacity: .65;
transform: scale(.85) translateY(-0.5rem) translateX(0.15rem);
}
.form-floating > label {
position: absolute;
top: 0;
left: 0;
height: 100%;
padding: 1rem 0.75rem;
pointer-events: none;
border: 1px solid transparent;
transform-origin: 0 0;
transition: opacity .1s ease-in-out, transform .1s ease-in-out;
}
.form-section {
padding: 1.5rem;
border-radius: 0.5rem;
background-color: rgba(var(--bs-light-rgb), 0.5);
margin-bottom: 1.5rem;
}
.form-section-title {
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--bs-gray-300);
}
.required-field label::after {
content: " *";
color: var(--bs-danger);
}
.help-sidebar {
position: sticky;
top: 1rem;
}
.help-card {
transition: all 0.2s;
}
.help-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.dynamic-formset {
border: 1px solid var(--bs-gray-300);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
position: relative;
}
.dynamic-formset .remove-formset {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
.preview-section {
border: 1px solid var(--bs-gray-300);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
}
.preview-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.preview-content {
font-size: 0.9rem;
}
.preview-badge {
font-size: 0.8rem;
padding: 0.35em 0.65em;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-xl-12">
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-tasks me-2"></i>Update Care Plan Progress — {{ object.title }}
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<form method="post" novalidate>
{% csrf_token %}
<div class="row g-3">
<div class="col-md-4">{{ form.status.label_tag }} {{ form.status }}</div>
<div class="col-md-4">{{ form.completion_percentage.label_tag }} {{ form.completion_percentage }}</div>
<div class="col-md-4">{{ form.last_reviewed.label_tag }} {{ form.last_reviewed }}</div>
<div class="col-md-4">{{ form.next_review_date.label_tag }} {{ form.next_review_date }}</div>
<div class="col-12">{{ form.progress_notes.label_tag }} {{ form.progress_notes }}</div>
<div class="col-12">{{ form.monitoring_parameters.label_tag }} {{ form.monitoring_parameters }}</div>
<div class="col-12">{{ form.evaluation_criteria.label_tag }} {{ form.evaluation_criteria }}</div>
<div class="col-12">{{ form.outcomes_achieved.label_tag }} {{ form.outcomes_achieved }}</div>
</div>
{% if form.non_field_errors %}
<div class="alert alert-danger mt-3">{{ form.non_field_errors }}</div>
{% endif %}
<div class="mt-4 d-flex gap-2">
<button class="btn btn-primary btn-sm" type="submit">
<i class="fas fa-save me-1"></i> Save Progress
</button>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'emr:care_plan_detail' object.pk %}">
Cancel
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize select2
$('.form-select').select2({
theme: 'bootstrap',
width: '100%'
});
// Initialize datepicker
$('.datepicker').datepicker({
format: 'yyyy-mm-dd',
autoclose: true,
todayHighlight: true
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,120 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div>
<h1 class="h2">
<i class="fas fa-dna me-2"></i>
ICD10
<span class="fw-light">
<span class="fw-light">{{ record.code }}</span>
</span>
{% if record.is_header %}
<span class="badge bg-secondary">Header</span>
{% endif %}
</h1>
<p class="text-muted">{{ record.description }}</p>
</div>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="{% url 'emr:icd10_search' %}" class="btn btn-sm btn-outline-theme">
<i class="fas fa-arrow-alt-circle-left me-2"></i> Back to search
</a>
</div>
</div>
</div>
<div class="container-fluid">
<div class="panel panel-inverse" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title"><i class="fas fa-search me-2"></i> {{ _("Search")}}</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row">
<div class="col-12">
{% if ancestors %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb small">
{% for a in ancestors %}
<li class="breadcrumb-item">
<a href="{% url 'emr:icd10_detail' a.code %}">{{ a.code }}</a>
</li>
{% endfor %}
<li class="breadcrumb-item active" aria-current="page">{{ record.code }}</li>
</ol>
</nav>
{% endif %}
<div class="card mb-4">
<div class="card-body">
{% if record %}
<dl class="row mb-0">
<dt class="col-sm-3">Code</dt>
<dd class="col-sm-9">{{ record.code }}</dd>
<dt class="col-sm-3">Description</dt>
<dd class="col-sm-9">
{{ record.description|default:"—" }}
</dd>
{% if record.section_name or record.chapter_name %}
<dt class="col-sm-3">Context</dt>
<dd class="col-sm-9">
{% if record.section_name %}<span class="me-2">Section: {{ record.section_name }}</span>{% endif %}
{% if record.chapter_name %}<span>Chapter: {{ record.chapter_name }}</span>{% endif %}
</dd>
{% endif %}
</dl>
{% else %}
<p class="text-muted mb-0">No data for this code.</p>
{% endif %}
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<h2 class="h6 mb-0">Subcodes</h2>
{% if children %}
<span class="text-muted small">{{ children|length }} item{{ children|length|pluralize }}</span>
{% endif %}
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 align-middle">
<thead class="table-primary">
<tr>
<th style="width: 160px;">Code</th>
<th>Description</th>
<th style="width: 120px;"></th>
</tr>
</thead>
<tbody>
{% for child in children %}
<tr>
<td class="fw-semibold">
{{ child.code }}
{% if child.is_header %}<span class="badge bg-secondary ms-1">Header</span>{% endif %}
</td>
<td>{{ child.description|default:"—" }}</td>
<td class="text-end">
<a class="btn btn-outline-primary btn-sm" href="{% url 'emr:icd10_detail' child.code %}">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-muted">No subcodes.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,107 @@
{% extends 'base.html' %}
{% load static %}
{% load humanize custom_filters %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div>
<h1 class="h2">
<i class="fas fa-book-medical me-2"></i>ICD10<span class="fw-light">Search</span>
</h1>
<p class="text-muted">Classification of Diseases, Functioning, and Disability</p>
</div>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
</div>
</div>
</div>
<div class="container-fluid">
<div class="panel panel-inverse" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title"><i class="fas fa-search me-2"></i> {{ _("Search")}}</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row">
<div class="col-12">
<form method="get" class="row gy-2 gx-3 align-items-center mb-4">
<div class="col-sm-6">
<input type="text" name="q" value="{{ query }}" class="form-control" placeholder="Type description or code…">
</div>
<div class="col-sm-3">
<select name="mode" class="form-select">
<option value="description" {% if mode == "description" %}selected{% endif %}>Search by description</option>
<option value="code" {% if mode == "code" %}selected{% endif %}>Search by code</option>
</select>
</div>
<div class="col-sm-3 d-grid d-sm-block">
<button class="btn btn-primary" type="submit">Search</button>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-12">
{% if page_obj %}
<div class="card">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 align-middle">
<thead class="table-primary">
<tr>
<th style="width: 140px;">Code</th>
<th>Description</th>
<th style="width: 140px;"></th>
</tr>
</thead>
<tbody>
{% for item in results %}
<tr>
<td class="fw-semibold">
{{ item.code }}
{% if item.is_header %}
<span class="badge bg-secondary ms-1">Header</span>
{% endif %}
</td>
<td>
{{ item.description|default:"—" }}
{% if item.section_name or item.chapter_name %}
<div class="small text-muted mt-1">
{% if item.section_name %}Section: {{ item.section_name }}{% endif %}
{% if item.section_name and item.chapter_name %} · {% endif %}
{% if item.chapter_name %}Chapter: {{ item.chapter_name }}{% endif %}
</div>
{% endif %}
</td>
<td class="text-end">
<a class="btn btn-outline-primary btn-sm" href="{% url 'emr:icd10_detail' item.code %}">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-muted">No results.</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if is_paginated %}
{% include 'partial/pagination.html' %}
{% endif %}
</div>
</div>
{% elif query %}
<div class="alert alert-info">No results found.</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -4,7 +4,7 @@
{% block title %}{{ problem.problem_name }} | Problem Detail{% endblock %}
{% block css %}
<link href="{% static 'plugins/timeline/timeline.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/lity/dist/lity.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/bootstrap-icons/font/bootstrap-icons.css' %}" rel="stylesheet" />
<style>
.problem-badge {
@ -12,10 +12,10 @@
padding: 0.35em 0.65em;
}
.problem-header {
background: linear-gradient(45deg, var(--bs-indigo), var(--bs-purple));
background: linear-gradient(90deg, var(--bs-black), var(--bs-secondary));
color: white;
padding: 1.5rem;
border-radius: 0.5rem;
border-radius: 0.25rem;
margin-bottom: 1.5rem;
}
.problem-status-active {
@ -71,310 +71,316 @@
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div>
<h1 class="h2">
<i class="fas fa-book-medical me-2"></i>Problem<span class="fw-light">Detail</span>
</h1>
<p class="text-muted">Comprehensive problem information</p>
</div>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="{% url 'emr:problem_list' %}" class="btn btn-sm btn-outline-theme me-2">
<i class="fa fa-arrow-left me-1"></i> Back to Problem List
</a>
</div>
</div>
</div>
<!-- begin page-header -->
<h1 class="page-header">Problem Detail <small>Comprehensive problem information</small></h1>
<!-- end page-header -->
<div class="container-fluid">
<div class="row">
<div class="col-xl-12">
<div class="problem-header d-flex justify-content-between align-items-center">
<div>
<h2 class="mb-1">{{ problem.problem_name }}</h2>
<div class="d-flex align-items-center">
<span class="me-3">
<i class="bi bi-person-fill me-1"></i> {{ problem.patient.get_full_name }}
</span>
<span class="me-3">
<i class="bi bi-calendar-event me-1"></i> Onset: {{ problem.onset_date|default:"Not specified" }}
</span>
{% if problem.problem_code %}
<span>
<i class="bi bi-upc-scan me-1"></i> {{ problem.get_coding_system_display }}: {{ problem.problem_code }}
</span>
{% endif %}
</div>
</div>
<div class="d-flex">
<p class="fw-bold me-2">Status:
<span class="badge problem-status-{{ problem.status|lower }}">{{ problem.get_status_display }}</span>
</p>
<p class="fw-bold me-2">Priority:
<span class="badge problem-priority-{{ problem.priority|lower }}">{{ problem.get_priority_display }} Priority</span>
</p>
<p class="fw-bold me-2">Severity:
<span class="badge problem-severity-{{ problem.severity|lower }}">{{ problem.get_severity_display }}</span>
</p>
</div>
</div>
<!-- end problem header -->
<!-- begin row -->
<div class="row">
<!-- begin col-12 -->
<div class="col-xl-12">
<!-- begin problem header -->
<div class="problem-header d-flex justify-content-between align-items-center">
<div>
<h2 class="mb-1">{{ problem.problem_name }}</h2>
<div class="d-flex align-items-center">
<span class="me-3">
<i class="bi bi-person-fill me-1"></i> {{ problem.patient.get_full_name }}
</span>
<span class="me-3">
<i class="bi bi-calendar-event me-1"></i> Onset: {{ problem.onset_date|default:"Not specified" }}
</span>
{% if problem.problem_code %}
<span>
<i class="bi bi-upc-scan me-1"></i> {{ problem.get_coding_system_display }}: {{ problem.problem_code }}
</span>
<!-- begin panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Problem Information</h4>
<div class="panel-heading-btn">
<a href="{% url 'patients:patient_detail' problem.patient.id %}" class="btn btn-xs btn-outline-secondary me-2">
<i class="fa fa-user me-1"></i> Patient Profile
</a>
{% if problem.status != 'RESOLVED' and problem.status != 'REMISSION' %}
<a href="#" class="btn btn-xs btn-outline-success me-2" data-bs-toggle="modal" data-bs-target="#resolveModal">
<i class="fa fa-check-circle me-1"></i>Mark as Resolved
</a>
{% endif %}
<a href="{% url 'emr:problem_update' problem.id %}" class="btn btn-xs btn-outline-warning me-2">
<i class="fa fa-edit me-1"></i>Edit
</a>
<a href="{% url 'emr:problem_delete' problem.id %}" class="btn btn-xs btn-outline-danger me-2">
<i class="fa fa-trash me-1"></i>Delete
</a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row">
<!-- begin col-6 -->
<div class="col-md-6">
<div class="mb-4">
<h5 class="mb-3">Basic Information</h5>
<table class="table table-bordered">
<tbody>
<tr>
<th width="35%">Problem ID</th>
<td>{{ problem.problem_id }}</td>
</tr>
<tr>
<th>Problem Type</th>
<td>{{ problem.get_problem_type_display }}</td>
</tr>
<tr>
<th>Status</th>
<td>{{ problem.get_status_display }}</td>
</tr>
<tr>
<th>Onset Date</th>
<td>{{ problem.onset_date|default:"Not specified" }}</td>
</tr>
{% if problem.status == 'RESOLVED' or problem.status == 'REMISSION' %}
<tr>
<th>Resolution Date</th>
<td>{{ problem.resolution_date }}</td>
</tr>
<tr>
<th>Duration</th>
<td>{{ problem.duration.days }} days</td>
</tr>
{% endif %}
<tr>
<th>Body Site</th>
<td>{{ problem.body_site|default:"Not specified" }}</td>
</tr>
<tr>
<th>Laterality</th>
<td>{{ problem.get_laterality_display|default:"Not specified" }}</td>
</tr>
</tbody>
</table>
</div>
{% if problem.clinical_notes %}
<div class="mb-4">
<h5 class="mb-3">Clinical Notes</h5>
<div class="p-3 bg-light rounded">
{{ problem.clinical_notes|linebreaks }}
</div>
</div>
{% endif %}
{% if problem.patient_concerns %}
<div class="mb-4">
<h5 class="mb-3">Patient Concerns</h5>
<div class="p-3 bg-light rounded">
{{ problem.patient_concerns|linebreaks }}
</div>
</div>
{% endif %}
</div>
<!-- end col-6 -->
<!-- begin col-6 -->
<div class="col-md-6">
<div class="mb-4">
<h5 class="mb-3">Provider Information</h5>
<table class="table table-bordered">
<tbody>
<tr>
<th width="35%">Diagnosing Provider</th>
<td>{{ problem.diagnosing_provider.get_full_name }}</td>
</tr>
<tr>
<th>Managing Provider</th>
<td>{{ problem.managing_provider.get_full_name|default:"Not assigned" }}</td>
</tr>
<tr>
<th>Related Encounter</th>
<td>
{% if problem.related_encounter %}
<a href="{% url 'emr:encounter_detail' problem.related_encounter.id %}">
{{ problem.related_encounter.encounter_type }} ({{ problem.related_encounter.start_datetime|date:"M d, Y" }})
</a>
{% else %}
Not linked to an encounter
{% endif %}
</td>
</tr>
<tr>
<th>Verified</th>
<td>
{% if problem.verified %}
<span class="badge bg-success">Verified</span>
by {{ problem.verified_by.get_full_name }} on {{ problem.verified_date|date:"M d, Y" }}
{% else %}
<span class="badge bg-warning">Not Verified</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
{% if problem.treatment_goals %}
<div class="mb-4">
<h5 class="mb-3">Treatment Goals</h5>
<div class="p-3 bg-light rounded">
{{ problem.treatment_goals|linebreaks }}
</div>
</div>
{% endif %}
{% if problem.outcome_measures %}
<div class="mb-4">
<h5 class="mb-3">Outcome Measures</h5>
<div class="p-3 bg-light rounded">
{{ problem.outcome_measures|linebreaks }}
</div>
</div>
{% endif %}
{% if problem.status == 'RESOLVED' or problem.status == 'REMISSION' %}
<div class="mb-4">
<h5 class="mb-3">Resolution Notes</h5>
<div class="p-3 bg-light rounded">
{{ problem.resolution_notes|linebreaks|default:"No resolution notes provided." }}
</div>
</div>
{% endif %}
</div>
<!-- end col-6 -->
</div>
</div>
</div>
<!-- end panel -->
<!-- begin related care plans panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Related Care Plans</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">
{% if problem.care_plans.all %}
<div class="row">
{% for care_plan in problem.care_plans.all %}
<div class="col-md-6 col-xl-4 mb-3">
<div class="card related-item h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">{{ care_plan.title }}</h5>
<span class="badge bg-{{ care_plan.status|lower }}">{{ care_plan.get_status_display }}</span>
</div>
<div class="card-body">
<p class="card-text">{{ care_plan.description|truncatechars:100 }}</p>
<div class="progress mb-2" style="height: 10px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: {{ care_plan.completion_percentage }}%">{{ care_plan.completion_percentage }}%</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">{{ care_plan.get_plan_type_display }}</small>
<small class="text-muted">{{ care_plan.start_date|date:"M d, Y" }}</small>
</div>
</div>
<div class="card-footer">
<a href="{% url 'emr:care_plan_detail' care_plan.id %}" class="btn btn-sm btn-primary">View Care Plan</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<i class="fa fa-info-circle me-2"></i> No care plans are associated with this problem.
</div>
{% endif %}
</div>
</div>
<div class="d-flex">
<span class="badge problem-status-{{ problem.status|lower }} me-2">{{ problem.get_status_display }}</span>
<span class="badge problem-priority-{{ problem.priority|lower }} me-2">{{ problem.get_priority_display }} Priority</span>
<span class="badge problem-severity-{{ problem.severity|lower }}">{{ problem.get_severity_display }}</span>
</div>
</div>
<!-- end problem header -->
<!-- end related care plans panel -->
<!-- begin panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Problem Information</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row">
<!-- begin col-6 -->
<div class="col-md-6">
<div class="mb-4">
<h5 class="mb-3">Basic Information</h5>
<table class="table table-bordered">
<tbody>
<tr>
<th width="35%">Problem ID</th>
<td>{{ problem.problem_id }}</td>
</tr>
<tr>
<th>Problem Type</th>
<td>{{ problem.get_problem_type_display }}</td>
</tr>
<tr>
<th>Status</th>
<td>{{ problem.get_status_display }}</td>
</tr>
<tr>
<th>Onset Date</th>
<td>{{ problem.onset_date|default:"Not specified" }}</td>
</tr>
{% if problem.status == 'RESOLVED' or problem.status == 'REMISSION' %}
<tr>
<th>Resolution Date</th>
<td>{{ problem.resolution_date }}</td>
</tr>
<tr>
<th>Duration</th>
<td>{{ problem.duration.days }} days</td>
</tr>
{% endif %}
<tr>
<th>Body Site</th>
<td>{{ problem.body_site|default:"Not specified" }}</td>
</tr>
<tr>
<th>Laterality</th>
<td>{{ problem.get_laterality_display|default:"Not specified" }}</td>
</tr>
</tbody>
</table>
</div>
{% if problem.clinical_notes %}
<div class="mb-4">
<h5 class="mb-3">Clinical Notes</h5>
<div class="p-3 bg-light rounded">
{{ problem.clinical_notes|linebreaks }}
</div>
</div>
{% endif %}
{% if problem.patient_concerns %}
<div class="mb-4">
<h5 class="mb-3">Patient Concerns</h5>
<div class="p-3 bg-light rounded">
{{ problem.patient_concerns|linebreaks }}
</div>
</div>
{% endif %}
<!-- begin clinical notes panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Related Clinical Notes</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>
<!-- end col-6 -->
<!-- begin col-6 -->
<div class="col-md-6">
<div class="mb-4">
<h5 class="mb-3">Provider Information</h5>
<table class="table table-bordered">
<tbody>
<tr>
<th width="35%">Diagnosing Provider</th>
<td>{{ problem.diagnosing_provider.get_full_name }}</td>
</tr>
<tr>
<th>Managing Provider</th>
<td>{{ problem.managing_provider.get_full_name|default:"Not assigned" }}</td>
</tr>
<tr>
<th>Related Encounter</th>
<td>
{% if problem.related_encounter %}
<a href="{% url 'emr:encounter_detail' problem.related_encounter.id %}">
{{ problem.related_encounter.encounter_type }} ({{ problem.related_encounter.start_datetime|date:"M d, Y" }})
</a>
{% else %}
Not linked to an encounter
{% endif %}
</td>
</tr>
<tr>
<th>Verified</th>
<td>
{% if problem.verified %}
<span class="badge bg-success">Verified</span>
by {{ problem.verified_by.get_full_name }} on {{ problem.verified_date|date:"M d, Y" }}
{% else %}
<span class="badge bg-warning">Not Verified</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
{% if problem.treatment_goals %}
<div class="mb-4">
<h5 class="mb-3">Treatment Goals</h5>
<div class="p-3 bg-light rounded">
{{ problem.treatment_goals|linebreaks }}
</div>
</div>
{% endif %}
{% if problem.outcome_measures %}
<div class="mb-4">
<h5 class="mb-3">Outcome Measures</h5>
<div class="p-3 bg-light rounded">
{{ problem.outcome_measures|linebreaks }}
</div>
</div>
{% endif %}
{% if problem.status == 'RESOLVED' or problem.status == 'REMISSION' %}
<div class="mb-4">
<h5 class="mb-3">Resolution Notes</h5>
<div class="p-3 bg-light rounded">
{{ problem.resolution_notes|linebreaks|default:"No resolution notes provided." }}
</div>
</div>
{% endif %}
</div>
<!-- end col-6 -->
</div>
</div>
</div>
<!-- end panel -->
<!-- begin related care plans panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Related Care Plans</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
{% if problem.care_plans.all %}
<div class="row">
{% for care_plan in problem.care_plans.all %}
<div class="col-md-6 col-xl-4 mb-3">
<div class="card related-item h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">{{ care_plan.title }}</h5>
<span class="badge bg-{{ care_plan.status|lower }}">{{ care_plan.get_status_display }}</span>
<div class="panel-body">
{% if problem.related_clinical_notes.all %}
<div class="timeline">
{% for note in problem.related_clinical_notes.all %}
<div class="timeline-item">
<div class="timeline-time">
{{ note.note_datetime|date:"M d, Y" }} <small>{{ note.note_datetime|time:"h:i A" }}</small>
</div>
<div class="card-body">
<p class="card-text">{{ care_plan.description|truncatechars:100 }}</p>
<div class="progress mb-2" style="height: 10px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: {{ care_plan.completion_percentage }}%">{{ care_plan.completion_percentage }}%</div>
</div>
<div class="timeline-icon">
<a href="javascript:;">&nbsp;</a>
</div>
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">{{ care_plan.get_plan_type_display }}</small>
<small class="text-muted">{{ care_plan.start_date|date:"M d, Y" }}</small>
<h5>{{ note.title }}</h5>
<span class="badge bg-{{ note.status|lower }}">{{ note.get_status_display }}</span>
</div>
<p class="mb-2">{{ note.content|truncatechars:200 }}</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">{{ note.get_note_type_display }} by {{ note.author.get_full_name }}</small>
<a href="{% url 'emr:clinical_note_detail' note.id %}" class="btn btn-sm btn-primary">View Note</a>
</div>
</div>
<div class="card-footer">
<a href="{% url 'emr:care_plan_detail' care_plan.id %}" class="btn btn-sm btn-primary">View Care Plan</a>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<i class="fa fa-info-circle me-2"></i> No care plans are associated with this problem.
</div>
{% endif %}
</div>
</div>
<!-- end related care plans panel -->
<!-- begin clinical notes panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Related Clinical Notes</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
{% if problem.related_clinical_notes.all %}
<div class="timeline">
{% for note in problem.related_clinical_notes.all %}
<div class="timeline-item">
<div class="timeline-time">
{{ note.note_datetime|date:"M d, Y" }} <small>{{ note.note_datetime|time:"h:i A" }}</small>
</div>
<div class="timeline-icon">
<a href="javascript:;">&nbsp;</a>
</div>
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-center">
<h5>{{ note.title }}</h5>
<span class="badge bg-{{ note.status|lower }}">{{ note.get_status_display }}</span>
</div>
<p class="mb-2">{{ note.content|truncatechars:200 }}</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">{{ note.get_note_type_display }} by {{ note.author.get_full_name }}</small>
<a href="{% url 'emr:clinical_note_detail' note.id %}" class="btn btn-sm btn-primary">View Note</a>
</div>
</div>
{% else %}
<div class="alert alert-info">
<i class="fa fa-info-circle me-2"></i> No clinical notes are associated with this problem.
</div>
{% endfor %}
{% endif %}
</div>
{% else %}
<div class="alert alert-info">
<i class="fa fa-info-circle me-2"></i> No clinical notes are associated with this problem.
</div>
{% endif %}
</div>
</div>
<!-- end clinical notes panel -->
<!-- end clinical notes panel -->
<!-- begin action buttons -->
<div class="d-flex justify-content-between mt-3">
<div>
<a href="{% url 'emr:problem_list' %}" class="btn btn-secondary me-2">
<i class="fa fa-arrow-left me-1"></i> Back to Problem List
</a>
<a href="{% url 'patients:patient_detail' problem.patient.id %}" class="btn btn-info">
<i class="fa fa-user me-1"></i> Patient Profile
</a>
</div>
<div>
{% if problem.status != 'RESOLVED' and problem.status != 'REMISSION' %}
<a href="#" class="btn btn-success me-2" data-bs-toggle="modal" data-bs-target="#resolveModal">
<i class="fa fa-check-circle me-1"></i> Mark as Resolved
</a>
{% endif %}
<a href="{% url 'emr:problem_update' problem.id %}" class="btn btn-primary me-2">
<i class="fa fa-edit me-1"></i> Edit Problem
</a>
<a href="{% url 'emr:problem_delete' problem.id %}" class="btn btn-danger">
<i class="fa fa-trash me-1"></i> Delete
</a>
</div>
</div>
<!-- end action buttons -->
</div>
<!-- end col-12 -->
</div>
<!-- end row -->
<!-- begin resolve modal -->
<div class="modal fade" id="resolveModal" tabindex="-1" aria-labelledby="resolveModalLabel" aria-hidden="true">
@ -415,6 +421,7 @@
{% endblock %}
{% block js %}
<script src="{% static 'plugins/lity/dist/lity.min.js' %}"></script>
<script src="{% static 'plugins/moment/min/moment.min.js' %}"></script>
<script>
$(document).ready(function() {

View File

@ -4,165 +4,174 @@
{% block title %}Problem List - {{ block.super }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div>
<h1 class="h2">
<i class="fas fa-list me-2"></i>Problem<span class="fw-light">Management</span>
</h1>
<p class="text-muted">Comprehensive list of Diagnosis, Symptoms and more.</p>
</div>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="panel panel-inverse" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-list me-2"></i>Problem List Management
</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">
<!-- Filters -->
<div class="row mb-3">
<div class="col-md-2">
<select name="problem_type" class="form-select">
<option value="">All Types</option>
{% for value, label in problem_types %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select name="status" class="form-select">
<option value="">All Status</option>
{% for value, label in problem_statuses %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select name="priority" class="form-select">
<option value="">All Priorities</option>
{% for value, label in priorities %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<input type="text" name="search" class="form-control" placeholder="Search patients, problems, codes...">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<!-- Problems Table -->
<div class="card">
<div class="card-header">
<h4 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Problem List Management
</h4>
</div>
<div class="card-body">
<!-- Filters -->
<div class="row mb-3">
<div class="col-md-2">
<select name="problem_type" class="form-select">
<option value="">All Types</option>
{% for value, label in problem_types %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select name="status" class="form-select">
<option value="">All Status</option>
{% for value, label in problem_statuses %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select name="priority" class="form-select">
<option value="">All Priorities</option>
{% for value, label in priorities %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<input type="text" name="search" class="form-control" placeholder="Search patients, problems, codes...">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<!-- Problems Table -->
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Patient</th>
<th>Problem</th>
<th>Type</th>
<th>Priority</th>
<th>Status</th>
<th>Onset Date</th>
<th>Diagnosed By</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for problem in problems %}
<tr>
<td>
<strong>{{ problem.patient.get_full_name }}</strong><br>
<small class="text-muted">
MRN: {{ problem.patient.mrn }} •
{{ problem.patient.age }}y {{ problem.patient.get_gender_display }}
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 align-middle">
<thead class="table-primary">
<tr>
<th>Patient</th>
<th>Problem</th>
<th>Type</th>
<th>Priority</th>
<th>Status</th>
<th>Onset Date</th>
<th>Diagnosed By</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for problem in problems %}
<tr>
<td>
<strong>{{ problem.patient.get_full_name }}</strong><br>
<small class="text-muted">
MRN: {{ problem.patient.mrn }} •
{{ problem.patient.age }}y {{ problem.patient.get_gender_display }}
</small>
</td>
<td>
<strong>{{ problem.problem_name }}</strong>
{% if problem.problem_code %}
<br><small class="text-muted">Code: {{ problem.problem_code }}</small>
{% endif %}
{% if problem.description %}
<br><small class="text-muted">{{ problem.description|truncatechars:50 }}</small>
{% endif %}
</td>
<td>{{ problem.get_problem_type_display }}</td>
<td>
{% if problem.priority == 'HIGH' %}
<span class="badge bg-danger">High</span>
{% elif problem.priority == 'MEDIUM' %}
<span class="badge bg-warning">Medium</span>
{% else %}
<span class="badge bg-info">Low</span>
{% endif %}
</td>
<td>
{% if problem.status == 'ACTIVE' %}
<span class="badge bg-success">Active</span>
{% elif problem.status == 'RESOLVED' %}
<span class="badge bg-secondary">Resolved</span>
{% elif problem.status == 'INACTIVE' %}
<span class="badge bg-secondary">Inactive</span>
{% elif problem.status == 'RULED_OUT' %}
<span class="badge bg-danger">Ruled Out</span>
{% endif %}
</td>
<td>
{{ problem.onset_date|date:"M d, Y" }}
{% if problem.resolution_date %}
<br><small class="text-muted">
Resolved: {{ problem.resolution_date|date:"M d, Y" }}
</small>
</td>
<td>
<strong>{{ problem.problem_name }}</strong>
{% if problem.problem_code %}
<br><small class="text-muted">Code: {{ problem.problem_code }}</small>
{% endif %}
{% if problem.description %}
<br><small class="text-muted">{{ problem.description|truncatechars:50 }}</small>
{% endif %}
</td>
<td>{{ problem.get_problem_type_display }}</td>
<td>
{% if problem.priority == 'HIGH' %}
<span class="badge bg-danger">High</span>
{% elif problem.priority == 'MEDIUM' %}
<span class="badge bg-warning">Medium</span>
{% else %}
<span class="badge bg-info">Low</span>
{% endif %}
</td>
<td>
{% endif %}
</td>
<td>
{{ problem.diagnosing_provider.get_full_name }}
{% if problem.managing_provider and problem.managing_provider != problem.diagnosing_provider %}
<br><small class="text-muted">
Managing: {{ problem.managing_provider.get_full_name }}
</small>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{% url 'emr:problem_detail' problem.id %}" class="btn btn-outline-primary" title="View Details">
<i class="fas fa-eye"></i>
</a>
{% if problem.status == 'ACTIVE' %}
<span class="badge bg-success">Active</span>
{% elif problem.status == 'RESOLVED' %}
<span class="badge bg-secondary">Resolved</span>
{% elif problem.status == 'INACTIVE' %}
<span class="badge bg-secondary">Inactive</span>
{% elif problem.status == 'RULED_OUT' %}
<span class="badge bg-danger">Ruled Out</span>
<button class="btn btn-outline-success"
title="Mark Resolved"
hx-post="{% url 'emr:resolve_problem' problem.id %}"
hx-confirm="Mark this problem as resolved?"
hx-swap="none"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<i class="fas fa-check"></i>
</button>
{% endif %}
</td>
<td>
{{ problem.onset_date|date:"M d, Y" }}
{% if problem.resolution_date %}
<br><small class="text-muted">
Resolved: {{ problem.resolution_date|date:"M d, Y" }}
</small>
{% endif %}
</td>
<td>
{{ problem.diagnosing_provider.get_full_name }}
{% if problem.managing_provider and problem.managing_provider != problem.diagnosing_provider %}
<br><small class="text-muted">
Managing: {{ problem.managing_provider.get_full_name }}
</small>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" title="View Details">
<i class="fas fa-eye"></i>
</button>
{% if problem.status == 'ACTIVE' %}
<button class="btn btn-outline-success"
title="Mark Resolved"
hx-post="{% url 'emr:resolve_problem' problem.id %}"
hx-confirm="Mark this problem as resolved?"
hx-swap="none">
<i class="fas fa-check"></i>
</button>
{% endif %}
<button class="btn btn-outline-info" title="Care Plan">
<i class="fas fa-clipboard-list"></i>
</button>
<button class="btn btn-outline-secondary" title="Notes">
<i class="fas fa-sticky-note"></i>
</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center py-4">
<i class="fas fa-list fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No problems found</h5>
<p class="text-muted">No problems match your current filters.</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if is_paginated %}
{% include 'partial/pagination.html' %}
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center py-4">
<i class="fas fa-list fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No problems found</h5>
<p class="text-muted">No problems match your current filters.</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if is_paginated %}
{% include 'partial/pagination.html' %}
{% endif %}
</div>
</div>
</div>

View File

@ -29,6 +29,9 @@ urlpatterns = [
path('care-plans/<int:pk>/', views.CarePlanDetailView.as_view(), name='care_plan_detail'),
path('care-plans/<int:pk>/update/', views.CarePlanUpdateView.as_view(), name='care_plan_update'),
path('care-plans/<int:pk>/delete/', views.CarePlanDeleteView.as_view(), name='care_plan_delete'),
path('care-plans/<int:pk>/progress/', views.CarePlanProgressUpdateView.as_view(), name='care_plan_progress_update'),
path('care-plans/approve/<int:pk>/', views.approve_care_plan, name='approve_care_plan'),
path('care-plans/complete/<int:pk>/', views.complete_care_plan, name='complete_care_plan'),
path('notes/', views.ClinicalNoteListView.as_view(), name='clinical_note_list'),
path('notes/<int:pk>/', views.ClinicalNoteDetailView.as_view(), name='clinical_note_detail'),
@ -50,6 +53,10 @@ urlpatterns = [
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("icd10/", views.Icd10SearchView.as_view(), name="icd10_search"),
path("icd10/<str:code>/", views.Icd10DetailView.as_view(), name="icd10_detail"),
# API endpoints
# path('api/', include('emr.api.urls')),
]

17
emr/utils.py Normal file
View File

@ -0,0 +1,17 @@
from django.conf import settings
from python_icd10.icd10_warehouse import ICD10Warehouse
icd10 = ICD10Warehouse(settings.BASE_DIR)
def search_by_description(description):
return icd10.search_by_description(description)
def search_by_code(code):
return icd10.search_by_code(code)
def get_by_code(code):
return icd10.get_by_code(code)

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