update
This commit is contained in:
parent
beba30e532
commit
2780a2dc7c
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
accounts/__pycache__/signals.cpython-312.pyc
Normal file
BIN
accounts/__pycache__/signals.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -3,75 +3,45 @@ Admin configuration for accounts app.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
||||||
|
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from .models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
|
from .models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
|
||||||
|
from hr.models import Employee
|
||||||
|
|
||||||
|
class EmployeeInline(admin.StackedInline):
|
||||||
|
model = Employee
|
||||||
|
can_delete = False
|
||||||
|
fk_name = 'user'
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
@admin.register(User)
|
@admin.register(User)
|
||||||
class UserAdmin(BaseUserAdmin):
|
class UserAdmin(DjangoUserAdmin):
|
||||||
"""
|
inlines = [EmployeeInline]
|
||||||
Admin configuration for User model.
|
list_display = ('username', 'email', 'tenant', 'is_active', 'is_staff', 'two_factor_enabled', 'locked_until')
|
||||||
"""
|
list_filter = ('tenant', 'is_active', 'is_staff', 'is_superuser', 'two_factor_enabled')
|
||||||
list_display = [
|
search_fields = ('username', 'email', 'first_name', 'last_name')
|
||||||
'username', 'email', 'get_full_name', 'role', 'tenant',
|
readonly_fields = ('last_login', 'date_joined', 'last_password_change')
|
||||||
'is_active', 'is_verified', 'is_approved', 'last_login'
|
fieldsets = (
|
||||||
]
|
(None, {'fields': ('tenant', 'username', 'password')}),
|
||||||
list_filter = [
|
('Personal info', {'fields': ('first_name', 'last_name', 'email')}),
|
||||||
'role', 'tenant', 'is_active', 'is_verified', 'is_approved',
|
('Security', {'fields': (
|
||||||
'two_factor_enabled', 'is_staff', 'is_superuser'
|
'force_password_change', 'password_expires_at', 'last_password_change',
|
||||||
]
|
|
||||||
search_fields = [
|
|
||||||
'username', 'email', 'first_name', 'last_name',
|
|
||||||
'employee_id', 'license_number', 'npi_number'
|
|
||||||
]
|
|
||||||
ordering = ['last_name', 'first_name']
|
|
||||||
|
|
||||||
fieldsets = BaseUserAdmin.fieldsets + (
|
|
||||||
('Tenant Information', {
|
|
||||||
'fields': ('tenant', 'user_id')
|
|
||||||
}),
|
|
||||||
('Personal Information', {
|
|
||||||
'fields': (
|
|
||||||
'middle_name', 'preferred_name', 'phone_number', 'mobile_number',
|
|
||||||
'profile_picture', 'bio'
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
('Professional Information', {
|
|
||||||
'fields': (
|
|
||||||
'employee_id', 'department', 'job_title', 'role',
|
|
||||||
'license_number', 'license_state', 'license_expiry',
|
|
||||||
'dea_number', 'npi_number'
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
('Security Settings', {
|
|
||||||
'fields': (
|
|
||||||
'force_password_change', 'password_expires_at',
|
|
||||||
'failed_login_attempts', 'locked_until', 'two_factor_enabled',
|
'failed_login_attempts', 'locked_until', 'two_factor_enabled',
|
||||||
'max_concurrent_sessions', 'session_timeout_minutes'
|
'max_concurrent_sessions', 'session_timeout_minutes',
|
||||||
|
)}),
|
||||||
|
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
|
||||||
|
('Important dates', {'fields': ('last_login', 'date_joined')}),
|
||||||
)
|
)
|
||||||
}),
|
add_fieldsets = (
|
||||||
('Preferences', {
|
(None, {
|
||||||
'fields': ( 'language', 'theme')
|
'classes': ('wide',),
|
||||||
}),
|
'fields': ('tenant', 'username', 'email', 'password1', 'password2'),
|
||||||
('Status', {
|
|
||||||
'fields': (
|
|
||||||
'is_verified', 'is_approved', 'approval_date', 'approved_by'
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
('Metadata', {
|
|
||||||
'fields': ('created_at', 'updated_at', 'last_password_change'),
|
|
||||||
'classes': ('collapse',)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly_fields = [
|
|
||||||
'user_id', 'created_at', 'updated_at', 'last_password_change'
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
|
||||||
return super().get_queryset(request).select_related('tenant', 'approved_by')
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(TwoFactorDevice)
|
@admin.register(TwoFactorDevice)
|
||||||
class TwoFactorDeviceAdmin(admin.ModelAdmin):
|
class TwoFactorDeviceAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@ -4,3 +4,6 @@ from django.apps import AppConfig
|
|||||||
class AccountsConfig(AppConfig):
|
class AccountsConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'accounts'
|
name = 'accounts'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import signals
|
||||||
|
|||||||
@ -15,25 +15,23 @@ class UserForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = [
|
fields = [
|
||||||
'first_name', 'last_name', 'email', 'phone_number', 'mobile_number',
|
'email',
|
||||||
'employee_id', 'role', 'department', 'bio', 'user_timezone', 'language',
|
|
||||||
'theme', 'is_active', 'is_approved'
|
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
# 'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
# 'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||||
'phone_number': forms.TextInput(attrs={'class': 'form-control'}),
|
# 'phone_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'mobile_number': forms.TextInput(attrs={'class': 'form-control'}),
|
# 'mobile_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
|
# 'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'role': forms.Select(attrs={'class': 'form-select'}),
|
# 'role': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'department': forms.TextInput(attrs={'class': 'form-control'}),
|
# 'department': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
# 'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
'user_timezone': forms.Select(attrs={'class': 'form-select'}),
|
# 'user_timezone': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'language': forms.Select(attrs={'class': 'form-select'}),
|
# 'language': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'theme': forms.Select(attrs={'class': 'form-select'}),
|
# 'theme': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
# 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
'is_approved': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
# 'is_approved': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -45,23 +43,22 @@ class UserCreateForm(UserCreationForm):
|
|||||||
last_name = forms.CharField(max_length=150, required=True)
|
last_name = forms.CharField(max_length=150, required=True)
|
||||||
email = forms.EmailField(required=True)
|
email = forms.EmailField(required=True)
|
||||||
employee_id = forms.CharField(max_length=50, required=False)
|
employee_id = forms.CharField(max_length=50, required=False)
|
||||||
role = forms.ChoiceField(choices=User._meta.get_field('role').choices, required=True)
|
# role = forms.ChoiceField(choices=User._meta.get_field('role').choices, required=True)
|
||||||
department = forms.CharField(max_length=100, required=False)
|
department = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = [
|
fields = [
|
||||||
'username', 'first_name', 'last_name', 'email', 'employee_id',
|
'username', 'first_name', 'last_name', 'email', 'password1', 'password2'
|
||||||
'role', 'department', 'password1', 'password2'
|
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'username': forms.TextInput(attrs={'class': 'form-control'}),
|
'username': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||||
'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
|
# 'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'role': forms.Select(attrs={'class': 'form-select'}),
|
# 'role': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'department': forms.TextInput(attrs={'class': 'form-control'}),
|
# 'department': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -148,11 +145,11 @@ class AccountsSearchForm(forms.Form):
|
|||||||
'placeholder': 'Search users, sessions, devices...'
|
'placeholder': 'Search users, sessions, devices...'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
role = forms.ChoiceField(
|
# role = forms.ChoiceField(
|
||||||
choices=[('', 'All Roles')] + list(User._meta.get_field('role').choices),
|
# choices=[('', 'All Roles')] + list(User._meta.get_field('role').choices),
|
||||||
required=False,
|
# required=False,
|
||||||
widget=forms.Select(attrs={'class': 'form-select'})
|
# widget=forms.Select(attrs={'class': 'form-select'})
|
||||||
)
|
# )
|
||||||
status = forms.ChoiceField(
|
status = forms.ChoiceField(
|
||||||
choices=[
|
choices=[
|
||||||
('', 'All Status'),
|
('', 'All Status'),
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
# Generated by Django 5.2.6 on 2025-09-15 14:05
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.contrib.auth.validators
|
|
||||||
import django.core.validators
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
import uuid
|
import uuid
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@ -390,21 +386,6 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="superuser status",
|
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",
|
"first_name",
|
||||||
models.CharField(
|
models.CharField(
|
||||||
@ -417,12 +398,6 @@ class Migration(migrations.Migration):
|
|||||||
blank=True, max_length=150, verbose_name="last name"
|
blank=True, max_length=150, verbose_name="last name"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"email",
|
|
||||||
models.EmailField(
|
|
||||||
blank=True, max_length=254, verbose_name="email address"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"is_staff",
|
"is_staff",
|
||||||
models.BooleanField(
|
models.BooleanField(
|
||||||
@ -447,157 +422,17 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_id",
|
"user_id",
|
||||||
models.UUIDField(
|
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||||
default=uuid.uuid4,
|
),
|
||||||
editable=False,
|
(
|
||||||
help_text="Unique user identifier",
|
"username",
|
||||||
|
models.CharField(
|
||||||
|
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||||
|
max_length=150,
|
||||||
unique=True,
|
unique=True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
("email", models.EmailField(blank=True, max_length=254, null=True)),
|
||||||
"middle_name",
|
|
||||||
models.CharField(
|
|
||||||
blank=True, help_text="Middle name", max_length=150, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"preferred_name",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Preferred name",
|
|
||||||
max_length=150,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"phone_number",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Primary phone number",
|
|
||||||
max_length=20,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.RegexValidator(
|
|
||||||
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.',
|
|
||||||
regex="^\\+?1?\\d{9,15}$",
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"mobile_number",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Mobile phone number",
|
|
||||||
max_length=20,
|
|
||||||
null=True,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.RegexValidator(
|
|
||||||
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.',
|
|
||||||
regex="^\\+?1?\\d{9,15}$",
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"employee_id",
|
|
||||||
models.CharField(
|
|
||||||
blank=True, help_text="Employee ID", max_length=50, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"department",
|
|
||||||
models.CharField(
|
|
||||||
blank=True, help_text="Department", max_length=100, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"job_title",
|
|
||||||
models.CharField(
|
|
||||||
blank=True, help_text="Job title", max_length=100, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"role",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("SUPER_ADMIN", "Super Administrator"),
|
|
||||||
("ADMIN", "Administrator"),
|
|
||||||
("PHYSICIAN", "Physician"),
|
|
||||||
("NURSE", "Nurse"),
|
|
||||||
("NURSE_PRACTITIONER", "Nurse Practitioner"),
|
|
||||||
("PHYSICIAN_ASSISTANT", "Physician Assistant"),
|
|
||||||
("PHARMACIST", "Pharmacist"),
|
|
||||||
("PHARMACY_TECH", "Pharmacy Technician"),
|
|
||||||
("LAB_TECH", "Laboratory Technician"),
|
|
||||||
("RADIOLOGIST", "Radiologist"),
|
|
||||||
("RAD_TECH", "Radiology Technician"),
|
|
||||||
("THERAPIST", "Therapist"),
|
|
||||||
("SOCIAL_WORKER", "Social Worker"),
|
|
||||||
("CASE_MANAGER", "Case Manager"),
|
|
||||||
("BILLING_SPECIALIST", "Billing Specialist"),
|
|
||||||
("REGISTRATION", "Registration Staff"),
|
|
||||||
("SCHEDULER", "Scheduler"),
|
|
||||||
("MEDICAL_ASSISTANT", "Medical Assistant"),
|
|
||||||
("CLERICAL", "Clerical Staff"),
|
|
||||||
("IT_SUPPORT", "IT Support"),
|
|
||||||
("QUALITY_ASSURANCE", "Quality Assurance"),
|
|
||||||
("COMPLIANCE", "Compliance Officer"),
|
|
||||||
("SECURITY", "Security"),
|
|
||||||
("MAINTENANCE", "Maintenance"),
|
|
||||||
("VOLUNTEER", "Volunteer"),
|
|
||||||
("STUDENT", "Student"),
|
|
||||||
("RESEARCHER", "Researcher"),
|
|
||||||
("CONSULTANT", "Consultant"),
|
|
||||||
("VENDOR", "Vendor"),
|
|
||||||
("GUEST", "Guest"),
|
|
||||||
],
|
|
||||||
default="CLERICAL",
|
|
||||||
max_length=50,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"license_number",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Professional license number",
|
|
||||||
max_length=100,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"license_state",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="License issuing state",
|
|
||||||
max_length=50,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"license_expiry",
|
|
||||||
models.DateField(
|
|
||||||
blank=True, help_text="License expiry date", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"dea_number",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="DEA number for prescribing",
|
|
||||||
max_length=20,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"npi_number",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="National Provider Identifier",
|
|
||||||
max_length=10,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"force_password_change",
|
"force_password_change",
|
||||||
models.BooleanField(
|
models.BooleanField(
|
||||||
@ -643,65 +478,6 @@ class Migration(migrations.Migration):
|
|||||||
default=30, help_text="Session timeout in minutes"
|
default=30, help_text="Session timeout in minutes"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"user_timezone",
|
|
||||||
models.CharField(
|
|
||||||
default="UTC", help_text="User timezone", max_length=50
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"language",
|
|
||||||
models.CharField(
|
|
||||||
default="en", help_text="Preferred language", max_length=10
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"theme",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("LIGHT", "Light"),
|
|
||||||
("DARK", "Dark"),
|
|
||||||
("AUTO", "Auto"),
|
|
||||||
],
|
|
||||||
default="LIGHT",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"profile_picture",
|
|
||||||
models.ImageField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Profile picture",
|
|
||||||
null=True,
|
|
||||||
upload_to="profile_pictures/",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"bio",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, help_text="Professional bio", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_verified",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False, help_text="User account is verified"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_approved",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False, help_text="User account is approved"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"approval_date",
|
|
||||||
models.DateTimeField(
|
|
||||||
blank=True, help_text="Account approval date", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
||||||
("updated_at", models.DateTimeField(auto_now=True)),
|
|
||||||
(
|
(
|
||||||
"last_password_change",
|
"last_password_change",
|
||||||
models.DateTimeField(
|
models.DateTimeField(
|
||||||
@ -709,17 +485,6 @@ class Migration(migrations.Migration):
|
|||||||
help_text="Last password change date",
|
help_text="Last password change date",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"approved_by",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text="User who approved this account",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="approved_users",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"groups",
|
"groups",
|
||||||
models.ManyToManyField(
|
models.ManyToManyField(
|
||||||
@ -733,8 +498,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"verbose_name": "User",
|
|
||||||
"verbose_name_plural": "Users",
|
|
||||||
"db_table": "accounts_user",
|
"db_table": "accounts_user",
|
||||||
"ordering": ["last_name", "first_name"],
|
"ordering": ["last_name", "first_name"],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
# Generated by Django 5.2.6 on 2025-09-15 14:05
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
name="tenant",
|
name="tenant",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
help_text="Organization tenant",
|
help_text="Organization tenant",
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
related_name="users",
|
related_name="users",
|
||||||
to="core.tenant",
|
to="core.tenant",
|
||||||
),
|
),
|
||||||
@ -77,25 +77,25 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="user",
|
model_name="user",
|
||||||
index=models.Index(
|
index=models.Index(
|
||||||
fields=["tenant", "role"], name="accounts_us_tenant__731b87_idx"
|
fields=["tenant", "email"], name="accounts_us_tenant__162cd2_idx"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="user",
|
model_name="user",
|
||||||
index=models.Index(
|
index=models.Index(
|
||||||
fields=["employee_id"], name="accounts_us_employe_0cbd94_idx"
|
fields=["tenant", "username"], name="accounts_us_tenant__d92906_idx"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="user",
|
model_name="user",
|
||||||
index=models.Index(
|
index=models.Index(
|
||||||
fields=["license_number"], name="accounts_us_license_02eb85_idx"
|
fields=["tenant", "user_id"], name="accounts_us_tenant__bd3758_idx"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="user",
|
model_name="user",
|
||||||
index=models.Index(
|
index=models.Index(
|
||||||
fields=["npi_number"], name="accounts_us_npi_num_800ef1_idx"
|
fields=["tenant", "is_active"], name="accounts_us_tenant__78e6c9_idx"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
|
|||||||
18
accounts/migrations/0003_alter_user_is_active.py
Normal file
18
accounts/migrations/0003_alter_user_is_active.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# 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"),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -4,6 +4,8 @@ Provides user management, authentication, and authorization functionality.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
@ -11,158 +13,379 @@ from django.utils import timezone
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
# class User(AbstractUser):
|
||||||
|
# """
|
||||||
|
# Extended user model for hospital management system.
|
||||||
|
# """
|
||||||
|
# ROLE_CHOICES = [
|
||||||
|
# ('SUPER_ADMIN', 'Super Administrator'),
|
||||||
|
# ('ADMIN', 'Administrator'),
|
||||||
|
# ('PHYSICIAN', 'Physician'),
|
||||||
|
# ('NURSE', 'Nurse'),
|
||||||
|
# ('NURSE_PRACTITIONER', 'Nurse Practitioner'),
|
||||||
|
# ('PHYSICIAN_ASSISTANT', 'Physician Assistant'),
|
||||||
|
# ('PHARMACIST', 'Pharmacist'),
|
||||||
|
# ('PHARMACY_TECH', 'Pharmacy Technician'),
|
||||||
|
# ('LAB_TECH', 'Laboratory Technician'),
|
||||||
|
# ('RADIOLOGIST', 'Radiologist'),
|
||||||
|
# ('RAD_TECH', 'Radiology Technician'),
|
||||||
|
# ('THERAPIST', 'Therapist'),
|
||||||
|
# ('SOCIAL_WORKER', 'Social Worker'),
|
||||||
|
# ('CASE_MANAGER', 'Case Manager'),
|
||||||
|
# ('BILLING_SPECIALIST', 'Billing Specialist'),
|
||||||
|
# ('REGISTRATION', 'Registration Staff'),
|
||||||
|
# ('SCHEDULER', 'Scheduler'),
|
||||||
|
# ('MEDICAL_ASSISTANT', 'Medical Assistant'),
|
||||||
|
# ('CLERICAL', 'Clerical Staff'),
|
||||||
|
# ('IT_SUPPORT', 'IT Support'),
|
||||||
|
# ('QUALITY_ASSURANCE', 'Quality Assurance'),
|
||||||
|
# ('COMPLIANCE', 'Compliance Officer'),
|
||||||
|
# ('SECURITY', 'Security'),
|
||||||
|
# ('MAINTENANCE', 'Maintenance'),
|
||||||
|
# ('VOLUNTEER', 'Volunteer'),
|
||||||
|
# ('STUDENT', 'Student'),
|
||||||
|
# ('RESEARCHER', 'Researcher'),
|
||||||
|
# ('CONSULTANT', 'Consultant'),
|
||||||
|
# ('VENDOR', 'Vendor'),
|
||||||
|
# ('GUEST', 'Guest'),
|
||||||
|
# ]
|
||||||
|
# THEME_CHOICES = [
|
||||||
|
# ('LIGHT', 'Light'),
|
||||||
|
# ('DARK', 'Dark'),
|
||||||
|
# ('AUTO', 'Auto'),
|
||||||
|
# ]
|
||||||
|
# # Basic Information
|
||||||
|
# user_id = models.UUIDField(
|
||||||
|
# default=uuid.uuid4,
|
||||||
|
# unique=True,
|
||||||
|
# editable=False,
|
||||||
|
# help_text='Unique user identifier'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Tenant relationship
|
||||||
|
# tenant = models.ForeignKey(
|
||||||
|
# 'core.Tenant',
|
||||||
|
# on_delete=models.CASCADE,
|
||||||
|
# related_name='users',
|
||||||
|
# help_text='Organization tenant'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Personal Information
|
||||||
|
# middle_name = models.CharField(
|
||||||
|
# max_length=150,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='Middle name'
|
||||||
|
# )
|
||||||
|
# preferred_name = models.CharField(
|
||||||
|
# max_length=150,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='Preferred name'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Contact Information
|
||||||
|
# phone_number = models.CharField(
|
||||||
|
# max_length=20,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# validators=[RegexValidator(
|
||||||
|
# regex=r'^\+?1?\d{9,15}$',
|
||||||
|
# message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
|
||||||
|
# )],
|
||||||
|
# help_text='Primary phone number'
|
||||||
|
# )
|
||||||
|
# mobile_number = models.CharField(
|
||||||
|
# max_length=20,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# validators=[RegexValidator(
|
||||||
|
# regex=r'^\+?1?\d{9,15}$',
|
||||||
|
# message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
|
||||||
|
# )],
|
||||||
|
# help_text='Mobile phone number'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Professional Information
|
||||||
|
# employee_id = models.CharField(
|
||||||
|
# max_length=50,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='Employee ID'
|
||||||
|
# )
|
||||||
|
# department = models.ForeignKey(
|
||||||
|
# 'hr.Department',
|
||||||
|
# on_delete=models.SET_NULL,
|
||||||
|
# null=True,
|
||||||
|
# blank=True,
|
||||||
|
# related_name='users',
|
||||||
|
# help_text='Department'
|
||||||
|
# )
|
||||||
|
# job_title = models.CharField(
|
||||||
|
# max_length=100,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='Job title'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Role and Permissions
|
||||||
|
# role = models.CharField(
|
||||||
|
# max_length=50,
|
||||||
|
# choices=ROLE_CHOICES,
|
||||||
|
# default='CLERICAL'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # License and Certification
|
||||||
|
# license_number = models.CharField(
|
||||||
|
# max_length=100,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='Professional license number'
|
||||||
|
# )
|
||||||
|
# license_state = models.CharField(
|
||||||
|
# max_length=50,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='License issuing state'
|
||||||
|
# )
|
||||||
|
# license_expiry = models.DateField(
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='License expiry date'
|
||||||
|
# )
|
||||||
|
# dea_number = models.CharField(
|
||||||
|
# max_length=20,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='DEA number for prescribing'
|
||||||
|
# )
|
||||||
|
# npi_number = models.CharField(
|
||||||
|
# max_length=10,
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='National Provider Identifier'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Security Settings
|
||||||
|
# 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'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Session Management
|
||||||
|
# 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'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Preferences
|
||||||
|
# user_timezone = models.CharField(
|
||||||
|
# max_length=50,
|
||||||
|
# default='Asia/Riyadh',
|
||||||
|
# help_text='User timezone'
|
||||||
|
# )
|
||||||
|
# language = models.CharField(
|
||||||
|
# max_length=10,
|
||||||
|
# default='en',
|
||||||
|
# help_text='Preferred language'
|
||||||
|
# )
|
||||||
|
# theme = models.CharField(
|
||||||
|
# max_length=20,
|
||||||
|
# choices=THEME_CHOICES,
|
||||||
|
# default='LIGHT'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Profile Information
|
||||||
|
# profile_picture = models.ImageField(
|
||||||
|
# upload_to='profile_pictures/',
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='Profile picture'
|
||||||
|
# )
|
||||||
|
# bio = models.TextField(
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='Professional bio'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Status
|
||||||
|
# is_verified = models.BooleanField(
|
||||||
|
# default=False,
|
||||||
|
# help_text='User account is verified'
|
||||||
|
# )
|
||||||
|
# is_approved = models.BooleanField(
|
||||||
|
# default=False,
|
||||||
|
# help_text='User account is approved'
|
||||||
|
# )
|
||||||
|
# approval_date = models.DateTimeField(
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# help_text='Account approval date'
|
||||||
|
# )
|
||||||
|
# approved_by = models.ForeignKey(
|
||||||
|
# 'self',
|
||||||
|
# on_delete=models.SET_NULL,
|
||||||
|
# null=True,
|
||||||
|
# blank=True,
|
||||||
|
# related_name='approved_users',
|
||||||
|
# help_text='User who approved this account'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Metadata
|
||||||
|
# created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
# updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
# last_password_change = models.DateTimeField(
|
||||||
|
# default=timezone.now,
|
||||||
|
# help_text='Last password change date'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# class Meta:
|
||||||
|
# db_table = 'accounts_user'
|
||||||
|
# verbose_name = 'User'
|
||||||
|
# verbose_name_plural = 'Users'
|
||||||
|
# ordering = ['last_name', 'first_name']
|
||||||
|
# indexes = [
|
||||||
|
# models.Index(fields=['tenant', 'role']),
|
||||||
|
# models.Index(fields=['employee_id']),
|
||||||
|
# models.Index(fields=['license_number']),
|
||||||
|
# models.Index(fields=['npi_number']),
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# def __str__(self):
|
||||||
|
# return f"{self.get_full_name()} ({self.username})"
|
||||||
|
#
|
||||||
|
# def get_full_name(self):
|
||||||
|
# """
|
||||||
|
# Return the full name for the user.
|
||||||
|
# """
|
||||||
|
# if self.preferred_name:
|
||||||
|
# return f"{self.preferred_name} {self.last_name}"
|
||||||
|
# return super().get_full_name()
|
||||||
|
#
|
||||||
|
# def get_display_name(self):
|
||||||
|
# """
|
||||||
|
# Return the display name for the user.
|
||||||
|
# """
|
||||||
|
# full_name = self.get_full_name()
|
||||||
|
# if full_name.strip():
|
||||||
|
# return full_name
|
||||||
|
# return self.username
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def is_account_locked(self):
|
||||||
|
# """
|
||||||
|
# Check if account is currently locked.
|
||||||
|
# """
|
||||||
|
# if self.locked_until:
|
||||||
|
# return timezone.now() < self.locked_until
|
||||||
|
# return False
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def is_password_expired(self):
|
||||||
|
# """
|
||||||
|
# Check if password has expired.
|
||||||
|
# """
|
||||||
|
# if self.password_expires_at:
|
||||||
|
# return timezone.now() > self.password_expires_at
|
||||||
|
# return False
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def is_license_expired(self):
|
||||||
|
# """
|
||||||
|
# Check if professional license has expired.
|
||||||
|
# """
|
||||||
|
# if self.license_expiry:
|
||||||
|
# return timezone.now().date() > self.license_expiry
|
||||||
|
# return False
|
||||||
|
#
|
||||||
|
# def lock_account(self, duration_minutes=15):
|
||||||
|
# """
|
||||||
|
# Lock the user account for specified duration.
|
||||||
|
# """
|
||||||
|
# self.locked_until = timezone.now() + timezone.timedelta(minutes=duration_minutes)
|
||||||
|
# self.save(update_fields=['locked_until'])
|
||||||
|
#
|
||||||
|
# def unlock_account(self):
|
||||||
|
# """
|
||||||
|
# Unlock the user account.
|
||||||
|
# """
|
||||||
|
# self.locked_until = None
|
||||||
|
# self.failed_login_attempts = 0
|
||||||
|
# self.save(update_fields=['locked_until', 'failed_login_attempts'])
|
||||||
|
#
|
||||||
|
# def increment_failed_login(self):
|
||||||
|
# """
|
||||||
|
# Increment failed login attempts and lock if threshold reached.
|
||||||
|
# """
|
||||||
|
# self.failed_login_attempts += 1
|
||||||
|
#
|
||||||
|
# # Lock account after 5 failed attempts
|
||||||
|
# max_attempts = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('MAX_LOGIN_ATTEMPTS', 5)
|
||||||
|
# if self.failed_login_attempts >= max_attempts:
|
||||||
|
# lockout_duration = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('LOCKOUT_DURATION_MINUTES', 15)
|
||||||
|
# self.lock_account(lockout_duration)
|
||||||
|
#
|
||||||
|
# self.save(update_fields=['failed_login_attempts'])
|
||||||
|
#
|
||||||
|
# def reset_failed_login(self):
|
||||||
|
# """
|
||||||
|
# Reset failed login attempts.
|
||||||
|
# """
|
||||||
|
# self.failed_login_attempts = 0
|
||||||
|
# self.save(update_fields=['failed_login_attempts'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
"""
|
"""
|
||||||
Extended user model for hospital management system.
|
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.
|
||||||
"""
|
"""
|
||||||
ROLE_CHOICES = [
|
|
||||||
('SUPER_ADMIN', 'Super Administrator'),
|
|
||||||
('ADMIN', 'Administrator'),
|
|
||||||
('PHYSICIAN', 'Physician'),
|
|
||||||
('NURSE', 'Nurse'),
|
|
||||||
('NURSE_PRACTITIONER', 'Nurse Practitioner'),
|
|
||||||
('PHYSICIAN_ASSISTANT', 'Physician Assistant'),
|
|
||||||
('PHARMACIST', 'Pharmacist'),
|
|
||||||
('PHARMACY_TECH', 'Pharmacy Technician'),
|
|
||||||
('LAB_TECH', 'Laboratory Technician'),
|
|
||||||
('RADIOLOGIST', 'Radiologist'),
|
|
||||||
('RAD_TECH', 'Radiology Technician'),
|
|
||||||
('THERAPIST', 'Therapist'),
|
|
||||||
('SOCIAL_WORKER', 'Social Worker'),
|
|
||||||
('CASE_MANAGER', 'Case Manager'),
|
|
||||||
('BILLING_SPECIALIST', 'Billing Specialist'),
|
|
||||||
('REGISTRATION', 'Registration Staff'),
|
|
||||||
('SCHEDULER', 'Scheduler'),
|
|
||||||
('MEDICAL_ASSISTANT', 'Medical Assistant'),
|
|
||||||
('CLERICAL', 'Clerical Staff'),
|
|
||||||
('IT_SUPPORT', 'IT Support'),
|
|
||||||
('QUALITY_ASSURANCE', 'Quality Assurance'),
|
|
||||||
('COMPLIANCE', 'Compliance Officer'),
|
|
||||||
('SECURITY', 'Security'),
|
|
||||||
('MAINTENANCE', 'Maintenance'),
|
|
||||||
('VOLUNTEER', 'Volunteer'),
|
|
||||||
('STUDENT', 'Student'),
|
|
||||||
('RESEARCHER', 'Researcher'),
|
|
||||||
('CONSULTANT', 'Consultant'),
|
|
||||||
('VENDOR', 'Vendor'),
|
|
||||||
('GUEST', 'Guest'),
|
|
||||||
]
|
|
||||||
THEME_CHOICES = [
|
|
||||||
('LIGHT', 'Light'),
|
|
||||||
('DARK', 'Dark'),
|
|
||||||
('AUTO', 'Auto'),
|
|
||||||
]
|
|
||||||
# Basic Information
|
|
||||||
user_id = models.UUIDField(
|
|
||||||
default=uuid.uuid4,
|
|
||||||
unique=True,
|
|
||||||
editable=False,
|
|
||||||
help_text='Unique user identifier'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Tenant relationship
|
# Stable internal UUID
|
||||||
|
user_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||||||
|
|
||||||
|
# Tenant (PROTECT = safer than cascading deletion)
|
||||||
tenant = models.ForeignKey(
|
tenant = models.ForeignKey(
|
||||||
'core.Tenant',
|
'core.Tenant',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.PROTECT,
|
||||||
related_name='users',
|
related_name='users',
|
||||||
help_text='Organization tenant'
|
help_text='Organization tenant',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Personal Information
|
username = models.CharField(
|
||||||
middle_name = models.CharField(
|
|
||||||
max_length=150,
|
max_length=150,
|
||||||
blank=True,
|
unique=True,
|
||||||
null=True,
|
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'
|
||||||
help_text='Middle name'
|
|
||||||
)
|
|
||||||
preferred_name = models.CharField(
|
|
||||||
max_length=150,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='Preferred name'
|
|
||||||
)
|
)
|
||||||
|
email = models.EmailField(blank=True, null=True)
|
||||||
|
|
||||||
# Contact Information
|
# --- Security & session controls kept on User (auth-level concerns) ---
|
||||||
phone_number = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[RegexValidator(
|
|
||||||
regex=r'^\+?1?\d{9,15}$',
|
|
||||||
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
|
|
||||||
)],
|
|
||||||
help_text='Primary phone number'
|
|
||||||
)
|
|
||||||
mobile_number = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
validators=[RegexValidator(
|
|
||||||
regex=r'^\+?1?\d{9,15}$',
|
|
||||||
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
|
|
||||||
)],
|
|
||||||
help_text='Mobile phone number'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Professional Information
|
|
||||||
employee_id = models.CharField(
|
|
||||||
max_length=50,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='Employee ID'
|
|
||||||
)
|
|
||||||
department = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='Department'
|
|
||||||
)
|
|
||||||
job_title = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='Job title'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Role and Permissions
|
|
||||||
role = models.CharField(
|
|
||||||
max_length=50,
|
|
||||||
choices=ROLE_CHOICES,
|
|
||||||
default='CLERICAL'
|
|
||||||
)
|
|
||||||
|
|
||||||
# License and Certification
|
|
||||||
license_number = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='Professional license number'
|
|
||||||
)
|
|
||||||
license_state = models.CharField(
|
|
||||||
max_length=50,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='License issuing state'
|
|
||||||
)
|
|
||||||
license_expiry = models.DateField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='License expiry date'
|
|
||||||
)
|
|
||||||
dea_number = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='DEA number for prescribing'
|
|
||||||
)
|
|
||||||
npi_number = models.CharField(
|
|
||||||
max_length=10,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='National Provider Identifier'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Security Settings
|
|
||||||
force_password_change = models.BooleanField(
|
force_password_change = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text='User must change password on next login'
|
help_text='User must change password on next login'
|
||||||
@ -185,8 +408,6 @@ class User(AbstractUser):
|
|||||||
default=False,
|
default=False,
|
||||||
help_text='Two-factor authentication enabled'
|
help_text='Two-factor authentication enabled'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Session Management
|
|
||||||
max_concurrent_sessions = models.PositiveIntegerField(
|
max_concurrent_sessions = models.PositiveIntegerField(
|
||||||
default=3,
|
default=3,
|
||||||
help_text='Maximum concurrent sessions allowed'
|
help_text='Maximum concurrent sessions allowed'
|
||||||
@ -195,160 +416,64 @@ class User(AbstractUser):
|
|||||||
default=30,
|
default=30,
|
||||||
help_text='Session timeout in minutes'
|
help_text='Session timeout in minutes'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Preferences
|
|
||||||
user_timezone = models.CharField(
|
|
||||||
max_length=50,
|
|
||||||
default='UTC',
|
|
||||||
help_text='User timezone'
|
|
||||||
)
|
|
||||||
language = models.CharField(
|
|
||||||
max_length=10,
|
|
||||||
default='en',
|
|
||||||
help_text='Preferred language'
|
|
||||||
)
|
|
||||||
theme = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
choices=THEME_CHOICES,
|
|
||||||
default='LIGHT'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Profile Information
|
|
||||||
profile_picture = models.ImageField(
|
|
||||||
upload_to='profile_pictures/',
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='Profile picture'
|
|
||||||
)
|
|
||||||
bio = models.TextField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='Professional bio'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Status
|
|
||||||
is_verified = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text='User account is verified'
|
|
||||||
)
|
|
||||||
is_approved = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text='User account is approved'
|
|
||||||
)
|
|
||||||
approval_date = models.DateTimeField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
help_text='Account approval date'
|
|
||||||
)
|
|
||||||
approved_by = models.ForeignKey(
|
|
||||||
'self',
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name='approved_users',
|
|
||||||
help_text='User who approved this account'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Metadata
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
last_password_change = models.DateTimeField(
|
last_password_change = models.DateTimeField(
|
||||||
default=timezone.now,
|
default=timezone.now,
|
||||||
help_text='Last password change date'
|
help_text='Last password change date'
|
||||||
)
|
)
|
||||||
|
is_active = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text='User account is active'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'accounts_user'
|
db_table = 'accounts_user'
|
||||||
verbose_name = 'User'
|
|
||||||
verbose_name_plural = 'Users'
|
|
||||||
ordering = ['last_name', 'first_name']
|
ordering = ['last_name', 'first_name']
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['tenant', 'role']),
|
models.Index(fields=['tenant', 'email']),
|
||||||
models.Index(fields=['employee_id']),
|
models.Index(fields=['tenant', 'username']),
|
||||||
models.Index(fields=['license_number']),
|
models.Index(fields=['tenant', 'user_id']),
|
||||||
models.Index(fields=['npi_number']),
|
models.Index(fields=['tenant', 'is_active']),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.get_full_name()} ({self.username})"
|
full = super().get_full_name().strip()
|
||||||
|
return f"{full or self.username} (tenant={self.tenant_id})"
|
||||||
|
|
||||||
def get_full_name(self):
|
# ---- Security helpers ----
|
||||||
"""
|
@property
|
||||||
Return the full name for the user.
|
def is_account_locked(self) -> bool:
|
||||||
"""
|
return bool(self.locked_until and timezone.now() < self.locked_until)
|
||||||
if self.preferred_name:
|
|
||||||
return f"{self.preferred_name} {self.last_name}"
|
|
||||||
return super().get_full_name()
|
|
||||||
|
|
||||||
def get_display_name(self):
|
|
||||||
"""
|
|
||||||
Return the display name for the user.
|
|
||||||
"""
|
|
||||||
full_name = self.get_full_name()
|
|
||||||
if full_name.strip():
|
|
||||||
return full_name
|
|
||||||
return self.username
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_account_locked(self):
|
def is_password_expired(self) -> bool:
|
||||||
"""
|
return bool(self.password_expires_at and timezone.now() > self.password_expires_at)
|
||||||
Check if account is currently locked.
|
|
||||||
"""
|
|
||||||
if self.locked_until:
|
|
||||||
return timezone.now() < self.locked_until
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
def lock_account(self, duration_minutes: int = 15):
|
||||||
def is_password_expired(self):
|
self.locked_until = timezone.now() + timedelta(minutes=duration_minutes)
|
||||||
"""
|
|
||||||
Check if password has expired.
|
|
||||||
"""
|
|
||||||
if self.password_expires_at:
|
|
||||||
return timezone.now() > self.password_expires_at
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_license_expired(self):
|
|
||||||
"""
|
|
||||||
Check if professional license has expired.
|
|
||||||
"""
|
|
||||||
if self.license_expiry:
|
|
||||||
return timezone.now().date() > self.license_expiry
|
|
||||||
return False
|
|
||||||
|
|
||||||
def lock_account(self, duration_minutes=15):
|
|
||||||
"""
|
|
||||||
Lock the user account for specified duration.
|
|
||||||
"""
|
|
||||||
self.locked_until = timezone.now() + timezone.timedelta(minutes=duration_minutes)
|
|
||||||
self.save(update_fields=['locked_until'])
|
self.save(update_fields=['locked_until'])
|
||||||
|
|
||||||
def unlock_account(self):
|
def unlock_account(self):
|
||||||
"""
|
|
||||||
Unlock the user account.
|
|
||||||
"""
|
|
||||||
self.locked_until = None
|
self.locked_until = None
|
||||||
self.failed_login_attempts = 0
|
self.failed_login_attempts = 0
|
||||||
self.save(update_fields=['locked_until', 'failed_login_attempts'])
|
self.save(update_fields=['locked_until', 'failed_login_attempts'])
|
||||||
|
|
||||||
def increment_failed_login(self):
|
def increment_failed_login(self, *, max_attempts: int | None = None, lockout_minutes: int | None = None):
|
||||||
"""
|
"""
|
||||||
Increment failed login attempts and lock if threshold reached.
|
Increment failed login attempts and lock the account if threshold reached.
|
||||||
|
Defaults pulled from settings.HOSPITAL_SETTINGS if provided, else (5, 15).
|
||||||
"""
|
"""
|
||||||
self.failed_login_attempts += 1
|
self.failed_login_attempts += 1
|
||||||
|
if max_attempts is None:
|
||||||
# Lock account after 5 failed attempts
|
|
||||||
max_attempts = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('MAX_LOGIN_ATTEMPTS', 5)
|
max_attempts = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('MAX_LOGIN_ATTEMPTS', 5)
|
||||||
if self.failed_login_attempts >= max_attempts:
|
if lockout_minutes is None:
|
||||||
lockout_duration = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('LOCKOUT_DURATION_MINUTES', 15)
|
lockout_minutes = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('LOCKOUT_DURATION_MINUTES', 15)
|
||||||
self.lock_account(lockout_duration)
|
|
||||||
|
|
||||||
|
if self.failed_login_attempts >= max_attempts:
|
||||||
|
self.lock_account(lockout_minutes)
|
||||||
|
else:
|
||||||
self.save(update_fields=['failed_login_attempts'])
|
self.save(update_fields=['failed_login_attempts'])
|
||||||
|
|
||||||
def reset_failed_login(self):
|
def reset_failed_login(self):
|
||||||
"""
|
|
||||||
Reset failed login attempts.
|
|
||||||
"""
|
|
||||||
self.failed_login_attempts = 0
|
self.failed_login_attempts = 0
|
||||||
self.save(update_fields=['failed_login_attempts'])
|
self.save(update_fields=['failed_login_attempts'])
|
||||||
|
|
||||||
|
|||||||
41
accounts/signals.py
Normal file
41
accounts/signals.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# accounts/signals.py
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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}"
|
||||||
|
|
||||||
|
# Create with basic info mirrored from User
|
||||||
|
Employee.objects.create(
|
||||||
|
tenant=instance.tenant,
|
||||||
|
user=instance,
|
||||||
|
employee_number=emp_no,
|
||||||
|
first_name=instance.first_name or '',
|
||||||
|
last_name=instance.last_name or '',
|
||||||
|
email=instance.email
|
||||||
|
)
|
||||||
|
|
||||||
|
# Defer until after outer transaction commits (avoids race conditions in tests/views)
|
||||||
|
transaction.on_commit(_make_employee)
|
||||||
@ -124,8 +124,8 @@
|
|||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div class="avatar-upload">
|
<div class="avatar-upload">
|
||||||
{% if user.profile_picture %}
|
{% if user.employee_profile.profile_picture %}
|
||||||
<img src="{{ user.profile_picture.url }}" alt="{{ user.get_full_name }}" class="profile-avatar">
|
<img src="{{ user.employee_profile.profile_picture.url }}" alt="{{ user.employee_profile.get_full_name }}" class="profile-avatar">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="profile-avatar bg-secondary d-flex align-items-center justify-content-center">
|
<div class="profile-avatar bg-secondary d-flex align-items-center justify-content-center">
|
||||||
<i class="fas fa-user fa-2x text-white"></i>
|
<i class="fas fa-user fa-2x text-white"></i>
|
||||||
@ -138,8 +138,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h1 class="mb-2">{{ user.get_full_name }}</h1>
|
<h1 class="mb-2">{{ user.employee_profile.get_full_name }}</h1>
|
||||||
<p class="mb-0 opacity-75">{{ user.role|title }} • {{ user.department|default:"No Department" }}</p>
|
<p class="mb-0 opacity-75">{{ user.employee_profile.get_role_display }} • {{ user.employee_profile.department|default:"No Department" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -167,14 +167,14 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="first_name" class="form-label">First Name</label>
|
<label for="first_name" class="form-label">First Name</label>
|
||||||
<input type="text" class="form-control" id="first_name" name="first_name"
|
<input type="text" class="form-control" id="first_name" name="first_name"
|
||||||
value="{{ user.first_name }}" required>
|
value="{{ user.employee_profile.first_name }}" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="last_name" class="form-label">Last Name</label>
|
<label for="last_name" class="form-label">Last Name</label>
|
||||||
<input type="text" class="form-control" id="last_name" name="last_name"
|
<input type="text" class="form-control" id="last_name" name="last_name"
|
||||||
value="{{ user.last_name }}" required>
|
value="{{ user.employee_profile.last_name }}" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -184,14 +184,14 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">Email Address</label>
|
<label for="email" class="form-label">Email Address</label>
|
||||||
<input type="email" class="form-control" id="email" name="email"
|
<input type="email" class="form-control" id="email" name="email"
|
||||||
value="{{ user.email }}" required>
|
value="{{ user.employee_profile.email }}" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="phone_number" class="form-label">Phone Number</label>
|
<label for="phone" class="form-label">Phone Number</label>
|
||||||
<input type="tel" class="form-control" id="phone_number" name="phone_number"
|
<input type="tel" class="form-control" id="phone" name="phone"
|
||||||
value="{{ user.phone_number }}">
|
value="{{ user.employee_profile.phone }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -199,9 +199,9 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="mobile_number" class="form-label">Mobile Number</label>
|
<label for="mobile_phone" class="form-label">Mobile Number</label>
|
||||||
<input type="tel" class="form-control" id="mobile_number" name="mobile_number"
|
<input type="tel" class="form-control" id="mobile_phone" name="mobile_phone"
|
||||||
value="{{ user.mobile_number }}">
|
value="{{ user.employee_profile.mobile_phone }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -209,7 +209,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="bio" class="form-label">Bio</label>
|
<label for="bio" class="form-label">Bio</label>
|
||||||
<textarea class="form-control" id="bio" name="bio" rows="3"
|
<textarea class="form-control" id="bio" name="bio" rows="3"
|
||||||
placeholder="Tell us about yourself...">{{ user.bio }}</textarea>
|
placeholder="Tell us about yourself...">{{ user.employee_profile.bio }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -219,29 +219,29 @@
|
|||||||
<div class="preference-item">
|
<div class="preference-item">
|
||||||
<label for="timezone" class="form-label">Timezone</label>
|
<label for="timezone" class="form-label">Timezone</label>
|
||||||
<select class="form-select" id="timezone" name="timezone">
|
<select class="form-select" id="timezone" name="timezone">
|
||||||
<option value="UTC" {% if user.timezone == 'UTC' %}selected{% endif %}>UTC</option>
|
<option value="Asia/Riyadh" {% if user.employee_profile.timezone == 'Asia/Riyadh' %}selected{% endif %}>Asia/Riyadh</option>
|
||||||
<option value="America/New_York" {% if user.timezone == 'America/New_York' %}selected{% endif %}>Eastern Time</option>
|
<option value="America/New_York" {% if user.employee_profile.timezone == 'America/New_York' %}selected{% endif %}>Eastern Time</option>
|
||||||
<option value="America/Chicago" {% if user.timezone == 'America/Chicago' %}selected{% endif %}>Central Time</option>
|
<option value="America/Chicago" {% if user.employee_profile.timezone == 'America/Chicago' %}selected{% endif %}>Central Time</option>
|
||||||
<option value="America/Denver" {% if user.timezone == 'America/Denver' %}selected{% endif %}>Mountain Time</option>
|
<option value="America/Denver" {% if user.employee_profile.timezone == 'America/Denver' %}selected{% endif %}>Mountain Time</option>
|
||||||
<option value="America/Los_Angeles" {% if user.timezone == 'America/Los_Angeles' %}selected{% endif %}>Pacific Time</option>
|
<option value="America/Los_Angeles" {% if user.employee_profile.timezone == 'America/Los_Angeles' %}selected{% endif %}>Pacific Time</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="preference-item">
|
<div class="preference-item">
|
||||||
<label for="language" class="form-label">Language</label>
|
<label for="language" class="form-label">Language</label>
|
||||||
<select class="form-select" id="language" name="language">
|
<select class="form-select" id="language" name="language">
|
||||||
<option value="en" {% if user.language == 'en' %}selected{% endif %}>English</option>
|
<option value="en" {% if user.employee_profile.language == 'en' %}selected{% endif %}>English</option>
|
||||||
<option value="es" {% if user.language == 'es' %}selected{% endif %}>Spanish</option>
|
<option value="ar" {% if user.employee_profile.language == 'ar' %}selected{% endif %}>Arabic</option>
|
||||||
<option value="fr" {% if user.language == 'fr' %}selected{% endif %}>French</option>
|
<option value="fr" {% if user.employee_profile.language == 'fr' %}selected{% endif %}>French</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="preference-item">
|
<div class="preference-item">
|
||||||
<label for="theme" class="form-label">Theme</label>
|
<label for="theme" class="form-label">Theme</label>
|
||||||
<select class="form-select" id="theme" name="theme">
|
<select class="form-select" id="theme" name="theme">
|
||||||
<option value="light" {% if user.theme == 'light' %}selected{% endif %}>Light</option>
|
<option value="light" {% if user.employee_profile.theme == 'light' %}selected{% endif %}>Light</option>
|
||||||
<option value="dark" {% if user.theme == 'dark' %}selected{% endif %}>Dark</option>
|
<option value="dark" {% if user.employee_profile.theme == 'dark' %}selected{% endif %}>Dark</option>
|
||||||
<option value="auto" {% if user.theme == 'auto' %}selected{% endif %}>Auto</option>
|
<option value="auto" {% if user.employee_profile.theme == 'auto' %}selected{% endif %}>Auto</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,6 +18,8 @@ from django.utils import timezone
|
|||||||
from django.urls import reverse_lazy, reverse
|
from django.urls import reverse_lazy, reverse
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from hr.models import Employee
|
||||||
from .models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
|
from .models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
|
||||||
from .forms import (
|
from .forms import (
|
||||||
UserForm, UserCreateForm, TwoFactorDeviceForm, SocialAccountForm,
|
UserForm, UserCreateForm, TwoFactorDeviceForm, SocialAccountForm,
|
||||||
@ -26,17 +28,6 @@ from .forms import (
|
|||||||
from core.utils import AuditLogger
|
from core.utils import AuditLogger
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# USER VIEWS (FULL CRUD - Master Data)
|
|
||||||
# ============================================================================ession, PasswordHistory
|
|
||||||
from .forms import (
|
|
||||||
UserForm, UserCreateForm, TwoFactorDeviceForm, SocialAccountForm,
|
|
||||||
AccountsSearchForm, PasswordChangeForm
|
|
||||||
)
|
|
||||||
from core.utils import AuditLogger
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# USER VIEWS (FULL CRUD - Master Data)
|
# USER VIEWS (FULL CRUD - Master Data)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@ -1024,22 +1015,22 @@ def user_profile_update(request):
|
|||||||
HTMX view for user profile update.
|
HTMX view for user profile update.
|
||||||
"""
|
"""
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
user = request.user
|
employee = Employee.objects.get(user=request.user)
|
||||||
|
|
||||||
# Update basic information
|
# Update basic information
|
||||||
user.first_name = request.POST.get('first_name', user.first_name)
|
employee.first_name = request.POST.get('first_name', employee.first_name)
|
||||||
user.last_name = request.POST.get('last_name', user.last_name)
|
employee.last_name = request.POST.get('last_name', employee.last_name)
|
||||||
user.email = request.POST.get('email', user.email)
|
employee.email = request.POST.get('email', employee.email)
|
||||||
user.phone_number = request.POST.get('phone_number', user.phone_number)
|
employee.phone = request.POST.get('phone', employee.phone)
|
||||||
user.mobile_number = request.POST.get('mobile_number', user.mobile_number)
|
employee.mobile_phone = request.POST.get('mobile_phone', employee.mobile_phone)
|
||||||
user.bio = request.POST.get('bio', user.bio)
|
employee.bio = request.POST.get('bio', employee.bio)
|
||||||
|
|
||||||
# Update preferences
|
# Update preferences
|
||||||
user.timezone = request.POST.get('timezone', user.timezone)
|
employee.user_timezone = request.POST.get('timezone', employee.user_timezone)
|
||||||
user.language = request.POST.get('language', user.language)
|
employee.language = request.POST.get('language', employee.language)
|
||||||
user.theme = request.POST.get('theme', user.theme)
|
employee.theme = request.POST.get('theme', employee.theme)
|
||||||
|
|
||||||
user.save()
|
employee.save()
|
||||||
|
|
||||||
# Log the update
|
# Log the update
|
||||||
AuditLogger.log_event(
|
AuditLogger.log_event(
|
||||||
@ -1047,14 +1038,15 @@ def user_profile_update(request):
|
|||||||
event_type='UPDATE',
|
event_type='UPDATE',
|
||||||
event_category='DATA_MODIFICATION',
|
event_category='DATA_MODIFICATION',
|
||||||
action='Update User Profile',
|
action='Update User Profile',
|
||||||
description=f'User updated their profile: {user.username}',
|
description=f'User updated their profile: {employee.user.username}',
|
||||||
user=request.user,
|
user=request.user,
|
||||||
content_object=user,
|
content_object=employee,
|
||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
|
|
||||||
messages.success(request, 'Profile updated successfully.')
|
messages.success(request, 'Profile updated successfully.')
|
||||||
return JsonResponse({'status': 'success'})
|
return redirect('accounts:user_profile')
|
||||||
|
|
||||||
|
|
||||||
return JsonResponse({'error': 'Invalid request'}, status=400)
|
return JsonResponse({'error': 'Invalid request'}, status=400)
|
||||||
|
|
||||||
|
|||||||
350
accounts_data.py
350
accounts_data.py
@ -1,3 +1,5 @@
|
|||||||
|
# scripts/seed_saudi_accounts.py
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import django
|
import django
|
||||||
|
|
||||||
@ -5,56 +7,59 @@ import django
|
|||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
from django.contrib.auth.hashers import make_password
|
|
||||||
from django.utils import timezone as django_timezone
|
|
||||||
from accounts.models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
|
|
||||||
from core.models import Tenant
|
|
||||||
import uuid
|
import uuid
|
||||||
import secrets
|
import secrets
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.utils import timezone as django_timezone
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from accounts.models import User
|
||||||
|
from core.models import Tenant
|
||||||
|
from hr.models import Employee
|
||||||
|
from hr.models import Department # make sure hr.Department exists
|
||||||
|
|
||||||
|
# If these exist in your project, keep them. If not, comment them out.
|
||||||
|
from accounts.models import TwoFactorDevice, SocialAccount, UserSession, PasswordHistory # noqa
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
# Saudi-specific data constants
|
# Saudi-specific data constants
|
||||||
|
# -------------------------------
|
||||||
SAUDI_FIRST_NAMES_MALE = [
|
SAUDI_FIRST_NAMES_MALE = [
|
||||||
'Mohammed', 'Abdullah', 'Ahmed', 'Omar', 'Ali', 'Hassan', 'Khalid', 'Faisal',
|
'Mohammed', 'Abdullah', 'Ahmed', 'Omar', 'Ali', 'Hassan', 'Khalid', 'Faisal',
|
||||||
'Saad', 'Fahd', 'Bandar', 'Turki', 'Nasser', 'Saud', 'Abdulrahman',
|
'Saad', 'Fahd', 'Bandar', 'Turki', 'Nasser', 'Saud', 'Abdulrahman',
|
||||||
'Abdulaziz', 'Salman', 'Waleed', 'Majid', 'Rayan', 'Yazeed', 'Mansour',
|
'Abdulaziz', 'Salman', 'Waleed', 'Majid', 'Rayan', 'Yazeed', 'Mansour',
|
||||||
'Osama', 'Tariq', 'Adel', 'Nawaf', 'Sultan', 'Mishaal', 'Badr', 'Ziad'
|
'Osama', 'Tariq', 'Adel', 'Nawaf', 'Sultan', 'Mishaal', 'Badr', 'Ziad'
|
||||||
]
|
]
|
||||||
|
|
||||||
SAUDI_FIRST_NAMES_FEMALE = [
|
SAUDI_FIRST_NAMES_FEMALE = [
|
||||||
'Fatima', 'Aisha', 'Maryam', 'Khadija', 'Sarah', 'Noura', 'Hala', 'Reem',
|
'Fatima', 'Aisha', 'Maryam', 'Khadija', 'Sarah', 'Noura', 'Hala', 'Reem',
|
||||||
'Lina', 'Dana', 'Rana', 'Nada', 'Layla', 'Amira', 'Zahra', 'Yasmin',
|
'Lina', 'Dana', 'Rana', 'Nada', 'Layla', 'Amira', 'Zahra', 'Yasmin',
|
||||||
'Dina', 'Noor', 'Rahma', 'Salma', 'Lama', 'Ghada', 'Rania', 'Maha',
|
'Dina', 'Noor', 'Rahma', 'Salma', 'Lama', 'Ghada', 'Rania', 'Maha',
|
||||||
'Wedad', 'Najla', 'Shahd', 'Jood', 'Rand', 'Malak'
|
'Wedad', 'Najla', 'Shahd', 'Jood', 'Rand', 'Malak'
|
||||||
]
|
]
|
||||||
|
|
||||||
SAUDI_FAMILY_NAMES = [
|
SAUDI_FAMILY_NAMES = [
|
||||||
'Al-Rashid', 'Al-Harbi', 'Al-Qahtani', 'Al-Dosari', 'Al-Otaibi', 'Al-Mutairi',
|
'Al-Rashid', 'Al-Harbi', 'Al-Qahtani', 'Al-Dosari', 'Al-Otaibi', 'Al-Mutairi',
|
||||||
'Al-Shammari', 'Al-Zahrani', 'Al-Ghamdi', 'Al-Maliki', 'Al-Subai', 'Al-Jubayr',
|
'Al-Shammari', 'Al-Zahrani', 'Al-Ghamdi', 'Al-Maliki', 'Al-Subai', 'Al-Jubayr',
|
||||||
'Al-Faisal', 'Al-Saud', 'Al-Thani', 'Al-Maktoum', 'Al-Sabah', 'Al-Khalifa',
|
'Al-Faisal', 'Al-Saud', 'Al-Shaalan', 'Al-Rajhi', 'Al-Sudairy', 'Al-Ajmi',
|
||||||
'Bin-Laden', 'Al-Rajhi', 'Al-Sudairy', 'Al-Shaalan', 'Al-Kabeer', 'Al-Ajmi',
|
|
||||||
'Al-Anzi', 'Al-Dawsari', 'Al-Shamrani', 'Al-Balawi', 'Al-Juhani', 'Al-Sulami'
|
'Al-Anzi', 'Al-Dawsari', 'Al-Shamrani', 'Al-Balawi', 'Al-Juhani', 'Al-Sulami'
|
||||||
]
|
]
|
||||||
|
|
||||||
SAUDI_MIDDLE_NAMES = [
|
|
||||||
'bin Ahmed', 'bin Mohammed', 'bin Abdullah', 'bin Omar', 'bin Ali', 'bin Hassan',
|
|
||||||
'bin Khalid', 'bin Faisal', 'bin Saad', 'bin Fahd', 'bin Abdulaziz', 'bin Salman'
|
|
||||||
]
|
|
||||||
|
|
||||||
SAUDI_CITIES = [
|
SAUDI_CITIES = [
|
||||||
'Riyadh', 'Jeddah', 'Mecca', 'Medina', 'Dammam', 'Khobar', 'Dhahran',
|
'Riyadh', 'Jeddah', 'Mecca', 'Medina', 'Dammam', 'Khobar', 'Dhahran',
|
||||||
'Taif', 'Tabuk', 'Buraidah', 'Khamis Mushait', 'Hofuf', 'Mubarraz',
|
'Taif', 'Tabuk', 'Buraidah', 'Khamis Mushait', 'Hofuf', 'Mubarraz',
|
||||||
'Jubail', 'Yanbu', 'Abha', 'Najran', 'Jazan', 'Hail', 'Arar'
|
'Jubail', 'Yanbu', 'Abha', 'Najran', 'Jazan', 'Hail', 'Arar'
|
||||||
]
|
]
|
||||||
|
|
||||||
SAUDI_PROVINCES = [
|
SAUDI_PROVINCES = [
|
||||||
'Riyadh Province', 'Makkah Province', 'Eastern Province', 'Asir Province',
|
'Riyadh Province', 'Makkah Province', 'Eastern Province', 'Asir Province',
|
||||||
'Jazan Province', 'Medina Province', 'Qassim Province', 'Tabuk Province',
|
'Jazan Province', 'Medina Province', 'Qassim Province', 'Tabuk Province',
|
||||||
'Hail Province', 'Northern Borders Province', 'Najran Province', 'Al Bahah Province'
|
'Hail Province', 'Northern Borders Province', 'Najran Province', 'Al Bahah Province'
|
||||||
]
|
]
|
||||||
|
SAUDI_DEPARTMENTS = [
|
||||||
|
'Internal Medicine', 'Cardiology', 'Orthopedics', 'Neurology', 'Oncology',
|
||||||
|
'Pediatrics', 'Emergency Medicine', 'Radiology', 'Laboratory Medicine',
|
||||||
|
'Pharmacy', 'Surgery', 'Obstetrics and Gynecology', 'Dermatology',
|
||||||
|
'Ophthalmology', 'ENT', 'Anesthesiology', 'Pathology', 'Psychiatry'
|
||||||
|
]
|
||||||
SAUDI_JOB_TITLES = {
|
SAUDI_JOB_TITLES = {
|
||||||
'PHYSICIAN': ['Consultant Physician', 'Senior Physician', 'Staff Physician', 'Resident Physician',
|
'PHYSICIAN': ['Consultant Physician', 'Senior Physician', 'Staff Physician', 'Resident Physician',
|
||||||
'Chief Medical Officer'],
|
'Chief Medical Officer'],
|
||||||
@ -63,50 +68,13 @@ SAUDI_JOB_TITLES = {
|
|||||||
'ADMIN': ['Medical Director', 'Hospital Administrator', 'Department Manager', 'Operations Manager'],
|
'ADMIN': ['Medical Director', 'Hospital Administrator', 'Department Manager', 'Operations Manager'],
|
||||||
'LAB_TECH': ['Senior Lab Technician', 'Medical Laboratory Scientist', 'Lab Supervisor'],
|
'LAB_TECH': ['Senior Lab Technician', 'Medical Laboratory Scientist', 'Lab Supervisor'],
|
||||||
'RAD_TECH': ['Senior Radiologic Technologist', 'CT Technologist', 'MRI Technologist'],
|
'RAD_TECH': ['Senior Radiologic Technologist', 'CT Technologist', 'MRI Technologist'],
|
||||||
'RADIOLOGIST': ['Consultant Radiologist', 'Senior Radiologist', 'Interventional Radiologist']
|
'RADIOLOGIST': ['Consultant Radiologist', 'Senior Radiologist', 'Interventional Radiologist'],
|
||||||
|
'MEDICAL_ASSISTANT': ['Medical Assistant'],
|
||||||
|
'CLERICAL': ['Clerical Staff'],
|
||||||
}
|
}
|
||||||
|
|
||||||
SAUDI_DEPARTMENTS = [
|
|
||||||
'Internal Medicine', 'Cardiology', 'Orthopedics', 'Neurology', 'Oncology',
|
|
||||||
'Pediatrics', 'Emergency Medicine', 'Radiology', 'Laboratory Medicine',
|
|
||||||
'Pharmacy', 'Surgery', 'Obstetrics and Gynecology', 'Dermatology',
|
|
||||||
'Ophthalmology', 'ENT', 'Anesthesiology', 'Pathology', 'Psychiatry'
|
|
||||||
]
|
|
||||||
|
|
||||||
# Saudi Medical License Formats
|
|
||||||
SAUDI_LICENSE_PREFIXES = ['MOH', 'SCFHS', 'SMLE', 'SFH']
|
SAUDI_LICENSE_PREFIXES = ['MOH', 'SCFHS', 'SMLE', 'SFH']
|
||||||
|
|
||||||
|
ROLE_DISTRIBUTION = {
|
||||||
def generate_saudi_phone():
|
|
||||||
"""Generate Saudi phone number"""
|
|
||||||
area_codes = ['11', '12', '13', '14', '16', '17'] # Major Saudi area codes
|
|
||||||
return f"+966-{random.choice(area_codes)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}"
|
|
||||||
|
|
||||||
|
|
||||||
def generate_saudi_mobile():
|
|
||||||
"""Generate Saudi mobile number"""
|
|
||||||
mobile_prefixes = ['50', '53', '54', '55', '56', '57', '58', '59'] # Saudi mobile prefixes
|
|
||||||
return f"+966-{random.choice(mobile_prefixes)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}"
|
|
||||||
|
|
||||||
|
|
||||||
def generate_saudi_license():
|
|
||||||
"""Generate Saudi medical license number"""
|
|
||||||
prefix = random.choice(SAUDI_LICENSE_PREFIXES)
|
|
||||||
return f"{prefix}-{random.randint(100000, 999999)}"
|
|
||||||
|
|
||||||
|
|
||||||
def generate_saudi_employee_id(tenant_name, role):
|
|
||||||
"""Generate Saudi employee ID"""
|
|
||||||
tenant_code = ''.join([c for c in tenant_name.upper() if c.isalpha()])[:3]
|
|
||||||
role_code = role[:3].upper()
|
|
||||||
return f"{tenant_code}-{role_code}-{random.randint(1000, 9999)}"
|
|
||||||
|
|
||||||
|
|
||||||
def create_saudi_users(tenants, users_per_tenant=50):
|
|
||||||
"""Create Saudi healthcare users"""
|
|
||||||
users = []
|
|
||||||
|
|
||||||
role_distribution = {
|
|
||||||
'PHYSICIAN': 0.15,
|
'PHYSICIAN': 0.15,
|
||||||
'NURSE': 0.25,
|
'NURSE': 0.25,
|
||||||
'PHARMACIST': 0.08,
|
'PHARMACIST': 0.08,
|
||||||
@ -118,122 +86,171 @@ def create_saudi_users(tenants, users_per_tenant=50):
|
|||||||
'CLERICAL': 0.10
|
'CLERICAL': 0.10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Helpers
|
||||||
|
# -------------------------------
|
||||||
|
def ensure_departments(tenant):
|
||||||
|
"""
|
||||||
|
Ensure Department objects exist for this tenant; return a list of them.
|
||||||
|
Adjust if your Department is global (then drop tenant filtering).
|
||||||
|
"""
|
||||||
|
existing = list(Department.objects.filter(tenant=tenant)) if 'tenant' in [f.name for f in Department._meta.fields] else list(Department.objects.all())
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
# create seed departments
|
||||||
|
bulk = []
|
||||||
|
for name in SAUDI_DEPARTMENTS:
|
||||||
|
if 'tenant' in [f.name for f in Department._meta.fields]:
|
||||||
|
bulk.append(Department(name=name, tenant=tenant))
|
||||||
|
else:
|
||||||
|
bulk.append(Department(name=name))
|
||||||
|
Department.objects.bulk_create(bulk, ignore_conflicts=True)
|
||||||
|
|
||||||
|
return list(Department.objects.filter(tenant=tenant)) if 'tenant' in [f.name for f in Department._meta.fields] else list(Department.objects.all())
|
||||||
|
|
||||||
|
|
||||||
|
def generate_saudi_mobile_e164():
|
||||||
|
"""Generate Saudi E.164 mobile: +9665XXXXXXXX"""
|
||||||
|
return f"+9665{random.randint(10000000, 99999999)}"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_saudi_license():
|
||||||
|
"""Generate Saudi medical license number (fictional format)"""
|
||||||
|
prefix = random.choice(SAUDI_LICENSE_PREFIXES)
|
||||||
|
return f"{prefix}-{random.randint(100000, 999999)}"
|
||||||
|
|
||||||
|
|
||||||
|
def tenant_scoped_unique_username(tenant, base_username: str) -> str:
|
||||||
|
"""
|
||||||
|
Make username unique within a tenant (your User has tenant-scoped unique constraint).
|
||||||
|
"""
|
||||||
|
username = base_username
|
||||||
|
i = 1
|
||||||
|
while User.objects.filter(tenant=tenant, username=username).exists():
|
||||||
|
i += 1
|
||||||
|
username = f"{base_username}{i}"
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
def pick_job_title(role: str) -> str:
|
||||||
|
titles = SAUDI_JOB_TITLES.get(role)
|
||||||
|
if titles:
|
||||||
|
return random.choice(titles)
|
||||||
|
# fallback
|
||||||
|
return role.replace('_', ' ').title()
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Generators
|
||||||
|
# -------------------------------
|
||||||
|
def create_saudi_users(tenants, users_per_tenant=50):
|
||||||
|
"""
|
||||||
|
Create Users (auth + security), then populate Employee profile.
|
||||||
|
Relies on the post_save signal to create Employee automatically.
|
||||||
|
"""
|
||||||
|
all_users = []
|
||||||
|
|
||||||
for tenant in tenants:
|
for tenant in tenants:
|
||||||
|
departments = ensure_departments(tenant)
|
||||||
tenant_users = []
|
tenant_users = []
|
||||||
|
|
||||||
for role, percentage in role_distribution.items():
|
for role, pct in ROLE_DISTRIBUTION.items():
|
||||||
user_count = max(1, int(users_per_tenant * percentage))
|
count = max(1, int(users_per_tenant * pct))
|
||||||
|
|
||||||
for i in range(user_count):
|
for _ in range(count):
|
||||||
# Determine gender for Arabic naming
|
|
||||||
is_male = random.choice([True, False])
|
is_male = random.choice([True, False])
|
||||||
first_name = random.choice(SAUDI_FIRST_NAMES_MALE if is_male else SAUDI_FIRST_NAMES_FEMALE)
|
first_name = random.choice(SAUDI_FIRST_NAMES_MALE if is_male else SAUDI_FIRST_NAMES_FEMALE)
|
||||||
last_name = random.choice(SAUDI_FAMILY_NAMES)
|
last_name = random.choice(SAUDI_FAMILY_NAMES)
|
||||||
middle_name = random.choice(SAUDI_MIDDLE_NAMES) if random.choice([True, False]) else None
|
|
||||||
|
|
||||||
# Generate username
|
# base username like "mohammed.alrashid"
|
||||||
username = f"{first_name.lower()}.{last_name.lower().replace('-', '').replace('al', '')}"
|
base_username = f"{first_name.lower()}.{last_name.lower().replace('-', '').replace('al', '')}"
|
||||||
counter = 1
|
username = tenant_scoped_unique_username(tenant, base_username)
|
||||||
original_username = username
|
|
||||||
while User.objects.filter(username=username).exists():
|
|
||||||
username = f"{original_username}{counter}"
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
# Generate email
|
|
||||||
email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa"
|
email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa"
|
||||||
|
|
||||||
# Professional information
|
is_admin = role in ['ADMIN', 'SUPER_ADMIN']
|
||||||
department = random.choice(SAUDI_DEPARTMENTS)
|
is_superuser = role == 'SUPER_ADMIN'
|
||||||
job_title = random.choice(SAUDI_JOB_TITLES.get(role, [f"{role.replace('_', ' ').title()}"]))
|
|
||||||
|
|
||||||
# License information for medical professionals
|
|
||||||
license_number = None
|
|
||||||
license_state = None
|
|
||||||
license_expiry = None
|
|
||||||
npi_number = None
|
|
||||||
|
|
||||||
if role in ['PHYSICIAN', 'NURSE', 'PHARMACIST', 'RADIOLOGIST']:
|
|
||||||
license_number = generate_saudi_license()
|
|
||||||
license_state = random.choice(SAUDI_PROVINCES)
|
|
||||||
license_expiry = django_timezone.now().date() + timedelta(days=random.randint(365, 1095))
|
|
||||||
if role == 'PHYSICIAN':
|
|
||||||
npi_number = f"SA{random.randint(1000000, 9999999)}"
|
|
||||||
|
|
||||||
|
# Auth-level fields only
|
||||||
user = User.objects.create(
|
user = User.objects.create(
|
||||||
|
tenant=tenant,
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
middle_name=middle_name,
|
is_active=True,
|
||||||
preferred_name=first_name if random.choice([True, False]) else None,
|
is_staff=is_admin,
|
||||||
tenant=tenant,
|
is_superuser=is_superuser,
|
||||||
|
|
||||||
# Contact information
|
# security/session (these live on User by design)
|
||||||
phone_number=generate_saudi_phone(),
|
|
||||||
mobile_number=generate_saudi_mobile(),
|
|
||||||
|
|
||||||
# Professional information
|
|
||||||
employee_id=generate_saudi_employee_id(tenant.name, role),
|
|
||||||
department=department,
|
|
||||||
job_title=job_title,
|
|
||||||
role=role,
|
|
||||||
|
|
||||||
# License information
|
|
||||||
license_number=license_number,
|
|
||||||
license_state=license_state,
|
|
||||||
license_expiry=license_expiry,
|
|
||||||
npi_number=npi_number,
|
|
||||||
|
|
||||||
# Security settings
|
|
||||||
force_password_change=random.choice([True, False]),
|
force_password_change=random.choice([True, False]),
|
||||||
password_expires_at=django_timezone.now() + timedelta(days=random.randint(90, 365)),
|
password_expires_at=django_timezone.now() + timedelta(days=random.randint(90, 365)),
|
||||||
failed_login_attempts=random.randint(0, 2),
|
failed_login_attempts=random.randint(0, 2),
|
||||||
two_factor_enabled=random.choice([True, False]) if role in ['PHYSICIAN', 'ADMIN',
|
two_factor_enabled=random.choice([True, False]) if role in ['PHYSICIAN', 'ADMIN', 'PHARMACIST'] else False,
|
||||||
'PHARMACIST'] else False,
|
|
||||||
|
|
||||||
# Session settings
|
|
||||||
max_concurrent_sessions=random.choice([1, 2, 3, 5]),
|
max_concurrent_sessions=random.choice([1, 2, 3, 5]),
|
||||||
session_timeout_minutes=random.choice([30, 60, 120, 240]),
|
session_timeout_minutes=random.choice([30, 60, 120, 240]),
|
||||||
|
last_password_change=django_timezone.now() - timedelta(days=random.randint(1, 90)),
|
||||||
|
date_joined=django_timezone.now() - timedelta(days=random.randint(1, 365)),
|
||||||
|
)
|
||||||
|
user.set_password('Hospital@123')
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# Signal should have created Employee; now populate Employee fields
|
||||||
|
emp: Employee = user.employee_profile # created by signal
|
||||||
|
emp.tenant = tenant # ensure alignment
|
||||||
|
emp.first_name = first_name
|
||||||
|
emp.last_name = last_name
|
||||||
|
emp.preferred_name = first_name if random.choice([True, False]) else None
|
||||||
|
|
||||||
|
# Contact (E.164 KSA)
|
||||||
|
mobile = generate_saudi_mobile_e164()
|
||||||
|
emp.phone = mobile
|
||||||
|
emp.mobile_phone = mobile
|
||||||
|
emp.email = email
|
||||||
|
|
||||||
|
# Role/Org
|
||||||
|
emp.role = role
|
||||||
|
emp.department = random.choice(departments) if departments else None
|
||||||
|
emp.job_title = pick_job_title(role)
|
||||||
|
|
||||||
|
# License (only some roles)
|
||||||
|
if role in ['PHYSICIAN', 'NURSE', 'PHARMACIST', 'RADIOLOGIST']:
|
||||||
|
emp.license_number = generate_saudi_license()
|
||||||
|
emp.license_state = random.choice(SAUDI_PROVINCES)
|
||||||
|
emp.license_expiry_date = django_timezone.now().date() + timedelta(days=random.randint(365, 1095))
|
||||||
|
if role == 'PHYSICIAN':
|
||||||
|
# fictitious local analogue to NPI
|
||||||
|
emp.npi_number = f"SA{random.randint(1000000, 9999999)}"
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
user_timezone='Asia/Riyadh',
|
emp.user_timezone = 'Asia/Riyadh'
|
||||||
language=random.choice(['ar', 'en', 'ar_SA']),
|
emp.language = random.choice(['ar', 'en', 'ar_SA'])
|
||||||
theme=random.choice(['LIGHT', 'DARK', 'AUTO']),
|
emp.theme = random.choice([Employee.Theme.LIGHT, Employee.Theme.DARK, Employee.Theme.AUTO])
|
||||||
|
|
||||||
# Status
|
# Status / approval (approved later per-tenant)
|
||||||
is_verified=True,
|
emp.is_verified = True
|
||||||
is_approved=True,
|
emp.is_approved = True
|
||||||
approval_date=django_timezone.now() - timedelta(days=random.randint(1, 180)),
|
emp.approval_date = django_timezone.now() - timedelta(days=random.randint(1, 180))
|
||||||
is_active=True,
|
|
||||||
is_staff=role in ['ADMIN', 'SUPER_ADMIN'],
|
|
||||||
is_superuser=role == 'SUPER_ADMIN',
|
|
||||||
|
|
||||||
# Metadata
|
emp.save()
|
||||||
created_at=django_timezone.now() - timedelta(days=random.randint(1, 365)),
|
|
||||||
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)),
|
|
||||||
last_password_change=django_timezone.now() - timedelta(days=random.randint(1, 90)),
|
|
||||||
date_joined=django_timezone.now() - timedelta(days=random.randint(1, 365))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set password
|
|
||||||
user.set_password('Hospital@123') # Default password
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
users.append(user)
|
|
||||||
tenant_users.append(user)
|
tenant_users.append(user)
|
||||||
|
all_users.append(user)
|
||||||
|
|
||||||
# Set approval relationships
|
# Approval relationships: choose an approver among admins in this tenant
|
||||||
admin_users = [u for u in tenant_users if u.role in ['ADMIN', 'SUPER_ADMIN']]
|
admin_users = [u for u in tenant_users if u.is_staff or u.is_superuser]
|
||||||
if admin_users:
|
if admin_users:
|
||||||
approver = random.choice(admin_users)
|
approver = random.choice(admin_users)
|
||||||
for user in tenant_users:
|
for u in tenant_users:
|
||||||
if user != approver and user.role != 'SUPER_ADMIN':
|
if u != approver:
|
||||||
user.approved_by = approver
|
emp = u.employee_profile
|
||||||
user.save()
|
emp.approved_by = approver
|
||||||
|
emp.save(update_fields=['approved_by'])
|
||||||
|
|
||||||
print(f"Created {len(tenant_users)} users for {tenant.name}")
|
print(f"Created {len(tenant_users)} users for {tenant.name}")
|
||||||
|
|
||||||
return users
|
return all_users
|
||||||
|
|
||||||
|
|
||||||
def create_saudi_two_factor_devices(users):
|
def create_saudi_two_factor_devices(users):
|
||||||
@ -249,8 +266,8 @@ def create_saudi_two_factor_devices(users):
|
|||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
if user.two_factor_enabled:
|
if user.two_factor_enabled:
|
||||||
# Create 1-3 devices per user
|
|
||||||
device_count = random.randint(1, 3)
|
device_count = random.randint(1, 3)
|
||||||
|
emp = getattr(user, 'employee_profile', None)
|
||||||
|
|
||||||
for _ in range(device_count):
|
for _ in range(device_count):
|
||||||
device_type = random.choice(device_types)
|
device_type = random.choice(device_types)
|
||||||
@ -271,9 +288,9 @@ def create_saudi_two_factor_devices(users):
|
|||||||
if device_type == 'TOTP':
|
if device_type == 'TOTP':
|
||||||
device_data['secret_key'] = secrets.token_urlsafe(32)
|
device_data['secret_key'] = secrets.token_urlsafe(32)
|
||||||
elif device_type == 'SMS':
|
elif device_type == 'SMS':
|
||||||
device_data['phone_number'] = user.mobile_number
|
device_data['phone_number'] = emp.mobile_phone if emp else None
|
||||||
elif device_type == 'EMAIL':
|
elif device_type == 'EMAIL':
|
||||||
device_data['email_address'] = user.email
|
device_data['email_address'] = emp.email if emp and emp.email else user.email
|
||||||
|
|
||||||
device = TwoFactorDevice.objects.create(**device_data)
|
device = TwoFactorDevice.objects.create(**device_data)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
@ -285,21 +302,19 @@ def create_saudi_two_factor_devices(users):
|
|||||||
def create_saudi_social_accounts(users):
|
def create_saudi_social_accounts(users):
|
||||||
"""Create social authentication accounts for Saudi users"""
|
"""Create social authentication accounts for Saudi users"""
|
||||||
social_accounts = []
|
social_accounts = []
|
||||||
|
|
||||||
# Common providers in Saudi Arabia
|
|
||||||
providers = ['GOOGLE', 'MICROSOFT', 'APPLE', 'LINKEDIN']
|
providers = ['GOOGLE', 'MICROSOFT', 'APPLE', 'LINKEDIN']
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
# 30% chance of having social accounts
|
if random.choice([True, False, False, False]): # ~25% chance
|
||||||
if random.choice([True, False, False, False]):
|
|
||||||
provider = random.choice(providers)
|
provider = random.choice(providers)
|
||||||
|
display_name = user.get_full_name() or (user.employee_profile.get_display_name() if hasattr(user, 'employee_profile') else user.username)
|
||||||
|
|
||||||
social_account = SocialAccount.objects.create(
|
social_account = SocialAccount.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
provider_id=f"{provider.lower()}_{random.randint(100000000, 999999999)}",
|
provider_id=f"{provider.lower()}_{random.randint(100000000, 999999999)}",
|
||||||
provider_email=user.email,
|
provider_email=user.email,
|
||||||
display_name=user.get_full_name(),
|
display_name=display_name,
|
||||||
profile_url=f"https://{provider.lower()}.com/profile/{user.username}",
|
profile_url=f"https://{provider.lower()}.com/profile/{user.username}",
|
||||||
avatar_url=f"https://{provider.lower()}.com/avatar/{user.username}.jpg",
|
avatar_url=f"https://{provider.lower()}.com/avatar/{user.username}.jpg",
|
||||||
access_token=secrets.token_urlsafe(64),
|
access_token=secrets.token_urlsafe(64),
|
||||||
@ -323,30 +338,27 @@ def create_saudi_user_sessions(users):
|
|||||||
'37.99.', '37.200.', '31.9.', '31.173.', '188.161.',
|
'37.99.', '37.200.', '31.9.', '31.173.', '188.161.',
|
||||||
'185.84.', '188.245.', '217.9.', '82.205.', '5.63.'
|
'185.84.', '188.245.', '217.9.', '82.205.', '5.63.'
|
||||||
]
|
]
|
||||||
|
|
||||||
browsers = [
|
browsers = [
|
||||||
'Chrome 120.0.0.0', 'Safari 17.1.2', 'Firefox 121.0.0', 'Edge 120.0.0.0',
|
'Chrome 120.0.0.0', 'Safari 17.1.2', 'Firefox 121.0.0', 'Edge 120.0.0.0',
|
||||||
'Chrome Mobile 120.0.0.0', 'Safari Mobile 17.1.2'
|
'Chrome Mobile 120.0.0.0', 'Safari Mobile 17.1.2'
|
||||||
]
|
]
|
||||||
|
|
||||||
operating_systems = [
|
operating_systems = [
|
||||||
'Windows 11', 'Windows 10', 'macOS 14.0', 'iOS 17.1.2',
|
'Windows 11', 'Windows 10', 'macOS 14.0', 'iOS 17.1.2',
|
||||||
'Android 14', 'Ubuntu 22.04'
|
'Android 14', 'Ubuntu 22.04'
|
||||||
]
|
]
|
||||||
|
|
||||||
device_types = ['DESKTOP', 'MOBILE', 'TABLET']
|
device_types = ['DESKTOP', 'MOBILE', 'TABLET']
|
||||||
login_methods = ['PASSWORD', 'TWO_FACTOR', 'SOCIAL', 'SSO']
|
login_methods = ['PASSWORD', 'TWO_FACTOR', 'SOCIAL', 'SSO']
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
# Create 1-5 sessions per user
|
|
||||||
session_count = random.randint(1, 5)
|
session_count = random.randint(1, 5)
|
||||||
|
timeout_minutes = user.session_timeout_minutes or 30
|
||||||
|
|
||||||
for i in range(session_count):
|
for i in range(session_count):
|
||||||
ip_prefix = random.choice(saudi_ips)
|
ip_prefix = random.choice(saudi_ips)
|
||||||
ip_address = f"{ip_prefix}{random.randint(1, 255)}.{random.randint(1, 255)}"
|
ip_address = f"{ip_prefix}{random.randint(1, 255)}.{random.randint(1, 255)}"
|
||||||
|
|
||||||
session_start = django_timezone.now() - timedelta(hours=random.randint(1, 720))
|
session_start = django_timezone.now() - timedelta(hours=random.randint(1, 720))
|
||||||
is_active = i == 0 and random.choice([True, True, False]) # Most recent session likely active
|
is_active = (i == 0) and random.choice([True, True, False]) # recent likely active
|
||||||
|
|
||||||
session = UserSession.objects.create(
|
session = UserSession.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
@ -364,7 +376,7 @@ def create_saudi_user_sessions(users):
|
|||||||
login_method=random.choice(login_methods),
|
login_method=random.choice(login_methods),
|
||||||
created_at=session_start,
|
created_at=session_start,
|
||||||
last_activity_at=session_start + timedelta(minutes=random.randint(1, 480)),
|
last_activity_at=session_start + timedelta(minutes=random.randint(1, 480)),
|
||||||
expires_at=session_start + timedelta(hours=user.session_timeout_minutes // 60),
|
expires_at=session_start + timedelta(minutes=timeout_minutes),
|
||||||
ended_at=None if is_active else session_start + timedelta(hours=random.randint(1, 8))
|
ended_at=None if is_active else session_start + timedelta(hours=random.randint(1, 8))
|
||||||
)
|
)
|
||||||
sessions.append(session)
|
sessions.append(session)
|
||||||
@ -376,16 +388,12 @@ def create_saudi_user_sessions(users):
|
|||||||
def create_saudi_password_history(users):
|
def create_saudi_password_history(users):
|
||||||
"""Create password history for Saudi users"""
|
"""Create password history for Saudi users"""
|
||||||
password_history = []
|
password_history = []
|
||||||
|
|
||||||
passwords = ['Hospital@123', 'Medical@456', 'Health@789', 'Saudi@2024', 'Secure@Pass']
|
passwords = ['Hospital@123', 'Medical@456', 'Health@789', 'Saudi@2024', 'Secure@Pass']
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
# Create 1-5 password history entries per user
|
|
||||||
history_count = random.randint(1, 5)
|
history_count = random.randint(1, 5)
|
||||||
|
|
||||||
for i in range(history_count):
|
for i in range(history_count):
|
||||||
password = random.choice(passwords)
|
password = random.choice(passwords)
|
||||||
|
|
||||||
history_entry = PasswordHistory.objects.create(
|
history_entry = PasswordHistory.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
password_hash=make_password(password),
|
password_hash=make_password(password),
|
||||||
@ -397,33 +405,29 @@ def create_saudi_password_history(users):
|
|||||||
return password_history
|
return password_history
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Main
|
||||||
|
# -------------------------------
|
||||||
def main():
|
def main():
|
||||||
"""Main function to generate all Saudi accounts data"""
|
|
||||||
print("Starting Saudi Healthcare Accounts Data Generation...")
|
print("Starting Saudi Healthcare Accounts Data Generation...")
|
||||||
|
|
||||||
# Get existing tenants
|
|
||||||
tenants = list(Tenant.objects.all())
|
tenants = list(Tenant.objects.all())
|
||||||
if not tenants:
|
if not tenants:
|
||||||
print("❌ No tenants found. Please run the core data generator first.")
|
print("❌ No tenants found. Please seed core tenants first.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create users
|
print("\n1. Creating Saudi Healthcare Users (with Employee profiles)...")
|
||||||
print("\n1. Creating Saudi Healthcare Users...")
|
users = create_saudi_users(tenants, users_per_tenant=40)
|
||||||
users = create_saudi_users(tenants, 40) # 40 users per tenant
|
|
||||||
|
|
||||||
# Create two-factor devices
|
|
||||||
print("\n2. Creating Two-Factor Authentication Devices...")
|
print("\n2. Creating Two-Factor Authentication Devices...")
|
||||||
devices = create_saudi_two_factor_devices(users)
|
devices = create_saudi_two_factor_devices(users)
|
||||||
|
|
||||||
# Create social accounts
|
|
||||||
print("\n3. Creating Social Authentication Accounts...")
|
print("\n3. Creating Social Authentication Accounts...")
|
||||||
social_accounts = create_saudi_social_accounts(users)
|
social_accounts = create_saudi_social_accounts(users)
|
||||||
|
|
||||||
# Create user sessions
|
|
||||||
print("\n4. Creating User Sessions...")
|
print("\n4. Creating User Sessions...")
|
||||||
sessions = create_saudi_user_sessions(users)
|
sessions = create_saudi_user_sessions(users)
|
||||||
|
|
||||||
# Create password history
|
|
||||||
print("\n5. Creating Password History...")
|
print("\n5. Creating Password History...")
|
||||||
password_history = create_saudi_password_history(users)
|
password_history = create_saudi_password_history(users)
|
||||||
|
|
||||||
@ -435,10 +439,10 @@ def main():
|
|||||||
print(f" - User Sessions: {len(sessions)}")
|
print(f" - User Sessions: {len(sessions)}")
|
||||||
print(f" - Password History Entries: {len(password_history)}")
|
print(f" - Password History Entries: {len(password_history)}")
|
||||||
|
|
||||||
# Role distribution summary
|
|
||||||
role_counts = {}
|
role_counts = {}
|
||||||
for user in users:
|
for u in users:
|
||||||
role_counts[user.role] = role_counts.get(user.role, 0) + 1
|
role = u.employee_profile.role if hasattr(u, 'employee_profile') else 'UNKNOWN'
|
||||||
|
role_counts[role] = role_counts.get(role, 0) + 1
|
||||||
|
|
||||||
print(f"\n👥 User Role Distribution:")
|
print(f"\n👥 User Role Distribution:")
|
||||||
for role, count in sorted(role_counts.items()):
|
for role, count in sorted(role_counts.items()):
|
||||||
@ -449,7 +453,7 @@ def main():
|
|||||||
'devices': devices,
|
'devices': devices,
|
||||||
'social_accounts': social_accounts,
|
'social_accounts': social_accounts,
|
||||||
'sessions': sessions,
|
'sessions': sessions,
|
||||||
'password_history': password_history
|
'password_history': password_history,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
# Generated by Django 5.2.6 on 2025-09-15 14:05
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
# Generated by Django 5.2.6 on 2025-09-15 14:05
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-10">
|
<div class="col-xl-10">
|
||||||
@ -229,7 +229,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-10">
|
<div class="col-xl-10">
|
||||||
@ -186,7 +186,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-10">
|
<div class="col-xl-10">
|
||||||
@ -134,7 +134,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -56,7 +56,7 @@ def generate_dashboards(tenants, users):
|
|||||||
created_by=creator
|
created_by=creator
|
||||||
)
|
)
|
||||||
dashboard.allowed_users.add(creator)
|
dashboard.allowed_users.add(creator)
|
||||||
dashboard.allowed_roles = [creator.role]
|
dashboard.allowed_roles = [creator.employee_profile.role]
|
||||||
dashboard.save()
|
dashboard.save()
|
||||||
dashboards.append(dashboard)
|
dashboards.append(dashboard)
|
||||||
print(f"✅ Created {len(dashboards)} dashboards")
|
print(f"✅ Created {len(dashboards)} dashboards")
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
appointments/api/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
appointments/api/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
appointments/api/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
appointments/api/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
appointments/api/__pycache__/urls.cpython-312.pyc
Normal file
BIN
appointments/api/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
appointments/api/__pycache__/views.cpython-312.pyc
Normal file
BIN
appointments/api/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
@ -353,7 +353,7 @@ class AppointmentSearchForm(forms.Form):
|
|||||||
self.fields['provider'].queryset = User.objects.filter(
|
self.fields['provider'].queryset = User.objects.filter(
|
||||||
tenant=user.tenant,
|
tenant=user.tenant,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
||||||
).order_by('last_name', 'first_name')
|
).order_by('last_name', 'first_name')
|
||||||
|
|
||||||
|
|
||||||
@ -645,34 +645,34 @@ class WaitingListContactLogForm(forms.ModelForm):
|
|||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
'contact_method': forms.Select(attrs={
|
'contact_method': forms.Select(attrs={
|
||||||
'class': 'form-select',
|
'class': 'form-select form-select-sm',
|
||||||
'required': True
|
'required': True
|
||||||
}),
|
}),
|
||||||
'contact_outcome': forms.Select(attrs={
|
'contact_outcome': forms.Select(attrs={
|
||||||
'class': 'form-select',
|
'class': 'form-select form-select-sm',
|
||||||
'required': True
|
'required': True
|
||||||
}),
|
}),
|
||||||
'appointment_offered': forms.CheckboxInput(attrs={
|
'appointment_offered': forms.CheckboxInput(attrs={
|
||||||
'class': 'form-check-input'
|
'class': 'form-check-input'
|
||||||
}),
|
}),
|
||||||
'offered_date': forms.DateInput(attrs={
|
'offered_date': forms.DateInput(attrs={
|
||||||
'class': 'form-control',
|
'class': 'form-control form-control-sm',
|
||||||
'type': 'date'
|
'type': 'date'
|
||||||
}),
|
}),
|
||||||
'offered_time': forms.TimeInput(attrs={
|
'offered_time': forms.TimeInput(attrs={
|
||||||
'class': 'form-control',
|
'class': 'form-control form-control-sm',
|
||||||
'type': 'time'
|
'type': 'time'
|
||||||
}),
|
}),
|
||||||
'patient_response': forms.Select(attrs={
|
'patient_response': forms.Select(attrs={
|
||||||
'class': 'form-select'
|
'class': 'form-select form-select-sm'
|
||||||
}),
|
}),
|
||||||
'notes': forms.Textarea(attrs={
|
'notes': forms.Textarea(attrs={
|
||||||
'class': 'form-control',
|
'class': 'form-control form-control-sm',
|
||||||
'rows': 4,
|
'rows': 4,
|
||||||
'placeholder': 'Notes from contact attempt...'
|
'placeholder': 'Notes from contact attempt...'
|
||||||
}),
|
}),
|
||||||
'next_contact_date': forms.DateInput(attrs={
|
'next_contact_date': forms.DateInput(attrs={
|
||||||
'class': 'form-control',
|
'class': 'form-control form-control-sm',
|
||||||
'type': 'date',
|
'type': 'date',
|
||||||
'min': date.today().isoformat()
|
'min': date.today().isoformat()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
# Generated by Django 5.2.6 on 2025-09-15 14:05
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -549,6 +549,526 @@ class Migration(migrations.Migration):
|
|||||||
"ordering": ["-scheduled_start"],
|
"ordering": ["-scheduled_start"],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="WaitingList",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"waiting_list_id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
help_text="Unique waiting list entry identifier",
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"appointment_type",
|
||||||
|
models.CharField(
|
||||||
|
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"),
|
||||||
|
],
|
||||||
|
help_text="Type of appointment requested",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"specialty",
|
||||||
|
models.CharField(
|
||||||
|
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"),
|
||||||
|
],
|
||||||
|
help_text="Medical specialty required",
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"priority",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("ROUTINE", "Routine"),
|
||||||
|
("URGENT", "Urgent"),
|
||||||
|
("STAT", "STAT"),
|
||||||
|
("EMERGENCY", "Emergency"),
|
||||||
|
],
|
||||||
|
default="ROUTINE",
|
||||||
|
help_text="Clinical priority level",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"urgency_score",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
default=1,
|
||||||
|
help_text="Clinical urgency score (1-10, 10 being most urgent)",
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"clinical_indication",
|
||||||
|
models.TextField(
|
||||||
|
help_text="Clinical reason for appointment request"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"diagnosis_codes",
|
||||||
|
models.JSONField(
|
||||||
|
blank=True, default=list, help_text="ICD-10 diagnosis codes"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"preferred_date",
|
||||||
|
models.DateField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Patient preferred appointment date",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"preferred_time",
|
||||||
|
models.TimeField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Patient preferred appointment time",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flexible_scheduling",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Patient accepts alternative dates/times",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"earliest_acceptable_date",
|
||||||
|
models.DateField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Earliest acceptable appointment date",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"latest_acceptable_date",
|
||||||
|
models.DateField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Latest acceptable appointment date",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"acceptable_days",
|
||||||
|
models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
help_text="Acceptable days of week (0=Monday, 6=Sunday)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"acceptable_times",
|
||||||
|
models.JSONField(
|
||||||
|
blank=True, default=list, help_text="Acceptable time ranges"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contact_method",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("PHONE", "Phone"),
|
||||||
|
("EMAIL", "Email"),
|
||||||
|
("SMS", "SMS"),
|
||||||
|
("PORTAL", "Patient Portal"),
|
||||||
|
("MAIL", "Mail"),
|
||||||
|
],
|
||||||
|
default="PHONE",
|
||||||
|
help_text="Preferred contact method",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contact_phone",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Contact phone number",
|
||||||
|
max_length=20,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contact_email",
|
||||||
|
models.EmailField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Contact email address",
|
||||||
|
max_length=254,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"status",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("ACTIVE", "Active"),
|
||||||
|
("CONTACTED", "Contacted"),
|
||||||
|
("OFFERED", "Appointment Offered"),
|
||||||
|
("SCHEDULED", "Scheduled"),
|
||||||
|
("CANCELLED", "Cancelled"),
|
||||||
|
("EXPIRED", "Expired"),
|
||||||
|
("TRANSFERRED", "Transferred"),
|
||||||
|
],
|
||||||
|
default="ACTIVE",
|
||||||
|
help_text="Waiting list status",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"position",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Position in waiting list queue",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"estimated_wait_time",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
blank=True, help_text="Estimated wait time in days", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"last_contacted",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Last contact attempt date/time",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contact_attempts",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
default=0, help_text="Number of contact attempts made"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"max_contact_attempts",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
default=3, help_text="Maximum contact attempts before expiring"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"appointments_offered",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
default=0, help_text="Number of appointments offered"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"appointments_declined",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
default=0, help_text="Number of appointments declined"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"last_offer_date",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Date of last appointment offer",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"requires_interpreter",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, help_text="Patient requires interpreter services"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"interpreter_language",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Required interpreter language",
|
||||||
|
max_length=50,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"accessibility_requirements",
|
||||||
|
models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Special accessibility requirements",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"transportation_needed",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Patient needs transportation assistance",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"insurance_verified",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, help_text="Insurance coverage verified"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"authorization_required",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, help_text="Prior authorization required"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"authorization_status",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("NOT_REQUIRED", "Not Required"),
|
||||||
|
("PENDING", "Pending"),
|
||||||
|
("APPROVED", "Approved"),
|
||||||
|
("DENIED", "Denied"),
|
||||||
|
("EXPIRED", "Expired"),
|
||||||
|
],
|
||||||
|
default="NOT_REQUIRED",
|
||||||
|
help_text="Authorization status",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"authorization_number",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Authorization number",
|
||||||
|
max_length=100,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"referring_provider",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Referring provider name",
|
||||||
|
max_length=200,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"referral_date",
|
||||||
|
models.DateField(
|
||||||
|
blank=True, help_text="Date of referral", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"referral_urgency",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("ROUTINE", "Routine"),
|
||||||
|
("URGENT", "Urgent"),
|
||||||
|
("STAT", "STAT"),
|
||||||
|
],
|
||||||
|
default="ROUTINE",
|
||||||
|
help_text="Referral urgency level",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"removal_reason",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
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"),
|
||||||
|
],
|
||||||
|
help_text="Reason for removal from waiting list",
|
||||||
|
max_length=50,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"removal_notes",
|
||||||
|
models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Additional notes about removal",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"removed_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Date/time removed from waiting list",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"notes",
|
||||||
|
models.TextField(
|
||||||
|
blank=True, help_text="Additional notes and comments", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Waiting List Entry",
|
||||||
|
"verbose_name_plural": "Waiting List Entries",
|
||||||
|
"db_table": "appointments_waiting_list",
|
||||||
|
"ordering": ["priority", "urgency_score", "created_at"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="WaitingListContactLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contact_date",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True, help_text="Date and time of contact attempt"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contact_method",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("PHONE", "Phone Call"),
|
||||||
|
("EMAIL", "Email"),
|
||||||
|
("SMS", "SMS"),
|
||||||
|
("PORTAL", "Patient Portal Message"),
|
||||||
|
("MAIL", "Mail"),
|
||||||
|
("IN_PERSON", "In Person"),
|
||||||
|
],
|
||||||
|
help_text="Method of contact used",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contact_outcome",
|
||||||
|
models.CharField(
|
||||||
|
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"),
|
||||||
|
],
|
||||||
|
help_text="Outcome of contact attempt",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"appointment_offered",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Appointment was offered during contact",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"offered_date",
|
||||||
|
models.DateField(
|
||||||
|
blank=True, help_text="Date of offered appointment", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"offered_time",
|
||||||
|
models.TimeField(
|
||||||
|
blank=True, help_text="Time of offered appointment", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"patient_response",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
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"),
|
||||||
|
],
|
||||||
|
help_text="Patient response to contact",
|
||||||
|
max_length=20,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"notes",
|
||||||
|
models.TextField(
|
||||||
|
blank=True, help_text="Notes from contact attempt", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"next_contact_date",
|
||||||
|
models.DateField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Scheduled date for next contact attempt",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Waiting List Contact Log",
|
||||||
|
"verbose_name_plural": "Waiting List Contact Logs",
|
||||||
|
"db_table": "appointments_waiting_list_contact_log",
|
||||||
|
"ordering": ["-contact_date"],
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="WaitingQueue",
|
name="WaitingQueue",
|
||||||
fields=[
|
fields=[
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
# Generated by Django 5.2.6 on 2025-09-15 14:05
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -12,6 +12,7 @@ class Migration(migrations.Migration):
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
("appointments", "0001_initial"),
|
("appointments", "0001_initial"),
|
||||||
("core", "0001_initial"),
|
("core", "0001_initial"),
|
||||||
|
("hr", "0001_initial"),
|
||||||
("patients", "0001_initial"),
|
("patients", "0001_initial"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
@ -179,6 +180,105 @@ class Migration(migrations.Migration):
|
|||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="waitinglist",
|
||||||
|
name="created_by",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="User who created the waiting list entry",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="created_waiting_list_entries",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="waitinglist",
|
||||||
|
name="department",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text="Department for appointment",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="waiting_list_entries",
|
||||||
|
to="hr.department",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="waitinglist",
|
||||||
|
name="patient",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text="Patient on waiting list",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="waiting_list_entries",
|
||||||
|
to="patients.patientprofile",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="waitinglist",
|
||||||
|
name="provider",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Preferred healthcare provider",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="provider_waiting_list",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="waitinglist",
|
||||||
|
name="removed_by",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="User who removed entry from waiting list",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="removed_waiting_list_entries",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="waitinglist",
|
||||||
|
name="scheduled_appointment",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Scheduled appointment from waiting list",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="waiting_list_entry",
|
||||||
|
to="appointments.appointmentrequest",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="waitinglist",
|
||||||
|
name="tenant",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text="Organization tenant",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="waiting_list_entries",
|
||||||
|
to="core.tenant",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="waitinglistcontactlog",
|
||||||
|
name="contacted_by",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Staff member who made contact",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="waitinglistcontactlog",
|
||||||
|
name="waiting_list_entry",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text="Associated waiting list entry",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="contact_logs",
|
||||||
|
to="appointments.waitinglist",
|
||||||
|
),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="waitingqueue",
|
model_name="waitingqueue",
|
||||||
name="created_by",
|
name="created_by",
|
||||||
@ -324,6 +424,63 @@ class Migration(migrations.Migration):
|
|||||||
fields=["scheduled_start"], name="appointment_schedul_8a4e8e_idx"
|
fields=["scheduled_start"], name="appointment_schedul_8a4e8e_idx"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="waitinglist",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["tenant", "status"], name="appointment_tenant__a558da_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="waitinglist",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["patient", "status"], name="appointment_patient_73f03d_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="waitinglist",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["department", "specialty", "status"],
|
||||||
|
name="appointment_departm_78fd70_idx",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="waitinglist",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["priority", "urgency_score"],
|
||||||
|
name="appointment_priorit_30fb90_idx",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="waitinglist",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["status", "created_at"], name="appointment_status_cfe551_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="waitinglist",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["provider", "status"], name="appointment_provide_dd6c2b_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="waitinglistcontactlog",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["waiting_list_entry", "contact_date"],
|
||||||
|
name="appointment_waiting_50d8ac_idx",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="waitinglistcontactlog",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["contact_outcome"], name="appointment_contact_ad9c45_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="waitinglistcontactlog",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["next_contact_date"], name="appointment_next_co_b29984_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name="waitingqueue",
|
model_name="waitingqueue",
|
||||||
index=models.Index(
|
index=models.Index(
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
# 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,688 +0,0 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-09-11 17:03
|
|
||||||
|
|
||||||
import django.core.validators
|
|
||||||
import django.db.models.deletion
|
|
||||||
import uuid
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("appointments", "0002_initial"),
|
|
||||||
("core", "0001_initial"),
|
|
||||||
("hr", "0001_initial"),
|
|
||||||
("patients", "0003_remove_insuranceinfo_subscriber_ssn_and_more"),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="WaitingList",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"waiting_list_id",
|
|
||||||
models.UUIDField(
|
|
||||||
default=uuid.uuid4,
|
|
||||||
editable=False,
|
|
||||||
help_text="Unique waiting list entry identifier",
|
|
||||||
unique=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"appointment_type",
|
|
||||||
models.CharField(
|
|
||||||
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"),
|
|
||||||
],
|
|
||||||
help_text="Type of appointment requested",
|
|
||||||
max_length=50,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"specialty",
|
|
||||||
models.CharField(
|
|
||||||
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"),
|
|
||||||
],
|
|
||||||
help_text="Medical specialty required",
|
|
||||||
max_length=100,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"priority",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("ROUTINE", "Routine"),
|
|
||||||
("URGENT", "Urgent"),
|
|
||||||
("STAT", "STAT"),
|
|
||||||
("EMERGENCY", "Emergency"),
|
|
||||||
],
|
|
||||||
default="ROUTINE",
|
|
||||||
help_text="Clinical priority level",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"urgency_score",
|
|
||||||
models.PositiveIntegerField(
|
|
||||||
default=1,
|
|
||||||
help_text="Clinical urgency score (1-10, 10 being most urgent)",
|
|
||||||
validators=[
|
|
||||||
django.core.validators.MinValueValidator(1),
|
|
||||||
django.core.validators.MaxValueValidator(10),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"clinical_indication",
|
|
||||||
models.TextField(
|
|
||||||
help_text="Clinical reason for appointment request"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"diagnosis_codes",
|
|
||||||
models.JSONField(
|
|
||||||
blank=True, default=list, help_text="ICD-10 diagnosis codes"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"preferred_date",
|
|
||||||
models.DateField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Patient preferred appointment date",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"preferred_time",
|
|
||||||
models.TimeField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Patient preferred appointment time",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"flexible_scheduling",
|
|
||||||
models.BooleanField(
|
|
||||||
default=True,
|
|
||||||
help_text="Patient accepts alternative dates/times",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"earliest_acceptable_date",
|
|
||||||
models.DateField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Earliest acceptable appointment date",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"latest_acceptable_date",
|
|
||||||
models.DateField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Latest acceptable appointment date",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"acceptable_days",
|
|
||||||
models.JSONField(
|
|
||||||
blank=True,
|
|
||||||
default=list,
|
|
||||||
help_text="Acceptable days of week (0=Monday, 6=Sunday)",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"acceptable_times",
|
|
||||||
models.JSONField(
|
|
||||||
blank=True, default=list, help_text="Acceptable time ranges"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"contact_method",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("PHONE", "Phone"),
|
|
||||||
("EMAIL", "Email"),
|
|
||||||
("SMS", "SMS"),
|
|
||||||
("PORTAL", "Patient Portal"),
|
|
||||||
("MAIL", "Mail"),
|
|
||||||
],
|
|
||||||
default="PHONE",
|
|
||||||
help_text="Preferred contact method",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"contact_phone",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Contact phone number",
|
|
||||||
max_length=20,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"contact_email",
|
|
||||||
models.EmailField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Contact email address",
|
|
||||||
max_length=254,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"status",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("ACTIVE", "Active"),
|
|
||||||
("CONTACTED", "Contacted"),
|
|
||||||
("OFFERED", "Appointment Offered"),
|
|
||||||
("SCHEDULED", "Scheduled"),
|
|
||||||
("CANCELLED", "Cancelled"),
|
|
||||||
("EXPIRED", "Expired"),
|
|
||||||
("TRANSFERRED", "Transferred"),
|
|
||||||
],
|
|
||||||
default="ACTIVE",
|
|
||||||
help_text="Waiting list status",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"position",
|
|
||||||
models.PositiveIntegerField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Position in waiting list queue",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"estimated_wait_time",
|
|
||||||
models.PositiveIntegerField(
|
|
||||||
blank=True, help_text="Estimated wait time in days", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"last_contacted",
|
|
||||||
models.DateTimeField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Last contact attempt date/time",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"contact_attempts",
|
|
||||||
models.PositiveIntegerField(
|
|
||||||
default=0, help_text="Number of contact attempts made"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"max_contact_attempts",
|
|
||||||
models.PositiveIntegerField(
|
|
||||||
default=3, help_text="Maximum contact attempts before expiring"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"appointments_offered",
|
|
||||||
models.PositiveIntegerField(
|
|
||||||
default=0, help_text="Number of appointments offered"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"appointments_declined",
|
|
||||||
models.PositiveIntegerField(
|
|
||||||
default=0, help_text="Number of appointments declined"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"last_offer_date",
|
|
||||||
models.DateTimeField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Date of last appointment offer",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"requires_interpreter",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False, help_text="Patient requires interpreter services"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"interpreter_language",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Required interpreter language",
|
|
||||||
max_length=50,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"accessibility_requirements",
|
|
||||||
models.TextField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Special accessibility requirements",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"transportation_needed",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="Patient needs transportation assistance",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"insurance_verified",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False, help_text="Insurance coverage verified"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"authorization_required",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False, help_text="Prior authorization required"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"authorization_status",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("NOT_REQUIRED", "Not Required"),
|
|
||||||
("PENDING", "Pending"),
|
|
||||||
("APPROVED", "Approved"),
|
|
||||||
("DENIED", "Denied"),
|
|
||||||
("EXPIRED", "Expired"),
|
|
||||||
],
|
|
||||||
default="NOT_REQUIRED",
|
|
||||||
help_text="Authorization status",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"authorization_number",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Authorization number",
|
|
||||||
max_length=100,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"referring_provider",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Referring provider name",
|
|
||||||
max_length=200,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"referral_date",
|
|
||||||
models.DateField(
|
|
||||||
blank=True, help_text="Date of referral", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"referral_urgency",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("ROUTINE", "Routine"),
|
|
||||||
("URGENT", "Urgent"),
|
|
||||||
("STAT", "STAT"),
|
|
||||||
],
|
|
||||||
default="ROUTINE",
|
|
||||||
help_text="Referral urgency level",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"removal_reason",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
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"),
|
|
||||||
],
|
|
||||||
help_text="Reason for removal from waiting list",
|
|
||||||
max_length=50,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"removal_notes",
|
|
||||||
models.TextField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Additional notes about removal",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"removed_at",
|
|
||||||
models.DateTimeField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Date/time removed from waiting list",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
||||||
("updated_at", models.DateTimeField(auto_now=True)),
|
|
||||||
(
|
|
||||||
"notes",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, help_text="Additional notes and comments", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"created_by",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text="User who created the waiting list entry",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="created_waiting_list_entries",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"department",
|
|
||||||
models.ForeignKey(
|
|
||||||
help_text="Department for appointment",
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="waiting_list_entries",
|
|
||||||
to="hr.department",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"patient",
|
|
||||||
models.ForeignKey(
|
|
||||||
help_text="Patient on waiting list",
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="waiting_list_entries",
|
|
||||||
to="patients.patientprofile",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"provider",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text="Preferred healthcare provider",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="provider_waiting_list",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"removed_by",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text="User who removed entry from waiting list",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="removed_waiting_list_entries",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"scheduled_appointment",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text="Scheduled appointment from waiting list",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="waiting_list_entry",
|
|
||||||
to="appointments.appointmentrequest",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"tenant",
|
|
||||||
models.ForeignKey(
|
|
||||||
help_text="Organization tenant",
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="waiting_list_entries",
|
|
||||||
to="core.tenant",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Waiting List Entry",
|
|
||||||
"verbose_name_plural": "Waiting List Entries",
|
|
||||||
"db_table": "appointments_waiting_list",
|
|
||||||
"ordering": ["priority", "urgency_score", "created_at"],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="WaitingListContactLog",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"contact_date",
|
|
||||||
models.DateTimeField(
|
|
||||||
auto_now_add=True, help_text="Date and time of contact attempt"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"contact_method",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("PHONE", "Phone Call"),
|
|
||||||
("EMAIL", "Email"),
|
|
||||||
("SMS", "SMS"),
|
|
||||||
("PORTAL", "Patient Portal Message"),
|
|
||||||
("MAIL", "Mail"),
|
|
||||||
("IN_PERSON", "In Person"),
|
|
||||||
],
|
|
||||||
help_text="Method of contact used",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"contact_outcome",
|
|
||||||
models.CharField(
|
|
||||||
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"),
|
|
||||||
],
|
|
||||||
help_text="Outcome of contact attempt",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"appointment_offered",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="Appointment was offered during contact",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"offered_date",
|
|
||||||
models.DateField(
|
|
||||||
blank=True, help_text="Date of offered appointment", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"offered_time",
|
|
||||||
models.TimeField(
|
|
||||||
blank=True, help_text="Time of offered appointment", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"patient_response",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
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"),
|
|
||||||
],
|
|
||||||
help_text="Patient response to contact",
|
|
||||||
max_length=20,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"notes",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, help_text="Notes from contact attempt", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"next_contact_date",
|
|
||||||
models.DateField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Scheduled date for next contact attempt",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"contacted_by",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text="Staff member who made contact",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"waiting_list_entry",
|
|
||||||
models.ForeignKey(
|
|
||||||
help_text="Associated waiting list entry",
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="contact_logs",
|
|
||||||
to="appointments.waitinglist",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Waiting List Contact Log",
|
|
||||||
"verbose_name_plural": "Waiting List Contact Logs",
|
|
||||||
"db_table": "appointments_waiting_list_contact_log",
|
|
||||||
"ordering": ["-contact_date"],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="waitinglist",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["tenant", "status"], name="appointment_tenant__a558da_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="waitinglist",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["patient", "status"], name="appointment_patient_73f03d_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="waitinglist",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["department", "specialty", "status"],
|
|
||||||
name="appointment_departm_78fd70_idx",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="waitinglist",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["priority", "urgency_score"],
|
|
||||||
name="appointment_priorit_30fb90_idx",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="waitinglist",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["status", "created_at"], name="appointment_status_cfe551_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="waitinglist",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["provider", "status"], name="appointment_provide_dd6c2b_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="waitinglistcontactlog",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["waiting_list_entry", "contact_date"],
|
|
||||||
name="appointment_waiting_50d8ac_idx",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="waitinglistcontactlog",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["contact_outcome"], name="appointment_contact_ad9c45_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="waitinglistcontactlog",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["next_contact_date"], name="appointment_next_co_b29984_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1371,6 +1371,7 @@ class WaitingList(models.Model):
|
|||||||
|
|
||||||
acceptable_days = models.JSONField(
|
acceptable_days = models.JSONField(
|
||||||
default=list,
|
default=list,
|
||||||
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text='Acceptable days of week (0=Monday, 6=Sunday)'
|
help_text='Acceptable days of week (0=Monday, 6=Sunday)'
|
||||||
)
|
)
|
||||||
|
|||||||
BIN
appointments/templates/appointments/.DS_Store
vendored
BIN
appointments/templates/appointments/.DS_Store
vendored
Binary file not shown.
@ -9,7 +9,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
@ -188,7 +188,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
@ -241,7 +241,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
182
appointments/templates/appointments/calendar.html
Normal file
182
appointments/templates/appointments/calendar.html
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
{# templates/appointments/calendar.html #}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Appointments Calendar{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<style>
|
||||||
|
.fc .fc-toolbar-title { font-weight: 600; }
|
||||||
|
.calendar-wrapper { min-height: 70vh; }
|
||||||
|
</style>
|
||||||
|
{% 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-calendar-alt"></i> Scheduling<span class="fw-light">Calendar</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted">View your calendar and manage schedules.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="calendar" class="calendar-wrapper"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header fw-bold">Appointment Details</div>
|
||||||
|
<div class="card-body" id="appt-details">
|
||||||
|
<div class="text-muted small">Click an event to see details.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header fw-bold">Filters</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="calendarFilters">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Status</label>
|
||||||
|
<select class="form-select" name="status">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="CONFIRMED">Confirmed</option>
|
||||||
|
<option value="CHECKED_IN">Checked-in</option>
|
||||||
|
<option value="IN_PROGRESS">In progress</option>
|
||||||
|
<option value="COMPLETED">Completed</option>
|
||||||
|
<option value="CANCELLED">Cancelled</option>
|
||||||
|
<option value="NO_SHOW">No show</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="provider" class="form-label">Provider</label>
|
||||||
|
<input id="provider" class="form-control" name="provider_id" placeholder="Provider ID (optional)">
|
||||||
|
</div>
|
||||||
|
<button type="button" id="applyFilters" class="btn btn-theme w-100">Apply</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Optional: Bootstrap modal for full details #}
|
||||||
|
<div class="modal fade" id="apptModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h6 class="modal-title">Appointment</h6>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="apptModalBody">
|
||||||
|
<!-- HTMX fills here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<script src="{% static 'plugins/moment/min/moment.min.js' %}"></script>
|
||||||
|
<script src="{% static 'plugins/@fullcalendar/core/index.global.js' %}"></script>
|
||||||
|
<script src="{% static 'plugins/@fullcalendar/daygrid/index.global.js' %}"></script>
|
||||||
|
<script src="{% static 'plugins/@fullcalendar/timegrid/index.global.js' %}"></script>
|
||||||
|
<script src="{% static 'plugins/@fullcalendar/interaction/index.global.js' %}"></script>
|
||||||
|
<script src="{% static 'plugins/@fullcalendar/list/index.global.js' %}"></script>
|
||||||
|
<script src="{% static 'plugins/@fullcalendar/bootstrap/index.global.js' %}"></script>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
const calEl = document.getElementById('calendar');
|
||||||
|
const detailsEl = document.getElementById('appt-details');
|
||||||
|
const filtersForm = document.getElementById('calendarFilters');
|
||||||
|
|
||||||
|
function buildEventsUrl(info){
|
||||||
|
const base = "{% url 'appointments:calendar_events' %}";
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
start: info.startStr,
|
||||||
|
end: info.endStr,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = filtersForm.querySelector('[name=status]').value;
|
||||||
|
const provider_id = filtersForm.querySelector('[name=provider_id]').value;
|
||||||
|
if (status) params.set('status', status);
|
||||||
|
if (provider_id) params.set('provider_id', provider_id);
|
||||||
|
return `${base}?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendar = new FullCalendar.Calendar(calEl, {
|
||||||
|
timeZone: 'Asia/Riyadh',
|
||||||
|
initialView: 'timeGridWeek',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
||||||
|
},
|
||||||
|
slotMinTime: '07:00:00',
|
||||||
|
slotMaxTime: '22:00:00',
|
||||||
|
nowIndicator: true,
|
||||||
|
navLinks: true,
|
||||||
|
selectable: false,
|
||||||
|
editable: true, // allow drag/resize
|
||||||
|
eventDurationEditable: true,
|
||||||
|
eventSources: [{
|
||||||
|
events: function(info, success, failure){
|
||||||
|
fetch(buildEventsUrl(info), {credentials: 'same-origin'})
|
||||||
|
.then(r => r.ok ? r.json() : Promise.reject(r))
|
||||||
|
.then(data => success(data))
|
||||||
|
.catch(() => failure());
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
eventClick: function(info){
|
||||||
|
// Sidebar card via HTMX-like fetch
|
||||||
|
fetch("{% url 'appointments:appointment_detail_card' 0 %}".replace('0', info.event.id), {credentials:'same-origin'})
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(html => { detailsEl.innerHTML = html; });
|
||||||
|
|
||||||
|
// Also open modal
|
||||||
|
{#const modalBody = document.getElementById('apptModalBody');#}
|
||||||
|
{#modalBody.innerHTML = '<div class="text-center text-muted py-3">Loading...</div>';#}
|
||||||
|
{#fetch("{% url 'appointments:appointment_detail_card' 0 %}".replace('0', info.event.id), {credentials:'same-origin'})#}
|
||||||
|
{# .then(r => r.text())#}
|
||||||
|
{# .then(html => { modalBody.innerHTML = html; new bootstrap.Modal('#apptModal').show(); });#}
|
||||||
|
},
|
||||||
|
eventDrop: function(info){ sendReschedule(info); },
|
||||||
|
eventResize: function(info){ sendReschedule(info); }
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCsrf(){
|
||||||
|
// works with Django’s default CSRF cookie name
|
||||||
|
const m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||||
|
return m ? m[1] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendReschedule(info){
|
||||||
|
const url = "{% url 'appointments:reschedule_appointment' 0 %}".replace('0', info.event.id);
|
||||||
|
const payload = new URLSearchParams({
|
||||||
|
start: info.event.start.toISOString(),
|
||||||
|
end: (info.event.end ? info.event.end : info.event.start).toISOString()
|
||||||
|
});
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {'X-CSRFToken': getCsrf(), 'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: payload.toString()
|
||||||
|
}).then(r => {
|
||||||
|
if(!r.ok){ info.revert(); }
|
||||||
|
}).catch(()=> info.revert());
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('applyFilters').addEventListener('click', function(){
|
||||||
|
calendar.refetchEvents();
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@ -7,7 +7,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
@ -159,7 +159,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Appointment Detail Modal -->
|
<!-- Appointment Detail Modal -->
|
||||||
<div class="modal fade" id="appointmentModal" tabindex="-1">
|
<div class="modal fade" id="appointmentModal" tabindex="-1">
|
||||||
|
|||||||
@ -2,67 +2,86 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Cancel Appointment{% endblock %}
|
{% block title %}Cancel Appointment{% endblock %}
|
||||||
|
{% block css %}
|
||||||
|
<style>
|
||||||
|
.modal-content {
|
||||||
|
border: none;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 1.5rem 1.5rem 0.5rem;
|
||||||
|
border-radius: 1rem 1rem 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-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 class="container">
|
<div>
|
||||||
|
<h1 class="h2">
|
||||||
|
<i class="fas fa-calendar-alt"></i> Cancel<span class="fw-light">Appointment</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted">Appointment cancellation form.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-8">
|
<div class="col-xl-8">
|
||||||
<ul class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_detail' appointment.id %}">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</a></li>
|
|
||||||
<li class="breadcrumb-item active">Cancel</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h1 class="page-header">Cancel Appointment</h1>
|
|
||||||
|
|
||||||
<!-- Appointment Info -->
|
<!-- Appointment Info -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse mb-4" data-sortable-id="index-1">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h4 class="card-title">Appointment Details</h4>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-calendar-day"></i> Appointment Details
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-4"><strong>Patient:</strong></div>
|
<div class="col-4"><strong>Patient:</strong></div>
|
||||||
<div class="col-8">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</div>
|
<div class="col-8">{{ appointment.patient.get_full_name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-4"><strong>Patient ID:</strong></div>
|
<div class="col-4"><strong>Patient ID:</strong></div>
|
||||||
<div class="col-8">{{ appointment.patient.patient_id }}</div>
|
<div class="col-8">{{ appointment.patient.mrn }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-4"><strong>Provider:</strong></div>
|
<div class="col-4"><strong>Provider:</strong></div>
|
||||||
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
|
<div class="col-8">{{ appointment.provider.get_full_name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-4"><strong>Department:</strong></div>
|
<div class="col-4"><strong>Department:</strong></div>
|
||||||
<div class="col-8">{{ appointment.department.name }}</div>
|
<div class="col-8">{{ appointment.provider.department }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-4"><strong>Date:</strong></div>
|
<div class="col-4"><strong>Date:</strong></div>
|
||||||
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div>
|
<div class="col-8">{{ appointment.preferred_date|date:"M d, Y" }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-4"><strong>Time:</strong></div>
|
<div class="col-4"><strong>Time:</strong></div>
|
||||||
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
|
<div class="col-8">{{ appointment.preferred_time|time:"g:i A" }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-4"><strong>Type:</strong></div>
|
<div class="col-4"><strong>Type:</strong></div>
|
||||||
<div class="col-8">{{ appointment.appointment_type.name }}</div>
|
<div class="col-8">{{ appointment.get_appointment_type_display }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-4"><strong>Status:</strong></div>
|
<div class="col-4"><strong>Status:</strong></div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
{% if appointment.status == 'scheduled' %}
|
{% if appointment.status == 'SCHEDULED' %}
|
||||||
<span class="badge bg-info">Scheduled</span>
|
<span class="badge bg-info">Scheduled</span>
|
||||||
{% elif appointment.status == 'confirmed' %}
|
{% elif appointment.status == 'CONFIRMED' %}
|
||||||
<span class="badge bg-success">Confirmed</span>
|
<span class="badge bg-success">Confirmed</span>
|
||||||
{% elif appointment.status == 'checked_in' %}
|
{% elif appointment.status == 'CHECKED_IN' %}
|
||||||
<span class="badge bg-primary">Checked In</span>
|
<span class="badge bg-primary">Checked In</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -82,11 +101,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cancellation Form -->
|
<!-- Cancellation Form -->
|
||||||
<div class="card">
|
<div class="panel panel-inverse mb-4" data-sortable-id="index-2">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h4 class="card-title">Cancellation Details</h4>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-times-rectangle"></i> Cancellation Details
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
@ -129,7 +156,7 @@
|
|||||||
<label class="col-form-label col-md-3">Cancellation Notes</label>
|
<label class="col-form-label col-md-3">Cancellation Notes</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<textarea name="cancellation_notes" class="form-control" rows="4"
|
<textarea name="cancellation_notes" class="form-control" rows="4"
|
||||||
placeholder="Additional details about the cancellation">{{ form.cancellation_notes.value|default:'' }}</textarea>
|
placeholder="Additional details about the cancellation">{{ form.cancellation_notes.value }}</textarea>
|
||||||
{% if form.cancellation_notes.errors %}
|
{% if form.cancellation_notes.errors %}
|
||||||
<div class="text-danger">{{ form.cancellation_notes.errors.0 }}</div>
|
<div class="text-danger">{{ form.cancellation_notes.errors.0 }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -140,7 +167,9 @@
|
|||||||
<label class="col-form-label col-md-3">Cancellation Fee</label>
|
<label class="col-form-label col-md-3">Cancellation Fee</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text">$</span>
|
<span class="input-group-text">
|
||||||
|
<span class="symbol m-0 p-0">ê</span>
|
||||||
|
</span>
|
||||||
<input type="number" name="cancellation_fee" class="form-control"
|
<input type="number" name="cancellation_fee" class="form-control"
|
||||||
step="0.01" min="0" value="{{ form.cancellation_fee.value|default:'0.00' }}">
|
step="0.01" min="0" value="{{ form.cancellation_fee.value|default:'0.00' }}">
|
||||||
</div>
|
</div>
|
||||||
@ -179,24 +208,48 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9 offset-md-3">
|
<div class="col-md-9 offset-md-3">
|
||||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to cancel this appointment? This action cannot be undone.')">
|
<button type="button" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#confirm-cancellation">
|
||||||
<i class="fa fa-times me-2"></i>Cancel Appointment
|
<i class="fa fa-times me-2"></i>Cancel Appointment
|
||||||
</button>
|
</button>
|
||||||
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
|
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-sm btn-secondary ms-2">
|
||||||
<i class="fa fa-arrow-left me-2"></i>Go Back
|
<i class="fa fa-arrow-left me-2"></i>Go Back
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="modal fade" id="confirm-cancellation" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-gradient-danger text-white">
|
||||||
|
<h5 class="modal-title" id="cancelModalLabel">Confirm Cancellation</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="mb-0">Are you sure you want to cancel this appointment? This action cannot be undone.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-danger" id="confirmDelete">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cancellation Policy -->
|
|
||||||
<div class="card mt-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="card-title">Cancellation Policy</h4>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="col-xl-4">
|
||||||
|
<!-- Cancellation Policy -->
|
||||||
|
<div class="panel panel-inverse mb-4" data-sortable-id="index-3">
|
||||||
|
<div class="panel-heading bg-gradient-danger">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-calendar-times"></i> Cancellation Policy
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
<li>Appointments cancelled with less than 24 hours notice may incur a cancellation fee</li>
|
<li>Appointments cancelled with less than 24 hours notice may incur a cancellation fee</li>
|
||||||
<li>Emergency cancellations are exempt from cancellation fees</li>
|
<li>Emergency cancellations are exempt from cancellation fees</li>
|
||||||
@ -209,7 +262,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
@ -220,13 +273,6 @@ $(document).ready(function() {
|
|||||||
var now = new Date();
|
var now = new Date();
|
||||||
var hoursNotice = (appointmentDate - now) / (1000 * 60 * 60);
|
var hoursNotice = (appointmentDate - now) / (1000 * 60 * 60);
|
||||||
|
|
||||||
// Auto-set cancellation fee based on policy
|
|
||||||
if (hoursNotice < 24 && hoursNotice > 0) {
|
|
||||||
var reason = $('select[name="cancellation_reason"]').val();
|
|
||||||
if (reason !== 'emergency' && reason !== 'provider_unavailable') {
|
|
||||||
$('input[name="cancellation_fee"]').val('25.00');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cancellation fee when reason changes
|
// Update cancellation fee when reason changes
|
||||||
$('select[name="cancellation_reason"]').change(function() {
|
$('select[name="cancellation_reason"]').change(function() {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
{% block title %}Check In Patient{% endblock %}
|
{% block title %}Check In Patient{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-10">
|
<div class="col-xl-10">
|
||||||
@ -307,7 +307,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
{% block title %}Confirm Appointment{% endblock %}
|
{% block title %}Confirm Appointment{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-8">
|
<div class="col-xl-8">
|
||||||
@ -251,7 +251,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -4,28 +4,18 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% 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 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">
|
<h1 class="h2">
|
||||||
<i class="fas fa-calendar-alt"></i> Appointment Dashboard
|
<i class="fas fa-calendar-alt"></i> Appointment<span class="fw-light">Dashboard</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<p class="text-muted">View your appointments, manage queues, and track your progress.</p>
|
||||||
<div class="btn-group me-2">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary">
|
|
||||||
<i class="fas fa-calendar-plus"></i> Schedule
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary">
|
|
||||||
<i class="fas fa-users"></i> Queue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-sm btn-primary">
|
|
||||||
<i class="fas fa-video"></i> Telemedicine
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Appointment Statistics -->
|
<!-- Appointment Statistics -->
|
||||||
<div id="appointment-stats"
|
<div id="appointment-stats"
|
||||||
hx-get="{% url 'appointments:appointment_stats' %}"
|
hx-get="{% url 'appointments:appointment_stats' %}"
|
||||||
hx-trigger="load, every 30s"
|
hx-trigger="load, every 60s"
|
||||||
class="auto-refresh mb-4">
|
class="auto-refresh mb-4">
|
||||||
<div class="htmx-indicator">
|
<div class="htmx-indicator">
|
||||||
<div class="spinner-border spinner-border-sm" role="status">
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
@ -43,11 +33,11 @@
|
|||||||
<i class="fas fa-calendar-day"></i> Today's Appointments
|
<i class="fas fa-calendar-day"></i> Today's Appointments
|
||||||
</h4>
|
</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
<a href="{% url 'appointments:appointment_list' %}" class="btn btn-xs btn-outline-primary me-2">View All</a>
|
<a href="{% url 'appointments:appointment_list' %}" class="btn btn-xs btn-outline-theme me-2">View All</a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@ -149,11 +139,11 @@
|
|||||||
<i class="fas fa-users"></i> Active Queues
|
<i class="fas fa-users"></i> Active Queues
|
||||||
</h4>
|
</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
<a href="{% url 'appointments:queue_management' %}" class="btn btn-xs btn-outline-primary me-2">View All</a>
|
<a href="{% url 'appointments:waiting_queue_list' %}" class="btn btn-xs btn-outline-theme me-2">View All</a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@ -174,14 +164,6 @@
|
|||||||
<br>No active queues.
|
<br>No active queues.
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if active_queues %}
|
|
||||||
<div class="d-grid">
|
|
||||||
<a href="{% url 'appointments:queue_management' %}" class="btn btn-outline-primary">
|
|
||||||
<i class="fas fa-cog"></i> Manage Queues
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -193,22 +175,31 @@
|
|||||||
<i class="fas fa-bolt"></i> Quick Actions
|
<i class="fas fa-bolt"></i> Quick Actions
|
||||||
</h4>
|
</h4>
|
||||||
<div class="panel-heading-btn">
|
<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-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<a href="{% url 'appointments:scheduling_calendar' %}" class="btn btn-outline-primary">
|
<a href="{% url 'appointments:calendar' %}" class="btn btn-outline-primary">
|
||||||
<i class="fas fa-calendar-plus"></i> Schedule Appointment
|
<i class="fas fa-calendar-plus"></i> Schedule Appointment
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'appointments:appointment_list' %}" class="btn btn-outline-secondary">
|
<a href="{% url 'appointments:appointment_list' %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-list"></i> View All Appointments
|
<i class="fas fa-list"></i> Appointments
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'appointments:telemedicine' %}" class="btn btn-outline-info">
|
<a href="{% url 'appointments:telemedicine' %}" class="btn btn-outline-info">
|
||||||
<i class="fas fa-video"></i> Telemedicine Sessions
|
<i class="fas fa-video"></i> Telemedicine
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'appointments:waiting_queue_list' %}" class="btn btn-outline-success">
|
||||||
|
<i class="fas fa-users"></i> Queues
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'appointments:calendar' %}" class="btn btn-outline-warning">
|
||||||
|
<i class="fas fa-calendar-days"></i> Calendar
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-danger">
|
||||||
|
<i class="fas fa-calendar-check"></i> Waiting List
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
{% block title %}Mark No Show{% endblock %}
|
{% block title %}Mark No Show{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-8">
|
<div class="col-xl-8">
|
||||||
@ -275,7 +275,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="fw-bold">{{ appointment.patient.get_full_name }}</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
{{ appointment.provider.get_full_name }} • {{ appointment.get_appointment_type_display }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 small">
|
||||||
|
<div><i class="far fa-clock me-1"></i>{{ appointment.scheduled_datetime|date:"M d, Y H:i" }}</div>
|
||||||
|
{% if appointment.chief_complaint %}
|
||||||
|
<div class="mt-1"><i class="fas fa-notes-medical me-1"></i>{{ appointment.chief_complaint }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if appointment.is_telemedicine %}
|
||||||
|
<div class="mt-1"><i class="fas fa-video me-1"></i>Telemedicine</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="badge
|
||||||
|
{% if appointment.status == 'PENDING' %}bg-warning
|
||||||
|
{% elif appointment.status == 'CONFIRMED' %}bg-info
|
||||||
|
{% elif appointment.status == 'CHECKED_IN' %}bg-primary
|
||||||
|
{% elif appointment.status == 'IN_PROGRESS' %}bg-success
|
||||||
|
{% elif appointment.status == 'COMPLETED' %}bg-success
|
||||||
|
{% elif appointment.status == 'CANCELLED' %}bg-danger
|
||||||
|
{% elif appointment.status == 'NO_SHOW' %}bg-secondary
|
||||||
|
{% endif %}">
|
||||||
|
{{ appointment.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'appointments:appointment_detail' appointment.pk %}" class="btn btn-sm btn-outline-primary">{{ _("View") }}</a>
|
||||||
|
{% if appointment.status in 'PENDING, CONFIRMED ,SCHEDULED' %}
|
||||||
|
<a href="{% url 'appointments:cancel_appointment' appointment.pk %}" class="btn btn-sm btn-outline-danger">{{ _("Cancel") }}</a>
|
||||||
|
<a href="{% url 'appointments:reschedule_appointment' appointment.pk %}" class="btn btn-sm btn-outline-warning">{{ _("Reschedule") }}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -1,84 +1,61 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-2 mb-3">
|
<div class="col-lg-2 col-sm-4">
|
||||||
<div class="card stat-card bg-primary text-white">
|
<div class="widget widget-stats bg-blue mb-7px">
|
||||||
<div class="card-body">
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-calendar-alt fa-fw"></i></div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="stats-content">
|
||||||
<div>
|
<div class="stats-title">Total Appointments</div>
|
||||||
<h4 class="card-title">{{ stats.total_appointments }}</h4>
|
<div class="stats-number">{{ stats.total_appointments }}</div>
|
||||||
<p class="card-text">Total Appointments</p>
|
<div class="stats-desc">Better than last week (40.5%)</div>
|
||||||
</div>
|
|
||||||
<i class="fas fa-calendar-alt fa-2x opacity-75"></i>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-lg-2 col-sm-4">
|
||||||
|
<div class="widget widget-stats bg-info mb-7px">
|
||||||
<div class="col-md-2 mb-3">
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-calendar-day fa-fw"></i></div>
|
||||||
<div class="card stat-card bg-info text-white">
|
<div class="stats-content">
|
||||||
<div class="card-body">
|
<div class="stats-title">Today's Appointments</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="stats-number">{{ stats.total_appointments_today }}</div>
|
||||||
<div>
|
<div class="stats-desc">Better than last week (40.5%)</div>
|
||||||
<h4 class="card-title">{{ stats.todays_appointments }}</h4>
|
|
||||||
<p class="card-text">Today's Appointments</p>
|
|
||||||
</div>
|
|
||||||
<i class="fas fa-calendar-day fa-2x opacity-75"></i>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-lg-2 col-sm-4">
|
||||||
|
<div class="widget widget-stats bg-warning mb-7px">
|
||||||
<div class="col-md-2 mb-3">
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-clock fa-fw"></i></div>
|
||||||
<div class="card stat-card bg-warning text-white">
|
<div class="stats-content">
|
||||||
<div class="card-body">
|
<div class="stats-title">Pending</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="stats-number">{{ stats.pending_appointments }}</div>
|
||||||
<div>
|
<div class="stats-desc">Better than last week (40.5%)</div>
|
||||||
<h4 class="card-title">{{ stats.pending_appointments }}</h4>
|
|
||||||
<p class="card-text">Pending</p>
|
|
||||||
</div>
|
|
||||||
<i class="fas fa-clock fa-2x opacity-75"></i>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-lg-2 col-sm-4">
|
||||||
|
<div class="widget widget-stats bg-success mb-7px">
|
||||||
<div class="col-md-2 mb-3">
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-check-circle fa-fw"></i></div>
|
||||||
<div class="card stat-card bg-success text-white">
|
<div class="stats-content">
|
||||||
<div class="card-body">
|
<div class="stats-title">Completed Today</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="stats-number">{{ stats.completed_appointments }}</div>
|
||||||
<div>
|
<div class="stats-desc">Better than last week (40.5%)</div>
|
||||||
<h4 class="card-title">{{ stats.completed_today }}</h4>
|
|
||||||
<p class="card-text">Completed Today</p>
|
|
||||||
</div>
|
|
||||||
<i class="fas fa-check-circle fa-2x opacity-75"></i>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-lg-2 col-sm-4">
|
||||||
|
<div class="widget widget-stats bg-danger mb-7px">
|
||||||
<div class="col-md-2 mb-3">
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-users fa-fw"></i></div>
|
||||||
<div class="card stat-card bg-secondary text-white">
|
<div class="stats-content">
|
||||||
<div class="card-body">
|
<div class="stats-title">Active Queue</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="stats-number">{{ stats.active_queues }}</div>
|
||||||
<div>
|
<div class="stats-desc">Better than last week (40.5%)</div>
|
||||||
<h4 class="card-title">{{ stats.total_in_queue }}</h4>
|
|
||||||
<p class="card-text">In Queue</p>
|
|
||||||
</div>
|
|
||||||
<i class="fas fa-users fa-2x opacity-75"></i>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-lg-2 col-sm-4">
|
||||||
|
<div class="widget widget-stats bg-dark mb-7px">
|
||||||
<div class="col-md-2 mb-3">
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-video fa-fw"></i></div>
|
||||||
<div class="card stat-card bg-dark text-white">
|
<div class="stats-content">
|
||||||
<div class="card-body">
|
<div class="stats-title">Telemedicine</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="stats-number">{{ stats.telemedicine_sessions }}</div>
|
||||||
<div>
|
<div class="stats-desc">Better than last week (40.5%)</div>
|
||||||
<h4 class="card-title">{{ stats.telemedicine_today }}</h4>
|
|
||||||
<p class="card-text">Telemedicine</p>
|
|
||||||
</div>
|
|
||||||
<i class="fas fa-video fa-2x opacity-75"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,34 +1,45 @@
|
|||||||
|
<div class="accordion" id="accordion">
|
||||||
{% for log in contact_logs %}
|
{% for log in contact_logs %}
|
||||||
<div class="contact-log-item">
|
<div class="accordion-item border-0">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
<div class="accordion-header mb-2" id="heading-{{ log.id }}">
|
||||||
<h6 class="mb-0">
|
<button class="accordion-button bg-gray-900 text-white px-3 py-1 pointer-cursor" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-{{ log.id }}">
|
||||||
<i class="fas fa-{{ log.contact_method|lower }} me-1"></i>
|
{{ log.contact_date|date:"M d, Y H:i" }}
|
||||||
{{ log.get_contact_method_display }} -
|
</button>
|
||||||
<span class="badge bg-{% if log.contact_outcome == 'SUCCESSFUL' %}success{% elif log.contact_outcome == 'DECLINED' %}danger{% else %}info{% endif %}">
|
</div>
|
||||||
|
<div id="collapse-{{ log.id }}" class="accordion-collapse collapse show" data-bs-parent="#accordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<i class="fas fa-{{ log.contact_method|capfirst }}"></i>{{ log.get_contact_method_display }} -
|
||||||
|
<span class="text-{% if log.contact_outcome == 'SUCCESSFUL' %}success{% elif log.contact_outcome == 'DECLINED' %}danger{% else %}info{% endif %}">
|
||||||
{{ log.get_contact_outcome_display }}
|
{{ log.get_contact_outcome_display }}
|
||||||
</span>
|
</span>
|
||||||
</h6>
|
|
||||||
<small class="text-muted">{{ log.contact_date|date:"M d, Y H:i" }}</small>
|
|
||||||
</div>
|
|
||||||
<p class="mb-1">{{ log.notes|default:"No notes." }}</p>
|
|
||||||
{% if log.appointment_offered %}
|
{% if log.appointment_offered %}
|
||||||
<p class="mb-1 text-primary">
|
<div class="row mb-1">
|
||||||
<i class="fas fa-calendar-check me-1"></i>Appointment Offered:
|
<div class="col-md-8">
|
||||||
|
<p class="fw-light">
|
||||||
|
<i class="fas fa-calendar-check"></i>Appointment Offered:
|
||||||
{{ log.offered_date|date:"M d, Y" }} at {{ log.offered_time|time:"g:i A" }}
|
{{ log.offered_date|date:"M d, Y" }} at {{ log.offered_time|time:"g:i A" }}
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-0 text-primary">
|
</div>
|
||||||
<i class="fas fa-reply me-1"></i>Patient Response:
|
</div>
|
||||||
<span class="badge bg-{% if log.patient_response == 'ACCEPTED' %}success{% elif log.patient_response == 'DECLINED' %}danger{% else %}secondary{% endif %}">
|
|
||||||
{{ log.get_patient_response_display }}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="row mb-1">
|
||||||
|
<div class="col-md-6">
|
||||||
{% if log.next_contact_date %}
|
{% if log.next_contact_date %}
|
||||||
<p class="mb-0 text-info">
|
<small class="text-muted">
|
||||||
<i class="fas fa-calendar-alt me-1"></i>Next Contact: {{ log.next_contact_date|date:"M d, Y" }}
|
<span class="fw-bold">Contacted by:</span> {{ log.contacted_by.get_full_name }}
|
||||||
</p>
|
</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<small class="text-muted">Contacted by: {{ log.contacted_by.get_full_name|default:"N/A" }}</small>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% if log.next_contact_date %}
|
||||||
|
<small class="text-muted">
|
||||||
|
<span class="fw-bold">Next Contact:</span> {{ log.next_contact_date|date:"M d, Y" }}
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="text-center text-muted py-3">
|
<div class="text-center text-muted py-3">
|
||||||
@ -36,4 +47,4 @@
|
|||||||
<p class="mb-0">No contact logs available for this entry.</p>
|
<p class="mb-0">No contact logs available for this entry.</p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
@ -183,7 +183,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
@ -367,7 +366,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
|||||||
@ -156,7 +156,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wait-time-number {
|
.wait-time-number {
|
||||||
font-size: 2.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
}
|
}
|
||||||
@ -200,22 +200,16 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:queue_entry_list' %}">Queue Entries</a></li>
|
|
||||||
<li class="breadcrumb-item active">{{ entry.patient.get_full_name }}</li>
|
|
||||||
</ol>
|
|
||||||
<h1 class="page-header mb-0">
|
<h1 class="page-header mb-0">
|
||||||
<i class="fas fa-user-clock me-2"></i>Queue Entry Details
|
<i class="fas fa-user-clock me-2"></i>Queue Entry Details
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
<a href="{% url 'appointments:queue_entry_list' %}?queue={{ entry.queue.pk }}" class="btn btn-outline-secondary">
|
<a href="{% url 'appointments:waiting_queue_detail' entry.queue.pk %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-arrow-left me-1"></i>Back to Queue
|
<i class="fas fa-arrow-left me-1"></i>Back to Queue
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,9 +3,8 @@
|
|||||||
|
|
||||||
{% block title %}{% if form.instance.pk %}Edit{% else %}Add to{% endif %} Queue Entry{% endblock %}
|
{% block title %}{% if form.instance.pk %}Edit{% else %}Add to{% endif %} Queue Entry{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block css %}
|
||||||
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'assets/plugins/select2-bootstrap5-theme/dist/select2-bootstrap-5-theme.min.css' %}" rel="stylesheet" />
|
|
||||||
<style>
|
<style>
|
||||||
.form-section {
|
.form-section {
|
||||||
background: white;
|
background: white;
|
||||||
@ -151,25 +150,17 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:queueentry_list' %}">Queue Entries</a></li>
|
|
||||||
<li class="breadcrumb-item active">
|
|
||||||
{% if form.instance.pk %}Edit Entry{% else %}Add to Queue{% endif %}
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<h1 class="page-header mb-0">
|
<h1 class="page-header mb-0">
|
||||||
<i class="fas fa-user-plus me-2"></i>
|
<i class="fas fa-user-plus me-2"></i>
|
||||||
{% if form.instance.pk %}Edit Queue Entry{% else %}Add Patient to Queue{% endif %}
|
{% if form.instance.pk %}Edit Queue Entry{% else %}Add Patient to Queue{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
<a href="{% url 'appointments:queueentry_list' %}" class="btn btn-outline-secondary">
|
<a href="{% url 'appointments:queue_entry_list' %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -465,7 +456,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'appointments:queueentry_list' %}" class="btn btn-outline-secondary me-2">
|
<a href="{% url 'appointments:queue_entry_list' %}" class="btn btn-outline-secondary me-2">
|
||||||
<i class="fas fa-times me-1"></i>Cancel
|
<i class="fas fa-times me-1"></i>Cancel
|
||||||
</a>
|
</a>
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
@ -476,11 +467,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block js %}
|
||||||
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|||||||
@ -130,27 +130,24 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<div>
|
<div>
|
||||||
<ol class="breadcrumb">
|
<h1 class="h2">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
<i class="fas fa-trash me-2"></i>Delete<span class="fw-light">Queue</span>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_list' %}">Waiting Queues</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_detail' queue.pk %}">{{ queue.name }}</a></li>
|
|
||||||
<li class="breadcrumb-item active">Delete</li>
|
|
||||||
</ol>
|
|
||||||
<h1 class="page-header mb-0">
|
|
||||||
<i class="fas fa-trash me-2"></i>Delete Waiting Queue
|
|
||||||
</h1>
|
</h1>
|
||||||
|
<p class="text-muted">View your appointments, manage queues, and track your progress.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
<a href="{% url 'appointments:waiting_queue_detail' queue.pk %}" class="btn btn-outline-secondary">
|
<a href="{% url 'appointments:waiting_queue_detail' queue.pk %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-arrow-left me-1"></i>Back to Queue
|
<i class="fas fa-arrow-left me-1"></i>Back to Queue
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
<!-- Delete Warning -->
|
<!-- Delete Warning -->
|
||||||
<div class="delete-warning text-center">
|
<div class="delete-warning text-center">
|
||||||
|
|||||||
@ -6,334 +6,186 @@
|
|||||||
{% block css %}
|
{% block css %}
|
||||||
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||||
|
<link href="{% static 'plugins/sweetalert2/css/sweetalert2.all.min.css' %}" rel="stylesheet" />
|
||||||
<style>
|
<style>
|
||||||
.queue-header {
|
/* The modal window itself */
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
.swal2-popup {
|
||||||
color: white;
|
border-radius: 1.5rem;
|
||||||
border-radius: 0.5rem;
|
background: #f8f9fa;
|
||||||
padding: 2rem;
|
font-family: 'Tajawal', sans-serif; /* Example: Arabic-friendly font */
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
/* Title */
|
||||||
display: grid;
|
.swal2-title {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
font-size: 20px;
|
||||||
gap: 1.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
text-align: center;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card:hover {
|
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 auto 1rem;
|
|
||||||
color: white;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.primary { background: #007bff; }
|
|
||||||
.stat-icon.success { background: #28a745; }
|
|
||||||
.stat-icon.warning { background: #ffc107; }
|
|
||||||
.stat-icon.info { background: #17a2b8; }
|
|
||||||
.stat-icon.danger { background: #dc3545; }
|
|
||||||
|
|
||||||
.stat-number {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #495057;
|
{#color: #0b505d;#}
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
/* Confirm button */
|
||||||
color: #6c757d;
|
.swal2-confirm {
|
||||||
font-size: 0.875rem;
|
background-color: #155724 !important;
|
||||||
font-weight: 600;
|
color: #fff !important;
|
||||||
text-transform: uppercase;
|
border-radius: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-info-card {
|
/* Cancel button */
|
||||||
background: white;
|
.swal2-cancel {
|
||||||
border: 1px solid #dee2e6;
|
background-color: #adb5bd !important;
|
||||||
border-radius: 0.5rem;
|
color: #fff !important;
|
||||||
padding: 1.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item {
|
/* Icon color override */
|
||||||
display: flex;
|
.swal2-icon.swal2-warning {
|
||||||
justify-content: space-between;
|
border-color: #f59c1a !important;
|
||||||
align-items: center;
|
color: #f59c1a !important;
|
||||||
padding: 0.75rem 0;
|
|
||||||
border-bottom: 1px solid #f8f9fa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #495057;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-type-badge {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-provider { background: #d4edda; color: #155724; }
|
|
||||||
.type-specialty { background: #d1ecf1; color: #0c5460; }
|
|
||||||
.type-location { background: #fff3cd; color: #856404; }
|
|
||||||
.type-procedure { background: #f8d7da; color: #721c24; }
|
|
||||||
.type-emergency { background: #f5c6cb; color: #721c24; }
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-active { background: #d4edda; color: #155724; }
|
|
||||||
.status-inactive { background: #f8d7da; color: #721c24; }
|
|
||||||
|
|
||||||
.entry-status {
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-waiting { background: #fff3cd; color: #856404; }
|
|
||||||
.status-called { background: #d1ecf1; color: #0c5460; }
|
|
||||||
.status-in-service { background: #cce5ff; color: #004085; }
|
|
||||||
.status-completed { background: #d4edda; color: #155724; }
|
|
||||||
.status-left { background: #f8d7da; color: #721c24; }
|
|
||||||
.status-no-show { background: #f5c6cb; color: #721c24; }
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-number {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<div>
|
<div>
|
||||||
<ol class="breadcrumb">
|
<h1 class="h2">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
<i class="fas fa-users me-2"></i> Queue<span class="fw-light">Details</span>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_list' %}">Waiting Queues</a></li>
|
|
||||||
<li class="breadcrumb-item active">{{ queue.name }}</li>
|
|
||||||
</ol>
|
|
||||||
<h1 class="page-header mb-0">
|
|
||||||
<i class="fas fa-users me-2"></i>Queue Details
|
|
||||||
</h1>
|
</h1>
|
||||||
|
<p class="text-muted">View your appointments, manage queues, and track your progress.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<div class="btn-group">
|
<div class="btn-group me-2">
|
||||||
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}" class="btn btn-warning">
|
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}" class="btn btn-sm btn-warning">
|
||||||
<i class="fas fa-edit me-1"></i>Edit Queue
|
<i class="fas fa-edit me-1"></i>Edit Queue
|
||||||
</a>
|
</a>
|
||||||
<button class="btn btn-success" onclick="refreshQueue()">
|
<button class="btn btn-sm btn-success" onclick="refreshQueue()">
|
||||||
<i class="fas fa-sync-alt me-1"></i>Refresh
|
<i class="fas fa-sync-alt me-1"></i>Refresh
|
||||||
</button>
|
</button>
|
||||||
<div class="btn-group">
|
<button class="btn btn-sm btn-danger" onclick="exportQueue()">
|
||||||
<button class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown">
|
|
||||||
<i class="fas fa-cog me-1"></i>Actions
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li><a class="dropdown-item" href="#" onclick="callNextPatient()">
|
|
||||||
<i class="fas fa-phone me-2"></i>Call Next Patient
|
|
||||||
</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" onclick="pauseQueue()">
|
|
||||||
<i class="fas fa-pause me-2"></i>Pause Queue
|
|
||||||
</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" onclick="clearQueue()">
|
|
||||||
<i class="fas fa-broom me-2"></i>Clear Queue
|
|
||||||
</a></li>
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportQueue()">
|
|
||||||
<i class="fas fa-download me-2"></i>Export Data
|
<i class="fas fa-download me-2"></i>Export Data
|
||||||
</a></li>
|
</button>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
<!-- Queue Header -->
|
<!-- Queue Header -->
|
||||||
<div class="queue-header">
|
<div class="card mb-3">
|
||||||
<div class="row align-items-center">
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<h2 class="mb-2">{{ queue.name }}</h2>
|
<div class="align-items-center g-3">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<h4 class="fw-bold me-2">{{ queue.name }}</h4>
|
||||||
<span class="queue-type-badge type-{{ queue.queue_type|lower }} me-3">
|
{% if queue.queue_type == 'PROVIDER' %}
|
||||||
|
<span class="badge bg-success fw-bold me-2">
|
||||||
|
{% elif queue.queue_type == 'SPECIALTY' %}
|
||||||
|
<span class="badge bg-purple fw-bold me-2">
|
||||||
|
{% elif queue.queue_type == 'LOCATION' %}
|
||||||
|
<span class="badge bg-warning fw-bold me-2">
|
||||||
|
{% elif queue.queue_type == 'PROCEDURE' %}
|
||||||
|
<span class="badge bg-info fw-bold me-2">
|
||||||
|
{% elif queue.queue_type == 'EMERGENCY' %}
|
||||||
|
<span class="badge bg-danger fw-bold me-2">
|
||||||
|
{% endif %}
|
||||||
{{ queue.get_queue_type_display }}
|
{{ queue.get_queue_type_display }}
|
||||||
</span>
|
</span>
|
||||||
<span class="status-badge status-{% if queue.is_active %}active{% else %}inactive{% endif %}">
|
<span class="badge bg-{% if queue.is_active %}success{% else %}danger{% endif %} me-2">
|
||||||
{% if queue.is_active %}Active{% else %}Inactive{% endif %}
|
{% if queue.is_active %}{{ _("Active") }}{% else %}{{ _("Inactive") }}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% if queue.description %}
|
|
||||||
<p class="mb-0 opacity-75">{{ queue.description }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-md-end">
|
<div class="col-md-4 text-md-end">
|
||||||
<div class="text-white">
|
<div class="align-items-center g-3">
|
||||||
<div class="h4 mb-1">{{ queue.current_queue_size }}/{{ queue.max_queue_size }}</div>
|
<div class="h4 mb-1">{{ queue.current_queue_size }}/{{ queue.max_queue_size }}</div>
|
||||||
<div class="opacity-75">Current Capacity</div>
|
<div class="opacity-75">Current Capacity</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statistics -->
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon warning">
|
|
||||||
<i class="fas fa-user-clock"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-number">{{ queue.current_queue_size }}</div>
|
|
||||||
<div class="stat-label">Patients Waiting</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon info">
|
|
||||||
<i class="fas fa-clock"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-number">{{ queue.estimated_wait_time_minutes }}</div>
|
|
||||||
<div class="stat-label">Est. Wait Time (min)</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon success">
|
|
||||||
<i class="fas fa-user-check"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-number">{{ stats.served_today|default:0 }}</div>
|
|
||||||
<div class="stat-label">Served Today</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon primary">
|
|
||||||
<i class="fas fa-chart-line"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-number">{{ queue.average_service_time_minutes }}</div>
|
|
||||||
<div class="stat-label">Avg Service (min)</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon danger">
|
|
||||||
<i class="fas fa-user-times"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-number">{{ stats.no_shows_today|default:0 }}</div>
|
|
||||||
<div class="stat-label">No Shows Today</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Queue Information -->
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="queue-info-card">
|
<!-- Queue Information -->
|
||||||
<h5 class="mb-3">
|
<div class="panel panel-inverse mb-4" data-sortable-id="index-1">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-info-circle me-2"></i>Queue Information
|
<i class="fas fa-info-circle me-2"></i>Queue Information
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
|
||||||
<div class="info-item">
|
<div class="row mb-1">
|
||||||
<span class="info-label">Queue ID:</span>
|
<div class="col-4 fw-bold">Queue ID:</div>
|
||||||
<span class="info-value">{{ queue.queue_id }}</span>
|
<div class="col-8">{{ queue.queue_id }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-item">
|
<div class="row mb-1">
|
||||||
<span class="info-label">Type:</span>
|
<div class="col-4 fw-bold">Type:</div>
|
||||||
<span class="info-value">{{ queue.get_queue_type_display }}</span>
|
<div class="col-8">{{ queue.get_queue_type_display }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if queue.specialty %}
|
{% if queue.specialty %}
|
||||||
<div class="info-item">
|
<div class="row mb-1">
|
||||||
<span class="info-label">Specialty:</span>
|
<div class="col-4 fw-bold">Specialty:</div>
|
||||||
<span class="info-value">{{ queue.specialty }}</span>
|
<div class="col-8">{{ queue.specialty }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if queue.location %}
|
{% if queue.location %}
|
||||||
<div class="info-item">
|
<div class="row mb-1">
|
||||||
<span class="info-label">Location:</span>
|
<div class="col-4 fw-bold">Location:</div>
|
||||||
<span class="info-value">{{ queue.location }}</span>
|
<div class="col-8">{{ queue.location }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="info-item">
|
<div class="row mb-1">
|
||||||
<span class="info-label">Max Capacity:</span>
|
<div class="col-4 fw-bold">Max Capacity:</div>
|
||||||
<span class="info-value">{{ queue.max_queue_size }} patients</span>
|
<div class="col-8">{{ queue.max_queue_size }} patients</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-item">
|
<div class="row mb-1">
|
||||||
<span class="info-label">Accepting Patients:</span>
|
<div class="col-4 fw-bold">Accepting Patients:</div>
|
||||||
<span class="info-value">
|
<div class="col-8">
|
||||||
{% if queue.is_accepting_patients %}
|
{% if queue.is_accepting_patients %}
|
||||||
<i class="fas fa-check text-success"></i> Yes
|
<i class="fas fa-check text-success"></i> Yes
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="fas fa-times text-danger"></i> No
|
<i class="fas fa-times text-danger"></i> No
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">Created:</span>
|
|
||||||
<span class="info-value">{{ queue.created_at|date:"M d, Y g:i A" }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">Last Updated:</span>
|
|
||||||
<span class="info-value">{{ queue.updated_at|timesince }} ago</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-1">
|
||||||
|
<div class="col-4 fw-bold">Created:</div>
|
||||||
|
<div class="col-8">{{ queue.created_at|date:"M d, Y g:i A" }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-1">
|
||||||
|
<div class="col-4 fw-bold">Last Updated:</div>
|
||||||
|
<div class="col-8">{{ queue.updated_at|timesince }} ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Assigned Providers -->
|
<!-- Assigned Providers -->
|
||||||
<div class="queue-info-card">
|
<div class="panel panel-inverse mb-4" data-sortable-id="index-2">
|
||||||
<h5 class="mb-3">
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-user-md me-2"></i>Assigned Providers
|
<i class="fas fa-user-md me-2"></i>Assigned Providers
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
{% for provider in queue.providers.all %}
|
{% for provider in queue.providers.all %}
|
||||||
<div class="d-flex align-items-center mb-2">
|
<div class="d-flex align-items-center mb-2">
|
||||||
<div class="avatar avatar-sm me-2">
|
<div class="avatar avatar-sm me-2">
|
||||||
@ -349,24 +201,35 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<!-- Current Queue -->
|
<!-- Current Queue -->
|
||||||
<div class="card">
|
<div class="panel panel-inverse mb-4" data-sortable-id="index-3">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-list me-2"></i>Current Queue
|
<i class="fas fa-list me-2"></i>Current Queue
|
||||||
</h5>
|
</h4>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="panel-heading-btn">
|
||||||
<button class="btn btn-outline-primary" onclick="callNextPatient()">
|
<a href="{% url 'appointments:queue_entry_create' %}" class="btn btn-xs btn-outline-success me-2">
|
||||||
<i class="fas fa-phone me-1"></i>Call Next
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-success" onclick="addPatientToQueue()">
|
|
||||||
<i class="fas fa-plus me-1"></i>Add Patient
|
<i class="fas fa-plus me-1"></i>Add Patient
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-xs btn-outline-warning me-2" onclick="pauseQueue()">
|
||||||
|
<i class="fas fa-pause me-2"></i>Pause Queue
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-xs btn-outline-danger me-2" onclick="clearQueue()">
|
||||||
|
<i class="fas fa-broom me-2"></i>Clear Queue
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'appointments:call_next_patient' queue.id %}" class="btn btn-xs btn-outline-primary me-2">
|
||||||
|
<i class="fas fa-phone me-1"></i>Call Next
|
||||||
|
</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>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table id="queueTable" class="table table-striped table-bordered align-middle">
|
<table id="queueTable" class="table table-striped table-bordered align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
@ -393,7 +256,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="fw-bold">{{ entry.patient.get_full_name }}</div>
|
<div class="fw-bold">{{ entry.patient.get_full_name }}</div>
|
||||||
<small class="text-muted">ID: {{ entry.patient.patient_id }}</small>
|
<small class="text-muted">MRN: {{ entry.patient.mrn }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -409,7 +272,19 @@
|
|||||||
<span class="fw-bold">{{ entry.joined_at|timesince }}</span>
|
<span class="fw-bold">{{ entry.joined_at|timesince }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="entry-status status-{{ entry.status|lower }}">
|
{% if entry.status == 'WAITING' %}
|
||||||
|
<span class="badge bg-warning">
|
||||||
|
{% elif entry.status == 'CALLED' %}
|
||||||
|
<span class="badge bg-info">
|
||||||
|
{% elif entry.status == 'IN_SERVICE' %}
|
||||||
|
<span class="badge bg-primary">
|
||||||
|
{% elif entry.status == 'COMPLETED' %}
|
||||||
|
<span class="badge bg-success">
|
||||||
|
{% elif entry.status == 'LEFT' %}
|
||||||
|
<span class="badge bg-cyan">
|
||||||
|
{% elif entry.status == 'NO_SHOW' %}
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
{% endif %}
|
||||||
{{ entry.get_status_display }}
|
{{ entry.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@ -450,11 +325,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block js %}
|
||||||
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
||||||
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||||
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||||
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||||
|
<script src="{% static 'plugins/sweetalert2/js/sweetalert2.all.min.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
@ -462,48 +338,69 @@ $(document).ready(function() {
|
|||||||
$('#queueTable').DataTable({
|
$('#queueTable').DataTable({
|
||||||
responsive: true,
|
responsive: true,
|
||||||
pageLength: 25,
|
pageLength: 25,
|
||||||
order: [[0, 'asc']], // Sort by position
|
order: [[0, 'asc']],
|
||||||
columnDefs: [
|
|
||||||
{ orderable: false, targets: [6] } // Disable sorting for actions
|
|
||||||
],
|
|
||||||
language: {
|
|
||||||
search: "",
|
|
||||||
searchPlaceholder: "Search queue entries...",
|
|
||||||
lengthMenu: "Show _MENU_ entries per page",
|
|
||||||
info: "Showing _START_ to _END_ of _TOTAL_ entries",
|
|
||||||
infoEmpty: "No entries in queue",
|
|
||||||
infoFiltered: "(filtered from _MAX_ total entries)"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-refresh every 30 seconds
|
// Auto-refresh every 60 seconds
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
refreshQueue();
|
refreshQueue();
|
||||||
}, 30000);
|
}, 60000);
|
||||||
});
|
});
|
||||||
|
|
||||||
function refreshQueue() {
|
function refreshQueue() {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function callNextPatient() {
|
function getCookie(name) {
|
||||||
if (confirm('Call the next patient in queue?')) {
|
let cookieValue = null;
|
||||||
$.post('{% url "appointments:call_next_patient" queue.pk %}', {
|
if (document.cookie && document.cookie !== '') {
|
||||||
csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()
|
const cookies = document.cookie.split(';');
|
||||||
}).done(function(response) {
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
if (response.success) {
|
const cookie = cookies[i].trim();
|
||||||
showAlert('success', 'Next patient called successfully');
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
setTimeout(function() {
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
location.reload();
|
break;
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
showAlert('error', response.message || 'Failed to call next patient');
|
|
||||||
}
|
|
||||||
}).fail(function() {
|
|
||||||
showAlert('error', 'Failed to call next patient');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
{#function callNextPatient(queueId) {#}
|
||||||
|
{# Swal.fire({#}
|
||||||
|
{# title: "Call the next patient?",#}
|
||||||
|
{# text: "This will notify the next patient in the queue.",#}
|
||||||
|
{# icon: "warning",#}
|
||||||
|
{# showCancelButton: true,#}
|
||||||
|
{# confirmButtonText: "Yes",#}
|
||||||
|
{# cancelButtonText: "Cancel"#}
|
||||||
|
{# }).then((result) => {#}
|
||||||
|
{# if (result.isConfirmed) {#}
|
||||||
|
{# $.ajax({#}
|
||||||
|
{# url: "{% url 'appointments:call_next_patient' 0 %}".replace("0", queueId),#}
|
||||||
|
{# type: "POST",#}
|
||||||
|
{# headers: { "X-CSRFToken": getCookie("csrftoken") },#}
|
||||||
|
{# success: function(response) {#}
|
||||||
|
{# if (response.success) {#}
|
||||||
|
{# Swal.fire({#}
|
||||||
|
{# icon: "success",#}
|
||||||
|
{# title: "Next patient called",#}
|
||||||
|
{# showConfirmButton: false,#}
|
||||||
|
{# timer: 1200#}
|
||||||
|
{# }).then(() => {#}
|
||||||
|
{# location.reload();#}
|
||||||
|
{# });#}
|
||||||
|
{# } else {#}
|
||||||
|
{# Swal.fire("Error", response.message || "Failed to call next patient", "error");#}
|
||||||
|
{# }#}
|
||||||
|
{# },#}
|
||||||
|
{# error: function() {#}
|
||||||
|
{# Swal.fire("Error", "Failed to call next patient", "error");#}
|
||||||
|
{# }#}
|
||||||
|
{# });#}
|
||||||
|
{# }#}
|
||||||
|
{# });#}
|
||||||
|
{#}#}
|
||||||
|
|
||||||
{#function callPatient(entryId) {#}
|
{#function callPatient(entryId) {#}
|
||||||
{# if (confirm('Call this patient?')) {#}
|
{# if (confirm('Call this patient?')) {#}
|
||||||
@ -545,11 +442,6 @@ function callNextPatient() {
|
|||||||
{# }#}
|
{# }#}
|
||||||
{# }#}
|
{# }#}
|
||||||
|
|
||||||
{#function addPatientToQueue() {#}
|
|
||||||
{# // Redirect to add patient form#}
|
|
||||||
{# window.location.href = '{% url "appointments:add_to_queue" queue.pk %}';#}
|
|
||||||
{# }#}
|
|
||||||
|
|
||||||
{#function pauseQueue() {#}
|
{#function pauseQueue() {#}
|
||||||
{# if (confirm('Pause this queue? No new patients will be accepted.')) {#}
|
{# if (confirm('Pause this queue? No new patients will be accepted.')) {#}
|
||||||
{# $.post('{% url "appointments:pause_queue" queue.pk %}', {#}
|
{# $.post('{% url "appointments:pause_queue" queue.pk %}', {#}
|
||||||
@ -592,26 +484,6 @@ function callNextPatient() {
|
|||||||
{# window.location.href = '{% url "appointments:export_queue" queue.pk %}';#}
|
{# window.location.href = '{% url "appointments:export_queue" queue.pk %}';#}
|
||||||
{# }#}
|
{# }#}
|
||||||
|
|
||||||
function showAlert(type, message) {
|
|
||||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
|
||||||
const alertHtml = `
|
|
||||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
|
||||||
${message}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Remove existing alerts
|
|
||||||
$('.alert').remove();
|
|
||||||
|
|
||||||
// Add new alert at the top of content
|
|
||||||
$('#content').prepend(alertHtml);
|
|
||||||
|
|
||||||
// Auto-dismiss after 5 seconds
|
|
||||||
setTimeout(function() {
|
|
||||||
$('.alert').fadeOut();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -123,7 +123,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
@ -464,7 +464,7 @@
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Initialize Select2
|
// Initialize Select2
|
||||||
$('#id_providers').select2({
|
$('.form-select').select2({
|
||||||
theme: 'bootstrap-5',
|
theme: 'bootstrap-5',
|
||||||
placeholder: 'Select providers...',
|
placeholder: 'Select providers...',
|
||||||
allowClear: true
|
allowClear: true
|
||||||
|
|||||||
@ -7,33 +7,6 @@
|
|||||||
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||||
<style>
|
<style>
|
||||||
.queue-card {
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-card:hover {
|
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 0.5rem 0.5rem 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
@ -46,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 0.75rem;
|
font-size: 0.5rem;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
@ -85,148 +58,23 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<div>
|
<div>
|
||||||
<ol class="breadcrumb">
|
<h1 class="h2">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
<i class="fas fa-calendar-alt"></i> Queue<span class="fw-light">Management</span>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
|
||||||
<li class="breadcrumb-item active">Waiting Queues</li>
|
|
||||||
</ol>
|
|
||||||
<h1 class="page-header mb-0">
|
|
||||||
<i class="fas fa-users me-2"></i>Waiting Queues Management
|
|
||||||
</h1>
|
</h1>
|
||||||
|
<p class="text-muted">Manage queues and track patient's journey.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
<a href="{% url 'appointments:waiting_queue_create' %}" class="btn btn-primary">
|
<a href="{% url 'appointments:waiting_queue_create' %}" class="btn btn-primary">
|
||||||
<i class="fas fa-plus me-1"></i>Create Queue
|
<i class="fas fa-plus me-1"></i>Create Queue
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statistics Overview -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-lg-3 col-md-6">
|
|
||||||
<div class="card bg-primary text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<h4 class="mb-0">{{ stats.total_queues|default:0 }}</h4>
|
|
||||||
<p class="mb-0">Total Queues</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-3">
|
<div class="container-fluid">
|
||||||
<i class="fas fa-list fa-2x"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6">
|
|
||||||
<div class="card bg-success text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<h4 class="mb-0">{{ stats.active_queues|default:0 }}</h4>
|
|
||||||
<p class="mb-0">Active Queues</p>
|
|
||||||
</div>
|
|
||||||
<div class="ms-3">
|
|
||||||
<i class="fas fa-check-circle fa-2x"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6">
|
|
||||||
<div class="card bg-warning text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<h4 class="mb-0">{{ stats.total_patients|default:0 }}</h4>
|
|
||||||
<p class="mb-0">Patients Waiting</p>
|
|
||||||
</div>
|
|
||||||
<div class="ms-3">
|
|
||||||
<i class="fas fa-user-clock fa-2x"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6">
|
|
||||||
<div class="card bg-info text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<h4 class="mb-0">{{ stats.avg_wait_time|default:"0" }} min</h4>
|
|
||||||
<p class="mb-0">Avg Wait Time</p>
|
|
||||||
</div>
|
|
||||||
<div class="ms-3">
|
|
||||||
<i class="fas fa-clock fa-2x"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filters -->
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Queue Type</label>
|
|
||||||
<select class="form-select" id="type-filter">
|
|
||||||
<option value="">All Types</option>
|
|
||||||
<option value="PROVIDER">Provider Queue</option>
|
|
||||||
<option value="SPECIALTY">Specialty Queue</option>
|
|
||||||
<option value="LOCATION">Location Queue</option>
|
|
||||||
<option value="PROCEDURE">Procedure Queue</option>
|
|
||||||
<option value="EMERGENCY">Emergency Queue</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Status</label>
|
|
||||||
<select class="form-select" id="status-filter">
|
|
||||||
<option value="">All Statuses</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="inactive">Inactive</option>
|
|
||||||
<option value="full">Full</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Specialty</label>
|
|
||||||
<select class="form-select" id="specialty-filter">
|
|
||||||
<option value="">All Specialties</option>
|
|
||||||
{% for specialty in specialties %}
|
|
||||||
<option value="{{ specialty }}">{{ specialty }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 d-flex align-items-end">
|
|
||||||
<button class="btn btn-outline-primary me-2" onclick="applyFilters()">
|
|
||||||
<i class="fas fa-filter me-1"></i>Apply
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" onclick="clearFilters()">
|
|
||||||
<i class="fas fa-times me-1"></i>Clear
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- View Toggle -->
|
|
||||||
<div class="d-flex justify-content-end mb-2">
|
|
||||||
<button class="btn btn-primary btn-sm me-2" id="card-view-btn" onclick="toggleView('cards')">
|
|
||||||
<i class="fas fa-th-large"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-primary btn-sm" id="table-view-btn" onclick="toggleView('table')">
|
|
||||||
<i class="fas fa-table"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Queue Cards View -->
|
|
||||||
<div class="row" id="queue-cards">
|
<div class="row" id="queue-cards">
|
||||||
{% for queue in queues %}
|
{% for queue in queues %}
|
||||||
<div class="col-lg-6 col-xl-4 "
|
<div class="col-lg-6 col-xl-4 "
|
||||||
@ -254,15 +102,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="panel-heading-btn">
|
<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-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="queue-stats">
|
<div class="d-flex justify-content-between align-items-center p-3">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<div class="stat-number">{{ queue.current_queue_size }}</div>
|
<div class="stat-number">{{ queue.current_queue_size }}</div>
|
||||||
<div class="stat-label">Waiting</div>
|
<div class="stat-label">Waiting</div>
|
||||||
@ -298,30 +146,30 @@
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<small class="text-muted">Providers:</small>
|
<small class="text-muted">Providers:</small>
|
||||||
<span class="fw-bold">{{ queue.providers.count }}</span>
|
<span class="fw-bold">{{ queue.providers.all|length}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
Updated: {{ queue.updated_at|timesince }} ago
|
{{ _("Updated") }}: {{ queue.updated_at|timesince }} {{ _("ago") }}
|
||||||
</small>
|
</small>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<a href="{% url 'appointments:waiting_queue_detail' queue.pk %}"
|
<a href="{% url 'appointments:waiting_queue_detail' queue.pk %}"
|
||||||
class="btn btn-outline-primary" title="View Details">
|
class="btn btn-outline-primary" title="{{ _("View Details")}}">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'appointments:queue_entry_list' %}?queue={{ queue.pk }}"
|
{# <a href="{% url 'appointments:queue_entry_list' %}?queue={{ queue.pk }}"#}
|
||||||
class="btn btn-outline-info" title="View Entries">
|
{# class="btn btn-outline-info" title="View Entries">#}
|
||||||
<i class="fas fa-list"></i>
|
{# <i class="fas fa-list"></i>#}
|
||||||
</a>
|
{# </a>#}
|
||||||
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}"
|
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}"
|
||||||
class="btn btn-outline-warning" title="Edit">
|
class="btn btn-outline-warning" title="Edit">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
{# <a href="{% url 'appointments:waiting_queue_delete' queue.pk %}"#}
|
<a href="{% url 'appointments:waiting_queue_delete' queue.pk %}"
|
||||||
{# class="btn btn-outline-danger" title="Delete">#}
|
class="btn btn-outline-danger" title="Delete">
|
||||||
{# <i class="fas fa-trash"></i>#}
|
<i class="fas fa-trash"></i>
|
||||||
{# </a>#}
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -331,95 +179,15 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
<i class="fas fa-users fa-4x text-muted mb-3"></i>
|
<i class="fas fa-users fa-4x text-muted mb-3"></i>
|
||||||
<h4 class="text-muted">No Waiting Queues Found</h4>
|
<h4 class="text-muted">{{ _("No Waiting Queues Found")}}</h4>
|
||||||
<p class="text-muted">Create your first waiting queue to start managing patient flow.</p>
|
<p class="text-muted">{{ _("Create your first waiting queue to start managing patient flow")}}.</p>
|
||||||
<a href="{% url 'appointments:waiting_queue_create' %}" class="btn btn-primary">
|
<a href="{% url 'appointments:waiting_queue_create' %}" class="btn btn-primary">
|
||||||
<i class="fas fa-plus me-1"></i>Create First Queue
|
<i class="fas fa-plus me-1"></i>{{ _("Create First Queue")}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Table View (Alternative) -->
|
|
||||||
<div class="card d-none" id="table-view">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="fas fa-table me-2"></i>Queue List
|
|
||||||
</h5>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn btn-outline-success btn-sm" onclick="exportQueues('excel')">
|
|
||||||
<i class="fas fa-file-excel me-1"></i>Excel
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-danger btn-sm" onclick="exportQueues('pdf')">
|
|
||||||
<i class="fas fa-file-pdf me-1"></i>PDF
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table id="queuesTable" class="table table-striped table-bordered align-middle">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Queue Name</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Specialty</th>
|
|
||||||
<th>Location</th>
|
|
||||||
<th>Current Size</th>
|
|
||||||
<th>Max Size</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for queue in queues %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="fw-bold">{{ queue.name }}</div>
|
|
||||||
<small class="text-muted">{{ queue.description|truncatechars:50 }}</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="queue-type-badge type-{{ queue.queue_type|lower }}">
|
|
||||||
{{ queue.get_queue_type_display }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>{{ queue.specialty }}</td>
|
|
||||||
<td>{{ queue.location }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="fw-bold">{{ queue.current_queue_size }}</span>
|
|
||||||
</td>
|
|
||||||
<td>{{ queue.max_queue_size }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="status-badge status-{% if queue.is_active %}active{% else %}inactive{% endif %}">
|
|
||||||
{% if queue.is_active %}Active{% else %}Inactive{% endif %}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group btn-group-sm">
|
|
||||||
<a href="{% url 'appointments:waiting_queue_detail' queue.pk %}"
|
|
||||||
class="btn btn-outline-primary" title="View">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}"
|
|
||||||
class="btn btn-outline-warning" title="Edit">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
{# <a href="{% url 'appointments:waiting_queue_delete' queue.pk %}"#}
|
|
||||||
{# class="btn btn-outline-danger" title="Delete">#}
|
|
||||||
{# <i class="fas fa-trash"></i>#}
|
|
||||||
{# </a>#}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -438,55 +206,12 @@ $(document).ready(function() {
|
|||||||
order: [[0, 'asc']],
|
order: [[0, 'asc']],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-refresh queue stats every 30 seconds
|
// Auto-refresh queue stats every 60 seconds
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
location.reload();
|
location.reload();
|
||||||
}, 60000);
|
}, 60000);
|
||||||
});
|
});
|
||||||
|
|
||||||
function applyFilters() {
|
|
||||||
const typeFilter = $('#type-filter').val();
|
|
||||||
const statusFilter = $('#status-filter').val();
|
|
||||||
const specialtyFilter = $('#specialty-filter').val();
|
|
||||||
|
|
||||||
$('.queue-item').each(function() {
|
|
||||||
const $item = $(this);
|
|
||||||
const type = $item.data('type');
|
|
||||||
const status = $item.data('status');
|
|
||||||
const specialty = $item.data('specialty');
|
|
||||||
|
|
||||||
let show = true;
|
|
||||||
|
|
||||||
if (typeFilter && type !== typeFilter) show = false;
|
|
||||||
if (statusFilter && status !== statusFilter) show = false;
|
|
||||||
if (specialtyFilter && specialty !== specialtyFilter) show = false;
|
|
||||||
|
|
||||||
if (show) {
|
|
||||||
$item.show();
|
|
||||||
} else {
|
|
||||||
$item.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFilters() {
|
|
||||||
$('#type-filter, #status-filter, #specialty-filter').val('');
|
|
||||||
$('.queue-item').show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleView(view) {
|
|
||||||
if (view === 'cards') {
|
|
||||||
$('#queue-cards').removeClass('d-none');
|
|
||||||
$('#table-view').addClass('d-none');
|
|
||||||
$('#card-view-btn').removeClass('btn-outline-primary').addClass('btn-primary');
|
|
||||||
$('#table-view-btn').removeClass('btn-primary').addClass('btn-outline-primary');
|
|
||||||
} else {
|
|
||||||
$('#queue-cards').addClass('d-none');
|
|
||||||
$('#table-view').removeClass('d-none');
|
|
||||||
$('#table-view-btn').removeClass('btn-outline-primary').addClass('btn-primary');
|
|
||||||
$('#card-view-btn').removeClass('btn-primary').addClass('btn-outline-primary');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{#function exportQueues(format) {#}
|
{#function exportQueues(format) {#}
|
||||||
{# window.location.href = `{% url 'appointments:waiting_queue_export' %}?format=${format}`;#}
|
{# window.location.href = `{% url 'appointments:waiting_queue_export' %}?format=${format}`;#}
|
||||||
|
|||||||
@ -4,56 +4,56 @@
|
|||||||
{% block title %}Queue Management - {{ block.super }}{% endblock %}
|
{% block title %}Queue Management - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
|
<div>
|
||||||
<h1 class="h2">
|
<h1 class="h2">
|
||||||
<i class="fas fa-users-gear"></i> Queue<span class="fw-light">Management</span>
|
<i class="fas fa-users-gear"></i> Queue<span class="fw-light">Management</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
<p class="text-muted">Manage your queues and view their status.</p>
|
||||||
|
</div>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary">
|
<button type="button" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-calendar-plus"></i> Schedule
|
<i class="fas fa-calendar-plus"></i> Schedule
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary">
|
<button type="button" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-users"></i> Queue
|
<i class="fas fa-users"></i> Queue
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button type="button" class="btn btn-outline-primary">
|
||||||
<button type="button" class="btn btn-sm btn-primary">
|
|
||||||
<i class="fas fa-video"></i> Telemedicine
|
<i class="fas fa-video"></i> Telemedicine
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="panel panel-inverse">
|
<div class="row">
|
||||||
|
{% for queue in queues %}
|
||||||
|
{% if queue %}
|
||||||
|
<div class="col-lg-6 col-xl-4 mb-4">
|
||||||
|
<div class="panel panel-inverse" data-sortable-id="index-{{ queue.queue_id }}">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-users me-2"></i>Active Queues
|
{{ queue.name }}
|
||||||
|
<span class="badge bg-primary ms-2">{{ queue.current_queue_size }}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="panel-heading-btn">
|
<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-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body"
|
||||||
<div class="row">
|
|
||||||
{% for queue in queues %}
|
|
||||||
{% if queue %}
|
|
||||||
<div class="col-lg-6 col-xl-4 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">{{ queue.name }}</h5>
|
|
||||||
<span class="badge bg-primary">{{ queue.current_queue_size }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body"
|
|
||||||
hx-get="{% url 'appointments:queue_status' queue.pk %}"
|
hx-get="{% url 'appointments:queue_status' queue.pk %}"
|
||||||
hx-trigger="load, every 60s">
|
hx-trigger="load, every 60s">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="spinner-border text-primary" role="status"></div>
|
<div class="spinner-border text-primary" role="status"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="panel-footer">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
Avg Wait: {{ queue.wait_time_minutes }}
|
Avg Wait: {{ queue.wait_time_minutes }}
|
||||||
@ -84,7 +84,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -4,15 +4,16 @@
|
|||||||
{% block title %}Appointment Details - {{ block.super }}{% endblock %}
|
{% block title %}Appointment Details - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<div class="row mb-3">
|
<div>
|
||||||
<div class="col">
|
<h1 class="h2">
|
||||||
<h1>Appointment Details</h1>
|
<i class="fas fa-calendar-alt"></i> Appointment<span class="fw-light">Details</span>
|
||||||
|
</h1>
|
||||||
<p class="text-muted">{{ appointment.scheduled_datetime|date:"M d, Y H:i" }} • {{ appointment.get_status_display }}</p>
|
<p class="text-muted">{{ appointment.scheduled_datetime|date:"M d, Y H:i" }} • {{ appointment.get_status_display }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<div class="btn-group">
|
<div class="btn-group me-2">
|
||||||
{% if appointment.status == 'PENDING' %}
|
{% if appointment.status in 'CONFIRMED, SCHEDULED' %}
|
||||||
<button class="btn btn-success"
|
<button class="btn btn-success"
|
||||||
hx-post="{% url 'appointments:check_in_patient' appointment.id %}"
|
hx-post="{% url 'appointments:check_in_patient' appointment.id %}"
|
||||||
hx-confirm="Check in this patient?"
|
hx-confirm="Check in this patient?"
|
||||||
@ -29,62 +30,113 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Appointment Information -->
|
<!-- Appointment Information -->
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div class="card mb-3">
|
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0"><i class="fas fa-calendar me-2"></i>Appointment Information</h5>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-calendar me-2"></i>Appointment Information
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<tr><td>Date & Time</td><td>{{ appointment.scheduled_datetime|date:"M d, Y H:i" }}</td></tr>
|
<tr>
|
||||||
<tr><td>Duration</td><td>{{ appointment.duration_minutes }} minutes</td></tr>
|
<td>Date & Time</td>
|
||||||
<tr><td>Type</td><td>{{ appointment.get_appointment_type_display }}</td></tr>
|
<td>{{ appointment.scheduled_datetime|date:"M d, Y H:i" }}</td>
|
||||||
<tr><td>Specialty</td><td>{{ appointment.specialty|default:"General" }}</td></tr>
|
</tr>
|
||||||
<tr><td>Priority</td><td>{{ appointment.get_priority_display }}</td></tr>
|
<tr>
|
||||||
<tr><td>Status</td><td>
|
<td>Duration</td>
|
||||||
|
<td>{{ appointment.duration_minutes }} minutes</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Type</td>
|
||||||
|
<td>{{ appointment.get_appointment_type_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Specialty</td>
|
||||||
|
<td>{{ appointment.specialty|default:"General" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Priority</td>
|
||||||
|
<td>{{ appointment.get_priority_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td>
|
||||||
{% if appointment.status == 'PENDING' %}
|
{% if appointment.status == 'PENDING' %}
|
||||||
<span class="badge bg-warning">{{ appointment.get_status_display }}</span>
|
<span class="badge bg-warning">
|
||||||
{% elif appointment.status == 'CONFIRMED' %}
|
{% elif appointment.status == 'CONFIRMED' %}
|
||||||
<span class="badge bg-info">{{ appointment.get_status_display }}</span>
|
<span class="badge bg-info">
|
||||||
|
{% elif appointment.status == 'SCHEDULED' %}
|
||||||
|
<span class="badge bg-purple">
|
||||||
{% elif appointment.status == 'CHECKED_IN' %}
|
{% elif appointment.status == 'CHECKED_IN' %}
|
||||||
<span class="badge bg-primary">{{ appointment.get_status_display }}</span>
|
<span class="badge bg-primary">
|
||||||
{% elif appointment.status == 'IN_PROGRESS' %}
|
{% elif appointment.status == 'IN_PROGRESS' %}
|
||||||
<span class="badge bg-success">{{ appointment.get_status_display }}</span>
|
<span class="badge bg-success">
|
||||||
{% elif appointment.status == 'COMPLETED' %}
|
{% elif appointment.status == 'COMPLETED' %}
|
||||||
<span class="badge bg-success">{{ appointment.get_status_display }}</span>
|
<span class="badge bg-success">
|
||||||
{% elif appointment.status == 'CANCELLED' %}
|
{% elif appointment.status == 'CANCELLED' %}
|
||||||
<span class="badge bg-danger">{{ appointment.get_status_display }}</span>
|
<span class="badge bg-danger">
|
||||||
{% elif appointment.status == 'NO_SHOW' %}
|
{% elif appointment.status == 'NO_SHOW' %}
|
||||||
<span class="badge bg-secondary">{{ appointment.get_status_display }}</span>
|
<span class="badge bg-secondary">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td></tr>
|
{{ appointment.get_status_display }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% if appointment.is_telemedicine %}
|
{% if appointment.is_telemedicine %}
|
||||||
<tr><td>Telemedicine</td><td><span class="badge bg-info">Yes</span></td></tr>
|
<tr>
|
||||||
|
<td>Telemedicine</td>
|
||||||
|
<td><span class="badge bg-info">Yes</span></td>
|
||||||
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if appointment.chief_complaint %}
|
{% if appointment.chief_complaint %}
|
||||||
<div class="card mb-3">
|
<div class="panel panel-inverse" data-sortable-id="index-2">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0"><i class="fas fa-stethoscope me-2"></i>Chief Complaint</h5>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-stethoscope me-2"></i>Chief Complaint
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<p>{{ appointment.chief_complaint }}</p>
|
<p>{{ appointment.chief_complaint }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if appointment.notes %}
|
{% if appointment.notes %}
|
||||||
<div class="card mb-3">
|
<div class="panel panel-inverse" data-sortable-id="index-3">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0"><i class="fas fa-sticky-note me-2"></i>Notes</h5>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-sticky-note me-2"></i>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>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<p>{{ appointment.notes|linebreaks }}</p>
|
<p>{{ appointment.notes|linebreaks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -93,34 +145,71 @@
|
|||||||
|
|
||||||
<!-- Patient & Provider Information -->
|
<!-- Patient & Provider Information -->
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div class="card mb-3">
|
<div class="panel panel-inverse" data-sortable-id="index-4">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0"><i class="fas fa-user me-2"></i>Patient Information</h5>
|
<h4 class="panel-title">
|
||||||
</div>
|
<i class="fas fa-user me-2"></i>Patient Information
|
||||||
<div class="card-body">
|
</h4>
|
||||||
<table class="table table-sm">
|
<div class="panel-heading-btn">
|
||||||
<tr><td>Name</td><td><strong>{{ appointment.patient.get_full_name }}</strong></td></tr>
|
<a href="{% url 'patients:patient_detail' appointment.patient.id %}" class="btn btn-xs btn-outline-primary me-2">
|
||||||
<tr><td>MRN</td><td>{{ appointment.patient.mrn }}</td></tr>
|
|
||||||
<tr><td>Date of Birth</td><td>{{ appointment.patient.date_of_birth|date:"M d, Y" }}</td></tr>
|
|
||||||
<tr><td>Age</td><td>{{ appointment.patient.age }}</td></tr>
|
|
||||||
<tr><td>Gender</td><td>{{ appointment.patient.get_gender_display }}</td></tr>
|
|
||||||
<tr><td>Phone</td><td>{{ appointment.patient.phone_number|default:"Not provided" }}</td></tr>
|
|
||||||
<tr><td>Email</td><td>{{ appointment.patient.email|default:"Not provided" }}</td></tr>
|
|
||||||
</table>
|
|
||||||
<div class="mt-2">
|
|
||||||
<a href="{% url 'patients:patient_detail' appointment.patient.id %}" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="fas fa-eye me-1"></i>View Patient
|
<i class="fas fa-eye me-1"></i>View Patient
|
||||||
</a>
|
</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>
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td><strong>{{ appointment.patient.get_full_name }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MRN</td>
|
||||||
|
<td>{{ appointment.patient.mrn }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Date of Birth</td>
|
||||||
|
<td>{{ appointment.patient.date_of_birth|date:"M d, Y" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Age</td>
|
||||||
|
<td>{{ appointment.patient.age }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Gender</td>
|
||||||
|
<td>{{ appointment.patient.get_gender_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Phone</td>
|
||||||
|
<td>{{ appointment.patient.phone_number|default:appointment.patient.mobile_number }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Email</td>
|
||||||
|
<td>{{ appointment.patient.email|default:"Not provided" }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if appointment.provider %}
|
{% if appointment.provider %}
|
||||||
<div class="card mb-3">
|
<div class="panel panel-inverse" data-sortable-id="index-5">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0"><i class="fas fa-user-md me-2"></i>Provider Information</h5>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-user-md me-2"></i>Provider Information
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<tr><td>Name</td><td><strong>{{ appointment.provider.get_full_name }}</strong></td></tr>
|
<tr><td>Name</td><td><strong>{{ appointment.provider.get_full_name }}</strong></td></tr>
|
||||||
<tr><td>Role</td><td>{{ appointment.provider.get_role_display }}</td></tr>
|
<tr><td>Role</td><td>{{ appointment.provider.get_role_display }}</td></tr>
|
||||||
@ -133,11 +222,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Appointment Timeline -->
|
<!-- Appointment Timeline -->
|
||||||
<div class="card mb-3">
|
<div class="panel panel-inverse" data-sortable-id="index-6">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0"><i class="fas fa-clock me-2"></i>Timeline</h5>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-clock me-2"></i>Timeline
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="timeline">
|
<div class="timeline">
|
||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
<strong>Created:</strong> {{ appointment.created_at|date:"M d, Y H:i" }}
|
<strong>Created:</strong> {{ appointment.created_at|date:"M d, Y H:i" }}
|
||||||
@ -146,9 +244,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if appointment.confirmed_at %}
|
{% if appointment.scheduled_datetime %}
|
||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
<strong>Confirmed:</strong> {{ appointment.confirmed_at|date:"M d, Y H:i" }}
|
<strong>Scheduled:</strong> {{ appointment.scheduled_datetime|date:"M d, Y H:i" }}
|
||||||
{% if appointment.confirmed_by %}
|
{% if appointment.confirmed_by %}
|
||||||
<br><small class="text-muted">by {{ appointment.confirmed_by.get_full_name }}</small>
|
<br><small class="text-muted">by {{ appointment.confirmed_by.get_full_name }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -357,7 +357,7 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
$('#{{form.provider.id_for_label}}').select2({
|
$('.form-select').select2({
|
||||||
|
|
||||||
}).on('select2:select', function (e) {
|
}).on('select2:select', function (e) {
|
||||||
loadAvailableSlots();
|
loadAvailableSlots();
|
||||||
|
|||||||
@ -1,18 +1,34 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
{% block title %}Appointments - Hospital Management System{% endblock %}
|
{% block title %}Appointments - Hospital Management System{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% 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-calendar-alt"></i> Appointments<span class="fw-light">Requests</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted">Manage your appointments and view their details.</p>
|
||||||
|
</div>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
|
<button type="button" class="btn btn-outline-secondary"><i class="fas fa-download"></i> Export</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"><i class="fas fa-print"></i> Print</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary"><i class="fas fa-calendar-plus"></i> Schedule New</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="panel panel-inverse mb-4" data-sortable-id="index-1" id="availableSlotsCard">
|
<!-- Appointment List -->
|
||||||
|
<div class="panel panel-inverse mb-4" data-sortable-id="index-1" id="appointment-list">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-calendar-alt"></i> Appointments
|
<i class="fas fa-calendar-alt"></i> Appointments
|
||||||
</h4>
|
</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
<button type="button" class="btn btn-xs btn-outline-secondary me-2"><i class="fas fa-download"></i> Export</button>
|
|
||||||
<button type="button" class="btn btn-xs btn-outline-secondary me-2"><i class="fas fa-print"></i> Print</button>
|
|
||||||
<button type="button" class="btn btn-xs btn-primary me-2"><i class="fas fa-calendar-plus"></i> Schedule New</button>
|
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
@ -93,9 +109,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Appointment List -->
|
|
||||||
<div class="card" id="appointment-list">
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
@ -231,14 +244,12 @@
|
|||||||
<i class="fas fa-calendar-alt"></i>
|
<i class="fas fa-calendar-alt"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- View Details Button -->
|
<!-- View Details Button -->
|
||||||
<a href="{% url 'appointments:appointment_detail' appointment.pk %}"
|
<a href="{% url 'appointments:appointment_detail' appointment.pk %}"
|
||||||
class="btn btn-outline-info"
|
class="btn btn-outline-info"
|
||||||
title="View Details">
|
title="View Details">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- More Actions Dropdown -->
|
<!-- More Actions Dropdown -->
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@ -279,12 +290,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
<script src="{% static 'plugins/sweetalert/dist/sweetalert.min.js' %}"></script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -4,12 +4,12 @@
|
|||||||
{% block title %}Appointment Reminders{% endblock %}
|
{% block title %}Appointment Reminders{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ul class="breadcrumb">
|
<ul class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||||
@ -277,7 +277,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Send Reminders Modal -->
|
<!-- Send Reminders Modal -->
|
||||||
<div class="modal fade" id="sendRemindersModal" tabindex="-1">
|
<div class="modal fade" id="sendRemindersModal" tabindex="-1">
|
||||||
@ -347,10 +347,10 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|||||||
@ -3,11 +3,16 @@
|
|||||||
|
|
||||||
{% block title %}Reschedule Appointment{% endblock %}
|
{% block title %}Reschedule Appointment{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% 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 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">
|
<h1 class="h2">
|
||||||
<i class="fas fa-calendar-alt"></i> Reschedule<span class="fw-light">Appointments</span>
|
<i class="fas fa-calendar-alt"></i> Reschedule<span class="fw-light">Appointments</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
<p class="text-muted">Something Something Something Something Something</p>
|
||||||
|
</div>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary">
|
<button type="button" class="btn btn-sm btn-outline-secondary">
|
||||||
@ -16,12 +21,12 @@
|
|||||||
<button type="button" class="btn btn-sm btn-outline-secondary">
|
<button type="button" class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="fas fa-users"></i> Queue
|
<i class="fas fa-users"></i> Queue
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-sm btn-primary">
|
<button type="button" class="btn btn-sm btn-primary">
|
||||||
<i class="fas fa-video"></i> Telemedicine
|
<i class="fas fa-video"></i> Telemedicine
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-8">
|
<div class="col-xl-8">
|
||||||
@ -176,7 +181,7 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div id="availableSlots"
|
<div id="availableSlots"
|
||||||
hx-get="{% url 'appointments:available_slots' %}"
|
hx-get="{% url 'appointments:available_slots' %}"
|
||||||
hx-trigger="change from:#new_date,change from:#new_provider"
|
hx-trigger="change from:#new_provider"
|
||||||
hx-target="#availableSlots"
|
hx-target="#availableSlots"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-include="#new_date,#new_provider"
|
hx-include="#new_date,#new_provider"
|
||||||
@ -188,7 +193,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<label for="provider-filter" class="form-label">Provider</label>
|
<label for="provider-filter" class="form-label">Provider</label>
|
||||||
<select id="provider-filter" class="form-select">
|
<select id="provider-filter" class="form-select">
|
||||||
<option value="">All Providers</option>
|
<option value="">All Providers</option>
|
||||||
{% for provider in providers %}
|
{% for provider in appointments.providers.all %}
|
||||||
<option value="{{ provider.id }}">{{ provider.get_full_name }}</option>
|
<option value="{{ provider.id }}">{{ provider.get_full_name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@ -499,7 +499,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="availability-header fade-in">
|
<div class="availability-header fade-in">
|
||||||
@ -844,7 +843,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bulk Create Modal -->
|
<!-- Bulk Create Modal -->
|
||||||
<div class="modal fade" id="bulkCreateModal" tabindex="-1">
|
<div class="modal fade" id="bulkCreateModal" tabindex="-1">
|
||||||
|
|||||||
@ -539,7 +539,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="booking-header fade-in">
|
<div class="booking-header fade-in">
|
||||||
@ -930,7 +929,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading Overlay -->
|
<!-- Loading Overlay -->
|
||||||
<div class="loading-overlay" id="loadingOverlay">
|
<div class="loading-overlay" id="loadingOverlay">
|
||||||
|
|||||||
@ -572,7 +572,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="calendar-header fade-in">
|
<div class="calendar-header fade-in">
|
||||||
@ -867,7 +866,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Slot Details Modal -->
|
<!-- Slot Details Modal -->
|
||||||
<div class="modal fade" id="slotModal" tabindex="-1">
|
<div class="modal fade" id="slotModal" tabindex="-1">
|
||||||
|
|||||||
@ -418,7 +418,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -712,7 +712,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
|||||||
@ -391,7 +391,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="slot-detail-header fade-in">
|
<div class="slot-detail-header fade-in">
|
||||||
@ -794,7 +794,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Book Appointment Modal -->
|
<!-- Book Appointment Modal -->
|
||||||
<div class="modal fade" id="bookAppointmentModal" tabindex="-1">
|
<div class="modal fade" id="bookAppointmentModal" tabindex="-1">
|
||||||
|
|||||||
@ -402,7 +402,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="slot-form-header fade-in">
|
<div class="slot-form-header fade-in">
|
||||||
@ -806,7 +805,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -302,7 +302,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="slot-management-header fade-in">
|
<div class="slot-management-header fade-in">
|
||||||
@ -583,7 +582,7 @@
|
|||||||
<div id="slotCalendar"></div>
|
<div id="slotCalendar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Book Slot Modal -->
|
<!-- Book Slot Modal -->
|
||||||
<div class="modal fade" id="bookSlotModal" tabindex="-1">
|
<div class="modal fade" id="bookSlotModal" tabindex="-1">
|
||||||
|
|||||||
@ -591,7 +591,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="management-header fade-in">
|
<div class="management-header fade-in">
|
||||||
@ -921,7 +920,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bulk Create Modal -->
|
<!-- Bulk Create Modal -->
|
||||||
<div class="modal fade" id="bulkCreateModal" tabindex="-1">
|
<div class="modal fade" id="bulkCreateModal" tabindex="-1">
|
||||||
|
|||||||
@ -109,10 +109,10 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<button class="btn btn-outline-info"
|
<a href="{% url 'appointments:telemedicine_session_detail' session.pk %}" class="btn btn-outline-info"
|
||||||
title="View Details">
|
title="View Details">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</a>
|
||||||
|
|
||||||
{% if session.status not in 'COMPLETED,CANCELLED' %}
|
{% if session.status not in 'COMPLETED,CANCELLED' %}
|
||||||
<button class="btn btn-outline-danger"
|
<button class="btn btn-outline-danger"
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -4,23 +4,28 @@
|
|||||||
{% block title %}Appointment Templates - Appointments{% endblock %}
|
{% block title %}Appointment Templates - Appointments{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- BEGIN breadcrumb -->
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<ol class="breadcrumb float-xl-end">
|
<div>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
<h1 class="h2">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
<i class="fas fa-book"></i> Appointment<span class="fw-light">Templates</span>
|
||||||
<li class="breadcrumb-item active">Appointment Templates</li>
|
|
||||||
</ol>
|
|
||||||
<!-- END breadcrumb -->
|
|
||||||
|
|
||||||
<!-- BEGIN page-header -->
|
|
||||||
<h1 class="page-header">
|
|
||||||
Appointment Templates
|
|
||||||
<small>Reusable configurations for common appointment types</small>
|
|
||||||
</h1>
|
</h1>
|
||||||
<!-- END page-header -->
|
<p class="text-muted">Reusable configurations for common appointment types.</p>
|
||||||
|
</div>
|
||||||
|
{# <div class="btn-toolbar mb-2 mb-md-0">#}
|
||||||
|
{# <div class="btn-group me-2">#}
|
||||||
|
{# <a href="{% url 'appointments:waiting_list_edit' entry.pk %}" class="btn btn-warning">#}
|
||||||
|
{# <i class="fas fa-edit me-1"></i>Edit Entry#}
|
||||||
|
{# </a>#}
|
||||||
|
{# <a href="{% url 'appointments:waiting_list_delete' entry.pk %}" class="btn btn-danger">#}
|
||||||
|
{# <i class="fas fa-trash me-1"></i>Delete Entry#}
|
||||||
|
{# </a>#}
|
||||||
|
{# <a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-secondary">#}
|
||||||
|
{# <i class="fas fa-arrow-left me-1"></i>Back to List#}
|
||||||
|
{# </a>#}
|
||||||
|
{# </div>#}
|
||||||
|
{# </div>#}
|
||||||
|
</div>
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="panel panel-inverse">
|
<div class="panel panel-inverse">
|
||||||
@ -51,7 +56,7 @@
|
|||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Search</label>
|
<label class="form-label">Search</label>
|
||||||
<input type="text" name="search" value="{{ request.GET.search|default:'' }}" class="form-control" placeholder="Name, description, department...">
|
<input type="text" name="search" value="{{ request.GET.search }}" class="form-control" placeholder="Name, description, department...">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@ -66,7 +71,7 @@
|
|||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Department</label>
|
<label class="form-label">Department</label>
|
||||||
<input type="text" name="department" value="{{ request.GET.department|default:'' }}" class="form-control form-control-sm" placeholder="Department">
|
<input type="text" name="department" value="{{ request.GET.department }}" class="form-control form-control-sm" placeholder="Department">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@ -163,7 +168,7 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="text-nowrap small">
|
<td class="text-nowrap small">
|
||||||
<div><i class="fa fa-user me-1 text-muted"></i>{{ t.created_by.get_full_name|default:"—" }}</div>
|
<div><i class="fa fa-user me-1 text-muted"></i>{{ t.created_by.get_full_name }}</div>
|
||||||
<div class="text-muted">{{ t.updated_at|date:"M d, Y H:i" }}</div>
|
<div class="text-muted">{{ t.updated_at|date:"M d, Y H:i" }}</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@ -230,4 +235,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -27,21 +27,14 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- BEGIN breadcrumb -->
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<ol class="breadcrumb float-xl-end">
|
<div>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
|
<h1 class="h2">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
<i class="fas fa-clock"></i> Waiting List<span class="fw-light">Management</span>
|
||||||
<li class="breadcrumb-item active">Waiting List</li>
|
|
||||||
</ol>
|
|
||||||
<!-- END breadcrumb -->
|
|
||||||
|
|
||||||
<!-- BEGIN page-header -->
|
|
||||||
<h1 class="page-header">
|
|
||||||
<i class="fas fa-clock text-primary me-2"></i>
|
|
||||||
Patient Waiting List Management
|
|
||||||
<small class="text-muted ms-2">Manage appointment waiting list and patient queue</small>
|
|
||||||
</h1>
|
</h1>
|
||||||
<!-- END page-header -->
|
<p class="text-muted">Manage appointment waiting list and patient queue.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- BEGIN statistics cards -->
|
<!-- BEGIN statistics cards -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
@ -99,7 +92,7 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h6 class="card-title mb-1">Avg. Wait</h6>
|
<h6 class="card-title mb-1">Avg. Wait</h6>
|
||||||
<h3 class="mb-0" id="avg-wait">{{ stats.avg_wait_days }}</h3>
|
<h3 class="mb-0" id="avg-wait">{{ stats.avg_wait_days|default:0 }}</h3>
|
||||||
<small class="opacity-75">Days waiting</small>
|
<small class="opacity-75">Days waiting</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
@ -113,18 +106,18 @@
|
|||||||
<!-- END statistics cards -->
|
<!-- END statistics cards -->
|
||||||
|
|
||||||
<!-- BEGIN filter panel -->
|
<!-- BEGIN filter panel -->
|
||||||
<div class="panel panel-inverse">
|
{#<div class="panel panel-inverse">#}
|
||||||
<div class="panel-heading">
|
{# <div class="panel-heading">#}
|
||||||
<h4 class="panel-title">
|
{# <h4 class="panel-title">#}
|
||||||
<i class="fas fa-filter me-2"></i>Filter Waiting List
|
{# <i class="fas fa-filter me-2"></i>Filter Waiting List#}
|
||||||
</h4>
|
{# </h4>#}
|
||||||
<div class="panel-heading-btn">
|
{# <div class="panel-heading-btn">#}
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse">
|
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse">#}
|
||||||
<i class="fa fa-minus"></i>
|
{# <i class="fa fa-minus"></i>#}
|
||||||
</a>
|
{# </a>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
<div class="panel-body">
|
{# <div class="panel-body">#}
|
||||||
{# <form method="get" class="row g-3">#}
|
{# <form method="get" class="row g-3">#}
|
||||||
{# <div class="col-md-2">#}
|
{# <div class="col-md-2">#}
|
||||||
{# {{ filter_form.department.label_tag }}#}
|
{# {{ filter_form.department.label_tag }}#}
|
||||||
@ -158,8 +151,8 @@
|
|||||||
{# </div>#}
|
{# </div>#}
|
||||||
{# </div>#}
|
{# </div>#}
|
||||||
{# </form>#}
|
{# </form>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
</div>
|
{#</div>#}
|
||||||
<!-- END filter panel -->
|
<!-- END filter panel -->
|
||||||
|
|
||||||
<!-- BEGIN main panel -->
|
<!-- BEGIN main panel -->
|
||||||
@ -170,10 +163,10 @@
|
|||||||
<i class="fas fa-list me-2"></i>Waiting List Entries
|
<i class="fas fa-list me-2"></i>Waiting List Entries
|
||||||
</h4>
|
</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
<a href="{% url 'appointments:waiting_list_create' %}" class="btn btn-primary btn-sm me-2">
|
<a href="{% url 'appointments:waiting_list_create' %}" class="btn btn-primary btn-xs me-2">
|
||||||
<i class="fas fa-plus me-1"></i>Add to Waiting List
|
<i class="fas fa-plus me-1"></i>Add to Waiting List
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshStats()">
|
<button type="button" class="btn btn-outline-secondary btn-xs" onclick="refreshStats()">
|
||||||
<i class="fas fa-sync-alt"></i> Refresh
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
<!-- END breadcrumb -->
|
<!-- END breadcrumb -->
|
||||||
|
|
||||||
<!-- BEGIN page-header -->
|
<!-- BEGIN page-header -->
|
||||||
<h1 class="page-header">
|
<h1 class="h2">
|
||||||
<i class="fas fa-trash-alt text-danger me-2"></i>
|
<i class="fas fa-trash-alt text-danger me-2"></i>
|
||||||
Confirm Waiting List Entry Cancellation
|
Confirm Waiting List Entry Cancellation
|
||||||
<small class="text-muted ms-2">Permanently remove this patient from the waiting list</small>
|
<small class="text-muted ms-2">Permanently remove this patient from the waiting list</small>
|
||||||
|
|||||||
@ -3,83 +3,61 @@
|
|||||||
|
|
||||||
{% block title %}Waiting List Entry Details{% endblock %}
|
{% block title %}Waiting List Entry Details{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block css %}
|
||||||
<style>
|
|
||||||
.detail-section {
|
|
||||||
border-left: 4px solid #007bff;
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
.detail-section h5 {
|
|
||||||
color: #007bff;
|
|
||||||
}
|
|
||||||
.priority-badge {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 0.4em 0.6em;
|
|
||||||
}
|
|
||||||
.status-badge {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 0.4em 0.6em;
|
|
||||||
}
|
|
||||||
.contact-log-item {
|
|
||||||
border-bottom: 1px dashed #eee;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.contact-log-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- BEGIN breadcrumb -->
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<ol class="breadcrumb float-xl-end">
|
<div>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
|
<h1 class="h2">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
<i class="fas fa-calendar-alt"></i> Waiting List<span class="fw-light">Entry Details</span>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_list' %}">Waiting List</a></li>
|
|
||||||
<li class="breadcrumb-item active">Entry Details</li>
|
|
||||||
</ol>
|
|
||||||
<!-- END breadcrumb -->
|
|
||||||
|
|
||||||
<!-- BEGIN page-header -->
|
|
||||||
<h1 class="page-header">
|
|
||||||
<i class="fas fa-info-circle text-primary me-2"></i>
|
|
||||||
Waiting List Entry Details
|
|
||||||
<small class="text-muted ms-2">Comprehensive view of patient waiting list entry</small>
|
|
||||||
</h1>
|
</h1>
|
||||||
<!-- END page-header -->
|
<p class="text-muted">Comprehensive view of patient waiting list entry.</p>
|
||||||
|
</div>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
|
<a href="{% url 'appointments:waiting_list_edit' entry.pk %}" class="btn btn-warning">
|
||||||
|
<i class="fas fa-edit me-1"></i>Edit Entry
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'appointments:waiting_list_delete' entry.pk %}" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash me-1"></i>Delete Entry
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- BEGIN panel -->
|
<!-- BEGIN panel -->
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="panel panel-inverse">
|
<div class="panel panel-inverse">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-user-tag me-2"></i>Patient: {{ entry.patient.get_full_name }}
|
<i class="fas fa-user-tag me-2"></i>Patient: {{ entry.patient.get_full_name }}
|
||||||
</h4>
|
</h4>
|
||||||
<div class="panel-heading-btn">
|
|
||||||
<a href="{% url 'appointments:waiting_list_edit' entry.pk %}" class="btn btn-warning btn-sm me-2">
|
|
||||||
<i class="fas fa-edit me-1"></i>Edit Entry
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'appointments:waiting_list_delete' entry.pk %}" class="btn btn-danger btn-sm me-2">
|
|
||||||
<i class="fas fa-trash me-1"></i>Delete Entry
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-secondary btn-sm">
|
|
||||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-4">
|
||||||
<!-- Patient & Service Information -->
|
<!-- Patient & Service Information -->
|
||||||
<div class="detail-section mb-4">
|
<div class="panel panel-inverse border" data-sortable-id="index-1">
|
||||||
<h5 class="mb-3"><i class="fas fa-user me-2"></i>Patient & Service Information</h5>
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-user me-2"></i>Patient & Service Information
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Patient Name:</strong></div>
|
<div class="col-md-4"><strong>Patient Name:</strong></div>
|
||||||
<div class="col-md-8">{{ entry.patient.get_full_name }} (MRN: {{ entry.patient.mrn|default:'N/A' }})</div>
|
<div class="col-md-8">{{ entry.patient.get_full_name }} (MRN: {{ entry.patient.mrn }})</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Department:</strong></div>
|
<div class="col-md-4"><strong>Department:</strong></div>
|
||||||
@ -98,41 +76,49 @@
|
|||||||
<div class="col-md-8">{{ entry.get_specialty_display }}</div>
|
<div class="col-md-8">{{ entry.get_specialty_display }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Clinical Priority & Urgency -->
|
|
||||||
<div class="detail-section mb-4">
|
|
||||||
<h5 class="mb-3"><i class="fas fa-exclamation-triangle me-2"></i>Clinical Priority & Urgency</h5>
|
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-md-4"><strong>Priority Level:</strong></div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
{% if entry.priority == 'EMERGENCY' %}
|
|
||||||
<span class="badge bg-danger priority-badge">{{ entry.get_priority_display }}</span>
|
|
||||||
{% elif entry.priority == 'STAT' %}
|
|
||||||
<span class="badge bg-warning priority-badge">{{ entry.get_priority_display }}</span>
|
|
||||||
{% elif entry.priority == 'URGENT' %}
|
|
||||||
<span class="badge bg-warning priority-badge">{{ entry.get_priority_display }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-success priority-badge">{{ entry.get_priority_display }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Contact Information -->
|
||||||
|
<div class="panel panel-inverse border" data-sortable-id="index-2">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-phone me-2"></i>Contact Information
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-md-4"><strong>Preferred Method:</strong></div>
|
||||||
|
<div class="col-md-8">{{ entry.get_contact_method_display }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Urgency Score:</strong></div>
|
<div class="col-md-4"><strong>Phone:</strong></div>
|
||||||
<div class="col-md-8">{{ entry.urgency_score }} / 10</div>
|
<div class="col-md-8">{{ entry.contact_phone|default:'N/A' }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Clinical Indication:</strong></div>
|
<div class="col-md-4"><strong>Email:</strong></div>
|
||||||
<div class="col-md-8">{{ entry.clinical_indication|linebreaksbr }}</div>
|
<div class="col-md-8">{{ entry.contact_email|default:'N/A' }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-md-4"><strong>Diagnosis Codes:</strong></div>
|
|
||||||
<div class="col-md-8">{{ entry.diagnosis_codes|join:", "|default:'N/A' }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Patient Scheduling Preferences -->
|
<!-- Patient Scheduling Preferences -->
|
||||||
<div class="detail-section mb-4">
|
<div class="panel panel-inverse border" data-sortable-id="index-3">
|
||||||
<h5 class="mb-3"><i class="fas fa-calendar-alt me-2"></i>Patient Scheduling Preferences</h5>
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-calendar-alt me-2"></i>Patient Scheduling Preferences
|
||||||
|
</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 mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Preferred Date:</strong></div>
|
<div class="col-md-4"><strong>Preferred Date:</strong></div>
|
||||||
<div class="col-md-8">{{ entry.preferred_date|date:"M d, Y"|default:'Any' }}</div>
|
<div class="col-md-8">{{ entry.preferred_date|date:"M d, Y"|default:'Any' }}</div>
|
||||||
@ -156,27 +142,66 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact Information -->
|
|
||||||
<div class="detail-section mb-4">
|
|
||||||
<h5 class="mb-3"><i class="fas fa-phone me-2"></i>Contact Information</h5>
|
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-md-4"><strong>Preferred Method:</strong></div>
|
|
||||||
<div class="col-md-8">{{ entry.get_contact_method_display }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-md-4"><strong>Phone:</strong></div>
|
|
||||||
<div class="col-md-8">{{ entry.contact_phone|default:'N/A' }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-md-4"><strong>Email:</strong></div>
|
|
||||||
<div class="col-md-8">{{ entry.contact_email|default:'N/A' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- Clinical Priority & Urgency -->
|
||||||
|
<div class="panel panel-inverse border" data-sortable-id="index-4">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>Clinical Priority & Urgency
|
||||||
|
</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 mb-2">
|
||||||
|
<div class="col-md-4"><strong>Priority Level:</strong></div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
{% if entry.priority == 'EMERGENCY' %}
|
||||||
|
<span class="badge bg-danger priority-badge">{{ entry.get_priority_display }}</span>
|
||||||
|
{% elif entry.priority == 'STAT' %}
|
||||||
|
<span class="badge bg-red priority-badge">{{ entry.get_priority_display }}</span>
|
||||||
|
{% elif entry.priority == 'URGENT' %}
|
||||||
|
<span class="badge bg-warning priority-badge">{{ entry.get_priority_display }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success priority-badge">{{ entry.get_priority_display }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-md-4"><strong>Urgency Score:</strong></div>
|
||||||
|
<div class="col-md-8">{{ entry.urgency_score }} / 10</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-md-4"><strong>Clinical Indication:</strong></div>
|
||||||
|
<div class="col-md-8">{{ entry.clinical_indication|linebreaksbr }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-md-4"><strong>Diagnosis Codes:</strong></div>
|
||||||
|
<div class="col-md-8">{{ entry.diagnosis_codes|join:", " }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Special Requirements -->
|
<!-- Special Requirements -->
|
||||||
<div class="detail-section mb-4">
|
<div class="panel panel-inverse border" data-sortable-id="index-5">
|
||||||
<h5 class="mb-3"><i class="fas fa-universal-access me-2"></i>Special Requirements & Accommodations</h5>
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-universal-access me-2"></i>Special Requirements & Accommodations
|
||||||
|
</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 mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Interpreter Needed:</strong></div>
|
<div class="col-md-4"><strong>Interpreter Needed:</strong></div>
|
||||||
<div class="col-md-8">{% if entry.requires_interpreter %}Yes ({{ entry.interpreter_language|default:'N/A' }}){% else %}No{% endif %}</div>
|
<div class="col-md-8">{% if entry.requires_interpreter %}Yes ({{ entry.interpreter_language|default:'N/A' }}){% else %}No{% endif %}</div>
|
||||||
@ -190,31 +215,21 @@
|
|||||||
<div class="col-md-8">{{ entry.accessibility_requirements|default:'None'|linebreaksbr }}</div>
|
<div class="col-md-8">{{ entry.accessibility_requirements|default:'None'|linebreaksbr }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Insurance & Authorization -->
|
|
||||||
<div class="detail-section mb-4">
|
|
||||||
<h5 class="mb-3"><i class="fas fa-shield-alt me-2"></i>Insurance & Authorization</h5>
|
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-md-4"><strong>Insurance Verified:</strong></div>
|
|
||||||
<div class="col-md-8">{% if entry.insurance_verified %}Yes{% else %}No{% endif %}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-md-4"><strong>Authorization Required:</strong></div>
|
|
||||||
<div class="col-md-8">{% if entry.authorization_required %}Yes{% else %}No{% endif %}</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-md-4"><strong>Authorization Status:</strong></div>
|
|
||||||
<div class="col-md-8">{{ entry.get_authorization_status_display }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-md-4"><strong>Authorization Number:</strong></div>
|
|
||||||
<div class="col-md-8">{{ entry.authorization_number|default:'N/A' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Referral Information -->
|
<!-- Referral Information -->
|
||||||
<div class="detail-section mb-4">
|
<div class="panel panel-inverse border" data-sortable-id="index-6">
|
||||||
<h5 class="mb-3"><i class="fas fa-user-md me-2"></i>Referral Information</h5>
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-user-md me-2"></i>Referral Information
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Referring Provider:</strong></div>
|
<div class="col-md-4"><strong>Referring Provider:</strong></div>
|
||||||
<div class="col-md-8">{{ entry.referring_provider|default:'N/A' }}</div>
|
<div class="col-md-8">{{ entry.referring_provider|default:'N/A' }}</div>
|
||||||
@ -228,33 +243,145 @@
|
|||||||
<div class="col-md-8">{{ entry.get_referral_urgency_display }}</div>
|
<div class="col-md-8">{{ entry.get_referral_urgency_display }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- Waiting List Metrics -->
|
||||||
|
<div class="panel panel-inverse border" data-sortable-id="index-7">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-chart-bar me-2"></i>Waiting List Metrics
|
||||||
|
</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="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div><strong>Current Position:</strong></div>
|
||||||
|
<div class="text-primary">{{ entry.position|default:'N/A' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div><strong>Days Waiting:</strong></div>
|
||||||
|
<div class="text-info">{{ entry.days_waiting }} days</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div><strong>Estimated Wait Time:</strong></div>
|
||||||
|
<div class="text-warning">{{ estimated_wait_time }} days</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div><strong>Contact Attempts:</strong></div>
|
||||||
|
<div class="text-secondary">{{ entry.contact_attempts }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div><strong>Overdue Contact:</strong></div>
|
||||||
|
<div class="">{% if entry.is_overdue_contact %}<span class="badge bg-danger">Yes</span>{% else %}<span class="badge bg-success">No</span>{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Contact Log -->
|
||||||
|
<div class="panel panel-inverse border" data-sortable-id="index-8">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-history me-2"></i>Contact Log
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<button type="button" class="btn btn-outline-theme btn-xs me-2" data-bs-toggle="modal" data-bs-target="#addContactLogModal">
|
||||||
|
<i class="fas fa-plus me-1"></i>Add Contact Log
|
||||||
|
</button>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body" id="contact-log-container">
|
||||||
|
{% include 'appointments/partials/contact_log_list.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Insurance & Authorization -->
|
||||||
|
<div class="panel panel-inverse border" data-sortable-id="index-9">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-shield-alt me-2"></i>Insurance & Authorization
|
||||||
|
</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 mb-2">
|
||||||
|
<div class="col-md-4"><strong>Insurance Verified:</strong></div>
|
||||||
|
<div class="col-md-8">{{ entry.insurance_verified|yesno|capfirst }} </div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-md-4"><strong>Authorization Required:</strong></div>
|
||||||
|
<div class="col-md-8">{{ entry.authorization_required|yesno|capfirst }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-md-4"><strong>Authorization Status:</strong></div>
|
||||||
|
<div class="col-md-8">{{ entry.get_authorization_status_display }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-md-4"><strong>Authorization Number:</strong></div>
|
||||||
|
<div class="col-md-8">{{ entry.authorization_number|default:'N/A' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Additional Notes -->
|
<!-- Additional Notes -->
|
||||||
<div class="detail-section mb-4">
|
<div class="panel panel-inverse border" data-sortable-id="index-10">
|
||||||
<h5 class="mb-3"><i class="fas fa-sticky-note me-2"></i>Additional Notes</h5>
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-sticky-note me-2"></i>Additional 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>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-12">{{ entry.notes|default:'No additional notes.'|linebreaksbr }}</div>
|
<div class="col-md-12">{{ entry.notes|default:'No additional notes.'|linebreaksbr }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Outcome Tracking -->
|
<!-- Outcome Tracking -->
|
||||||
<div class="detail-section mb-4">
|
<div class="panel panel-inverse border" data-sortable-id="index-11">
|
||||||
<h5 class="mb-3"><i class="fas fa-check-circle me-2"></i>Outcome Tracking</h5>
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>Outcome Tracking
|
||||||
|
</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 mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Status:</strong></div>
|
<div class="col-md-4"><strong>Status:</strong></div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
{% if entry.status == 'ACTIVE' %}
|
{% if entry.status == 'ACTIVE' %}
|
||||||
<span class="badge bg-primary status-badge">{{ entry.get_status_display }}</span>
|
<span class="badge bg-primary">{{ entry.get_status_display }}</span>
|
||||||
{% elif entry.status == 'CONTACTED' %}
|
{% elif entry.status == 'CONTACTED' %}
|
||||||
<span class="badge bg-info status-badge">{{ entry.get_status_display }}</span>
|
<span class="badge bg-info">{{ entry.get_status_display }}</span>
|
||||||
{% elif entry.status == 'OFFERED' %}
|
{% elif entry.status == 'OFFERED' %}
|
||||||
<span class="badge bg-warning status-badge">{{ entry.get_status_display }}</span>
|
<span class="badge bg-warning">{{ entry.get_status_display }}</span>
|
||||||
{% elif entry.status == 'SCHEDULED' %}
|
{% elif entry.status == 'SCHEDULED' %}
|
||||||
<span class="badge bg-success status-badge">{{ entry.get_status_display }}</span>
|
<span class="badge bg-success">{{ entry.get_status_display }}</span>
|
||||||
{% elif entry.status == 'CANCELLED' %}
|
{% elif entry.status == 'CANCELLED' %}
|
||||||
<span class="badge bg-danger status-badge">{{ entry.get_status_display }}</span>
|
<span class="badge bg-danger">{{ entry.get_status_display }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary status-badge">{{ entry.get_status_display }}</span>
|
<span class="badge bg-secondary">{{ entry.get_status_display }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -289,17 +416,28 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Metadata -->
|
<!-- Metadata -->
|
||||||
<div class="detail-section mb-4">
|
<div class="panel panel-inverse border" data-sortable-id="index-12">
|
||||||
<h5 class="mb-3"><i class="fas fa-database me-2"></i>Metadata</h5>
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-database me-2"></i>Metadata
|
||||||
|
</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 mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Entry ID:</strong></div>
|
<div class="col-md-4"><strong>Entry ID:</strong></div>
|
||||||
<div class="col-md-8">{{ entry.waiting_list_id }}</div>
|
<div class="col-md-8">{{ entry.waiting_list_id }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Created At:</strong></div>
|
<div class="col-md-4"><strong>Created At:</strong></div>
|
||||||
<div class="col-md-8">{{ entry.created_at|date:"M d, Y H:i" }} by {{ entry.created_by.get_full_name|default:'N/A' }}</div>
|
<div class="col-md-8">{{ entry.created_at|date:"M d, Y H:i" }} by {{ entry.created_by.get_display_name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Last Updated:</strong></div>
|
<div class="col-md-4"><strong>Last Updated:</strong></div>
|
||||||
@ -307,121 +445,277 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<!-- Waiting List Metrics -->
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header bg-primary text-white">
|
|
||||||
<h5 class="card-title mb-0"><i class="fas fa-chart-bar me-2"></i>Waiting List Metrics</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<div><strong>Current Position:</strong></div>
|
|
||||||
<div class="fs-4 text-primary">{{ entry.position|default:'N/A' }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<div><strong>Days Waiting:</strong></div>
|
|
||||||
<div class="fs-4 text-info">{{ entry.days_waiting }} days</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<div><strong>Estimated Wait Time:</strong></div>
|
|
||||||
<div class="fs-4 text-warning">{{ estimated_wait_time }} days</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<div><strong>Contact Attempts:</strong></div>
|
|
||||||
<div class="fs-4 text-secondary">{{ entry.contact_attempts }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<div><strong>Overdue Contact:</strong></div>
|
|
||||||
<div class="fs-4">{% if entry.is_overdue_contact %}<span class="badge bg-danger">Yes</span>{% else %}<span class="badge bg-success">No</span>{% endif %}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact Log -->
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header bg-success text-white">
|
|
||||||
<h5 class="card-title mb-0"><i class="fas fa-history me-2"></i>Contact Log</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="contact-log-container">
|
|
||||||
{% include 'appointments/partials/contact_log_list.html' %}
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#addContactLogModal">
|
|
||||||
<i class="fas fa-plus me-1"></i>Add Contact Log
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- END panel -->
|
|
||||||
|
|
||||||
<!-- Add Contact Log Modal -->
|
<!-- Add Contact Log Modal -->
|
||||||
<div class="modal fade" id="addContactLogModal" tabindex="-1">
|
<div class="modal fade" id="addContactLogModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||||
<div class="modal-content">
|
<div class="modal-content border-0 shadow">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Add Contact Log for {{ entry.patient.get_full_name }}</h5>
|
<h5 class="modal-title">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<i class="fas fa-phone-alt"></i>Add Contact Log for {{ entry.patient.get_full_name }}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="{% url 'appointments:add_contact_log' entry.pk %}" hx-post="{% url 'appointments:add_contact_log' entry.pk %}" hx-target="#contact-log-container" hx-swap="innerHTML">
|
|
||||||
|
<!-- Use hx-post to refresh the contact log list on success -->
|
||||||
|
<form
|
||||||
|
method="post"
|
||||||
|
action="{% url 'appointments:add_contact_log' entry.pk %}"
|
||||||
|
hx-post="{% url 'appointments:add_contact_log' entry.pk %}"
|
||||||
|
hx-target="#contact-log-container"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="needs-validation"
|
||||||
|
novalidate
|
||||||
|
>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{% for field in contact_form %}
|
<!-- Top-level (non-field) errors -->
|
||||||
<div class="mb-3">
|
{% if contact_form.non_field_errors %}
|
||||||
<label class="form-label">{{ field.label }}</label>
|
<div class="alert alert-danger mb-3" role="alert">
|
||||||
{{ field }}
|
<ul class="mb-0">
|
||||||
{% if field.help_text %}<small class="form-text text-muted">{{ field.help_text }}</small>{% endif %}
|
{% for err in contact_form.non_field_errors %}
|
||||||
{% for error in field.errors %}
|
<li>{{ err }}</li>
|
||||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Contact method -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold" for="{{ contact_form.contact_method.id_for_label }}">
|
||||||
|
Contact Method <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ contact_form.contact_method }}
|
||||||
|
{% if contact_form.contact_method.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ contact_form.contact_method.errors|striptags }}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
{% else %}
|
||||||
|
<div class="form-text">Call, SMS, email, etc.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact outcome -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold" for="{{ contact_form.contact_outcome.id_for_label }}">
|
||||||
|
Contact Outcome <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ contact_form.contact_outcome }}
|
||||||
|
{% if contact_form.contact_outcome.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ contact_form.contact_outcome.errors|striptags }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="form-text">Answered, no answer, voicemail, etc.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Appointment offered (toggle) -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
{{ contact_form.appointment_offered }}
|
||||||
|
<label class="form-check-label fw-semibold ms-1" for="{{ contact_form.appointment_offered.id_for_label }}">
|
||||||
|
Appointment offered?
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% if contact_form.appointment_offered.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ contact_form.appointment_offered.errors|striptags }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Offered date/time (conditional) -->
|
||||||
|
<div class="col-md-6 offered-fields">
|
||||||
|
<label class="form-label fw-semibold" for="{{ contact_form.offered_date.id_for_label }}">
|
||||||
|
Offered Date
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-calendar-day"></i></span>
|
||||||
|
{{ contact_form.offered_date }}
|
||||||
|
</div>
|
||||||
|
{% if contact_form.offered_date.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ contact_form.offered_date.errors|striptags }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="form-text">Date of the offered appointment.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 offered-fields">
|
||||||
|
<label class="form-label fw-semibold" for="{{ contact_form.offered_time.id_for_label }}">
|
||||||
|
Offered Time
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-clock"></i></span>
|
||||||
|
{{ contact_form.offered_time }}
|
||||||
|
</div>
|
||||||
|
{% if contact_form.offered_time.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ contact_form.offered_time.errors|striptags }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="form-text">Time of the offered slot.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Patient response -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold" for="{{ contact_form.patient_response.id_for_label }}">
|
||||||
|
Patient Response
|
||||||
|
</label>
|
||||||
|
{{ contact_form.patient_response }}
|
||||||
|
{% if contact_form.patient_response.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ contact_form.patient_response.errors|striptags }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="form-text">Accepted, declined, call back, etc.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Next contact date -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold" for="{{ contact_form.next_contact_date.id_for_label }}">
|
||||||
|
Next Contact Date
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-bell"></i></span>
|
||||||
|
{{ contact_form.next_contact_date }}
|
||||||
|
</div>
|
||||||
|
{% if contact_form.next_contact_date.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ contact_form.next_contact_date.errors|striptags }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="form-text">When to follow up if needed.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label fw-semibold" for="{{ contact_form.notes.id_for_label }}">
|
||||||
|
Notes
|
||||||
|
</label>
|
||||||
|
{{ contact_form.notes }}
|
||||||
|
{% if contact_form.notes.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ contact_form.notes.errors|striptags }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="form-text">Conversation details and any context.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer bg-light">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">Save Contact Log</button>
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save me-1"></i> Save Contact Log
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block js %}
|
||||||
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
|
<script src="{% static 'plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
(function() {
|
||||||
// Initialize date picker for offered_date and next_contact_date
|
// Bootstrap validation
|
||||||
$("#addContactLogModal input[type='date']").datepicker({
|
document.addEventListener('submit', function(e) {
|
||||||
format: 'yyyy-mm-dd',
|
const form = e.target.closest('form.needs-validation');
|
||||||
autoclose: true,
|
if (form) {
|
||||||
todayHighlight: true,
|
if (!form.checkValidity()) {
|
||||||
startDate: 'today'
|
e.preventDefault();
|
||||||
});
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
// Toggle offered date/time and patient response based on appointment_offered checkbox
|
// Helpers
|
||||||
$("input[name='appointment_offered']").change(function() {
|
function initDatePickers(scope) {
|
||||||
const isChecked = $(this).is(':checked');
|
// jQuery datepicker on any date input inside the modal
|
||||||
$("input[name='offered_date']").prop('required', isChecked);
|
const $scope = scope ? $(scope) : $("#addContactLogModal");
|
||||||
$("input[name='offered_time']").prop('required', isChecked);
|
$scope.find("input[type='date']").datepicker({
|
||||||
$("select[name='patient_response']").prop('required', isChecked);
|
|
||||||
}).trigger('change'); // Trigger on load for initial state
|
|
||||||
|
|
||||||
// Handle HTMX after swap to re-initialize datepickers if needed
|
|
||||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
|
||||||
if (event.detail.target.id === 'contact-log-container') {
|
|
||||||
$("#addContactLogModal input[type='date']").datepicker({
|
|
||||||
format: 'yyyy-mm-dd',
|
format: 'yyyy-mm-dd',
|
||||||
autoclose: true,
|
autoclose: true,
|
||||||
todayHighlight: true,
|
todayHighlight: true,
|
||||||
startDate: 'today'
|
startDate: 'today'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleOfferedFields(scope) {
|
||||||
|
const root = scope || document;
|
||||||
|
const offeredSwitch = root.querySelector('#{{ contact_form.appointment_offered.id_for_label }}');
|
||||||
|
const offeredFields = (root.querySelectorAll('.offered-fields')) || [];
|
||||||
|
if (!offeredSwitch) return;
|
||||||
|
|
||||||
|
const on = offeredSwitch.checked;
|
||||||
|
offeredFields.forEach(function(el) {
|
||||||
|
el.style.display = on ? '' : 'none';
|
||||||
|
const inputs = el.querySelectorAll('input, select, textarea');
|
||||||
|
inputs.forEach(function(input) {
|
||||||
|
input.required = !!on;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init on modal show
|
||||||
|
const modalEl = document.getElementById('addContactLogModal');
|
||||||
|
modalEl.addEventListener('shown.bs.modal', function() {
|
||||||
|
initDatePickers(modalEl);
|
||||||
|
toggleOfferedFields(modalEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// React to switch changes
|
||||||
|
modalEl.addEventListener('change', function(e) {
|
||||||
|
if (e.target && e.target.id === '{{ contact_form.appointment_offered.id_for_label }}') {
|
||||||
|
toggleOfferedFields(modalEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-initialize after HTMX swaps (when the contact log list is refreshed)
|
||||||
|
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||||
|
// if we just updated the contact log container, close the modal & reset form
|
||||||
|
if (evt.detail && evt.detail.target && evt.detail.target.id === 'contact-log-container') {
|
||||||
|
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||||
|
if (modal) modal.hide();
|
||||||
|
|
||||||
|
// Optional: reset the form for next use
|
||||||
|
const form = modalEl.querySelector('form');
|
||||||
|
if (form) {
|
||||||
|
form.reset();
|
||||||
|
form.classList.remove('was-validated');
|
||||||
|
toggleOfferedFields(modalEl);
|
||||||
|
initDatePickers(modalEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also handle server-sent HX-Trigger (recommended best practice)
|
||||||
|
document.body.addEventListener('contact-log:added', function() {
|
||||||
|
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||||
|
if (modal) modal.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial setup (if modal already visible)
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initDatePickers(modalEl);
|
||||||
|
toggleOfferedFields(modalEl);
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -3,19 +3,19 @@
|
|||||||
|
|
||||||
{% block title %}{% if object %}Edit{% else %}Add{% endif %} Waiting List Entry{% endblock %}
|
{% block title %}{% if object %}Edit{% else %}Add{% endif %} Waiting List Entry{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block css %}
|
||||||
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
|
||||||
<style>
|
<style>
|
||||||
.required-field::after {
|
.required-field::after {
|
||||||
content: " *";
|
content: " *";
|
||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
}
|
}
|
||||||
.form-section {
|
{#.form-section {#}
|
||||||
border-left: 4px solid #007bff;
|
{# border-left: 4px solid #007bff;#}
|
||||||
padding-left: 1rem;
|
{# padding-left: 1rem;#}
|
||||||
margin-bottom: 2rem;
|
{# margin-bottom: 2rem;#}
|
||||||
}
|
{# }#}
|
||||||
.priority-indicator {
|
.priority-indicator {
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
@ -148,7 +148,7 @@
|
|||||||
<label class="form-label required-field">Urgency Score (1-10)</label>
|
<label class="form-label required-field">Urgency Score (1-10)</label>
|
||||||
{{ form.urgency_score }}
|
{{ form.urgency_score }}
|
||||||
<div class="progress mt-2" style="height: 8px;">
|
<div class="progress mt-2" style="height: 8px;">
|
||||||
<div class="progress-bar bg-success" id="urgency-progress" style="width: 10%"></div>
|
<div class="progress-bar bg-success" id="urgency-progress" aria-valuemax="10" style="width: {{ object.urgency_score }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<small class="form-text text-muted">1 = Routine, 10 = Most Urgent</small>
|
<small class="form-text text-muted">1 = Routine, 10 = Most Urgent</small>
|
||||||
{% if form.urgency_score.errors %}
|
{% if form.urgency_score.errors %}
|
||||||
@ -431,15 +431,15 @@
|
|||||||
<!-- END form panel -->
|
<!-- END form panel -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block js %}
|
||||||
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
|
<script src="{% static 'plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Initialize Select2 for dropdowns
|
// Initialize Select2 for dropdowns
|
||||||
$('.form-select').select2({
|
$('.form-select').select2({
|
||||||
theme: 'bootstrap-5',
|
{#theme: 'bootstrap-5',#}
|
||||||
width: '100%'
|
width: '100%'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ul class="breadcrumb">
|
<ul class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||||
@ -269,7 +268,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add to Waitlist Modal -->
|
<!-- Add to Waitlist Modal -->
|
||||||
<div class="modal fade" id="addToWaitlistModal" tabindex="-1">
|
<div class="modal fade" id="addToWaitlistModal" tabindex="-1">
|
||||||
|
|||||||
@ -13,8 +13,14 @@ urlpatterns = [
|
|||||||
path('requests/', views.AppointmentRequestListView.as_view(), name='appointment_list'),
|
path('requests/', views.AppointmentRequestListView.as_view(), name='appointment_list'),
|
||||||
path('<int:pk>/requests/create/', views.AppointmentRequestCreateView.as_view(), name='appointment_create'),
|
path('<int:pk>/requests/create/', views.AppointmentRequestCreateView.as_view(), name='appointment_create'),
|
||||||
path('requests/<int:pk>/detail/', views.AppointmentRequestDetailView.as_view(), name='appointment_detail'),
|
path('requests/<int:pk>/detail/', views.AppointmentRequestDetailView.as_view(), name='appointment_detail'),
|
||||||
path('calendar/', views.SchedulingCalendarView.as_view(), name='scheduling_calendar'),
|
path('stats/', views.appointment_stats, name='appointment_stats'),
|
||||||
path('queue/', views.QueueManagementView.as_view(), name='queue_management'),
|
|
||||||
|
# Calendar
|
||||||
|
path("calendar/", views.calendar_view, name="calendar"),
|
||||||
|
path("calendar/events/", views.calendar_events, name="calendar_events"),
|
||||||
|
path("<int:pk>/detail-card/", views.appointment_detail_card, name="appointment_detail_card"),
|
||||||
|
# path('calendar/appointments/', views.calendar_appointments, name='calendar_appointments'),
|
||||||
|
|
||||||
|
|
||||||
# Telemedicine
|
# Telemedicine
|
||||||
path('telemedicine/', views.TelemedicineView.as_view(), name='telemedicine'),
|
path('telemedicine/', views.TelemedicineView.as_view(), name='telemedicine'),
|
||||||
@ -25,19 +31,18 @@ urlpatterns = [
|
|||||||
path('telemedicine/<int:pk>/end/', views.end_telemedicine_session, name='stop_telemedicine_session'),
|
path('telemedicine/<int:pk>/end/', views.end_telemedicine_session, name='stop_telemedicine_session'),
|
||||||
path('telemedicine/<int:pk>/cancel/', views.cancel_telemedicine_session, name='cancel_telemedicine_session'),
|
path('telemedicine/<int:pk>/cancel/', views.cancel_telemedicine_session, name='cancel_telemedicine_session'),
|
||||||
|
|
||||||
# HTMX endpoints
|
|
||||||
path('search/', views.appointment_search, name='appointment_search'),
|
|
||||||
path('stats/', views.appointment_stats, name='appointment_stats'),
|
|
||||||
path('calendar/appointments/', views.calendar_appointments, name='calendar_appointments'),
|
|
||||||
path('slots/available/', views.available_slots, name='available_slots'),
|
|
||||||
path('queue/<int:queue_id>/status/', views.queue_status, name='queue_status'),
|
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
path('check-in/<int:appointment_id>/', views.check_in_patient, name='check_in_patient'),
|
path('check-in/<int:appointment_id>/', views.check_in_patient, name='check_in_patient'),
|
||||||
|
path('complete/<int:pk>/', views.complete_appointment, name='complete_appointment'),
|
||||||
|
path('reschedule/<int:pk>/', views.reschedule_appointment, name='reschedule_appointment'),
|
||||||
|
path('cancel/<int:pk>/', views.cancel_appointment, name='cancel_appointment'),
|
||||||
|
path('search/', views.appointment_search, name='appointment_search'),
|
||||||
|
|
||||||
path('queue/<int:queue_id>/call-next/', views.call_next_patient, name='call_next_patient'),
|
# Queue management
|
||||||
|
# path('queue/', views.QueueManagementView.as_view(), name='queue_management'),
|
||||||
path('queue/list/', views.WaitingQueueListView.as_view(), name='waiting_queue_list'),
|
path('queue/', views.WaitingQueueListView.as_view(), name='waiting_queue_list'),
|
||||||
path('queue/create/', views.WaitingQueueCreateView.as_view(), name='waiting_queue_create'),
|
path('queue/create/', views.WaitingQueueCreateView.as_view(), name='waiting_queue_create'),
|
||||||
path('queue/<int:pk>/', views.WaitingQueueDetailView.as_view(), name='waiting_queue_detail'),
|
path('queue/<int:pk>/', views.WaitingQueueDetailView.as_view(), name='waiting_queue_detail'),
|
||||||
path('queue/<int:pk>/update/', views.WaitingQueueUpdateView.as_view(), name='waiting_queue_update'),
|
path('queue/<int:pk>/update/', views.WaitingQueueUpdateView.as_view(), name='waiting_queue_update'),
|
||||||
@ -46,17 +51,18 @@ urlpatterns = [
|
|||||||
path('queue/entry/list/', views.QueueEntryListView.as_view(), name='queue_entry_list'),
|
path('queue/entry/list/', views.QueueEntryListView.as_view(), name='queue_entry_list'),
|
||||||
path('queue/entry/<int:pk>/', views.QueueEntryDetailView.as_view(), name='queue_entry_detail'),
|
path('queue/entry/<int:pk>/', views.QueueEntryDetailView.as_view(), name='queue_entry_detail'),
|
||||||
path('queue/entry/<int:pk>/update/', views.QueueEntryUpdateView.as_view(), name='queue_entry_update'),
|
path('queue/entry/<int:pk>/update/', views.QueueEntryUpdateView.as_view(), name='queue_entry_update'),
|
||||||
|
path('queue/<int:queue_id>/call-next/', views.next_in_queue, name='call_next_patient'),
|
||||||
|
path('queue/<int:queue_id>/status/', views.queue_status, name='queue_status'),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
path('complete/<int:appointment_id>/', views.complete_appointment, name='complete_appointment'),
|
|
||||||
path('reschedule/<int:pk>/', views.reschedule_appointment, name='reschedule_appointment'),
|
|
||||||
path('cancel/<int:appointment_id>/', views.cancel_appointment, name='cancel_appointment'),
|
|
||||||
|
|
||||||
path('slots/',views.SlotAvailabilityListView.as_view(), name='slot_list'),
|
path('slots/',views.SlotAvailabilityListView.as_view(), name='slot_list'),
|
||||||
path('slots/<int:pk>/',views.SlotAvailabilityDetailView.as_view(), name='slot_detail'),
|
path('slots/<int:pk>/',views.SlotAvailabilityDetailView.as_view(), name='slot_detail'),
|
||||||
path('slots/create/',views.SlotAvailabilityCreateView.as_view(), name='slot_create'),
|
path('slots/create/',views.SlotAvailabilityCreateView.as_view(), name='slot_create'),
|
||||||
path('slots/<int:pk>/update/',views.SlotAvailabilityUpdateView.as_view(), name='slot_update'),
|
path('slots/<int:pk>/update/',views.SlotAvailabilityUpdateView.as_view(), name='slot_update'),
|
||||||
path('slots/<int:pk>/delete/',views.SlotAvailabilityDeleteView.as_view(), name='slot_delete'),
|
path('slots/<int:pk>/delete/',views.SlotAvailabilityDeleteView.as_view(), name='slot_delete'),
|
||||||
|
path('slots/available/', views.available_slots, name='available_slots'),
|
||||||
|
|
||||||
path('templates/', views.AppointmentTemplateListView.as_view(), name='appointment_template_list'),
|
path('templates/', views.AppointmentTemplateListView.as_view(), name='appointment_template_list'),
|
||||||
path('templates/<int:pk>/', views.AppointmentTemplateDetailView.as_view(), name='appointment_template_detail'),
|
path('templates/<int:pk>/', views.AppointmentTemplateDetailView.as_view(), name='appointment_template_detail'),
|
||||||
@ -76,6 +82,6 @@ urlpatterns = [
|
|||||||
path('waiting-list/stats/', views.waiting_list_stats, name='waiting_list_stats'),
|
path('waiting-list/stats/', views.waiting_list_stats, name='waiting_list_stats'),
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
# path('api/', include('appointments.api.urls')),
|
path('api/', include('appointments.api.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
Appointments app views for hospital management system with comprehensive CRUD operations.
|
Appointments app views for hospital management system with comprehensive CRUD operations.
|
||||||
"""
|
"""
|
||||||
|
from django.contrib.messages import success
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||||
|
from django.template.defaulttags import csrf_token
|
||||||
|
from django.utils.dateparse import parse_datetime
|
||||||
|
from django.views.decorators.http import require_GET, require_POST
|
||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView
|
TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView
|
||||||
)
|
)
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse, HttpResponseBadRequest
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db.models.functions import Now
|
from django.db.models.functions import Now
|
||||||
from django.db.models import Q, Count, Avg, Case, When, Value, DurationField, FloatField, F, ExpressionWrapper, IntegerField
|
from django.db.models import Q, Count, Avg, Case, When, Value, DurationField, FloatField, F, ExpressionWrapper, IntegerField
|
||||||
@ -53,14 +56,17 @@ class AppointmentDashboardView(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
# Statistics
|
# Statistics
|
||||||
context['stats'] = {
|
context['stats'] = {
|
||||||
|
'total_appointments': AppointmentRequest.objects.filter(
|
||||||
|
tenant=tenant,
|
||||||
|
).count(),
|
||||||
'total_appointments_today': AppointmentRequest.objects.filter(
|
'total_appointments_today': AppointmentRequest.objects.filter(
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
scheduled_datetime__date=today
|
scheduled_datetime__date=today
|
||||||
).count(),
|
).count(),
|
||||||
'confirmed_appointments': AppointmentRequest.objects.filter(
|
'pending_appointments': AppointmentRequest.objects.filter(
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
scheduled_datetime__date=today,
|
scheduled_datetime__date=today,
|
||||||
status='CONFIRMED'
|
status='PENDING'
|
||||||
).count(),
|
).count(),
|
||||||
'active_queues_count': WaitingQueue.objects.filter(
|
'active_queues_count': WaitingQueue.objects.filter(
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
@ -179,7 +185,7 @@ class AppointmentRequestDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class AppointmentRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
class AppointmentRequestCreateView(LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create new appointment request.
|
Create new appointment request.
|
||||||
"""
|
"""
|
||||||
@ -239,7 +245,7 @@ class AppointmentRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class AppointmentRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
class AppointmentRequestUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Update appointment request (limited fields after scheduling).
|
Update appointment request (limited fields after scheduling).
|
||||||
"""
|
"""
|
||||||
@ -281,7 +287,7 @@ class AppointmentRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class AppointmentRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
class AppointmentRequestDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
"""
|
"""
|
||||||
Cancel appointment request.
|
Cancel appointment request.
|
||||||
"""
|
"""
|
||||||
@ -394,7 +400,7 @@ class SlotAvailabilityDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class SlotAvailabilityCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
class SlotAvailabilityCreateView(LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create new slot availability.
|
Create new slot availability.
|
||||||
"""
|
"""
|
||||||
@ -428,7 +434,7 @@ class SlotAvailabilityCreateView(LoginRequiredMixin, PermissionRequiredMixin, Cr
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class SlotAvailabilityUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
class SlotAvailabilityUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Update slot availability.
|
Update slot availability.
|
||||||
"""
|
"""
|
||||||
@ -470,7 +476,7 @@ class SlotAvailabilityUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Up
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class SlotAvailabilityDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
class SlotAvailabilityDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
"""
|
"""
|
||||||
Delete slot availability.
|
Delete slot availability.
|
||||||
"""
|
"""
|
||||||
@ -599,6 +605,8 @@ class WaitingQueueDetailView(LoginRequiredMixin, DetailView):
|
|||||||
'total_entries': QueueEntry.objects.filter(queue=queue).count(),
|
'total_entries': QueueEntry.objects.filter(queue=queue).count(),
|
||||||
'waiting_entries': QueueEntry.objects.filter(queue=queue, status='WAITING').count(),
|
'waiting_entries': QueueEntry.objects.filter(queue=queue, status='WAITING').count(),
|
||||||
'in_progress_entries': QueueEntry.objects.filter(queue=queue, status='IN_PROGRESS').count(),
|
'in_progress_entries': QueueEntry.objects.filter(queue=queue, status='IN_PROGRESS').count(),
|
||||||
|
'served_today': QueueEntry.objects.filter(queue=queue, status='COMPLETED').count(),
|
||||||
|
'no_show_entries': QueueEntry.objects.filter(queue=queue, status='NO_SHOW').count(),
|
||||||
# 'average_wait_time': QueueEntry.objects.filter(
|
# 'average_wait_time': QueueEntry.objects.filter(
|
||||||
# queue=queue,
|
# queue=queue,
|
||||||
# status='COMPLETED'
|
# status='COMPLETED'
|
||||||
@ -608,7 +616,7 @@ class WaitingQueueDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class WaitingQueueCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
class WaitingQueueCreateView(LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create new waiting queue.
|
Create new waiting queue.
|
||||||
"""
|
"""
|
||||||
@ -644,7 +652,7 @@ class WaitingQueueCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class WaitingQueueUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
class WaitingQueueUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Update waiting queue.
|
Update waiting queue.
|
||||||
"""
|
"""
|
||||||
@ -686,7 +694,7 @@ class WaitingQueueUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class WaitingQueueDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
class WaitingQueueDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
"""
|
"""
|
||||||
Delete waiting queue.
|
Delete waiting queue.
|
||||||
"""
|
"""
|
||||||
@ -796,7 +804,7 @@ class QueueEntryDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return QueueEntry.objects.filter(queue__tenant=tenant)
|
return QueueEntry.objects.filter(queue__tenant=tenant)
|
||||||
|
|
||||||
|
|
||||||
class QueueEntryCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
class QueueEntryCreateView(LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create new queue entry.
|
Create new queue entry.
|
||||||
"""
|
"""
|
||||||
@ -836,7 +844,7 @@ class QueueEntryCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class QueueEntryUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
class QueueEntryUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Update queue entry.
|
Update queue entry.
|
||||||
"""
|
"""
|
||||||
@ -883,7 +891,7 @@ class TelemedicineSessionListView(LoginRequiredMixin, ListView):
|
|||||||
List telemedicine sessions.
|
List telemedicine sessions.
|
||||||
"""
|
"""
|
||||||
model = TelemedicineSession
|
model = TelemedicineSession
|
||||||
template_name = 'appointments/telemedicine_session_list.html'
|
template_name = 'appointments/telemedicine/telemedicine.html'
|
||||||
context_object_name = 'sessions'
|
context_object_name = 'sessions'
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
@ -941,13 +949,13 @@ class TelemedicineSessionDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return TelemedicineSession.objects.filter(appointment__tenant=tenant)
|
return TelemedicineSession.objects.filter(appointment__tenant=tenant)
|
||||||
|
|
||||||
|
|
||||||
class TelemedicineSessionCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
class TelemedicineSessionCreateView(LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create new telemedicine session.
|
Create new telemedicine session.
|
||||||
"""
|
"""
|
||||||
model = TelemedicineSession
|
model = TelemedicineSession
|
||||||
form_class = TelemedicineSessionForm
|
form_class = TelemedicineSessionForm
|
||||||
template_name = 'appointments/telemedicine_session_form.html'
|
template_name = 'appointments/telemedicine/telemedicine_session_form.html'
|
||||||
permission_required = 'appointments.add_telemedicinesession'
|
permission_required = 'appointments.add_telemedicinesession'
|
||||||
success_url = reverse_lazy('appointments:telemedicine_session_list')
|
success_url = reverse_lazy('appointments:telemedicine_session_list')
|
||||||
|
|
||||||
@ -975,13 +983,13 @@ class TelemedicineSessionCreateView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class TelemedicineSessionUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
class TelemedicineSessionUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Update telemedicine session.
|
Update telemedicine session.
|
||||||
"""
|
"""
|
||||||
model = TelemedicineSession
|
model = TelemedicineSession
|
||||||
form_class = TelemedicineSessionForm
|
form_class = TelemedicineSessionForm
|
||||||
template_name = 'appointments/telemedicine_session_form.html'
|
template_name = 'appointments/telemedicine/telemedicine_session_form.html'
|
||||||
permission_required = 'appointments.change_telemedicinesession'
|
permission_required = 'appointments.change_telemedicinesession'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -1096,13 +1104,13 @@ class AppointmentTemplateDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return AppointmentTemplate.objects.filter(tenant=tenant)
|
return AppointmentTemplate.objects.filter(tenant=tenant)
|
||||||
|
|
||||||
|
|
||||||
class AppointmentTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
class AppointmentTemplateCreateView(LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create new appointment template.
|
Create new appointment template.
|
||||||
"""
|
"""
|
||||||
model = AppointmentTemplate
|
model = AppointmentTemplate
|
||||||
form_class = AppointmentTemplateForm
|
form_class = AppointmentTemplateForm
|
||||||
template_name = 'appointments/appointment_template_form.html'
|
template_name = 'appointments/templates/appointment_template_form.html'
|
||||||
permission_required = 'appointments.add_appointmenttemplate'
|
permission_required = 'appointments.add_appointmenttemplate'
|
||||||
success_url = reverse_lazy('appointments:appointment_template_list')
|
success_url = reverse_lazy('appointments:appointment_template_list')
|
||||||
|
|
||||||
@ -1132,7 +1140,7 @@ class AppointmentTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class AppointmentTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
class AppointmentTemplateUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Update appointment template.
|
Update appointment template.
|
||||||
"""
|
"""
|
||||||
@ -1174,7 +1182,7 @@ class AppointmentTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class AppointmentTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
class AppointmentTemplateDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
"""
|
"""
|
||||||
Delete appointment template.
|
Delete appointment template.
|
||||||
"""
|
"""
|
||||||
@ -1292,7 +1300,7 @@ class WaitingListDetailView(LoginRequiredMixin, DetailView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return WaitingList.objects.filter(
|
return WaitingList.objects.filter(
|
||||||
tenant=getattr(self.request.user, 'current_tenant', None)
|
tenant=getattr(self.request.user, 'tenant', None)
|
||||||
).select_related(
|
).select_related(
|
||||||
'patient', 'department', 'provider', 'scheduled_appointment',
|
'patient', 'department', 'provider', 'scheduled_appointment',
|
||||||
'created_by', 'removed_by'
|
'created_by', 'removed_by'
|
||||||
@ -1325,14 +1333,13 @@ class WaitingListCreateView(LoginRequiredMixin, CreateView):
|
|||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
kwargs['tenant'] = getattr(self.request.user, 'current_tenant', None)
|
kwargs['tenant'] = getattr(self.request.user, 'tenant', None)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
form.instance.tenant = getattr(self.request.user, 'current_tenant', None)
|
form.instance.tenant = getattr(self.request.user, 'tenant', None)
|
||||||
form.instance.created_by = self.request.user
|
form.instance.created_by = self.request.user
|
||||||
|
|
||||||
# Calculate initial position and estimated wait time
|
|
||||||
response = super().form_valid(form)
|
response = super().form_valid(form)
|
||||||
self.object.update_position()
|
self.object.update_position()
|
||||||
self.object.estimated_wait_time = self.object.estimate_wait_time()
|
self.object.estimated_wait_time = self.object.estimate_wait_time()
|
||||||
@ -1355,13 +1362,14 @@ class WaitingListUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
template_name = 'appointments/waiting_list/waiting_list_form.html'
|
template_name = 'appointments/waiting_list/waiting_list_form.html'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
tenant = self.request.user.tenant
|
||||||
return WaitingList.objects.filter(
|
return WaitingList.objects.filter(
|
||||||
tenant=getattr(self.request.user, 'current_tenant', None)
|
tenant=tenant
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
kwargs['tenant'] = getattr(self.request.user, 'current_tenant', None)
|
kwargs['tenant'] = getattr(self.request.user, 'tenant', None)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@ -1394,7 +1402,7 @@ class WaitingListDeleteView(LoginRequiredMixin, DeleteView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return WaitingList.objects.filter(
|
return WaitingList.objects.filter(
|
||||||
tenant=getattr(self.request.user, 'current_tenant', None)
|
tenant=getattr(self.request.user, 'tenant', None)
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, *args, **kwargs):
|
def delete(self, request, *args, **kwargs):
|
||||||
@ -1424,7 +1432,7 @@ def add_contact_log(request, pk):
|
|||||||
entry = get_object_or_404(
|
entry = get_object_or_404(
|
||||||
WaitingList,
|
WaitingList,
|
||||||
pk=pk,
|
pk=pk,
|
||||||
tenant=getattr(request.user, 'current_tenant', None)
|
tenant=getattr(request.user, 'tenant', None)
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@ -1473,7 +1481,7 @@ def waiting_list_bulk_action(request):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = WaitingListBulkActionForm(
|
form = WaitingListBulkActionForm(
|
||||||
request.POST,
|
request.POST,
|
||||||
tenant=getattr(request.user, 'current_tenant', None)
|
tenant=getattr(request.user, 'tenant', None)
|
||||||
)
|
)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@ -1486,7 +1494,7 @@ def waiting_list_bulk_action(request):
|
|||||||
|
|
||||||
entries = WaitingList.objects.filter(
|
entries = WaitingList.objects.filter(
|
||||||
id__in=entry_ids,
|
id__in=entry_ids,
|
||||||
tenant=getattr(request.user, 'current_tenant', None)
|
tenant=getattr(request.user, 'tenant', None)
|
||||||
)
|
)
|
||||||
|
|
||||||
if action == 'contact':
|
if action == 'contact':
|
||||||
@ -1533,7 +1541,7 @@ def waiting_list_stats(request):
|
|||||||
"""
|
"""
|
||||||
HTMX endpoint for waiting list statistics.
|
HTMX endpoint for waiting list statistics.
|
||||||
"""
|
"""
|
||||||
tenant = getattr(request.user, 'current_tenant', None)
|
tenant = getattr(request.user, 'tenant', None)
|
||||||
if not tenant:
|
if not tenant:
|
||||||
return JsonResponse({'error': 'No tenant'})
|
return JsonResponse({'error': 'No tenant'})
|
||||||
|
|
||||||
@ -1546,9 +1554,9 @@ def waiting_list_stats(request):
|
|||||||
'scheduled': waiting_list.filter(status='SCHEDULED').count(),
|
'scheduled': waiting_list.filter(status='SCHEDULED').count(),
|
||||||
'urgent': waiting_list.filter(priority__in=['URGENT', 'STAT', 'EMERGENCY']).count(),
|
'urgent': waiting_list.filter(priority__in=['URGENT', 'STAT', 'EMERGENCY']).count(),
|
||||||
'overdue_contact': sum(1 for entry in waiting_list.filter(status='ACTIVE') if entry.is_overdue_contact),
|
'overdue_contact': sum(1 for entry in waiting_list.filter(status='ACTIVE') if entry.is_overdue_contact),
|
||||||
'avg_wait_days': int(waiting_list.aggregate(
|
# 'avg_wait_days': int(waiting_list.aggregate(
|
||||||
avg_days=Avg(timezone.now().date() - F('created_at__date'))
|
# avg_days=Avg(timezone.now().date() - F('created_at__date'))
|
||||||
)['avg_days'] or 0),
|
# )['avg_days'] or 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse(stats)
|
return JsonResponse(stats)
|
||||||
@ -1595,14 +1603,17 @@ def appointment_stats(request):
|
|||||||
|
|
||||||
# Calculate appointment statistics
|
# Calculate appointment statistics
|
||||||
stats = {
|
stats = {
|
||||||
|
'total_appointments': AppointmentRequest.objects.filter(
|
||||||
|
tenant=tenant,
|
||||||
|
).count(),
|
||||||
'total_appointments_today': AppointmentRequest.objects.filter(
|
'total_appointments_today': AppointmentRequest.objects.filter(
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
scheduled_datetime__date=today
|
scheduled_datetime__date=today
|
||||||
).count(),
|
).count(),
|
||||||
'confirmed_appointments': AppointmentRequest.objects.filter(
|
'pending_appointments': AppointmentRequest.objects.filter(
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
scheduled_datetime__date=today,
|
scheduled_datetime__date=today,
|
||||||
status='CONFIRMED'
|
status='PENDING'
|
||||||
).count(),
|
).count(),
|
||||||
'completed_appointments': AppointmentRequest.objects.filter(
|
'completed_appointments': AppointmentRequest.objects.filter(
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
@ -1649,22 +1660,16 @@ def available_slots(request):
|
|||||||
selected_date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
selected_date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return render(request, 'appointments/partials/available_slots.html', status=400)
|
return render(request, 'appointments/partials/available_slots.html', status=400)
|
||||||
print(selected_date)
|
|
||||||
provider = get_object_or_404(User, pk=provider_id)
|
provider = get_object_or_404(User, pk=provider_id)
|
||||||
print(provider)
|
|
||||||
slots = SlotAvailability.objects.filter(
|
slots = SlotAvailability.objects.filter(
|
||||||
provider=provider,
|
provider=provider,
|
||||||
provider__tenant=tenant,
|
provider__tenant=tenant,
|
||||||
date=selected_date,
|
date=selected_date,
|
||||||
).order_by('start_time')
|
).order_by('start_time')
|
||||||
if slots:
|
|
||||||
print(slots)
|
|
||||||
else:
|
|
||||||
print('no slots')
|
|
||||||
# current_excluded = qs.exclude(pk=exclude_id)
|
|
||||||
# print(current_excluded)
|
|
||||||
|
|
||||||
return render(request, 'appointments/partials/available_slots.html', {'slots': slots}, status=200)
|
current_excluded = slots.exclude(pk=exclude_id)
|
||||||
|
|
||||||
|
return render(request, 'appointments/partials/available_slots.html', {'slots': current_excluded}, status=200)
|
||||||
# def available_slots(request):
|
# def available_slots(request):
|
||||||
# """
|
# """
|
||||||
# HTMX view for available slots.
|
# HTMX view for available slots.
|
||||||
@ -1846,10 +1851,10 @@ def confirm_appointment(request, pk):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def reschedule_appointment(request, pk):
|
def reschedule_appointment(request, pk):
|
||||||
tenant = getattr(request.user, 'tenant', None)
|
tenant = request.user.tenant
|
||||||
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
|
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
|
||||||
providers = User.objects.filter(
|
providers = User.objects.filter(
|
||||||
tenant=tenant, role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
tenant=tenant, employee_profile__role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
||||||
).order_by('last_name', 'first_name')
|
).order_by('last_name', 'first_name')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@ -1884,6 +1889,18 @@ def reschedule_appointment(request, pk):
|
|||||||
# optionally send notifications if notify_patient is True
|
# optionally send notifications if notify_patient is True
|
||||||
|
|
||||||
messages.success(request, 'Appointment has been rescheduled.')
|
messages.success(request, 'Appointment has been rescheduled.')
|
||||||
|
|
||||||
|
AuditLogger.log_event(
|
||||||
|
tenant=tenant,
|
||||||
|
event_type='UPDATE',
|
||||||
|
event_category='APPOINTMENT_MANAGEMENT',
|
||||||
|
action='Reschedule Appointment',
|
||||||
|
description=f'Rescheduled appointment: {appointment.patient} with {appointment.provider}',
|
||||||
|
user=request.user,
|
||||||
|
content_object=appointment,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
return redirect('appointments:appointment_detail', pk=appointment.pk)
|
return redirect('appointments:appointment_detail', pk=appointment.pk)
|
||||||
|
|
||||||
return render(request, 'appointments/reschedule_appointment.html', {
|
return render(request, 'appointments/reschedule_appointment.html', {
|
||||||
@ -1922,13 +1939,13 @@ def cancel_appointment(request, pk):
|
|||||||
"""
|
"""
|
||||||
Complete an appointment.
|
Complete an appointment.
|
||||||
"""
|
"""
|
||||||
tenant = getattr(request, 'tenant', None)
|
tenant = request.user.tenant
|
||||||
if not tenant:
|
if not tenant:
|
||||||
messages.error(request, 'No tenant found.')
|
messages.error(request, 'No tenant found.')
|
||||||
return redirect('appointments:appointment_request_list')
|
return redirect('appointments:appointment_request_list')
|
||||||
|
|
||||||
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
|
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
|
||||||
|
if appointment.status == 'SCHEDULED':
|
||||||
appointment.status = 'CANCELLED'
|
appointment.status = 'CANCELLED'
|
||||||
# appointment.actual_end_time = timezone.now()
|
# appointment.actual_end_time = timezone.now()
|
||||||
appointment.save()
|
appointment.save()
|
||||||
@ -1946,7 +1963,11 @@ def cancel_appointment(request, pk):
|
|||||||
)
|
)
|
||||||
|
|
||||||
messages.success(request, f'Appointment for {appointment.patient} cancelled successfully.')
|
messages.success(request, f'Appointment for {appointment.patient} cancelled successfully.')
|
||||||
return redirect('appointments:appointment_request_detail', pk=pk)
|
return redirect('appointments:appointment_detail', pk=pk)
|
||||||
|
|
||||||
|
return render(request, 'appointments/cancel_appointment.html', {
|
||||||
|
'appointment': appointment,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -2028,13 +2049,13 @@ def next_in_queue(request, queue_id):
|
|||||||
next_entry = QueueEntry.objects.filter(
|
next_entry = QueueEntry.objects.filter(
|
||||||
queue=queue,
|
queue=queue,
|
||||||
status='WAITING'
|
status='WAITING'
|
||||||
).order_by('position', 'created_at').first()
|
).order_by('queue_position', 'called_at').first()
|
||||||
|
|
||||||
if next_entry:
|
if next_entry:
|
||||||
next_entry.status = 'IN_PROGRESS'
|
next_entry.status = 'IN_SERVICE'
|
||||||
next_entry.called_at = timezone.now()
|
next_entry.called_at = timezone.now()
|
||||||
next_entry.save()
|
next_entry.save()
|
||||||
|
messages.success(request, f"Patient has been called in for appointment.")
|
||||||
# Log queue progression
|
# Log queue progression
|
||||||
AuditLogger.log_event(
|
AuditLogger.log_event(
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
@ -2047,13 +2068,10 @@ def next_in_queue(request, queue_id):
|
|||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
|
|
||||||
return JsonResponse({
|
return redirect('appointments:waiting_queue_detail', pk=queue.pk)
|
||||||
'status': 'success',
|
|
||||||
'patient': str(next_entry.patient),
|
|
||||||
'position': next_entry.queue_position
|
|
||||||
})
|
|
||||||
else:
|
else:
|
||||||
return JsonResponse({'status': 'no_patients'})
|
messages.error(request, f"No more patients in queue.")
|
||||||
|
return redirect('appointments:waiting_queue_detail', pk=queue.pk)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -2070,17 +2088,7 @@ def check_in_patient(request, appointment_id):
|
|||||||
appointment.save()
|
appointment.save()
|
||||||
|
|
||||||
messages.success(request, f"Patient {appointment.patient} has been checked in.")
|
messages.success(request, f"Patient {appointment.patient} has been checked in.")
|
||||||
return redirect('appointments:queue_management')
|
return redirect('appointments:waiting_queue_list')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def call_next_patient(request, queue_id):
|
|
||||||
"""
|
|
||||||
Call the next patient in the queue.
|
|
||||||
"""
|
|
||||||
# Mock implementation - in real system, this would manage actual queue
|
|
||||||
messages.success(request, 'Next patient has been called.')
|
|
||||||
return redirect('appointments:queue_management')
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -2107,6 +2115,7 @@ def complete_queue_entry(request, pk):
|
|||||||
queue_entry.actual_wait_time_minutes = int(wait_time)
|
queue_entry.actual_wait_time_minutes = int(wait_time)
|
||||||
|
|
||||||
queue_entry.save()
|
queue_entry.save()
|
||||||
|
messages.success(request, f"Queue entry {queue_entry.pk} completed successfully.")
|
||||||
|
|
||||||
# Log completion
|
# Log completion
|
||||||
AuditLogger.log_event(
|
AuditLogger.log_event(
|
||||||
@ -2120,7 +2129,7 @@ def complete_queue_entry(request, pk):
|
|||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
|
|
||||||
return JsonResponse({'status': 'completed'})
|
return redirect('appointments:waiting_queue_detail', pk=queue_entry.queue.pk)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -2263,18 +2272,21 @@ def cancel_telemedicine_session(request, pk):
|
|||||||
# )
|
# )
|
||||||
|
|
||||||
|
|
||||||
class SchedulingCalendarView(LoginRequiredMixin, TemplateView):
|
class SchedulingCalendarView(LoginRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
Calendar view for scheduling appointments.
|
Calendar view for scheduling appointments.
|
||||||
"""
|
"""
|
||||||
|
model = AppointmentRequest
|
||||||
template_name = 'appointments/scheduling_calendar.html'
|
template_name = 'appointments/scheduling_calendar.html'
|
||||||
|
context_object_name = 'appointments'
|
||||||
|
paginate_by = 20
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['appointments'] = AppointmentRequest.objects.filter(
|
context['appointments'] = AppointmentRequest.objects.filter(
|
||||||
tenant=self.request.user.tenant,
|
tenant=self.request.user.tenant,
|
||||||
status='SCHEDULED'
|
status='SCHEDULED'
|
||||||
)
|
).select_related('patient', 'provider').order_by('-scheduled_datetime')
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -2327,6 +2339,126 @@ class TelemedicineView(LoginRequiredMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def calendar_view(request):
|
||||||
|
"""Renders the calendar page"""
|
||||||
|
return render(request, "appointments/calendar.html")
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_GET
|
||||||
|
def calendar_events(request):
|
||||||
|
"""
|
||||||
|
FullCalendar event feed (GET /calendar/events?start=..&end=..[&provider_id=&status=...])
|
||||||
|
FullCalendar sends ISO timestamps; we return a list of event dicts.
|
||||||
|
"""
|
||||||
|
STATUS_COLORS = {
|
||||||
|
"PENDING": {"bg": "#f59c1a", "border": "#d08916"},
|
||||||
|
"CONFIRMED": {"bg": "#49b6d6", "border": "#3f9db9"},
|
||||||
|
"CHECKED_IN": {"bg": "#348fe2", "border": "#2c79bf"},
|
||||||
|
"IN_PROGRESS": {"bg": "#00acac", "border": "#009494"},
|
||||||
|
"COMPLETED": {"bg": "#32a932", "border": "#298a29"},
|
||||||
|
"CANCELLED": {"bg": "#ff5b57", "border": "#d64d4a"},
|
||||||
|
"NO_SHOW": {"bg": "#6c757d", "border": "#5a636b"},
|
||||||
|
}
|
||||||
|
|
||||||
|
tenant = request.user.tenant
|
||||||
|
if not tenant:
|
||||||
|
return JsonResponse([], safe=False)
|
||||||
|
|
||||||
|
start = request.GET.get("start")
|
||||||
|
end = request.GET.get("end")
|
||||||
|
provider_id = request.GET.get("provider_id")
|
||||||
|
status = request.GET.get("status")
|
||||||
|
|
||||||
|
if not start or not end:
|
||||||
|
return HttpResponseBadRequest("Missing start/end")
|
||||||
|
|
||||||
|
# Parse (FullCalendar uses ISO 8601)
|
||||||
|
# They can include timezone; parse_datetime handles offsets.
|
||||||
|
start_dt = parse_datetime(start)
|
||||||
|
end_dt = parse_datetime(end)
|
||||||
|
if not start_dt or not end_dt:
|
||||||
|
return HttpResponseBadRequest("Invalid start/end")
|
||||||
|
|
||||||
|
qs = AppointmentRequest.objects.filter(
|
||||||
|
tenant=tenant,
|
||||||
|
scheduled_datetime__gte=start_dt,
|
||||||
|
scheduled_datetime__lt=end_dt,
|
||||||
|
).select_related("patient", "provider")
|
||||||
|
|
||||||
|
if provider_id:
|
||||||
|
qs = qs.filter(provider_id=provider_id)
|
||||||
|
if status:
|
||||||
|
qs = qs.filter(status=status)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
for appt in qs:
|
||||||
|
color = STATUS_COLORS.get(appt.status, {"bg": "#495057", "border": "#3e444a"})
|
||||||
|
title = f"{appt.patient.get_full_name()} • {appt.get_appointment_type_display()}"
|
||||||
|
if appt.is_telemedicine:
|
||||||
|
title = "📹 " + title
|
||||||
|
|
||||||
|
# If you store end time separately, use it; else estimate duration (e.g., 30 min)
|
||||||
|
end_time = getattr(appt, "end_datetime", None)
|
||||||
|
if not end_time:
|
||||||
|
end_time = appt.scheduled_datetime + timedelta(minutes=getattr(appt, "duration_minutes", 30))
|
||||||
|
|
||||||
|
events.append({
|
||||||
|
"id": str(appt.pk),
|
||||||
|
"title": title,
|
||||||
|
"start": appt.scheduled_datetime.isoformat(),
|
||||||
|
"end": end_time.isoformat(),
|
||||||
|
"backgroundColor": color["bg"],
|
||||||
|
"borderColor": color["border"],
|
||||||
|
"textColor": "#fff",
|
||||||
|
"extendedProps": {
|
||||||
|
"status": appt.status,
|
||||||
|
"provider": appt.provider.get_full_name() if appt.provider_id else "",
|
||||||
|
"chief_complaint": (appt.chief_complaint or "")[:120],
|
||||||
|
"telemedicine": appt.is_telemedicine,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse(events, safe=False)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def appointment_detail_card(request, pk):
|
||||||
|
tenant = request.user.tenant
|
||||||
|
"""HTMX partial with appointment quick details for the sidebar/modal."""
|
||||||
|
appt = get_object_or_404(AppointmentRequest.objects.select_related("patient","provider"), pk=pk, tenant=tenant)
|
||||||
|
return render(request, "appointments/partials/appointment_detail_card.html", {"appointment": appt})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("appointments.change_appointment")
|
||||||
|
@require_POST
|
||||||
|
def appointment_reschedule(request, pk):
|
||||||
|
"""
|
||||||
|
Handle drag/drop or resize from FullCalendar.
|
||||||
|
Expect JSON: {"start":"...", "end":"..."} ISO strings (local/offset).
|
||||||
|
"""
|
||||||
|
tenant = request.user.tenant
|
||||||
|
appt = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
|
||||||
|
try:
|
||||||
|
data = request.POST if request.content_type == "application/x-www-form-urlencoded" else request.json()
|
||||||
|
except Exception:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
start = data.get("start")
|
||||||
|
end = data.get("end")
|
||||||
|
start_dt = parse_datetime(start) if start else None
|
||||||
|
end_dt = parse_datetime(end) if end else None
|
||||||
|
if not start_dt or not end_dt:
|
||||||
|
return HttpResponseBadRequest("Invalid start/end")
|
||||||
|
|
||||||
|
appt.scheduled_datetime = start_dt
|
||||||
|
if hasattr(appt, "end_datetime"):
|
||||||
|
appt.end_datetime = end_dt
|
||||||
|
elif hasattr(appt, "duration_minutes"):
|
||||||
|
appt.duration_minutes = int((end_dt - start_dt).total_seconds() // 60)
|
||||||
|
appt.save(update_fields=["scheduled_datetime"] + (["end_datetime"] if hasattr(appt,"end_datetime") else ["duration_minutes"]))
|
||||||
|
|
||||||
|
return JsonResponse({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@ -429,7 +429,7 @@ class InsuranceClaimAdmin(admin.ModelAdmin):
|
|||||||
else:
|
else:
|
||||||
color = 'red'
|
color = 'red'
|
||||||
return format_html(
|
return format_html(
|
||||||
'<span style="color: {};">{:.1f}%</span>',
|
'<span style="color: {};">{}%</span>',
|
||||||
color, percentage
|
color, percentage
|
||||||
)
|
)
|
||||||
payment_percentage_display.short_description = 'Paid %'
|
payment_percentage_display.short_description = 'Paid %'
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
# Generated by Django 5.2.6 on 2025-09-15 14:05
|
||||||
|
|
||||||
import billing.utils
|
import billing.utils
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-09-08 07:28
|
# Generated by Django 5.2.6 on 2025-09-15 14:05
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -4,12 +4,11 @@
|
|||||||
{% block title %}Export Bills{% endblock %}
|
{% block title %}Export Bills{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ul class="breadcrumb">
|
<ul class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||||
@ -289,12 +288,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
{% block title %}Submit Bill{% endblock %}
|
{% block title %}Submit Bill{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="app-content">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-xl-8">
|
<div class="col-xl-8">
|
||||||
@ -270,7 +269,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -199,7 +199,7 @@ def create_saudi_medical_bills():
|
|||||||
users = list(User.objects.filter(is_active=True))
|
users = list(User.objects.filter(is_active=True))
|
||||||
provider_users = list(User.objects.filter(
|
provider_users = list(User.objects.filter(
|
||||||
is_active=True,
|
is_active=True,
|
||||||
role__in=['PHYSICIAN', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'RADIOLOGIST']
|
employee_profile__role__in=['PHYSICIAN', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'RADIOLOGIST']
|
||||||
))
|
))
|
||||||
|
|
||||||
if not provider_users:
|
if not provider_users:
|
||||||
@ -298,7 +298,7 @@ def create_saudi_bill_line_items(medical_bills):
|
|||||||
|
|
||||||
provider_users = list(User.objects.filter(
|
provider_users = list(User.objects.filter(
|
||||||
is_active=True,
|
is_active=True,
|
||||||
role__in=['PHYSICIAN', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'RADIOLOGIST']
|
employee_profile__role__in=['PHYSICIAN', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'RADIOLOGIST']
|
||||||
)) or list(User.objects.filter(is_active=True))
|
)) or list(User.objects.filter(is_active=True))
|
||||||
|
|
||||||
created_line_items = []
|
created_line_items = []
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user