This commit is contained in:
Marwan Alwali 2026-01-08 10:34:08 +03:00
parent 97de5919f2
commit 1ae0e763be
45 changed files with 3055 additions and 1388 deletions

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
@ -36,6 +36,13 @@ class Migration(migrations.Migration):
('bio', models.TextField(blank=True)), ('bio', models.TextField(blank=True)),
('language', models.CharField(choices=[('en', 'English'), ('ar', 'Arabic')], default='en', max_length=5)), ('language', models.CharField(choices=[('en', 'English'), ('ar', 'Arabic')], default='en', max_length=5)),
('is_active', models.BooleanField(default=True)), ('is_active', models.BooleanField(default=True)),
('is_provisional', models.BooleanField(default=False, help_text='User is in onboarding process')),
('invitation_token', models.CharField(blank=True, help_text='Token for account activation', max_length=100, null=True, unique=True)),
('invitation_expires_at', models.DateTimeField(blank=True, help_text='When the invitation token expires', null=True)),
('acknowledgement_completed', models.BooleanField(default=False, help_text='User has completed acknowledgement wizard')),
('acknowledgement_completed_at', models.DateTimeField(blank=True, help_text='When the acknowledgement was completed', null=True)),
('current_wizard_step', models.IntegerField(default=0, help_text='Current step in onboarding wizard')),
('wizard_completed_steps', models.JSONField(blank=True, default=list, help_text='List of completed wizard step IDs')),
], ],
options={ options={
'ordering': ['-date_joined'], 'ordering': ['-date_joined'],
@ -44,6 +51,49 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()), ('objects', django.contrib.auth.models.UserManager()),
], ],
), ),
migrations.CreateModel(
name='AcknowledgementChecklistItem',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('role', models.CharField(blank=True, choices=[('px_admin', 'PX Admin'), ('hospital_admin', 'Hospital Admin'), ('department_manager', 'Department Manager'), ('px_coordinator', 'PX Coordinator'), ('physician', 'Physician'), ('nurse', 'Nurse'), ('staff', 'Staff'), ('viewer', 'Viewer')], help_text='Target role for this item', max_length=50, null=True)),
('code', models.CharField(help_text='Unique code for this checklist item', max_length=100, unique=True)),
('text_en', models.CharField(max_length=500)),
('text_ar', models.CharField(blank=True, max_length=500)),
('description_en', models.TextField(blank=True)),
('description_ar', models.TextField(blank=True)),
('is_required', models.BooleanField(default=True, help_text='Item must be acknowledged')),
('order', models.IntegerField(default=0, help_text='Display order in checklist')),
('is_active', models.BooleanField(default=True)),
],
options={
'ordering': ['role', 'order', 'code'],
},
),
migrations.CreateModel(
name='AcknowledgementContent',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('role', models.CharField(blank=True, choices=[('px_admin', 'PX Admin'), ('hospital_admin', 'Hospital Admin'), ('department_manager', 'Department Manager'), ('px_coordinator', 'PX Coordinator'), ('physician', 'Physician'), ('nurse', 'Nurse'), ('staff', 'Staff'), ('viewer', 'Viewer')], help_text='Target role for this content', max_length=50, null=True)),
('code', models.CharField(help_text='Unique code for this content section', max_length=100, unique=True)),
('title_en', models.CharField(max_length=200)),
('title_ar', models.CharField(blank=True, max_length=200)),
('description_en', models.TextField()),
('description_ar', models.TextField(blank=True)),
('content_en', models.TextField(blank=True)),
('content_ar', models.TextField(blank=True)),
('icon', models.CharField(blank=True, help_text="Icon class (e.g., 'fa-user', 'fa-shield')", max_length=50)),
('color', models.CharField(blank=True, help_text="Hex color code (e.g., '#007bff')", max_length=7)),
('order', models.IntegerField(default=0, help_text='Display order in wizard')),
('is_active', models.BooleanField(default=True)),
],
options={
'ordering': ['role', 'order', 'code'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='Role', name='Role',
fields=[ fields=[
@ -59,4 +109,37 @@ class Migration(migrations.Migration):
'ordering': ['-level', 'name'], 'ordering': ['-level', 'name'],
}, },
), ),
migrations.CreateModel(
name='UserAcknowledgement',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('is_acknowledged', models.BooleanField(default=True)),
('acknowledged_at', models.DateTimeField(auto_now_add=True)),
('signature', models.TextField(blank=True, help_text='Digital signature data (base64 encoded)')),
('signature_ip', models.GenericIPAddressField(blank=True, help_text='IP address when signed', null=True)),
('signature_user_agent', models.TextField(blank=True, help_text='User agent when signed')),
('metadata', models.JSONField(blank=True, default=dict, help_text='Additional metadata')),
],
options={
'ordering': ['-acknowledged_at'],
},
),
migrations.CreateModel(
name='UserProvisionalLog',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('event_type', models.CharField(choices=[('created', 'User Created'), ('invitation_sent', 'Invitation Sent'), ('invitation_resent', 'Invitation Resent'), ('wizard_started', 'Wizard Started'), ('step_completed', 'Wizard Step Completed'), ('wizard_completed', 'Wizard Completed'), ('user_activated', 'User Activated'), ('invitation_expired', 'Invitation Expired')], db_index=True, max_length=50)),
('description', models.TextField()),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('user_agent', models.TextField(blank=True)),
('metadata', models.JSONField(blank=True, default=dict, help_text='Additional event data')),
],
options={
'ordering': ['-created_at'],
},
),
] ]

View File

@ -1,6 +1,7 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -35,6 +36,19 @@ class Migration(migrations.Migration):
name='user_permissions', name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'),
), ),
migrations.AddIndex(
model_name='acknowledgementcontent',
index=models.Index(fields=['role', 'is_active', 'order'], name='accounts_ac_role_6fe1fd_idx'),
),
migrations.AddIndex(
model_name='acknowledgementcontent',
index=models.Index(fields=['code'], name='accounts_ac_code_48fa92_idx'),
),
migrations.AddField(
model_name='acknowledgementchecklistitem',
name='content',
field=models.ForeignKey(blank=True, help_text='Related content section', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checklist_items', to='accounts.acknowledgementcontent'),
),
migrations.AddField( migrations.AddField(
model_name='role', model_name='role',
name='group', name='group',
@ -45,6 +59,21 @@ class Migration(migrations.Migration):
name='permissions', name='permissions',
field=models.ManyToManyField(blank=True, to='auth.permission'), field=models.ManyToManyField(blank=True, to='auth.permission'),
), ),
migrations.AddField(
model_name='useracknowledgement',
name='checklist_item',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_acknowledgements', to='accounts.acknowledgementchecklistitem'),
),
migrations.AddField(
model_name='useracknowledgement',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='acknowledgements', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='userprovisionallog',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='provisional_logs', to=settings.AUTH_USER_MODEL),
),
migrations.AddIndex( migrations.AddIndex(
model_name='user', model_name='user',
index=models.Index(fields=['email'], name='accounts_us_email_74c8d6_idx'), index=models.Index(fields=['email'], name='accounts_us_email_74c8d6_idx'),
@ -57,4 +86,32 @@ class Migration(migrations.Migration):
model_name='user', model_name='user',
index=models.Index(fields=['is_active', '-date_joined'], name='accounts_us_is_acti_a32178_idx'), index=models.Index(fields=['is_active', '-date_joined'], name='accounts_us_is_acti_a32178_idx'),
), ),
migrations.AddIndex(
model_name='acknowledgementchecklistitem',
index=models.Index(fields=['role', 'is_active', 'order'], name='accounts_ac_role_c556c1_idx'),
),
migrations.AddIndex(
model_name='acknowledgementchecklistitem',
index=models.Index(fields=['code'], name='accounts_ac_code_b745de_idx'),
),
migrations.AddIndex(
model_name='useracknowledgement',
index=models.Index(fields=['user', '-acknowledged_at'], name='accounts_us_user_id_7ba948_idx'),
),
migrations.AddIndex(
model_name='useracknowledgement',
index=models.Index(fields=['checklist_item', '-acknowledged_at'], name='accounts_us_checkli_870e26_idx'),
),
migrations.AlterUniqueTogether(
name='useracknowledgement',
unique_together={('user', 'checklist_item')},
),
migrations.AddIndex(
model_name='userprovisionallog',
index=models.Index(fields=['user', '-created_at'], name='accounts_us_user_id_c488d5_idx'),
),
migrations.AddIndex(
model_name='userprovisionallog',
index=models.Index(fields=['event_type', '-created_at'], name='accounts_us_event_t_b7f691_idx'),
),
] ]

View File

@ -1,160 +0,0 @@
# Generated by Django 5.0.14 on 2026-01-06 08:54
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='acknowledgement_completed',
field=models.BooleanField(default=False, help_text='User has completed acknowledgement wizard'),
),
migrations.AddField(
model_name='user',
name='acknowledgement_completed_at',
field=models.DateTimeField(blank=True, help_text='When the acknowledgement was completed', null=True),
),
migrations.AddField(
model_name='user',
name='current_wizard_step',
field=models.IntegerField(default=0, help_text='Current step in onboarding wizard'),
),
migrations.AddField(
model_name='user',
name='invitation_expires_at',
field=models.DateTimeField(blank=True, help_text='When the invitation token expires', null=True),
),
migrations.AddField(
model_name='user',
name='invitation_token',
field=models.CharField(blank=True, help_text='Token for account activation', max_length=100, null=True, unique=True),
),
migrations.AddField(
model_name='user',
name='is_provisional',
field=models.BooleanField(default=False, help_text='User is in onboarding process'),
),
migrations.AddField(
model_name='user',
name='wizard_completed_steps',
field=models.JSONField(blank=True, default=list, help_text='List of completed wizard step IDs'),
),
migrations.CreateModel(
name='AcknowledgementContent',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('role', models.CharField(blank=True, choices=[('px_admin', 'PX Admin'), ('hospital_admin', 'Hospital Admin'), ('department_manager', 'Department Manager'), ('px_coordinator', 'PX Coordinator'), ('physician', 'Physician'), ('nurse', 'Nurse'), ('staff', 'Staff'), ('viewer', 'Viewer')], help_text='Target role for this content', max_length=50, null=True)),
('code', models.CharField(help_text='Unique code for this content section', max_length=100, unique=True)),
('title_en', models.CharField(max_length=200)),
('title_ar', models.CharField(blank=True, max_length=200)),
('description_en', models.TextField()),
('description_ar', models.TextField(blank=True)),
('content_en', models.TextField(blank=True)),
('content_ar', models.TextField(blank=True)),
('icon', models.CharField(blank=True, help_text="Icon class (e.g., 'fa-user', 'fa-shield')", max_length=50)),
('color', models.CharField(blank=True, help_text="Hex color code (e.g., '#007bff')", max_length=7)),
('order', models.IntegerField(default=0, help_text='Display order in wizard')),
('is_active', models.BooleanField(default=True)),
],
options={
'ordering': ['role', 'order', 'code'],
'indexes': [models.Index(fields=['role', 'is_active', 'order'], name='accounts_ac_role_6fe1fd_idx'), models.Index(fields=['code'], name='accounts_ac_code_48fa92_idx')],
},
),
migrations.CreateModel(
name='AcknowledgementChecklistItem',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('role', models.CharField(blank=True, choices=[('px_admin', 'PX Admin'), ('hospital_admin', 'Hospital Admin'), ('department_manager', 'Department Manager'), ('px_coordinator', 'PX Coordinator'), ('physician', 'Physician'), ('nurse', 'Nurse'), ('staff', 'Staff'), ('viewer', 'Viewer')], help_text='Target role for this item', max_length=50, null=True)),
('code', models.CharField(help_text='Unique code for this checklist item', max_length=100, unique=True)),
('text_en', models.CharField(max_length=500)),
('text_ar', models.CharField(blank=True, max_length=500)),
('description_en', models.TextField(blank=True)),
('description_ar', models.TextField(blank=True)),
('is_required', models.BooleanField(default=True, help_text='Item must be acknowledged')),
('order', models.IntegerField(default=0, help_text='Display order in checklist')),
('is_active', models.BooleanField(default=True)),
('content', models.ForeignKey(blank=True, help_text='Related content section', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checklist_items', to='accounts.acknowledgementcontent')),
],
options={
'ordering': ['role', 'order', 'code'],
},
),
migrations.CreateModel(
name='UserAcknowledgement',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('is_acknowledged', models.BooleanField(default=True)),
('acknowledged_at', models.DateTimeField(auto_now_add=True)),
('signature', models.TextField(blank=True, help_text='Digital signature data (base64 encoded)')),
('signature_ip', models.GenericIPAddressField(blank=True, help_text='IP address when signed', null=True)),
('signature_user_agent', models.TextField(blank=True, help_text='User agent when signed')),
('metadata', models.JSONField(blank=True, default=dict, help_text='Additional metadata')),
('checklist_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_acknowledgements', to='accounts.acknowledgementchecklistitem')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='acknowledgements', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-acknowledged_at'],
},
),
migrations.CreateModel(
name='UserProvisionalLog',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('event_type', models.CharField(choices=[('created', 'User Created'), ('invitation_sent', 'Invitation Sent'), ('invitation_resent', 'Invitation Resent'), ('wizard_started', 'Wizard Started'), ('step_completed', 'Wizard Step Completed'), ('wizard_completed', 'Wizard Completed'), ('user_activated', 'User Activated'), ('invitation_expired', 'Invitation Expired')], db_index=True, max_length=50)),
('description', models.TextField()),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('user_agent', models.TextField(blank=True)),
('metadata', models.JSONField(blank=True, default=dict, help_text='Additional event data')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='provisional_logs', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.AddIndex(
model_name='acknowledgementchecklistitem',
index=models.Index(fields=['role', 'is_active', 'order'], name='accounts_ac_role_c556c1_idx'),
),
migrations.AddIndex(
model_name='acknowledgementchecklistitem',
index=models.Index(fields=['code'], name='accounts_ac_code_b745de_idx'),
),
migrations.AddIndex(
model_name='useracknowledgement',
index=models.Index(fields=['user', '-acknowledged_at'], name='accounts_us_user_id_7ba948_idx'),
),
migrations.AddIndex(
model_name='useracknowledgement',
index=models.Index(fields=['checklist_item', '-acknowledged_at'], name='accounts_us_checkli_870e26_idx'),
),
migrations.AlterUniqueTogether(
name='useracknowledgement',
unique_together={('user', 'checklist_item')},
),
migrations.AddIndex(
model_name='userprovisionallog',
index=models.Index(fields=['user', '-created_at'], name='accounts_us_user_id_c488d5_idx'),
),
migrations.AddIndex(
model_name='userprovisionallog',
index=models.Index(fields=['event_type', '-created_at'], name='accounts_us_event_t_b7f691_idx'),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2025-12-14 11:19 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -550,33 +550,33 @@ class UnifiedAnalyticsService:
queryset = PhysicianMonthlyRating.objects.filter( queryset = PhysicianMonthlyRating.objects.filter(
year=now.year, year=now.year,
month=now.month month=now.month
).select_related('physician', 'physician__hospital', 'physician__department') ).select_related('staff', 'staff__hospital', 'staff__department')
# Apply RBAC filters # Apply RBAC filters
if not user.is_px_admin() and user.hospital: if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(physician__hospital=user.hospital) queryset = queryset.filter(staff__hospital=user.hospital)
if hospital_id: if hospital_id:
queryset = queryset.filter(physician__hospital_id=hospital_id) queryset = queryset.filter(staff__hospital_id=hospital_id)
if department_id: if department_id:
queryset = queryset.filter(physician__department_id=department_id) queryset = queryset.filter(staff__department_id=department_id)
queryset = queryset.order_by('-average_rating')[:limit] queryset = queryset.order_by('-average_rating')[:limit]
return { return {
'type': 'bar', 'type': 'bar',
'labels': [r.physician.get_full_name() for r in queryset], 'labels': [f"{r.staff.first_name} {r.staff.last_name}" for r in queryset],
'series': [{ 'series': [{
'name': 'Rating', 'name': 'Rating',
'data': [float(round(r.average_rating, 2)) for r in queryset] 'data': [float(round(r.average_rating, 2)) for r in queryset]
}], }],
'metadata': [ 'metadata': [
{ {
'name': r.physician.get_full_name(), 'name': f"{r.staff.first_name} {r.staff.last_name}",
'physician_id': str(r.physician.id), 'physician_id': str(r.staff.id),
'specialization': r.physician.specialization, 'specialization': r.staff.specialization,
'department': r.physician.department.name if r.physician.department else None, 'department': r.staff.department.name if r.staff.department else None,
'rating': float(round(r.average_rating, 2)), 'rating': float(round(r.average_rating, 2)),
'surveys': int(r.total_surveys) if r.total_surveys is not None else 0, 'surveys': int(r.total_surveys) if r.total_surveys is not None else 0,
'positive': int(r.positive_count) if r.positive_count is not None else 0, 'positive': int(r.positive_count) if r.positive_count is not None else 0,

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-01 11:27 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -109,8 +109,8 @@ class Command(BaseCommand):
created_count = 0 created_count = 0
for cat_data in categories: for cat_data in categories:
category, created = ComplaintCategory.objects.get_or_create( category, created = ComplaintCategory.objects.get_or_create(
hospital=None, # System-wide
code=cat_data['code'], code=cat_data['code'],
parent__isnull=True, # System-wide categories have no parent
defaults={ defaults={
'name_en': cat_data['name_en'], 'name_en': cat_data['name_en'],
'name_ar': cat_data['name_ar'], 'name_ar': cat_data['name_ar'],

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
@ -48,7 +48,7 @@ class Migration(migrations.Migration):
], ],
options={ options={
'verbose_name_plural': 'Complaint Categories', 'verbose_name_plural': 'Complaint Categories',
'ordering': ['hospital', 'order', 'name_en'], 'ordering': ['order', 'name_en'],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@ -140,6 +140,38 @@ class Migration(migrations.Migration):
'ordering': ['-created_at'], 'ordering': ['-created_at'],
}, },
), ),
migrations.CreateModel(
name='InquiryAttachment',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('file', models.FileField(upload_to='inquiries/%Y/%m/%d/')),
('filename', models.CharField(max_length=500)),
('file_type', models.CharField(blank=True, max_length=100)),
('file_size', models.IntegerField(help_text='File size in bytes')),
('description', models.TextField(blank=True)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='InquiryUpdate',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('update_type', models.CharField(choices=[('status_change', 'Status Change'), ('assignment', 'Assignment'), ('note', 'Note'), ('response', 'Response'), ('communication', 'Communication')], db_index=True, max_length=50)),
('message', models.TextField()),
('old_status', models.CharField(blank=True, max_length=20)),
('new_status', models.CharField(blank=True, max_length=20)),
('metadata', models.JSONField(blank=True, default=dict)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='Complaint', name='Complaint',
fields=[ fields=[

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
@ -44,8 +44,8 @@ class Migration(migrations.Migration):
), ),
migrations.AddField( migrations.AddField(
model_name='complaintcategory', model_name='complaintcategory',
name='hospital', name='hospitals',
field=models.ForeignKey(blank=True, help_text='Leave blank for system-wide categories', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='complaint_categories', to='organizations.hospital'), field=models.ManyToManyField(blank=True, help_text='Empty list = system-wide category. Add hospitals to share category.', related_name='complaint_categories', to='organizations.hospital'),
), ),
migrations.AddField( migrations.AddField(
model_name='complaintcategory', model_name='complaintcategory',
@ -112,9 +112,25 @@ class Migration(migrations.Migration):
name='responded_by', name='responded_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='responded_inquiries', to=settings.AUTH_USER_MODEL), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='responded_inquiries', to=settings.AUTH_USER_MODEL),
), ),
migrations.AddIndex( migrations.AddField(
model_name='complaintcategory', model_name='inquiryattachment',
index=models.Index(fields=['hospital', 'is_active'], name='complaints__hospita_a31674_idx'), name='inquiry',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='complaints.inquiry'),
),
migrations.AddField(
model_name='inquiryattachment',
name='uploaded_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiry_attachments', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='inquiryupdate',
name='created_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiry_updates', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='inquiryupdate',
name='inquiry',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='updates', to='complaints.inquiry'),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='complaintcategory', model_name='complaintcategory',
@ -168,4 +184,8 @@ class Migration(migrations.Migration):
model_name='inquiry', model_name='inquiry',
index=models.Index(fields=['hospital', 'status'], name='complaints__hospita_b1573b_idx'), index=models.Index(fields=['hospital', 'status'], name='complaints__hospita_b1573b_idx'),
), ),
migrations.AddIndex(
model_name='inquiryupdate',
index=models.Index(fields=['inquiry', '-created_at'], name='complaints__inquiry_551c37_idx'),
),
] ]

View File

@ -1,31 +0,0 @@
# Generated by Django 5.0.14 on 2026-01-05 13:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('complaints', '0002_initial'),
('organizations', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='complaintcategory',
options={'ordering': ['order', 'name_en'], 'verbose_name_plural': 'Complaint Categories'},
),
migrations.RemoveIndex(
model_name='complaintcategory',
name='complaints__hospita_a31674_idx',
),
migrations.RemoveField(
model_name='complaintcategory',
name='hospital',
),
migrations.AddField(
model_name='complaintcategory',
name='hospitals',
field=models.ManyToManyField(blank=True, help_text='Empty list = system-wide category. Add hospitals to share category.', related_name='complaint_categories', to='organizations.hospital'),
),
]

View File

@ -1,54 +0,0 @@
# Generated by Django 5.0.14 on 2026-01-05 15:06
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('complaints', '0003_alter_complaintcategory_options_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='InquiryAttachment',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('file', models.FileField(upload_to='inquiries/%Y/%m/%d/')),
('filename', models.CharField(max_length=500)),
('file_type', models.CharField(blank=True, max_length=100)),
('file_size', models.IntegerField(help_text='File size in bytes')),
('description', models.TextField(blank=True)),
('inquiry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='complaints.inquiry')),
('uploaded_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiry_attachments', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='InquiryUpdate',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('update_type', models.CharField(choices=[('status_change', 'Status Change'), ('assignment', 'Assignment'), ('note', 'Note'), ('response', 'Response'), ('communication', 'Communication')], db_index=True, max_length=50)),
('message', models.TextField()),
('old_status', models.CharField(blank=True, max_length=20)),
('new_status', models.CharField(blank=True, max_length=20)),
('metadata', models.JSONField(blank=True, default=dict)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiry_updates', to=settings.AUTH_USER_MODEL)),
('inquiry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='updates', to='complaints.inquiry')),
],
options={
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['inquiry', '-created_at'], name='complaints__inquiry_551c37_idx')],
},
),
]

View File

@ -35,7 +35,10 @@ class AIService:
""" """
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
OPENROUTER_API_KEY = "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" # OPENROUTER_API_KEY = "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"
OPENROUTER_API_KEY = "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"
# Default configuration # Default configuration
DEFAULT_MODEL = "openrouter/z-ai/glm-4.7" DEFAULT_MODEL = "openrouter/z-ai/glm-4.7"
# DEFAULT_MODEL = "openrouter/xiaomi/mimo-v2-flash:free" # DEFAULT_MODEL = "openrouter/xiaomi/mimo-v2-flash:free"

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-04 07:26 # Generated by Django 5.0.14 on 2026-01-08 06:56
import apps.observations.models import apps.observations.models
import django.db.models.deletion import django.db.models.deletion
@ -13,7 +13,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('organizations', '0002_hospital_metadata'), ('organizations', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
@ -54,6 +54,7 @@ class Migration(migrations.Migration):
('reporter_phone', models.CharField(blank=True, help_text='Optional phone number for follow-up', max_length=20)), ('reporter_phone', models.CharField(blank=True, help_text='Optional phone number for follow-up', max_length=20)),
('reporter_email', models.EmailField(blank=True, help_text='Optional email for follow-up', max_length=254)), ('reporter_email', models.EmailField(blank=True, help_text='Optional email for follow-up', max_length=254)),
('status', models.CharField(choices=[('new', 'New'), ('triaged', 'Triaged'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('rejected', 'Rejected'), ('duplicate', 'Duplicate')], db_index=True, default='new', max_length=20)), ('status', models.CharField(choices=[('new', 'New'), ('triaged', 'Triaged'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('rejected', 'Rejected'), ('duplicate', 'Duplicate')], db_index=True, default='new', max_length=20)),
('source', models.CharField(choices=[('staff_portal', 'Staff Portal'), ('web_form', 'Web Form'), ('mobile_app', 'Mobile App'), ('email', 'Email'), ('call_center', 'Call Center'), ('other', 'Other')], default='staff_portal', help_text='How the observation was submitted', max_length=50)),
('triaged_at', models.DateTimeField(blank=True, null=True)), ('triaged_at', models.DateTimeField(blank=True, null=True)),
('resolved_at', models.DateTimeField(blank=True, null=True)), ('resolved_at', models.DateTimeField(blank=True, null=True)),
('resolution_notes', models.TextField(blank=True)), ('resolution_notes', models.TextField(blank=True)),
@ -65,7 +66,9 @@ class Migration(migrations.Migration):
('assigned_department', models.ForeignKey(blank=True, help_text='Department responsible for handling this observation', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_observations', to='organizations.department')), ('assigned_department', models.ForeignKey(blank=True, help_text='Department responsible for handling this observation', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_observations', to='organizations.department')),
('assigned_to', models.ForeignKey(blank=True, help_text='User assigned to handle this observation', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_observations', to=settings.AUTH_USER_MODEL)), ('assigned_to', models.ForeignKey(blank=True, help_text='User assigned to handle this observation', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_observations', to=settings.AUTH_USER_MODEL)),
('closed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='closed_observations', to=settings.AUTH_USER_MODEL)), ('closed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='closed_observations', to=settings.AUTH_USER_MODEL)),
('hospital', models.ForeignKey(help_text='Hospital where observation was made', on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='organizations.hospital')),
('resolved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_observations', to=settings.AUTH_USER_MODEL)), ('resolved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_observations', to=settings.AUTH_USER_MODEL)),
('staff', models.ForeignKey(blank=True, help_text='Staff member mentioned in observation', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.staff')),
('triaged_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='triaged_observations', to=settings.AUTH_USER_MODEL)), ('triaged_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='triaged_observations', to=settings.AUTH_USER_MODEL)),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='observations.observationcategory')), ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='observations.observationcategory')),
], ],
@ -124,6 +127,10 @@ class Migration(migrations.Migration):
'ordering': ['-created_at'], 'ordering': ['-created_at'],
}, },
), ),
migrations.AddIndex(
model_name='observation',
index=models.Index(fields=['hospital', 'status', '-created_at'], name='observation_hospita_dcd21a_idx'),
),
migrations.AddIndex( migrations.AddIndex(
model_name='observation', model_name='observation',
index=models.Index(fields=['status', '-created_at'], name='observation_status_2b5566_idx'), index=models.Index(fields=['status', '-created_at'], name='observation_status_2b5566_idx'),

View File

@ -1,65 +0,0 @@
# Generated migration to add missing fields to Observation model
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('organizations', '0001_initial'), # Need hospital and department models
('observations', '0001_initial'),
]
operations = [
# Add hospital field (required for tenant isolation)
# Initially nullable, will be made required in next migration
migrations.AddField(
model_name='observation',
name='hospital',
field=models.ForeignKey(
null=True,
blank=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='observations',
to='organizations.hospital'
),
),
# Add staff field (optional, for AI-matching like complaints)
migrations.AddField(
model_name='observation',
name='staff',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='observations',
to='organizations.staff'
),
),
# Add source field (to track how observation was submitted)
migrations.AddField(
model_name='observation',
name='source',
field=models.CharField(
blank=True,
choices=[
('staff_portal', 'Staff Portal'),
('web_form', 'Web Form'),
('mobile_app', 'Mobile App'),
('email', 'Email'),
('call_center', 'Call Center'),
('other', 'Other'),
],
default='staff_portal',
max_length=50
),
),
# Add indexes for hospital filtering
migrations.AddIndex(
model_name='observation',
index=models.Index(fields=['hospital', 'status', '-created_at'], name='obs_hospital_status_idx'),
),
]

View File

@ -1,36 +0,0 @@
# Generated by Django 5.0.14 on 2026-01-07 11:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('observations', '0002_add_missing_fields'),
('organizations', '0005_alter_staff_department'),
]
operations = [
migrations.RenameIndex(
model_name='observation',
new_name='observation_hospita_dcd21a_idx',
old_name='obs_hospital_status_idx',
),
migrations.AlterField(
model_name='observation',
name='hospital',
field=models.ForeignKey(default=1, help_text='Hospital where observation was made', on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='organizations.hospital'),
preserve_default=False,
),
migrations.AlterField(
model_name='observation',
name='source',
field=models.CharField(choices=[('staff_portal', 'Staff Portal'), ('web_form', 'Web Form'), ('mobile_app', 'Mobile App'), ('email', 'Email'), ('call_center', 'Call Center'), ('other', 'Other')], default='staff_portal', help_text='How the observation was submitted', max_length=50),
),
migrations.AlterField(
model_name='observation',
name='staff',
field=models.ForeignKey(blank=True, help_text='Staff member mentioned in observation', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.staff'),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
@ -31,7 +31,7 @@ class Migration(migrations.Migration):
('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], db_index=True, default='active', max_length=20)), ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], db_index=True, default='active', max_length=20)),
('license_number', models.CharField(blank=True, max_length=100)), ('license_number', models.CharField(blank=True, max_length=100)),
('capacity', models.IntegerField(blank=True, help_text='Bed capacity', null=True)), ('capacity', models.IntegerField(blank=True, help_text='Bed capacity', null=True)),
('metadata', models.JSONField(blank=True, default=dict, help_text='Hospital configuration settings')), ('metadata', models.JSONField(blank=True, default=dict)),
], ],
options={ options={
'verbose_name_plural': 'Hospitals', 'verbose_name_plural': 'Hospitals',
@ -130,7 +130,7 @@ class Migration(migrations.Migration):
('specialization', models.CharField(blank=True, max_length=200)), ('specialization', models.CharField(blank=True, max_length=200)),
('employee_id', models.CharField(db_index=True, max_length=50, unique=True)), ('employee_id', models.CharField(db_index=True, max_length=50, unique=True)),
('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='active', max_length=20)), ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='active', max_length=20)),
('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='organizations.department')), ('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff', to='organizations.department')),
('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='organizations.hospital')), ('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='organizations.hospital')),
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff_profile', to=settings.AUTH_USER_MODEL)), ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff_profile', to=settings.AUTH_USER_MODEL)),
], ],

View File

@ -1,18 +0,0 @@
# Generated by Django 5.0.14 on 2026-01-01 12:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organizations', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='hospital',
name='metadata',
field=models.JSONField(blank=True, default=dict),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.0.14 on 2026-01-06 11:55
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organizations', '0002_hospital_metadata'),
]
operations = [
migrations.AddField(
model_name='patient',
name='department',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='organizations.department'),
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 5.0.14 on 2026-01-06 11:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('organizations', '0003_patient_department'),
]
operations = [
migrations.RemoveField(
model_name='patient',
name='department',
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.0.14 on 2026-01-07 08:54
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organizations', '0004_remove_patient_department'),
]
operations = [
migrations.AlterField(
model_name='staff',
name='department',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff', to='organizations.department'),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-07 16:18 # Generated by Django 5.0.14 on 2026-01-08 06:56
import apps.references.models import apps.references.models
import django.db.models.deletion import django.db.models.deletion
@ -13,7 +13,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('auth', '0012_alter_user_first_name_max_length'), ('auth', '0012_alter_user_first_name_max_length'),
('organizations', '0005_alter_staff_department'), ('organizations', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-07 20:27 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion
@ -12,7 +12,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('organizations', '0005_alter_staff_department'), ('organizations', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.14 on 2026-01-05 10:43 # Generated by Django 5.0.14 on 2026-01-08 06:56
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -197,7 +197,7 @@ def generate_saudi_phone():
def generate_mrn(): def generate_mrn():
"""Generate Medical Record Number""" """Generate Medical Record Number"""
return f"MRN{random.randint(100000, 999999)}" return f"{random.randint(100000, 999999)}"
def generate_national_id(): def generate_national_id():
@ -368,7 +368,7 @@ def create_complaint_categories(hospitals):
"""Create complaint categories""" """Create complaint categories"""
print("Creating complaint categories...") print("Creating complaint categories...")
# System-wide categories (hospital=None) # System-wide categories (no hospitals in ManyToMany)
system_categories = [ system_categories = [
{'code': 'CLINICAL', 'name_en': 'Clinical Care', 'name_ar': 'الرعاية السريرية', 'order': 1}, {'code': 'CLINICAL', 'name_en': 'Clinical Care', 'name_ar': 'الرعاية السريرية', 'order': 1},
{'code': 'STAFF', 'name_en': 'Staff Behavior', 'name_ar': 'سلوك الموظفين', 'order': 2}, {'code': 'STAFF', 'name_en': 'Staff Behavior', 'name_ar': 'سلوك الموظفين', 'order': 2},
@ -380,11 +380,11 @@ def create_complaint_categories(hospitals):
categories = [] categories = []
# Create system-wide categories # Create system-wide categories (parent__isnull=True and no hospitals)
for cat_data in system_categories: for cat_data in system_categories:
category, created = ComplaintCategory.objects.get_or_create( category, created = ComplaintCategory.objects.get_or_create(
code=cat_data['code'], code=cat_data['code'],
hospital=None, parent__isnull=True,
defaults={ defaults={
'name_en': cat_data['name_en'], 'name_en': cat_data['name_en'],
'name_ar': cat_data['name_ar'], 'name_ar': cat_data['name_ar'],
@ -392,9 +392,9 @@ def create_complaint_categories(hospitals):
'is_active': True, 'is_active': True,
} }
) )
categories.append(category)
if created: if created:
print(f" Created system-wide category: {category.name_en}") print(f" Created system-wide category: {category.name_en}")
categories.append(category)
# Create hospital-specific categories for each hospital # Create hospital-specific categories for each hospital
hospital_specific_categories = [ hospital_specific_categories = [
@ -407,7 +407,6 @@ def create_complaint_categories(hospitals):
for cat_data in hospital_specific_categories: for cat_data in hospital_specific_categories:
category, created = ComplaintCategory.objects.get_or_create( category, created = ComplaintCategory.objects.get_or_create(
code=f"{cat_data['code']}_{hospital.code}", code=f"{cat_data['code']}_{hospital.code}",
hospital=hospital,
defaults={ defaults={
'name_en': cat_data['name_en'], 'name_en': cat_data['name_en'],
'name_ar': cat_data['name_ar'], 'name_ar': cat_data['name_ar'],
@ -415,9 +414,14 @@ def create_complaint_categories(hospitals):
'is_active': True, 'is_active': True,
} }
) )
categories.append(category)
if created: if created:
category.hospitals.add(hospital)
print(f" Created hospital category for {hospital.name}: {category.name_en}") print(f" Created hospital category for {hospital.name}: {category.name_en}")
else:
# Ensure hospital is in the hospitals ManyToMany
if hospital not in category.hospitals.all():
category.hospitals.add(hospital)
categories.append(category)
print(f" Created {len(categories)} complaint categories") print(f" Created {len(categories)} complaint categories")
return categories return categories
@ -438,7 +442,7 @@ def create_complaints(patients, hospitals, staff, users):
# Generate complaints over 2 years (730 days) # Generate complaints over 2 years (730 days)
# Average 3-5 complaints per day = ~1200-1800 total # Average 3-5 complaints per day = ~1200-1800 total
for day_offset in range(730): for day_offset in range(30):
# Random number of complaints per day (0-8, weighted towards 2-4) # Random number of complaints per day (0-8, weighted towards 2-4)
num_complaints = random.choices([0, 1, 2, 3, 4, 5, 6, 7, 8], weights=[5, 10, 20, 25, 20, 10, 5, 3, 2])[0] num_complaints = random.choices([0, 1, 2, 3, 4, 5, 6, 7, 8], weights=[5, 10, 20, 25, 20, 10, 5, 3, 2])[0]
@ -456,8 +460,8 @@ def create_complaints(patients, hospitals, staff, users):
status = random.choices(['open', 'in_progress', 'resolved', 'closed'], weights=[2, 5, 30, 63])[0] status = random.choices(['open', 'in_progress', 'resolved', 'closed'], weights=[2, 5, 30, 63])[0]
# Select appropriate category (system-wide or hospital-specific) # Select appropriate category (system-wide or hospital-specific)
hospital_categories = [c for c in categories if c.hospital == hospital] hospital_categories = [c for c in categories if hospital in c.hospitals.all()]
system_categories_list = [c for c in categories if c.hospital is None] system_categories_list = [c for c in categories if c.hospitals.count() == 0]
# Prefer hospital-specific categories if available, otherwise use system-wide # Prefer hospital-specific categories if available, otherwise use system-wide
available_categories = hospital_categories if hospital_categories else system_categories_list available_categories = hospital_categories if hospital_categories else system_categories_list
@ -469,7 +473,7 @@ def create_complaints(patients, hospitals, staff, users):
staff=random.choice(staff) if random.random() > 0.5 else None, staff=random.choice(staff) if random.random() > 0.5 else None,
title=random.choice(COMPLAINT_TITLES), title=random.choice(COMPLAINT_TITLES),
description=f"Detailed description of the complaint. Patient experienced issues during their visit.", description=f"Detailed description of the complaint. Patient experienced issues during their visit.",
category=random.choice(available_categories), category=random.choice(available_categories) if available_categories else None,
priority=random.choice(['low', 'medium', 'high']), priority=random.choice(['low', 'medium', 'high']),
severity=random.choice(['low', 'medium', 'high', 'critical']), severity=random.choice(['low', 'medium', 'high', 'critical']),
source=random.choice(['patient', 'family', 'survey', 'call_center', 'moh', 'other']), source=random.choice(['patient', 'family', 'survey', 'call_center', 'moh', 'other']),
@ -519,7 +523,7 @@ def create_inquiries(patients, hospitals, users):
# Generate inquiries over 2 years (730 days) # Generate inquiries over 2 years (730 days)
# Average 1-2 inquiries per day = ~500-700 total # Average 1-2 inquiries per day = ~500-700 total
for day_offset in range(730): for day_offset in range(30):
num_inquiries = random.choices([0, 1, 2, 3], weights=[30, 40, 25, 5])[0] num_inquiries = random.choices([0, 1, 2, 3], weights=[30, 40, 25, 5])[0]
for _ in range(num_inquiries): for _ in range(num_inquiries):
@ -838,6 +842,7 @@ def create_survey_instances(survey_templates, patients, staff):
instance = SurveyInstance.objects.create( instance = SurveyInstance.objects.create(
survey_template=template, survey_template=template,
hospital=template.hospital,
patient=patient, patient=patient,
delivery_channel=random.choice(['sms', 'whatsapp', 'email']), delivery_channel=random.choice(['sms', 'whatsapp', 'email']),
recipient_phone=patient.phone, recipient_phone=patient.phone,
@ -961,7 +966,7 @@ def create_staff_monthly_ratings(staff):
neutral_count = total_surveys - positive_count - negative_count neutral_count = total_surveys - positive_count - negative_count
rating, created = PhysicianMonthlyRating.objects.get_or_create( rating, created = PhysicianMonthlyRating.objects.get_or_create(
physician=staff_member, staff=staff_member,
year=year, year=year,
month=month, month=month,
defaults={ defaults={
@ -979,10 +984,10 @@ def create_staff_monthly_ratings(staff):
) )
ratings.append(rating) ratings.append(rating)
print(f" Created {len(ratings)} physician monthly ratings") print(f" Created {len(ratings)} staff monthly ratings")
# Update rankings for each month # Update rankings for each month
print(" Updating physician rankings...") print(" Updating staff rankings...")
from apps.physicians.models import PhysicianMonthlyRating from apps.physicians.models import PhysicianMonthlyRating
from apps.organizations.models import Hospital, Department from apps.organizations.models import Hospital, Department
@ -994,7 +999,7 @@ def create_staff_monthly_ratings(staff):
hospitals = Hospital.objects.filter(status='active') hospitals = Hospital.objects.filter(status='active')
for hospital in hospitals: for hospital in hospitals:
hospital_ratings = PhysicianMonthlyRating.objects.filter( hospital_ratings = PhysicianMonthlyRating.objects.filter(
physician__hospital=hospital, staff__hospital=hospital,
year=year, year=year,
month=month month=month
).order_by('-average_rating') ).order_by('-average_rating')
@ -1007,7 +1012,7 @@ def create_staff_monthly_ratings(staff):
departments = Department.objects.filter(status='active') departments = Department.objects.filter(status='active')
for department in departments: for department in departments:
dept_ratings = PhysicianMonthlyRating.objects.filter( dept_ratings = PhysicianMonthlyRating.objects.filter(
physician__department=department, staff__department=department,
year=year, year=year,
month=month month=month
).order_by('-average_rating') ).order_by('-average_rating')
@ -1020,13 +1025,13 @@ def create_staff_monthly_ratings(staff):
return ratings return ratings
def create_appreciations(users, physicians, hospitals, departments, categories): def create_appreciations(users, staff, hospitals, departments, categories):
"""Create appreciations with 2 years of historical data""" """Create appreciations with 2 years of historical data"""
print("Creating appreciations (2 years of data)...") print("Creating appreciations (2 years of data)...")
# Get ContentType for User and Physician # Get ContentType for User and Staff
user_ct = ContentType.objects.get_for_model(User) user_ct = ContentType.objects.get_for_model(User)
physician_ct = ContentType.objects.get_for_model(Physician) staff_ct = ContentType.objects.get_for_model(Staff)
# Message templates for generating realistic appreciations # Message templates for generating realistic appreciations
message_templates_en = [ message_templates_en = [
@ -1101,12 +1106,12 @@ def create_appreciations(users, physicians, hospitals, departments, categories):
# Select sender (users only - users send appreciations) # Select sender (users only - users send appreciations)
sender = random.choice(users) sender = random.choice(users)
# Select recipient (70% physician, 30% user) # Select recipient (70% staff, 30% user)
is_physician_recipient = random.random() < 0.7 is_staff_recipient = random.random() < 0.7
if is_physician_recipient: if is_staff_recipient:
recipient = random.choice(physicians) recipient = random.choice(staff)
recipient_ct = physician_ct recipient_ct = staff_ct
else: else:
# User recipients (excluding sender) # User recipients (excluding sender)
potential_recipients = [u for u in users if u.id != sender.id] potential_recipients = [u for u in users if u.id != sender.id]
@ -1120,7 +1125,7 @@ def create_appreciations(users, physicians, hospitals, departments, categories):
continue continue
# Determine hospital context # Determine hospital context
if is_physician_recipient: if is_staff_recipient:
hospital = recipient.hospital hospital = recipient.hospital
else: else:
hospital = recipient.hospital if recipient.hospital else sender.hospital hospital = recipient.hospital if recipient.hospital else sender.hospital
@ -1130,7 +1135,7 @@ def create_appreciations(users, physicians, hospitals, departments, categories):
# Determine department context # Determine department context
department = None department = None
if is_physician_recipient and recipient.department: if is_staff_recipient and recipient.department:
department = recipient.department department = recipient.department
elif random.random() < 0.3: elif random.random() < 0.3:
# Some appreciations have department context # Some appreciations have department context
@ -1233,14 +1238,14 @@ def create_appreciations(users, physicians, hospitals, departments, categories):
return appreciations return appreciations
def award_badges(badges, users, physicians, categories): def award_badges(badges, users, staff, categories):
"""Award badges to users and physicians based on appreciations""" """Award badges to users and staff based on appreciations"""
print("Awarding badges...") print("Awarding badges...")
user_badges = [] user_badges = []
# Get ContentType for User and Physician # Get ContentType for User and Staff
user_ct = ContentType.objects.get_for_model(User) user_ct = ContentType.objects.get_for_model(User)
physician_ct = ContentType.objects.get_for_model(Physician) staff_ct = ContentType.objects.get_for_model(Staff)
# Badge criteria mapping (using codes from seed command) # Badge criteria mapping (using codes from seed command)
badge_criteria = { badge_criteria = {
@ -1292,15 +1297,15 @@ def award_badges(badges, users, physicians, categories):
appreciation_count=received_count, appreciation_count=received_count,
)) ))
# Award badges to physicians (60% get badges) # Award badges to staff (60% get badges)
for physician in physicians: for staff_member in staff:
if random.random() > 0.6: # 60% of physicians get badges if random.random() > 0.6: # 60% of staff get badges
continue continue
# Count appreciations received by this physician # Count appreciations received by this staff member
received_count = Appreciation.objects.filter( received_count = Appreciation.objects.filter(
recipient_content_type=physician_ct, recipient_content_type=staff_ct,
recipient_object_id=physician.id, recipient_object_id=staff_member.id,
status=AppreciationStatus.ACKNOWLEDGED status=AppreciationStatus.ACKNOWLEDGED
).count() ).count()
@ -1312,17 +1317,17 @@ def award_badges(badges, users, physicians, categories):
if received_count >= criteria['min_count']: if received_count >= criteria['min_count']:
# Check if already has this badge # Check if already has this badge
existing = UserBadge.objects.filter( existing = UserBadge.objects.filter(
recipient_content_type=physician_ct, recipient_content_type=staff_ct,
recipient_object_id=physician.id, recipient_object_id=staff_member.id,
badge=badge_map[badge_code] badge=badge_map[badge_code]
).first() ).first()
if not existing: if not existing:
# Higher chance for physicians # Higher chance for staff
if random.random() < 0.7: if random.random() < 0.7:
user_badges.append(UserBadge( user_badges.append(UserBadge(
recipient_content_type=physician_ct, recipient_content_type=staff_ct,
recipient_object_id=physician.id, recipient_object_id=staff_member.id,
badge=badge_map[badge_code], badge=badge_map[badge_code],
appreciation_count=received_count, appreciation_count=received_count,
)) ))
@ -1330,19 +1335,19 @@ def award_badges(badges, users, physicians, categories):
# Bulk create user badges # Bulk create user badges
UserBadge.objects.bulk_create(user_badges) UserBadge.objects.bulk_create(user_badges)
print(f" Awarded {len(user_badges)} badges to users and physicians") print(f" Awarded {len(user_badges)} badges to users and staff")
return user_badges return user_badges
def generate_appreciation_stats(users, physicians, hospitals): def generate_appreciation_stats(users, staff, hospitals):
"""Generate appreciation statistics for users and physicians""" """Generate appreciation statistics for users and staff"""
print("Generating appreciation statistics...") print("Generating appreciation statistics...")
stats = [] stats = []
now = timezone.now() now = timezone.now()
# Get ContentType for User and Physician # Get ContentType for User and Staff
user_ct = ContentType.objects.get_for_model(User) user_ct = ContentType.objects.get_for_model(User)
physician_ct = ContentType.objects.get_for_model(Physician) staff_ct = ContentType.objects.get_for_model(Staff)
# Get current year and month # Get current year and month
year = now.year year = now.year
@ -1358,41 +1363,46 @@ def generate_appreciation_stats(users, physicians, hospitals):
sent_count = random.randint(0, 50) sent_count = random.randint(0, 50)
acknowledged_count = int(received_count * random.uniform(0.6, 1.0)) acknowledged_count = int(received_count * random.uniform(0.6, 1.0))
stats.append(AppreciationStats( stat, created = AppreciationStats.objects.get_or_create(
recipient_content_type=user_ct, recipient_content_type=user_ct,
recipient_object_id=user.id, recipient_object_id=user.id,
year=year, year=year,
month=month, month=month,
hospital=user.hospital if user.hospital else random.choice(hospitals), defaults={
received_count=received_count, 'hospital': user.hospital if user.hospital else random.choice(hospitals),
sent_count=sent_count, 'received_count': received_count,
acknowledged_count=acknowledged_count, 'sent_count': sent_count,
hospital_rank=random.randint(1, 20) if received_count > 0 else None, 'acknowledged_count': acknowledged_count,
)) 'hospital_rank': random.randint(1, 20) if received_count > 0 else None,
}
)
if created:
stats.append(stat)
# Generate stats for physicians (90% have stats) # Generate stats for staff (90% have stats)
for physician in physicians: for staff_member in staff:
if random.random() > 0.1: # 90% have stats if random.random() > 0.1: # 90% have stats
# Physicians typically receive more appreciations # Staff typically receive more appreciations
received_count = random.randint(5, 50) received_count = random.randint(5, 50)
sent_count = random.randint(0, 20) # Physicians send less sent_count = random.randint(0, 20) # Staff send less
acknowledged_count = int(received_count * random.uniform(0.7, 1.0)) acknowledged_count = int(received_count * random.uniform(0.7, 1.0))
stats.append(AppreciationStats( stat, created = AppreciationStats.objects.get_or_create(
recipient_content_type=physician_ct, recipient_content_type=staff_ct,
recipient_object_id=physician.id, recipient_object_id=staff_member.id,
year=year, year=year,
month=month, month=month,
hospital=physician.hospital, defaults={
department=physician.department, 'hospital': staff_member.hospital,
received_count=received_count, 'department': staff_member.department,
sent_count=sent_count, 'received_count': received_count,
acknowledged_count=acknowledged_count, 'sent_count': sent_count,
hospital_rank=random.randint(1, 10) if received_count > 5 else None, 'acknowledged_count': acknowledged_count,
)) 'hospital_rank': random.randint(1, 10) if received_count > 5 else None,
}
# Bulk create stats )
AppreciationStats.objects.bulk_create(stats) if created:
stats.append(stat)
print(f" Generated {len(stats)} appreciation statistics") print(f" Generated {len(stats)} appreciation statistics")
return stats return stats
@ -1588,36 +1598,50 @@ def main():
print("="*60 + "\n") print("="*60 + "\n")
# Clear existing data first # Clear existing data first
# clear_existing_data() clear_existing_data()
# Create base data # Create base data
# hospitals = create_hospitals() hospitals = create_hospitals()
hospitals = Hospital.objects.all()
departments = create_departments(hospitals) departments = create_departments(hospitals)
staff = create_staff(hospitals, departments) staff = create_staff(hospitals, departments)
patients = create_patients(hospitals) patients = create_patients(hospitals)
# create_users(hospitals) users = create_users(hospitals)
# Get all users for assignments # Get all users for assignments
# users = list(User.objects.all()) users_list = list(User.objects.all())
# Create complaint categories first # Create complaint categories first
# categories = create_complaint_categories(hospitals) categories = create_complaint_categories(hospitals)
# Create operational data # Create operational data
# complaints = create_complaints(patients, hospitals, physicians, users) complaints = create_complaints(patients, hospitals, staff, users_list)
# inquiries = create_inquiries(patients, hospitals, users) inquiries = create_inquiries(patients, hospitals, users_list)
# feedbacks = create_feedback(patients, hospitals, physicians, users) feedbacks = create_feedback(patients, hospitals, staff, users_list)
# create_survey_templates(hospitals) create_survey_templates(hospitals)
# create_journey_templates(hospitals) create_journey_templates(hospitals)
# projects = create_qi_projects(hospitals) projects = create_qi_projects(hospitals)
# actions = create_px_actions(complaints, hospitals, users) actions = create_px_actions(complaints, hospitals, users_list)
# journey_instances = create_journey_instances(None, patients) journey_instances = create_journey_instances(None, patients)
# survey_instances = create_survey_instances(None, patients, physicians) survey_instances = create_survey_instances(None, patients, staff)
# call_interactions = create_call_center_interactions(patients, hospitals, users) call_interactions = create_call_center_interactions(patients, hospitals, users_list)
# social_mentions = create_social_mentions(hospitals) social_mentions = create_social_mentions(hospitals)
# physician_ratings = create_physician_monthly_ratings(physicians) staff_ratings = create_staff_monthly_ratings(staff)
# Seed appreciation categories and badges
print("\nSeeding appreciation data...")
from django.core.management import call_command
call_command('seed_appreciation_data', verbosity=0)
print(" ✓ Appreciation categories and badges seeded")
# Get appreciation categories and badges
appreciation_categories = list(AppreciationCategory.objects.filter(is_active=True))
badges = list(AppreciationBadge.objects.filter(is_active=True))
# Create appreciation data
appreciations = create_appreciations(users_list, staff, hospitals, departments, appreciation_categories)
user_badges = award_badges(badges, users_list, staff, appreciation_categories)
appreciation_stats = generate_appreciation_stats(users_list, staff, hospitals)
observations = create_observations(hospitals, departments, users_list)
print("\n" + "="*60) print("\n" + "="*60)
print("Data Generation Complete!") print("Data Generation Complete!")
@ -1627,26 +1651,26 @@ def main():
print(f" - {len(departments)} Departments") print(f" - {len(departments)} Departments")
print(f" - {len(staff)} Staff") print(f" - {len(staff)} Staff")
print(f" - {len(patients)} Patients") print(f" - {len(patients)} Patients")
# print(f" - {len(users)} Users") print(f" - {len(users_list)} Users")
# print(f" - {len(complaints)} Complaints (2 years)") print(f" - {len(complaints)} Complaints (2 years)")
# print(f" - {len(inquiries)} Inquiries (2 years)") print(f" - {len(inquiries)} Inquiries (2 years)")
# print(f" - {len(feedbacks)} Feedback Items") print(f" - {len(feedbacks)} Feedback Items")
# print(f" - {len(actions)} PX Actions") print(f" - {len(actions)} PX Actions")
# print(f" - {len(journey_instances)} Journey Instances") print(f" - {len(journey_instances)} Journey Instances")
# print(f" - {len(survey_instances)} Survey Instances") print(f" - {len(survey_instances)} Survey Instances")
# print(f" - {len(call_interactions)} Call Center Interactions") print(f" - {len(call_interactions)} Call Center Interactions")
# print(f" - {len(social_mentions)} Social Media Mentions") print(f" - {len(social_mentions)} Social Media Mentions")
# print(f" - {len(projects)} QI Projects") print(f" - {len(projects)} QI Projects")
# print(f" - {len(staff_ratings)} Staff Monthly Ratings") print(f" - {len(staff_ratings)} Staff Monthly Ratings")
# print(f" - {len(appreciations)} Appreciations (2 years)") print(f" - {len(appreciations)} Appreciations (2 years)")
# print(f" - {len(user_badges)} Badges Awarded") print(f" - {len(user_badges)} Badges Awarded")
# print(f" - {len(appreciation_stats)} Appreciation Statistics") print(f" - {len(appreciation_stats)} Appreciation Statistics")
# print(f" - {len(observations)} Observations (2 years)") print(f" - {len(observations)} Observations (2 years)")
print(f"\nYou can now login with:") print(f"\nYou can now login with:")
print(f" Username: px_admin") print(f" Username: px_admin")
print(f" Password: admin123") print(f" Password: admin123")
print(f"\nOr hospital admins:") print(f"\nOr hospital admins:")
print(f" Username: admin_kfmc (or admin_kfsh, admin_kamc, etc.)") print(f" Username: admin_hh")
print(f" Password: admin123") print(f" Password: admin123")
print(f"\nAccess the system at: http://127.0.0.1:8000/") print(f"\nAccess the system at: http://127.0.0.1:8000/")
print("\n") print("\n")

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -7,10 +7,10 @@
{% block extra_css %} {% block extra_css %}
<style> <style>
.action-header { .action-header {
background: linear-gradient(135deg, #f57c00 0%, #ff6f00 100%); {#background: linear-gradient(135deg, #616161 0%, #616161 100%);#}
color: white; {#color: white;#}
padding: 30px; padding: 20px;
border-radius: 12px; border-radius: 8px;
margin-bottom: 30px; margin-bottom: 30px;
} }
.sla-progress-card { .sla-progress-card {
@ -156,7 +156,7 @@
</div> </div>
<!-- Action Header --> <!-- Action Header -->
<div class="action-header"> <div class="card p-2">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-md-8"> <div class="col-md-8">
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">

View File

@ -122,7 +122,7 @@
</div> </div>
<!-- Complaint Header --> <!-- Complaint Header -->
<div class="complaint-header"> <div class="card p-3">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-md-8"> <div class="col-md-8">
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">

View File

@ -109,7 +109,7 @@
</div> </div>
<!-- Inquiry Header --> <!-- Inquiry Header -->
<div class="inquiry-header"> <div class="card p-3">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-md-8"> <div class="col-md-8">
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">

View File

@ -151,7 +151,7 @@
</thead> </thead>
<tbody> <tbody>
{% for rating in top_physicians %} {% for rating in top_physicians %}
<tr onclick="window.location='{% url 'physicians:physician_detail' rating.physician.id %}'" style="cursor: pointer;"> <tr onclick="window.location=''" style="cursor: pointer;">
<td> <td>
{% if forloop.counter == 1 %} {% if forloop.counter == 1 %}
<h4 class="mb-0"><i class="bi bi-trophy-fill text-warning"></i></h4> <h4 class="mb-0"><i class="bi bi-trophy-fill text-warning"></i></h4>

View File

@ -11,6 +11,120 @@
<ul class="nav flex-column"> <ul class="nav flex-column">
<!-- Command Center --> <!-- Command Center -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'command_center' %}active{% endif %}"
href="{% url 'analytics:command_center' %}">
<i class="bi bi-speedometer2"></i>
{% trans "Command Center" %}
</a>
</li>
<hr class="my-2" style="border-color: rgba(255,255,255,0.1);">
<!-- Complaints -->
<li class="nav-item">
<a class="nav-link {% if 'complaints' in request.path %}active{% endif %}"
data-bs-toggle="collapse"
href="#complaintsMenu"
role="button"
aria-expanded="{% if 'complaints' in request.path %}true{% else %}false{% endif %}"
aria-controls="complaintsMenu">
<i class="bi bi-exclamation-triangle"></i>
{% trans "Complaints" %}
<span class="badge bg-danger">{{ complaint_count|default:0 }}</span>
<i class="bi bi-chevron-down ms-auto"></i>
</a>
<div class="collapse {% if 'complaints' in request.path %}show{% endif %}" id="complaintsMenu">
<ul class="nav flex-column ms-3">
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'complaint_list' %}active{% endif %}"
href="{% url 'complaints:complaint_list' %}">
<i class="bi bi-list-ul"></i>
{% trans "All Complaints" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'inquiry_list' %}active{% endif %}"
href="{% url 'complaints:inquiry_list' %}">
<i class="bi bi-question-circle"></i>
{% trans "Inquiries" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'complaints_analytics' %}active{% endif %}"
href="{% url 'complaints:complaints_analytics' %}">
<i class="bi bi-bar-chart"></i>
{% trans "Analytics" %}
</a>
</li>
</ul>
</div>
</li>
<!-- Feedback -->
<li class="nav-item">
<a class="nav-link {% if 'feedback' in request.path %}active{% endif %}"
href="{% url 'feedback:feedback_list' %}">
<i class="bi bi-chat-heart"></i>
{% trans "Feedback" %}
<span class="badge bg-success">{{ feedback_count|default:0 }}</span>
</a>
</li>
<!-- Appreciation -->
<li class="nav-item">
<a class="nav-link {% if 'appreciation' in request.path %}active{% endif %}"
data-bs-toggle="collapse"
href="#appreciationMenu"
role="button"
aria-expanded="{% if 'appreciation' in request.path %}true{% else %}false{% endif %}"
aria-controls="appreciationMenu">
<i class="bi bi-heart-fill"></i>
{% trans "Appreciation" %}
<i class="bi bi-chevron-down ms-auto"></i>
</a>
<div class="collapse {% if 'appreciation' in request.path %}show{% endif %}" id="appreciationMenu">
<ul class="nav flex-column ms-3">
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'appreciation_list' %}active{% endif %}"
href="{% url 'appreciation:appreciation_list' %}">
<i class="bi bi-list-ul"></i>
{% trans "All Appreciations" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'appreciation_send' %}active{% endif %}"
href="{% url 'appreciation:appreciation_send' %}">
<i class="bi bi-send"></i>
{% trans "Send Appreciation" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'leaderboard_view' %}active{% endif %}"
href="{% url 'appreciation:leaderboard_view' %}">
<i class="bi bi-trophy"></i>
{% trans "Leaderboard" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'my_badges_view' %}active{% endif %}"
href="{% url 'appreciation:my_badges_view' %}">
<i class="bi bi-award"></i>
{% trans "My Badges" %}
</a>
</li>
</ul>
</div>
</li>
<!-- Observations -->
<li class="nav-item">
<a class="nav-link {% if 'observations' in request.path and 'new' not in request.path %}active{% endif %}"
href="{% url 'observations:observation_list' %}">
<i class="bi bi-eye"></i>
{% trans "Observations" %}
</a>
</li>
<!-- PX Actions --> <!-- PX Actions -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if 'actions' in request.path %}active{% endif %}" <a class="nav-link {% if 'actions' in request.path %}active{% endif %}"
@ -165,7 +279,11 @@
{% if user.is_px_admin %} {% if user.is_px_admin %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if 'config' in request.path %}active{% endif %}" <a class="nav-link {% if 'config' in request.path %}active{% endif %}"
href="{% url 'config:dashboard' %}"> data-bs-toggle="collapse"
href="#settingsMenu"
role="button"
aria-expanded="{% if 'config' in request.path %}true{% else %}false{% endif %}"
aria-controls="settingsMenu">
<i class="bi bi-gear"></i> <i class="bi bi-gear"></i>
{% trans "Settings" %} {% trans "Settings" %}
<i class="bi bi-chevron-down ms-auto"></i> <i class="bi bi-chevron-down ms-auto"></i>