update
This commit is contained in:
parent
beba30e532
commit
2780a2dc7c
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
accounts/__pycache__/signals.cpython-312.pyc
Normal file
BIN
accounts/__pycache__/signals.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -3,75 +3,45 @@ Admin configuration for accounts app.
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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"],
|
||||
},
|
||||
|
||||
@ -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(
|
||||
|
||||
18
accounts/migrations/0003_alter_user_is_active.py
Normal file
18
accounts/migrations/0003_alter_user_is_active.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-15 14:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0002_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="is_active",
|
||||
field=models.BooleanField(default=True, help_text="User account is active"),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -4,6 +4,8 @@ Provides user management, authentication, and authorization functionality.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
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
41
accounts/signals.py
Normal file
@ -0,0 +1,41 @@
|
||||
# accounts/signals.py
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.db import transaction
|
||||
from django.utils.text import slugify
|
||||
|
||||
from hr.models import Employee
|
||||
from .models import User
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_employee_for_user(sender, instance: User, created: bool, **kwargs):
|
||||
"""
|
||||
Auto-create an Employee profile when a User is created.
|
||||
Idempotent, tenant-aligned, and makes a unique employee_number per tenant.
|
||||
"""
|
||||
if not created:
|
||||
return
|
||||
|
||||
def _make_employee():
|
||||
# Generate a readable employee_number from username/email, unique within tenant
|
||||
base = (instance.username or (instance.email or 'user')).split('@')[0]
|
||||
candidate = slugify(base)[:16] or 'emp'
|
||||
suffix = 1
|
||||
emp_no = candidate
|
||||
while Employee.objects.filter(tenant=instance.tenant, employee_number=emp_no).exists():
|
||||
suffix += 1
|
||||
emp_no = f"{candidate}-{suffix}"
|
||||
|
||||
# Create with basic info mirrored from User
|
||||
Employee.objects.create(
|
||||
tenant=instance.tenant,
|
||||
user=instance,
|
||||
employee_number=emp_no,
|
||||
first_name=instance.first_name or '',
|
||||
last_name=instance.last_name or '',
|
||||
email=instance.email
|
||||
)
|
||||
|
||||
# Defer until after outer transaction commits (avoids race conditions in tests/views)
|
||||
transaction.on_commit(_make_employee)
|
||||
@ -124,8 +124,8 @@
|
||||
<div class="row align-items-center">
|
||||
<div class="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>
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
350
accounts_data.py
350
accounts_data.py
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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.
Binary file not shown.
BIN
appointments/api/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
appointments/api/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
appointments/api/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
appointments/api/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
appointments/api/__pycache__/urls.cpython-312.pyc
Normal file
BIN
appointments/api/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
appointments/api/__pycache__/views.cpython-312.pyc
Normal file
BIN
appointments/api/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
@ -353,7 +353,7 @@ class AppointmentSearchForm(forms.Form):
|
||||
self.fields['provider'].queryset = User.objects.filter(
|
||||
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()
|
||||
}),
|
||||
|
||||
@ -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=[
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-16 12:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("appointments", "0002_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="waitinglist",
|
||||
name="acceptable_days",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="Acceptable days of week (0=Monday, 6=Sunday)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,688 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-11 17:03
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("appointments", "0002_initial"),
|
||||
("core", "0001_initial"),
|
||||
("hr", "0001_initial"),
|
||||
("patients", "0003_remove_insuranceinfo_subscriber_ssn_and_more"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="WaitingList",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"waiting_list_id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
help_text="Unique waiting list entry identifier",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"appointment_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("CONSULTATION", "Consultation"),
|
||||
("FOLLOW_UP", "Follow-up"),
|
||||
("PROCEDURE", "Procedure"),
|
||||
("SURGERY", "Surgery"),
|
||||
("DIAGNOSTIC", "Diagnostic"),
|
||||
("THERAPY", "Therapy"),
|
||||
("VACCINATION", "Vaccination"),
|
||||
("SCREENING", "Screening"),
|
||||
("EMERGENCY", "Emergency"),
|
||||
("TELEMEDICINE", "Telemedicine"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
help_text="Type of appointment requested",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
(
|
||||
"specialty",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("FAMILY_MEDICINE", "Family Medicine"),
|
||||
("INTERNAL_MEDICINE", "Internal Medicine"),
|
||||
("PEDIATRICS", "Pediatrics"),
|
||||
("CARDIOLOGY", "Cardiology"),
|
||||
("DERMATOLOGY", "Dermatology"),
|
||||
("ENDOCRINOLOGY", "Endocrinology"),
|
||||
("GASTROENTEROLOGY", "Gastroenterology"),
|
||||
("NEUROLOGY", "Neurology"),
|
||||
("ONCOLOGY", "Oncology"),
|
||||
("ORTHOPEDICS", "Orthopedics"),
|
||||
("PSYCHIATRY", "Psychiatry"),
|
||||
("RADIOLOGY", "Radiology"),
|
||||
("SURGERY", "Surgery"),
|
||||
("UROLOGY", "Urology"),
|
||||
("GYNECOLOGY", "Gynecology"),
|
||||
("OPHTHALMOLOGY", "Ophthalmology"),
|
||||
("ENT", "Ear, Nose & Throat"),
|
||||
("EMERGENCY", "Emergency Medicine"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
help_text="Medical specialty required",
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
(
|
||||
"priority",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("ROUTINE", "Routine"),
|
||||
("URGENT", "Urgent"),
|
||||
("STAT", "STAT"),
|
||||
("EMERGENCY", "Emergency"),
|
||||
],
|
||||
default="ROUTINE",
|
||||
help_text="Clinical priority level",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"urgency_score",
|
||||
models.PositiveIntegerField(
|
||||
default=1,
|
||||
help_text="Clinical urgency score (1-10, 10 being most urgent)",
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(10),
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
"clinical_indication",
|
||||
models.TextField(
|
||||
help_text="Clinical reason for appointment request"
|
||||
),
|
||||
),
|
||||
(
|
||||
"diagnosis_codes",
|
||||
models.JSONField(
|
||||
blank=True, default=list, help_text="ICD-10 diagnosis codes"
|
||||
),
|
||||
),
|
||||
(
|
||||
"preferred_date",
|
||||
models.DateField(
|
||||
blank=True,
|
||||
help_text="Patient preferred appointment date",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"preferred_time",
|
||||
models.TimeField(
|
||||
blank=True,
|
||||
help_text="Patient preferred appointment time",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"flexible_scheduling",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Patient accepts alternative dates/times",
|
||||
),
|
||||
),
|
||||
(
|
||||
"earliest_acceptable_date",
|
||||
models.DateField(
|
||||
blank=True,
|
||||
help_text="Earliest acceptable appointment date",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"latest_acceptable_date",
|
||||
models.DateField(
|
||||
blank=True,
|
||||
help_text="Latest acceptable appointment date",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"acceptable_days",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="Acceptable days of week (0=Monday, 6=Sunday)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"acceptable_times",
|
||||
models.JSONField(
|
||||
blank=True, default=list, help_text="Acceptable time ranges"
|
||||
),
|
||||
),
|
||||
(
|
||||
"contact_method",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("PHONE", "Phone"),
|
||||
("EMAIL", "Email"),
|
||||
("SMS", "SMS"),
|
||||
("PORTAL", "Patient Portal"),
|
||||
("MAIL", "Mail"),
|
||||
],
|
||||
default="PHONE",
|
||||
help_text="Preferred contact method",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"contact_phone",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Contact phone number",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"contact_email",
|
||||
models.EmailField(
|
||||
blank=True,
|
||||
help_text="Contact email address",
|
||||
max_length=254,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("ACTIVE", "Active"),
|
||||
("CONTACTED", "Contacted"),
|
||||
("OFFERED", "Appointment Offered"),
|
||||
("SCHEDULED", "Scheduled"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
("EXPIRED", "Expired"),
|
||||
("TRANSFERRED", "Transferred"),
|
||||
],
|
||||
default="ACTIVE",
|
||||
help_text="Waiting list status",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"position",
|
||||
models.PositiveIntegerField(
|
||||
blank=True,
|
||||
help_text="Position in waiting list queue",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"estimated_wait_time",
|
||||
models.PositiveIntegerField(
|
||||
blank=True, help_text="Estimated wait time in days", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_contacted",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="Last contact attempt date/time",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"contact_attempts",
|
||||
models.PositiveIntegerField(
|
||||
default=0, help_text="Number of contact attempts made"
|
||||
),
|
||||
),
|
||||
(
|
||||
"max_contact_attempts",
|
||||
models.PositiveIntegerField(
|
||||
default=3, help_text="Maximum contact attempts before expiring"
|
||||
),
|
||||
),
|
||||
(
|
||||
"appointments_offered",
|
||||
models.PositiveIntegerField(
|
||||
default=0, help_text="Number of appointments offered"
|
||||
),
|
||||
),
|
||||
(
|
||||
"appointments_declined",
|
||||
models.PositiveIntegerField(
|
||||
default=0, help_text="Number of appointments declined"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_offer_date",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="Date of last appointment offer",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"requires_interpreter",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Patient requires interpreter services"
|
||||
),
|
||||
),
|
||||
(
|
||||
"interpreter_language",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Required interpreter language",
|
||||
max_length=50,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"accessibility_requirements",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Special accessibility requirements",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"transportation_needed",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Patient needs transportation assistance",
|
||||
),
|
||||
),
|
||||
(
|
||||
"insurance_verified",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Insurance coverage verified"
|
||||
),
|
||||
),
|
||||
(
|
||||
"authorization_required",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Prior authorization required"
|
||||
),
|
||||
),
|
||||
(
|
||||
"authorization_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("NOT_REQUIRED", "Not Required"),
|
||||
("PENDING", "Pending"),
|
||||
("APPROVED", "Approved"),
|
||||
("DENIED", "Denied"),
|
||||
("EXPIRED", "Expired"),
|
||||
],
|
||||
default="NOT_REQUIRED",
|
||||
help_text="Authorization status",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"authorization_number",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Authorization number",
|
||||
max_length=100,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"referring_provider",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Referring provider name",
|
||||
max_length=200,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"referral_date",
|
||||
models.DateField(
|
||||
blank=True, help_text="Date of referral", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"referral_urgency",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("ROUTINE", "Routine"),
|
||||
("URGENT", "Urgent"),
|
||||
("STAT", "STAT"),
|
||||
],
|
||||
default="ROUTINE",
|
||||
help_text="Referral urgency level",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"removal_reason",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("SCHEDULED", "Appointment Scheduled"),
|
||||
("PATIENT_CANCELLED", "Patient Cancelled"),
|
||||
("PROVIDER_CANCELLED", "Provider Cancelled"),
|
||||
("NO_RESPONSE", "No Response to Contact"),
|
||||
("INSURANCE_ISSUE", "Insurance Issue"),
|
||||
("TRANSFERRED", "Transferred to Another Provider"),
|
||||
("EXPIRED", "Entry Expired"),
|
||||
("DUPLICATE", "Duplicate Entry"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
help_text="Reason for removal from waiting list",
|
||||
max_length=50,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"removal_notes",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Additional notes about removal",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"removed_at",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="Date/time removed from waiting list",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"notes",
|
||||
models.TextField(
|
||||
blank=True, help_text="Additional notes and comments", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="User who created the waiting list entry",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="created_waiting_list_entries",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"department",
|
||||
models.ForeignKey(
|
||||
help_text="Department for appointment",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="waiting_list_entries",
|
||||
to="hr.department",
|
||||
),
|
||||
),
|
||||
(
|
||||
"patient",
|
||||
models.ForeignKey(
|
||||
help_text="Patient on waiting list",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="waiting_list_entries",
|
||||
to="patients.patientprofile",
|
||||
),
|
||||
),
|
||||
(
|
||||
"provider",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Preferred healthcare provider",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="provider_waiting_list",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"removed_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="User who removed entry from waiting list",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="removed_waiting_list_entries",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"scheduled_appointment",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Scheduled appointment from waiting list",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="waiting_list_entry",
|
||||
to="appointments.appointmentrequest",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
help_text="Organization tenant",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="waiting_list_entries",
|
||||
to="core.tenant",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Waiting List Entry",
|
||||
"verbose_name_plural": "Waiting List Entries",
|
||||
"db_table": "appointments_waiting_list",
|
||||
"ordering": ["priority", "urgency_score", "created_at"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="WaitingListContactLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"contact_date",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, help_text="Date and time of contact attempt"
|
||||
),
|
||||
),
|
||||
(
|
||||
"contact_method",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("PHONE", "Phone Call"),
|
||||
("EMAIL", "Email"),
|
||||
("SMS", "SMS"),
|
||||
("PORTAL", "Patient Portal Message"),
|
||||
("MAIL", "Mail"),
|
||||
("IN_PERSON", "In Person"),
|
||||
],
|
||||
help_text="Method of contact used",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"contact_outcome",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("SUCCESSFUL", "Successful Contact"),
|
||||
("NO_ANSWER", "No Answer"),
|
||||
("BUSY", "Line Busy"),
|
||||
("VOICEMAIL", "Left Voicemail"),
|
||||
("EMAIL_SENT", "Email Sent"),
|
||||
("EMAIL_BOUNCED", "Email Bounced"),
|
||||
("SMS_SENT", "SMS Sent"),
|
||||
("SMS_FAILED", "SMS Failed"),
|
||||
("WRONG_NUMBER", "Wrong Number"),
|
||||
("DECLINED", "Patient Declined"),
|
||||
],
|
||||
help_text="Outcome of contact attempt",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"appointment_offered",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Appointment was offered during contact",
|
||||
),
|
||||
),
|
||||
(
|
||||
"offered_date",
|
||||
models.DateField(
|
||||
blank=True, help_text="Date of offered appointment", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"offered_time",
|
||||
models.TimeField(
|
||||
blank=True, help_text="Time of offered appointment", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"patient_response",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("ACCEPTED", "Accepted Appointment"),
|
||||
("DECLINED", "Declined Appointment"),
|
||||
("REQUESTED_DIFFERENT", "Requested Different Time"),
|
||||
("WILL_CALL_BACK", "Will Call Back"),
|
||||
("NO_LONGER_NEEDED", "No Longer Needed"),
|
||||
("INSURANCE_ISSUE", "Insurance Issue"),
|
||||
("NO_RESPONSE", "No Response"),
|
||||
],
|
||||
help_text="Patient response to contact",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"notes",
|
||||
models.TextField(
|
||||
blank=True, help_text="Notes from contact attempt", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"next_contact_date",
|
||||
models.DateField(
|
||||
blank=True,
|
||||
help_text="Scheduled date for next contact attempt",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"contacted_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Staff member who made contact",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"waiting_list_entry",
|
||||
models.ForeignKey(
|
||||
help_text="Associated waiting list entry",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="contact_logs",
|
||||
to="appointments.waitinglist",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Waiting List Contact Log",
|
||||
"verbose_name_plural": "Waiting List Contact Logs",
|
||||
"db_table": "appointments_waiting_list_contact_log",
|
||||
"ordering": ["-contact_date"],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="waitinglist",
|
||||
index=models.Index(
|
||||
fields=["tenant", "status"], name="appointment_tenant__a558da_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="waitinglist",
|
||||
index=models.Index(
|
||||
fields=["patient", "status"], name="appointment_patient_73f03d_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="waitinglist",
|
||||
index=models.Index(
|
||||
fields=["department", "specialty", "status"],
|
||||
name="appointment_departm_78fd70_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="waitinglist",
|
||||
index=models.Index(
|
||||
fields=["priority", "urgency_score"],
|
||||
name="appointment_priorit_30fb90_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="waitinglist",
|
||||
index=models.Index(
|
||||
fields=["status", "created_at"], name="appointment_status_cfe551_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="waitinglist",
|
||||
index=models.Index(
|
||||
fields=["provider", "status"], name="appointment_provide_dd6c2b_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="waitinglistcontactlog",
|
||||
index=models.Index(
|
||||
fields=["waiting_list_entry", "contact_date"],
|
||||
name="appointment_waiting_50d8ac_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="waitinglistcontactlog",
|
||||
index=models.Index(
|
||||
fields=["contact_outcome"], name="appointment_contact_ad9c45_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="waitinglistcontactlog",
|
||||
index=models.Index(
|
||||
fields=["next_contact_date"], name="appointment_next_co_b29984_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1371,6 +1371,7 @@ class WaitingList(models.Model):
|
||||
|
||||
acceptable_days = models.JSONField(
|
||||
default=list,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text='Acceptable days of week (0=Monday, 6=Sunday)'
|
||||
)
|
||||
|
||||
BIN
appointments/templates/appointments/.DS_Store
vendored
BIN
appointments/templates/appointments/.DS_Store
vendored
Binary file not shown.
@ -9,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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
182
appointments/templates/appointments/calendar.html
Normal file
182
appointments/templates/appointments/calendar.html
Normal file
@ -0,0 +1,182 @@
|
||||
{# templates/appointments/calendar.html #}
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Appointments Calendar{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
.fc .fc-toolbar-title { font-weight: 600; }
|
||||
.calendar-wrapper { min-height: 70vh; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<div>
|
||||
<h1 class="h2">
|
||||
<i class="fas fa-calendar-alt"></i> Scheduling<span class="fw-light">Calendar</span>
|
||||
</h1>
|
||||
<p class="text-muted">View your calendar and manage schedules.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div id="calendar" class="calendar-wrapper"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-header fw-bold">Appointment Details</div>
|
||||
<div class="card-body" id="appt-details">
|
||||
<div class="text-muted small">Click an event to see details.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header fw-bold">Filters</div>
|
||||
<div class="card-body">
|
||||
<form id="calendarFilters">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All</option>
|
||||
<option value="PENDING">Pending</option>
|
||||
<option value="CONFIRMED">Confirmed</option>
|
||||
<option value="CHECKED_IN">Checked-in</option>
|
||||
<option value="IN_PROGRESS">In progress</option>
|
||||
<option value="COMPLETED">Completed</option>
|
||||
<option value="CANCELLED">Cancelled</option>
|
||||
<option value="NO_SHOW">No show</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="provider" class="form-label">Provider</label>
|
||||
<input id="provider" class="form-control" name="provider_id" placeholder="Provider ID (optional)">
|
||||
</div>
|
||||
<button type="button" id="applyFilters" class="btn btn-theme w-100">Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Optional: Bootstrap modal for full details #}
|
||||
<div class="modal fade" id="apptModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title">Appointment</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="apptModalBody">
|
||||
<!-- HTMX fills here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'plugins/moment/min/moment.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/@fullcalendar/core/index.global.js' %}"></script>
|
||||
<script src="{% static 'plugins/@fullcalendar/daygrid/index.global.js' %}"></script>
|
||||
<script src="{% static 'plugins/@fullcalendar/timegrid/index.global.js' %}"></script>
|
||||
<script src="{% static 'plugins/@fullcalendar/interaction/index.global.js' %}"></script>
|
||||
<script src="{% static 'plugins/@fullcalendar/list/index.global.js' %}"></script>
|
||||
<script src="{% static 'plugins/@fullcalendar/bootstrap/index.global.js' %}"></script>
|
||||
<script>
|
||||
(function(){
|
||||
const calEl = document.getElementById('calendar');
|
||||
const detailsEl = document.getElementById('appt-details');
|
||||
const filtersForm = document.getElementById('calendarFilters');
|
||||
|
||||
function buildEventsUrl(info){
|
||||
const base = "{% url 'appointments:calendar_events' %}";
|
||||
const params = new URLSearchParams({
|
||||
start: info.startStr,
|
||||
end: info.endStr,
|
||||
});
|
||||
|
||||
const status = filtersForm.querySelector('[name=status]').value;
|
||||
const provider_id = filtersForm.querySelector('[name=provider_id]').value;
|
||||
if (status) params.set('status', status);
|
||||
if (provider_id) params.set('provider_id', provider_id);
|
||||
return `${base}?${params.toString()}`;
|
||||
}
|
||||
|
||||
const calendar = new FullCalendar.Calendar(calEl, {
|
||||
timeZone: 'Asia/Riyadh',
|
||||
initialView: 'timeGridWeek',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
||||
},
|
||||
slotMinTime: '07:00:00',
|
||||
slotMaxTime: '22:00:00',
|
||||
nowIndicator: true,
|
||||
navLinks: true,
|
||||
selectable: false,
|
||||
editable: true, // allow drag/resize
|
||||
eventDurationEditable: true,
|
||||
eventSources: [{
|
||||
events: function(info, success, failure){
|
||||
fetch(buildEventsUrl(info), {credentials: 'same-origin'})
|
||||
.then(r => r.ok ? r.json() : Promise.reject(r))
|
||||
.then(data => success(data))
|
||||
.catch(() => failure());
|
||||
}
|
||||
}],
|
||||
eventClick: function(info){
|
||||
// Sidebar card via HTMX-like fetch
|
||||
fetch("{% url 'appointments:appointment_detail_card' 0 %}".replace('0', info.event.id), {credentials:'same-origin'})
|
||||
.then(r => r.text())
|
||||
.then(html => { detailsEl.innerHTML = html; });
|
||||
|
||||
// Also open modal
|
||||
{#const modalBody = document.getElementById('apptModalBody');#}
|
||||
{#modalBody.innerHTML = '<div class="text-center text-muted py-3">Loading...</div>';#}
|
||||
{#fetch("{% url 'appointments:appointment_detail_card' 0 %}".replace('0', info.event.id), {credentials:'same-origin'})#}
|
||||
{# .then(r => r.text())#}
|
||||
{# .then(html => { modalBody.innerHTML = html; new bootstrap.Modal('#apptModal').show(); });#}
|
||||
},
|
||||
eventDrop: function(info){ sendReschedule(info); },
|
||||
eventResize: function(info){ sendReschedule(info); }
|
||||
});
|
||||
|
||||
function getCsrf(){
|
||||
// works with Django’s default CSRF cookie name
|
||||
const m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||
return m ? m[1] : '';
|
||||
}
|
||||
|
||||
function sendReschedule(info){
|
||||
const url = "{% url 'appointments:reschedule_appointment' 0 %}".replace('0', info.event.id);
|
||||
const payload = new URLSearchParams({
|
||||
start: info.event.start.toISOString(),
|
||||
end: (info.event.end ? info.event.end : info.event.start).toISOString()
|
||||
});
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {'X-CSRFToken': getCsrf(), 'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: payload.toString()
|
||||
}).then(r => {
|
||||
if(!r.ok){ info.revert(); }
|
||||
}).catch(()=> info.revert());
|
||||
}
|
||||
|
||||
document.getElementById('applyFilters').addEventListener('click', function(){
|
||||
calendar.refetchEvents();
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -7,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">
|
||||
|
||||
@ -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">ê</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() {
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold">{{ appointment.patient.get_full_name }}</div>
|
||||
<div class="text-muted small">
|
||||
{{ appointment.provider.get_full_name }} • {{ appointment.get_appointment_type_display }}
|
||||
</div>
|
||||
<div class="mt-2 small">
|
||||
<div><i class="far fa-clock me-1"></i>{{ appointment.scheduled_datetime|date:"M d, Y H:i" }}</div>
|
||||
{% if appointment.chief_complaint %}
|
||||
<div class="mt-1"><i class="fas fa-notes-medical me-1"></i>{{ appointment.chief_complaint }}</div>
|
||||
{% endif %}
|
||||
{% if appointment.is_telemedicine %}
|
||||
<div class="mt-1"><i class="fas fa-video me-1"></i>Telemedicine</div>
|
||||
{% endif %}
|
||||
<div class="mt-2">
|
||||
<span class="badge
|
||||
{% if appointment.status == 'PENDING' %}bg-warning
|
||||
{% elif appointment.status == 'CONFIRMED' %}bg-info
|
||||
{% elif appointment.status == 'CHECKED_IN' %}bg-primary
|
||||
{% elif appointment.status == 'IN_PROGRESS' %}bg-success
|
||||
{% elif appointment.status == 'COMPLETED' %}bg-success
|
||||
{% elif appointment.status == 'CANCELLED' %}bg-danger
|
||||
{% elif appointment.status == 'NO_SHOW' %}bg-secondary
|
||||
{% endif %}">
|
||||
{{ appointment.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'appointments:appointment_detail' appointment.pk %}" class="btn btn-sm btn-outline-primary">{{ _("View") }}</a>
|
||||
{% if appointment.status in 'PENDING, CONFIRMED ,SCHEDULED' %}
|
||||
<a href="{% url 'appointments:cancel_appointment' appointment.pk %}" class="btn btn-sm btn-outline-danger">{{ _("Cancel") }}</a>
|
||||
<a href="{% url 'appointments:reschedule_appointment' appointment.pk %}" class="btn btn-sm btn-outline-warning">{{ _("Reschedule") }}</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,84 +1,61 @@
|
||||
<div class="row">
|
||||
<div class="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>
|
||||
|
||||
@ -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>
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}`;#}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -357,7 +357,7 @@
|
||||
<script>
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
$('#{{form.provider.id_for_label}}').select2({
|
||||
$('.form-select').select2({
|
||||
|
||||
}).on('select2:select', function (e) {
|
||||
loadAvailableSlots();
|
||||
|
||||
@ -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 %}
|
||||
@ -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"> </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"> </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() {
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -4,23 +4,28 @@
|
||||
{% block title %}Appointment Templates - Appointments{% endblock %}
|
||||
|
||||
{% block 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 %}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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%'
|
||||
});
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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')),
|
||||
]
|
||||
|
||||
|
||||
@ -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})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@ -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 %'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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() {
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user