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,74 +3,44 @@ 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',
] 'failed_login_attempts', 'locked_until', 'two_factor_enabled',
search_fields = [ 'max_concurrent_sessions', 'session_timeout_minutes',
'username', 'email', 'first_name', 'last_name', )}),
'employee_id', 'license_number', 'npi_number' ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
] ('Important dates', {'fields': ('last_login', 'date_joined')}),
ordering = ['last_name', 'first_name'] )
add_fieldsets = (
fieldsets = BaseUserAdmin.fieldsets + ( (None, {
('Tenant Information', { 'classes': ('wide',),
'fields': ('tenant', 'user_id') 'fields': ('tenant', 'username', 'email', 'password1', 'password2'),
}),
('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',
'max_concurrent_sessions', 'session_timeout_minutes'
)
}),
('Preferences', {
'fields': ( 'language', 'theme')
}),
('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)
@ -85,7 +55,7 @@ class TwoFactorDeviceAdmin(admin.ModelAdmin):
list_filter = ['device_type', 'is_active', 'is_verified'] list_filter = ['device_type', 'is_active', 'is_verified']
search_fields = ['user__username', 'user__email', 'name'] search_fields = ['user__username', 'user__email', 'name']
ordering = ['-created_at'] ordering = ['-created_at']
fieldsets = ( fieldsets = (
('Device Information', { ('Device Information', {
'fields': ('user', 'device_id', 'name', 'device_type') 'fields': ('user', 'device_id', 'name', 'device_type')
@ -104,7 +74,7 @@ class TwoFactorDeviceAdmin(admin.ModelAdmin):
'classes': ('collapse',) 'classes': ('collapse',)
}), }),
) )
readonly_fields = ['device_id', 'created_at', 'updated_at'] readonly_fields = ['device_id', 'created_at', 'updated_at']
@ -123,7 +93,7 @@ class SocialAccountAdmin(admin.ModelAdmin):
'display_name', 'provider_id' 'display_name', 'provider_id'
] ]
ordering = ['-created_at'] ordering = ['-created_at']
fieldsets = ( fieldsets = (
('User Information', { ('User Information', {
'fields': ('user',) 'fields': ('user',)
@ -146,7 +116,7 @@ class SocialAccountAdmin(admin.ModelAdmin):
'classes': ('collapse',) 'classes': ('collapse',)
}), }),
) )
readonly_fields = ['created_at', 'updated_at'] readonly_fields = ['created_at', 'updated_at']
@ -168,7 +138,7 @@ class UserSessionAdmin(admin.ModelAdmin):
'user_agent', 'browser', 'operating_system' 'user_agent', 'browser', 'operating_system'
] ]
ordering = ['-created_at'] ordering = ['-created_at']
fieldsets = ( fieldsets = (
('User Information', { ('User Information', {
'fields': ('user', 'session_key', 'session_id') 'fields': ('user', 'session_key', 'session_id')
@ -192,11 +162,11 @@ class UserSessionAdmin(admin.ModelAdmin):
) )
}), }),
) )
readonly_fields = [ readonly_fields = [
'session_id', 'created_at', 'last_activity_at' 'session_id', 'created_at', 'last_activity_at'
] ]
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).select_related('user') return super().get_queryset(request).select_related('user')
@ -210,7 +180,7 @@ class PasswordHistoryAdmin(admin.ModelAdmin):
list_filter = ['created_at'] list_filter = ['created_at']
search_fields = ['user__username', 'user__email'] search_fields = ['user__username', 'user__email']
ordering = ['-created_at'] ordering = ['-created_at']
fieldsets = ( fieldsets = (
('User Information', { ('User Information', {
'fields': ('user',) 'fields': ('user',)
@ -222,9 +192,9 @@ class PasswordHistoryAdmin(admin.ModelAdmin):
'fields': ('created_at',) 'fields': ('created_at',)
}), }),
) )
readonly_fields = ['created_at'] readonly_fields = ['created_at']
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).select_related('user') return super().get_queryset(request).select_related('user')

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'), # Stable internal UUID
('ADMIN', 'Administrator'), user_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
('PHYSICIAN', 'Physician'),
('NURSE', 'Nurse'), # Tenant (PROTECT = safer than cascading deletion)
('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( 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( email = models.EmailField(blank=True, null=True)
max_length=150,
blank=True, # --- Security & session controls kept on User (auth-level concerns) ---
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.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 ----
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 @property
def is_account_locked(self): def is_account_locked(self) -> bool:
""" return bool(self.locked_until and timezone.now() < self.locked_until)
Check if account is currently locked.
"""
if self.locked_until:
return timezone.now() < self.locked_until
return False
@property @property
def is_password_expired(self): def is_password_expired(self) -> bool:
""" return bool(self.password_expires_at and timezone.now() > self.password_expires_at)
Check if password has expired.
""" def lock_account(self, duration_minutes: int = 15):
if self.password_expires_at: self.locked_until = timezone.now() + timedelta(minutes=duration_minutes)
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 lockout_minutes is None:
lockout_minutes = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('LOCKOUT_DURATION_MINUTES', 15)
if self.failed_login_attempts >= max_attempts: if self.failed_login_attempts >= max_attempts:
lockout_duration = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('LOCKOUT_DURATION_MINUTES', 15) self.lock_account(lockout_minutes)
self.lock_account(lockout_duration) 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,37 +1015,38 @@ 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(
tenant=getattr(request, 'tenant', None), tenant=getattr(request, 'tenant', None),
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,177 +68,189 @@ 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(): 'PHYSICIAN': 0.15,
"""Generate Saudi phone number""" 'NURSE': 0.25,
area_codes = ['11', '12', '13', '14', '16', '17'] # Major Saudi area codes 'PHARMACIST': 0.08,
return f"+966-{random.choice(area_codes)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}" 'LAB_TECH': 0.10,
'RAD_TECH': 0.08,
'RADIOLOGIST': 0.05,
'ADMIN': 0.07,
'MEDICAL_ASSISTANT': 0.12,
'CLERICAL': 0.10
}
def generate_saudi_mobile(): # -------------------------------
"""Generate Saudi mobile number""" # Helpers
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 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(): def generate_saudi_license():
"""Generate Saudi medical license number""" """Generate Saudi medical license number (fictional format)"""
prefix = random.choice(SAUDI_LICENSE_PREFIXES) prefix = random.choice(SAUDI_LICENSE_PREFIXES)
return f"{prefix}-{random.randint(100000, 999999)}" return f"{prefix}-{random.randint(100000, 999999)}"
def generate_saudi_employee_id(tenant_name, role): def tenant_scoped_unique_username(tenant, base_username: str) -> str:
"""Generate Saudi employee ID""" """
tenant_code = ''.join([c for c in tenant_name.upper() if c.isalpha()])[:3] Make username unique within a tenant (your User has tenant-scoped unique constraint).
role_code = role[:3].upper() """
return f"{tenant_code}-{role_code}-{random.randint(1000, 9999)}" 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): def create_saudi_users(tenants, users_per_tenant=50):
"""Create Saudi healthcare users""" """
users = [] Create Users (auth + security), then populate Employee profile.
Relies on the post_save signal to create Employee automatically.
role_distribution = { """
'PHYSICIAN': 0.15, all_users = []
'NURSE': 0.25,
'PHARMACIST': 0.08,
'LAB_TECH': 0.10,
'RAD_TECH': 0.08,
'RADIOLOGIST': 0.05,
'ADMIN': 0.07,
'MEDICAL_ASSISTANT': 0.12,
'CLERICAL': 0.10
}
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]),
# Preferences
user_timezone='Asia/Riyadh',
language=random.choice(['ar', 'en', 'ar_SA']),
theme=random.choice(['LIGHT', 'DARK', 'AUTO']),
# Status
is_verified=True,
is_approved=True,
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
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)), last_password_change=django_timezone.now() - timedelta(days=random.randint(1, 90)),
date_joined=django_timezone.now() - timedelta(days=random.randint(1, 365)) date_joined=django_timezone.now() - timedelta(days=random.randint(1, 365)),
) )
user.set_password('Hospital@123')
# Set password
user.set_password('Hospital@123') # Default password
user.save() user.save()
users.append(user) # Signal should have created Employee; now populate Employee fields
tenant_users.append(user) 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
# Set approval relationships # Contact (E.164 KSA)
admin_users = [u for u in tenant_users if u.role in ['ADMIN', 'SUPER_ADMIN']] 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
emp.user_timezone = 'Asia/Riyadh'
emp.language = random.choice(['ar', 'en', 'ar_SA'])
emp.theme = random.choice([Employee.Theme.LIGHT, Employee.Theme.DARK, Employee.Theme.AUTO])
# Status / approval (approved later per-tenant)
emp.is_verified = True
emp.is_approved = True
emp.approval_date = django_timezone.now() - timedelta(days=random.randint(1, 180))
emp.save()
tenant_users.append(user)
all_users.append(user)
# Approval relationships: choose an approver among admins in this tenant
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,220 +8,219 @@
{% 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">
<div class="row"> <div class="row">
<div class="col-xl-9"> <div class="col-xl-9">
<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>
<li class="breadcrumb-item"><a href="{% url 'analytics:metric_definition_list' %}">Metrics</a></li> <li class="breadcrumb-item"><a href="{% url 'analytics:metric_definition_list' %}">Metrics</a></li>
<li class="breadcrumb-item active">Calculate Metric</li> <li class="breadcrumb-item active">Calculate Metric</li>
</ul> </ul>
<h1 class="page-header">Calculate Metric</h1> <h1 class="page-header">Calculate Metric</h1>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Metric Calculation</h4> <h4 class="card-title">Metric Calculation</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Metric Definition</label>
<div class="col-md-9">
<select name="metric_id" class="form-select" required>
<option value="">Select Metric</option>
{% for metric in metrics %}
<option value="{{ metric.id }}">{{ metric.name }} ({{ metric.category }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Calculation Period</label>
<div class="col-md-4">
<input type="date" name="start_date" class="form-control" required>
<small class="form-text text-muted">Start Date</small>
</div>
<div class="col-md-4">
<input type="date" name="end_date" class="form-control" required>
<small class="form-text text-muted">End Date</small>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Filters</label>
<div class="col-md-9">
<div class="row">
<div class="col-md-6">
<select name="department" class="form-select mb-2">
<option value="">All Departments</option>
{% for dept in departments %}
<option value="{{ dept.id }}">{{ dept.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<select name="location" class="form-select mb-2">
<option value="">All Locations</option>
{% for loc in locations %}
<option value="{{ loc.id }}">{{ loc.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Calculation Mode</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="radio" name="calculation_mode" value="real_time" id="real_time" checked>
<label class="form-check-label" for="real_time">
Real-time Calculation
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="calculation_mode" value="cached" id="cached">
<label class="form-check-label" for="cached">
Use Cached Data (Faster)
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-primary">
<i class="fa fa-calculator me-2"></i>Calculate Metric
</button>
<a href="{% url 'analytics:metric_definition_list' %}" class="btn btn-secondary ms-2">Cancel</a>
</div>
</div>
</form>
</div>
</div> </div>
<div class="card-body">
{% if calculation_result %} {% if messages %}
<div class="card mt-4"> {% for message in messages %}
<div class="card-header"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
<h4 class="card-title">Calculation Results</h4> {{ message }}
</div> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
<div class="card-body"> </div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Metric Definition</label>
<div class="col-md-9">
<select name="metric_id" class="form-select" required>
<option value="">Select Metric</option>
{% for metric in metrics %}
<option value="{{ metric.id }}">{{ metric.name }} ({{ metric.category }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Calculation Period</label>
<div class="col-md-4">
<input type="date" name="start_date" class="form-control" required>
<small class="form-text text-muted">Start Date</small>
</div>
<div class="col-md-4">
<input type="date" name="end_date" class="form-control" required>
<small class="form-text text-muted">End Date</small>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Filters</label>
<div class="col-md-9">
<div class="row">
<div class="col-md-6">
<select name="department" class="form-select mb-2">
<option value="">All Departments</option>
{% for dept in departments %}
<option value="{{ dept.id }}">{{ dept.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<select name="location" class="form-select mb-2">
<option value="">All Locations</option>
{% for loc in locations %}
<option value="{{ loc.id }}">{{ loc.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Calculation Mode</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="radio" name="calculation_mode" value="real_time" id="real_time" checked>
<label class="form-check-label" for="real_time">
Real-time Calculation
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="calculation_mode" value="cached" id="cached">
<label class="form-check-label" for="cached">
Use Cached Data (Faster)
</label>
</div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-9 offset-md-3">
<div class="card bg-primary text-white"> <button type="submit" class="btn btn-primary">
<div class="card-body"> <i class="fa fa-calculator me-2"></i>Calculate Metric
<div class="d-flex align-items-center"> </button>
<div class="flex-grow-1"> <a href="{% url 'analytics:metric_definition_list' %}" class="btn btn-secondary ms-2">Cancel</a>
<div class="fs-3 fw-bold">{{ calculation_result.value }}</div>
<div>{{ calculation_result.metric_name }}</div>
</div>
<div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-chart-line fa-lg"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<div class="row mb-2">
<div class="col-6"><strong>Unit:</strong></div>
<div class="col-6">{{ calculation_result.unit }}</div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>Period:</strong></div>
<div class="col-6">{{ calculation_result.period }}</div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>Calculated:</strong></div>
<div class="col-6">{{ calculation_result.calculated_at }}</div>
</div>
<div class="row">
<div class="col-6"><strong>Status:</strong></div>
<div class="col-6">
{% if calculation_result.status == 'success' %}
<span class="badge bg-success">Success</span>
{% else %}
<span class="badge bg-warning">{{ calculation_result.status|title }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</form>
{% if calculation_result.breakdown %}
<div class="mt-4">
<h6>Calculation Breakdown:</h6>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Component</th>
<th>Value</th>
<th>Weight</th>
<th>Contribution</th>
</tr>
</thead>
<tbody>
{% for item in calculation_result.breakdown %}
<tr>
<td>{{ item.component }}</td>
<td>{{ item.value }}</td>
<td>{{ item.weight }}%</td>
<td>{{ item.contribution }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if calculation_result.notes %}
<div class="mt-3">
<h6>Notes:</h6>
<p class="text-muted">{{ calculation_result.notes }}</p>
</div>
{% endif %}
</div>
</div> </div>
{% endif %}
</div> </div>
<div class="col-xl-3"> {% if calculation_result %}
<div class="card"> <div class="card mt-4">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Quick Actions</h4> <h4 class="card-title">Calculation Results</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="d-grid gap-2"> <div class="row">
<a href="{% url 'analytics:metric_definition_list' %}" class="btn btn-outline-primary btn-sm"> <div class="col-md-6">
<i class="fa fa-list me-2"></i>All Metrics <div class="card bg-primary text-white">
</a> <div class="card-body">
<a href="{% url 'analytics:metric_definition_create' %}" class="btn btn-outline-success btn-sm"> <div class="d-flex align-items-center">
<i class="fa fa-plus me-2"></i>Create Metric <div class="flex-grow-1">
</a> <div class="fs-3 fw-bold">{{ calculation_result.value }}</div>
<div>{{ calculation_result.metric_name }}</div>
</div>
<div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-chart-line fa-lg"></i>
</div>
</div>
</div>
</div>
</div> </div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<div class="row mb-2">
<div class="col-6"><strong>Unit:</strong></div>
<div class="col-6">{{ calculation_result.unit }}</div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>Period:</strong></div>
<div class="col-6">{{ calculation_result.period }}</div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>Calculated:</strong></div>
<div class="col-6">{{ calculation_result.calculated_at }}</div>
</div>
<div class="row">
<div class="col-6"><strong>Status:</strong></div>
<div class="col-6">
{% if calculation_result.status == 'success' %}
<span class="badge bg-success">Success</span>
{% else %}
<span class="badge bg-warning">{{ calculation_result.status|title }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% if calculation_result.breakdown %}
<div class="mt-4">
<h6>Calculation Breakdown:</h6>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Component</th>
<th>Value</th>
<th>Weight</th>
<th>Contribution</th>
</tr>
</thead>
<tbody>
{% for item in calculation_result.breakdown %}
<tr>
<td>{{ item.component }}</td>
<td>{{ item.value }}</td>
<td>{{ item.weight }}%</td>
<td>{{ item.contribution }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if calculation_result.notes %}
<div class="mt-3">
<h6>Notes:</h6>
<p class="text-muted">{{ calculation_result.notes }}</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
<div class="col-xl-3">
<div class="card">
<div class="card-header">
<h4 class="card-title">Quick Actions</h4>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'analytics:metric_definition_list' %}" class="btn btn-outline-primary btn-sm">
<i class="fa fa-list me-2"></i>All Metrics
</a>
<a href="{% url 'analytics:metric_definition_create' %}" class="btn btn-outline-success btn-sm">
<i class="fa fa-plus me-2"></i>Create Metric
</a>
</div> </div>
</div> </div>
</div> </div>
@ -230,6 +229,7 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -8,177 +8,176 @@
{% 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">
<div class="row"> <div class="row">
<div class="col-xl-9"> <div class="col-xl-9">
<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>
<li class="breadcrumb-item"><a href="{% url 'analytics:report_list' %}">Reports</a></li> <li class="breadcrumb-item"><a href="{% url 'analytics:report_list' %}">Reports</a></li>
<li class="breadcrumb-item active">Execute Report</li> <li class="breadcrumb-item active">Execute Report</li>
</ul> </ul>
<h1 class="page-header">Execute Report</h1> <h1 class="page-header">Execute Report</h1>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Report Execution</h4> <h4 class="card-title">Report Execution</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Report</label>
<div class="col-md-9">
<select name="report_id" class="form-select" required>
<option value="">Select Report</option>
{% for report in reports %}
<option value="{{ report.id }}">{{ report.name }} - {{ report.category }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Date Range</label>
<div class="col-md-4">
<input type="date" name="start_date" class="form-control" required>
<small class="form-text text-muted">Start Date</small>
</div>
<div class="col-md-4">
<input type="date" name="end_date" class="form-control" required>
<small class="form-text text-muted">End Date</small>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Output Format</label>
<div class="col-md-9">
<select name="output_format" class="form-select">
<option value="html">HTML (View in Browser)</option>
<option value="pdf">PDF Download</option>
<option value="excel">Excel Download</option>
<option value="csv">CSV Download</option>
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Parameters</label>
<div class="col-md-9">
<textarea name="parameters" class="form-control" rows="3" placeholder="Enter report parameters as JSON (optional)">{"department": "all", "status": "active"}</textarea>
<small class="form-text text-muted">Optional parameters in JSON format</small>
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-primary">
<i class="fa fa-play me-2"></i>Execute Report
</button>
<a href="{% url 'analytics:report_list' %}" class="btn btn-secondary ms-2">Cancel</a>
</div>
</div>
</form>
</div>
</div> </div>
<div class="card-body">
{% if execution_result %} {% if messages %}
<div class="card mt-4"> {% for message in messages %}
<div class="card-header"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
<h4 class="card-title">Execution Results</h4> {{ message }}
</div> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
<div class="card-body"> </div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-3"><strong>Status:</strong></div> <label class="col-form-label col-md-3">Report</label>
<div class="col-md-9"> <div class="col-md-9">
{% if execution_result.success %} <select name="report_id" class="form-select" required>
<span class="badge bg-success">Completed</span> <option value="">Select Report</option>
{% else %} {% for report in reports %}
<span class="badge bg-danger">Failed</span> <option value="{{ report.id }}">{{ report.name }} - {{ report.category }}</option>
{% endif %} {% endfor %}
</select>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-3"><strong>Execution Time:</strong></div> <label class="col-form-label col-md-3">Date Range</label>
<div class="col-md-9">{{ execution_result.execution_time }}s</div> <div class="col-md-4">
</div> <input type="date" name="start_date" class="form-control" required>
<small class="form-text text-muted">Start Date</small>
<div class="row mb-3"> </div>
<div class="col-md-3"><strong>Records:</strong></div> <div class="col-md-4">
<div class="col-md-9">{{ execution_result.record_count|default:"N/A" }}</div> <input type="date" name="end_date" class="form-control" required>
</div> <small class="form-text text-muted">End Date</small>
{% if execution_result.download_url %}
<div class="row mb-3">
<div class="col-md-3"><strong>Download:</strong></div>
<div class="col-md-9">
<a href="{{ execution_result.download_url }}" class="btn btn-sm btn-outline-primary">
<i class="fa fa-download me-2"></i>Download Report
</a>
</div> </div>
</div> </div>
{% endif %}
<div class="row mb-3">
{% if execution_result.preview_data %} <label class="col-form-label col-md-3">Output Format</label>
<div class="col-md-9">
<select name="output_format" class="form-select">
<option value="html">HTML (View in Browser)</option>
<option value="pdf">PDF Download</option>
<option value="excel">Excel Download</option>
<option value="csv">CSV Download</option>
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Parameters</label>
<div class="col-md-9">
<textarea name="parameters" class="form-control" rows="3" placeholder="Enter report parameters as JSON (optional)">{"department": "all", "status": "active"}</textarea>
<small class="form-text text-muted">Optional parameters in JSON format</small>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-md-9 offset-md-3">
<h6>Preview Data:</h6> <button type="submit" class="btn btn-primary">
<div class="table-responsive"> <i class="fa fa-play me-2"></i>Execute Report
<table class="table table-striped table-sm"> </button>
<thead> <a href="{% url 'analytics:report_list' %}" class="btn btn-secondary ms-2">Cancel</a>
</div>
</div>
</form>
</div>
</div>
{% if execution_result %}
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">Execution Results</h4>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-3"><strong>Status:</strong></div>
<div class="col-md-9">
{% if execution_result.success %}
<span class="badge bg-success">Completed</span>
{% else %}
<span class="badge bg-danger">Failed</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-3"><strong>Execution Time:</strong></div>
<div class="col-md-9">{{ execution_result.execution_time }}s</div>
</div>
<div class="row mb-3">
<div class="col-md-3"><strong>Records:</strong></div>
<div class="col-md-9">{{ execution_result.record_count|default:"N/A" }}</div>
</div>
{% if execution_result.download_url %}
<div class="row mb-3">
<div class="col-md-3"><strong>Download:</strong></div>
<div class="col-md-9">
<a href="{{ execution_result.download_url }}" class="btn btn-sm btn-outline-primary">
<i class="fa fa-download me-2"></i>Download Report
</a>
</div>
</div>
{% endif %}
{% if execution_result.preview_data %}
<div class="row">
<div class="col-12">
<h6>Preview Data:</h6>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
{% for header in execution_result.headers %}
<th>{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in execution_result.preview_data %}
<tr> <tr>
{% for header in execution_result.headers %} {% for cell in row %}
<th>{{ header }}</th> <td>{{ cell }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> {% endfor %}
<tbody> </tbody>
{% for row in execution_result.preview_data %} </table>
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
{% endif %}
<div class="col-xl-3"> </div>
<div class="card">
<div class="card-header"> <div class="col-xl-3">
<h4 class="card-title">Quick Actions</h4> <div class="card">
</div> <div class="card-header">
<div class="card-body"> <h4 class="card-title">Quick Actions</h4>
<div class="d-grid gap-2"> </div>
<a href="{% url 'analytics:report_list' %}" class="btn btn-outline-primary btn-sm"> <div class="card-body">
<i class="fa fa-list me-2"></i>All Reports <div class="d-grid gap-2">
</a> <a href="{% url 'analytics:report_list' %}" class="btn btn-outline-primary btn-sm">
<a href="{% url 'analytics:report_create' %}" class="btn btn-outline-success btn-sm"> <i class="fa fa-list me-2"></i>All Reports
<i class="fa fa-plus me-2"></i>Create Report </a>
</a> <a href="{% url 'analytics:report_create' %}" class="btn btn-outline-success btn-sm">
</div> <i class="fa fa-plus me-2"></i>Create Report
</a>
</div> </div>
</div> </div>
</div> </div>
@ -187,6 +186,7 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -8,126 +8,125 @@
{% 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">
<div class="row"> <div class="row">
<div class="col-xl-9"> <div class="col-xl-9">
<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>
<li class="breadcrumb-item"><a href="{% url 'analytics:data_source_list' %}">Data Sources</a></li> <li class="breadcrumb-item"><a href="{% url 'analytics:data_source_list' %}">Data Sources</a></li>
<li class="breadcrumb-item active">Test Data Source</li> <li class="breadcrumb-item active">Test Data Source</li>
</ul> </ul>
<h1 class="page-header">Test Data Source Connection</h1> <h1 class="page-header">Test Data Source Connection</h1>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Connection Test</h4> <h4 class="card-title">Connection Test</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Data Source</label>
<div class="col-md-9">
<select name="data_source_id" class="form-select" required>
<option value="">Select Data Source</option>
{% for source in data_sources %}
<option value="{{ source.id }}">{{ source.name }} ({{ source.connection_type }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Test Query</label>
<div class="col-md-9">
<textarea name="test_query" class="form-control" rows="4" placeholder="Enter test query (optional)">SELECT 1 as test</textarea>
<small class="form-text text-muted">Leave empty to test basic connection</small>
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-primary">
<i class="fa fa-play me-2"></i>Test Connection
</button>
<a href="{% url 'analytics:data_source_list' %}" class="btn btn-secondary ms-2">Cancel</a>
</div>
</div>
</form>
</div>
</div> </div>
<div class="card-body">
{% if test_result %} {% if messages %}
<div class="card mt-4"> {% for message in messages %}
<div class="card-header"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
<h4 class="card-title">Test Results</h4> {{ message }}
</div> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
<div class="card-body"> </div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-3"><strong>Status:</strong></div> <label class="col-form-label col-md-3">Data Source</label>
<div class="col-md-9"> <div class="col-md-9">
{% if test_result.success %} <select name="data_source_id" class="form-select" required>
<span class="badge bg-success">Connected</span> <option value="">Select Data Source</option>
{% else %} {% for source in data_sources %}
<span class="badge bg-danger">Failed</span> <option value="{{ source.id }}">{{ source.name }} ({{ source.connection_type }})</option>
{% endif %} {% endfor %}
</select>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-3"><strong>Response Time:</strong></div> <label class="col-form-label col-md-3">Test Query</label>
<div class="col-md-9">{{ test_result.response_time }}ms</div> <div class="col-md-9">
<textarea name="test_query" class="form-control" rows="4" placeholder="Enter test query (optional)">SELECT 1 as test</textarea>
<small class="form-text text-muted">Leave empty to test basic connection</small>
</div>
</div> </div>
{% if test_result.message %}
<div class="row mb-3">
<div class="col-md-3"><strong>Message:</strong></div>
<div class="col-md-9">{{ test_result.message }}</div>
</div>
{% endif %}
{% if test_result.data %}
<div class="row"> <div class="row">
<div class="col-md-3"><strong>Sample Data:</strong></div> <div class="col-md-9 offset-md-3">
<div class="col-md-9"> <button type="submit" class="btn btn-primary">
<pre class="bg-light p-3 rounded">{{ test_result.data|truncatechars:500 }}</pre> <i class="fa fa-play me-2"></i>Test Connection
</button>
<a href="{% url 'analytics:data_source_list' %}" class="btn btn-secondary ms-2">Cancel</a>
</div> </div>
</div> </div>
{% endif %} </form>
</div>
</div> </div>
{% endif %}
</div> </div>
<div class="col-xl-3"> {% if test_result %}
<div class="card"> <div class="card mt-4">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Quick Actions</h4> <h4 class="card-title">Test Results</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="d-grid gap-2"> <div class="row mb-3">
<a href="{% url 'analytics:data_source_list' %}" class="btn btn-outline-primary btn-sm"> <div class="col-md-3"><strong>Status:</strong></div>
<i class="fa fa-list me-2"></i>All Data Sources <div class="col-md-9">
</a> {% if test_result.success %}
<a href="{% url 'analytics:data_source_create' %}" class="btn btn-outline-success btn-sm"> <span class="badge bg-success">Connected</span>
<i class="fa fa-plus me-2"></i>Add Data Source {% else %}
</a> <span class="badge bg-danger">Failed</span>
{% endif %}
</div> </div>
</div> </div>
<div class="row mb-3">
<div class="col-md-3"><strong>Response Time:</strong></div>
<div class="col-md-9">{{ test_result.response_time }}ms</div>
</div>
{% if test_result.message %}
<div class="row mb-3">
<div class="col-md-3"><strong>Message:</strong></div>
<div class="col-md-9">{{ test_result.message }}</div>
</div>
{% endif %}
{% if test_result.data %}
<div class="row">
<div class="col-md-3"><strong>Sample Data:</strong></div>
<div class="col-md-9">
<pre class="bg-light p-3 rounded">{{ test_result.data|truncatechars:500 }}</pre>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
<div class="col-xl-3">
<div class="card">
<div class="card-header">
<h4 class="card-title">Quick Actions</h4>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'analytics:data_source_list' %}" class="btn btn-outline-primary btn-sm">
<i class="fa fa-list me-2"></i>All Data Sources
</a>
<a href="{% url 'analytics:data_source_create' %}" class="btn btn-outline-success btn-sm">
<i class="fa fa-plus me-2"></i>Add Data Source
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -135,6 +134,7 @@
</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,186 +9,186 @@
{% 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">
<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>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li> <li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li>
<li class="breadcrumb-item active">Search</li> <li class="breadcrumb-item active">Search</li>
</ul> </ul>
<h1 class="page-header">Search Appointments</h1> <h1 class="page-header">Search Appointments</h1>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Search Filters</h4> <h4 class="card-title">Search Filters</h4>
</div>
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-3">
<label class="form-label">Patient Name</label>
<input type="text" name="patient_name" class="form-control" value="{{ request.GET.patient_name }}" placeholder="Enter patient name">
</div>
<div class="col-md-3">
<label class="form-label">Patient ID</label>
<input type="text" name="patient_id" class="form-control" value="{{ request.GET.patient_id }}" placeholder="Enter patient ID">
</div>
<div class="col-md-3">
<label class="form-label">Provider</label>
<select name="provider" class="form-select">
<option value="">All Providers</option>
{% for provider in providers %}
<option value="{{ provider.id }}" {% if request.GET.provider == provider.id|stringformat:"s" %}selected{% endif %}>
{{ provider.first_name }} {{ provider.last_name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label class="form-label">Department</label>
<select name="department" class="form-select">
<option value="">All Departments</option>
{% for dept in departments %}
<option value="{{ dept.id }}" {% if request.GET.department == dept.id|stringformat:"s" %}selected{% endif %}>
{{ dept.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label class="form-label">Date From</label>
<input type="date" name="date_from" class="form-control" value="{{ request.GET.date_from }}">
</div>
<div class="col-md-3">
<label class="form-label">Date To</label>
<input type="date" name="date_to" class="form-control" value="{{ request.GET.date_to }}">
</div>
<div class="col-md-3">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="">All Statuses</option>
<option value="scheduled" {% if request.GET.status == 'scheduled' %}selected{% endif %}>Scheduled</option>
<option value="confirmed" {% if request.GET.status == 'confirmed' %}selected{% endif %}>Confirmed</option>
<option value="checked_in" {% if request.GET.status == 'checked_in' %}selected{% endif %}>Checked In</option>
<option value="in_progress" {% if request.GET.status == 'in_progress' %}selected{% endif %}>In Progress</option>
<option value="completed" {% if request.GET.status == 'completed' %}selected{% endif %}>Completed</option>
<option value="cancelled" {% if request.GET.status == 'cancelled' %}selected{% endif %}>Cancelled</option>
<option value="no_show" {% if request.GET.status == 'no_show' %}selected{% endif %}>No Show</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Appointment Type</label>
<select name="appointment_type" class="form-select">
<option value="">All Types</option>
{% for type in appointment_types %}
<option value="{{ type.id }}" {% if request.GET.appointment_type == type.id|stringformat:"s" %}selected{% endif %}>
{{ type.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">
<i class="fa fa-search me-2"></i>Search
</button>
<a href="{% url 'appointments:appointment_search' %}" class="btn btn-secondary ms-2">
<i class="fa fa-refresh me-2"></i>Clear
</a>
<a href="{% url 'appointments:appointment_create' %}" class="btn btn-success ms-2">
<i class="fa fa-plus me-2"></i>New Appointment
</a>
</div>
</form>
</div>
</div> </div>
<div class="card-body">
{% if appointments %} <form method="get" class="row g-3">
<div class="card mt-4"> <div class="col-md-3">
<div class="card-header"> <label class="form-label">Patient Name</label>
<h4 class="card-title">Search Results ({{ appointments.count }} found)</h4> <input type="text" name="patient_name" class="form-control" value="{{ request.GET.patient_name }}" placeholder="Enter patient name">
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped" id="appointmentsTable">
<thead>
<tr>
<th>Date/Time</th>
<th>Patient</th>
<th>Provider</th>
<th>Department</th>
<th>Type</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for appointment in appointments %}
<tr>
<td>
<div class="fw-bold">{{ appointment.appointment_date|date:"M d, Y" }}</div>
<small class="text-muted">{{ appointment.appointment_time|time:"g:i A" }}</small>
</td>
<td>
<div class="fw-bold">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</div>
<small class="text-muted">ID: {{ appointment.patient.patient_id }}</small>
</td>
<td>{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</td>
<td>{{ appointment.department.name }}</td>
<td>{{ appointment.appointment_type.name }}</td>
<td>
{% if appointment.status == 'scheduled' %}
<span class="badge bg-info">Scheduled</span>
{% elif appointment.status == 'confirmed' %}
<span class="badge bg-success">Confirmed</span>
{% elif appointment.status == 'checked_in' %}
<span class="badge bg-primary">Checked In</span>
{% elif appointment.status == 'in_progress' %}
<span class="badge bg-warning">In Progress</span>
{% elif appointment.status == 'completed' %}
<span class="badge bg-success">Completed</span>
{% elif appointment.status == 'cancelled' %}
<span class="badge bg-danger">Cancelled</span>
{% elif appointment.status == 'no_show' %}
<span class="badge bg-secondary">No Show</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-outline-primary btn-sm">
<i class="fa fa-eye"></i>
</a>
<a href="{% url 'appointments:appointment_update' appointment.id %}" class="btn btn-outline-warning btn-sm">
<i class="fa fa-edit"></i>
</a>
{% if appointment.status == 'scheduled' or appointment.status == 'confirmed' %}
<a href="{% url 'appointments:check_in_patient' appointment.id %}" class="btn btn-outline-success btn-sm">
<i class="fa fa-check"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> <div class="col-md-3">
<label class="form-label">Patient ID</label>
<input type="text" name="patient_id" class="form-control" value="{{ request.GET.patient_id }}" placeholder="Enter patient ID">
</div>
<div class="col-md-3">
<label class="form-label">Provider</label>
<select name="provider" class="form-select">
<option value="">All Providers</option>
{% for provider in providers %}
<option value="{{ provider.id }}" {% if request.GET.provider == provider.id|stringformat:"s" %}selected{% endif %}>
{{ provider.first_name }} {{ provider.last_name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label class="form-label">Department</label>
<select name="department" class="form-select">
<option value="">All Departments</option>
{% for dept in departments %}
<option value="{{ dept.id }}" {% if request.GET.department == dept.id|stringformat:"s" %}selected{% endif %}>
{{ dept.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label class="form-label">Date From</label>
<input type="date" name="date_from" class="form-control" value="{{ request.GET.date_from }}">
</div>
<div class="col-md-3">
<label class="form-label">Date To</label>
<input type="date" name="date_to" class="form-control" value="{{ request.GET.date_to }}">
</div>
<div class="col-md-3">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="">All Statuses</option>
<option value="scheduled" {% if request.GET.status == 'scheduled' %}selected{% endif %}>Scheduled</option>
<option value="confirmed" {% if request.GET.status == 'confirmed' %}selected{% endif %}>Confirmed</option>
<option value="checked_in" {% if request.GET.status == 'checked_in' %}selected{% endif %}>Checked In</option>
<option value="in_progress" {% if request.GET.status == 'in_progress' %}selected{% endif %}>In Progress</option>
<option value="completed" {% if request.GET.status == 'completed' %}selected{% endif %}>Completed</option>
<option value="cancelled" {% if request.GET.status == 'cancelled' %}selected{% endif %}>Cancelled</option>
<option value="no_show" {% if request.GET.status == 'no_show' %}selected{% endif %}>No Show</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Appointment Type</label>
<select name="appointment_type" class="form-select">
<option value="">All Types</option>
{% for type in appointment_types %}
<option value="{{ type.id }}" {% if request.GET.appointment_type == type.id|stringformat:"s" %}selected{% endif %}>
{{ type.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">
<i class="fa fa-search me-2"></i>Search
</button>
<a href="{% url 'appointments:appointment_search' %}" class="btn btn-secondary ms-2">
<i class="fa fa-refresh me-2"></i>Clear
</a>
<a href="{% url 'appointments:appointment_create' %}" class="btn btn-success ms-2">
<i class="fa fa-plus me-2"></i>New Appointment
</a>
</div>
</form>
</div> </div>
{% elif request.GET %}
<div class="card mt-4">
<div class="card-body text-center">
<i class="fa fa-search fa-3x text-muted mb-3"></i>
<h5>No appointments found</h5>
<p class="text-muted">Try adjusting your search criteria</p>
</div>
</div>
{% endif %}
</div> </div>
{% if appointments %}
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">Search Results ({{ appointments.count }} found)</h4>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped" id="appointmentsTable">
<thead>
<tr>
<th>Date/Time</th>
<th>Patient</th>
<th>Provider</th>
<th>Department</th>
<th>Type</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for appointment in appointments %}
<tr>
<td>
<div class="fw-bold">{{ appointment.appointment_date|date:"M d, Y" }}</div>
<small class="text-muted">{{ appointment.appointment_time|time:"g:i A" }}</small>
</td>
<td>
<div class="fw-bold">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</div>
<small class="text-muted">ID: {{ appointment.patient.patient_id }}</small>
</td>
<td>{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</td>
<td>{{ appointment.department.name }}</td>
<td>{{ appointment.appointment_type.name }}</td>
<td>
{% if appointment.status == 'scheduled' %}
<span class="badge bg-info">Scheduled</span>
{% elif appointment.status == 'confirmed' %}
<span class="badge bg-success">Confirmed</span>
{% elif appointment.status == 'checked_in' %}
<span class="badge bg-primary">Checked In</span>
{% elif appointment.status == 'in_progress' %}
<span class="badge bg-warning">In Progress</span>
{% elif appointment.status == 'completed' %}
<span class="badge bg-success">Completed</span>
{% elif appointment.status == 'cancelled' %}
<span class="badge bg-danger">Cancelled</span>
{% elif appointment.status == 'no_show' %}
<span class="badge bg-secondary">No Show</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-outline-primary btn-sm">
<i class="fa fa-eye"></i>
</a>
<a href="{% url 'appointments:appointment_update' appointment.id %}" class="btn btn-outline-warning btn-sm">
<i class="fa fa-edit"></i>
</a>
{% if appointment.status == 'scheduled' or appointment.status == 'confirmed' %}
<a href="{% url 'appointments:check_in_patient' appointment.id %}" class="btn btn-outline-success btn-sm">
<i class="fa fa-check"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% elif request.GET %}
<div class="card mt-4">
<div class="card-body text-center">
<i class="fa fa-search fa-3x text-muted mb-3"></i>
<h5>No appointments found</h5>
<p class="text-muted">Try adjusting your search criteria</p>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -8,232 +8,231 @@
{% 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">
<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>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li> <li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li>
<li class="breadcrumb-item active">Statistics</li> <li class="breadcrumb-item active">Statistics</li>
</ul> </ul>
<h1 class="page-header">Appointment Statistics</h1> <h1 class="page-header">Appointment Statistics</h1>
<!-- Summary Cards --> <!-- Summary Cards -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-xl-3 col-md-6"> <div class="col-xl-3 col-md-6">
<div class="card bg-primary text-white"> <div class="card bg-primary text-white">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="fs-3 fw-bold">{{ stats.total_appointments|default:0 }}</div> <div class="fs-3 fw-bold">{{ stats.total_appointments|default:0 }}</div>
<div>Total Appointments</div> <div>Total Appointments</div>
</div>
<div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-calendar fa-lg"></i>
</div>
</div> </div>
</div> <div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
</div> <i class="fa fa-calendar fa-lg"></i>
</div>
<div class="col-xl-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">
<div class="fs-3 fw-bold">{{ stats.completed_appointments|default:0 }}</div>
<div>Completed</div>
</div>
<div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-check fa-lg"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-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">
<div class="fs-3 fw-bold">{{ stats.cancelled_appointments|default:0 }}</div>
<div>Cancelled</div>
</div>
<div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-times fa-lg"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-danger text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<div class="fs-3 fw-bold">{{ stats.no_show_appointments|default:0 }}</div>
<div>No Shows</div>
</div>
<div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-user-times fa-lg"></i>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-xl-3 col-md-6">
<div class="row"> <div class="card bg-success text-white">
<div class="col-xl-8"> <div class="card-body">
<!-- Appointments by Status --> <div class="d-flex align-items-center">
<div class="card mb-4"> <div class="flex-grow-1">
<div class="card-header"> <div class="fs-3 fw-bold">{{ stats.completed_appointments|default:0 }}</div>
<h4 class="card-title">Appointments by Status</h4> <div>Completed</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Status</th>
<th>Count</th>
<th>Percentage</th>
<th>Visual</th>
</tr>
</thead>
<tbody>
{% for status in stats.by_status %}
<tr>
<td>{{ status.status|title }}</td>
<td>{{ status.count }}</td>
<td>{{ status.percentage|floatformat:1 }}%</td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar" role="progressbar" style="width: {{ status.percentage }}%"></div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> <div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
</div> <i class="fa fa-check fa-lg"></i>
<!-- Appointments by Department -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Appointments by Department</h4>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Department</th>
<th>Total</th>
<th>Completed</th>
<th>Cancelled</th>
<th>No Shows</th>
</tr>
</thead>
<tbody>
{% for dept in stats.by_department %}
<tr>
<td>{{ dept.department_name }}</td>
<td>{{ dept.total }}</td>
<td>{{ dept.completed }}</td>
<td>{{ dept.cancelled }}</td>
<td>{{ dept.no_shows }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="col-xl-4"> <div class="col-xl-3 col-md-6">
<!-- Key Metrics --> <div class="card bg-warning text-white">
<div class="card mb-4"> <div class="card-body">
<div class="card-header"> <div class="d-flex align-items-center">
<h4 class="card-title">Key Metrics</h4> <div class="flex-grow-1">
</div> <div class="fs-3 fw-bold">{{ stats.cancelled_appointments|default:0 }}</div>
<div class="card-body"> <div>Cancelled</div>
<div class="row mb-3">
<div class="col-8">Show Rate:</div>
<div class="col-4 text-end">
<span class="badge bg-success">{{ stats.show_rate|floatformat:1 }}%</span>
</div>
</div> </div>
<div class="row mb-3"> <div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
<div class="col-8">Completion Rate:</div> <i class="fa fa-times fa-lg"></i>
<div class="col-4 text-end">
<span class="badge bg-primary">{{ stats.completion_rate|floatformat:1 }}%</span>
</div>
</div>
<div class="row mb-3">
<div class="col-8">Cancellation Rate:</div>
<div class="col-4 text-end">
<span class="badge bg-warning">{{ stats.cancellation_rate|floatformat:1 }}%</span>
</div>
</div>
<div class="row mb-3">
<div class="col-8">Average Duration:</div>
<div class="col-4 text-end">{{ stats.avg_duration|default:"N/A" }} min</div>
</div>
<div class="row">
<div class="col-8">Peak Hour:</div>
<div class="col-4 text-end">{{ stats.peak_hour|default:"N/A" }}</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Top Providers --> </div>
<div class="card mb-4"> <div class="col-xl-3 col-md-6">
<div class="card-header"> <div class="card bg-danger text-white">
<h4 class="card-title">Top Providers</h4> <div class="card-body">
</div> <div class="d-flex align-items-center">
<div class="card-body"> <div class="flex-grow-1">
{% for provider in stats.top_providers %} <div class="fs-3 fw-bold">{{ stats.no_show_appointments|default:0 }}</div>
<div class="d-flex align-items-center mb-3"> <div>No Shows</div>
<div class="flex-grow-1"> </div>
<div class="fw-bold">{{ provider.provider_name }}</div> <div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
<small class="text-muted">{{ provider.department }}</small> <i class="fa fa-user-times fa-lg"></i>
</div>
<div class="text-end">
<div class="fw-bold">{{ provider.appointment_count }}</div>
<small class="text-muted">appointments</small>
</div>
</div> </div>
{% endfor %}
</div> </div>
</div> </div>
</div>
<!-- Recent Trends --> </div>
<div class="card"> </div>
<div class="card-header">
<h4 class="card-title">Recent Trends</h4> <div class="row">
<div class="col-xl-8">
<!-- Appointments by Status -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Appointments by Status</h4>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Status</th>
<th>Count</th>
<th>Percentage</th>
<th>Visual</th>
</tr>
</thead>
<tbody>
{% for status in stats.by_status %}
<tr>
<td>{{ status.status|title }}</td>
<td>{{ status.count }}</td>
<td>{{ status.percentage|floatformat:1 }}%</td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar" role="progressbar" style="width: {{ status.percentage }}%"></div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<div class="card-body"> </div>
<div class="row mb-2"> </div>
<div class="col-8">This Week:</div>
<div class="col-4 text-end">{{ stats.this_week|default:0 }}</div> <!-- Appointments by Department -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Appointments by Department</h4>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Department</th>
<th>Total</th>
<th>Completed</th>
<th>Cancelled</th>
<th>No Shows</th>
</tr>
</thead>
<tbody>
{% for dept in stats.by_department %}
<tr>
<td>{{ dept.department_name }}</td>
<td>{{ dept.total }}</td>
<td>{{ dept.completed }}</td>
<td>{{ dept.cancelled }}</td>
<td>{{ dept.no_shows }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<!-- Key Metrics -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Key Metrics</h4>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-8">Show Rate:</div>
<div class="col-4 text-end">
<span class="badge bg-success">{{ stats.show_rate|floatformat:1 }}%</span>
</div> </div>
<div class="row mb-2"> </div>
<div class="col-8">Last Week:</div> <div class="row mb-3">
<div class="col-4 text-end">{{ stats.last_week|default:0 }}</div> <div class="col-8">Completion Rate:</div>
<div class="col-4 text-end">
<span class="badge bg-primary">{{ stats.completion_rate|floatformat:1 }}%</span>
</div> </div>
<div class="row mb-2"> </div>
<div class="col-8">This Month:</div> <div class="row mb-3">
<div class="col-4 text-end">{{ stats.this_month|default:0 }}</div> <div class="col-8">Cancellation Rate:</div>
<div class="col-4 text-end">
<span class="badge bg-warning">{{ stats.cancellation_rate|floatformat:1 }}%</span>
</div> </div>
<div class="row"> </div>
<div class="col-8">Last Month:</div> <div class="row mb-3">
<div class="col-4 text-end">{{ stats.last_month|default:0 }}</div> <div class="col-8">Average Duration:</div>
<div class="col-4 text-end">{{ stats.avg_duration|default:"N/A" }} min</div>
</div>
<div class="row">
<div class="col-8">Peak Hour:</div>
<div class="col-4 text-end">{{ stats.peak_hour|default:"N/A" }}</div>
</div>
</div>
</div>
<!-- Top Providers -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Top Providers</h4>
</div>
<div class="card-body">
{% for provider in stats.top_providers %}
<div class="d-flex align-items-center mb-3">
<div class="flex-grow-1">
<div class="fw-bold">{{ provider.provider_name }}</div>
<small class="text-muted">{{ provider.department }}</small>
</div> </div>
<div class="text-end">
<div class="fw-bold">{{ provider.appointment_count }}</div>
<small class="text-muted">appointments</small>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Recent Trends -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Recent Trends</h4>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-8">This Week:</div>
<div class="col-4 text-end">{{ stats.this_week|default:0 }}</div>
</div>
<div class="row mb-2">
<div class="col-8">Last Week:</div>
<div class="col-4 text-end">{{ stats.last_week|default:0 }}</div>
</div>
<div class="row mb-2">
<div class="col-8">This Month:</div>
<div class="col-4 text-end">{{ stats.this_month|default:0 }}</div>
</div>
<div class="row">
<div class="col-8">Last Month:</div>
<div class="col-4 text-end">{{ stats.last_month|default:0 }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -242,6 +241,7 @@
</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,152 +7,151 @@
{% 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">
<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>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li> <li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li>
<li class="breadcrumb-item active">Calendar</li> <li class="breadcrumb-item active">Calendar</li>
</ul> </ul>
<div class="d-flex align-items-center justify-content-between mb-3"> <div class="d-flex align-items-center justify-content-between mb-3">
<h1 class="page-header mb-0">Appointment Calendar</h1> <h1 class="page-header mb-0">Appointment Calendar</h1>
<div> <div>
<a href="#" class="btn btn-success"> <a href="#" class="btn btn-success">
<i class="fa fa-plus me-2"></i>New Appointment <i class="fa fa-plus me-2"></i>New Appointment
</a> </a>
</div>
</div>
<div class="row">
<div class="col-xl-9">
<div class="card">
<div class="card-body">
<div id="calendar"></div>
</div>
</div> </div>
</div> </div>
<div class="row"> <div class="col-xl-3">
<div class="col-xl-9"> <!-- Filters -->
<div class="card"> <div class="card mb-4">
<div class="card-body"> <div class="card-header">
<div id="calendar"></div> <h4 class="card-title">Filters</h4>
</div>
<div class="card-body">
<form id="filterForm">
<div class="mb-3">
<label class="form-label">Provider</label>
<select name="provider" class="form-select" id="providerFilter">
<option value="">All Providers</option>
{% for provider in appointments.provider.all %}
<option value="{{ provider.id }}">{{ provider.first_name }} {{ provider.last_name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Department</label>
<select name="department" class="form-select" id="departmentFilter">
<option value="">All Departments</option>
{% for dept in departments %}
<option value="{{ dept.id }}">{{ dept.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Status</label>
<select name="status" class="form-select" id="statusFilter">
<option value="">All Statuses</option>
<option value="scheduled">Scheduled</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>
<button type="button" class="btn btn-primary" onclick="applyFilters()">
<i class="fa fa-filter me-2"></i>Apply Filters
</button>
</form>
</div>
</div>
<!-- Legend -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Status Legend</h4>
</div>
<div class="card-body">
<div class="d-flex align-items-center mb-2">
<div class="w-20px h-20px bg-info rounded me-2"></div>
<span>Scheduled</span>
</div>
<div class="d-flex align-items-center mb-2">
<div class="w-20px h-20px bg-success rounded me-2"></div>
<span>Confirmed</span>
</div>
<div class="d-flex align-items-center mb-2">
<div class="w-20px h-20px bg-primary rounded me-2"></div>
<span>Checked In</span>
</div>
<div class="d-flex align-items-center mb-2">
<div class="w-20px h-20px bg-warning rounded me-2"></div>
<span>In Progress</span>
</div>
<div class="d-flex align-items-center mb-2">
<div class="w-20px h-20px bg-dark rounded me-2"></div>
<span>Completed</span>
</div>
<div class="d-flex align-items-center mb-2">
<div class="w-20px h-20px bg-danger rounded me-2"></div>
<span>Cancelled</span>
</div>
<div class="d-flex align-items-center">
<div class="w-20px h-20px bg-secondary rounded me-2"></div>
<span>No Show</span>
</div> </div>
</div> </div>
</div> </div>
<div class="col-xl-3"> <!-- Today's Appointments -->
<!-- Filters --> <div class="card">
<div class="card mb-4"> <div class="card-header">
<div class="card-header"> <h4 class="card-title">Today's Appointments</h4>
<h4 class="card-title">Filters</h4>
</div>
<div class="card-body">
<form id="filterForm">
<div class="mb-3">
<label class="form-label">Provider</label>
<select name="provider" class="form-select" id="providerFilter">
<option value="">All Providers</option>
{% for provider in appointments.provider.all %}
<option value="{{ provider.id }}">{{ provider.first_name }} {{ provider.last_name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Department</label>
<select name="department" class="form-select" id="departmentFilter">
<option value="">All Departments</option>
{% for dept in departments %}
<option value="{{ dept.id }}">{{ dept.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Status</label>
<select name="status" class="form-select" id="statusFilter">
<option value="">All Statuses</option>
<option value="scheduled">Scheduled</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>
<button type="button" class="btn btn-primary" onclick="applyFilters()">
<i class="fa fa-filter me-2"></i>Apply Filters
</button>
</form>
</div>
</div> </div>
<div class="card-body">
<!-- Legend --> {% for appointment in appointments %}
<div class="card mb-4"> <div class="d-flex align-items-center mb-3 p-2 border rounded">
<div class="card-header"> <div class="flex-grow-1">
<h4 class="card-title">Status Legend</h4> <div class="fw-bold">{{ appointment.appointment_time|time:"g:i A" }}</div>
</div> <div class="small">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</div>
<div class="card-body"> <div class="small text-muted">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
<div class="d-flex align-items-center mb-2">
<div class="w-20px h-20px bg-info rounded me-2"></div>
<span>Scheduled</span>
</div> </div>
<div class="d-flex align-items-center mb-2"> <div>
<div class="w-20px h-20px bg-success rounded me-2"></div> {% if appointment.status == 'SCHEDULED' %}
<span>Confirmed</span> <span class="badge bg-info">Scheduled</span>
</div> {% elif appointment.status == 'CONFIRMED' %}
<div class="d-flex align-items-center mb-2"> <span class="badge bg-success">Confirmed</span>
<div class="w-20px h-20px bg-primary rounded me-2"></div> {% elif appointment.status == 'CHECKED_IN' %}
<span>Checked In</span> <span class="badge bg-primary">Checked In</span>
</div> {% elif appointment.status == 'IN_PROGRESS' %}
<div class="d-flex align-items-center mb-2"> <span class="badge bg-warning">In Progress</span>
<div class="w-20px h-20px bg-warning rounded me-2"></div> {% elif appointment.status == 'COMPLETED' %}
<span>In Progress</span> <span class="badge bg-dark">Completed</span>
</div> {% elif appointment.status == 'CANCELLED' %}
<div class="d-flex align-items-center mb-2"> <span class="badge bg-danger">Cancelled</span>
<div class="w-20px h-20px bg-dark rounded me-2"></div> {% elif appointment.status == 'NO_SHOW' %}
<span>Completed</span> <span class="badge bg-secondary">No Show</span>
</div> {% endif %}
<div class="d-flex align-items-center mb-2">
<div class="w-20px h-20px bg-danger rounded me-2"></div>
<span>Cancelled</span>
</div>
<div class="d-flex align-items-center">
<div class="w-20px h-20px bg-secondary rounded me-2"></div>
<span>No Show</span>
</div> </div>
</div> </div>
</div> {% empty %}
<p class="text-muted text-center">No appointments today</p>
<!-- Today's Appointments --> {% endfor %}
<div class="card">
<div class="card-header">
<h4 class="card-title">Today's Appointments</h4>
</div>
<div class="card-body">
{% for appointment in appointments %}
<div class="d-flex align-items-center mb-3 p-2 border rounded">
<div class="flex-grow-1">
<div class="fw-bold">{{ appointment.appointment_time|time:"g:i A" }}</div>
<div class="small">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</div>
<div class="small text-muted">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
<div>
{% if appointment.status == 'SCHEDULED' %}
<span class="badge bg-info">Scheduled</span>
{% elif appointment.status == 'CONFIRMED' %}
<span class="badge bg-success">Confirmed</span>
{% elif appointment.status == 'CHECKED_IN' %}
<span class="badge bg-primary">Checked In</span>
{% elif appointment.status == 'IN_PROGRESS' %}
<span class="badge bg-warning">In Progress</span>
{% elif appointment.status == 'COMPLETED' %}
<span class="badge bg-dark">Completed</span>
{% elif appointment.status == 'CANCELLED' %}
<span class="badge bg-danger">Cancelled</span>
{% elif appointment.status == 'NO_SHOW' %}
<span class="badge bg-secondary">No Show</span>
{% endif %}
</div>
</div>
{% empty %}
<p class="text-muted text-center">No appointments today</p>
{% endfor %}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -161,6 +160,7 @@
</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">
<div class="modal-dialog"> <div class="modal-dialog">

View File

@ -2,214 +2,267 @@
{% 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>
<div class="row justify-content-center"> <h1 class="h2">
<div class="col-xl-8"> <i class="fas fa-calendar-alt"></i> Cancel<span class="fw-light">Appointment</span>
<ul class="breadcrumb"> </h1>
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li> <p class="text-muted">Appointment cancellation form.</p>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li> </div>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_detail' appointment.id %}">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</a></li> </div>
<li class="breadcrumb-item active">Cancel</li> <div class="container-fluid">
</ul> <div class="row justify-content-center">
<div class="col-xl-8">
<h1 class="page-header">Cancel Appointment</h1> <!-- Appointment Info -->
<div class="panel panel-inverse mb-4" data-sortable-id="index-1">
<!-- Appointment Info --> <div class="panel-heading">
<div class="card mb-4"> <h4 class="panel-title">
<div class="card-header"> <i class="fas fa-calendar-day"></i> Appointment Details
<h4 class="card-title">Appointment Details</h4> </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="col-md-6">
<div class="row mb-2">
<div class="col-4"><strong>Patient:</strong></div>
<div class="col-8">{{ appointment.patient.get_full_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.mrn }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.get_full_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.provider.department }}</div>
</div>
</div>
<div class="col-md-6">
<div class="row mb-2">
<div class="col-4"><strong>Date:</strong></div>
<div class="col-8">{{ appointment.preferred_date|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Time:</strong></div>
<div class="col-8">{{ appointment.preferred_time|time:"g:i A" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Type:</strong></div>
<div class="col-8">{{ appointment.get_appointment_type_display }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Status:</strong></div>
<div class="col-8">
{% if appointment.status == 'SCHEDULED' %}
<span class="badge bg-info">Scheduled</span>
{% elif appointment.status == 'CONFIRMED' %}
<span class="badge bg-success">Confirmed</span>
{% elif appointment.status == 'CHECKED_IN' %}
<span class="badge bg-primary">Checked In</span>
{% endif %}
</div>
</div>
</div>
</div>
{% if appointment.notes %}
<div class="row mt-3">
<div class="col-12">
<strong>Current Notes:</strong>
<p class="mt-2">{{ appointment.notes }}</p>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Cancellation Form -->
<div class="panel panel-inverse mb-4" data-sortable-id="index-2">
<div class="panel-heading">
<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="panel-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle me-2"></i>
<strong>Warning:</strong> This action will permanently cancel the appointment. This cannot be undone.
</div>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Reason <span class="text-danger">*</span></label>
<div class="col-md-9">
<select name="cancellation_reason" class="form-select" required>
<option value="">Select Reason</option>
<option value="patient_request" {% if form.cancellation_reason.value == 'patient_request' %}selected{% endif %}>Patient Request</option>
<option value="patient_illness" {% if form.cancellation_reason.value == 'patient_illness' %}selected{% endif %}>Patient Illness</option>
<option value="provider_unavailable" {% if form.cancellation_reason.value == 'provider_unavailable' %}selected{% endif %}>Provider Unavailable</option>
<option value="emergency" {% if form.cancellation_reason.value == 'emergency' %}selected{% endif %}>Emergency</option>
<option value="equipment_failure" {% if form.cancellation_reason.value == 'equipment_failure' %}selected{% endif %}>Equipment Failure</option>
<option value="weather" {% if form.cancellation_reason.value == 'weather' %}selected{% endif %}>Weather Conditions</option>
<option value="scheduling_error" {% if form.cancellation_reason.value == 'scheduling_error' %}selected{% endif %}>Scheduling Error</option>
<option value="insurance_issue" {% if form.cancellation_reason.value == 'insurance_issue' %}selected{% endif %}>Insurance Issue</option>
<option value="other" {% if form.cancellation_reason.value == 'other' %}selected{% endif %}>Other</option>
</select>
{% if form.cancellation_reason.errors %}
<div class="text-danger">{{ form.cancellation_reason.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Notes</label>
<div class="col-md-9">
<textarea name="cancellation_notes" class="form-control" rows="4"
placeholder="Additional details about the cancellation">{{ form.cancellation_notes.value }}</textarea>
{% if form.cancellation_notes.errors %}
<div class="text-danger">{{ form.cancellation_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Fee</label>
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text">
<span class="symbol m-0 p-0">&#xea;</span>
</span>
<input type="number" name="cancellation_fee" class="form-control"
step="0.01" min="0" value="{{ form.cancellation_fee.value|default:'0.00' }}">
</div>
<small class="form-text text-muted">Enter cancellation fee if applicable</small>
{% if form.cancellation_fee.errors %}
<div class="text-danger">{{ form.cancellation_fee.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="notify_patient" id="notify_patient"
{% if form.notify_patient.value %}checked{% endif %}>
<label class="form-check-label" for="notify_patient">
Send cancellation notification to patient
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="offer_reschedule" id="offer_reschedule"
{% if form.offer_reschedule.value %}checked{% endif %}>
<label class="form-check-label" for="offer_reschedule">
Offer rescheduling options to patient
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="add_to_waitlist" id="add_to_waitlist"
{% if form.add_to_waitlist.value %}checked{% endif %}>
<label class="form-check-label" for="add_to_waitlist">
Add patient to waitlist for earlier appointments
</label>
</div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-9 offset-md-3">
<div class="row mb-2"> <button type="button" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#confirm-cancellation">
<div class="col-4"><strong>Patient:</strong></div> <i class="fa fa-times me-2"></i>Cancel Appointment
<div class="col-8">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</div> </button>
</div> <a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-sm btn-secondary ms-2">
<div class="row mb-2"> <i class="fa fa-arrow-left me-2"></i>Go Back
<div class="col-4"><strong>Patient ID:</strong></div> </a>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
</div>
</div> </div>
<div class="col-md-6"> </div>
<div class="row mb-2"> <div>
<div class="col-4"><strong>Date:</strong></div> <div class="modal fade" id="confirm-cancellation" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div> <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>
<div class="row mb-2"> <div class="modal-body">
<div class="col-4"><strong>Time:</strong></div> <p class="mb-0">Are you sure you want to cancel this appointment? This action cannot be undone.</p>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
</div> </div>
<div class="row mb-2"> <div class="modal-footer">
<div class="col-4"><strong>Type:</strong></div> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<div class="col-8">{{ appointment.appointment_type.name }}</div> <button type="submit" class="btn btn-danger" id="confirmDelete">Confirm</button>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Status:</strong></div>
<div class="col-8">
{% if appointment.status == 'scheduled' %}
<span class="badge bg-info">Scheduled</span>
{% elif appointment.status == 'confirmed' %}
<span class="badge bg-success">Confirmed</span>
{% elif appointment.status == 'checked_in' %}
<span class="badge bg-primary">Checked In</span>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>
{% if appointment.notes %}
<div class="row mt-3">
<div class="col-12">
<strong>Current Notes:</strong>
<p class="mt-2">{{ appointment.notes }}</p>
</div>
</div>
{% endif %}
</div> </div>
</div>
</form>
</div> </div>
</div>
<!-- Cancellation Form -->
<div class="card">
<div class="card-header"> </div>
<h4 class="card-title">Cancellation Details</h4> <div class="col-xl-4">
</div> <!-- Cancellation Policy -->
<div class="card-body"> <div class="panel panel-inverse mb-4" data-sortable-id="index-3">
{% if messages %} <div class="panel-heading bg-gradient-danger">
{% for message in messages %} <h4 class="panel-title">
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert"> <i class="fas fa-calendar-times"></i> Cancellation Policy
{{ message }} </h4>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle me-2"></i>
<strong>Warning:</strong> This action will permanently cancel the appointment. This cannot be undone.
</div>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Reason <span class="text-danger">*</span></label>
<div class="col-md-9">
<select name="cancellation_reason" class="form-select" required>
<option value="">Select Reason</option>
<option value="patient_request" {% if form.cancellation_reason.value == 'patient_request' %}selected{% endif %}>Patient Request</option>
<option value="patient_illness" {% if form.cancellation_reason.value == 'patient_illness' %}selected{% endif %}>Patient Illness</option>
<option value="provider_unavailable" {% if form.cancellation_reason.value == 'provider_unavailable' %}selected{% endif %}>Provider Unavailable</option>
<option value="emergency" {% if form.cancellation_reason.value == 'emergency' %}selected{% endif %}>Emergency</option>
<option value="equipment_failure" {% if form.cancellation_reason.value == 'equipment_failure' %}selected{% endif %}>Equipment Failure</option>
<option value="weather" {% if form.cancellation_reason.value == 'weather' %}selected{% endif %}>Weather Conditions</option>
<option value="scheduling_error" {% if form.cancellation_reason.value == 'scheduling_error' %}selected{% endif %}>Scheduling Error</option>
<option value="insurance_issue" {% if form.cancellation_reason.value == 'insurance_issue' %}selected{% endif %}>Insurance Issue</option>
<option value="other" {% if form.cancellation_reason.value == 'other' %}selected{% endif %}>Other</option>
</select>
{% if form.cancellation_reason.errors %}
<div class="text-danger">{{ form.cancellation_reason.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Notes</label>
<div class="col-md-9">
<textarea name="cancellation_notes" class="form-control" rows="4"
placeholder="Additional details about the cancellation">{{ form.cancellation_notes.value|default:'' }}</textarea>
{% if form.cancellation_notes.errors %}
<div class="text-danger">{{ form.cancellation_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Fee</label>
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" name="cancellation_fee" class="form-control"
step="0.01" min="0" value="{{ form.cancellation_fee.value|default:'0.00' }}">
</div>
<small class="form-text text-muted">Enter cancellation fee if applicable</small>
{% if form.cancellation_fee.errors %}
<div class="text-danger">{{ form.cancellation_fee.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="notify_patient" id="notify_patient"
{% if form.notify_patient.value %}checked{% endif %}>
<label class="form-check-label" for="notify_patient">
Send cancellation notification to patient
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="offer_reschedule" id="offer_reschedule"
{% if form.offer_reschedule.value %}checked{% endif %}>
<label class="form-check-label" for="offer_reschedule">
Offer rescheduling options to patient
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="add_to_waitlist" id="add_to_waitlist"
{% if form.add_to_waitlist.value %}checked{% endif %}>
<label class="form-check-label" for="add_to_waitlist">
Add patient to waitlist for earlier appointments
</label>
</div>
</div>
</div>
<div class="row">
<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.')">
<i class="fa fa-times me-2"></i>Cancel Appointment
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div>
</div> </div>
<div class="panel-body">
<!-- Cancellation Policy --> <ul class="mb-0">
<div class="card mt-4"> <li>Appointments cancelled with less than 24 hours notice may incur a cancellation fee</li>
<div class="card-header"> <li>Emergency cancellations are exempt from cancellation fees</li>
<h4 class="card-title">Cancellation Policy</h4> <li>Patients will be notified of the cancellation via their preferred communication method</li>
</div> <li>Cancelled appointments will be made available to other patients on the waitlist</li>
<div class="card-body"> <li>Refunds for prepaid appointments will be processed according to the refund policy</li>
<ul class="mb-0"> </ul>
<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>Patients will be notified of the cancellation via their preferred communication method</li>
<li>Cancelled appointments will be made available to other patients on the waitlist</li>
<li>Refunds for prepaid appointments will be processed according to the refund policy</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
@ -220,14 +273,7 @@ $(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() {
var reason = $(this).val(); var reason = $(this).val();

View File

@ -4,302 +4,301 @@
{% 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">
<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>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</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"><a href="{% url 'appointments:appointment_detail' appointment.id %}">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</a></li>
<li class="breadcrumb-item active">Check In</li> <li class="breadcrumb-item active">Check In</li>
</ul> </ul>
<h1 class="page-header">Patient Check-In</h1> <h1 class="page-header">Patient Check-In</h1>
<div class="row"> <div class="row">
<div class="col-xl-8"> <div class="col-xl-8">
<!-- Appointment Info --> <!-- Appointment Info -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Appointment Details</h4> <h4 class="card-title">Appointment Details</h4>
</div> </div>
<div class="card-body"> <div class="card-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.first_name }} {{ appointment.patient.last_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>DOB:</strong></div>
<div class="col-8">{{ appointment.patient.date_of_birth|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Phone:</strong></div>
<div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
</div>
</div> </div>
<div class="col-md-6"> <div class="row mb-2">
<div class="row mb-2"> <div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-4"><strong>Date:</strong></div> <div class="col-8">{{ appointment.patient.patient_id }}</div>
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div> </div>
</div> <div class="row mb-2">
<div class="row mb-2"> <div class="col-4"><strong>DOB:</strong></div>
<div class="col-4"><strong>Time:</strong></div> <div class="col-8">{{ appointment.patient.date_of_birth|date:"M d, Y" }}</div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div> </div>
</div> <div class="row mb-2">
<div class="row mb-2"> <div class="col-4"><strong>Phone:</strong></div>
<div class="col-4"><strong>Provider:</strong></div> <div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
</div>
</div> </div>
</div> </div>
</div> <div class="col-md-6">
</div> <div class="row mb-2">
<div class="col-4"><strong>Date:</strong></div>
<!-- Check-in Form --> <div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div>
<div class="card">
<div class="card-header">
<h4 class="card-title">Check-In Information</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Check-in Time</label>
<div class="col-md-9">
<input type="datetime-local" name="checkin_time" class="form-control"
value="{{ form.checkin_time.value|default:now|date:'Y-m-d\TH:i' }}" required>
{% if form.checkin_time.errors %}
<div class="text-danger">{{ form.checkin_time.errors.0 }}</div>
{% endif %}
</div>
</div> </div>
<div class="row mb-2">
<div class="row mb-3"> <div class="col-4"><strong>Time:</strong></div>
<label class="col-form-label col-md-3">Checked In By</label> <div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
<div class="col-md-9">
<input type="text" name="checked_in_by" class="form-control"
value="{{ form.checked_in_by.value|default:request.user.get_full_name }}" readonly>
{% if form.checked_in_by.errors %}
<div class="text-danger">{{ form.checked_in_by.errors.0 }}</div>
{% endif %}
</div>
</div> </div>
<div class="row mb-2">
<div class="row mb-3"> <div class="col-4"><strong>Provider:</strong></div>
<label class="col-form-label col-md-3">Verification Checklist</label> <div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="id_verified" id="id_verified"
{% if form.id_verified.value %}checked{% endif %}>
<label class="form-check-label" for="id_verified">
Photo ID verified
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="insurance_verified" id="insurance_verified"
{% if form.insurance_verified.value %}checked{% endif %}>
<label class="form-check-label" for="insurance_verified">
Insurance card verified
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="forms_completed" id="forms_completed"
{% if form.forms_completed.value %}checked{% endif %}>
<label class="form-check-label" for="forms_completed">
Required forms completed
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="copay_collected" id="copay_collected"
{% if form.copay_collected.value %}checked{% endif %}>
<label class="form-check-label" for="copay_collected">
Co-payment collected (if applicable)
</label>
</div>
</div>
</div> </div>
<div class="row mb-2">
<div class="row mb-3"> <div class="col-4"><strong>Department:</strong></div>
<label class="col-form-label col-md-3">Waiting Area Assignment</label> <div class="col-8">{{ appointment.department.name }}</div>
<div class="col-md-9">
<select name="waiting_area" class="form-select">
<option value="">Select Waiting Area</option>
<option value="main_lobby" {% if form.waiting_area.value == 'main_lobby' %}selected{% endif %}>Main Lobby</option>
<option value="pediatric_area" {% if form.waiting_area.value == 'pediatric_area' %}selected{% endif %}>Pediatric Area</option>
<option value="specialty_clinic" {% if form.waiting_area.value == 'specialty_clinic' %}selected{% endif %}>Specialty Clinic</option>
<option value="urgent_care" {% if form.waiting_area.value == 'urgent_care' %}selected{% endif %}>Urgent Care</option>
<option value="surgical_waiting" {% if form.waiting_area.value == 'surgical_waiting' %}selected{% endif %}>Surgical Waiting</option>
</select>
{% if form.waiting_area.errors %}
<div class="text-danger">{{ form.waiting_area.errors.0 }}</div>
{% endif %}
</div>
</div> </div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Special Needs</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="wheelchair_needed" id="wheelchair_needed"
{% if form.wheelchair_needed.value %}checked{% endif %}>
<label class="form-check-label" for="wheelchair_needed">
Wheelchair assistance needed
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="interpreter_needed" id="interpreter_needed"
{% if form.interpreter_needed.value %}checked{% endif %}>
<label class="form-check-label" for="interpreter_needed">
Interpreter services needed
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="special_assistance" id="special_assistance"
{% if form.special_assistance.value %}checked{% endif %}>
<label class="form-check-label" for="special_assistance">
Other special assistance required
</label>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Check-in Notes</label>
<div class="col-md-9">
<textarea name="checkin_notes" class="form-control" rows="3"
placeholder="Any additional notes about the check-in process">{{ form.checkin_notes.value|default:'' }}</textarea>
{% if form.checkin_notes.errors %}
<div class="text-danger">{{ form.checkin_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-success">
<i class="fa fa-check me-2"></i>Complete Check-In
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
<div class="col-xl-4"> <!-- Check-in Form -->
<!-- Patient Information --> <div class="card">
<div class="card mb-4"> <div class="card-header">
<div class="card-header"> <h4 class="card-title">Check-In Information</h4>
<h4 class="card-title">Patient Information</h4> </div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Check-in Time</label>
<div class="col-md-9">
<input type="datetime-local" name="checkin_time" class="form-control"
value="{{ form.checkin_time.value|default:now|date:'Y-m-d\TH:i' }}" required>
{% if form.checkin_time.errors %}
<div class="text-danger">{{ form.checkin_time.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Checked In By</label>
<div class="col-md-9">
<input type="text" name="checked_in_by" class="form-control"
value="{{ form.checked_in_by.value|default:request.user.get_full_name }}" readonly>
{% if form.checked_in_by.errors %}
<div class="text-danger">{{ form.checked_in_by.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Verification Checklist</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="id_verified" id="id_verified"
{% if form.id_verified.value %}checked{% endif %}>
<label class="form-check-label" for="id_verified">
Photo ID verified
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="insurance_verified" id="insurance_verified"
{% if form.insurance_verified.value %}checked{% endif %}>
<label class="form-check-label" for="insurance_verified">
Insurance card verified
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="forms_completed" id="forms_completed"
{% if form.forms_completed.value %}checked{% endif %}>
<label class="form-check-label" for="forms_completed">
Required forms completed
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="copay_collected" id="copay_collected"
{% if form.copay_collected.value %}checked{% endif %}>
<label class="form-check-label" for="copay_collected">
Co-payment collected (if applicable)
</label>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Waiting Area Assignment</label>
<div class="col-md-9">
<select name="waiting_area" class="form-select">
<option value="">Select Waiting Area</option>
<option value="main_lobby" {% if form.waiting_area.value == 'main_lobby' %}selected{% endif %}>Main Lobby</option>
<option value="pediatric_area" {% if form.waiting_area.value == 'pediatric_area' %}selected{% endif %}>Pediatric Area</option>
<option value="specialty_clinic" {% if form.waiting_area.value == 'specialty_clinic' %}selected{% endif %}>Specialty Clinic</option>
<option value="urgent_care" {% if form.waiting_area.value == 'urgent_care' %}selected{% endif %}>Urgent Care</option>
<option value="surgical_waiting" {% if form.waiting_area.value == 'surgical_waiting' %}selected{% endif %}>Surgical Waiting</option>
</select>
{% if form.waiting_area.errors %}
<div class="text-danger">{{ form.waiting_area.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Special Needs</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="wheelchair_needed" id="wheelchair_needed"
{% if form.wheelchair_needed.value %}checked{% endif %}>
<label class="form-check-label" for="wheelchair_needed">
Wheelchair assistance needed
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="interpreter_needed" id="interpreter_needed"
{% if form.interpreter_needed.value %}checked{% endif %}>
<label class="form-check-label" for="interpreter_needed">
Interpreter services needed
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="special_assistance" id="special_assistance"
{% if form.special_assistance.value %}checked{% endif %}>
<label class="form-check-label" for="special_assistance">
Other special assistance required
</label>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Check-in Notes</label>
<div class="col-md-9">
<textarea name="checkin_notes" class="form-control" rows="3"
placeholder="Any additional notes about the check-in process">{{ form.checkin_notes.value|default:'' }}</textarea>
{% if form.checkin_notes.errors %}
<div class="text-danger">{{ form.checkin_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-success">
<i class="fa fa-check me-2"></i>Complete Check-In
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-4">
<!-- Patient Information -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Patient Information</h4>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-5"><strong>Age:</strong></div>
<div class="col-7">{{ appointment.patient.age|default:"N/A" }}</div>
</div> </div>
<div class="card-body"> <div class="row mb-2">
<div class="row mb-2"> <div class="col-5"><strong>Gender:</strong></div>
<div class="col-5"><strong>Age:</strong></div> <div class="col-7">{{ appointment.patient.gender|default:"N/A" }}</div>
<div class="col-7">{{ appointment.patient.age|default:"N/A" }}</div> </div>
</div> <div class="row mb-2">
<div class="row mb-2"> <div class="col-5"><strong>Address:</strong></div>
<div class="col-5"><strong>Gender:</strong></div> <div class="col-7">{{ appointment.patient.address|default:"Not provided" }}</div>
<div class="col-7">{{ appointment.patient.gender|default:"N/A" }}</div> </div>
</div> <div class="row mb-2">
<div class="row mb-2"> <div class="col-5"><strong>Emergency Contact:</strong></div>
<div class="col-5"><strong>Address:</strong></div> <div class="col-7">{{ appointment.patient.emergency_contact|default:"Not provided" }}</div>
<div class="col-7">{{ appointment.patient.address|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Emergency Contact:</strong></div>
<div class="col-7">{{ appointment.patient.emergency_contact|default:"Not provided" }}</div>
</div>
</div> </div>
</div> </div>
</div>
<!-- Insurance Information -->
<div class="card mb-4"> <!-- Insurance Information -->
<div class="card-header"> <div class="card mb-4">
<h4 class="card-title">Insurance Information</h4> <div class="card-header">
</div> <h4 class="card-title">Insurance Information</h4>
<div class="card-body">
{% if appointment.patient.insurance_provider %}
<div class="row mb-2">
<div class="col-5"><strong>Provider:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_provider }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Policy #:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_policy_number }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Group #:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_group_number|default:"N/A" }}</div>
</div>
{% else %}
<p class="text-muted">No insurance information on file</p>
{% endif %}
</div>
</div> </div>
<div class="card-body">
<!-- Alerts --> {% if appointment.patient.insurance_provider %}
<div class="card mb-4"> <div class="row mb-2">
<div class="card-header"> <div class="col-5"><strong>Provider:</strong></div>
<h4 class="card-title">Patient Alerts</h4> <div class="col-7">{{ appointment.patient.insurance_provider }}</div>
</div>
<div class="card-body">
{% if appointment.patient.allergies %}
<div class="alert alert-warning">
<strong>Allergies:</strong> {{ appointment.patient.allergies }}
</div>
{% endif %}
{% if appointment.patient.medical_alerts %}
<div class="alert alert-danger">
<strong>Medical Alerts:</strong> {{ appointment.patient.medical_alerts }}
</div>
{% endif %}
{% if not appointment.patient.allergies and not appointment.patient.medical_alerts %}
<p class="text-muted">No alerts on file</p>
{% endif %}
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Quick Actions</h4>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'patients:patient_detail' appointment.patient.id %}" class="btn btn-outline-primary btn-sm">
<i class="fa fa-user me-2"></i>View Patient Profile
</a>
<a href="{% url 'appointments:appointment_update' appointment.id %}" class="btn btn-outline-warning btn-sm">
<i class="fa fa-edit me-2"></i>Edit Appointment
</a>
<button type="button" class="btn btn-outline-info btn-sm" onclick="printCheckinForm()">
<i class="fa fa-print me-2"></i>Print Check-in Form
</button>
</div> </div>
<div class="row mb-2">
<div class="col-5"><strong>Policy #:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_policy_number }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Group #:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_group_number|default:"N/A" }}</div>
</div>
{% else %}
<p class="text-muted">No insurance information on file</p>
{% endif %}
</div>
</div>
<!-- Alerts -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Patient Alerts</h4>
</div>
<div class="card-body">
{% if appointment.patient.allergies %}
<div class="alert alert-warning">
<strong>Allergies:</strong> {{ appointment.patient.allergies }}
</div>
{% endif %}
{% if appointment.patient.medical_alerts %}
<div class="alert alert-danger">
<strong>Medical Alerts:</strong> {{ appointment.patient.medical_alerts }}
</div>
{% endif %}
{% if not appointment.patient.allergies and not appointment.patient.medical_alerts %}
<p class="text-muted">No alerts on file</p>
{% endif %}
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Quick Actions</h4>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'patients:patient_detail' appointment.patient.id %}" class="btn btn-outline-primary btn-sm">
<i class="fa fa-user me-2"></i>View Patient Profile
</a>
<a href="{% url 'appointments:appointment_update' appointment.id %}" class="btn btn-outline-warning btn-sm">
<i class="fa fa-edit me-2"></i>Edit Appointment
</a>
<button type="button" class="btn btn-outline-info btn-sm" onclick="printCheckinForm()">
<i class="fa fa-print me-2"></i>Print Check-in Form
</button>
</div> </div>
</div> </div>
</div> </div>
@ -308,6 +307,7 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -4,247 +4,246 @@
{% 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">
<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>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</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"><a href="{% url 'appointments:appointment_detail' appointment.id %}">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</a></li>
<li class="breadcrumb-item active">Confirm</li> <li class="breadcrumb-item active">Confirm</li>
</ul> </ul>
<h1 class="page-header">Confirm Appointment</h1> <h1 class="page-header">Confirm Appointment</h1>
<!-- Appointment Info --> <!-- Appointment Info -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Appointment Details</h4> <h4 class="card-title">Appointment Details</h4>
</div> </div>
<div class="card-body"> <div class="card-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.first_name }} {{ appointment.patient.last_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Phone:</strong></div>
<div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Email:</strong></div>
<div class="col-8">{{ appointment.patient.email|default:"Not provided" }}</div>
</div>
</div> </div>
<div class="col-md-6"> <div class="row mb-2">
<div class="row mb-2"> <div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-4"><strong>Date:</strong></div> <div class="col-8">{{ appointment.patient.patient_id }}</div>
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div> </div>
<div class="row mb-2">
<div class="col-4"><strong>Phone:</strong></div>
<div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Email:</strong></div>
<div class="col-8">{{ appointment.patient.email|default:"Not provided" }}</div>
</div>
</div>
<div class="col-md-6">
<div class="row mb-2">
<div class="col-4"><strong>Date:</strong></div>
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Time:</strong></div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Type:</strong></div>
<div class="col-8">{{ appointment.appointment_type.name }}</div>
</div>
</div>
</div>
{% if appointment.notes %}
<div class="row mt-3">
<div class="col-12">
<strong>Appointment Notes:</strong>
<p class="mt-2">{{ appointment.notes }}</p>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Confirmation Form -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Confirmation Details</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmation Method</label>
<div class="col-md-9">
<select name="confirmation_method" class="form-select">
<option value="phone" {% if form.confirmation_method.value == 'phone' %}selected{% endif %}>Phone Call</option>
<option value="email" {% if form.confirmation_method.value == 'email' %}selected{% endif %}>Email</option>
<option value="sms" {% if form.confirmation_method.value == 'sms' %}selected{% endif %}>SMS/Text</option>
<option value="in_person" {% if form.confirmation_method.value == 'in_person' %}selected{% endif %}>In Person</option>
<option value="online" {% if form.confirmation_method.value == 'online' %}selected{% endif %}>Online Portal</option>
</select>
{% if form.confirmation_method.errors %}
<div class="text-danger">{{ form.confirmation_method.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmed By</label>
<div class="col-md-9">
<input type="text" name="confirmed_by" class="form-control"
value="{{ form.confirmed_by.value|default:request.user.get_full_name }}" readonly>
{% if form.confirmed_by.errors %}
<div class="text-danger">{{ form.confirmed_by.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Patient Contact Verified</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="contact_verified" id="contact_verified"
{% if form.contact_verified.value %}checked{% endif %}>
<label class="form-check-label" for="contact_verified">
Patient contact information has been verified and is current
</label>
</div> </div>
<div class="row mb-2"> {% if form.contact_verified.errors %}
<div class="col-4"><strong>Time:</strong></div> <div class="text-danger">{{ form.contact_verified.errors.0 }}</div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div> {% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Insurance Verified</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="insurance_verified" id="insurance_verified"
{% if form.insurance_verified.value %}checked{% endif %}>
<label class="form-check-label" for="insurance_verified">
Patient insurance information has been verified
</label>
</div> </div>
<div class="row mb-2"> {% if form.insurance_verified.errors %}
<div class="col-4"><strong>Provider:</strong></div> <div class="text-danger">{{ form.insurance_verified.errors.0 }}</div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div> {% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Pre-appointment Instructions Given</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="instructions_given" id="instructions_given"
{% if form.instructions_given.value %}checked{% endif %}>
<label class="form-check-label" for="instructions_given">
Pre-appointment instructions have been provided to the patient
</label>
</div> </div>
<div class="row mb-2"> {% if form.instructions_given.errors %}
<div class="col-4"><strong>Department:</strong></div> <div class="text-danger">{{ form.instructions_given.errors.0 }}</div>
<div class="col-8">{{ appointment.department.name }}</div> {% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmation Notes</label>
<div class="col-md-9">
<textarea name="confirmation_notes" class="form-control" rows="4"
placeholder="Additional notes about the confirmation process">{{ form.confirmation_notes.value|default:'' }}</textarea>
{% if form.confirmation_notes.errors %}
<div class="text-danger">{{ form.confirmation_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="send_reminder" id="send_reminder"
{% if form.send_reminder.value %}checked{% endif %}>
<label class="form-check-label" for="send_reminder">
Send appointment reminder 24 hours before appointment
</label>
</div> </div>
<div class="row mb-2"> <div class="form-check">
<div class="col-4"><strong>Type:</strong></div> <input class="form-check-input" type="checkbox" name="send_confirmation_email" id="send_confirmation_email"
<div class="col-8">{{ appointment.appointment_type.name }}</div> {% if form.send_confirmation_email.value %}checked{% endif %}>
<label class="form-check-label" for="send_confirmation_email">
Send confirmation email to patient
</label>
</div> </div>
</div> </div>
</div> </div>
{% if appointment.notes %} <div class="row">
<div class="row mt-3"> <div class="col-md-9 offset-md-3">
<div class="col-12"> <button type="submit" class="btn btn-success">
<strong>Appointment Notes:</strong> <i class="fa fa-check me-2"></i>Confirm Appointment
<p class="mt-2">{{ appointment.notes }}</p> </button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div> </div>
</div> </div>
{% endif %} </form>
</div>
</div> </div>
</div>
<!-- Confirmation Form -->
<div class="card"> <!-- Pre-appointment Checklist -->
<div class="card-header"> <div class="card mt-4">
<h4 class="card-title">Confirmation Details</h4> <div class="card-header">
</div> <h4 class="card-title">Pre-appointment Checklist</h4>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmation Method</label>
<div class="col-md-9">
<select name="confirmation_method" class="form-select">
<option value="phone" {% if form.confirmation_method.value == 'phone' %}selected{% endif %}>Phone Call</option>
<option value="email" {% if form.confirmation_method.value == 'email' %}selected{% endif %}>Email</option>
<option value="sms" {% if form.confirmation_method.value == 'sms' %}selected{% endif %}>SMS/Text</option>
<option value="in_person" {% if form.confirmation_method.value == 'in_person' %}selected{% endif %}>In Person</option>
<option value="online" {% if form.confirmation_method.value == 'online' %}selected{% endif %}>Online Portal</option>
</select>
{% if form.confirmation_method.errors %}
<div class="text-danger">{{ form.confirmation_method.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmed By</label>
<div class="col-md-9">
<input type="text" name="confirmed_by" class="form-control"
value="{{ form.confirmed_by.value|default:request.user.get_full_name }}" readonly>
{% if form.confirmed_by.errors %}
<div class="text-danger">{{ form.confirmed_by.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Patient Contact Verified</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="contact_verified" id="contact_verified"
{% if form.contact_verified.value %}checked{% endif %}>
<label class="form-check-label" for="contact_verified">
Patient contact information has been verified and is current
</label>
</div>
{% if form.contact_verified.errors %}
<div class="text-danger">{{ form.contact_verified.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Insurance Verified</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="insurance_verified" id="insurance_verified"
{% if form.insurance_verified.value %}checked{% endif %}>
<label class="form-check-label" for="insurance_verified">
Patient insurance information has been verified
</label>
</div>
{% if form.insurance_verified.errors %}
<div class="text-danger">{{ form.insurance_verified.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Pre-appointment Instructions Given</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="instructions_given" id="instructions_given"
{% if form.instructions_given.value %}checked{% endif %}>
<label class="form-check-label" for="instructions_given">
Pre-appointment instructions have been provided to the patient
</label>
</div>
{% if form.instructions_given.errors %}
<div class="text-danger">{{ form.instructions_given.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmation Notes</label>
<div class="col-md-9">
<textarea name="confirmation_notes" class="form-control" rows="4"
placeholder="Additional notes about the confirmation process">{{ form.confirmation_notes.value|default:'' }}</textarea>
{% if form.confirmation_notes.errors %}
<div class="text-danger">{{ form.confirmation_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="send_reminder" id="send_reminder"
{% if form.send_reminder.value %}checked{% endif %}>
<label class="form-check-label" for="send_reminder">
Send appointment reminder 24 hours before appointment
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="send_confirmation_email" id="send_confirmation_email"
{% if form.send_confirmation_email.value %}checked{% endif %}>
<label class="form-check-label" for="send_confirmation_email">
Send confirmation email to patient
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-success">
<i class="fa fa-check me-2"></i>Confirm Appointment
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div>
</div> </div>
<div class="card-body">
<!-- Pre-appointment Checklist --> <div class="row">
<div class="card mt-4"> <div class="col-md-6">
<div class="card-header"> <h6>Patient Preparation:</h6>
<h4 class="card-title">Pre-appointment Checklist</h4> <ul>
</div> <li>Arrive 15 minutes early for check-in</li>
<div class="card-body"> <li>Bring valid photo ID</li>
<div class="row"> <li>Bring insurance cards</li>
<div class="col-md-6"> <li>Bring list of current medications</li>
<h6>Patient Preparation:</h6> <li>Complete any required forms</li>
<ul> </ul>
<li>Arrive 15 minutes early for check-in</li> </div>
<li>Bring valid photo ID</li> <div class="col-md-6">
<li>Bring insurance cards</li> <h6>Special Instructions:</h6>
<li>Bring list of current medications</li> <ul>
<li>Complete any required forms</li> {% if appointment.appointment_type.requires_fasting %}
</ul> <li>Fasting required - no food or drink 8 hours before appointment</li>
</div> {% endif %}
<div class="col-md-6"> {% if appointment.appointment_type.requires_preparation %}
<h6>Special Instructions:</h6> <li>Special preparation required - see appointment type instructions</li>
<ul> {% endif %}
{% if appointment.appointment_type.requires_fasting %} <li>Wear comfortable, loose-fitting clothing</li>
<li>Fasting required - no food or drink 8 hours before appointment</li> <li>Arrange transportation if sedation will be used</li>
{% endif %} <li>Bring a list of questions for the provider</li>
{% if appointment.appointment_type.requires_preparation %} </ul>
<li>Special preparation required - see appointment type instructions</li>
{% endif %}
<li>Wear comfortable, loose-fitting clothing</li>
<li>Arrange transportation if sedation will be used</li>
<li>Bring a list of questions for the provider</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -252,6 +251,7 @@
</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">
<h1 class="h2"> <div>
<i class="fas fa-calendar-alt"></i> Appointment Dashboard <h1 class="h2">
</h1> <i class="fas fa-calendar-alt"></i> Appointment<span class="fw-light">Dashboard</span>
<div class="btn-toolbar mb-2 mb-md-0"> </h1>
<div class="btn-group me-2"> <p class="text-muted">View your appointments, manage queues, and track your progress.</p>
<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,278 +4,278 @@
{% 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">
<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>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</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"><a href="{% url 'appointments:appointment_detail' appointment.id %}">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</a></li>
<li class="breadcrumb-item active">No Show</li> <li class="breadcrumb-item active">No Show</li>
</ul> </ul>
<h1 class="page-header">Mark Appointment as No Show</h1> <h1 class="page-header">Mark Appointment as No Show</h1>
<!-- Appointment Info --> <!-- Appointment Info -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Appointment Details</h4> <h4 class="card-title">Appointment Details</h4>
</div> </div>
<div class="card-body"> <div class="card-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.first_name }} {{ appointment.patient.last_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Phone:</strong></div>
<div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
</div> </div>
<div class="col-md-6"> <div class="row mb-2">
<div class="row mb-2"> <div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-4"><strong>Date:</strong></div> <div class="col-8">{{ appointment.patient.patient_id }}</div>
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div> </div>
</div> <div class="row mb-2">
<div class="row mb-2"> <div class="col-4"><strong>Phone:</strong></div>
<div class="col-4"><strong>Time:</strong></div> <div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</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>Department:</strong></div> <div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
<div class="col-8">{{ appointment.department.name }}</div> </div>
</div> </div>
<div class="row mb-2"> <div class="col-md-6">
<div class="col-4"><strong>Type:</strong></div> <div class="row mb-2">
<div class="col-8">{{ appointment.appointment_type.name }}</div> <div class="col-4"><strong>Date:</strong></div>
</div> <div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Time:</strong></div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Type:</strong></div>
<div class="col-8">{{ appointment.appointment_type.name }}</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- No Show Form -->
<div class="card"> <!-- No Show Form -->
<div class="card-header"> <div class="card">
<h4 class="card-title">No Show Documentation</h4> <div class="card-header">
</div> <h4 class="card-title">No Show Documentation</h4>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle me-2"></i>
<strong>Important:</strong> Please ensure you have attempted to contact the patient before marking as no show.
</div>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Wait Time</label>
<div class="col-md-9">
<div class="input-group">
<input type="number" name="wait_time" class="form-control"
value="{{ form.wait_time.value|default:'15' }}" min="0" max="120">
<span class="input-group-text">minutes</span>
</div>
<small class="form-text text-muted">How long did you wait for the patient?</small>
{% if form.wait_time.errors %}
<div class="text-danger">{{ form.wait_time.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Contact Attempts</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="phone_attempted" id="phone_attempted"
{% if form.phone_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="phone_attempted">
Phone call attempted
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="text_attempted" id="text_attempted"
{% if form.text_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="text_attempted">
Text message sent
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="email_attempted" id="email_attempted"
{% if form.email_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="email_attempted">
Email sent
</label>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Number of Contact Attempts</label>
<div class="col-md-9">
<select name="contact_attempts" class="form-select">
<option value="1" {% if form.contact_attempts.value == '1' %}selected{% endif %}>1 attempt</option>
<option value="2" {% if form.contact_attempts.value == '2' %}selected{% endif %}>2 attempts</option>
<option value="3" {% if form.contact_attempts.value == '3' %}selected{% endif %}>3 attempts</option>
<option value="4" {% if form.contact_attempts.value == '4' %}selected{% endif %}>4+ attempts</option>
</select>
{% if form.contact_attempts.errors %}
<div class="text-danger">{{ form.contact_attempts.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">No Show Reason</label>
<div class="col-md-9">
<select name="no_show_reason" class="form-select">
<option value="">Select Reason (if known)</option>
<option value="forgot" {% if form.no_show_reason.value == 'forgot' %}selected{% endif %}>Patient forgot appointment</option>
<option value="illness" {% if form.no_show_reason.value == 'illness' %}selected{% endif %}>Patient became ill</option>
<option value="emergency" {% if form.no_show_reason.value == 'emergency' %}selected{% endif %}>Emergency situation</option>
<option value="transportation" {% if form.no_show_reason.value == 'transportation' %}selected{% endif %}>Transportation issues</option>
<option value="work_conflict" {% if form.no_show_reason.value == 'work_conflict' %}selected{% endif %}>Work conflict</option>
<option value="weather" {% if form.no_show_reason.value == 'weather' %}selected{% endif %}>Weather conditions</option>
<option value="financial" {% if form.no_show_reason.value == 'financial' %}selected{% endif %}>Financial concerns</option>
<option value="no_response" {% if form.no_show_reason.value == 'no_response' %}selected{% endif %}>No response to contact attempts</option>
<option value="other" {% if form.no_show_reason.value == 'other' %}selected{% endif %}>Other</option>
</select>
{% if form.no_show_reason.errors %}
<div class="text-danger">{{ form.no_show_reason.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">No Show Fee</label>
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" name="no_show_fee" class="form-control"
step="0.01" min="0" value="{{ form.no_show_fee.value|default:'25.00' }}">
</div>
<small class="form-text text-muted">Standard no-show fee as per policy</small>
{% if form.no_show_fee.errors %}
<div class="text-danger">{{ form.no_show_fee.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Documented By</label>
<div class="col-md-9">
<input type="text" name="documented_by" class="form-control"
value="{{ form.documented_by.value|default:request.user.get_full_name }}" readonly>
{% if form.documented_by.errors %}
<div class="text-danger">{{ form.documented_by.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Notes</label>
<div class="col-md-9">
<textarea name="no_show_notes" class="form-control" rows="4"
placeholder="Additional details about the no-show incident, contact attempts, or patient communication">{{ form.no_show_notes.value|default:'' }}</textarea>
{% if form.no_show_notes.errors %}
<div class="text-danger">{{ form.no_show_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="reschedule_offered" id="reschedule_offered"
{% if form.reschedule_offered.value %}checked{% endif %}>
<label class="form-check-label" for="reschedule_offered">
Rescheduling was offered to patient
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="add_to_waitlist" id="add_to_waitlist"
{% if form.add_to_waitlist.value %}checked{% endif %}>
<label class="form-check-label" for="add_to_waitlist">
Add patient to waitlist for future appointments
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="send_follow_up" id="send_follow_up"
{% if form.send_follow_up.value %}checked{% endif %}>
<label class="form-check-label" for="send_follow_up">
Send follow-up communication to patient
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-warning" onclick="return confirm('Are you sure you want to mark this appointment as no show? This will apply the no-show fee and update the patient record.')">
<i class="fa fa-exclamation-triangle me-2"></i>Mark as No Show
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div>
</div> </div>
<div class="card-body">
<!-- No Show Policy --> {% if messages %}
<div class="card mt-4"> {% for message in messages %}
<div class="card-header"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
<h4 class="card-title">No Show Policy</h4> {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle me-2"></i>
<strong>Important:</strong> Please ensure you have attempted to contact the patient before marking as no show.
</div> </div>
<div class="card-body">
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Wait Time</label>
<div class="col-md-9">
<div class="input-group">
<input type="number" name="wait_time" class="form-control"
value="{{ form.wait_time.value|default:'15' }}" min="0" max="120">
<span class="input-group-text">minutes</span>
</div>
<small class="form-text text-muted">How long did you wait for the patient?</small>
{% if form.wait_time.errors %}
<div class="text-danger">{{ form.wait_time.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Contact Attempts</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="phone_attempted" id="phone_attempted"
{% if form.phone_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="phone_attempted">
Phone call attempted
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="text_attempted" id="text_attempted"
{% if form.text_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="text_attempted">
Text message sent
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="email_attempted" id="email_attempted"
{% if form.email_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="email_attempted">
Email sent
</label>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Number of Contact Attempts</label>
<div class="col-md-9">
<select name="contact_attempts" class="form-select">
<option value="1" {% if form.contact_attempts.value == '1' %}selected{% endif %}>1 attempt</option>
<option value="2" {% if form.contact_attempts.value == '2' %}selected{% endif %}>2 attempts</option>
<option value="3" {% if form.contact_attempts.value == '3' %}selected{% endif %}>3 attempts</option>
<option value="4" {% if form.contact_attempts.value == '4' %}selected{% endif %}>4+ attempts</option>
</select>
{% if form.contact_attempts.errors %}
<div class="text-danger">{{ form.contact_attempts.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">No Show Reason</label>
<div class="col-md-9">
<select name="no_show_reason" class="form-select">
<option value="">Select Reason (if known)</option>
<option value="forgot" {% if form.no_show_reason.value == 'forgot' %}selected{% endif %}>Patient forgot appointment</option>
<option value="illness" {% if form.no_show_reason.value == 'illness' %}selected{% endif %}>Patient became ill</option>
<option value="emergency" {% if form.no_show_reason.value == 'emergency' %}selected{% endif %}>Emergency situation</option>
<option value="transportation" {% if form.no_show_reason.value == 'transportation' %}selected{% endif %}>Transportation issues</option>
<option value="work_conflict" {% if form.no_show_reason.value == 'work_conflict' %}selected{% endif %}>Work conflict</option>
<option value="weather" {% if form.no_show_reason.value == 'weather' %}selected{% endif %}>Weather conditions</option>
<option value="financial" {% if form.no_show_reason.value == 'financial' %}selected{% endif %}>Financial concerns</option>
<option value="no_response" {% if form.no_show_reason.value == 'no_response' %}selected{% endif %}>No response to contact attempts</option>
<option value="other" {% if form.no_show_reason.value == 'other' %}selected{% endif %}>Other</option>
</select>
{% if form.no_show_reason.errors %}
<div class="text-danger">{{ form.no_show_reason.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">No Show Fee</label>
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" name="no_show_fee" class="form-control"
step="0.01" min="0" value="{{ form.no_show_fee.value|default:'25.00' }}">
</div>
<small class="form-text text-muted">Standard no-show fee as per policy</small>
{% if form.no_show_fee.errors %}
<div class="text-danger">{{ form.no_show_fee.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Documented By</label>
<div class="col-md-9">
<input type="text" name="documented_by" class="form-control"
value="{{ form.documented_by.value|default:request.user.get_full_name }}" readonly>
{% if form.documented_by.errors %}
<div class="text-danger">{{ form.documented_by.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Notes</label>
<div class="col-md-9">
<textarea name="no_show_notes" class="form-control" rows="4"
placeholder="Additional details about the no-show incident, contact attempts, or patient communication">{{ form.no_show_notes.value|default:'' }}</textarea>
{% if form.no_show_notes.errors %}
<div class="text-danger">{{ form.no_show_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="reschedule_offered" id="reschedule_offered"
{% if form.reschedule_offered.value %}checked{% endif %}>
<label class="form-check-label" for="reschedule_offered">
Rescheduling was offered to patient
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="add_to_waitlist" id="add_to_waitlist"
{% if form.add_to_waitlist.value %}checked{% endif %}>
<label class="form-check-label" for="add_to_waitlist">
Add patient to waitlist for future appointments
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="send_follow_up" id="send_follow_up"
{% if form.send_follow_up.value %}checked{% endif %}>
<label class="form-check-label" for="send_follow_up">
Send follow-up communication to patient
</label>
</div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-9 offset-md-3">
<h6>Policy Guidelines:</h6> <button type="submit" class="btn btn-warning" onclick="return confirm('Are you sure you want to mark this appointment as no show? This will apply the no-show fee and update the patient record.')">
<ul> <i class="fa fa-exclamation-triangle me-2"></i>Mark as No Show
<li>Wait minimum 15 minutes past appointment time</li> </button>
<li>Make at least 2 contact attempts</li> <a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<li>Document all contact attempts</li> <i class="fa fa-arrow-left me-2"></i>Go Back
<li>Apply standard no-show fee ($25.00)</li> </a>
<li>Offer rescheduling when possible</li>
</ul>
</div>
<div class="col-md-6">
<h6>Patient Impact:</h6>
<ul>
<li>No-show fee will be added to patient account</li>
<li>Appointment slot becomes available for other patients</li>
<li>Patient's no-show history is tracked</li>
<li>Multiple no-shows may affect future scheduling</li>
<li>Follow-up communication will be sent</li>
</ul>
</div> </div>
</div> </div>
</form>
</div>
</div>
<!-- No Show Policy -->
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">No Show Policy</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Policy Guidelines:</h6>
<ul>
<li>Wait minimum 15 minutes past appointment time</li>
<li>Make at least 2 contact attempts</li>
<li>Document all contact attempts</li>
<li>Apply standard no-show fee ($25.00)</li>
<li>Offer rescheduling when possible</li>
</ul>
</div>
<div class="col-md-6">
<h6>Patient Impact:</h6>
<ul>
<li>No-show fee will be added to patient account</li>
<li>Appointment slot becomes available for other patients</li>
<li>Patient's no-show history is tracked</li>
<li>Multiple no-shows may affect future scheduling</li>
<li>Follow-up communication will be sent</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </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="col-md-2 mb-3"> <div class="widget widget-stats bg-info mb-7px">
<div class="card stat-card bg-info text-white"> <div class="stats-icon stats-icon-lg"><i class="fas fa-calendar-day fa-fw"></i></div>
<div class="card-body"> <div class="stats-content">
<div class="d-flex justify-content-between align-items-center"> <div class="stats-title">Today's Appointments</div>
<div> <div class="stats-number">{{ stats.total_appointments_today }}</div>
<h4 class="card-title">{{ stats.todays_appointments }}</h4> <div class="stats-desc">Better than last week (40.5%)</div>
<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="col-md-2 mb-3"> <div class="widget widget-stats bg-warning mb-7px">
<div class="card stat-card bg-warning text-white"> <div class="stats-icon stats-icon-lg"><i class="fas fa-clock fa-fw"></i></div>
<div class="card-body"> <div class="stats-content">
<div class="d-flex justify-content-between align-items-center"> <div class="stats-title">Pending</div>
<div> <div class="stats-number">{{ stats.pending_appointments }}</div>
<h4 class="card-title">{{ stats.pending_appointments }}</h4> <div class="stats-desc">Better than last week (40.5%)</div>
<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="col-md-2 mb-3"> <div class="widget widget-stats bg-success mb-7px">
<div class="card stat-card bg-success text-white"> <div class="stats-icon stats-icon-lg"><i class="fas fa-check-circle fa-fw"></i></div>
<div class="card-body"> <div class="stats-content">
<div class="d-flex justify-content-between align-items-center"> <div class="stats-title">Completed Today</div>
<div> <div class="stats-number">{{ stats.completed_appointments }}</div>
<h4 class="card-title">{{ stats.completed_today }}</h4> <div class="stats-desc">Better than last week (40.5%)</div>
<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="col-md-2 mb-3"> <div class="widget widget-stats bg-danger mb-7px">
<div class="card stat-card bg-secondary text-white"> <div class="stats-icon stats-icon-lg"><i class="fas fa-users fa-fw"></i></div>
<div class="card-body"> <div class="stats-content">
<div class="d-flex justify-content-between align-items-center"> <div class="stats-title">Active Queue</div>
<div> <div class="stats-number">{{ stats.active_queues }}</div>
<h4 class="card-title">{{ stats.total_in_queue }}</h4> <div class="stats-desc">Better than last week (40.5%)</div>
<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="col-md-2 mb-3"> <div class="widget widget-stats bg-dark mb-7px">
<div class="card stat-card bg-dark text-white"> <div class="stats-icon stats-icon-lg"><i class="fas fa-video fa-fw"></i></div>
<div class="card-body"> <div class="stats-content">
<div class="d-flex justify-content-between align-items-center"> <div class="stats-title">Telemedicine</div>
<div> <div class="stats-number">{{ stats.telemedicine_sessions }}</div>
<h4 class="card-title">{{ stats.telemedicine_today }}</h4> <div class="stats-desc">Better than last week (40.5%)</div>
<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,39 +1,50 @@
<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> {% if log.appointment_offered %}
<small class="text-muted">{{ log.contact_date|date:"M d, Y H:i" }}</small> <div class="row mb-1">
<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" }}
</p>
</div>
</div>
{% endif %}
<div class="row mb-1">
<div class="col-md-6">
{% if log.next_contact_date %}
<small class="text-muted">
<span class="fw-bold">Contacted by:</span> {{ log.contacted_by.get_full_name }}
</small>
{% endif %}
</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>
<p class="mb-1">{{ log.notes|default:"No notes." }}</p>
{% if log.appointment_offered %}
<p class="mb-1 text-primary">
<i class="fas fa-calendar-check me-1"></i>Appointment Offered:
{{ log.offered_date|date:"M d, Y" }} at {{ log.offered_time|time:"g:i A" }}
</p>
<p class="mb-0 text-primary">
<i class="fas fa-reply me-1"></i>Patient Response:
<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 %}
{% if log.next_contact_date %}
<p class="mb-0 text-info">
<i class="fas fa-calendar-alt me-1"></i>Next Contact: {{ log.next_contact_date|date:"M d, Y" }}
</p>
{% endif %}
<small class="text-muted">Contacted by: {{ log.contacted_by.get_full_name|default:"N/A" }}</small>
</div>
{% empty %} {% empty %}
<div class="text-center text-muted py-3"> <div class="text-center text-muted py-3">
<i class="fas fa-comment-slash fa-2x mb-2"></i> <i class="fas fa-comment-slash fa-2x mb-2"></i>
<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> </h1>
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_list' %}">Waiting Queues</a></li> <p class="text-muted">View your appointments, manage queues, and track your progress.</p>
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_detail' queue.pk %}">{{ queue.name }}</a></li> </div>
<li class="breadcrumb-item active">Delete</li> <div class="btn-toolbar mb-2 mb-md-0">
</ol> <div class="btn-group me-2">
<h1 class="page-header mb-0">
<i class="fas fa-trash me-2"></i>Delete Waiting Queue
</h1>
</div>
<div class="ms-auto">
<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; font-weight: bold;
margin-bottom: 2rem; {#color: #0b505d;#}
} }
.stat-card { /* Confirm button */
background: white; .swal2-confirm {
border: 1px solid #dee2e6; background-color: #155724 !important;
border-radius: 0.5rem; color: #fff !important;
padding: 1.5rem; border-radius: 8px !important;
text-align: center;
transition: all 0.3s ease;
} }
.stat-card:hover { /* Cancel button */
box-shadow: 0 4px 8px rgba(0,0,0,0.1); .swal2-cancel {
transform: translateY(-2px); background-color: #adb5bd !important;
color: #fff !important;
} }
.stat-icon { /* Icon color override */
width: 60px; .swal2-icon.swal2-warning {
height: 60px; border-color: #f59c1a !important;
border-radius: 50%; color: #f59c1a !important;
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;
color: #495057;
margin-bottom: 0.5rem;
}
.stat-label {
color: #6c757d;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
}
.queue-info-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
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> </h1>
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_list' %}">Waiting Queues</a></li> <p class="text-muted">View your appointments, manage queues, and track your progress.</p>
<li class="breadcrumb-item active">{{ queue.name }}</li> </div>
</ol> <div class="btn-toolbar mb-2 mb-md-0">
<h1 class="page-header mb-0"> <div class="btn-group me-2">
<i class="fas fa-users me-2"></i>Queue Details <a href="{% url 'appointments:waiting_queue_update' queue.pk %}" class="btn btn-sm btn-warning">
</h1> <i class="fas fa-edit me-1"></i>Edit Queue
</div> </a>
<div class="ms-auto"> <button class="btn btn-sm btn-success" onclick="refreshQueue()">
<div class="btn-group"> <i class="fas fa-sync-alt me-1"></i>Refresh
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}" class="btn btn-warning"> </button>
<i class="fas fa-edit me-1"></i>Edit Queue <button class="btn btn-sm btn-danger" onclick="exportQueue()">
</a> <i class="fas fa-download me-2"></i>Export Data
<button class="btn btn-success" onclick="refreshQueue()"> </button>
<i class="fas fa-sync-alt me-1"></i>Refresh
</button>
<div class="btn-group">
<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
</a></li>
</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="col-md-8"> <div class="row">
<h2 class="mb-2">{{ queue.name }}</h2> <div class="col-md-8">
<div class="d-flex align-items-center mb-3"> <div class="align-items-center g-3">
<span class="queue-type-badge type-{{ queue.queue_type|lower }} me-3"> <h4 class="fw-bold me-2">{{ queue.name }}</h4>
{% 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> </div>
{% if queue.description %} <div class="col-md-4 text-md-end">
<p class="mb-0 opacity-75">{{ queue.description }}</p> <div class="align-items-center g-3">
{% endif %} <div class="h4 mb-1">{{ queue.current_queue_size }}/{{ queue.max_queue_size }}</div>
</div> <div class="opacity-75">Current Capacity</div>
<div class="col-md-4 text-md-end"> </div>
<div class="text-white">
<div class="h4 mb-1">{{ queue.current_queue_size }}/{{ queue.max_queue_size }}</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 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> </div>
<div class="info-item"> <div class="row mb-1">
<span class="info-label">Created:</span> <div class="col-4 fw-bold">Created:</div>
<span class="info-value">{{ queue.created_at|date:"M d, Y g:i A" }}</span> <div class="col-8">{{ queue.created_at|date:"M d, Y g:i A" }}</div>
</div> </div>
<div class="info-item"> <div class="row mb-1">
<span class="info-label">Last Updated:</span> <div class="col-4 fw-bold">Last Updated:</div>
<span class="info-value">{{ queue.updated_at|timesince }} ago</span> <div class="col-8">{{ queue.updated_at|timesince }} ago</div>
</div> </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,49 +338,70 @@ $(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?')) {#}
{# $.post('{% url "appointments:call_patient" %}', {#} {# $.post('{% url "appointments:call_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 justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div class="d-flex align-items-center mb-3"> <div>
<div> <h1 class="h2">
<ol class="breadcrumb"> <i class="fas fa-calendar-alt"></i> Queue<span class="fw-light">Management</span>
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li> </h1>
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li> <p class="text-muted">Manage queues and track patient's journey.</p>
<li class="breadcrumb-item active">Waiting Queues</li> </div>
</ol> <div class="btn-toolbar mb-2 mb-md-0">
<h1 class="page-header mb-0"> <div class="btn-group me-2">
<i class="fas fa-users me-2"></i>Waiting Queues Management
</h1>
</div>
<div class="ms-auto">
<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>
</div>
<!-- Statistics Overview --> <div class="container-fluid">
<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 class="ms-3">
<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 %}
@ -428,7 +196,7 @@
<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/sweetalert/dist/sweetalert.min.js' %}"></script> <script src="{% static 'plugins/sweetalert/dist/sweetalert.min.js' %}"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
@ -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,61 +4,61 @@
{% 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">
<h1 class="h2"> <div>
<i class="fas fa-users-gear"></i> Queue<span class="fw-light">Management</span> <h1 class="h2">
</h1> <i class="fas fa-users-gear"></i> Queue<span class="fw-light">Management</span>
</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>
<button type="button" class="btn btn-outline-primary">
<i class="fas fa-video"></i> Telemedicine
</button>
</div> </div>
<button type="button" class="btn btn-sm btn-primary">
<i class="fas fa-video"></i> Telemedicine
</button>
</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">
<div class="panel-heading"> {% for queue in queues %}
<h4 class="panel-title"> {% if queue %}
<i class="fas fa-users me-2"></i>Active Queues
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row">
{% for queue in queues %}
{% if queue %}
<div class="col-lg-6 col-xl-4 mb-4"> <div class="col-lg-6 col-xl-4 mb-4">
<div class="card"> <div class="panel panel-inverse" data-sortable-id="index-{{ queue.queue_id }}">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="panel-heading">
<h5 class="mb-0">{{ queue.name }}</h5> <h4 class="panel-title">
<span class="badge bg-primary">{{ queue.current_queue_size }}</span> {{ queue.name }}
<span class="badge bg-primary ms-2">{{ queue.current_queue_size }}</span>
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div> </div>
<div class="card-body" <div class="panel-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 }}
</small> </small>
<button class="btn btn-sm btn-primary" <button class="btn btn-sm btn-primary"
hx-post="{% url 'appointments:call_next_patient' queue.id %}" hx-post="{% url 'appointments:call_next_patient' queue.id %}"
hx-confirm="Call next patient?" hx-confirm="Call next patient?"
hx-target="closest .card-body" hx-target="closest .card-body"
@ -70,18 +70,16 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% empty %} {% empty %}
<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-3x text-muted mb-3"></i> <i class="fas fa-users fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Active Queues</h5> <h5 class="text-muted">No Active Queues</h5>
<p class="text-muted">No queues are currently active.</p> <p class="text-muted">No queues are currently active.</p>
</div>
</div> </div>
{% endfor %}
</div> </div>
</div> {% endfor %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,87 +4,139 @@
{% 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>
<p class="text-muted">{{ appointment.scheduled_datetime|date:"M d, Y H:i" }} • {{ appointment.get_status_display }}</p> </h1>
</div> <p class="text-muted">{{ appointment.scheduled_datetime|date:"M d, Y H:i" }} • {{ appointment.get_status_display }}</p>
<div class="col-auto"> </div>
<div class="btn-group"> <div class="btn-toolbar mb-2 mb-md-0">
{% if appointment.status == 'PENDING' %} <div class="btn-group me-2">
<button class="btn btn-success" {% if appointment.status in 'CONFIRMED, SCHEDULED' %}
hx-post="{% url 'appointments:check_in_patient' appointment.id %}" <button class="btn btn-success"
hx-confirm="Check in this patient?" hx-post="{% url 'appointments:check_in_patient' appointment.id %}"
hx-swap="none"> hx-confirm="Check in this patient?"
<i class="fas fa-check me-1"></i>Check In hx-swap="none">
</button> <i class="fas fa-check me-1"></i>Check In
{% endif %} </button>
<button class="btn btn-outline-primary"> {% endif %}
<i class="fas fa-edit me-1"></i>Edit <button class="btn btn-outline-primary">
</button> <i class="fas fa-edit me-1"></i>Edit
<button class="btn btn-outline-secondary"> </button>
<i class="fas fa-print me-1"></i>Print <button class="btn btn-outline-secondary">
</button> <i class="fas fa-print me-1"></i>Print
</div> </button>
</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">
</div> <i class="fas fa-calendar me-2"></i>Appointment Information
<div class="card-body"> </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">
<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">
</div> <i class="fas fa-stethoscope me-2"></i>Chief Complaint
<div class="card-body"> </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">
<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">
</div> <i class="fas fa-sticky-note me-2"></i>Notes
<div class="card-body"> </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">
<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>
</div> <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">
<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> </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">
</div> <i class="fas fa-user-md me-2"></i>Provider Information
<div class="card-body"> </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">
<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">
</div> <i class="fas fa-clock me-2"></i>Timeline
<div class="card-body"> </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="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>
@ -20,7 +36,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<!-- Search and Filters --> <!-- Search and Filters -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
@ -93,49 +109,46 @@
</form> </form>
</div> </div>
</div> </div>
<!-- Appointment List --> <div class="table-responsive">
<div class="card" id="appointment-list"> <table class="table table-hover mb-0">
<div class="card-body p-0"> <thead class="table-light">
<div class="table-responsive"> <tr>
<table class="table table-hover mb-0"> <th>Date & Time</th>
<thead class="table-light"> <th>Patient</th>
<tr> <th>Provider</th>
<th>Date & Time</th> <th>Type & Specialty</th>
<th>Patient</th> <th>Status</th>
<th>Provider</th> <th>Priority</th>
<th>Type & Specialty</th> <th>Actions</th>
<th>Status</th> </tr>
<th>Priority</th> </thead>
<th>Actions</th> <tbody>
</tr> {% for appointment in appointments %}
</thead> <tr>
<tbody> <td>
{% for appointment in appointments %} {% if appointment.scheduled_datetime %}
<tr> <div class="fw-bold">{{ appointment.scheduled_datetime|date:"M d, Y" }}</div>
<td> <div class="text-muted">{{ appointment.scheduled_datetime|time:"g:i A" }}</div>
{% if appointment.scheduled_datetime %} {% else %}
<div class="fw-bold">{{ appointment.scheduled_datetime|date:"M d, Y" }}</div> <div class="text-muted">Not scheduled</div>
<div class="text-muted">{{ appointment.scheduled_datetime|time:"g:i A" }}</div> {% endif %}
{% else %} {% if appointment.is_telemedicine %}
<div class="text-muted">Not scheduled</div> <span class="badge bg-info mt-1">
{% endif %} <i class="fas fa-video"></i> Telemedicine
{% if appointment.is_telemedicine %} </span>
<span class="badge bg-info mt-1"> {% endif %}
<i class="fas fa-video"></i> Telemedicine </td>
</span> <td>
{% endif %} <div class="fw-bold">{{ appointment.patient.get_full_name }}</div>
</td> <div class="text-muted small">MRN: {{ appointment.patient.mrn }}</div>
<td> {% if appointment.patient.date_of_birth %}
<div class="fw-bold">{{ appointment.patient.get_full_name }}</div> <div class="text-muted small">Age: {{ appointment.patient.age }}</div>
<div class="text-muted small">MRN: {{ appointment.patient.mrn }}</div> {% endif %}
{% if appointment.patient.date_of_birth %} </td>
<div class="text-muted small">Age: {{ appointment.patient.age }}</div> <td>
{% endif %} <div>{{ appointment.provider.get_full_name }}</div>
</td> <div class="text-muted small">{{ appointment.provider.get_role_display }}</div>
<td> </td>
<div>{{ appointment.provider.get_full_name }}</div>
<div class="text-muted small">{{ appointment.provider.get_role_display }}</div>
</td>
<td> <td>
<div>{{ appointment.get_appointment_type_display }}</div> <div>{{ appointment.get_appointment_type_display }}</div>
<div class="text-muted small">{{ appointment.get_specialty_display }}</div> <div class="text-muted small">{{ appointment.get_specialty_display }}</div>
@ -167,124 +180,121 @@
<br><span class="badge bg-danger mt-1">Overdue</span> <br><span class="badge bg-danger mt-1">Overdue</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if appointment.priority == 'ROUTINE' %} {% if appointment.priority == 'ROUTINE' %}
<span class="badge bg-light text-dark">{{ appointment.get_priority_display }}</span> <span class="badge bg-light text-dark">{{ appointment.get_priority_display }}</span>
{% elif appointment.priority == 'URGENT' %} {% elif appointment.priority == 'URGENT' %}
<span class="badge bg-warning">{{ appointment.get_priority_display }}</span> <span class="badge bg-warning">{{ appointment.get_priority_display }}</span>
{% elif appointment.priority == 'STAT' %} {% elif appointment.priority == 'STAT' %}
<span class="badge bg-danger">{{ appointment.get_priority_display }}</span> <span class="badge bg-danger">{{ appointment.get_priority_display }}</span>
{% elif appointment.priority == 'EMERGENCY' %} {% elif appointment.priority == 'EMERGENCY' %}
<span class="badge bg-danger">{{ appointment.get_priority_display }}</span> <span class="badge bg-danger">{{ appointment.get_priority_display }}</span>
{% endif %}
{% if appointment.urgency_score > 5 %}
<br><small class="text-danger">Score: {{ appointment.urgency_score }}/10</small>
{% else %}
<br><small class="text-muted">Score: {{ appointment.urgency_score }}/10</small>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<!-- Check In Button -->
{% if appointment.status == 'SCHEDULED' or appointment.status == 'CONFIRMED' %}
<button type="button"
class="btn btn-outline-success"
title="Check In Patient"
hx-post="{% url 'appointments:check_in_patient' appointment.id %}"
hx-target="#appointment-list"
hx-swap="innerHTML"
hx-confirm="Are you sure you want to check-in this patient?"
hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'>
<i class="fas fa-check"></i>
</button>
{% endif %}
<!-- Telemedicine Join Button -->
{% if appointment.is_telemedicine and appointment.status in 'CHECKED_IN,IN_PROGRESS' and appointment.meeting_url %}
<button type="button"
class="btn btn-outline-primary"
title="Join Telemedicine Session"
onclick="window.open('{{ appointment.meeting_url }}', '_blank')">
<i class="fas fa-video"></i>
</button>
{% endif %}
<!-- Complete Button -->
{% if appointment.status == 'IN_PROGRESS' %}
<button type="button"
class="btn btn-outline-success"
title="Complete Appointment"
hx-post="{% url 'appointments:complete_appointment' appointment.id %}"
hx-target="#appointment-list"
hx-swap="innerHTML"
hx-confirm="Are you sure you want to complete this appointment?"
hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'>
<i class="fas fa-check-circle"></i>
</button>
{% endif %} {% endif %}
<!-- Reschedule Button --> {% if appointment.urgency_score > 5 %}
{% if appointment.status in 'PENDING,SCHEDULED,CONFIRMED' %} <br><small class="text-danger">Score: {{ appointment.urgency_score }}/10</small>
<a class="btn btn-outline-warning" {% else %}
href="{% url 'appointments:reschedule_appointment' appointment.id %}"> <br><small class="text-muted">Score: {{ appointment.urgency_score }}/10</small>
<i class="fas fa-calendar-alt"></i>
</a>
{% endif %} {% endif %}
</td>
<!-- View Details Button --> <td>
<a href="{% url 'appointments:appointment_detail' appointment.pk %}"
class="btn btn-outline-info"
title="View Details">
<i class="fas fa-eye"></i>
</a>
<!-- More Actions Dropdown -->
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
<!-- Check In Button -->
{% if appointment.status == 'SCHEDULED' or appointment.status == 'CONFIRMED' %}
<button type="button"
class="btn btn-outline-success"
title="Check In Patient"
hx-post="{% url 'appointments:check_in_patient' appointment.id %}"
hx-target="#appointment-list"
hx-swap="innerHTML"
hx-confirm="Are you sure you want to check-in this patient?"
hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'>
<i class="fas fa-check"></i>
</button>
{% endif %}
<!-- Telemedicine Join Button -->
{% if appointment.is_telemedicine and appointment.status in 'CHECKED_IN,IN_PROGRESS' and appointment.meeting_url %}
<button type="button" <button type="button"
class="btn btn-outline-secondary dropdown-toggle" class="btn btn-outline-primary"
data-bs-toggle="dropdown" title="Join Telemedicine Session"
aria-expanded="false"> onclick="window.open('{{ appointment.meeting_url }}', '_blank')">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-video"></i>
</button> </button>
<ul class="dropdown-menu"> {% endif %}
<li><a class="dropdown-item" href="#"><i class="fas fa-edit"></i> Edit</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-copy"></i> Duplicate</a></li> <!-- Complete Button -->
<li><hr class="dropdown-divider"></li> {% if appointment.status == 'IN_PROGRESS' %}
<li><a class="dropdown-item text-danger" href="#"><i class="fas fa-times"></i> Cancel</a></li> <button type="button"
</ul> class="btn btn-outline-success"
title="Complete Appointment"
hx-post="{% url 'appointments:complete_appointment' appointment.id %}"
hx-target="#appointment-list"
hx-swap="innerHTML"
hx-confirm="Are you sure you want to complete this appointment?"
hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'>
<i class="fas fa-check-circle"></i>
</button>
{% endif %}
<!-- Reschedule Button -->
{% if appointment.status in 'PENDING,SCHEDULED,CONFIRMED' %}
<a class="btn btn-outline-warning"
href="{% url 'appointments:reschedule_appointment' appointment.id %}">
<i class="fas fa-calendar-alt"></i>
</a>
{% endif %}
<!-- View Details Button -->
<a href="{% url 'appointments:appointment_detail' appointment.pk %}"
class="btn btn-outline-info"
title="View Details">
<i class="fas fa-eye"></i>
</a>
<!-- More Actions Dropdown -->
<div class="btn-group btn-group-sm" role="group">
<button type="button"
class="btn btn-outline-secondary dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#"><i class="fas fa-edit"></i> Edit</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-copy"></i> Duplicate</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#"><i class="fas fa-times"></i> Cancel</a></li>
</ul>
</div>
</div> </div>
</div> </td>
</td> </tr>
</tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="7" class="text-center text-muted py-5"> <td colspan="7" class="text-center text-muted py-5">
<i class="fas fa-calendar fa-3x mb-3"></i> <i class="fas fa-calendar fa-3x mb-3"></i>
<h5>No appointments found</h5> <h5>No appointments found</h5>
<p>Try adjusting your search criteria or schedule a new appointment.</p> <p>Try adjusting your search criteria or schedule a new appointment.</p>
<button type="button" class="btn btn-primary"> <button type="button" class="btn btn-primary">
<i class="fas fa-calendar-plus"></i> Schedule New Appointment <i class="fas fa-calendar-plus"></i> Schedule New Appointment
</button> </button>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<!-- Pagination --> <!-- Pagination -->
{% if is_paginated %} {% if is_paginated %}
{% include 'partial/pagination.html' %} {% include 'partial/pagination.html' %}
{% endif %} {% endif %}
</div>
</div> </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,100 +4,61 @@
{% 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>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li> <li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li>
<li class="breadcrumb-item active">Reminders</li> <li class="breadcrumb-item active">Reminders</li>
</ul> </ul>
<div class="row align-items-center mb-3"> <div class="row align-items-center mb-3">
<div class="col"> <div class="col">
<h1 class="page-header">Appointment Reminders</h1> <h1 class="page-header">Appointment Reminders</h1>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#sendRemindersModal"> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#sendRemindersModal">
<i class="fa fa-paper-plane me-2"></i>Send Reminders <i class="fa fa-paper-plane me-2"></i>Send Reminders
</button> </button>
</div>
</div>
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6">
<div class="card border-0 text-truncate">
<div class="card-body">
<div class="row">
<div class="col-7">
<div class="text-dark fs-13px">Due Today</div>
<div class="text-dark fs-20px fw-600 lh-14">{{ reminders_due_today|length }}</div>
</div>
<div class="col-5">
<div class="mt-n1 mb-n1">
<div id="visitors-donut-chart" class="w-75 mx-auto"></div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="col-xl-3 col-md-6">
<!-- Summary Cards --> <div class="card border-0 text-truncate">
<div class="row mb-4"> <div class="card-body">
<div class="col-xl-3 col-md-6"> <div class="row">
<div class="card border-0 text-truncate"> <div class="col-7">
<div class="card-body"> <div class="text-dark fs-13px">Sent Today</div>
<div class="row"> <div class="text-dark fs-20px fw-600 lh-14">{{ reminders_sent_today|length }}</div>
<div class="col-7">
<div class="text-dark fs-13px">Due Today</div>
<div class="text-dark fs-20px fw-600 lh-14">{{ reminders_due_today|length }}</div>
</div>
<div class="col-5">
<div class="mt-n1 mb-n1">
<div id="visitors-donut-chart" class="w-75 mx-auto"></div>
</div>
</div>
</div> </div>
</div> <div class="col-5">
</div> <div class="mt-n1 mb-n1">
</div> <div class="w-75 mx-auto">
<div class="col-xl-3 col-md-6"> <i class="fa fa-check-circle fa-2x text-success"></i>
<div class="card border-0 text-truncate">
<div class="card-body">
<div class="row">
<div class="col-7">
<div class="text-dark fs-13px">Sent Today</div>
<div class="text-dark fs-20px fw-600 lh-14">{{ reminders_sent_today|length }}</div>
</div>
<div class="col-5">
<div class="mt-n1 mb-n1">
<div class="w-75 mx-auto">
<i class="fa fa-check-circle fa-2x text-success"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card border-0 text-truncate">
<div class="card-body">
<div class="row">
<div class="col-7">
<div class="text-dark fs-13px">Failed</div>
<div class="text-dark fs-20px fw-600 lh-14">{{ reminders_failed|length }}</div>
</div>
<div class="col-5">
<div class="mt-n1 mb-n1">
<div class="w-75 mx-auto">
<i class="fa fa-exclamation-triangle fa-2x text-warning"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card border-0 text-truncate">
<div class="card-body">
<div class="row">
<div class="col-7">
<div class="text-dark fs-13px">Response Rate</div>
<div class="text-dark fs-20px fw-600 lh-14">{{ response_rate|floatformat:1 }}%</div>
</div>
<div class="col-5">
<div class="mt-n1 mb-n1">
<div class="w-75 mx-auto">
<i class="fa fa-chart-line fa-2x text-info"></i>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -105,180 +66,219 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-xl-3 col-md-6">
<!-- Filters --> <div class="card border-0 text-truncate">
<div class="card mb-4"> <div class="card-body">
<div class="card-body"> <div class="row">
<form method="get" class="row g-3"> <div class="col-7">
<div class="col-md-3"> <div class="text-dark fs-13px">Failed</div>
<label class="form-label">Date Range</label> <div class="text-dark fs-20px fw-600 lh-14">{{ reminders_failed|length }}</div>
<select name="date_range" class="form-select"> </div>
<option value="today" {% if request.GET.date_range == 'today' %}selected{% endif %}>Today</option> <div class="col-5">
<option value="tomorrow" {% if request.GET.date_range == 'tomorrow' %}selected{% endif %}>Tomorrow</option> <div class="mt-n1 mb-n1">
<option value="week" {% if request.GET.date_range == 'week' %}selected{% endif %}>This Week</option> <div class="w-75 mx-auto">
<option value="month" {% if request.GET.date_range == 'month' %}selected{% endif %}>This Month</option> <i class="fa fa-exclamation-triangle fa-2x text-warning"></i>
<option value="all" {% if request.GET.date_range == 'all' %}selected{% endif %}>All</option> </div>
</select> </div>
</div>
<div class="col-md-3">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="">All Statuses</option>
<option value="pending" {% if request.GET.status == 'pending' %}selected{% endif %}>Pending</option>
<option value="sent" {% if request.GET.status == 'sent' %}selected{% endif %}>Sent</option>
<option value="delivered" {% if request.GET.status == 'delivered' %}selected{% endif %}>Delivered</option>
<option value="failed" {% if request.GET.status == 'failed' %}selected{% endif %}>Failed</option>
<option value="responded" {% if request.GET.status == 'responded' %}selected{% endif %}>Responded</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Method</label>
<select name="method" class="form-select">
<option value="">All Methods</option>
<option value="email" {% if request.GET.method == 'email' %}selected{% endif %}>Email</option>
<option value="sms" {% if request.GET.method == 'sms' %}selected{% endif %}>SMS</option>
<option value="phone" {% if request.GET.method == 'phone' %}selected{% endif %}>Phone</option>
<option value="push" {% if request.GET.method == 'push' %}selected{% endif %}>Push Notification</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">&nbsp;</label>
<div>
<button type="submit" class="btn btn-primary">
<i class="fa fa-search me-2"></i>Filter
</button>
<a href="{% url 'appointments:appointment_reminders' %}" class="btn btn-secondary ms-2">Reset</a>
</div> </div>
</div> </div>
</form> </div>
</div> </div>
</div> </div>
<div class="col-xl-3 col-md-6">
<!-- Reminders Table --> <div class="card border-0 text-truncate">
<div class="card"> <div class="card-body">
<div class="card-header"> <div class="row">
<h4 class="card-title">Reminder History</h4> <div class="col-7">
</div> <div class="text-dark fs-13px">Response Rate</div>
<div class="card-body"> <div class="text-dark fs-20px fw-600 lh-14">{{ response_rate|floatformat:1 }}%</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div> </div>
{% endfor %} <div class="col-5">
{% endif %} <div class="mt-n1 mb-n1">
<div class="w-75 mx-auto">
<div class="table-responsive"> <i class="fa fa-chart-line fa-2x text-info"></i>
<table id="remindersTable" class="table table-striped table-bordered align-middle"> </div>
<thead> </div>
<tr> </div>
<th width="5%"> </div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAll">
</div>
</th>
<th>Patient</th>
<th>Appointment</th>
<th>Method</th>
<th>Status</th>
<th>Sent</th>
<th>Response</th>
<th width="10%">Actions</th>
</tr>
</thead>
<tbody>
{% for reminder in reminders %}
<tr>
<td>
<div class="form-check">
<input class="form-check-input reminder-checkbox" type="checkbox" value="{{ reminder.id }}">
</div>
</td>
<td>
<div class="d-flex align-items-center">
<div>
<div class="fw-bold">{{ reminder.appointment.patient.first_name }} {{ reminder.appointment.patient.last_name }}</div>
<div class="text-muted small">{{ reminder.appointment.patient.patient_id }}</div>
</div>
</div>
</td>
<td>
<div>
<div class="fw-bold">{{ reminder.appointment.appointment_date|date:"M d, Y" }}</div>
<div class="text-muted small">{{ reminder.appointment.appointment_time|time:"g:i A" }} - {{ reminder.appointment.provider.first_name }} {{ reminder.appointment.provider.last_name }}</div>
</div>
</td>
<td>
{% if reminder.method == 'email' %}
<span class="badge bg-primary"><i class="fa fa-envelope me-1"></i>Email</span>
{% elif reminder.method == 'sms' %}
<span class="badge bg-success"><i class="fa fa-sms me-1"></i>SMS</span>
{% elif reminder.method == 'phone' %}
<span class="badge bg-info"><i class="fa fa-phone me-1"></i>Phone</span>
{% elif reminder.method == 'push' %}
<span class="badge bg-warning"><i class="fa fa-bell me-1"></i>Push</span>
{% endif %}
</td>
<td>
{% if reminder.status == 'pending' %}
<span class="badge bg-secondary">Pending</span>
{% elif reminder.status == 'sent' %}
<span class="badge bg-primary">Sent</span>
{% elif reminder.status == 'delivered' %}
<span class="badge bg-success">Delivered</span>
{% elif reminder.status == 'failed' %}
<span class="badge bg-danger">Failed</span>
{% elif reminder.status == 'responded' %}
<span class="badge bg-info">Responded</span>
{% endif %}
</td>
<td>
{% if reminder.sent_at %}
<div>{{ reminder.sent_at|date:"M d, Y" }}</div>
<div class="text-muted small">{{ reminder.sent_at|time:"g:i A" }}</div>
{% else %}
<span class="text-muted">Not sent</span>
{% endif %}
</td>
<td>
{% if reminder.response_received_at %}
<div class="text-success">
<i class="fa fa-check me-1"></i>{{ reminder.response_received_at|date:"M d, Y" }}
</div>
{% else %}
<span class="text-muted">No response</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
{% if reminder.status == 'pending' or reminder.status == 'failed' %}
<button type="button" class="btn btn-outline-primary" onclick="resendReminder('{{ reminder.id }}')">
<i class="fa fa-redo"></i>
</button>
{% endif %}
<button type="button" class="btn btn-outline-info" onclick="viewReminderDetails('{{ reminder.id }}')">
<i class="fa fa-eye"></i>
</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center text-muted py-4">
<i class="fa fa-inbox fa-3x mb-3"></i>
<div>No reminders found</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-3">
<label class="form-label">Date Range</label>
<select name="date_range" class="form-select">
<option value="today" {% if request.GET.date_range == 'today' %}selected{% endif %}>Today</option>
<option value="tomorrow" {% if request.GET.date_range == 'tomorrow' %}selected{% endif %}>Tomorrow</option>
<option value="week" {% if request.GET.date_range == 'week' %}selected{% endif %}>This Week</option>
<option value="month" {% if request.GET.date_range == 'month' %}selected{% endif %}>This Month</option>
<option value="all" {% if request.GET.date_range == 'all' %}selected{% endif %}>All</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="">All Statuses</option>
<option value="pending" {% if request.GET.status == 'pending' %}selected{% endif %}>Pending</option>
<option value="sent" {% if request.GET.status == 'sent' %}selected{% endif %}>Sent</option>
<option value="delivered" {% if request.GET.status == 'delivered' %}selected{% endif %}>Delivered</option>
<option value="failed" {% if request.GET.status == 'failed' %}selected{% endif %}>Failed</option>
<option value="responded" {% if request.GET.status == 'responded' %}selected{% endif %}>Responded</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Method</label>
<select name="method" class="form-select">
<option value="">All Methods</option>
<option value="email" {% if request.GET.method == 'email' %}selected{% endif %}>Email</option>
<option value="sms" {% if request.GET.method == 'sms' %}selected{% endif %}>SMS</option>
<option value="phone" {% if request.GET.method == 'phone' %}selected{% endif %}>Phone</option>
<option value="push" {% if request.GET.method == 'push' %}selected{% endif %}>Push Notification</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">&nbsp;</label>
<div>
<button type="submit" class="btn btn-primary">
<i class="fa fa-search me-2"></i>Filter
</button>
<a href="{% url 'appointments:appointment_reminders' %}" class="btn btn-secondary ms-2">Reset</a>
</div>
</div>
</form>
</div>
</div>
<!-- Reminders Table -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Reminder History</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="table-responsive">
<table id="remindersTable" class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th width="5%">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAll">
</div>
</th>
<th>Patient</th>
<th>Appointment</th>
<th>Method</th>
<th>Status</th>
<th>Sent</th>
<th>Response</th>
<th width="10%">Actions</th>
</tr>
</thead>
<tbody>
{% for reminder in reminders %}
<tr>
<td>
<div class="form-check">
<input class="form-check-input reminder-checkbox" type="checkbox" value="{{ reminder.id }}">
</div>
</td>
<td>
<div class="d-flex align-items-center">
<div>
<div class="fw-bold">{{ reminder.appointment.patient.first_name }} {{ reminder.appointment.patient.last_name }}</div>
<div class="text-muted small">{{ reminder.appointment.patient.patient_id }}</div>
</div>
</div>
</td>
<td>
<div>
<div class="fw-bold">{{ reminder.appointment.appointment_date|date:"M d, Y" }}</div>
<div class="text-muted small">{{ reminder.appointment.appointment_time|time:"g:i A" }} - {{ reminder.appointment.provider.first_name }} {{ reminder.appointment.provider.last_name }}</div>
</div>
</td>
<td>
{% if reminder.method == 'email' %}
<span class="badge bg-primary"><i class="fa fa-envelope me-1"></i>Email</span>
{% elif reminder.method == 'sms' %}
<span class="badge bg-success"><i class="fa fa-sms me-1"></i>SMS</span>
{% elif reminder.method == 'phone' %}
<span class="badge bg-info"><i class="fa fa-phone me-1"></i>Phone</span>
{% elif reminder.method == 'push' %}
<span class="badge bg-warning"><i class="fa fa-bell me-1"></i>Push</span>
{% endif %}
</td>
<td>
{% if reminder.status == 'pending' %}
<span class="badge bg-secondary">Pending</span>
{% elif reminder.status == 'sent' %}
<span class="badge bg-primary">Sent</span>
{% elif reminder.status == 'delivered' %}
<span class="badge bg-success">Delivered</span>
{% elif reminder.status == 'failed' %}
<span class="badge bg-danger">Failed</span>
{% elif reminder.status == 'responded' %}
<span class="badge bg-info">Responded</span>
{% endif %}
</td>
<td>
{% if reminder.sent_at %}
<div>{{ reminder.sent_at|date:"M d, Y" }}</div>
<div class="text-muted small">{{ reminder.sent_at|time:"g:i A" }}</div>
{% else %}
<span class="text-muted">Not sent</span>
{% endif %}
</td>
<td>
{% if reminder.response_received_at %}
<div class="text-success">
<i class="fa fa-check me-1"></i>{{ reminder.response_received_at|date:"M d, Y" }}
</div>
{% else %}
<span class="text-muted">No response</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
{% if reminder.status == 'pending' or reminder.status == 'failed' %}
<button type="button" class="btn btn-outline-primary" onclick="resendReminder('{{ reminder.id }}')">
<i class="fa fa-redo"></i>
</button>
{% endif %}
<button type="button" class="btn btn-outline-info" onclick="viewReminderDetails('{{ reminder.id }}')">
<i class="fa fa-eye"></i>
</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center text-muted py-4">
<i class="fa fa-inbox fa-3x mb-3"></i>
<div>No reminders found</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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">
<div class="modal-dialog"> <div class="modal-dialog">
@ -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">
<h1 class="h2"> <div>
<i class="fas fa-calendar-alt"></i> Reschedule<span class="fw-light">Appointments</span> <h1 class="h2">
</h1> <i class="fas fa-calendar-alt"></i> Reschedule<span class="fw-light">Appointments</span>
</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,10 +21,10 @@
<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>
<button type="button" class="btn btn-sm btn-primary">
<i class="fas fa-video"></i> Telemedicine
</button>
</div> </div>
<button type="button" class="btn btn-sm btn-primary">
<i class="fas fa-video"></i> Telemedicine
</button>
</div> </div>
</div> </div>
<div class="container-fluid"> <div class="container-fluid">
@ -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"
@ -186,9 +191,9 @@
</div> </div>
</div> </div>
</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,411 +391,411 @@
{% 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">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-lg-8"> <div class="col-lg-8">
<h1 class="mb-2"> <h1 class="mb-2">
<i class="fas fa-clock me-3"></i> <i class="fas fa-clock me-3"></i>
Appointment Slot Details Appointment Slot Details
</h1> </h1>
<p class="mb-3 opacity-90"> <p class="mb-3 opacity-90">
Detailed information and management for appointment slot Detailed information and management for appointment slot
</p> </p>
<div class="d-flex flex-wrap gap-3"> <div class="d-flex flex-wrap gap-3">
<div class="availability-indicator"> <div class="availability-indicator">
<div class="availability-dot {{ slot.status|lower }}"></div> <div class="availability-dot {{ slot.status|lower }}"></div>
<span class="status-badge {{ slot.status|lower }}"> <span class="status-badge {{ slot.status|lower }}">
<i class="fas fa-circle"></i> <i class="fas fa-circle"></i>
{{ slot.get_status_display }} {{ slot.get_status_display }}
</span> </span>
</div>
<div class="d-flex align-items-center gap-2">
<i class="fas fa-calendar"></i>
<span>{{ slot.date|date:"F d, Y" }}</span>
</div>
<div class="d-flex align-items-center gap-2">
<i class="fas fa-clock"></i>
<span>{{ slot.start_time|time:"g:i A" }} - {{ slot.end_time|time:"g:i A" }}</span>
</div>
</div> </div>
</div> <div class="d-flex align-items-center gap-2">
<div class="col-lg-4 text-lg-end"> <i class="fas fa-calendar"></i>
<div class="d-flex flex-column gap-2"> <span>{{ slot.date|date:"F d, Y" }}</span>
{% if slot.status == 'AVAILABLE' %} </div>
<button class="btn btn-light btn-lg" onclick="bookSlot()"> <div class="d-flex align-items-center gap-2">
<i class="fas fa-calendar-plus me-2"></i>Book Appointment <i class="fas fa-clock"></i>
</button> <span>{{ slot.start_time|time:"g:i A" }} - {{ slot.end_time|time:"g:i A" }}</span>
{% elif slot.status == 'BOOKED' %}
<button class="btn btn-outline-light" onclick="viewAppointment()">
<i class="fas fa-eye me-2"></i>View Appointment
</button>
{% endif %}
<a href="{% url 'appointments:slot_update' slot.pk %}" class="btn btn-outline-light">
<i class="fas fa-edit me-2"></i>Edit Slot
</a>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="col-lg-4 text-lg-end">
<div class="d-flex flex-column gap-2">
<!-- Main Content Grid --> {% if slot.status == 'AVAILABLE' %}
<div class="slot-info-grid fade-in"> <button class="btn btn-light btn-lg" onclick="bookSlot()">
<!-- Left Column - Main Information --> <i class="fas fa-calendar-plus me-2"></i>Book Appointment
<div> </button>
<!-- Slot Information --> {% elif slot.status == 'BOOKED' %}
<div class="info-card"> <button class="btn btn-outline-light" onclick="viewAppointment()">
<div class="info-card-header"> <i class="fas fa-eye me-2"></i>View Appointment
<h4 class="info-card-title"> </button>
<i class="fas fa-info-circle text-primary me-2"></i>
Slot Information
</h4>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'appointments:slot_update' slot.pk %}">
<i class="fas fa-edit me-2"></i>Edit Slot
</a></li>
<li><a class="dropdown-item" href="#" onclick="duplicateSlot()">
<i class="fas fa-copy me-2"></i>Duplicate Slot
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="{% url 'appointments:slot_delete' slot.pk %}">
<i class="fas fa-trash me-2"></i>Delete Slot
</a></li>
</ul>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Date</div>
<div class="info-value">{{ slot.date|date:"F d, Y" }}</div>
</div>
<div class="info-item">
<div class="info-label">Time</div>
<div class="slot-time-display">
{{ slot.start_time|time:"g:i A" }} - {{ slot.end_time|time:"g:i A" }}
</div>
</div>
<div class="info-item">
<div class="info-label">Duration</div>
<div class="info-value">
<span class="duration-badge">{{ slot.duration_minutes }} minutes</span>
</div>
</div>
<div class="info-item">
<div class="info-label">Appointment Type</div>
<div class="info-value">
<span class="badge bg-light text-dark">{{ slot.appointment_type|default:"General" }}</span>
</div>
</div>
<div class="info-item">
<div class="info-label">Created</div>
<div class="info-value">{{ slot.created_at|date:"M d, Y g:i A" }}</div>
</div>
<div class="info-item">
<div class="info-label">Last Modified</div>
<div class="info-value">{{ slot.updated_at|date:"M d, Y g:i A" }}</div>
</div>
</div>
</div>
<!-- Provider Information -->
<div class="info-card">
<div class="info-card-header">
<h4 class="info-card-title">
<i class="fas fa-user-md text-success me-2"></i>
Provider Information
</h4>
<a href="{% url 'hr:employee_detail' slot.provider.pk %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-external-link-alt me-1"></i>View Profile
</a>
</div>
<div class="provider-profile">
{% if slot.provider.profile_picture %}
<img src="{{ slot.provider.profile_picture.url }}"
class="provider-avatar" alt="{{ slot.provider.get_full_name }}">
{% else %}
<div class="provider-avatar">
{{ slot.provider.first_name.0 }}{{ slot.provider.last_name.0 }}
</div>
{% endif %}
<div class="provider-details">
<div class="provider-name">{{ slot.provider.get_full_name }}</div>
<div class="provider-specialty">{{ slot.provider.specialty|default:"General Practice" }}</div>
<div class="provider-contact">{{ slot.provider.email }}</div>
{% if slot.provider.phone %}
<div class="provider-contact">{{ slot.provider.phone }}</div>
{% endif %}
</div>
</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Department</div>
<div class="info-value">{{ slot.provider.department|default:"Not assigned" }}</div>
</div>
<div class="info-item">
<div class="info-label">License Number</div>
<div class="info-value">{{ slot.provider.license_number|default:"N/A" }}</div>
</div>
<div class="info-item">
<div class="info-label">Years of Experience</div>
<div class="info-value">{{ slot.provider.years_experience|default:"N/A" }}</div>
</div>
<div class="info-item">
<div class="info-label">Languages</div>
<div class="info-value">{{ slot.provider.languages|default:"English" }}</div>
</div>
</div>
</div>
<!-- Appointment Details (if booked) -->
{% if slot.appointment %}
<div class="appointment-details">
<h5 class="mb-3">
<i class="fas fa-calendar-check text-primary me-2"></i>
Appointment Details
</h5>
<div class="appointment-patient">
{% if slot.appointment.patient.profile_picture %}
<img src="{{ slot.appointment.patient.profile_picture.url }}"
class="patient-avatar" alt="{{ slot.appointment.patient.get_full_name }}">
{% else %}
<div class="patient-avatar">
{{ slot.appointment.patient.first_name.0 }}{{ slot.appointment.patient.last_name.0 }}
</div>
{% endif %}
<div class="patient-info">
<div class="patient-name">{{ slot.appointment.patient.get_full_name }}</div>
<div class="patient-mrn">MRN: {{ slot.appointment.patient.mrn }}</div>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Appointment Type</div>
<div class="info-value">{{ slot.appointment.appointment_type|default:"Consultation" }}</div>
</div>
<div class="info-item">
<div class="info-label">Status</div>
<div class="info-value">
<span class="badge bg-primary">{{ slot.appointment.get_status_display }}</span>
</div>
</div>
<div class="info-item">
<div class="info-label">Booked On</div>
<div class="info-value">{{ slot.appointment.created_at|date:"M d, Y g:i A" }}</div>
</div>
<div class="info-item">
<div class="info-label">Booked By</div>
<div class="info-value">{{ slot.appointment.created_by.get_full_name|default:"System" }}</div>
</div>
</div>
{% if slot.appointment.notes %}
<div class="mt-3">
<div class="info-label">Appointment Notes</div>
<div class="info-value">{{ slot.appointment.notes }}</div>
</div>
{% endif %} {% endif %}
</div> <a href="{% url 'appointments:slot_update' slot.pk %}" class="btn btn-outline-light">
{% endif %} <i class="fas fa-edit me-2"></i>Edit Slot
</a>
<!-- Recurring Information -->
{% if slot.is_recurring %}
<div class="recurring-info">
<div class="recurring-pattern">
<i class="fas fa-repeat me-2"></i>
Recurring Appointment Slot
</div>
<div class="recurring-details">
<strong>Pattern:</strong> {{ slot.recurrence_pattern|default:"Weekly" }}<br>
<strong>Frequency:</strong> Every {{ slot.recurrence_interval|default:1 }} {{ slot.recurrence_unit|default:"week" }}(s)<br>
{% if slot.recurrence_end_date %}
<strong>Ends:</strong> {{ slot.recurrence_end_date|date:"F d, Y" }}
{% else %}
<strong>Ends:</strong> No end date
{% endif %}
</div>
</div>
{% endif %}
<!-- Notes Section -->
{% if slot.notes %}
<div class="notes-section">
<div class="notes-title">
<i class="fas fa-sticky-note me-2"></i>
Slot Notes
</div>
<div class="notes-content">{{ slot.notes }}</div>
</div>
{% endif %}
<!-- Conflict Warnings -->
{% if slot.conflicts %}
<div class="conflict-warning">
<div class="conflict-title">
<i class="fas fa-exclamation-triangle me-2"></i>
Schedule Conflicts Detected
</div>
<ul class="conflict-list">
{% for conflict in slot.conflicts %}
<li>{{ conflict.description }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<!-- Right Column - Timeline and Actions -->
<div>
<!-- Activity Timeline -->
<div class="timeline-section">
<div class="info-card-header">
<h4 class="info-card-title">
<i class="fas fa-history text-info me-2"></i>
Activity Timeline
</h4>
</div>
<div class="timeline">
<div class="timeline-item" data-icon="">
<div class="timeline-time">{{ slot.created_at|date:"M d, Y g:i A" }}</div>
<div class="timeline-content">
<strong>Slot Created</strong><br>
Created by {{ slot.created_by.get_full_name|default:"System" }}
</div>
</div>
{% if slot.appointment %}
<div class="timeline-item" data-icon="">
<div class="timeline-time">{{ slot.appointment.created_at|date:"M d, Y g:i A" }}</div>
<div class="timeline-content">
<strong>Appointment Booked</strong><br>
Booked for {{ slot.appointment.patient.get_full_name }}
</div>
</div>
{% endif %}
{% if slot.updated_at != slot.created_at %}
<div class="timeline-item" data-icon="">
<div class="timeline-time">{{ slot.updated_at|date:"M d, Y g:i A" }}</div>
<div class="timeline-content">
<strong>Slot Modified</strong><br>
Last updated by {{ slot.updated_by.get_full_name|default:"System" }}
</div>
</div>
{% endif %}
{% for activity in slot.activity_log %}
<div class="timeline-item" data-icon="{{ activity.icon }}">
<div class="timeline-time">{{ activity.timestamp|date:"M d, Y g:i A" }}</div>
<div class="timeline-content">
<strong>{{ activity.title }}</strong><br>
{{ activity.description }}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Quick Stats -->
<div class="info-card">
<div class="info-card-header">
<h4 class="info-card-title">
<i class="fas fa-chart-bar text-warning me-2"></i>
Quick Stats
</h4>
</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Provider's Slots Today</div>
<div class="info-value">{{ provider_slots_today|default:0 }}</div>
</div>
<div class="info-item">
<div class="info-label">Utilization Rate</div>
<div class="info-value">{{ utilization_rate|default:0 }}%</div>
</div>
<div class="info-item">
<div class="info-label">Average Duration</div>
<div class="info-value">{{ avg_duration|default:30 }} min</div>
</div>
<div class="info-item">
<div class="info-label">No-Show Rate</div>
<div class="info-value">{{ no_show_rate|default:0 }}%</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Action Buttons -->
<div class="action-buttons fade-in"> <!-- Main Content Grid -->
{% if slot.status == 'AVAILABLE' %} <div class="slot-info-grid fade-in">
<button class="btn btn-primary btn-action" onclick="bookSlot()"> <!-- Left Column - Main Information -->
<i class="fas fa-calendar-plus"></i>Book Appointment <div>
</button> <!-- Slot Information -->
<button class="btn btn-warning btn-action" onclick="blockSlot()"> <div class="info-card">
<i class="fas fa-ban"></i>Block Slot <div class="info-card-header">
</button> <h4 class="info-card-title">
{% elif slot.status == 'BOOKED' %} <i class="fas fa-info-circle text-primary me-2"></i>
<a href="{% url 'appointments:appointment_detail' slot.appointment.pk %}" class="btn btn-primary btn-action"> Slot Information
<i class="fas fa-eye"></i>View Appointment </h4>
</a> <div class="dropdown">
<button class="btn btn-warning btn-action" onclick="rescheduleAppointment()"> <button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
<i class="fas fa-calendar-alt"></i>Reschedule <i class="fas fa-ellipsis-v"></i>
</button> </button>
<button class="btn btn-danger btn-action" onclick="cancelAppointment()"> <ul class="dropdown-menu">
<i class="fas fa-times"></i>Cancel Appointment <li><a class="dropdown-item" href="{% url 'appointments:slot_update' slot.pk %}">
</button> <i class="fas fa-edit me-2"></i>Edit Slot
{% elif slot.status == 'BLOCKED' %} </a></li>
<button class="btn btn-success btn-action" onclick="unblockSlot()"> <li><a class="dropdown-item" href="#" onclick="duplicateSlot()">
<i class="fas fa-check"></i>Unblock Slot <i class="fas fa-copy me-2"></i>Duplicate Slot
</button> </a></li>
{% endif %} <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="{% url 'appointments:slot_delete' slot.pk %}">
<a href="{% url 'appointments:slot_update' slot.pk %}" class="btn btn-secondary btn-action"> <i class="fas fa-trash me-2"></i>Delete Slot
<i class="fas fa-edit"></i>Edit Slot </a></li>
</a> </ul>
</div>
<button class="btn btn-info btn-action" onclick="duplicateSlot()"> </div>
<i class="fas fa-copy"></i>Duplicate Slot
</button> <div class="info-grid">
<div class="info-item">
<button class="btn btn-outline-secondary btn-action" onclick="printSlot()"> <div class="info-label">Date</div>
<i class="fas fa-print"></i>Print Details <div class="info-value">{{ slot.date|date:"F d, Y" }}</div>
</button> </div>
<a href="{% url 'appointments:slot_list' %}" class="btn btn-outline-secondary btn-action"> <div class="info-item">
<i class="fas fa-arrow-left"></i>Back to Slots <div class="info-label">Time</div>
</a> <div class="slot-time-display">
</div> {{ slot.start_time|time:"g:i A" }} - {{ slot.end_time|time:"g:i A" }}
</div>
</div>
<div class="info-item">
<div class="info-label">Duration</div>
<div class="info-value">
<span class="duration-badge">{{ slot.duration_minutes }} minutes</span>
</div>
</div>
<div class="info-item">
<div class="info-label">Appointment Type</div>
<div class="info-value">
<span class="badge bg-light text-dark">{{ slot.appointment_type|default:"General" }}</span>
</div>
</div>
<div class="info-item">
<div class="info-label">Created</div>
<div class="info-value">{{ slot.created_at|date:"M d, Y g:i A" }}</div>
</div>
<div class="info-item">
<div class="info-label">Last Modified</div>
<div class="info-value">{{ slot.updated_at|date:"M d, Y g:i A" }}</div>
</div>
</div>
</div>
<!-- Provider Information -->
<div class="info-card">
<div class="info-card-header">
<h4 class="info-card-title">
<i class="fas fa-user-md text-success me-2"></i>
Provider Information
</h4>
<a href="{% url 'hr:employee_detail' slot.provider.pk %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-external-link-alt me-1"></i>View Profile
</a>
</div>
<div class="provider-profile">
{% if slot.provider.profile_picture %}
<img src="{{ slot.provider.profile_picture.url }}"
class="provider-avatar" alt="{{ slot.provider.get_full_name }}">
{% else %}
<div class="provider-avatar">
{{ slot.provider.first_name.0 }}{{ slot.provider.last_name.0 }}
</div>
{% endif %}
<div class="provider-details">
<div class="provider-name">{{ slot.provider.get_full_name }}</div>
<div class="provider-specialty">{{ slot.provider.specialty|default:"General Practice" }}</div>
<div class="provider-contact">{{ slot.provider.email }}</div>
{% if slot.provider.phone %}
<div class="provider-contact">{{ slot.provider.phone }}</div>
{% endif %}
</div>
</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Department</div>
<div class="info-value">{{ slot.provider.department|default:"Not assigned" }}</div>
</div>
<div class="info-item">
<div class="info-label">License Number</div>
<div class="info-value">{{ slot.provider.license_number|default:"N/A" }}</div>
</div>
<div class="info-item">
<div class="info-label">Years of Experience</div>
<div class="info-value">{{ slot.provider.years_experience|default:"N/A" }}</div>
</div>
<div class="info-item">
<div class="info-label">Languages</div>
<div class="info-value">{{ slot.provider.languages|default:"English" }}</div>
</div>
</div>
</div>
<!-- Appointment Details (if booked) -->
{% if slot.appointment %}
<div class="appointment-details">
<h5 class="mb-3">
<i class="fas fa-calendar-check text-primary me-2"></i>
Appointment Details
</h5>
<div class="appointment-patient">
{% if slot.appointment.patient.profile_picture %}
<img src="{{ slot.appointment.patient.profile_picture.url }}"
class="patient-avatar" alt="{{ slot.appointment.patient.get_full_name }}">
{% else %}
<div class="patient-avatar">
{{ slot.appointment.patient.first_name.0 }}{{ slot.appointment.patient.last_name.0 }}
</div>
{% endif %}
<div class="patient-info">
<div class="patient-name">{{ slot.appointment.patient.get_full_name }}</div>
<div class="patient-mrn">MRN: {{ slot.appointment.patient.mrn }}</div>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Appointment Type</div>
<div class="info-value">{{ slot.appointment.appointment_type|default:"Consultation" }}</div>
</div>
<div class="info-item">
<div class="info-label">Status</div>
<div class="info-value">
<span class="badge bg-primary">{{ slot.appointment.get_status_display }}</span>
</div>
</div>
<div class="info-item">
<div class="info-label">Booked On</div>
<div class="info-value">{{ slot.appointment.created_at|date:"M d, Y g:i A" }}</div>
</div>
<div class="info-item">
<div class="info-label">Booked By</div>
<div class="info-value">{{ slot.appointment.created_by.get_full_name|default:"System" }}</div>
</div>
</div>
{% if slot.appointment.notes %}
<div class="mt-3">
<div class="info-label">Appointment Notes</div>
<div class="info-value">{{ slot.appointment.notes }}</div>
</div>
{% endif %}
</div>
{% endif %}
<!-- Recurring Information -->
{% if slot.is_recurring %}
<div class="recurring-info">
<div class="recurring-pattern">
<i class="fas fa-repeat me-2"></i>
Recurring Appointment Slot
</div>
<div class="recurring-details">
<strong>Pattern:</strong> {{ slot.recurrence_pattern|default:"Weekly" }}<br>
<strong>Frequency:</strong> Every {{ slot.recurrence_interval|default:1 }} {{ slot.recurrence_unit|default:"week" }}(s)<br>
{% if slot.recurrence_end_date %}
<strong>Ends:</strong> {{ slot.recurrence_end_date|date:"F d, Y" }}
{% else %}
<strong>Ends:</strong> No end date
{% endif %}
</div>
</div>
{% endif %}
<!-- Notes Section -->
{% if slot.notes %}
<div class="notes-section">
<div class="notes-title">
<i class="fas fa-sticky-note me-2"></i>
Slot Notes
</div>
<div class="notes-content">{{ slot.notes }}</div>
</div>
{% endif %}
<!-- Conflict Warnings -->
{% if slot.conflicts %}
<div class="conflict-warning">
<div class="conflict-title">
<i class="fas fa-exclamation-triangle me-2"></i>
Schedule Conflicts Detected
</div>
<ul class="conflict-list">
{% for conflict in slot.conflicts %}
<li>{{ conflict.description }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<!-- Right Column - Timeline and Actions -->
<div>
<!-- Activity Timeline -->
<div class="timeline-section">
<div class="info-card-header">
<h4 class="info-card-title">
<i class="fas fa-history text-info me-2"></i>
Activity Timeline
</h4>
</div>
<div class="timeline">
<div class="timeline-item" data-icon="">
<div class="timeline-time">{{ slot.created_at|date:"M d, Y g:i A" }}</div>
<div class="timeline-content">
<strong>Slot Created</strong><br>
Created by {{ slot.created_by.get_full_name|default:"System" }}
</div>
</div>
{% if slot.appointment %}
<div class="timeline-item" data-icon="">
<div class="timeline-time">{{ slot.appointment.created_at|date:"M d, Y g:i A" }}</div>
<div class="timeline-content">
<strong>Appointment Booked</strong><br>
Booked for {{ slot.appointment.patient.get_full_name }}
</div>
</div>
{% endif %}
{% if slot.updated_at != slot.created_at %}
<div class="timeline-item" data-icon="">
<div class="timeline-time">{{ slot.updated_at|date:"M d, Y g:i A" }}</div>
<div class="timeline-content">
<strong>Slot Modified</strong><br>
Last updated by {{ slot.updated_by.get_full_name|default:"System" }}
</div>
</div>
{% endif %}
{% for activity in slot.activity_log %}
<div class="timeline-item" data-icon="{{ activity.icon }}">
<div class="timeline-time">{{ activity.timestamp|date:"M d, Y g:i A" }}</div>
<div class="timeline-content">
<strong>{{ activity.title }}</strong><br>
{{ activity.description }}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Quick Stats -->
<div class="info-card">
<div class="info-card-header">
<h4 class="info-card-title">
<i class="fas fa-chart-bar text-warning me-2"></i>
Quick Stats
</h4>
</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Provider's Slots Today</div>
<div class="info-value">{{ provider_slots_today|default:0 }}</div>
</div>
<div class="info-item">
<div class="info-label">Utilization Rate</div>
<div class="info-value">{{ utilization_rate|default:0 }}%</div>
</div>
<div class="info-item">
<div class="info-label">Average Duration</div>
<div class="info-value">{{ avg_duration|default:30 }} min</div>
</div>
<div class="info-item">
<div class="info-label">No-Show Rate</div>
<div class="info-value">{{ no_show_rate|default:0 }}%</div>
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons fade-in">
{% if slot.status == 'AVAILABLE' %}
<button class="btn btn-primary btn-action" onclick="bookSlot()">
<i class="fas fa-calendar-plus"></i>Book Appointment
</button>
<button class="btn btn-warning btn-action" onclick="blockSlot()">
<i class="fas fa-ban"></i>Block Slot
</button>
{% elif slot.status == 'BOOKED' %}
<a href="{% url 'appointments:appointment_detail' slot.appointment.pk %}" class="btn btn-primary btn-action">
<i class="fas fa-eye"></i>View Appointment
</a>
<button class="btn btn-warning btn-action" onclick="rescheduleAppointment()">
<i class="fas fa-calendar-alt"></i>Reschedule
</button>
<button class="btn btn-danger btn-action" onclick="cancelAppointment()">
<i class="fas fa-times"></i>Cancel Appointment
</button>
{% elif slot.status == 'BLOCKED' %}
<button class="btn btn-success btn-action" onclick="unblockSlot()">
<i class="fas fa-check"></i>Unblock Slot
</button>
{% endif %}
<a href="{% url 'appointments:slot_update' slot.pk %}" class="btn btn-secondary btn-action">
<i class="fas fa-edit"></i>Edit Slot
</a>
<button class="btn btn-info btn-action" onclick="duplicateSlot()">
<i class="fas fa-copy"></i>Duplicate Slot
</button>
<button class="btn btn-outline-secondary btn-action" onclick="printSlot()">
<i class="fas fa-print"></i>Print Details
</button>
<a href="{% url 'appointments:slot_list' %}" class="btn btn-outline-secondary btn-action">
<i class="fas fa-arrow-left"></i>Back to Slots
</a>
</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">
<div class="modal-dialog"> <div class="modal-dialog">

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> </h1>
</ol> <p class="text-muted">Reusable configurations for common appointment types.</p>
<!-- END breadcrumb --> </div>
{# <div class="btn-toolbar mb-2 mb-md-0">#}
<!-- BEGIN page-header --> {# <div class="btn-group me-2">#}
<h1 class="page-header"> {# <a href="{% url 'appointments:waiting_list_edit' entry.pk %}" class="btn btn-warning">#}
Appointment Templates {# <i class="fas fa-edit me-1"></i>Edit Entry#}
<small>Reusable configurations for common appointment types</small> {# </a>#}
</h1> {# <a href="{% url 'appointments:waiting_list_delete' entry.pk %}" class="btn btn-danger">#}
<!-- END page-header --> {# <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> </h1>
</ol> <p class="text-muted">Manage appointment waiting list and patient queue.</p>
<!-- END breadcrumb --> </div>
</div>
<!-- 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>
<!-- END page-header -->
<!-- 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>
@ -317,7 +310,7 @@
class="btn btn-outline-primary btn-sm" title="View Details"> class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'appointments:waiting_list_edit' entry.pk %}" <a href="{% url 'appointments:waiting_list_edit' entry.pk %}"
class="btn btn-outline-warning btn-sm" title="Edit"> class="btn btn-outline-warning btn-sm" title="Edit">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>

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,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'),
@ -24,20 +30,19 @@ urlpatterns = [
path('telemedicine/<int:pk>/start/', views.start_telemedicine_session, name='start_telemedicine_session'), path('telemedicine/<int:pk>/start/', views.start_telemedicine_session, name='start_telemedicine_session'),
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,31 +1939,35 @@ 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.actual_end_time = timezone.now()
appointment.save()
appointment.status = 'CANCELLED' # Log completion
# appointment.actual_end_time = timezone.now() AuditLogger.log_event(
appointment.save() tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Cancel Appointment',
description=f'Cancelled appointment: {appointment.patient} with {appointment.provider}',
user=request.user,
content_object=appointment,
request=request
)
# Log completion messages.success(request, f'Appointment for {appointment.patient} cancelled successfully.')
AuditLogger.log_event( return redirect('appointments:appointment_detail', pk=pk)
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Cancel Appointment',
description=f'Cancelled appointment: {appointment.patient} with {appointment.provider}',
user=request.user,
content_object=appointment,
request=request
)
messages.success(request, f'Appointment for {appointment.patient} cancelled successfully.') return render(request, 'appointments/cancel_appointment.html', {
return redirect('appointments:appointment_request_detail', pk=pk) '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,
@ -2046,14 +2067,11 @@ def next_in_queue(request, queue_id):
content_object=next_entry, content_object=next_entry,
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,7 +2115,8 @@ 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(
tenant=tenant, tenant=tenant,
@ -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