This commit is contained in:
Marwan Alwali 2025-09-16 15:10:57 +03:00
parent beba30e532
commit 2780a2dc7c
234 changed files with 42726 additions and 11695 deletions

Binary file not shown.

View File

@ -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):

View File

@ -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

View File

@ -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'),

View File

@ -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"],
}, },

View File

@ -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(

View 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"),
),
]

View File

@ -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
View 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)

View File

@ -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>

View File

@ -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)

View File

@ -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,
} }

View File

@ -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

View File

@ -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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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.

View File

@ -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()
}), }),

View File

@ -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=[

View File

@ -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(

View File

@ -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,
),
),
]

View File

@ -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"
),
),
]

View File

@ -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)'
) )

Binary file not shown.

View File

@ -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 %}

View File

@ -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 %}

View 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 Djangos 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 %}

View File

@ -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">

View File

@ -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">&#xea;</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() {

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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() {

View File

@ -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">

View File

@ -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 %}

View File

@ -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

View File

@ -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}`;#}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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();

View File

@ -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 %}

View File

@ -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() {

View File

@ -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 %}

View File

@ -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>

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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 %}

View File

@ -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">

View File

@ -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 %}

View File

@ -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">

View File

@ -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">

View File

@ -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"

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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%'
}); });

View File

@ -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">

View File

@ -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')),
] ]

View File

@ -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})

View File

@ -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 %'

View File

@ -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

View File

@ -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

View File

@ -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() {

View File

@ -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 %}

View File

@ -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