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

Binary file not shown.

View File

@ -3,75 +3,45 @@ Admin configuration for accounts app.
"""
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.html import format_html
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)
class UserAdmin(BaseUserAdmin):
"""
Admin configuration for User model.
"""
list_display = [
'username', 'email', 'get_full_name', 'role', 'tenant',
'is_active', 'is_verified', 'is_approved', 'last_login'
]
list_filter = [
'role', 'tenant', 'is_active', 'is_verified', 'is_approved',
'two_factor_enabled', 'is_staff', 'is_superuser'
]
search_fields = [
'username', 'email', 'first_name', 'last_name',
'employee_id', 'license_number', 'npi_number'
]
ordering = ['last_name', 'first_name']
fieldsets = BaseUserAdmin.fieldsets + (
('Tenant Information', {
'fields': ('tenant', 'user_id')
}),
('Personal Information', {
'fields': (
'middle_name', 'preferred_name', 'phone_number', 'mobile_number',
'profile_picture', 'bio'
)
}),
('Professional Information', {
'fields': (
'employee_id', 'department', 'job_title', 'role',
'license_number', 'license_state', 'license_expiry',
'dea_number', 'npi_number'
)
}),
('Security Settings', {
'fields': (
'force_password_change', 'password_expires_at',
class UserAdmin(DjangoUserAdmin):
inlines = [EmployeeInline]
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')
search_fields = ('username', 'email', 'first_name', 'last_name')
readonly_fields = ('last_login', 'date_joined', 'last_password_change')
fieldsets = (
(None, {'fields': ('tenant', 'username', 'password')}),
('Personal info', {'fields': ('first_name', 'last_name', 'email')}),
('Security', {'fields': (
'force_password_change', 'password_expires_at', 'last_password_change',
'failed_login_attempts', 'locked_until', 'two_factor_enabled',
'max_concurrent_sessions', 'session_timeout_minutes'
'max_concurrent_sessions', 'session_timeout_minutes',
)}),
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
('Important dates', {'fields': ('last_login', 'date_joined')}),
)
}),
('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',)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('tenant', 'username', 'email', 'password1', 'password2'),
}),
)
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)
class TwoFactorDeviceAdmin(admin.ModelAdmin):

View File

@ -4,3 +4,6 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
def ready(self):
from . import signals

View File

@ -15,25 +15,23 @@ class UserForm(forms.ModelForm):
class Meta:
model = User
fields = [
'first_name', 'last_name', 'email', 'phone_number', 'mobile_number',
'employee_id', 'role', 'department', 'bio', 'user_timezone', 'language',
'theme', 'is_active', 'is_approved'
'email',
]
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
# 'first_name': forms.TextInput(attrs={'class': 'form-control'}),
# 'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone_number': forms.TextInput(attrs={'class': 'form-control'}),
'mobile_number': forms.TextInput(attrs={'class': 'form-control'}),
'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
'role': forms.Select(attrs={'class': 'form-select'}),
'department': forms.TextInput(attrs={'class': 'form-control'}),
'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'user_timezone': forms.Select(attrs={'class': 'form-select'}),
'language': forms.Select(attrs={'class': 'form-select'}),
'theme': forms.Select(attrs={'class': 'form-select'}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'is_approved': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
# 'phone_number': forms.TextInput(attrs={'class': 'form-control'}),
# 'mobile_number': forms.TextInput(attrs={'class': 'form-control'}),
# 'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
# 'role': forms.Select(attrs={'class': 'form-select'}),
# 'department': forms.TextInput(attrs={'class': 'form-control'}),
# 'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
# 'user_timezone': forms.Select(attrs={'class': 'form-select'}),
# 'language': forms.Select(attrs={'class': 'form-select'}),
# 'theme': forms.Select(attrs={'class': 'form-select'}),
# 'is_active': 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)
email = forms.EmailField(required=True)
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)
class Meta:
model = User
fields = [
'username', 'first_name', 'last_name', 'email', 'employee_id',
'role', 'department', 'password1', 'password2'
'username', 'first_name', 'last_name', 'email', 'password1', 'password2'
]
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'}),
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
'role': forms.Select(attrs={'class': 'form-select'}),
'department': forms.TextInput(attrs={'class': 'form-control'}),
# 'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
# 'role': forms.Select(attrs={'class': 'form-select'}),
# 'department': forms.TextInput(attrs={'class': 'form-control'}),
}
def __init__(self, *args, **kwargs):
@ -148,11 +145,11 @@ class AccountsSearchForm(forms.Form):
'placeholder': 'Search users, sessions, devices...'
})
)
role = forms.ChoiceField(
choices=[('', 'All Roles')] + list(User._meta.get_field('role').choices),
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
# role = forms.ChoiceField(
# choices=[('', 'All Roles')] + list(User._meta.get_field('role').choices),
# required=False,
# widget=forms.Select(attrs={'class': 'form-select'})
# )
status = forms.ChoiceField(
choices=[
('', '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.validators
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.conf import settings
from django.db import migrations, models
@ -390,21 +386,6 @@ class Migration(migrations.Migration):
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
@ -417,12 +398,6 @@ class Migration(migrations.Migration):
blank=True, max_length=150, verbose_name="last name"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
),
),
(
"is_staff",
models.BooleanField(
@ -447,157 +422,17 @@ class Migration(migrations.Migration):
),
(
"user_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique user identifier",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
(
"username",
models.CharField(
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
),
),
(
"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,
),
),
("email", models.EmailField(blank=True, max_length=254, null=True)),
(
"force_password_change",
models.BooleanField(
@ -643,65 +478,6 @@ class Migration(migrations.Migration):
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",
models.DateTimeField(
@ -709,17 +485,6 @@ class Migration(migrations.Migration):
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",
models.ManyToManyField(
@ -733,8 +498,6 @@ class Migration(migrations.Migration):
),
],
options={
"verbose_name": "User",
"verbose_name_plural": "Users",
"db_table": "accounts_user",
"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
from django.conf import settings
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
on_delete=django.db.models.deletion.PROTECT,
related_name="users",
to="core.tenant",
),
@ -77,25 +77,25 @@ class Migration(migrations.Migration):
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["tenant", "role"], name="accounts_us_tenant__731b87_idx"
fields=["tenant", "email"], name="accounts_us_tenant__162cd2_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["employee_id"], name="accounts_us_employe_0cbd94_idx"
fields=["tenant", "username"], name="accounts_us_tenant__d92906_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["license_number"], name="accounts_us_license_02eb85_idx"
fields=["tenant", "user_id"], name="accounts_us_tenant__bd3758_idx"
),
),
migrations.AddIndex(
model_name="user",
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(

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
from datetime import timedelta
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.core.validators import RegexValidator
@ -11,158 +13,379 @@ from django.utils import timezone
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):
"""
Extended user model for hospital management system.
Minimal auth user for a multi-tenant app:
- Authentication core (from AbstractUser)
- Tenant link
- Security/session controls (lockout, password expiry, 2FA flag, session caps)
Everything else lives on hr.Employee.
"""
ROLE_CHOICES = [
('SUPER_ADMIN', 'Super Administrator'),
('ADMIN', 'Administrator'),
('PHYSICIAN', 'Physician'),
('NURSE', 'Nurse'),
('NURSE_PRACTITIONER', 'Nurse Practitioner'),
('PHYSICIAN_ASSISTANT', 'Physician Assistant'),
('PHARMACIST', 'Pharmacist'),
('PHARMACY_TECH', 'Pharmacy Technician'),
('LAB_TECH', 'Laboratory Technician'),
('RADIOLOGIST', 'Radiologist'),
('RAD_TECH', 'Radiology Technician'),
('THERAPIST', 'Therapist'),
('SOCIAL_WORKER', 'Social Worker'),
('CASE_MANAGER', 'Case Manager'),
('BILLING_SPECIALIST', 'Billing Specialist'),
('REGISTRATION', 'Registration Staff'),
('SCHEDULER', 'Scheduler'),
('MEDICAL_ASSISTANT', 'Medical Assistant'),
('CLERICAL', 'Clerical Staff'),
('IT_SUPPORT', 'IT Support'),
('QUALITY_ASSURANCE', 'Quality Assurance'),
('COMPLIANCE', 'Compliance Officer'),
('SECURITY', 'Security'),
('MAINTENANCE', 'Maintenance'),
('VOLUNTEER', 'Volunteer'),
('STUDENT', 'Student'),
('RESEARCHER', 'Researcher'),
('CONSULTANT', 'Consultant'),
('VENDOR', 'Vendor'),
('GUEST', 'Guest'),
]
THEME_CHOICES = [
('LIGHT', 'Light'),
('DARK', 'Dark'),
('AUTO', 'Auto'),
]
# Basic Information
user_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique user identifier'
)
# Tenant relationship
# Stable internal UUID
user_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
# Tenant (PROTECT = safer than cascading deletion)
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
on_delete=models.PROTECT,
related_name='users',
help_text='Organization tenant'
help_text='Organization tenant',
)
# Personal Information
middle_name = models.CharField(
username = 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'
unique=True,
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'
)
email = models.EmailField(blank=True, null=True)
# 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
# --- Security & session controls kept on User (auth-level concerns) ---
force_password_change = models.BooleanField(
default=False,
help_text='User must change password on next login'
@ -185,8 +408,6 @@ class User(AbstractUser):
default=False,
help_text='Two-factor authentication enabled'
)
# Session Management
max_concurrent_sessions = models.PositiveIntegerField(
default=3,
help_text='Maximum concurrent sessions allowed'
@ -195,160 +416,64 @@ class User(AbstractUser):
default=30,
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(
default=timezone.now,
help_text='Last password change date'
)
is_active = models.BooleanField(
default=True,
help_text='User account is active'
)
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']),
models.Index(fields=['tenant', 'email']),
models.Index(fields=['tenant', 'username']),
models.Index(fields=['tenant', 'user_id']),
models.Index(fields=['tenant', 'is_active']),
]
def __str__(self):
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):
"""
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
# ---- Security helpers ----
@property
def is_account_locked(self) -> bool:
return bool(self.locked_until and timezone.now() < self.locked_until)
@property
def is_account_locked(self):
"""
Check if account is currently locked.
"""
if self.locked_until:
return timezone.now() < self.locked_until
return False
def is_password_expired(self) -> bool:
return bool(self.password_expires_at and timezone.now() > self.password_expires_at)
@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)
def lock_account(self, duration_minutes: int = 15):
self.locked_until = timezone.now() + 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):
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
# Lock account after 5 failed attempts
if max_attempts is None:
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)
if lockout_minutes is None:
lockout_minutes = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('LOCKOUT_DURATION_MINUTES', 15)
if self.failed_login_attempts >= max_attempts:
self.lock_account(lockout_minutes)
else:
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'])

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="col-auto">
<div class="avatar-upload">
{% if user.profile_picture %}
<img src="{{ user.profile_picture.url }}" alt="{{ user.get_full_name }}" class="profile-avatar">
{% if user.employee_profile.profile_picture %}
<img src="{{ user.employee_profile.profile_picture.url }}" alt="{{ user.employee_profile.get_full_name }}" class="profile-avatar">
{% else %}
<div class="profile-avatar bg-secondary d-flex align-items-center justify-content-center">
<i class="fas fa-user fa-2x text-white"></i>
@ -138,8 +138,8 @@
</div>
</div>
<div class="col">
<h1 class="mb-2">{{ user.get_full_name }}</h1>
<p class="mb-0 opacity-75">{{ user.role|title }} • {{ user.department|default:"No Department" }}</p>
<h1 class="mb-2">{{ user.employee_profile.get_full_name }}</h1>
<p class="mb-0 opacity-75">{{ user.employee_profile.get_role_display }} • {{ user.employee_profile.department|default:"No Department" }}</p>
</div>
</div>
</div>
@ -167,14 +167,14 @@
<div class="mb-3">
<label for="first_name" class="form-label">First Name</label>
<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 class="col-md-6">
<div class="mb-3">
<label for="last_name" class="form-label">Last Name</label>
<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>
@ -184,14 +184,14 @@
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<input type="email" class="form-control" id="email" name="email"
value="{{ user.email }}" required>
value="{{ user.employee_profile.email }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="phone_number" class="form-label">Phone Number</label>
<input type="tel" class="form-control" id="phone_number" name="phone_number"
value="{{ user.phone_number }}">
<label for="phone" class="form-label">Phone Number</label>
<input type="tel" class="form-control" id="phone" name="phone"
value="{{ user.employee_profile.phone }}">
</div>
</div>
</div>
@ -199,9 +199,9 @@
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="mobile_number" class="form-label">Mobile Number</label>
<input type="tel" class="form-control" id="mobile_number" name="mobile_number"
value="{{ user.mobile_number }}">
<label for="mobile_phone" class="form-label">Mobile Number</label>
<input type="tel" class="form-control" id="mobile_phone" name="mobile_phone"
value="{{ user.employee_profile.mobile_phone }}">
</div>
</div>
</div>
@ -209,7 +209,7 @@
<div class="mb-3">
<label for="bio" class="form-label">Bio</label>
<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>
@ -219,29 +219,29 @@
<div class="preference-item">
<label for="timezone" class="form-label">Timezone</label>
<select class="form-select" id="timezone" name="timezone">
<option value="UTC" {% if user.timezone == 'UTC' %}selected{% endif %}>UTC</option>
<option value="America/New_York" {% if user.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/Denver" {% if user.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="Asia/Riyadh" {% if user.employee_profile.timezone == 'Asia/Riyadh' %}selected{% endif %}>Asia/Riyadh</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.employee_profile.timezone == 'America/Chicago' %}selected{% endif %}>Central 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.employee_profile.timezone == 'America/Los_Angeles' %}selected{% endif %}>Pacific Time</option>
</select>
</div>
<div class="preference-item">
<label for="language" class="form-label">Language</label>
<select class="form-select" id="language" name="language">
<option value="en" {% if user.language == 'en' %}selected{% endif %}>English</option>
<option value="es" {% if user.language == 'es' %}selected{% endif %}>Spanish</option>
<option value="fr" {% if user.language == 'fr' %}selected{% endif %}>French</option>
<option value="en" {% if user.employee_profile.language == 'en' %}selected{% endif %}>English</option>
<option value="ar" {% if user.employee_profile.language == 'ar' %}selected{% endif %}>Arabic</option>
<option value="fr" {% if user.employee_profile.language == 'fr' %}selected{% endif %}>French</option>
</select>
</div>
<div class="preference-item">
<label for="theme" class="form-label">Theme</label>
<select class="form-select" id="theme" name="theme">
<option value="light" {% if user.theme == 'light' %}selected{% endif %}>Light</option>
<option value="dark" {% if user.theme == 'dark' %}selected{% endif %}>Dark</option>
<option value="auto" {% if user.theme == 'auto' %}selected{% endif %}>Auto</option>
<option value="light" {% if user.employee_profile.theme == 'light' %}selected{% endif %}>Light</option>
<option value="dark" {% if user.employee_profile.theme == 'dark' %}selected{% endif %}>Dark</option>
<option value="auto" {% if user.employee_profile.theme == 'auto' %}selected{% endif %}>Auto</option>
</select>
</div>
</div>

View File

@ -18,6 +18,8 @@ from django.utils import timezone
from django.urls import reverse_lazy, reverse
from django.core.paginator import Paginator
from datetime import timedelta
from hr.models import Employee
from .models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
from .forms import (
UserForm, UserCreateForm, TwoFactorDeviceForm, SocialAccountForm,
@ -26,17 +28,6 @@ from .forms import (
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)
# ============================================================================
@ -1024,22 +1015,22 @@ def user_profile_update(request):
HTMX view for user profile update.
"""
if request.method == 'POST':
user = request.user
employee = Employee.objects.get(user=request.user)
# Update basic information
user.first_name = request.POST.get('first_name', user.first_name)
user.last_name = request.POST.get('last_name', user.last_name)
user.email = request.POST.get('email', user.email)
user.phone_number = request.POST.get('phone_number', user.phone_number)
user.mobile_number = request.POST.get('mobile_number', user.mobile_number)
user.bio = request.POST.get('bio', user.bio)
employee.first_name = request.POST.get('first_name', employee.first_name)
employee.last_name = request.POST.get('last_name', employee.last_name)
employee.email = request.POST.get('email', employee.email)
employee.phone = request.POST.get('phone', employee.phone)
employee.mobile_phone = request.POST.get('mobile_phone', employee.mobile_phone)
employee.bio = request.POST.get('bio', employee.bio)
# Update preferences
user.timezone = request.POST.get('timezone', user.timezone)
user.language = request.POST.get('language', user.language)
user.theme = request.POST.get('theme', user.theme)
employee.user_timezone = request.POST.get('timezone', employee.user_timezone)
employee.language = request.POST.get('language', employee.language)
employee.theme = request.POST.get('theme', employee.theme)
user.save()
employee.save()
# Log the update
AuditLogger.log_event(
@ -1047,14 +1038,15 @@ def user_profile_update(request):
event_type='UPDATE',
event_category='DATA_MODIFICATION',
action='Update User Profile',
description=f'User updated their profile: {user.username}',
description=f'User updated their profile: {employee.user.username}',
user=request.user,
content_object=user,
content_object=employee,
request=request
)
messages.success(request, 'Profile updated successfully.')
return JsonResponse({'status': 'success'})
return redirect('accounts:user_profile')
return JsonResponse({'error': 'Invalid request'}, status=400)

View File

@ -1,3 +1,5 @@
# scripts/seed_saudi_accounts.py
import os
import django
@ -5,56 +7,59 @@ import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
django.setup()
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 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_FIRST_NAMES_MALE = [
'Mohammed', 'Abdullah', 'Ahmed', 'Omar', 'Ali', 'Hassan', 'Khalid', 'Faisal',
'Saad', 'Fahd', 'Bandar', 'Turki', 'Nasser', 'Saud', 'Abdulrahman',
'Abdulaziz', 'Salman', 'Waleed', 'Majid', 'Rayan', 'Yazeed', 'Mansour',
'Osama', 'Tariq', 'Adel', 'Nawaf', 'Sultan', 'Mishaal', 'Badr', 'Ziad'
]
SAUDI_FIRST_NAMES_FEMALE = [
'Fatima', 'Aisha', 'Maryam', 'Khadija', 'Sarah', 'Noura', 'Hala', 'Reem',
'Lina', 'Dana', 'Rana', 'Nada', 'Layla', 'Amira', 'Zahra', 'Yasmin',
'Dina', 'Noor', 'Rahma', 'Salma', 'Lama', 'Ghada', 'Rania', 'Maha',
'Wedad', 'Najla', 'Shahd', 'Jood', 'Rand', 'Malak'
]
SAUDI_FAMILY_NAMES = [
'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-Faisal', 'Al-Saud', 'Al-Thani', 'Al-Maktoum', 'Al-Sabah', 'Al-Khalifa',
'Bin-Laden', 'Al-Rajhi', 'Al-Sudairy', 'Al-Shaalan', 'Al-Kabeer', 'Al-Ajmi',
'Al-Faisal', 'Al-Saud', 'Al-Shaalan', 'Al-Rajhi', 'Al-Sudairy', 'Al-Ajmi',
'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 = [
'Riyadh', 'Jeddah', 'Mecca', 'Medina', 'Dammam', 'Khobar', 'Dhahran',
'Taif', 'Tabuk', 'Buraidah', 'Khamis Mushait', 'Hofuf', 'Mubarraz',
'Jubail', 'Yanbu', 'Abha', 'Najran', 'Jazan', 'Hail', 'Arar'
]
SAUDI_PROVINCES = [
'Riyadh Province', 'Makkah Province', 'Eastern Province', 'Asir Province',
'Jazan Province', 'Medina Province', 'Qassim Province', 'Tabuk 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 = {
'PHYSICIAN': ['Consultant Physician', 'Senior Physician', 'Staff Physician', 'Resident Physician',
'Chief Medical Officer'],
@ -63,50 +68,13 @@ SAUDI_JOB_TITLES = {
'ADMIN': ['Medical Director', 'Hospital Administrator', 'Department Manager', 'Operations Manager'],
'LAB_TECH': ['Senior Lab Technician', 'Medical Laboratory Scientist', 'Lab Supervisor'],
'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']
def generate_saudi_phone():
"""Generate Saudi phone number"""
area_codes = ['11', '12', '13', '14', '16', '17'] # Major Saudi area codes
return f"+966-{random.choice(area_codes)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}"
def generate_saudi_mobile():
"""Generate Saudi mobile number"""
mobile_prefixes = ['50', '53', '54', '55', '56', '57', '58', '59'] # Saudi mobile prefixes
return f"+966-{random.choice(mobile_prefixes)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}"
def generate_saudi_license():
"""Generate Saudi medical license number"""
prefix = random.choice(SAUDI_LICENSE_PREFIXES)
return f"{prefix}-{random.randint(100000, 999999)}"
def generate_saudi_employee_id(tenant_name, role):
"""Generate Saudi employee ID"""
tenant_code = ''.join([c for c in tenant_name.upper() if c.isalpha()])[:3]
role_code = role[:3].upper()
return f"{tenant_code}-{role_code}-{random.randint(1000, 9999)}"
def create_saudi_users(tenants, users_per_tenant=50):
"""Create Saudi healthcare users"""
users = []
role_distribution = {
ROLE_DISTRIBUTION = {
'PHYSICIAN': 0.15,
'NURSE': 0.25,
'PHARMACIST': 0.08,
@ -116,124 +84,173 @@ def create_saudi_users(tenants, users_per_tenant=50):
'ADMIN': 0.07,
'MEDICAL_ASSISTANT': 0.12,
'CLERICAL': 0.10
}
}
# -------------------------------
# Helpers
# -------------------------------
def ensure_departments(tenant):
"""
Ensure Department objects exist for this tenant; return a list of them.
Adjust if your Department is global (then drop tenant filtering).
"""
existing = list(Department.objects.filter(tenant=tenant)) if 'tenant' in [f.name for f in Department._meta.fields] else list(Department.objects.all())
if existing:
return existing
# create seed departments
bulk = []
for name in SAUDI_DEPARTMENTS:
if 'tenant' in [f.name for f in Department._meta.fields]:
bulk.append(Department(name=name, tenant=tenant))
else:
bulk.append(Department(name=name))
Department.objects.bulk_create(bulk, ignore_conflicts=True)
return list(Department.objects.filter(tenant=tenant)) if 'tenant' in [f.name for f in Department._meta.fields] else list(Department.objects.all())
def generate_saudi_mobile_e164():
"""Generate Saudi E.164 mobile: +9665XXXXXXXX"""
return f"+9665{random.randint(10000000, 99999999)}"
def generate_saudi_license():
"""Generate Saudi medical license number (fictional format)"""
prefix = random.choice(SAUDI_LICENSE_PREFIXES)
return f"{prefix}-{random.randint(100000, 999999)}"
def tenant_scoped_unique_username(tenant, base_username: str) -> str:
"""
Make username unique within a tenant (your User has tenant-scoped unique constraint).
"""
username = base_username
i = 1
while User.objects.filter(tenant=tenant, username=username).exists():
i += 1
username = f"{base_username}{i}"
return username
def pick_job_title(role: str) -> str:
titles = SAUDI_JOB_TITLES.get(role)
if titles:
return random.choice(titles)
# fallback
return role.replace('_', ' ').title()
# -------------------------------
# Generators
# -------------------------------
def create_saudi_users(tenants, users_per_tenant=50):
"""
Create Users (auth + security), then populate Employee profile.
Relies on the post_save signal to create Employee automatically.
"""
all_users = []
for tenant in tenants:
departments = ensure_departments(tenant)
tenant_users = []
for role, percentage in role_distribution.items():
user_count = max(1, int(users_per_tenant * percentage))
for role, pct in ROLE_DISTRIBUTION.items():
count = max(1, int(users_per_tenant * pct))
for i in range(user_count):
# Determine gender for Arabic naming
for _ in range(count):
is_male = random.choice([True, False])
first_name = random.choice(SAUDI_FIRST_NAMES_MALE if is_male else SAUDI_FIRST_NAMES_FEMALE)
last_name = random.choice(SAUDI_FAMILY_NAMES)
middle_name = random.choice(SAUDI_MIDDLE_NAMES) if random.choice([True, False]) else None
# Generate username
username = f"{first_name.lower()}.{last_name.lower().replace('-', '').replace('al', '')}"
counter = 1
original_username = username
while User.objects.filter(username=username).exists():
username = f"{original_username}{counter}"
counter += 1
# Generate email
# base username like "mohammed.alrashid"
base_username = f"{first_name.lower()}.{last_name.lower().replace('-', '').replace('al', '')}"
username = tenant_scoped_unique_username(tenant, base_username)
email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa"
# Professional information
department = random.choice(SAUDI_DEPARTMENTS)
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)}"
is_admin = role in ['ADMIN', 'SUPER_ADMIN']
is_superuser = role == 'SUPER_ADMIN'
# Auth-level fields only
user = User.objects.create(
tenant=tenant,
username=username,
email=email,
first_name=first_name,
last_name=last_name,
middle_name=middle_name,
preferred_name=first_name if random.choice([True, False]) else None,
tenant=tenant,
is_active=True,
is_staff=is_admin,
is_superuser=is_superuser,
# Contact information
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
# security/session (these live on User by design)
force_password_change=random.choice([True, False]),
password_expires_at=django_timezone.now() + timedelta(days=random.randint(90, 365)),
failed_login_attempts=random.randint(0, 2),
two_factor_enabled=random.choice([True, False]) if role in ['PHYSICIAN', 'ADMIN',
'PHARMACIST'] else False,
# Session settings
two_factor_enabled=random.choice([True, False]) if role in ['PHYSICIAN', 'ADMIN', 'PHARMACIST'] else False,
max_concurrent_sessions=random.choice([1, 2, 3, 5]),
session_timeout_minutes=random.choice([30, 60, 120, 240]),
last_password_change=django_timezone.now() - timedelta(days=random.randint(1, 90)),
date_joined=django_timezone.now() - timedelta(days=random.randint(1, 365)),
)
user.set_password('Hospital@123')
user.save()
# Signal should have created Employee; now populate Employee fields
emp: Employee = user.employee_profile # created by signal
emp.tenant = tenant # ensure alignment
emp.first_name = first_name
emp.last_name = last_name
emp.preferred_name = first_name if random.choice([True, False]) else None
# Contact (E.164 KSA)
mobile = generate_saudi_mobile_e164()
emp.phone = mobile
emp.mobile_phone = mobile
emp.email = email
# Role/Org
emp.role = role
emp.department = random.choice(departments) if departments else None
emp.job_title = pick_job_title(role)
# License (only some roles)
if role in ['PHYSICIAN', 'NURSE', 'PHARMACIST', 'RADIOLOGIST']:
emp.license_number = generate_saudi_license()
emp.license_state = random.choice(SAUDI_PROVINCES)
emp.license_expiry_date = django_timezone.now().date() + timedelta(days=random.randint(365, 1095))
if role == 'PHYSICIAN':
# fictitious local analogue to NPI
emp.npi_number = f"SA{random.randint(1000000, 9999999)}"
# Preferences
user_timezone='Asia/Riyadh',
language=random.choice(['ar', 'en', 'ar_SA']),
theme=random.choice(['LIGHT', 'DARK', 'AUTO']),
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
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',
# 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))
# 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)),
date_joined=django_timezone.now() - timedelta(days=random.randint(1, 365))
)
emp.save()
# Set password
user.set_password('Hospital@123') # Default password
user.save()
users.append(user)
tenant_users.append(user)
all_users.append(user)
# Set approval relationships
admin_users = [u for u in tenant_users if u.role in ['ADMIN', 'SUPER_ADMIN']]
# 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:
approver = random.choice(admin_users)
for user in tenant_users:
if user != approver and user.role != 'SUPER_ADMIN':
user.approved_by = approver
user.save()
for u in tenant_users:
if u != approver:
emp = u.employee_profile
emp.approved_by = approver
emp.save(update_fields=['approved_by'])
print(f"Created {len(tenant_users)} users for {tenant.name}")
return users
return all_users
def create_saudi_two_factor_devices(users):
@ -249,8 +266,8 @@ def create_saudi_two_factor_devices(users):
for user in users:
if user.two_factor_enabled:
# Create 1-3 devices per user
device_count = random.randint(1, 3)
emp = getattr(user, 'employee_profile', None)
for _ in range(device_count):
device_type = random.choice(device_types)
@ -271,9 +288,9 @@ def create_saudi_two_factor_devices(users):
if device_type == 'TOTP':
device_data['secret_key'] = secrets.token_urlsafe(32)
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':
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)
devices.append(device)
@ -285,21 +302,19 @@ def create_saudi_two_factor_devices(users):
def create_saudi_social_accounts(users):
"""Create social authentication accounts for Saudi users"""
social_accounts = []
# Common providers in Saudi Arabia
providers = ['GOOGLE', 'MICROSOFT', 'APPLE', 'LINKEDIN']
for user in users:
# 30% chance of having social accounts
if random.choice([True, False, False, False]):
if random.choice([True, False, False, False]): # ~25% chance
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(
user=user,
provider=provider,
provider_id=f"{provider.lower()}_{random.randint(100000000, 999999999)}",
provider_email=user.email,
display_name=user.get_full_name(),
display_name=display_name,
profile_url=f"https://{provider.lower()}.com/profile/{user.username}",
avatar_url=f"https://{provider.lower()}.com/avatar/{user.username}.jpg",
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.',
'185.84.', '188.245.', '217.9.', '82.205.', '5.63.'
]
browsers = [
'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'
]
operating_systems = [
'Windows 11', 'Windows 10', 'macOS 14.0', 'iOS 17.1.2',
'Android 14', 'Ubuntu 22.04'
]
device_types = ['DESKTOP', 'MOBILE', 'TABLET']
login_methods = ['PASSWORD', 'TWO_FACTOR', 'SOCIAL', 'SSO']
for user in users:
# Create 1-5 sessions per user
session_count = random.randint(1, 5)
timeout_minutes = user.session_timeout_minutes or 30
for i in range(session_count):
ip_prefix = random.choice(saudi_ips)
ip_address = f"{ip_prefix}{random.randint(1, 255)}.{random.randint(1, 255)}"
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(
user=user,
@ -364,7 +376,7 @@ def create_saudi_user_sessions(users):
login_method=random.choice(login_methods),
created_at=session_start,
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))
)
sessions.append(session)
@ -376,16 +388,12 @@ def create_saudi_user_sessions(users):
def create_saudi_password_history(users):
"""Create password history for Saudi users"""
password_history = []
passwords = ['Hospital@123', 'Medical@456', 'Health@789', 'Saudi@2024', 'Secure@Pass']
for user in users:
# Create 1-5 password history entries per user
history_count = random.randint(1, 5)
for i in range(history_count):
password = random.choice(passwords)
history_entry = PasswordHistory.objects.create(
user=user,
password_hash=make_password(password),
@ -397,33 +405,29 @@ def create_saudi_password_history(users):
return password_history
# -------------------------------
# Main
# -------------------------------
def main():
"""Main function to generate all Saudi accounts data"""
print("Starting Saudi Healthcare Accounts Data Generation...")
# Get existing tenants
tenants = list(Tenant.objects.all())
if not tenants:
print("❌ No tenants found. Please run the core data generator first.")
print("❌ No tenants found. Please seed core tenants first.")
return
# Create users
print("\n1. Creating Saudi Healthcare Users...")
users = create_saudi_users(tenants, 40) # 40 users per tenant
print("\n1. Creating Saudi Healthcare Users (with Employee profiles)...")
users = create_saudi_users(tenants, users_per_tenant=40)
# Create two-factor devices
print("\n2. Creating Two-Factor Authentication Devices...")
devices = create_saudi_two_factor_devices(users)
# Create social accounts
print("\n3. Creating Social Authentication Accounts...")
social_accounts = create_saudi_social_accounts(users)
# Create user sessions
print("\n4. Creating User Sessions...")
sessions = create_saudi_user_sessions(users)
# Create password history
print("\n5. Creating Password History...")
password_history = create_saudi_password_history(users)
@ -435,10 +439,10 @@ def main():
print(f" - User Sessions: {len(sessions)}")
print(f" - Password History Entries: {len(password_history)}")
# Role distribution summary
role_counts = {}
for user in users:
role_counts[user.role] = role_counts.get(user.role, 0) + 1
for u in users:
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:")
for role, count in sorted(role_counts.items()):
@ -449,7 +453,7 @@ def main():
'devices': devices,
'social_accounts': social_accounts,
'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.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
from django.conf import settings

View File

@ -8,8 +8,8 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-10">
<div class="row">
@ -228,8 +228,8 @@
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@ -8,8 +8,8 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-10">
<div class="row">
@ -185,8 +185,8 @@
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@ -8,8 +8,8 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-10">
<div class="row">
@ -133,8 +133,8 @@
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@ -56,7 +56,7 @@ def generate_dashboards(tenants, users):
created_by=creator
)
dashboard.allowed_users.add(creator)
dashboard.allowed_roles = [creator.role]
dashboard.allowed_roles = [creator.employee_profile.role]
dashboard.save()
dashboards.append(dashboard)
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(
tenant=user.tenant,
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')
@ -645,34 +645,34 @@ class WaitingListContactLogForm(forms.ModelForm):
widgets = {
'contact_method': forms.Select(attrs={
'class': 'form-select',
'class': 'form-select form-select-sm',
'required': True
}),
'contact_outcome': forms.Select(attrs={
'class': 'form-select',
'class': 'form-select form-select-sm',
'required': True
}),
'appointment_offered': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
'offered_date': forms.DateInput(attrs={
'class': 'form-control',
'class': 'form-control form-control-sm',
'type': 'date'
}),
'offered_time': forms.TimeInput(attrs={
'class': 'form-control',
'class': 'form-control form-control-sm',
'type': 'time'
}),
'patient_response': forms.Select(attrs={
'class': 'form-select'
'class': 'form-select form-select-sm'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'class': 'form-control form-control-sm',
'rows': 4,
'placeholder': 'Notes from contact attempt...'
}),
'next_contact_date': forms.DateInput(attrs={
'class': 'form-control',
'class': 'form-control form-control-sm',
'type': 'date',
'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.db.models.deletion
@ -549,6 +549,526 @@ class Migration(migrations.Migration):
"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(
name="WaitingQueue",
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
from django.conf import settings
@ -12,6 +12,7 @@ class Migration(migrations.Migration):
dependencies = [
("appointments", "0001_initial"),
("core", "0001_initial"),
("hr", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
@ -179,6 +180,105 @@ class Migration(migrations.Migration):
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="created_by",
field=models.ForeignKey(
blank=True,
help_text="User who created the waiting list entry",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_waiting_list_entries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="department",
field=models.ForeignKey(
help_text="Department for appointment",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="hr.department",
),
),
migrations.AddField(
model_name="waitinglist",
name="patient",
field=models.ForeignKey(
help_text="Patient on waiting list",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="waitinglist",
name="provider",
field=models.ForeignKey(
blank=True,
help_text="Preferred healthcare provider",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="provider_waiting_list",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="removed_by",
field=models.ForeignKey(
blank=True,
help_text="User who removed entry from waiting list",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="removed_waiting_list_entries",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglist",
name="scheduled_appointment",
field=models.ForeignKey(
blank=True,
help_text="Scheduled appointment from waiting list",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="waiting_list_entry",
to="appointments.appointmentrequest",
),
),
migrations.AddField(
model_name="waitinglist",
name="tenant",
field=models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="core.tenant",
),
),
migrations.AddField(
model_name="waitinglistcontactlog",
name="contacted_by",
field=models.ForeignKey(
blank=True,
help_text="Staff member who made contact",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="waitinglistcontactlog",
name="waiting_list_entry",
field=models.ForeignKey(
help_text="Associated waiting list entry",
on_delete=django.db.models.deletion.CASCADE,
related_name="contact_logs",
to="appointments.waitinglist",
),
),
migrations.AddField(
model_name="waitingqueue",
name="created_by",
@ -324,6 +424,63 @@ class Migration(migrations.Migration):
fields=["scheduled_start"], name="appointment_schedul_8a4e8e_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["tenant", "status"], name="appointment_tenant__a558da_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["patient", "status"], name="appointment_patient_73f03d_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["department", "specialty", "status"],
name="appointment_departm_78fd70_idx",
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["priority", "urgency_score"],
name="appointment_priorit_30fb90_idx",
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["status", "created_at"], name="appointment_status_cfe551_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["provider", "status"], name="appointment_provide_dd6c2b_idx"
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["waiting_list_entry", "contact_date"],
name="appointment_waiting_50d8ac_idx",
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["contact_outcome"], name="appointment_contact_ad9c45_idx"
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["next_contact_date"], name="appointment_next_co_b29984_idx"
),
),
migrations.AddIndex(
model_name="waitingqueue",
index=models.Index(

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(
default=list,
null=True,
blank=True,
help_text='Acceptable days of week (0=Monday, 6=Sunday)'
)

Binary file not shown.

View File

@ -9,8 +9,8 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-12">
<ul class="breadcrumb">
@ -187,8 +187,8 @@
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@ -8,8 +8,8 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-12">
<ul class="breadcrumb">
@ -240,8 +240,8 @@
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% 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,8 +7,8 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-xl-12">
<ul class="breadcrumb">
@ -158,9 +158,9 @@
</div>
</div>
</div>
</div>
</div>
<!-- Appointment Detail Modal -->
<div class="modal fade" id="appointmentModal" tabindex="-1">
<div class="modal-dialog">

View File

@ -2,67 +2,86 @@
{% load static %}
{% 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 %}
<div id="content" class="app-content">
<div class="container">
<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> Cancel<span class="fw-light">Appointment</span>
</h1>
<p class="text-muted">Appointment cancellation form.</p>
</div>
</div>
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-xl-8">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_detail' appointment.id %}">{{ appointment.patient.first_name }} {{ appointment.patient.last_name }}</a></li>
<li class="breadcrumb-item active">Cancel</li>
</ul>
<h1 class="page-header">Cancel Appointment</h1>
<!-- Appointment Info -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Appointment Details</h4>
<div class="panel panel-inverse mb-4" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-calendar-day"></i> Appointment Details
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
<div 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.first_name }} {{ appointment.patient.last_name }}</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.patient_id }}</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.first_name }} {{ appointment.provider.last_name }}</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.department.name }}</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.appointment_date|date:"M d, Y" }}</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.appointment_time|time:"g:i A" }}</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.appointment_type.name }}</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' %}
{% if appointment.status == 'SCHEDULED' %}
<span class="badge bg-info">Scheduled</span>
{% elif appointment.status == 'confirmed' %}
{% elif appointment.status == 'CONFIRMED' %}
<span class="badge bg-success">Confirmed</span>
{% elif appointment.status == 'checked_in' %}
{% elif appointment.status == 'CHECKED_IN' %}
<span class="badge bg-primary">Checked In</span>
{% endif %}
</div>
@ -82,11 +101,19 @@
</div>
<!-- Cancellation Form -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Cancellation Details</h4>
<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 class="card-body">
</div>
<div class="panel-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
@ -129,7 +156,7 @@
<label class="col-form-label col-md-3">Cancellation Notes</label>
<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>
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 %}
@ -140,7 +167,9 @@
<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>
<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>
@ -179,24 +208,48 @@
<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.')">
<button type="button" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#confirm-cancellation">
<i class="fa fa-times me-2"></i>Cancel Appointment
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-sm btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
<div>
<div class="modal fade" id="confirm-cancellation" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-gradient-danger text-white">
<h5 class="modal-title" id="cancelModalLabel">Confirm Cancellation</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-0">Are you sure you want to cancel this appointment? This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger" id="confirmDelete">Confirm</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- Cancellation Policy -->
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">Cancellation Policy</h4>
</div>
<div class="card-body">
<div class="col-xl-4">
<!-- Cancellation Policy -->
<div class="panel panel-inverse mb-4" data-sortable-id="index-3">
<div class="panel-heading bg-gradient-danger">
<h4 class="panel-title">
<i class="fas fa-calendar-times"></i> Cancellation Policy
</h4>
</div>
<div class="panel-body">
<ul class="mb-0">
<li>Appointments cancelled with less than 24 hours notice may incur a cancellation fee</li>
<li>Emergency cancellations are exempt from cancellation fees</li>
@ -208,8 +261,8 @@
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
@ -220,13 +273,6 @@ $(document).ready(function() {
var now = new Date();
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
$('select[name="cancellation_reason"]').change(function() {

View File

@ -4,8 +4,8 @@
{% block title %}Check In Patient{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-10">
<ul class="breadcrumb">
@ -306,8 +306,8 @@
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@ -4,8 +4,8 @@
{% block title %}Confirm Appointment{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-8">
<ul class="breadcrumb">
@ -250,8 +250,8 @@
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@ -4,28 +4,18 @@
{% 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> Appointment Dashboard
<i class="fas fa-calendar-alt"></i> Appointment<span class="fw-light">Dashboard</span>
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-calendar-plus"></i> Schedule
</button>
<button type="button" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-users"></i> Queue
</button>
</div>
<button type="button" class="btn btn-sm btn-primary">
<i class="fas fa-video"></i> Telemedicine
</button>
<p class="text-muted">View your appointments, manage queues, and track your progress.</p>
</div>
</div>
<!-- Appointment Statistics -->
<div id="appointment-stats"
hx-get="{% url 'appointments:appointment_stats' %}"
hx-trigger="load, every 30s"
hx-trigger="load, every 60s"
class="auto-refresh mb-4">
<div class="htmx-indicator">
<div class="spinner-border spinner-border-sm" role="status">
@ -43,11 +33,11 @@
<i class="fas fa-calendar-day"></i> Today's Appointments
</h4>
<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="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>
<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-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">
@ -149,11 +139,11 @@
<i class="fas fa-users"></i> Active Queues
</h4>
<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="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>
<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-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">
@ -174,14 +164,6 @@
<br>No active queues.
</div>
{% 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>
@ -193,22 +175,31 @@
<i class="fas fa-bolt"></i> Quick Actions
</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>
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="d-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
</a>
<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 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>
</div>
</div>

View File

@ -4,8 +4,8 @@
{% block title %}Mark No Show{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-8">
<ul class="breadcrumb">
@ -274,8 +274,8 @@
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% 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="col-md-2 mb-3">
<div class="card stat-card bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h4 class="card-title">{{ stats.total_appointments }}</h4>
<p class="card-text">Total Appointments</p>
</div>
<i class="fas fa-calendar-alt fa-2x opacity-75"></i>
<div class="col-lg-2 col-sm-4">
<div class="widget widget-stats bg-blue mb-7px">
<div class="stats-icon stats-icon-lg"><i class="fas fa-calendar-alt fa-fw"></i></div>
<div class="stats-content">
<div class="stats-title">Total Appointments</div>
<div class="stats-number">{{ stats.total_appointments }}</div>
<div class="stats-desc">Better than last week (40.5%)</div>
</div>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="card stat-card bg-info text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h4 class="card-title">{{ stats.todays_appointments }}</h4>
<p class="card-text">Today's Appointments</p>
</div>
<i class="fas fa-calendar-day fa-2x opacity-75"></i>
<div class="col-lg-2 col-sm-4">
<div class="widget widget-stats bg-info mb-7px">
<div class="stats-icon stats-icon-lg"><i class="fas fa-calendar-day fa-fw"></i></div>
<div class="stats-content">
<div class="stats-title">Today's Appointments</div>
<div class="stats-number">{{ stats.total_appointments_today }}</div>
<div class="stats-desc">Better than last week (40.5%)</div>
</div>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="card stat-card bg-warning text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h4 class="card-title">{{ stats.pending_appointments }}</h4>
<p class="card-text">Pending</p>
</div>
<i class="fas fa-clock fa-2x opacity-75"></i>
<div class="col-lg-2 col-sm-4">
<div class="widget widget-stats bg-warning mb-7px">
<div class="stats-icon stats-icon-lg"><i class="fas fa-clock fa-fw"></i></div>
<div class="stats-content">
<div class="stats-title">Pending</div>
<div class="stats-number">{{ stats.pending_appointments }}</div>
<div class="stats-desc">Better than last week (40.5%)</div>
</div>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="card stat-card bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h4 class="card-title">{{ stats.completed_today }}</h4>
<p class="card-text">Completed Today</p>
</div>
<i class="fas fa-check-circle fa-2x opacity-75"></i>
<div class="col-lg-2 col-sm-4">
<div class="widget widget-stats bg-success mb-7px">
<div class="stats-icon stats-icon-lg"><i class="fas fa-check-circle fa-fw"></i></div>
<div class="stats-content">
<div class="stats-title">Completed Today</div>
<div class="stats-number">{{ stats.completed_appointments }}</div>
<div class="stats-desc">Better than last week (40.5%)</div>
</div>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="card stat-card bg-secondary text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h4 class="card-title">{{ stats.total_in_queue }}</h4>
<p class="card-text">In Queue</p>
</div>
<i class="fas fa-users fa-2x opacity-75"></i>
<div class="col-lg-2 col-sm-4">
<div class="widget widget-stats bg-danger mb-7px">
<div class="stats-icon stats-icon-lg"><i class="fas fa-users fa-fw"></i></div>
<div class="stats-content">
<div class="stats-title">Active Queue</div>
<div class="stats-number">{{ stats.active_queues }}</div>
<div class="stats-desc">Better than last week (40.5%)</div>
</div>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="card stat-card bg-dark text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h4 class="card-title">{{ stats.telemedicine_today }}</h4>
<p class="card-text">Telemedicine</p>
</div>
<i class="fas fa-video fa-2x opacity-75"></i>
</div>
<div class="col-lg-2 col-sm-4">
<div class="widget widget-stats bg-dark mb-7px">
<div class="stats-icon stats-icon-lg"><i class="fas fa-video fa-fw"></i></div>
<div class="stats-content">
<div class="stats-title">Telemedicine</div>
<div class="stats-number">{{ stats.telemedicine_sessions }}</div>
<div class="stats-desc">Better than last week (40.5%)</div>
</div>
</div>
</div>

View File

@ -1,39 +1,50 @@
<div class="accordion" id="accordion">
{% for log in contact_logs %}
<div class="contact-log-item">
<div class="d-flex justify-content-between align-items-center mb-1">
<h6 class="mb-0">
<i class="fas fa-{{ log.contact_method|lower }} me-1"></i>
{{ log.get_contact_method_display }} -
<span class="badge bg-{% if log.contact_outcome == 'SUCCESSFUL' %}success{% elif log.contact_outcome == 'DECLINED' %}danger{% else %}info{% endif %}">
<div class="accordion-item border-0">
<div class="accordion-header mb-2" id="heading-{{ log.id }}">
<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 }}">
{{ log.contact_date|date:"M d, Y H:i" }}
</button>
</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 }}
</span>
</h6>
<small class="text-muted">{{ log.contact_date|date:"M d, Y H:i" }}</small>
</div>
<p class="mb-1">{{ log.notes|default:"No notes." }}</p>
{% if log.appointment_offered %}
<p class="mb-1 text-primary">
<i class="fas fa-calendar-check me-1"></i>Appointment Offered:
<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>
<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>
</div>
</div>
{% endif %}
<div class="row mb-1">
<div class="col-md-6">
{% 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>
<small class="text-muted">
<span class="fw-bold">Contacted by:</span> {{ log.contacted_by.get_full_name }}
</small>
{% endif %}
<small class="text-muted">Contacted by: {{ log.contacted_by.get_full_name|default:"N/A" }}</small>
</div>
</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>
{% 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>
<p class="mb-0">No contact logs available for this entry.</p>
</div>
</div>
{% endfor %}
</div>

View File

@ -183,7 +183,6 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<!-- Page Header -->
<div class="d-flex align-items-center mb-3">
<div>
@ -367,7 +366,7 @@
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}

View File

@ -156,7 +156,7 @@
}
.wait-time-number {
font-size: 2.5rem;
font-size: 1.5rem;
font-weight: bold;
color: #495057;
}
@ -200,22 +200,16 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="d-flex align-items-center mb-3">
<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">
<i class="fas fa-user-clock me-2"></i>Queue Entry Details
</h1>
</div>
<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
</a>
</div>

View File

@ -3,9 +3,8 @@
{% block title %}{% if form.instance.pk %}Edit{% else %}Add to{% endif %} Queue Entry{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/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" />
{% block css %}
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<style>
.form-section {
background: white;
@ -151,25 +150,17 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<!-- Page Header -->
<div class="d-flex align-items-center mb-3">
<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">
<i class="fas fa-user-plus me-2"></i>
{% if form.instance.pk %}Edit Queue Entry{% else %}Add Patient to Queue{% endif %}
</h1>
</div>
<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
</a>
</div>
@ -465,7 +456,7 @@
{% endif %}
</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
</a>
<button type="submit" class="btn btn-primary">
@ -476,11 +467,11 @@
</div>
</div>
</form>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
{% block js %}
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
<script>
$(document).ready(function() {

View File

@ -130,27 +130,24 @@
{% endblock %}
{% block content %}
<div class="container">
<!-- 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>
<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:waiting_queue_list' %}">Waiting Queues</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_detail' queue.pk %}">{{ queue.name }}</a></li>
<li class="breadcrumb-item active">Delete</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-trash me-2"></i>Delete Waiting Queue
<h1 class="h2">
<i class="fas fa-trash me-2"></i>Delete<span class="fw-light">Queue</span>
</h1>
<p class="text-muted">View your appointments, manage queues, and track your progress.</p>
</div>
<div class="ms-auto">
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="{% url 'appointments:waiting_queue_detail' queue.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Queue
</a>
</div>
</div>
</div>
<div class="container">
<!-- Delete Warning -->
<div class="delete-warning text-center">

View File

@ -6,334 +6,186 @@
{% block css %}
<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/sweetalert2/css/sweetalert2.all.min.css' %}" rel="stylesheet" />
<style>
.queue-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.5rem;
padding: 2rem;
margin-bottom: 2rem;
/* The modal window itself */
.swal2-popup {
border-radius: 1.5rem;
background: #f8f9fa;
font-family: 'Tajawal', sans-serif; /* Example: Arabic-friendly font */
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
}
.stat-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
color: white;
font-size: 1.5rem;
}
.stat-icon.primary { background: #007bff; }
.stat-icon.success { background: #28a745; }
.stat-icon.warning { background: #ffc107; }
.stat-icon.info { background: #17a2b8; }
.stat-icon.danger { background: #dc3545; }
.stat-number {
font-size: 2.5rem;
/* Title */
.swal2-title {
font-size: 20px;
font-weight: bold;
color: #495057;
margin-bottom: 0.5rem;
{#color: #0b505d;#}
}
.stat-label {
color: #6c757d;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
/* Confirm button */
.swal2-confirm {
background-color: #155724 !important;
color: #fff !important;
border-radius: 8px !important;
}
.queue-info-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
/* Cancel button */
.swal2-cancel {
background-color: #adb5bd !important;
color: #fff !important;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #f8f9fa;
/* Icon color override */
.swal2-icon.swal2-warning {
border-color: #f59c1a !important;
color: #f59c1a !important;
}
.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>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<!-- 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>
<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:waiting_queue_list' %}">Waiting Queues</a></li>
<li class="breadcrumb-item active">{{ queue.name }}</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-users me-2"></i>Queue Details
<h1 class="h2">
<i class="fas fa-users me-2"></i> Queue<span class="fw-light">Details</span>
</h1>
<p class="text-muted">View your appointments, manage queues, and track your progress.</p>
</div>
<div class="ms-auto">
<div class="btn-group">
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}" class="btn btn-warning">
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}" class="btn btn-sm btn-warning">
<i class="fas fa-edit me-1"></i>Edit Queue
</a>
<button class="btn btn-success" onclick="refreshQueue()">
<button class="btn btn-sm btn-success" onclick="refreshQueue()">
<i class="fas fa-sync-alt me-1"></i>Refresh
</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()">
<button class="btn btn-sm btn-danger" onclick="exportQueue()">
<i class="fas fa-download me-2"></i>Export Data
</a></li>
</ul>
</div>
</div>
</button>
</div>
</div>
</div>
<div class="container-fluid">
<!-- Queue Header -->
<div class="queue-header">
<div class="row align-items-center">
<div class="card mb-3">
<div class="card-body">
<div class="row">
<div class="col-md-8">
<h2 class="mb-2">{{ queue.name }}</h2>
<div class="d-flex align-items-center mb-3">
<span class="queue-type-badge type-{{ queue.queue_type|lower }} me-3">
<div class="align-items-center g-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 }}
</span>
<span class="status-badge status-{% if queue.is_active %}active{% else %}inactive{% endif %}">
{% 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 %}
</span>
</div>
{% if queue.description %}
<p class="mb-0 opacity-75">{{ queue.description }}</p>
{% endif %}
</div>
<div class="col-md-4 text-md-end">
<div class="text-white">
<div class="align-items-center g-3">
<div class="h4 mb-1">{{ queue.current_queue_size }}/{{ queue.max_queue_size }}</div>
<div class="opacity-75">Current Capacity</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="col-lg-4">
<div class="queue-info-card">
<h5 class="mb-3">
<!-- Queue Information -->
<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
</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">
<span class="info-label">Queue ID:</span>
<span class="info-value">{{ queue.queue_id }}</span>
<div class="row mb-1">
<div class="col-4 fw-bold">Queue ID:</div>
<div class="col-8">{{ queue.queue_id }}</div>
</div>
<div class="info-item">
<span class="info-label">Type:</span>
<span class="info-value">{{ queue.get_queue_type_display }}</span>
<div class="row mb-1">
<div class="col-4 fw-bold">Type:</div>
<div class="col-8">{{ queue.get_queue_type_display }}</div>
</div>
{% if queue.specialty %}
<div class="info-item">
<span class="info-label">Specialty:</span>
<span class="info-value">{{ queue.specialty }}</span>
<div class="row mb-1">
<div class="col-4 fw-bold">Specialty:</div>
<div class="col-8">{{ queue.specialty }}</div>
</div>
{% endif %}
{% if queue.location %}
<div class="info-item">
<span class="info-label">Location:</span>
<span class="info-value">{{ queue.location }}</span>
<div class="row mb-1">
<div class="col-4 fw-bold">Location:</div>
<div class="col-8">{{ queue.location }}</div>
</div>
{% endif %}
<div class="info-item">
<span class="info-label">Max Capacity:</span>
<span class="info-value">{{ queue.max_queue_size }} patients</span>
<div class="row mb-1">
<div class="col-4 fw-bold">Max Capacity:</div>
<div class="col-8">{{ queue.max_queue_size }} patients</div>
</div>
<div class="info-item">
<span class="info-label">Accepting Patients:</span>
<span class="info-value">
<div class="row mb-1">
<div class="col-4 fw-bold">Accepting Patients:</div>
<div class="col-8">
{% if queue.is_accepting_patients %}
<i class="fas fa-check text-success"></i> Yes
{% else %}
<i class="fas fa-times text-danger"></i> No
{% endif %}
</span>
</div>
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">{{ queue.created_at|date:"M d, Y g:i A" }}</span>
</div>
<div class="info-item">
<span class="info-label">Last Updated:</span>
<span class="info-value">{{ queue.updated_at|timesince }} ago</span>
</div>
</div>
<div class="row mb-1">
<div class="col-4 fw-bold">Created:</div>
<div class="col-8">{{ queue.created_at|date:"M d, Y g:i A" }}</div>
</div>
<div class="row mb-1">
<div class="col-4 fw-bold">Last Updated:</div>
<div class="col-8">{{ queue.updated_at|timesince }} ago</div>
</div>
</div>
</div>
<!-- Assigned Providers -->
<div class="queue-info-card">
<h5 class="mb-3">
<div class="panel panel-inverse mb-4" data-sortable-id="index-2">
<div class="panel-heading">
<h4 class="panel-title">
<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 %}
<div class="d-flex align-items-center mb-2">
<div class="avatar avatar-sm me-2">
@ -349,24 +201,35 @@
{% endfor %}
</div>
</div>
</div>
<div class="col-lg-8">
<!-- Current Queue -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<div class="panel panel-inverse mb-4" data-sortable-id="index-3">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-list me-2"></i>Current Queue
</h5>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="callNextPatient()">
<i class="fas fa-phone me-1"></i>Call Next
</button>
<button class="btn btn-outline-success" onclick="addPatientToQueue()">
</h4>
<div class="panel-heading-btn">
<a href="{% url 'appointments:queue_entry_create' %}" class="btn btn-xs btn-outline-success me-2">
<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 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 class="card-body">
<div class="panel-body">
<div class="table-responsive">
<table id="queueTable" class="table table-striped table-bordered align-middle">
<thead>
@ -393,7 +256,7 @@
</div>
<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>
</td>
@ -409,7 +272,19 @@
<span class="fw-bold">{{ entry.joined_at|timesince }}</span>
</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 }}
</span>
</td>
@ -450,11 +325,12 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<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-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/sweetalert2/js/sweetalert2.all.min.js' %}"></script>
<script>
$(document).ready(function() {
@ -462,49 +338,70 @@ $(document).ready(function() {
$('#queueTable').DataTable({
responsive: true,
pageLength: 25,
order: [[0, 'asc']], // Sort by position
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)"
}
order: [[0, 'asc']],
});
// Auto-refresh every 30 seconds
// Auto-refresh every 60 seconds
setInterval(function() {
refreshQueue();
}, 30000);
}, 60000);
});
function refreshQueue() {
location.reload();
}
function callNextPatient() {
if (confirm('Call the next patient in queue?')) {
$.post('{% url "appointments:call_next_patient" queue.pk %}', {
csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()
}).done(function(response) {
if (response.success) {
showAlert('success', 'Next patient called successfully');
setTimeout(function() {
location.reload();
}, 1000);
} else {
showAlert('error', response.message || 'Failed to call next patient');
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}).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) {#}
{# if (confirm('Call this 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() {#}
{# if (confirm('Pause this queue? No new patients will be accepted.')) {#}
{# $.post('{% url "appointments:pause_queue" queue.pk %}', {#}
@ -592,26 +484,6 @@ function callNextPatient() {
{# 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>
{% endblock %}

View File

@ -123,7 +123,7 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="d-flex align-items-center mb-3">
<div>
@ -464,7 +464,7 @@
<script>
$(document).ready(function() {
// Initialize Select2
$('#id_providers').select2({
$('.form-select').select2({
theme: 'bootstrap-5',
placeholder: 'Select providers...',
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-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<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 {
text-align: center;
padding: 0.5rem;
@ -46,7 +19,7 @@
}
.stat-label {
font-size: 0.75rem;
font-size: 0.5rem;
color: #6c757d;
text-transform: uppercase;
}
@ -85,148 +58,23 @@
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- 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>
<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 active">Waiting Queues</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-users me-2"></i>Waiting Queues Management
<h1 class="h2">
<i class="fas fa-calendar-alt"></i> Queue<span class="fw-light">Management</span>
</h1>
<p class="text-muted">Manage queues and track patient's journey.</p>
</div>
<div class="ms-auto">
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="{% url 'appointments:waiting_queue_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>Create Queue
</a>
</div>
</div>
<!-- Statistics Overview -->
<div class="row mb-4">
<div class="col-lg-3 col-md-6">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h4 class="mb-0">{{ stats.total_queues|default:0 }}</h4>
<p class="mb-0">Total Queues</p>
</div>
<div 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>
<div class="container-fluid">
<div class="row" id="queue-cards">
{% for queue in queues %}
<div class="col-lg-6 col-xl-4 "
@ -254,15 +102,15 @@
</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>
<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="queue-stats">
<div class="d-flex justify-content-between align-items-center p-3">
<div class="stat-item">
<div class="stat-number">{{ queue.current_queue_size }}</div>
<div class="stat-label">Waiting</div>
@ -298,30 +146,30 @@
<div class="mb-3">
<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 class="d-flex justify-content-between align-items-center">
<small class="text-muted">
Updated: {{ queue.updated_at|timesince }} ago
{{ _("Updated") }}: {{ queue.updated_at|timesince }} {{ _("ago") }}
</small>
<div class="btn-group btn-group-sm">
<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>
</a>
<a href="{% url 'appointments:queue_entry_list' %}?queue={{ queue.pk }}"
class="btn btn-outline-info" title="View Entries">
<i class="fas fa-list"></i>
</a>
{# <a href="{% url 'appointments:queue_entry_list' %}?queue={{ queue.pk }}"#}
{# class="btn btn-outline-info" title="View Entries">#}
{# <i class="fas fa-list"></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>#}
<a href="{% url 'appointments:waiting_queue_delete' queue.pk %}"
class="btn btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</a>
</div>
</div>
</div>
@ -331,95 +179,15 @@
<div class="col-12">
<div class="text-center py-5">
<i class="fas fa-users fa-4x text-muted mb-3"></i>
<h4 class="text-muted">No Waiting Queues Found</h4>
<p class="text-muted">Create your first waiting queue to start managing patient flow.</p>
<h4 class="text-muted">{{ _("No Waiting Queues Found")}}</h4>
<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">
<i class="fas fa-plus me-1"></i>Create First Queue
<i class="fas fa-plus me-1"></i>{{ _("Create First Queue")}}
</a>
</div>
</div>
{% endfor %}
</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>
{% endblock %}
@ -428,7 +196,7 @@
<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-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>
$(document).ready(function() {
@ -438,55 +206,12 @@ $(document).ready(function() {
order: [[0, 'asc']],
});
// Auto-refresh queue stats every 30 seconds
// Auto-refresh queue stats every 60 seconds
setInterval(function() {
location.reload();
}, 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) {#}
{# window.location.href = `{% url 'appointments:waiting_queue_export' %}?format=${format}`;#}

View File

@ -4,56 +4,56 @@
{% block title %}Queue Management - {{ block.super }}{% endblock %}
{% 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>
<h1 class="h2">
<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-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
</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
</button>
</div>
<button type="button" class="btn btn-sm btn-primary">
<button type="button" class="btn btn-outline-primary">
<i class="fas fa-video"></i> Telemedicine
</button>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="panel panel-inverse">
<div class="row">
{% for queue in queues %}
{% if queue %}
<div class="col-lg-6 col-xl-4 mb-4">
<div class="panel panel-inverse" data-sortable-id="index-{{ queue.queue_id }}">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-users me-2"></i>Active Queues
{{ queue.name }}
<span class="badge bg-primary ms-2">{{ queue.current_queue_size }}</span>
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row">
{% for queue in queues %}
{% if queue %}
<div class="col-lg-6 col-xl-4 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">{{ queue.name }}</h5>
<span class="badge bg-primary">{{ queue.current_queue_size }}</span>
</div>
<div class="card-body"
<div class="panel-body"
hx-get="{% url 'appointments:queue_status' queue.pk %}"
hx-trigger="load, every 60s">
<div class="text-center">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
<div class="card-footer">
<div class="panel-footer">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
Avg Wait: {{ queue.wait_time_minutes }}
@ -83,8 +83,6 @@
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -4,15 +4,16 @@
{% block title %}Appointment Details - {{ block.super }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-3">
<div class="col">
<h1>Appointment Details</h1>
<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> Appointment<span class="fw-light">Details</span>
</h1>
<p class="text-muted">{{ appointment.scheduled_datetime|date:"M d, Y H:i" }} • {{ appointment.get_status_display }}</p>
</div>
<div class="col-auto">
<div class="btn-group">
{% if appointment.status == 'PENDING' %}
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
{% if appointment.status in 'CONFIRMED, SCHEDULED' %}
<button class="btn btn-success"
hx-post="{% url 'appointments:check_in_patient' appointment.id %}"
hx-confirm="Check in this patient?"
@ -28,63 +29,114 @@
</button>
</div>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<!-- Appointment Information -->
<div class="col-lg-6">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-calendar me-2"></i>Appointment Information</h5>
<div class="panel panel-inverse" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-calendar me-2"></i>Appointment Information
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<table class="table table-sm">
<tr><td>Date & Time</td><td>{{ appointment.scheduled_datetime|date:"M d, Y H:i" }}</td></tr>
<tr><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>
<tr>
<td>Date & Time</td>
<td>{{ appointment.scheduled_datetime|date:"M d, Y H:i" }}</td>
</tr>
<tr>
<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' %}
<span class="badge bg-warning">{{ appointment.get_status_display }}</span>
<span class="badge bg-warning">
{% 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' %}
<span class="badge bg-primary">{{ appointment.get_status_display }}</span>
<span class="badge bg-primary">
{% elif appointment.status == 'IN_PROGRESS' %}
<span class="badge bg-success">{{ appointment.get_status_display }}</span>
<span class="badge bg-success">
{% elif appointment.status == 'COMPLETED' %}
<span class="badge bg-success">{{ appointment.get_status_display }}</span>
<span class="badge bg-success">
{% elif appointment.status == 'CANCELLED' %}
<span class="badge bg-danger">{{ appointment.get_status_display }}</span>
<span class="badge bg-danger">
{% elif appointment.status == 'NO_SHOW' %}
<span class="badge bg-secondary">{{ appointment.get_status_display }}</span>
<span class="badge bg-secondary">
{% endif %}
</td></tr>
{{ appointment.get_status_display }}</span>
</td>
</tr>
{% 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 %}
</table>
</div>
</div>
{% if appointment.chief_complaint %}
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-stethoscope me-2"></i>Chief Complaint</h5>
<div class="panel panel-inverse" data-sortable-id="index-2">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-stethoscope me-2"></i>Chief Complaint
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<p>{{ appointment.chief_complaint }}</p>
</div>
</div>
{% endif %}
{% if appointment.notes %}
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-sticky-note me-2"></i>Notes</h5>
<div class="panel panel-inverse" data-sortable-id="index-3">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-sticky-note me-2"></i>Notes
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<p>{{ appointment.notes|linebreaks }}</p>
</div>
</div>
@ -93,34 +145,71 @@
<!-- Patient & Provider Information -->
<div class="col-lg-6">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-user me-2"></i>Patient Information</h5>
</div>
<div class="card-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:"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">
<div class="panel panel-inverse" data-sortable-id="index-4">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-user me-2"></i>Patient Information
</h4>
<div class="panel-heading-btn">
<a href="{% url 'patients:patient_detail' appointment.patient.id %}" class="btn btn-xs btn-outline-primary me-2">
<i class="fas fa-eye me-1"></i>View Patient
</a>
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<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>
{% if appointment.provider %}
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-user-md me-2"></i>Provider Information</h5>
<div class="panel panel-inverse" data-sortable-id="index-5">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-user-md me-2"></i>Provider Information
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<table class="table table-sm">
<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>
@ -133,11 +222,20 @@
{% endif %}
<!-- Appointment Timeline -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-clock me-2"></i>Timeline</h5>
<div class="panel panel-inverse" data-sortable-id="index-6">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-clock me-2"></i>Timeline
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<div class="timeline">
<div class="timeline-item">
<strong>Created:</strong> {{ appointment.created_at|date:"M d, Y H:i" }}
@ -146,9 +244,9 @@
{% endif %}
</div>
{% if appointment.confirmed_at %}
{% if appointment.scheduled_datetime %}
<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 %}
<br><small class="text-muted">by {{ appointment.confirmed_by.get_full_name }}</small>
{% endif %}

View File

@ -357,7 +357,7 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
$('#{{form.provider.id_for_label}}').select2({
$('.form-select').select2({
}).on('select2:select', function (e) {
loadAvailableSlots();

View File

@ -1,18 +1,34 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Appointments - Hospital Management System{% endblock %}
{% block css %}
{% 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> 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="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">
<h4 class="panel-title">
<i class="fas fa-calendar-alt"></i> Appointments
</h4>
<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-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>
@ -93,9 +109,6 @@
</form>
</div>
</div>
<!-- Appointment List -->
<div class="card" id="appointment-list">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
@ -231,14 +244,12 @@
<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"
@ -277,14 +288,13 @@
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'plugins/sweetalert/dist/sweetalert.min.js' %}"></script>
<script>
</script>
{% endblock %}

View File

@ -4,13 +4,13 @@
{% block title %}Appointment Reminders{% endblock %}
{% block css %}
<link href="{% static 'assets/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-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="container">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li>
@ -276,9 +276,9 @@
</div>
</div>
</div>
</div>
</div>
<!-- Send Reminders Modal -->
<div class="modal fade" id="sendRemindersModal" tabindex="-1">
<div class="modal-dialog">
@ -347,10 +347,10 @@
{% endblock %}
{% block js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'assets/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 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.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-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script>
$(document).ready(function() {

View File

@ -3,11 +3,16 @@
{% block title %}Reschedule Appointment{% endblock %}
{% block css %}
{% 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 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> 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-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary">
@ -16,11 +21,11 @@
<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 class="container-fluid">
<div class="row">
@ -176,7 +181,7 @@
<div class="panel-body">
<div id="availableSlots"
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-swap="innerHTML"
hx-include="#new_date,#new_provider"
@ -187,8 +192,8 @@
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@ -21,7 +21,7 @@
<label for="provider-filter" class="form-label">Provider</label>
<select id="provider-filter" class="form-select">
<option value="">All Providers</option>
{% for provider in providers %}
{% for provider in appointments.providers.all %}
<option value="{{ provider.id }}">{{ provider.get_full_name }}</option>
{% endfor %}
</select>

View File

@ -499,7 +499,6 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="availability-header fade-in">
@ -844,7 +843,7 @@
</div>
</div>
</div>
</div>
<!-- Bulk Create Modal -->
<div class="modal fade" id="bulkCreateModal" tabindex="-1">

View File

@ -539,7 +539,6 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="booking-header fade-in">
@ -930,7 +929,7 @@
</div>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay">

View File

@ -572,7 +572,6 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="calendar-header fade-in">
@ -867,7 +866,6 @@
</div>
</div>
</div>
</div>
<!-- Slot Details Modal -->
<div class="modal fade" id="slotModal" tabindex="-1">

View File

@ -418,7 +418,7 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="row">
@ -712,7 +712,7 @@
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}

View File

@ -391,8 +391,8 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<div class="container-fluid">
<!-- Page Header -->
<div class="slot-detail-header fade-in">
<div class="row align-items-center">
@ -793,9 +793,9 @@
<i class="fas fa-arrow-left"></i>Back to Slots
</a>
</div>
</div>
</div>
<!-- Book Appointment Modal -->
<div class="modal fade" id="bookAppointmentModal" tabindex="-1">
<div class="modal-dialog">

View File

@ -402,7 +402,6 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="slot-form-header fade-in">
@ -806,7 +805,7 @@
</form>
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@ -302,7 +302,6 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="slot-management-header fade-in">
@ -583,7 +582,7 @@
<div id="slotCalendar"></div>
</div>
</div>
</div>
<!-- Book Slot Modal -->
<div class="modal fade" id="bookSlotModal" tabindex="-1">

View File

@ -591,7 +591,6 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="management-header fade-in">
@ -921,7 +920,6 @@
</div>
</div>
</div>
</div>
<!-- Bulk Create Modal -->
<div class="modal fade" id="bulkCreateModal" tabindex="-1">

View File

@ -109,10 +109,10 @@
</a>
{% endif %}
<button class="btn btn-outline-info"
<a href="{% url 'appointments:telemedicine_session_detail' session.pk %}" class="btn btn-outline-info"
title="View Details">
<i class="fas fa-eye"></i>
</button>
</a>
{% if session.status not in 'COMPLETED,CANCELLED' %}
<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 content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<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 active">Appointment Templates</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
Appointment Templates
<small>Reusable configurations for common appointment types</small>
</h1>
<!-- END 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>
<h1 class="h2">
<i class="fas fa-book"></i> Appointment<span class="fw-light">Templates</span>
</h1>
<p class="text-muted">Reusable configurations for common appointment types.</p>
</div>
{# <div class="btn-toolbar mb-2 mb-md-0">#}
{# <div class="btn-group me-2">#}
{# <a href="{% url 'appointments:waiting_list_edit' entry.pk %}" class="btn btn-warning">#}
{# <i class="fas fa-edit me-1"></i>Edit Entry#}
{# </a>#}
{# <a href="{% url 'appointments:waiting_list_delete' entry.pk %}" class="btn btn-danger">#}
{# <i class="fas fa-trash me-1"></i>Delete Entry#}
{# </a>#}
{# <a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-secondary">#}
{# <i class="fas fa-arrow-left me-1"></i>Back to List#}
{# </a>#}
{# </div>#}
{# </div>#}
</div>
<div class="container-fluid">
<div class="row">
<div class="col-xl-12">
<div class="panel panel-inverse">
@ -51,7 +56,7 @@
<div class="row g-2">
<div class="col-md-3">
<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 class="col-md-3">
@ -66,7 +71,7 @@
<div class="col-md-3">
<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 class="col-md-3">
@ -163,7 +168,7 @@
</td>
<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>
</td>
@ -230,4 +235,5 @@
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -27,21 +27,14 @@
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
<li class="breadcrumb-item active">Waiting List</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-clock text-primary me-2"></i>
Patient Waiting List Management
<small class="text-muted ms-2">Manage appointment waiting list and patient queue</small>
</h1>
<!-- END 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>
<h1 class="h2">
<i class="fas fa-clock"></i> Waiting List<span class="fw-light">Management</span>
</h1>
<p class="text-muted">Manage appointment waiting list and patient queue.</p>
</div>
</div>
<!-- BEGIN statistics cards -->
<div class="row mb-4">
@ -99,7 +92,7 @@
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<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>
</div>
<div class="flex-shrink-0">
@ -113,18 +106,18 @@
<!-- END statistics cards -->
<!-- BEGIN filter panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-filter me-2"></i>Filter Waiting List
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse">
<i class="fa fa-minus"></i>
</a>
</div>
</div>
<div class="panel-body">
{#<div class="panel panel-inverse">#}
{# <div class="panel-heading">#}
{# <h4 class="panel-title">#}
{# <i class="fas fa-filter me-2"></i>Filter Waiting List#}
{# </h4>#}
{# <div class="panel-heading-btn">#}
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse">#}
{# <i class="fa fa-minus"></i>#}
{# </a>#}
{# </div>#}
{# </div>#}
{# <div class="panel-body">#}
{# <form method="get" class="row g-3">#}
{# <div class="col-md-2">#}
{# {{ filter_form.department.label_tag }}#}
@ -158,8 +151,8 @@
{# </div>#}
{# </div>#}
{# </form>#}
</div>
</div>
{# </div>#}
{#</div>#}
<!-- END filter panel -->
<!-- BEGIN main panel -->
@ -170,10 +163,10 @@
<i class="fas fa-list me-2"></i>Waiting List Entries
</h4>
<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
</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
</button>
</div>

View File

@ -14,7 +14,7 @@
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<h1 class="h2">
<i class="fas fa-trash-alt text-danger me-2"></i>
Confirm Waiting List Entry Cancellation
<small class="text-muted ms-2">Permanently remove this patient from the waiting list</small>

View File

@ -3,83 +3,61 @@
{% block title %}Waiting List Entry Details{% endblock %}
{% block extra_css %}
<style>
.detail-section {
border-left: 4px solid #007bff;
padding-left: 1rem;
margin-bottom: 2rem;
}
.detail-section h5 {
color: #007bff;
}
.priority-badge {
font-size: 0.9rem;
padding: 0.4em 0.6em;
}
.status-badge {
font-size: 0.9rem;
padding: 0.4em 0.6em;
}
.contact-log-item {
border-bottom: 1px dashed #eee;
padding-bottom: 10px;
margin-bottom: 10px;
}
.contact-log-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
</style>
{% block css %}
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_list' %}">Waiting List</a></li>
<li class="breadcrumb-item active">Entry Details</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-info-circle text-primary me-2"></i>
Waiting List Entry Details
<small class="text-muted ms-2">Comprehensive view of patient waiting list entry</small>
</h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-user-tag me-2"></i>Patient: {{ entry.patient.get_full_name }}
</h4>
<div class="panel-heading-btn">
<a href="{% url 'appointments:waiting_list_edit' entry.pk %}" class="btn btn-warning btn-sm me-2">
<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> Waiting List<span class="fw-light">Entry Details</span>
</h1>
<p class="text-muted">Comprehensive view of patient waiting list entry.</p>
</div>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="{% url 'appointments:waiting_list_edit' entry.pk %}" class="btn btn-warning">
<i class="fas fa-edit me-1"></i>Edit Entry
</a>
<a href="{% url 'appointments:waiting_list_delete' entry.pk %}" class="btn btn-danger btn-sm me-2">
<a href="{% url 'appointments:waiting_list_delete' entry.pk %}" class="btn btn-danger">
<i class="fas fa-trash me-1"></i>Delete Entry
</a>
<a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-secondary btn-sm">
<a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to List
</a>
</div>
</div>
</div>
<!-- BEGIN panel -->
<div class="container-fluid">
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-user-tag me-2"></i>Patient: {{ entry.patient.get_full_name }}
</h4>
</div>
<div class="panel-body">
<div class="row">
<div class="col-lg-8">
<div class="col-lg-4">
<!-- Patient & Service Information -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-user me-2"></i>Patient & Service Information</h5>
<div class="panel panel-inverse border" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-user me-2"></i>Patient & Service Information
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Patient Name:</strong></div>
<div class="col-md-8">{{ entry.patient.get_full_name }} (MRN: {{ entry.patient.mrn|default:'N/A' }})</div>
<div class="col-md-8">{{ entry.patient.get_full_name }} (MRN: {{ entry.patient.mrn }})</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Department:</strong></div>
@ -98,41 +76,49 @@
<div class="col-md-8">{{ entry.get_specialty_display }}</div>
</div>
</div>
<!-- Clinical Priority & Urgency -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-exclamation-triangle me-2"></i>Clinical Priority & Urgency</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Priority Level:</strong></div>
<div class="col-md-8">
{% if entry.priority == 'EMERGENCY' %}
<span class="badge bg-danger priority-badge">{{ entry.get_priority_display }}</span>
{% elif entry.priority == 'STAT' %}
<span class="badge bg-warning priority-badge">{{ entry.get_priority_display }}</span>
{% elif entry.priority == 'URGENT' %}
<span class="badge bg-warning priority-badge">{{ entry.get_priority_display }}</span>
{% else %}
<span class="badge bg-success priority-badge">{{ entry.get_priority_display }}</span>
{% endif %}
</div>
<!-- Contact Information -->
<div class="panel panel-inverse border" data-sortable-id="index-2">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-phone me-2"></i>Contact Information
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Preferred Method:</strong></div>
<div class="col-md-8">{{ entry.get_contact_method_display }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Urgency Score:</strong></div>
<div class="col-md-8">{{ entry.urgency_score }} / 10</div>
<div class="col-md-4"><strong>Phone:</strong></div>
<div class="col-md-8">{{ entry.contact_phone|default:'N/A' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Clinical Indication:</strong></div>
<div class="col-md-8">{{ entry.clinical_indication|linebreaksbr }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Diagnosis Codes:</strong></div>
<div class="col-md-8">{{ entry.diagnosis_codes|join:", "|default:'N/A' }}</div>
<div class="col-md-4"><strong>Email:</strong></div>
<div class="col-md-8">{{ entry.contact_email|default:'N/A' }}</div>
</div>
</div>
</div>
<!-- Patient Scheduling Preferences -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-calendar-alt me-2"></i>Patient Scheduling Preferences</h5>
<div class="panel panel-inverse border" data-sortable-id="index-3">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-calendar-alt me-2"></i>Patient Scheduling Preferences
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Preferred Date:</strong></div>
<div class="col-md-8">{{ entry.preferred_date|date:"M d, Y"|default:'Any' }}</div>
@ -156,27 +142,66 @@
</div>
{% endif %}
</div>
<!-- Contact Information -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-phone me-2"></i>Contact Information</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Preferred Method:</strong></div>
<div class="col-md-8">{{ entry.get_contact_method_display }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Phone:</strong></div>
<div class="col-md-8">{{ entry.contact_phone|default:'N/A' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Email:</strong></div>
<div class="col-md-8">{{ entry.contact_email|default:'N/A' }}</div>
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Clinical Priority & Urgency -->
<div class="panel panel-inverse border" data-sortable-id="index-4">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-exclamation-triangle me-2"></i>Clinical Priority & Urgency
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Priority Level:</strong></div>
<div class="col-md-8">
{% if entry.priority == 'EMERGENCY' %}
<span class="badge bg-danger priority-badge">{{ entry.get_priority_display }}</span>
{% elif entry.priority == 'STAT' %}
<span class="badge bg-red priority-badge">{{ entry.get_priority_display }}</span>
{% elif entry.priority == 'URGENT' %}
<span class="badge bg-warning priority-badge">{{ entry.get_priority_display }}</span>
{% else %}
<span class="badge bg-success priority-badge">{{ entry.get_priority_display }}</span>
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Urgency Score:</strong></div>
<div class="col-md-8">{{ entry.urgency_score }} / 10</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Clinical Indication:</strong></div>
<div class="col-md-8">{{ entry.clinical_indication|linebreaksbr }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Diagnosis Codes:</strong></div>
<div class="col-md-8">{{ entry.diagnosis_codes|join:", " }}</div>
</div>
</div>
</div>
<!-- Special Requirements -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-universal-access me-2"></i>Special Requirements & Accommodations</h5>
<div class="panel panel-inverse border" data-sortable-id="index-5">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-universal-access me-2"></i>Special Requirements & Accommodations
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Interpreter Needed:</strong></div>
<div class="col-md-8">{% if entry.requires_interpreter %}Yes ({{ entry.interpreter_language|default:'N/A' }}){% else %}No{% endif %}</div>
@ -190,31 +215,21 @@
<div class="col-md-8">{{ entry.accessibility_requirements|default:'None'|linebreaksbr }}</div>
</div>
</div>
<!-- Insurance & Authorization -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-shield-alt me-2"></i>Insurance & Authorization</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Insurance Verified:</strong></div>
<div class="col-md-8">{% if entry.insurance_verified %}Yes{% else %}No{% endif %}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Authorization Required:</strong></div>
<div class="col-md-8">{% if entry.authorization_required %}Yes{% else %}No{% endif %}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Authorization Status:</strong></div>
<div class="col-md-8">{{ entry.get_authorization_status_display }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Authorization Number:</strong></div>
<div class="col-md-8">{{ entry.authorization_number|default:'N/A' }}</div>
</div>
</div>
<!-- Referral Information -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-user-md me-2"></i>Referral Information</h5>
<div class="panel panel-inverse border" data-sortable-id="index-6">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-user-md me-2"></i>Referral Information
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Referring Provider:</strong></div>
<div class="col-md-8">{{ entry.referring_provider|default:'N/A' }}</div>
@ -228,33 +243,145 @@
<div class="col-md-8">{{ entry.get_referral_urgency_display }}</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Waiting List Metrics -->
<div class="panel panel-inverse border" data-sortable-id="index-7">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-chart-bar me-2"></i>Waiting List Metrics
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Current Position:</strong></div>
<div class="text-primary">{{ entry.position|default:'N/A' }}</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Days Waiting:</strong></div>
<div class="text-info">{{ entry.days_waiting }} days</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Estimated Wait Time:</strong></div>
<div class="text-warning">{{ estimated_wait_time }} days</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Contact Attempts:</strong></div>
<div class="text-secondary">{{ entry.contact_attempts }}</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Overdue Contact:</strong></div>
<div class="">{% if entry.is_overdue_contact %}<span class="badge bg-danger">Yes</span>{% else %}<span class="badge bg-success">No</span>{% endif %}</div>
</div>
</div>
</div>
<!-- Contact Log -->
<div class="panel panel-inverse border" data-sortable-id="index-8">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-history me-2"></i>Contact Log
</h4>
<div class="panel-heading-btn">
<button type="button" class="btn btn-outline-theme btn-xs me-2" data-bs-toggle="modal" data-bs-target="#addContactLogModal">
<i class="fas fa-plus me-1"></i>Add Contact Log
</button>
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body" id="contact-log-container">
{% include 'appointments/partials/contact_log_list.html' %}
</div>
</div>
<!-- Insurance & Authorization -->
<div class="panel panel-inverse border" data-sortable-id="index-9">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-shield-alt me-2"></i>Insurance & Authorization
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Insurance Verified:</strong></div>
<div class="col-md-8">{{ entry.insurance_verified|yesno|capfirst }} </div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Authorization Required:</strong></div>
<div class="col-md-8">{{ entry.authorization_required|yesno|capfirst }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Authorization Status:</strong></div>
<div class="col-md-8">{{ entry.get_authorization_status_display }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Authorization Number:</strong></div>
<div class="col-md-8">{{ entry.authorization_number|default:'N/A' }}</div>
</div>
</div>
</div>
<!-- Additional Notes -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-sticky-note me-2"></i>Additional Notes</h5>
<div class="panel panel-inverse border" data-sortable-id="index-10">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-sticky-note me-2"></i>Additional Notes
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row mb-2">
<div class="col-md-12">{{ entry.notes|default:'No additional notes.'|linebreaksbr }}</div>
</div>
</div>
</div>
<!-- Outcome Tracking -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-check-circle me-2"></i>Outcome Tracking</h5>
<div class="panel panel-inverse border" data-sortable-id="index-11">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-check-circle me-2"></i>Outcome Tracking
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Status:</strong></div>
<div class="col-md-8">
{% if entry.status == 'ACTIVE' %}
<span class="badge bg-primary status-badge">{{ entry.get_status_display }}</span>
<span class="badge bg-primary">{{ entry.get_status_display }}</span>
{% elif entry.status == 'CONTACTED' %}
<span class="badge bg-info status-badge">{{ entry.get_status_display }}</span>
<span class="badge bg-info">{{ entry.get_status_display }}</span>
{% elif entry.status == 'OFFERED' %}
<span class="badge bg-warning status-badge">{{ entry.get_status_display }}</span>
<span class="badge bg-warning">{{ entry.get_status_display }}</span>
{% elif entry.status == 'SCHEDULED' %}
<span class="badge bg-success status-badge">{{ entry.get_status_display }}</span>
<span class="badge bg-success">{{ entry.get_status_display }}</span>
{% elif entry.status == 'CANCELLED' %}
<span class="badge bg-danger status-badge">{{ entry.get_status_display }}</span>
<span class="badge bg-danger">{{ entry.get_status_display }}</span>
{% else %}
<span class="badge bg-secondary status-badge">{{ entry.get_status_display }}</span>
<span class="badge bg-secondary">{{ entry.get_status_display }}</span>
{% endif %}
</div>
</div>
@ -289,17 +416,28 @@
</div>
{% endif %}
</div>
</div>
<!-- Metadata -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-database me-2"></i>Metadata</h5>
<div class="panel panel-inverse border" data-sortable-id="index-12">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-database me-2"></i>Metadata
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Entry ID:</strong></div>
<div class="col-md-8">{{ entry.waiting_list_id }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Created At:</strong></div>
<div class="col-md-8">{{ entry.created_at|date:"M d, Y H:i" }} by {{ entry.created_by.get_full_name|default:'N/A' }}</div>
<div class="col-md-8">{{ entry.created_at|date:"M d, Y H:i" }} by {{ entry.created_by.get_display_name }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Last Updated:</strong></div>
@ -307,121 +445,277 @@
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Waiting List Metrics -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0"><i class="fas fa-chart-bar me-2"></i>Waiting List Metrics</h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Current Position:</strong></div>
<div class="fs-4 text-primary">{{ entry.position|default:'N/A' }}</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Days Waiting:</strong></div>
<div class="fs-4 text-info">{{ entry.days_waiting }} days</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Estimated Wait Time:</strong></div>
<div class="fs-4 text-warning">{{ estimated_wait_time }} days</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Contact Attempts:</strong></div>
<div class="fs-4 text-secondary">{{ entry.contact_attempts }}</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Overdue Contact:</strong></div>
<div class="fs-4">{% if entry.is_overdue_contact %}<span class="badge bg-danger">Yes</span>{% else %}<span class="badge bg-success">No</span>{% endif %}</div>
</div>
</div>
</div>
<!-- Contact Log -->
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0"><i class="fas fa-history me-2"></i>Contact Log</h5>
</div>
<div class="card-body" id="contact-log-container">
{% include 'appointments/partials/contact_log_list.html' %}
</div>
<div class="card-footer">
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#addContactLogModal">
<i class="fas fa-plus me-1"></i>Add Contact Log
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END panel -->
<!-- Add Contact Log Modal -->
<div class="modal fade" id="addContactLogModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal fade" id="addContactLogModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title">Add Contact Log for {{ entry.patient.get_full_name }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h5 class="modal-title">
<i class="fas fa-phone-alt"></i>Add Contact Log for {{ entry.patient.get_full_name }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'appointments:add_contact_log' entry.pk %}" hx-post="{% url 'appointments:add_contact_log' entry.pk %}" hx-target="#contact-log-container" hx-swap="innerHTML">
<!-- Use hx-post to refresh the contact log list on success -->
<form
method="post"
action="{% url 'appointments:add_contact_log' entry.pk %}"
hx-post="{% url 'appointments:add_contact_log' entry.pk %}"
hx-target="#contact-log-container"
hx-swap="innerHTML"
class="needs-validation"
novalidate
>
{% csrf_token %}
<div class="modal-body">
{% for field in contact_form %}
<div class="mb-3">
<label class="form-label">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}<small class="form-text text-muted">{{ field.help_text }}</small>{% endif %}
{% for error in field.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
<!-- Top-level (non-field) errors -->
{% if contact_form.non_field_errors %}
<div class="alert alert-danger mb-3" role="alert">
<ul class="mb-0">
{% for err in contact_form.non_field_errors %}
<li>{{ err }}</li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% endif %}
<div class="row g-3">
<!-- Contact method -->
<div class="col-md-6">
<label class="form-label fw-semibold" for="{{ contact_form.contact_method.id_for_label }}">
Contact Method <span class="text-danger">*</span>
</label>
{{ contact_form.contact_method }}
{% if contact_form.contact_method.errors %}
<div class="invalid-feedback d-block">
{{ contact_form.contact_method.errors|striptags }}
</div>
<div class="modal-footer">
{% else %}
<div class="form-text">Call, SMS, email, etc.</div>
{% endif %}
</div>
<!-- Contact outcome -->
<div class="col-md-6">
<label class="form-label fw-semibold" for="{{ contact_form.contact_outcome.id_for_label }}">
Contact Outcome <span class="text-danger">*</span>
</label>
{{ contact_form.contact_outcome }}
{% if contact_form.contact_outcome.errors %}
<div class="invalid-feedback d-block">
{{ contact_form.contact_outcome.errors|striptags }}
</div>
{% else %}
<div class="form-text">Answered, no answer, voicemail, etc.</div>
{% endif %}
</div>
<!-- Appointment offered (toggle) -->
<div class="col-12">
<div class="form-check form-switch">
{{ contact_form.appointment_offered }}
<label class="form-check-label fw-semibold ms-1" for="{{ contact_form.appointment_offered.id_for_label }}">
Appointment offered?
</label>
</div>
{% if contact_form.appointment_offered.errors %}
<div class="invalid-feedback d-block">
{{ contact_form.appointment_offered.errors|striptags }}
</div>
{% endif %}
</div>
<!-- Offered date/time (conditional) -->
<div class="col-md-6 offered-fields">
<label class="form-label fw-semibold" for="{{ contact_form.offered_date.id_for_label }}">
Offered Date
</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-calendar-day"></i></span>
{{ contact_form.offered_date }}
</div>
{% if contact_form.offered_date.errors %}
<div class="invalid-feedback d-block">
{{ contact_form.offered_date.errors|striptags }}
</div>
{% else %}
<div class="form-text">Date of the offered appointment.</div>
{% endif %}
</div>
<div class="col-md-6 offered-fields">
<label class="form-label fw-semibold" for="{{ contact_form.offered_time.id_for_label }}">
Offered Time
</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-clock"></i></span>
{{ contact_form.offered_time }}
</div>
{% if contact_form.offered_time.errors %}
<div class="invalid-feedback d-block">
{{ contact_form.offered_time.errors|striptags }}
</div>
{% else %}
<div class="form-text">Time of the offered slot.</div>
{% endif %}
</div>
<!-- Patient response -->
<div class="col-md-6">
<label class="form-label fw-semibold" for="{{ contact_form.patient_response.id_for_label }}">
Patient Response
</label>
{{ contact_form.patient_response }}
{% if contact_form.patient_response.errors %}
<div class="invalid-feedback d-block">
{{ contact_form.patient_response.errors|striptags }}
</div>
{% else %}
<div class="form-text">Accepted, declined, call back, etc.</div>
{% endif %}
</div>
<!-- Next contact date -->
<div class="col-md-6">
<label class="form-label fw-semibold" for="{{ contact_form.next_contact_date.id_for_label }}">
Next Contact Date
</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-bell"></i></span>
{{ contact_form.next_contact_date }}
</div>
{% if contact_form.next_contact_date.errors %}
<div class="invalid-feedback d-block">
{{ contact_form.next_contact_date.errors|striptags }}
</div>
{% else %}
<div class="form-text">When to follow up if needed.</div>
{% endif %}
</div>
<!-- Notes -->
<div class="col-12">
<label class="form-label fw-semibold" for="{{ contact_form.notes.id_for_label }}">
Notes
</label>
{{ contact_form.notes }}
{% if contact_form.notes.errors %}
<div class="invalid-feedback d-block">
{{ contact_form.notes.errors|striptags }}
</div>
{% else %}
<div class="form-text">Conversation details and any context.</div>
{% endif %}
</div>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Contact Log</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i> Save Contact Log
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
{% block js %}
<script src="{% static 'plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize date picker for offered_date and next_contact_date
$("#addContactLogModal input[type='date']").datepicker({
format: 'yyyy-mm-dd',
autoclose: true,
todayHighlight: true,
startDate: 'today'
});
(function() {
// Bootstrap validation
document.addEventListener('submit', function(e) {
const form = e.target.closest('form.needs-validation');
if (form) {
if (!form.checkValidity()) {
e.preventDefault();
e.stopPropagation();
}
form.classList.add('was-validated');
}
}, true);
// Toggle offered date/time and patient response based on appointment_offered checkbox
$("input[name='appointment_offered']").change(function() {
const isChecked = $(this).is(':checked');
$("input[name='offered_date']").prop('required', isChecked);
$("input[name='offered_time']").prop('required', isChecked);
$("select[name='patient_response']").prop('required', isChecked);
}).trigger('change'); // Trigger on load for initial state
// Handle HTMX after swap to re-initialize datepickers if needed
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.detail.target.id === 'contact-log-container') {
$("#addContactLogModal input[type='date']").datepicker({
// Helpers
function initDatePickers(scope) {
// jQuery datepicker on any date input inside the modal
const $scope = scope ? $(scope) : $("#addContactLogModal");
$scope.find("input[type='date']").datepicker({
format: 'yyyy-mm-dd',
autoclose: true,
todayHighlight: true,
startDate: 'today'
});
}
function toggleOfferedFields(scope) {
const root = scope || document;
const offeredSwitch = root.querySelector('#{{ contact_form.appointment_offered.id_for_label }}');
const offeredFields = (root.querySelectorAll('.offered-fields')) || [];
if (!offeredSwitch) return;
const on = offeredSwitch.checked;
offeredFields.forEach(function(el) {
el.style.display = on ? '' : 'none';
const inputs = el.querySelectorAll('input, select, textarea');
inputs.forEach(function(input) {
input.required = !!on;
});
});
});
}
// Init on modal show
const modalEl = document.getElementById('addContactLogModal');
modalEl.addEventListener('shown.bs.modal', function() {
initDatePickers(modalEl);
toggleOfferedFields(modalEl);
});
// React to switch changes
modalEl.addEventListener('change', function(e) {
if (e.target && e.target.id === '{{ contact_form.appointment_offered.id_for_label }}') {
toggleOfferedFields(modalEl);
}
});
// Re-initialize after HTMX swaps (when the contact log list is refreshed)
document.body.addEventListener('htmx:afterSwap', function(evt) {
// if we just updated the contact log container, close the modal & reset form
if (evt.detail && evt.detail.target && evt.detail.target.id === 'contact-log-container') {
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
// Optional: reset the form for next use
const form = modalEl.querySelector('form');
if (form) {
form.reset();
form.classList.remove('was-validated');
toggleOfferedFields(modalEl);
initDatePickers(modalEl);
}
}
});
// Also handle server-sent HX-Trigger (recommended best practice)
document.body.addEventListener('contact-log:added', function() {
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
});
// Initial setup (if modal already visible)
document.addEventListener('DOMContentLoaded', function() {
initDatePickers(modalEl);
toggleOfferedFields(modalEl);
});
})();
</script>
{% endblock %}

View File

@ -3,19 +3,19 @@
{% block title %}{% if object %}Edit{% else %}Add{% endif %} Waiting List Entry{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
{% block css %}
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
<style>
.required-field::after {
content: " *";
color: #dc3545;
}
.form-section {
border-left: 4px solid #007bff;
padding-left: 1rem;
margin-bottom: 2rem;
}
{#.form-section {#}
{# border-left: 4px solid #007bff;#}
{# padding-left: 1rem;#}
{# margin-bottom: 2rem;#}
{# }#}
.priority-indicator {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
@ -148,7 +148,7 @@
<label class="form-label required-field">Urgency Score (1-10)</label>
{{ form.urgency_score }}
<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>
<small class="form-text text-muted">1 = Routine, 10 = Most Urgent</small>
{% if form.urgency_score.errors %}
@ -431,15 +431,15 @@
<!-- END form panel -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
{% block js %}
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize Select2 for dropdowns
$('.form-select').select2({
theme: 'bootstrap-5',
{#theme: 'bootstrap-5',#}
width: '100%'
});

View File

@ -9,7 +9,6 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
@ -269,7 +268,7 @@
</div>
</div>
</div>
</div>
<!-- Add to Waitlist Modal -->
<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('<int:pk>/requests/create/', views.AppointmentRequestCreateView.as_view(), name='appointment_create'),
path('requests/<int:pk>/detail/', views.AppointmentRequestDetailView.as_view(), name='appointment_detail'),
path('calendar/', views.SchedulingCalendarView.as_view(), name='scheduling_calendar'),
path('queue/', views.QueueManagementView.as_view(), name='queue_management'),
path('stats/', views.appointment_stats, name='appointment_stats'),
# 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
path('telemedicine/', views.TelemedicineView.as_view(), name='telemedicine'),
@ -25,19 +31,18 @@ urlpatterns = [
path('telemedicine/<int:pk>/end/', views.end_telemedicine_session, name='stop_telemedicine_session'),
path('telemedicine/<int:pk>/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
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'),
path('queue/list/', views.WaitingQueueListView.as_view(), name='waiting_queue_list'),
# Queue management
# path('queue/', views.QueueManagementView.as_view(), name='queue_management'),
path('queue/', views.WaitingQueueListView.as_view(), name='waiting_queue_list'),
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>/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/<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/<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/<int:pk>/',views.SlotAvailabilityDetailView.as_view(), name='slot_detail'),
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>/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/<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'),
# 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.
"""
from django.contrib.messages import success
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.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 (
TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView
)
from django.http import JsonResponse
from django.http import JsonResponse, HttpResponseBadRequest
from django.contrib import messages
from django.db.models.functions import Now
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
context['stats'] = {
'total_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
).count(),
'total_appointments_today': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today
).count(),
'confirmed_appointments': AppointmentRequest.objects.filter(
'pending_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today,
status='CONFIRMED'
status='PENDING'
).count(),
'active_queues_count': WaitingQueue.objects.filter(
tenant=tenant,
@ -179,7 +185,7 @@ class AppointmentRequestDetailView(LoginRequiredMixin, DetailView):
return context
class AppointmentRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
class AppointmentRequestCreateView(LoginRequiredMixin, CreateView):
"""
Create new appointment request.
"""
@ -239,7 +245,7 @@ class AppointmentRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin,
return response
class AppointmentRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
class AppointmentRequestUpdateView(LoginRequiredMixin, UpdateView):
"""
Update appointment request (limited fields after scheduling).
"""
@ -281,7 +287,7 @@ class AppointmentRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
return response
class AppointmentRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
class AppointmentRequestDeleteView(LoginRequiredMixin, DeleteView):
"""
Cancel appointment request.
"""
@ -394,7 +400,7 @@ class SlotAvailabilityDetailView(LoginRequiredMixin, DetailView):
return context
class SlotAvailabilityCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
class SlotAvailabilityCreateView(LoginRequiredMixin, CreateView):
"""
Create new slot availability.
"""
@ -428,7 +434,7 @@ class SlotAvailabilityCreateView(LoginRequiredMixin, PermissionRequiredMixin, Cr
return response
class SlotAvailabilityUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
class SlotAvailabilityUpdateView(LoginRequiredMixin, UpdateView):
"""
Update slot availability.
"""
@ -470,7 +476,7 @@ class SlotAvailabilityUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Up
return response
class SlotAvailabilityDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
class SlotAvailabilityDeleteView(LoginRequiredMixin, DeleteView):
"""
Delete slot availability.
"""
@ -599,6 +605,8 @@ class WaitingQueueDetailView(LoginRequiredMixin, DetailView):
'total_entries': QueueEntry.objects.filter(queue=queue).count(),
'waiting_entries': QueueEntry.objects.filter(queue=queue, status='WAITING').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(
# queue=queue,
# status='COMPLETED'
@ -608,7 +616,7 @@ class WaitingQueueDetailView(LoginRequiredMixin, DetailView):
return context
class WaitingQueueCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
class WaitingQueueCreateView(LoginRequiredMixin, CreateView):
"""
Create new waiting queue.
"""
@ -644,7 +652,7 @@ class WaitingQueueCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create
return response
class WaitingQueueUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
class WaitingQueueUpdateView(LoginRequiredMixin, UpdateView):
"""
Update waiting queue.
"""
@ -686,7 +694,7 @@ class WaitingQueueUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
return response
class WaitingQueueDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
class WaitingQueueDeleteView(LoginRequiredMixin, DeleteView):
"""
Delete waiting queue.
"""
@ -796,7 +804,7 @@ class QueueEntryDetailView(LoginRequiredMixin, DetailView):
return QueueEntry.objects.filter(queue__tenant=tenant)
class QueueEntryCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
class QueueEntryCreateView(LoginRequiredMixin, CreateView):
"""
Create new queue entry.
"""
@ -836,7 +844,7 @@ class QueueEntryCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
return response
class QueueEntryUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
class QueueEntryUpdateView(LoginRequiredMixin, UpdateView):
"""
Update queue entry.
"""
@ -883,7 +891,7 @@ class TelemedicineSessionListView(LoginRequiredMixin, ListView):
List telemedicine sessions.
"""
model = TelemedicineSession
template_name = 'appointments/telemedicine_session_list.html'
template_name = 'appointments/telemedicine/telemedicine.html'
context_object_name = 'sessions'
paginate_by = 25
@ -941,13 +949,13 @@ class TelemedicineSessionDetailView(LoginRequiredMixin, DetailView):
return TelemedicineSession.objects.filter(appointment__tenant=tenant)
class TelemedicineSessionCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
class TelemedicineSessionCreateView(LoginRequiredMixin, CreateView):
"""
Create new telemedicine session.
"""
model = TelemedicineSession
form_class = TelemedicineSessionForm
template_name = 'appointments/telemedicine_session_form.html'
template_name = 'appointments/telemedicine/telemedicine_session_form.html'
permission_required = 'appointments.add_telemedicinesession'
success_url = reverse_lazy('appointments:telemedicine_session_list')
@ -975,13 +983,13 @@ class TelemedicineSessionCreateView(LoginRequiredMixin, PermissionRequiredMixin,
return response
class TelemedicineSessionUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
class TelemedicineSessionUpdateView(LoginRequiredMixin, UpdateView):
"""
Update telemedicine session.
"""
model = TelemedicineSession
form_class = TelemedicineSessionForm
template_name = 'appointments/telemedicine_session_form.html'
template_name = 'appointments/telemedicine/telemedicine_session_form.html'
permission_required = 'appointments.change_telemedicinesession'
def get_queryset(self):
@ -1096,13 +1104,13 @@ class AppointmentTemplateDetailView(LoginRequiredMixin, DetailView):
return AppointmentTemplate.objects.filter(tenant=tenant)
class AppointmentTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
class AppointmentTemplateCreateView(LoginRequiredMixin, CreateView):
"""
Create new appointment template.
"""
model = AppointmentTemplate
form_class = AppointmentTemplateForm
template_name = 'appointments/appointment_template_form.html'
template_name = 'appointments/templates/appointment_template_form.html'
permission_required = 'appointments.add_appointmenttemplate'
success_url = reverse_lazy('appointments:appointment_template_list')
@ -1132,7 +1140,7 @@ class AppointmentTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin,
return response
class AppointmentTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
class AppointmentTemplateUpdateView(LoginRequiredMixin, UpdateView):
"""
Update appointment template.
"""
@ -1174,7 +1182,7 @@ class AppointmentTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
return response
class AppointmentTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
class AppointmentTemplateDeleteView(LoginRequiredMixin, DeleteView):
"""
Delete appointment template.
"""
@ -1292,7 +1300,7 @@ class WaitingListDetailView(LoginRequiredMixin, DetailView):
def get_queryset(self):
return WaitingList.objects.filter(
tenant=getattr(self.request.user, 'current_tenant', None)
tenant=getattr(self.request.user, 'tenant', None)
).select_related(
'patient', 'department', 'provider', 'scheduled_appointment',
'created_by', 'removed_by'
@ -1325,14 +1333,13 @@ class WaitingListCreateView(LoginRequiredMixin, CreateView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['tenant'] = getattr(self.request.user, 'current_tenant', None)
kwargs['tenant'] = getattr(self.request.user, 'tenant', None)
return kwargs
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
# Calculate initial position and estimated wait time
response = super().form_valid(form)
self.object.update_position()
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'
def get_queryset(self):
tenant = self.request.user.tenant
return WaitingList.objects.filter(
tenant=getattr(self.request.user, 'current_tenant', None)
tenant=tenant
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['tenant'] = getattr(self.request.user, 'current_tenant', None)
kwargs['tenant'] = getattr(self.request.user, 'tenant', None)
return kwargs
def get_success_url(self):
@ -1394,7 +1402,7 @@ class WaitingListDeleteView(LoginRequiredMixin, DeleteView):
def get_queryset(self):
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):
@ -1424,7 +1432,7 @@ def add_contact_log(request, pk):
entry = get_object_or_404(
WaitingList,
pk=pk,
tenant=getattr(request.user, 'current_tenant', None)
tenant=getattr(request.user, 'tenant', None)
)
if request.method == 'POST':
@ -1473,7 +1481,7 @@ def waiting_list_bulk_action(request):
if request.method == 'POST':
form = WaitingListBulkActionForm(
request.POST,
tenant=getattr(request.user, 'current_tenant', None)
tenant=getattr(request.user, 'tenant', None)
)
if form.is_valid():
@ -1486,7 +1494,7 @@ def waiting_list_bulk_action(request):
entries = WaitingList.objects.filter(
id__in=entry_ids,
tenant=getattr(request.user, 'current_tenant', None)
tenant=getattr(request.user, 'tenant', None)
)
if action == 'contact':
@ -1533,7 +1541,7 @@ def waiting_list_stats(request):
"""
HTMX endpoint for waiting list statistics.
"""
tenant = getattr(request.user, 'current_tenant', None)
tenant = getattr(request.user, 'tenant', None)
if not tenant:
return JsonResponse({'error': 'No tenant'})
@ -1546,9 +1554,9 @@ def waiting_list_stats(request):
'scheduled': waiting_list.filter(status='SCHEDULED').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),
'avg_wait_days': int(waiting_list.aggregate(
avg_days=Avg(timezone.now().date() - F('created_at__date'))
)['avg_days'] or 0),
# 'avg_wait_days': int(waiting_list.aggregate(
# avg_days=Avg(timezone.now().date() - F('created_at__date'))
# )['avg_days'] or 0),
}
return JsonResponse(stats)
@ -1595,14 +1603,17 @@ def appointment_stats(request):
# Calculate appointment statistics
stats = {
'total_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
).count(),
'total_appointments_today': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today
).count(),
'confirmed_appointments': AppointmentRequest.objects.filter(
'pending_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
scheduled_datetime__date=today,
status='CONFIRMED'
status='PENDING'
).count(),
'completed_appointments': AppointmentRequest.objects.filter(
tenant=tenant,
@ -1649,22 +1660,16 @@ def available_slots(request):
selected_date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
return render(request, 'appointments/partials/available_slots.html', status=400)
print(selected_date)
provider = get_object_or_404(User, pk=provider_id)
print(provider)
slots = SlotAvailability.objects.filter(
provider=provider,
provider__tenant=tenant,
date=selected_date,
).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):
# """
# HTMX view for available slots.
@ -1846,10 +1851,10 @@ def confirm_appointment(request, pk):
@login_required
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)
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')
if request.method == 'POST':
@ -1884,6 +1889,18 @@ def reschedule_appointment(request, pk):
# optionally send notifications if notify_patient is True
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 render(request, 'appointments/reschedule_appointment.html', {
@ -1922,13 +1939,13 @@ def cancel_appointment(request, pk):
"""
Complete an appointment.
"""
tenant = getattr(request, 'tenant', None)
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('appointments:appointment_request_list')
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()
@ -1946,7 +1963,11 @@ def cancel_appointment(request, pk):
)
messages.success(request, f'Appointment for {appointment.patient} cancelled successfully.')
return redirect('appointments:appointment_request_detail', pk=pk)
return redirect('appointments:appointment_detail', pk=pk)
return render(request, 'appointments/cancel_appointment.html', {
'appointment': appointment,
})
@login_required
@ -2028,13 +2049,13 @@ def next_in_queue(request, queue_id):
next_entry = QueueEntry.objects.filter(
queue=queue,
status='WAITING'
).order_by('position', 'created_at').first()
).order_by('queue_position', 'called_at').first()
if next_entry:
next_entry.status = 'IN_PROGRESS'
next_entry.status = 'IN_SERVICE'
next_entry.called_at = timezone.now()
next_entry.save()
messages.success(request, f"Patient has been called in for appointment.")
# Log queue progression
AuditLogger.log_event(
tenant=tenant,
@ -2047,13 +2068,10 @@ def next_in_queue(request, queue_id):
request=request
)
return JsonResponse({
'status': 'success',
'patient': str(next_entry.patient),
'position': next_entry.queue_position
})
return redirect('appointments:waiting_queue_detail', pk=queue.pk)
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
@ -2070,17 +2088,7 @@ def check_in_patient(request, appointment_id):
appointment.save()
messages.success(request, f"Patient {appointment.patient} has been checked in.")
return redirect('appointments:queue_management')
@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')
return redirect('appointments:waiting_queue_list')
@login_required
@ -2107,6 +2115,7 @@ def complete_queue_entry(request, pk):
queue_entry.actual_wait_time_minutes = int(wait_time)
queue_entry.save()
messages.success(request, f"Queue entry {queue_entry.pk} completed successfully.")
# Log completion
AuditLogger.log_event(
@ -2120,7 +2129,7 @@ def complete_queue_entry(request, pk):
request=request
)
return JsonResponse({'status': 'completed'})
return redirect('appointments:waiting_queue_detail', pk=queue_entry.queue.pk)
@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.
"""
model = AppointmentRequest
template_name = 'appointments/scheduling_calendar.html'
context_object_name = 'appointments'
paginate_by = 20
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['appointments'] = AppointmentRequest.objects.filter(
tenant=self.request.user.tenant,
status='SCHEDULED'
)
).select_related('patient', 'provider').order_by('-scheduled_datetime')
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:
color = 'red'
return format_html(
'<span style="color: {};">{:.1f}%</span>',
'<span style="color: {};">{}%</span>',
color, percentage
)
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 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
from django.conf import settings

View File

@ -4,12 +4,11 @@
{% block title %}Export Bills{% endblock %}
{% block css %}
<link href="{% static 'assets/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-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
@ -289,12 +288,12 @@
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.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>
$(document).ready(function() {

View File

@ -4,7 +4,6 @@
{% block title %}Submit Bill{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-8">
@ -270,7 +269,6 @@
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@ -199,7 +199,7 @@ def create_saudi_medical_bills():
users = list(User.objects.filter(is_active=True))
provider_users = list(User.objects.filter(
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:
@ -298,7 +298,7 @@ def create_saudi_bill_line_items(medical_bills):
provider_users = list(User.objects.filter(
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))
created_line_items = []

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