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',
'failed_login_attempts', 'locked_until', 'two_factor_enabled',
'max_concurrent_sessions', 'session_timeout_minutes'
)
}),
('Preferences', {
'fields': ( 'language', 'theme')
}),
('Status', {
'fields': (
'is_verified', 'is_approved', 'approval_date', 'approved_by'
)
}),
('Metadata', {
'fields': ('created_at', 'updated_at', 'last_password_change'),
'classes': ('collapse',)
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',
)}),
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
('Important dates', {'fields': ('last_login', 'date_joined')}),
)
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
if max_attempts is None:
max_attempts = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('MAX_LOGIN_ATTEMPTS', 5)
if lockout_minutes is None:
lockout_minutes = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('LOCKOUT_DURATION_MINUTES', 15)
# 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'])
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,177 +68,189 @@ 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)}"
ROLE_DISTRIBUTION = {
'PHYSICIAN': 0.15,
'NURSE': 0.25,
'PHARMACIST': 0.08,
'LAB_TECH': 0.10,
'RAD_TECH': 0.08,
'RADIOLOGIST': 0.05,
'ADMIN': 0.07,
'MEDICAL_ASSISTANT': 0.12,
'CLERICAL': 0.10
}
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)}"
# -------------------------------
# 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"""
"""Generate Saudi medical license number (fictional format)"""
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 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 Saudi healthcare users"""
users = []
role_distribution = {
'PHYSICIAN': 0.15,
'NURSE': 0.25,
'PHARMACIST': 0.08,
'LAB_TECH': 0.10,
'RAD_TECH': 0.08,
'RADIOLOGIST': 0.05,
'ADMIN': 0.07,
'MEDICAL_ASSISTANT': 0.12,
'CLERICAL': 0.10
}
"""
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]),
# Preferences
user_timezone='Asia/Riyadh',
language=random.choice(['ar', 'en', 'ar_SA']),
theme=random.choice(['LIGHT', 'DARK', 'AUTO']),
# Status
is_verified=True,
is_approved=True,
approval_date=django_timezone.now() - timedelta(days=random.randint(1, 180)),
is_active=True,
is_staff=role in ['ADMIN', 'SUPER_ADMIN'],
is_superuser=role == 'SUPER_ADMIN',
# Metadata
created_at=django_timezone.now() - timedelta(days=random.randint(1, 365)),
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)),
last_password_change=django_timezone.now() - timedelta(days=random.randint(1, 90)),
date_joined=django_timezone.now() - timedelta(days=random.randint(1, 365))
date_joined=django_timezone.now() - timedelta(days=random.randint(1, 365)),
)
# Set password
user.set_password('Hospital@123') # Default password
user.set_password('Hospital@123')
user.save()
users.append(user)
tenant_users.append(user)
# 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
# Set approval relationships
admin_users = [u for u in tenant_users if u.role in ['ADMIN', 'SUPER_ADMIN']]
# 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
emp.user_timezone = 'Asia/Riyadh'
emp.language = random.choice(['ar', 'en', 'ar_SA'])
emp.theme = random.choice([Employee.Theme.LIGHT, Employee.Theme.DARK, Employee.Theme.AUTO])
# Status / approval (approved later per-tenant)
emp.is_verified = True
emp.is_approved = True
emp.approval_date = django_timezone.now() - timedelta(days=random.randint(1, 180))
emp.save()
tenant_users.append(user)
all_users.append(user)
# Approval relationships: choose an approver among admins in this tenant
admin_users = [u for u in tenant_users if u.is_staff or u.is_superuser]
if admin_users:
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,220 +8,219 @@
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-10">
<div class="row">
<div class="col-xl-9">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'analytics:metric_definition_list' %}">Metrics</a></li>
<li class="breadcrumb-item active">Calculate Metric</li>
</ul>
<h1 class="page-header">Calculate Metric</h1>
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-10">
<div class="row">
<div class="col-xl-9">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'analytics:metric_definition_list' %}">Metrics</a></li>
<li class="breadcrumb-item active">Calculate Metric</li>
</ul>
<div class="card">
<div class="card-header">
<h4 class="card-title">Metric Calculation</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<h1 class="page-header">Calculate Metric</h1>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Metric Definition</label>
<div class="col-md-9">
<select name="metric_id" class="form-select" required>
<option value="">Select Metric</option>
{% for metric in metrics %}
<option value="{{ metric.id }}">{{ metric.name }} ({{ metric.category }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Calculation Period</label>
<div class="col-md-4">
<input type="date" name="start_date" class="form-control" required>
<small class="form-text text-muted">Start Date</small>
</div>
<div class="col-md-4">
<input type="date" name="end_date" class="form-control" required>
<small class="form-text text-muted">End Date</small>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Filters</label>
<div class="col-md-9">
<div class="row">
<div class="col-md-6">
<select name="department" class="form-select mb-2">
<option value="">All Departments</option>
{% for dept in departments %}
<option value="{{ dept.id }}">{{ dept.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<select name="location" class="form-select mb-2">
<option value="">All Locations</option>
{% for loc in locations %}
<option value="{{ loc.id }}">{{ loc.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Calculation Mode</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="radio" name="calculation_mode" value="real_time" id="real_time" checked>
<label class="form-check-label" for="real_time">
Real-time Calculation
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="calculation_mode" value="cached" id="cached">
<label class="form-check-label" for="cached">
Use Cached Data (Faster)
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-primary">
<i class="fa fa-calculator me-2"></i>Calculate Metric
</button>
<a href="{% url 'analytics:metric_definition_list' %}" class="btn btn-secondary ms-2">Cancel</a>
</div>
</div>
</form>
</div>
<div class="card">
<div class="card-header">
<h4 class="card-title">Metric Calculation</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Metric Definition</label>
<div class="col-md-9">
<select name="metric_id" class="form-select" required>
<option value="">Select Metric</option>
{% for metric in metrics %}
<option value="{{ metric.id }}">{{ metric.name }} ({{ metric.category }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Calculation Period</label>
<div class="col-md-4">
<input type="date" name="start_date" class="form-control" required>
<small class="form-text text-muted">Start Date</small>
</div>
<div class="col-md-4">
<input type="date" name="end_date" class="form-control" required>
<small class="form-text text-muted">End Date</small>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Filters</label>
<div class="col-md-9">
<div class="row">
<div class="col-md-6">
<select name="department" class="form-select mb-2">
<option value="">All Departments</option>
{% for dept in departments %}
<option value="{{ dept.id }}">{{ dept.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<select name="location" class="form-select mb-2">
<option value="">All Locations</option>
{% for loc in locations %}
<option value="{{ loc.id }}">{{ loc.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Calculation Mode</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="radio" name="calculation_mode" value="real_time" id="real_time" checked>
<label class="form-check-label" for="real_time">
Real-time Calculation
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="calculation_mode" value="cached" id="cached">
<label class="form-check-label" for="cached">
Use Cached Data (Faster)
</label>
</div>
</div>
</div>
{% if calculation_result %}
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">Calculation Results</h4>
</div>
<div class="card-body">
<div class="row">
<div class="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">
<div class="fs-3 fw-bold">{{ calculation_result.value }}</div>
<div>{{ calculation_result.metric_name }}</div>
</div>
<div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-chart-line fa-lg"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<div class="row mb-2">
<div class="col-6"><strong>Unit:</strong></div>
<div class="col-6">{{ calculation_result.unit }}</div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>Period:</strong></div>
<div class="col-6">{{ calculation_result.period }}</div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>Calculated:</strong></div>
<div class="col-6">{{ calculation_result.calculated_at }}</div>
</div>
<div class="row">
<div class="col-6"><strong>Status:</strong></div>
<div class="col-6">
{% if calculation_result.status == 'success' %}
<span class="badge bg-success">Success</span>
{% else %}
<span class="badge bg-warning">{{ calculation_result.status|title }}</span>
{% endif %}
</div>
</div>
</div>
</div>
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-primary">
<i class="fa fa-calculator me-2"></i>Calculate Metric
</button>
<a href="{% url 'analytics:metric_definition_list' %}" class="btn btn-secondary ms-2">Cancel</a>
</div>
</div>
{% if calculation_result.breakdown %}
<div class="mt-4">
<h6>Calculation Breakdown:</h6>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Component</th>
<th>Value</th>
<th>Weight</th>
<th>Contribution</th>
</tr>
</thead>
<tbody>
{% for item in calculation_result.breakdown %}
<tr>
<td>{{ item.component }}</td>
<td>{{ item.value }}</td>
<td>{{ item.weight }}%</td>
<td>{{ item.contribution }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if calculation_result.notes %}
<div class="mt-3">
<h6>Notes:</h6>
<p class="text-muted">{{ calculation_result.notes }}</p>
</div>
{% endif %}
</div>
</form>
</div>
{% endif %}
</div>
<div class="col-xl-3">
<div class="card">
<div class="card-header">
<h4 class="card-title">Quick Actions</h4>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'analytics:metric_definition_list' %}" class="btn btn-outline-primary btn-sm">
<i class="fa fa-list me-2"></i>All Metrics
</a>
<a href="{% url 'analytics:metric_definition_create' %}" class="btn btn-outline-success btn-sm">
<i class="fa fa-plus me-2"></i>Create Metric
</a>
{% if calculation_result %}
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">Calculation Results</h4>
</div>
<div class="card-body">
<div class="row">
<div class="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">
<div class="fs-3 fw-bold">{{ calculation_result.value }}</div>
<div>{{ calculation_result.metric_name }}</div>
</div>
<div class="w-50px h-50px bg-white bg-opacity-20 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-chart-line fa-lg"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<div class="row mb-2">
<div class="col-6"><strong>Unit:</strong></div>
<div class="col-6">{{ calculation_result.unit }}</div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>Period:</strong></div>
<div class="col-6">{{ calculation_result.period }}</div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>Calculated:</strong></div>
<div class="col-6">{{ calculation_result.calculated_at }}</div>
</div>
<div class="row">
<div class="col-6"><strong>Status:</strong></div>
<div class="col-6">
{% if calculation_result.status == 'success' %}
<span class="badge bg-success">Success</span>
{% else %}
<span class="badge bg-warning">{{ calculation_result.status|title }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% if calculation_result.breakdown %}
<div class="mt-4">
<h6>Calculation Breakdown:</h6>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Component</th>
<th>Value</th>
<th>Weight</th>
<th>Contribution</th>
</tr>
</thead>
<tbody>
{% for item in calculation_result.breakdown %}
<tr>
<td>{{ item.component }}</td>
<td>{{ item.value }}</td>
<td>{{ item.weight }}%</td>
<td>{{ item.contribution }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if calculation_result.notes %}
<div class="mt-3">
<h6>Notes:</h6>
<p class="text-muted">{{ calculation_result.notes }}</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
<div class="col-xl-3">
<div class="card">
<div class="card-header">
<h4 class="card-title">Quick Actions</h4>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'analytics:metric_definition_list' %}" class="btn btn-outline-primary btn-sm">
<i class="fa fa-list me-2"></i>All Metrics
</a>
<a href="{% url 'analytics:metric_definition_create' %}" class="btn btn-outline-success btn-sm">
<i class="fa fa-plus me-2"></i>Create Metric
</a>
</div>
</div>
</div>
@ -230,6 +229,7 @@
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

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

View File

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

View File

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

View File

@ -2,214 +2,267 @@
{% 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="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="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">
<!-- Appointment Info -->
<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.get_full_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.mrn }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.get_full_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.provider.department }}</div>
</div>
</div>
<div class="col-md-6">
<div class="row mb-2">
<div class="col-4"><strong>Date:</strong></div>
<div class="col-8">{{ appointment.preferred_date|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Time:</strong></div>
<div class="col-8">{{ appointment.preferred_time|time:"g:i A" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Type:</strong></div>
<div class="col-8">{{ appointment.get_appointment_type_display }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Status:</strong></div>
<div class="col-8">
{% if appointment.status == 'SCHEDULED' %}
<span class="badge bg-info">Scheduled</span>
{% elif appointment.status == 'CONFIRMED' %}
<span class="badge bg-success">Confirmed</span>
{% elif appointment.status == 'CHECKED_IN' %}
<span class="badge bg-primary">Checked In</span>
{% endif %}
</div>
</div>
</div>
</div>
{% if appointment.notes %}
<div class="row mt-3">
<div class="col-12">
<strong>Current Notes:</strong>
<p class="mt-2">{{ appointment.notes }}</p>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Cancellation Form -->
<div class="panel panel-inverse mb-4" data-sortable-id="index-2">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-times-rectangle"></i> Cancellation Details
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="panel-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle me-2"></i>
<strong>Warning:</strong> This action will permanently cancel the appointment. This cannot be undone.
</div>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Reason <span class="text-danger">*</span></label>
<div class="col-md-9">
<select name="cancellation_reason" class="form-select" required>
<option value="">Select Reason</option>
<option value="patient_request" {% if form.cancellation_reason.value == 'patient_request' %}selected{% endif %}>Patient Request</option>
<option value="patient_illness" {% if form.cancellation_reason.value == 'patient_illness' %}selected{% endif %}>Patient Illness</option>
<option value="provider_unavailable" {% if form.cancellation_reason.value == 'provider_unavailable' %}selected{% endif %}>Provider Unavailable</option>
<option value="emergency" {% if form.cancellation_reason.value == 'emergency' %}selected{% endif %}>Emergency</option>
<option value="equipment_failure" {% if form.cancellation_reason.value == 'equipment_failure' %}selected{% endif %}>Equipment Failure</option>
<option value="weather" {% if form.cancellation_reason.value == 'weather' %}selected{% endif %}>Weather Conditions</option>
<option value="scheduling_error" {% if form.cancellation_reason.value == 'scheduling_error' %}selected{% endif %}>Scheduling Error</option>
<option value="insurance_issue" {% if form.cancellation_reason.value == 'insurance_issue' %}selected{% endif %}>Insurance Issue</option>
<option value="other" {% if form.cancellation_reason.value == 'other' %}selected{% endif %}>Other</option>
</select>
{% if form.cancellation_reason.errors %}
<div class="text-danger">{{ form.cancellation_reason.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Notes</label>
<div class="col-md-9">
<textarea name="cancellation_notes" class="form-control" rows="4"
placeholder="Additional details about the cancellation">{{ form.cancellation_notes.value }}</textarea>
{% if form.cancellation_notes.errors %}
<div class="text-danger">{{ form.cancellation_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Fee</label>
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text">
<span class="symbol m-0 p-0">&#xea;</span>
</span>
<input type="number" name="cancellation_fee" class="form-control"
step="0.01" min="0" value="{{ form.cancellation_fee.value|default:'0.00' }}">
</div>
<small class="form-text text-muted">Enter cancellation fee if applicable</small>
{% if form.cancellation_fee.errors %}
<div class="text-danger">{{ form.cancellation_fee.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="notify_patient" id="notify_patient"
{% if form.notify_patient.value %}checked{% endif %}>
<label class="form-check-label" for="notify_patient">
Send cancellation notification to patient
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="offer_reschedule" id="offer_reschedule"
{% if form.offer_reschedule.value %}checked{% endif %}>
<label class="form-check-label" for="offer_reschedule">
Offer rescheduling options to patient
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="add_to_waitlist" id="add_to_waitlist"
{% if form.add_to_waitlist.value %}checked{% endif %}>
<label class="form-check-label" for="add_to_waitlist">
Add patient to waitlist for earlier appointments
</label>
</div>
</div>
</div>
<div class="row">
<div class="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>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
</div>
<div class="col-md-9 offset-md-3">
<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-sm btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
<div class="col-md-6">
<div class="row mb-2">
<div class="col-4"><strong>Date:</strong></div>
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div>
</div>
<div>
<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="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="modal-body">
<p class="mb-0">Are you sure you want to cancel this appointment? This action cannot be undone.</p>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Type:</strong></div>
<div class="col-8">{{ appointment.appointment_type.name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Status:</strong></div>
<div class="col-8">
{% if appointment.status == 'scheduled' %}
<span class="badge bg-info">Scheduled</span>
{% elif appointment.status == 'confirmed' %}
<span class="badge bg-success">Confirmed</span>
{% elif appointment.status == 'checked_in' %}
<span class="badge bg-primary">Checked In</span>
{% endif %}
</div>
<div 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>
{% if appointment.notes %}
<div class="row mt-3">
<div class="col-12">
<strong>Current Notes:</strong>
<p class="mt-2">{{ appointment.notes }}</p>
</div>
</div>
{% endif %}
</div>
</div>
</form>
</div>
</div>
<!-- Cancellation Form -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Cancellation Details</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle me-2"></i>
<strong>Warning:</strong> This action will permanently cancel the appointment. This cannot be undone.
</div>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Reason <span class="text-danger">*</span></label>
<div class="col-md-9">
<select name="cancellation_reason" class="form-select" required>
<option value="">Select Reason</option>
<option value="patient_request" {% if form.cancellation_reason.value == 'patient_request' %}selected{% endif %}>Patient Request</option>
<option value="patient_illness" {% if form.cancellation_reason.value == 'patient_illness' %}selected{% endif %}>Patient Illness</option>
<option value="provider_unavailable" {% if form.cancellation_reason.value == 'provider_unavailable' %}selected{% endif %}>Provider Unavailable</option>
<option value="emergency" {% if form.cancellation_reason.value == 'emergency' %}selected{% endif %}>Emergency</option>
<option value="equipment_failure" {% if form.cancellation_reason.value == 'equipment_failure' %}selected{% endif %}>Equipment Failure</option>
<option value="weather" {% if form.cancellation_reason.value == 'weather' %}selected{% endif %}>Weather Conditions</option>
<option value="scheduling_error" {% if form.cancellation_reason.value == 'scheduling_error' %}selected{% endif %}>Scheduling Error</option>
<option value="insurance_issue" {% if form.cancellation_reason.value == 'insurance_issue' %}selected{% endif %}>Insurance Issue</option>
<option value="other" {% if form.cancellation_reason.value == 'other' %}selected{% endif %}>Other</option>
</select>
{% if form.cancellation_reason.errors %}
<div class="text-danger">{{ form.cancellation_reason.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Notes</label>
<div class="col-md-9">
<textarea name="cancellation_notes" class="form-control" rows="4"
placeholder="Additional details about the cancellation">{{ form.cancellation_notes.value|default:'' }}</textarea>
{% if form.cancellation_notes.errors %}
<div class="text-danger">{{ form.cancellation_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Cancellation Fee</label>
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" name="cancellation_fee" class="form-control"
step="0.01" min="0" value="{{ form.cancellation_fee.value|default:'0.00' }}">
</div>
<small class="form-text text-muted">Enter cancellation fee if applicable</small>
{% if form.cancellation_fee.errors %}
<div class="text-danger">{{ form.cancellation_fee.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="notify_patient" id="notify_patient"
{% if form.notify_patient.value %}checked{% endif %}>
<label class="form-check-label" for="notify_patient">
Send cancellation notification to patient
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="offer_reschedule" id="offer_reschedule"
{% if form.offer_reschedule.value %}checked{% endif %}>
<label class="form-check-label" for="offer_reschedule">
Offer rescheduling options to patient
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="add_to_waitlist" id="add_to_waitlist"
{% if form.add_to_waitlist.value %}checked{% endif %}>
<label class="form-check-label" for="add_to_waitlist">
Add patient to waitlist for earlier appointments
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to cancel this appointment? This action cannot be undone.')">
<i class="fa fa-times me-2"></i>Cancel Appointment
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div>
</div>
<div 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>
<!-- Cancellation Policy -->
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">Cancellation Policy</h4>
</div>
<div class="card-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>
<li>Patients will be notified of the cancellation via their preferred communication method</li>
<li>Cancelled appointments will be made available to other patients on the waitlist</li>
<li>Refunds for prepaid appointments will be processed according to the refund policy</li>
</ul>
</div>
<div 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>
<li>Patients will be notified of the cancellation via their preferred communication method</li>
<li>Cancelled appointments will be made available to other patients on the waitlist</li>
<li>Refunds for prepaid appointments will be processed according to the refund policy</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% 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,302 +4,301 @@
{% block title %}Check In Patient{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-10">
<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">Check In</li>
</ul>
<h1 class="page-header">Patient Check-In</h1>
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-10">
<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">Check In</li>
</ul>
<div class="row">
<div class="col-xl-8">
<!-- Appointment Info -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Appointment Details</h4>
</div>
<div class="card-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>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>DOB:</strong></div>
<div class="col-8">{{ appointment.patient.date_of_birth|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Phone:</strong></div>
<div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
</div>
<h1 class="page-header">Patient Check-In</h1>
<div class="row">
<div class="col-xl-8">
<!-- Appointment Info -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Appointment Details</h4>
</div>
<div class="card-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>
<div class="col-md-6">
<div class="row mb-2">
<div class="col-4"><strong>Date:</strong></div>
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Time:</strong></div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>DOB:</strong></div>
<div class="col-8">{{ appointment.patient.date_of_birth|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Phone:</strong></div>
<div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
</div>
</div>
</div>
</div>
<!-- Check-in Form -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Check-In Information</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Check-in Time</label>
<div class="col-md-9">
<input type="datetime-local" name="checkin_time" class="form-control"
value="{{ form.checkin_time.value|default:now|date:'Y-m-d\TH:i' }}" required>
{% if form.checkin_time.errors %}
<div class="text-danger">{{ form.checkin_time.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6">
<div class="row mb-2">
<div class="col-4"><strong>Date:</strong></div>
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Checked In By</label>
<div class="col-md-9">
<input type="text" name="checked_in_by" class="form-control"
value="{{ form.checked_in_by.value|default:request.user.get_full_name }}" readonly>
{% if form.checked_in_by.errors %}
<div class="text-danger">{{ form.checked_in_by.errors.0 }}</div>
{% endif %}
</div>
<div class="row mb-2">
<div class="col-4"><strong>Time:</strong></div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Verification Checklist</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="id_verified" id="id_verified"
{% if form.id_verified.value %}checked{% endif %}>
<label class="form-check-label" for="id_verified">
Photo ID verified
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="insurance_verified" id="insurance_verified"
{% if form.insurance_verified.value %}checked{% endif %}>
<label class="form-check-label" for="insurance_verified">
Insurance card verified
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="forms_completed" id="forms_completed"
{% if form.forms_completed.value %}checked{% endif %}>
<label class="form-check-label" for="forms_completed">
Required forms completed
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="copay_collected" id="copay_collected"
{% if form.copay_collected.value %}checked{% endif %}>
<label class="form-check-label" for="copay_collected">
Co-payment collected (if applicable)
</label>
</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Waiting Area Assignment</label>
<div class="col-md-9">
<select name="waiting_area" class="form-select">
<option value="">Select Waiting Area</option>
<option value="main_lobby" {% if form.waiting_area.value == 'main_lobby' %}selected{% endif %}>Main Lobby</option>
<option value="pediatric_area" {% if form.waiting_area.value == 'pediatric_area' %}selected{% endif %}>Pediatric Area</option>
<option value="specialty_clinic" {% if form.waiting_area.value == 'specialty_clinic' %}selected{% endif %}>Specialty Clinic</option>
<option value="urgent_care" {% if form.waiting_area.value == 'urgent_care' %}selected{% endif %}>Urgent Care</option>
<option value="surgical_waiting" {% if form.waiting_area.value == 'surgical_waiting' %}selected{% endif %}>Surgical Waiting</option>
</select>
{% if form.waiting_area.errors %}
<div class="text-danger">{{ form.waiting_area.errors.0 }}</div>
{% endif %}
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Special Needs</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="wheelchair_needed" id="wheelchair_needed"
{% if form.wheelchair_needed.value %}checked{% endif %}>
<label class="form-check-label" for="wheelchair_needed">
Wheelchair assistance needed
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="interpreter_needed" id="interpreter_needed"
{% if form.interpreter_needed.value %}checked{% endif %}>
<label class="form-check-label" for="interpreter_needed">
Interpreter services needed
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="special_assistance" id="special_assistance"
{% if form.special_assistance.value %}checked{% endif %}>
<label class="form-check-label" for="special_assistance">
Other special assistance required
</label>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Check-in Notes</label>
<div class="col-md-9">
<textarea name="checkin_notes" class="form-control" rows="3"
placeholder="Any additional notes about the check-in process">{{ form.checkin_notes.value|default:'' }}</textarea>
{% if form.checkin_notes.errors %}
<div class="text-danger">{{ form.checkin_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-success">
<i class="fa fa-check me-2"></i>Complete Check-In
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<!-- Patient Information -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Patient Information</h4>
<!-- Check-in Form -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Check-In Information</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Check-in Time</label>
<div class="col-md-9">
<input type="datetime-local" name="checkin_time" class="form-control"
value="{{ form.checkin_time.value|default:now|date:'Y-m-d\TH:i' }}" required>
{% if form.checkin_time.errors %}
<div class="text-danger">{{ form.checkin_time.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Checked In By</label>
<div class="col-md-9">
<input type="text" name="checked_in_by" class="form-control"
value="{{ form.checked_in_by.value|default:request.user.get_full_name }}" readonly>
{% if form.checked_in_by.errors %}
<div class="text-danger">{{ form.checked_in_by.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Verification Checklist</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="id_verified" id="id_verified"
{% if form.id_verified.value %}checked{% endif %}>
<label class="form-check-label" for="id_verified">
Photo ID verified
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="insurance_verified" id="insurance_verified"
{% if form.insurance_verified.value %}checked{% endif %}>
<label class="form-check-label" for="insurance_verified">
Insurance card verified
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="forms_completed" id="forms_completed"
{% if form.forms_completed.value %}checked{% endif %}>
<label class="form-check-label" for="forms_completed">
Required forms completed
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="copay_collected" id="copay_collected"
{% if form.copay_collected.value %}checked{% endif %}>
<label class="form-check-label" for="copay_collected">
Co-payment collected (if applicable)
</label>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Waiting Area Assignment</label>
<div class="col-md-9">
<select name="waiting_area" class="form-select">
<option value="">Select Waiting Area</option>
<option value="main_lobby" {% if form.waiting_area.value == 'main_lobby' %}selected{% endif %}>Main Lobby</option>
<option value="pediatric_area" {% if form.waiting_area.value == 'pediatric_area' %}selected{% endif %}>Pediatric Area</option>
<option value="specialty_clinic" {% if form.waiting_area.value == 'specialty_clinic' %}selected{% endif %}>Specialty Clinic</option>
<option value="urgent_care" {% if form.waiting_area.value == 'urgent_care' %}selected{% endif %}>Urgent Care</option>
<option value="surgical_waiting" {% if form.waiting_area.value == 'surgical_waiting' %}selected{% endif %}>Surgical Waiting</option>
</select>
{% if form.waiting_area.errors %}
<div class="text-danger">{{ form.waiting_area.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Special Needs</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="wheelchair_needed" id="wheelchair_needed"
{% if form.wheelchair_needed.value %}checked{% endif %}>
<label class="form-check-label" for="wheelchair_needed">
Wheelchair assistance needed
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="interpreter_needed" id="interpreter_needed"
{% if form.interpreter_needed.value %}checked{% endif %}>
<label class="form-check-label" for="interpreter_needed">
Interpreter services needed
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="special_assistance" id="special_assistance"
{% if form.special_assistance.value %}checked{% endif %}>
<label class="form-check-label" for="special_assistance">
Other special assistance required
</label>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Check-in Notes</label>
<div class="col-md-9">
<textarea name="checkin_notes" class="form-control" rows="3"
placeholder="Any additional notes about the check-in process">{{ form.checkin_notes.value|default:'' }}</textarea>
{% if form.checkin_notes.errors %}
<div class="text-danger">{{ form.checkin_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-success">
<i class="fa fa-check me-2"></i>Complete Check-In
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-4">
<!-- Patient Information -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Patient Information</h4>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-5"><strong>Age:</strong></div>
<div class="col-7">{{ appointment.patient.age|default:"N/A" }}</div>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-5"><strong>Age:</strong></div>
<div class="col-7">{{ appointment.patient.age|default:"N/A" }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Gender:</strong></div>
<div class="col-7">{{ appointment.patient.gender|default:"N/A" }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Address:</strong></div>
<div class="col-7">{{ appointment.patient.address|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Emergency Contact:</strong></div>
<div class="col-7">{{ appointment.patient.emergency_contact|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Gender:</strong></div>
<div class="col-7">{{ appointment.patient.gender|default:"N/A" }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Address:</strong></div>
<div class="col-7">{{ appointment.patient.address|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Emergency Contact:</strong></div>
<div class="col-7">{{ appointment.patient.emergency_contact|default:"Not provided" }}</div>
</div>
</div>
</div>
<!-- Insurance Information -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Insurance Information</h4>
</div>
<div class="card-body">
{% if appointment.patient.insurance_provider %}
<div class="row mb-2">
<div class="col-5"><strong>Provider:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_provider }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Policy #:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_policy_number }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Group #:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_group_number|default:"N/A" }}</div>
</div>
{% else %}
<p class="text-muted">No insurance information on file</p>
{% endif %}
</div>
<!-- Insurance Information -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Insurance Information</h4>
</div>
<!-- Alerts -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Patient Alerts</h4>
</div>
<div class="card-body">
{% if appointment.patient.allergies %}
<div class="alert alert-warning">
<strong>Allergies:</strong> {{ appointment.patient.allergies }}
</div>
{% endif %}
{% if appointment.patient.medical_alerts %}
<div class="alert alert-danger">
<strong>Medical Alerts:</strong> {{ appointment.patient.medical_alerts }}
</div>
{% endif %}
{% if not appointment.patient.allergies and not appointment.patient.medical_alerts %}
<p class="text-muted">No alerts on file</p>
{% endif %}
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Quick Actions</h4>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'patients:patient_detail' appointment.patient.id %}" class="btn btn-outline-primary btn-sm">
<i class="fa fa-user me-2"></i>View Patient Profile
</a>
<a href="{% url 'appointments:appointment_update' appointment.id %}" class="btn btn-outline-warning btn-sm">
<i class="fa fa-edit me-2"></i>Edit Appointment
</a>
<button type="button" class="btn btn-outline-info btn-sm" onclick="printCheckinForm()">
<i class="fa fa-print me-2"></i>Print Check-in Form
</button>
<div class="card-body">
{% if appointment.patient.insurance_provider %}
<div class="row mb-2">
<div class="col-5"><strong>Provider:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_provider }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Policy #:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_policy_number }}</div>
</div>
<div class="row mb-2">
<div class="col-5"><strong>Group #:</strong></div>
<div class="col-7">{{ appointment.patient.insurance_group_number|default:"N/A" }}</div>
</div>
{% else %}
<p class="text-muted">No insurance information on file</p>
{% endif %}
</div>
</div>
<!-- Alerts -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Patient Alerts</h4>
</div>
<div class="card-body">
{% if appointment.patient.allergies %}
<div class="alert alert-warning">
<strong>Allergies:</strong> {{ appointment.patient.allergies }}
</div>
{% endif %}
{% if appointment.patient.medical_alerts %}
<div class="alert alert-danger">
<strong>Medical Alerts:</strong> {{ appointment.patient.medical_alerts }}
</div>
{% endif %}
{% if not appointment.patient.allergies and not appointment.patient.medical_alerts %}
<p class="text-muted">No alerts on file</p>
{% endif %}
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Quick Actions</h4>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'patients:patient_detail' appointment.patient.id %}" class="btn btn-outline-primary btn-sm">
<i class="fa fa-user me-2"></i>View Patient Profile
</a>
<a href="{% url 'appointments:appointment_update' appointment.id %}" class="btn btn-outline-warning btn-sm">
<i class="fa fa-edit me-2"></i>Edit Appointment
</a>
<button type="button" class="btn btn-outline-info btn-sm" onclick="printCheckinForm()">
<i class="fa fa-print me-2"></i>Print Check-in Form
</button>
</div>
</div>
</div>
@ -308,6 +307,7 @@
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@ -4,247 +4,246 @@
{% block title %}Confirm Appointment{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<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">Confirm</li>
</ul>
<h1 class="page-header">Confirm Appointment</h1>
<div class="container">
<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">Confirm</li>
</ul>
<!-- Appointment Info -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Appointment Details</h4>
</div>
<div class="card-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>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Phone:</strong></div>
<div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Email:</strong></div>
<div class="col-8">{{ appointment.patient.email|default:"Not provided" }}</div>
</div>
<h1 class="page-header">Confirm Appointment</h1>
<!-- Appointment Info -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Appointment Details</h4>
</div>
<div class="card-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>
<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="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Phone:</strong></div>
<div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Email:</strong></div>
<div class="col-8">{{ appointment.patient.email|default:"Not provided" }}</div>
</div>
</div>
<div class="col-md-6">
<div class="row mb-2">
<div class="col-4"><strong>Date:</strong></div>
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Time:</strong></div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Type:</strong></div>
<div class="col-8">{{ appointment.appointment_type.name }}</div>
</div>
</div>
</div>
{% if appointment.notes %}
<div class="row mt-3">
<div class="col-12">
<strong>Appointment Notes:</strong>
<p class="mt-2">{{ appointment.notes }}</p>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Confirmation Form -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Confirmation Details</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmation Method</label>
<div class="col-md-9">
<select name="confirmation_method" class="form-select">
<option value="phone" {% if form.confirmation_method.value == 'phone' %}selected{% endif %}>Phone Call</option>
<option value="email" {% if form.confirmation_method.value == 'email' %}selected{% endif %}>Email</option>
<option value="sms" {% if form.confirmation_method.value == 'sms' %}selected{% endif %}>SMS/Text</option>
<option value="in_person" {% if form.confirmation_method.value == 'in_person' %}selected{% endif %}>In Person</option>
<option value="online" {% if form.confirmation_method.value == 'online' %}selected{% endif %}>Online Portal</option>
</select>
{% if form.confirmation_method.errors %}
<div class="text-danger">{{ form.confirmation_method.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmed By</label>
<div class="col-md-9">
<input type="text" name="confirmed_by" class="form-control"
value="{{ form.confirmed_by.value|default:request.user.get_full_name }}" readonly>
{% if form.confirmed_by.errors %}
<div class="text-danger">{{ form.confirmed_by.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Patient Contact Verified</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="contact_verified" id="contact_verified"
{% if form.contact_verified.value %}checked{% endif %}>
<label class="form-check-label" for="contact_verified">
Patient contact information has been verified and is current
</label>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Time:</strong></div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
{% if form.contact_verified.errors %}
<div class="text-danger">{{ form.contact_verified.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Insurance Verified</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="insurance_verified" id="insurance_verified"
{% if form.insurance_verified.value %}checked{% endif %}>
<label class="form-check-label" for="insurance_verified">
Patient insurance information has been verified
</label>
</div>
<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>
{% if form.insurance_verified.errors %}
<div class="text-danger">{{ form.insurance_verified.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Pre-appointment Instructions Given</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="instructions_given" id="instructions_given"
{% if form.instructions_given.value %}checked{% endif %}>
<label class="form-check-label" for="instructions_given">
Pre-appointment instructions have been provided to the patient
</label>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
{% if form.instructions_given.errors %}
<div class="text-danger">{{ form.instructions_given.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmation Notes</label>
<div class="col-md-9">
<textarea name="confirmation_notes" class="form-control" rows="4"
placeholder="Additional notes about the confirmation process">{{ form.confirmation_notes.value|default:'' }}</textarea>
{% if form.confirmation_notes.errors %}
<div class="text-danger">{{ form.confirmation_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="send_reminder" id="send_reminder"
{% if form.send_reminder.value %}checked{% endif %}>
<label class="form-check-label" for="send_reminder">
Send appointment reminder 24 hours before appointment
</label>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Type:</strong></div>
<div class="col-8">{{ appointment.appointment_type.name }}</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="send_confirmation_email" id="send_confirmation_email"
{% if form.send_confirmation_email.value %}checked{% endif %}>
<label class="form-check-label" for="send_confirmation_email">
Send confirmation email to patient
</label>
</div>
</div>
</div>
{% if appointment.notes %}
<div class="row mt-3">
<div class="col-12">
<strong>Appointment Notes:</strong>
<p class="mt-2">{{ appointment.notes }}</p>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-success">
<i class="fa fa-check me-2"></i>Confirm Appointment
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
{% endif %}
</div>
</form>
</div>
</div>
<!-- Confirmation Form -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Confirmation Details</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmation Method</label>
<div class="col-md-9">
<select name="confirmation_method" class="form-select">
<option value="phone" {% if form.confirmation_method.value == 'phone' %}selected{% endif %}>Phone Call</option>
<option value="email" {% if form.confirmation_method.value == 'email' %}selected{% endif %}>Email</option>
<option value="sms" {% if form.confirmation_method.value == 'sms' %}selected{% endif %}>SMS/Text</option>
<option value="in_person" {% if form.confirmation_method.value == 'in_person' %}selected{% endif %}>In Person</option>
<option value="online" {% if form.confirmation_method.value == 'online' %}selected{% endif %}>Online Portal</option>
</select>
{% if form.confirmation_method.errors %}
<div class="text-danger">{{ form.confirmation_method.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmed By</label>
<div class="col-md-9">
<input type="text" name="confirmed_by" class="form-control"
value="{{ form.confirmed_by.value|default:request.user.get_full_name }}" readonly>
{% if form.confirmed_by.errors %}
<div class="text-danger">{{ form.confirmed_by.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Patient Contact Verified</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="contact_verified" id="contact_verified"
{% if form.contact_verified.value %}checked{% endif %}>
<label class="form-check-label" for="contact_verified">
Patient contact information has been verified and is current
</label>
</div>
{% if form.contact_verified.errors %}
<div class="text-danger">{{ form.contact_verified.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Insurance Verified</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="insurance_verified" id="insurance_verified"
{% if form.insurance_verified.value %}checked{% endif %}>
<label class="form-check-label" for="insurance_verified">
Patient insurance information has been verified
</label>
</div>
{% if form.insurance_verified.errors %}
<div class="text-danger">{{ form.insurance_verified.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Pre-appointment Instructions Given</label>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="instructions_given" id="instructions_given"
{% if form.instructions_given.value %}checked{% endif %}>
<label class="form-check-label" for="instructions_given">
Pre-appointment instructions have been provided to the patient
</label>
</div>
{% if form.instructions_given.errors %}
<div class="text-danger">{{ form.instructions_given.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Confirmation Notes</label>
<div class="col-md-9">
<textarea name="confirmation_notes" class="form-control" rows="4"
placeholder="Additional notes about the confirmation process">{{ form.confirmation_notes.value|default:'' }}</textarea>
{% if form.confirmation_notes.errors %}
<div class="text-danger">{{ form.confirmation_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="send_reminder" id="send_reminder"
{% if form.send_reminder.value %}checked{% endif %}>
<label class="form-check-label" for="send_reminder">
Send appointment reminder 24 hours before appointment
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="send_confirmation_email" id="send_confirmation_email"
{% if form.send_confirmation_email.value %}checked{% endif %}>
<label class="form-check-label" for="send_confirmation_email">
Send confirmation email to patient
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-success">
<i class="fa fa-check me-2"></i>Confirm Appointment
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div>
<!-- Pre-appointment Checklist -->
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">Pre-appointment Checklist</h4>
</div>
<!-- Pre-appointment Checklist -->
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">Pre-appointment Checklist</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Patient Preparation:</h6>
<ul>
<li>Arrive 15 minutes early for check-in</li>
<li>Bring valid photo ID</li>
<li>Bring insurance cards</li>
<li>Bring list of current medications</li>
<li>Complete any required forms</li>
</ul>
</div>
<div class="col-md-6">
<h6>Special Instructions:</h6>
<ul>
{% if appointment.appointment_type.requires_fasting %}
<li>Fasting required - no food or drink 8 hours before appointment</li>
{% endif %}
{% if appointment.appointment_type.requires_preparation %}
<li>Special preparation required - see appointment type instructions</li>
{% endif %}
<li>Wear comfortable, loose-fitting clothing</li>
<li>Arrange transportation if sedation will be used</li>
<li>Bring a list of questions for the provider</li>
</ul>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Patient Preparation:</h6>
<ul>
<li>Arrive 15 minutes early for check-in</li>
<li>Bring valid photo ID</li>
<li>Bring insurance cards</li>
<li>Bring list of current medications</li>
<li>Complete any required forms</li>
</ul>
</div>
<div class="col-md-6">
<h6>Special Instructions:</h6>
<ul>
{% if appointment.appointment_type.requires_fasting %}
<li>Fasting required - no food or drink 8 hours before appointment</li>
{% endif %}
{% if appointment.appointment_type.requires_preparation %}
<li>Special preparation required - see appointment type instructions</li>
{% endif %}
<li>Wear comfortable, loose-fitting clothing</li>
<li>Arrange transportation if sedation will be used</li>
<li>Bring a list of questions for the provider</li>
</ul>
</div>
</div>
</div>
@ -252,6 +251,7 @@
</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">
<h1 class="h2">
<i class="fas fa-calendar-alt"></i> Appointment Dashboard
</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>
<div>
<h1 class="h2">
<i class="fas fa-calendar-alt"></i> Appointment<span class="fw-light">Dashboard</span>
</h1>
<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,278 +4,278 @@
{% block title %}Mark No Show{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<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">No Show</li>
</ul>
<h1 class="page-header">Mark Appointment as No Show</h1>
<div class="container">
<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">No Show</li>
</ul>
<!-- Appointment Info -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Appointment Details</h4>
</div>
<div class="card-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>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Phone:</strong></div>
<div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
<h1 class="page-header">Mark Appointment as No Show</h1>
<!-- Appointment Info -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Appointment Details</h4>
</div>
<div class="card-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>
<div class="col-md-6">
<div class="row mb-2">
<div class="col-4"><strong>Date:</strong></div>
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Time:</strong></div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Type:</strong></div>
<div class="col-8">{{ appointment.appointment_type.name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Patient ID:</strong></div>
<div class="col-8">{{ appointment.patient.patient_id }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Phone:</strong></div>
<div class="col-8">{{ appointment.patient.phone_number|default:"Not provided" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Provider:</strong></div>
<div class="col-8">{{ appointment.provider.first_name }} {{ appointment.provider.last_name }}</div>
</div>
</div>
<div class="col-md-6">
<div class="row mb-2">
<div class="col-4"><strong>Date:</strong></div>
<div class="col-8">{{ appointment.appointment_date|date:"M d, Y" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Time:</strong></div>
<div class="col-8">{{ appointment.appointment_time|time:"g:i A" }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Department:</strong></div>
<div class="col-8">{{ appointment.department.name }}</div>
</div>
<div class="row mb-2">
<div class="col-4"><strong>Type:</strong></div>
<div class="col-8">{{ appointment.appointment_type.name }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- No Show Form -->
<div class="card">
<div class="card-header">
<h4 class="card-title">No Show Documentation</h4>
<!-- No Show Form -->
<div class="card">
<div class="card-header">
<h4 class="card-title">No Show Documentation</h4>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle me-2"></i>
<strong>Important:</strong> Please ensure you have attempted to contact the patient before marking as no show.
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle me-2"></i>
<strong>Important:</strong> Please ensure you have attempted to contact the patient before marking as no show.
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Wait Time</label>
<div class="col-md-9">
<div class="input-group">
<input type="number" name="wait_time" class="form-control"
value="{{ form.wait_time.value|default:'15' }}" min="0" max="120">
<span class="input-group-text">minutes</span>
</div>
<small class="form-text text-muted">How long did you wait for the patient?</small>
{% if form.wait_time.errors %}
<div class="text-danger">{{ form.wait_time.errors.0 }}</div>
{% endif %}
</div>
</div>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Wait Time</label>
<div class="col-md-9">
<div class="input-group">
<input type="number" name="wait_time" class="form-control"
value="{{ form.wait_time.value|default:'15' }}" min="0" max="120">
<span class="input-group-text">minutes</span>
</div>
<small class="form-text text-muted">How long did you wait for the patient?</small>
{% if form.wait_time.errors %}
<div class="text-danger">{{ form.wait_time.errors.0 }}</div>
{% endif %}
<div class="row mb-3">
<label class="col-form-label col-md-3">Contact Attempts</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="phone_attempted" id="phone_attempted"
{% if form.phone_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="phone_attempted">
Phone call attempted
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="text_attempted" id="text_attempted"
{% if form.text_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="text_attempted">
Text message sent
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="email_attempted" id="email_attempted"
{% if form.email_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="email_attempted">
Email sent
</label>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Contact Attempts</label>
<div class="col-md-9">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="phone_attempted" id="phone_attempted"
{% if form.phone_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="phone_attempted">
Phone call attempted
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="text_attempted" id="text_attempted"
{% if form.text_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="text_attempted">
Text message sent
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="email_attempted" id="email_attempted"
{% if form.email_attempted.value %}checked{% endif %}>
<label class="form-check-label" for="email_attempted">
Email sent
</label>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Number of Contact Attempts</label>
<div class="col-md-9">
<select name="contact_attempts" class="form-select">
<option value="1" {% if form.contact_attempts.value == '1' %}selected{% endif %}>1 attempt</option>
<option value="2" {% if form.contact_attempts.value == '2' %}selected{% endif %}>2 attempts</option>
<option value="3" {% if form.contact_attempts.value == '3' %}selected{% endif %}>3 attempts</option>
<option value="4" {% if form.contact_attempts.value == '4' %}selected{% endif %}>4+ attempts</option>
</select>
{% if form.contact_attempts.errors %}
<div class="text-danger">{{ form.contact_attempts.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">No Show Reason</label>
<div class="col-md-9">
<select name="no_show_reason" class="form-select">
<option value="">Select Reason (if known)</option>
<option value="forgot" {% if form.no_show_reason.value == 'forgot' %}selected{% endif %}>Patient forgot appointment</option>
<option value="illness" {% if form.no_show_reason.value == 'illness' %}selected{% endif %}>Patient became ill</option>
<option value="emergency" {% if form.no_show_reason.value == 'emergency' %}selected{% endif %}>Emergency situation</option>
<option value="transportation" {% if form.no_show_reason.value == 'transportation' %}selected{% endif %}>Transportation issues</option>
<option value="work_conflict" {% if form.no_show_reason.value == 'work_conflict' %}selected{% endif %}>Work conflict</option>
<option value="weather" {% if form.no_show_reason.value == 'weather' %}selected{% endif %}>Weather conditions</option>
<option value="financial" {% if form.no_show_reason.value == 'financial' %}selected{% endif %}>Financial concerns</option>
<option value="no_response" {% if form.no_show_reason.value == 'no_response' %}selected{% endif %}>No response to contact attempts</option>
<option value="other" {% if form.no_show_reason.value == 'other' %}selected{% endif %}>Other</option>
</select>
{% if form.no_show_reason.errors %}
<div class="text-danger">{{ form.no_show_reason.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">No Show Fee</label>
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" name="no_show_fee" class="form-control"
step="0.01" min="0" value="{{ form.no_show_fee.value|default:'25.00' }}">
</div>
<small class="form-text text-muted">Standard no-show fee as per policy</small>
{% if form.no_show_fee.errors %}
<div class="text-danger">{{ form.no_show_fee.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Documented By</label>
<div class="col-md-9">
<input type="text" name="documented_by" class="form-control"
value="{{ form.documented_by.value|default:request.user.get_full_name }}" readonly>
{% if form.documented_by.errors %}
<div class="text-danger">{{ form.documented_by.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Notes</label>
<div class="col-md-9">
<textarea name="no_show_notes" class="form-control" rows="4"
placeholder="Additional details about the no-show incident, contact attempts, or patient communication">{{ form.no_show_notes.value|default:'' }}</textarea>
{% if form.no_show_notes.errors %}
<div class="text-danger">{{ form.no_show_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="reschedule_offered" id="reschedule_offered"
{% if form.reschedule_offered.value %}checked{% endif %}>
<label class="form-check-label" for="reschedule_offered">
Rescheduling was offered to patient
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="add_to_waitlist" id="add_to_waitlist"
{% if form.add_to_waitlist.value %}checked{% endif %}>
<label class="form-check-label" for="add_to_waitlist">
Add patient to waitlist for future appointments
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="send_follow_up" id="send_follow_up"
{% if form.send_follow_up.value %}checked{% endif %}>
<label class="form-check-label" for="send_follow_up">
Send follow-up communication to patient
</label>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Number of Contact Attempts</label>
<div class="col-md-9">
<select name="contact_attempts" class="form-select">
<option value="1" {% if form.contact_attempts.value == '1' %}selected{% endif %}>1 attempt</option>
<option value="2" {% if form.contact_attempts.value == '2' %}selected{% endif %}>2 attempts</option>
<option value="3" {% if form.contact_attempts.value == '3' %}selected{% endif %}>3 attempts</option>
<option value="4" {% if form.contact_attempts.value == '4' %}selected{% endif %}>4+ attempts</option>
</select>
{% if form.contact_attempts.errors %}
<div class="text-danger">{{ form.contact_attempts.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">No Show Reason</label>
<div class="col-md-9">
<select name="no_show_reason" class="form-select">
<option value="">Select Reason (if known)</option>
<option value="forgot" {% if form.no_show_reason.value == 'forgot' %}selected{% endif %}>Patient forgot appointment</option>
<option value="illness" {% if form.no_show_reason.value == 'illness' %}selected{% endif %}>Patient became ill</option>
<option value="emergency" {% if form.no_show_reason.value == 'emergency' %}selected{% endif %}>Emergency situation</option>
<option value="transportation" {% if form.no_show_reason.value == 'transportation' %}selected{% endif %}>Transportation issues</option>
<option value="work_conflict" {% if form.no_show_reason.value == 'work_conflict' %}selected{% endif %}>Work conflict</option>
<option value="weather" {% if form.no_show_reason.value == 'weather' %}selected{% endif %}>Weather conditions</option>
<option value="financial" {% if form.no_show_reason.value == 'financial' %}selected{% endif %}>Financial concerns</option>
<option value="no_response" {% if form.no_show_reason.value == 'no_response' %}selected{% endif %}>No response to contact attempts</option>
<option value="other" {% if form.no_show_reason.value == 'other' %}selected{% endif %}>Other</option>
</select>
{% if form.no_show_reason.errors %}
<div class="text-danger">{{ form.no_show_reason.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">No Show Fee</label>
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" name="no_show_fee" class="form-control"
step="0.01" min="0" value="{{ form.no_show_fee.value|default:'25.00' }}">
</div>
<small class="form-text text-muted">Standard no-show fee as per policy</small>
{% if form.no_show_fee.errors %}
<div class="text-danger">{{ form.no_show_fee.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Documented By</label>
<div class="col-md-9">
<input type="text" name="documented_by" class="form-control"
value="{{ form.documented_by.value|default:request.user.get_full_name }}" readonly>
{% if form.documented_by.errors %}
<div class="text-danger">{{ form.documented_by.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<label class="col-form-label col-md-3">Notes</label>
<div class="col-md-9">
<textarea name="no_show_notes" class="form-control" rows="4"
placeholder="Additional details about the no-show incident, contact attempts, or patient communication">{{ form.no_show_notes.value|default:'' }}</textarea>
{% if form.no_show_notes.errors %}
<div class="text-danger">{{ form.no_show_notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-9 offset-md-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="reschedule_offered" id="reschedule_offered"
{% if form.reschedule_offered.value %}checked{% endif %}>
<label class="form-check-label" for="reschedule_offered">
Rescheduling was offered to patient
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="add_to_waitlist" id="add_to_waitlist"
{% if form.add_to_waitlist.value %}checked{% endif %}>
<label class="form-check-label" for="add_to_waitlist">
Add patient to waitlist for future appointments
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="send_follow_up" id="send_follow_up"
{% if form.send_follow_up.value %}checked{% endif %}>
<label class="form-check-label" for="send_follow_up">
Send follow-up communication to patient
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-warning" onclick="return confirm('Are you sure you want to mark this appointment as no show? This will apply the no-show fee and update the patient record.')">
<i class="fa fa-exclamation-triangle me-2"></i>Mark as No Show
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div>
</div>
<!-- No Show Policy -->
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">No Show Policy</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Policy Guidelines:</h6>
<ul>
<li>Wait minimum 15 minutes past appointment time</li>
<li>Make at least 2 contact attempts</li>
<li>Document all contact attempts</li>
<li>Apply standard no-show fee ($25.00)</li>
<li>Offer rescheduling when possible</li>
</ul>
</div>
<div class="col-md-6">
<h6>Patient Impact:</h6>
<ul>
<li>No-show fee will be added to patient account</li>
<li>Appointment slot becomes available for other patients</li>
<li>Patient's no-show history is tracked</li>
<li>Multiple no-shows may affect future scheduling</li>
<li>Follow-up communication will be sent</li>
</ul>
<div class="col-md-9 offset-md-3">
<button type="submit" class="btn btn-warning" onclick="return confirm('Are you sure you want to mark this appointment as no show? This will apply the no-show fee and update the patient record.')">
<i class="fa fa-exclamation-triangle me-2"></i>Mark as No Show
</button>
<a href="{% url 'appointments:appointment_detail' appointment.id %}" class="btn btn-secondary ms-2">
<i class="fa fa-arrow-left me-2"></i>Go Back
</a>
</div>
</div>
</form>
</div>
</div>
<!-- No Show Policy -->
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title">No Show Policy</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Policy Guidelines:</h6>
<ul>
<li>Wait minimum 15 minutes past appointment time</li>
<li>Make at least 2 contact attempts</li>
<li>Document all contact attempts</li>
<li>Apply standard no-show fee ($25.00)</li>
<li>Offer rescheduling when possible</li>
</ul>
</div>
<div class="col-md-6">
<h6>Patient Impact:</h6>
<ul>
<li>No-show fee will be added to patient account</li>
<li>Appointment slot becomes available for other patients</li>
<li>Patient's no-show history is tracked</li>
<li>Multiple no-shows may affect future scheduling</li>
<li>Follow-up communication will be sent</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% 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>
<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 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>
<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 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>
<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 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>
<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 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>
<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 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>
</span>
{% if log.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>
</div>
</div>
{% endif %}
<div class="row mb-1">
<div class="col-md-6">
{% if log.next_contact_date %}
<small class="text-muted">
<span class="fw-bold">Contacted by:</span> {{ log.contacted_by.get_full_name }}
</small>
{% endif %}
</div>
<div class="col-md-6">
{% if log.next_contact_date %}
<small class="text-muted">
<span class="fw-bold">Next Contact:</span> {{ log.next_contact_date|date:"M d, Y" }}
</small>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<p class="mb-1">{{ log.notes|default:"No notes." }}</p>
{% if log.appointment_offered %}
<p class="mb-1 text-primary">
<i class="fas fa-calendar-check me-1"></i>Appointment Offered:
{{ log.offered_date|date:"M d, Y" }} at {{ log.offered_time|time:"g:i A" }}
</p>
<p class="mb-0 text-primary">
<i class="fas fa-reply me-1"></i>Patient Response:
<span class="badge bg-{% if log.patient_response == 'ACCEPTED' %}success{% elif log.patient_response == 'DECLINED' %}danger{% else %}secondary{% endif %}">
{{ log.get_patient_response_display }}
</span>
</p>
{% endif %}
{% if log.next_contact_date %}
<p class="mb-0 text-info">
<i class="fas fa-calendar-alt me-1"></i>Next Contact: {{ log.next_contact_date|date:"M d, Y" }}
</p>
{% endif %}
<small class="text-muted">Contacted by: {{ log.contacted_by.get_full_name|default:"N/A" }}</small>
</div>
{% empty %}
<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 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>
{% 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>
<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>
</div>
<div class="ms-auto">
<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-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="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;
/* Title */
.swal2-title {
font-size: 20px;
font-weight: bold;
{#color: #0b505d;#}
}
.stat-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
/* Confirm button */
.swal2-confirm {
background-color: #155724 !important;
color: #fff !important;
border-radius: 8px !important;
}
.stat-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
/* Cancel button */
.swal2-cancel {
background-color: #adb5bd !important;
color: #fff !important;
}
.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;
/* Icon color override */
.swal2-icon.swal2-warning {
border-color: #f59c1a !important;
color: #f59c1a !important;
}
.stat-icon.primary { background: #007bff; }
.stat-icon.success { background: #28a745; }
.stat-icon.warning { background: #ffc107; }
.stat-icon.info { background: #17a2b8; }
.stat-icon.danger { background: #dc3545; }
.stat-number {
font-size: 2.5rem;
font-weight: bold;
color: #495057;
margin-bottom: 0.5rem;
}
.stat-label {
color: #6c757d;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
}
.queue-info-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #f8f9fa;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-weight: 600;
color: #495057;
}
.info-value {
color: #6c757d;
}
.queue-type-badge {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
}
.type-provider { background: #d4edda; color: #155724; }
.type-specialty { background: #d1ecf1; color: #0c5460; }
.type-location { background: #fff3cd; color: #856404; }
.type-procedure { background: #f8d7da; color: #721c24; }
.type-emergency { background: #f5c6cb; color: #721c24; }
.status-badge {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
}
.status-active { background: #d4edda; color: #155724; }
.status-inactive { background: #f8d7da; color: #721c24; }
.entry-status {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-waiting { background: #fff3cd; color: #856404; }
.status-called { background: #d1ecf1; color: #0c5460; }
.status-in-service { background: #cce5ff; color: #004085; }
.status-completed { background: #d4edda; color: #155724; }
.status-left { background: #f8d7da; color: #721c24; }
.status-no-show { background: #f5c6cb; color: #721c24; }
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.stat-card {
padding: 1rem;
}
.stat-number {
font-size: 2rem;
}
}
</style>
{% 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: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>
</div>
<div class="ms-auto">
<div class="btn-group">
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}" class="btn btn-warning">
<i class="fas fa-edit me-1"></i>Edit Queue
</a>
<button class="btn 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()">
<i class="fas fa-download me-2"></i>Export Data
</a></li>
</ul>
</div>
</div>
<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 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="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-sm btn-success" onclick="refreshQueue()">
<i class="fas fa-sync-alt me-1"></i>Refresh
</button>
<button class="btn btn-sm btn-danger" onclick="exportQueue()">
<i class="fas fa-download me-2"></i>Export Data
</button>
</div>
</div>
</div>
<div class="container-fluid">
<!-- Queue Header -->
<div class="queue-header">
<div class="row align-items-center">
<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="card mb-3">
<div class="card-body">
<div class="row">
<div class="col-md-8">
<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>
</span>
<span class="badge bg-{% if queue.is_active %}success{% else %}danger{% endif %} me-2">
{% if queue.is_active %}{{ _("Active") }}{% else %}{{ _("Inactive") }}{% endif %}
</span>
</div>
</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="h4 mb-1">{{ queue.current_queue_size }}/{{ queue.max_queue_size }}</div>
<div class="opacity-75">Current Capacity</div>
<div class="col-md-4 text-md-end">
<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>
</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>
<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 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="info-item">
<span class="info-label">Last Updated:</span>
<span class="info-value">{{ queue.updated_at|timesince }} ago</span>
<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>
<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>
</div>
<div class="ms-auto">
<!-- 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-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="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">
<h1 class="h2">
<i class="fas fa-users-gear"></i> Queue<span class="fw-light">Management</span>
</h1>
<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>
<button type="button" class="btn btn-outline-primary">
<i class="fas fa-video"></i> Telemedicine
</button>
</div>
<button type="button" class="btn btn-sm btn-primary">
<i class="fas fa-video"></i> Telemedicine
</button>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-users me-2"></i>Active Queues
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<div class="row">
{% for queue in queues %}
{% if queue %}
<div class="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 class="panel panel-inverse" data-sortable-id="index-{{ queue.queue_id }}">
<div class="panel-heading">
<h4 class="panel-title">
{{ 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="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 }}
@ -70,18 +70,16 @@
</div>
</div>
</div>
{% endif %}
{% empty %}
<div class="col-12">
<div class="text-center py-5">
<i class="fas fa-users fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Active Queues</h5>
<p class="text-muted">No queues are currently active.</p>
</div>
{% endif %}
{% empty %}
<div class="col-12">
<div class="text-center py-5">
<i class="fas fa-users fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Active Queues</h5>
<p class="text-muted">No queues are currently active.</p>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>

View File

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