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.validators
@ -36,6 +36,13 @@ class Migration(migrations.Migration):
('bio', models.TextField(blank=True)),
('language', models.CharField(choices=[('en', 'English'), ('ar', 'Arabic')], default='en', max_length=5)),
('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={
'ordering': ['-date_joined'],
@ -44,6 +51,49 @@ class Migration(migrations.Migration):
('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(
name='Role',
fields=[
@ -59,4 +109,37 @@ class Migration(migrations.Migration):
'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
from django.conf import settings
from django.db import migrations, models
@ -35,6 +36,19 @@ class Migration(migrations.Migration):
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(
model_name='role',
name='group',
@ -45,6 +59,21 @@ class Migration(migrations.Migration):
name='permissions',
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(
model_name='user',
index=models.Index(fields=['email'], name='accounts_us_email_74c8d6_idx'),
@ -57,4 +86,32 @@ class Migration(migrations.Migration):
model_name='user',
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 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 uuid

View File

@ -550,33 +550,33 @@ class UnifiedAnalyticsService:
queryset = PhysicianMonthlyRating.objects.filter(
year=now.year,
month=now.month
).select_related('physician', 'physician__hospital', 'physician__department')
).select_related('staff', 'staff__hospital', 'staff__department')
# Apply RBAC filters
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:
queryset = queryset.filter(physician__hospital_id=hospital_id)
queryset = queryset.filter(staff__hospital_id=hospital_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]
return {
'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': [{
'name': 'Rating',
'data': [float(round(r.average_rating, 2)) for r in queryset]
}],
'metadata': [
{
'name': r.physician.get_full_name(),
'physician_id': str(r.physician.id),
'specialization': r.physician.specialization,
'department': r.physician.department.name if r.physician.department else None,
'name': f"{r.staff.first_name} {r.staff.last_name}",
'physician_id': str(r.staff.id),
'specialization': r.staff.specialization,
'department': r.staff.department.name if r.staff.department else None,
'rating': float(round(r.average_rating, 2)),
'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,

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 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 uuid

View File

@ -109,8 +109,8 @@ class Command(BaseCommand):
created_count = 0
for cat_data in categories:
category, created = ComplaintCategory.objects.get_or_create(
hospital=None, # System-wide
code=cat_data['code'],
parent__isnull=True, # System-wide categories have no parent
defaults={
'name_en': cat_data['name_en'],
'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 uuid
@ -48,7 +48,7 @@ class Migration(migrations.Migration):
],
options={
'verbose_name_plural': 'Complaint Categories',
'ordering': ['hospital', 'order', 'name_en'],
'ordering': ['order', 'name_en'],
},
),
migrations.CreateModel(
@ -140,6 +140,38 @@ class Migration(migrations.Migration):
'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(
name='Complaint',
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
from django.conf import settings
@ -44,8 +44,8 @@ class Migration(migrations.Migration):
),
migrations.AddField(
model_name='complaintcategory',
name='hospital',
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'),
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'),
),
migrations.AddField(
model_name='complaintcategory',
@ -112,9 +112,25 @@ class Migration(migrations.Migration):
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),
),
migrations.AddIndex(
model_name='complaintcategory',
index=models.Index(fields=['hospital', 'is_active'], name='complaints__hospita_a31674_idx'),
migrations.AddField(
model_name='inquiryattachment',
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(
model_name='complaintcategory',
@ -168,4 +184,8 @@ class Migration(migrations.Migration):
model_name='inquiry',
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_API_KEY = "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"
# OPENROUTER_API_KEY = "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"
OPENROUTER_API_KEY = "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"
# Default configuration
DEFAULT_MODEL = "openrouter/z-ai/glm-4.7"
# 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 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 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
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 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 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
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 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 django.db.models.deletion
@ -13,7 +13,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('organizations', '0002_hospital_metadata'),
('organizations', '0001_initial'),
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_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)),
('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)),
('resolved_at', models.DateTimeField(blank=True, null=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_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)),
('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)),
('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)),
('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'],
},
),
migrations.AddIndex(
model_name='observation',
index=models.Index(fields=['hospital', 'status', '-created_at'], name='observation_hospita_dcd21a_idx'),
),
migrations.AddIndex(
model_name='observation',
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 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)),
('license_number', models.CharField(blank=True, max_length=100)),
('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={
'verbose_name_plural': 'Hospitals',
@ -130,7 +130,7 @@ class Migration(migrations.Migration):
('specialization', models.CharField(blank=True, max_length=200)),
('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)),
('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')),
('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 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 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
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 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 django.db.models.deletion
@ -13,7 +13,7 @@ class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('organizations', '0005_alter_staff_department'),
('organizations', '0001_initial'),
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 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.db.models.deletion
@ -12,7 +12,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('organizations', '0005_alter_staff_department'),
('organizations', '0001_initial'),
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 uuid

View File

@ -197,7 +197,7 @@ def generate_saudi_phone():
def generate_mrn():
"""Generate Medical Record Number"""
return f"MRN{random.randint(100000, 999999)}"
return f"{random.randint(100000, 999999)}"
def generate_national_id():
@ -368,7 +368,7 @@ def create_complaint_categories(hospitals):
"""Create complaint categories"""
print("Creating complaint categories...")
# System-wide categories (hospital=None)
# System-wide categories (no hospitals in ManyToMany)
system_categories = [
{'code': 'CLINICAL', 'name_en': 'Clinical Care', 'name_ar': 'الرعاية السريرية', 'order': 1},
{'code': 'STAFF', 'name_en': 'Staff Behavior', 'name_ar': 'سلوك الموظفين', 'order': 2},
@ -380,11 +380,11 @@ def create_complaint_categories(hospitals):
categories = []
# Create system-wide categories
# Create system-wide categories (parent__isnull=True and no hospitals)
for cat_data in system_categories:
category, created = ComplaintCategory.objects.get_or_create(
code=cat_data['code'],
hospital=None,
parent__isnull=True,
defaults={
'name_en': cat_data['name_en'],
'name_ar': cat_data['name_ar'],
@ -392,9 +392,9 @@ def create_complaint_categories(hospitals):
'is_active': True,
}
)
categories.append(category)
if created:
print(f" Created system-wide category: {category.name_en}")
categories.append(category)
# Create hospital-specific categories for each hospital
hospital_specific_categories = [
@ -407,7 +407,6 @@ def create_complaint_categories(hospitals):
for cat_data in hospital_specific_categories:
category, created = ComplaintCategory.objects.get_or_create(
code=f"{cat_data['code']}_{hospital.code}",
hospital=hospital,
defaults={
'name_en': cat_data['name_en'],
'name_ar': cat_data['name_ar'],
@ -415,9 +414,14 @@ def create_complaint_categories(hospitals):
'is_active': True,
}
)
categories.append(category)
if created:
category.hospitals.add(hospital)
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")
return categories
@ -438,7 +442,7 @@ def create_complaints(patients, hospitals, staff, users):
# Generate complaints over 2 years (730 days)
# 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)
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]
# Select appropriate category (system-wide or hospital-specific)
hospital_categories = [c for c in categories if c.hospital == hospital]
system_categories_list = [c for c in categories if c.hospital is None]
hospital_categories = [c for c in categories if hospital in c.hospitals.all()]
system_categories_list = [c for c in categories if c.hospitals.count() == 0]
# Prefer hospital-specific categories if available, otherwise use system-wide
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,
title=random.choice(COMPLAINT_TITLES),
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']),
severity=random.choice(['low', 'medium', 'high', 'critical']),
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)
# 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]
for _ in range(num_inquiries):
@ -838,6 +842,7 @@ def create_survey_instances(survey_templates, patients, staff):
instance = SurveyInstance.objects.create(
survey_template=template,
hospital=template.hospital,
patient=patient,
delivery_channel=random.choice(['sms', 'whatsapp', 'email']),
recipient_phone=patient.phone,
@ -961,7 +966,7 @@ def create_staff_monthly_ratings(staff):
neutral_count = total_surveys - positive_count - negative_count
rating, created = PhysicianMonthlyRating.objects.get_or_create(
physician=staff_member,
staff=staff_member,
year=year,
month=month,
defaults={
@ -979,10 +984,10 @@ def create_staff_monthly_ratings(staff):
)
ratings.append(rating)
print(f" Created {len(ratings)} physician monthly ratings")
print(f" Created {len(ratings)} staff monthly ratings")
# Update rankings for each month
print(" Updating physician rankings...")
print(" Updating staff rankings...")
from apps.physicians.models import PhysicianMonthlyRating
from apps.organizations.models import Hospital, Department
@ -994,7 +999,7 @@ def create_staff_monthly_ratings(staff):
hospitals = Hospital.objects.filter(status='active')
for hospital in hospitals:
hospital_ratings = PhysicianMonthlyRating.objects.filter(
physician__hospital=hospital,
staff__hospital=hospital,
year=year,
month=month
).order_by('-average_rating')
@ -1007,7 +1012,7 @@ def create_staff_monthly_ratings(staff):
departments = Department.objects.filter(status='active')
for department in departments:
dept_ratings = PhysicianMonthlyRating.objects.filter(
physician__department=department,
staff__department=department,
year=year,
month=month
).order_by('-average_rating')
@ -1020,13 +1025,13 @@ def create_staff_monthly_ratings(staff):
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"""
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)
physician_ct = ContentType.objects.get_for_model(Physician)
staff_ct = ContentType.objects.get_for_model(Staff)
# Message templates for generating realistic appreciations
message_templates_en = [
@ -1101,12 +1106,12 @@ def create_appreciations(users, physicians, hospitals, departments, categories):
# Select sender (users only - users send appreciations)
sender = random.choice(users)
# Select recipient (70% physician, 30% user)
is_physician_recipient = random.random() < 0.7
# Select recipient (70% staff, 30% user)
is_staff_recipient = random.random() < 0.7
if is_physician_recipient:
recipient = random.choice(physicians)
recipient_ct = physician_ct
if is_staff_recipient:
recipient = random.choice(staff)
recipient_ct = staff_ct
else:
# User recipients (excluding sender)
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
# Determine hospital context
if is_physician_recipient:
if is_staff_recipient:
hospital = recipient.hospital
else:
hospital = recipient.hospital if recipient.hospital else sender.hospital
@ -1130,7 +1135,7 @@ def create_appreciations(users, physicians, hospitals, departments, categories):
# Determine department context
department = None
if is_physician_recipient and recipient.department:
if is_staff_recipient and recipient.department:
department = recipient.department
elif random.random() < 0.3:
# Some appreciations have department context
@ -1233,14 +1238,14 @@ def create_appreciations(users, physicians, hospitals, departments, categories):
return appreciations
def award_badges(badges, users, physicians, categories):
"""Award badges to users and physicians based on appreciations"""
def award_badges(badges, users, staff, categories):
"""Award badges to users and staff based on appreciations"""
print("Awarding badges...")
user_badges = []
# Get ContentType for User and Physician
# Get ContentType for User and Staff
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 = {
@ -1292,15 +1297,15 @@ def award_badges(badges, users, physicians, categories):
appreciation_count=received_count,
))
# Award badges to physicians (60% get badges)
for physician in physicians:
if random.random() > 0.6: # 60% of physicians get badges
# Award badges to staff (60% get badges)
for staff_member in staff:
if random.random() > 0.6: # 60% of staff get badges
continue
# Count appreciations received by this physician
# Count appreciations received by this staff member
received_count = Appreciation.objects.filter(
recipient_content_type=physician_ct,
recipient_object_id=physician.id,
recipient_content_type=staff_ct,
recipient_object_id=staff_member.id,
status=AppreciationStatus.ACKNOWLEDGED
).count()
@ -1312,17 +1317,17 @@ def award_badges(badges, users, physicians, categories):
if received_count >= criteria['min_count']:
# Check if already has this badge
existing = UserBadge.objects.filter(
recipient_content_type=physician_ct,
recipient_object_id=physician.id,
recipient_content_type=staff_ct,
recipient_object_id=staff_member.id,
badge=badge_map[badge_code]
).first()
if not existing:
# Higher chance for physicians
# Higher chance for staff
if random.random() < 0.7:
user_badges.append(UserBadge(
recipient_content_type=physician_ct,
recipient_object_id=physician.id,
recipient_content_type=staff_ct,
recipient_object_id=staff_member.id,
badge=badge_map[badge_code],
appreciation_count=received_count,
))
@ -1330,19 +1335,19 @@ def award_badges(badges, users, physicians, categories):
# 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
def generate_appreciation_stats(users, physicians, hospitals):
"""Generate appreciation statistics for users and physicians"""
def generate_appreciation_stats(users, staff, hospitals):
"""Generate appreciation statistics for users and staff"""
print("Generating appreciation statistics...")
stats = []
now = timezone.now()
# Get ContentType for User and Physician
# Get ContentType for User and Staff
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
year = now.year
@ -1358,41 +1363,46 @@ def generate_appreciation_stats(users, physicians, hospitals):
sent_count = random.randint(0, 50)
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_object_id=user.id,
year=year,
month=month,
hospital=user.hospital if user.hospital else random.choice(hospitals),
received_count=received_count,
sent_count=sent_count,
acknowledged_count=acknowledged_count,
hospital_rank=random.randint(1, 20) if received_count > 0 else None,
))
defaults={
'hospital': user.hospital if user.hospital else random.choice(hospitals),
'received_count': received_count,
'sent_count': sent_count,
'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)
for physician in physicians:
# Generate stats for staff (90% have stats)
for staff_member in staff:
if random.random() > 0.1: # 90% have stats
# Physicians typically receive more appreciations
# Staff typically receive more appreciations
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))
stats.append(AppreciationStats(
recipient_content_type=physician_ct,
recipient_object_id=physician.id,
stat, created = AppreciationStats.objects.get_or_create(
recipient_content_type=staff_ct,
recipient_object_id=staff_member.id,
year=year,
month=month,
hospital=physician.hospital,
department=physician.department,
received_count=received_count,
sent_count=sent_count,
acknowledged_count=acknowledged_count,
hospital_rank=random.randint(1, 10) if received_count > 5 else None,
))
# Bulk create stats
AppreciationStats.objects.bulk_create(stats)
defaults={
'hospital': staff_member.hospital,
'department': staff_member.department,
'received_count': received_count,
'sent_count': sent_count,
'acknowledged_count': acknowledged_count,
'hospital_rank': random.randint(1, 10) if received_count > 5 else None,
}
)
if created:
stats.append(stat)
print(f" Generated {len(stats)} appreciation statistics")
return stats
@ -1588,36 +1598,50 @@ def main():
print("="*60 + "\n")
# Clear existing data first
# clear_existing_data()
clear_existing_data()
# Create base data
# hospitals = create_hospitals()
hospitals = Hospital.objects.all()
hospitals = create_hospitals()
departments = create_departments(hospitals)
staff = create_staff(hospitals, departments)
patients = create_patients(hospitals)
# create_users(hospitals)
users = create_users(hospitals)
# Get all users for assignments
# users = list(User.objects.all())
users_list = list(User.objects.all())
# Create complaint categories first
# categories = create_complaint_categories(hospitals)
categories = create_complaint_categories(hospitals)
# Create operational data
# complaints = create_complaints(patients, hospitals, physicians, users)
# inquiries = create_inquiries(patients, hospitals, users)
# feedbacks = create_feedback(patients, hospitals, physicians, users)
# create_survey_templates(hospitals)
# create_journey_templates(hospitals)
# projects = create_qi_projects(hospitals)
# actions = create_px_actions(complaints, hospitals, users)
# journey_instances = create_journey_instances(None, patients)
# survey_instances = create_survey_instances(None, patients, physicians)
# call_interactions = create_call_center_interactions(patients, hospitals, users)
# social_mentions = create_social_mentions(hospitals)
# physician_ratings = create_physician_monthly_ratings(physicians)
complaints = create_complaints(patients, hospitals, staff, users_list)
inquiries = create_inquiries(patients, hospitals, users_list)
feedbacks = create_feedback(patients, hospitals, staff, users_list)
create_survey_templates(hospitals)
create_journey_templates(hospitals)
projects = create_qi_projects(hospitals)
actions = create_px_actions(complaints, hospitals, users_list)
journey_instances = create_journey_instances(None, patients)
survey_instances = create_survey_instances(None, patients, staff)
call_interactions = create_call_center_interactions(patients, hospitals, users_list)
social_mentions = create_social_mentions(hospitals)
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("Data Generation Complete!")
@ -1627,26 +1651,26 @@ def main():
print(f" - {len(departments)} Departments")
print(f" - {len(staff)} Staff")
print(f" - {len(patients)} Patients")
# print(f" - {len(users)} Users")
# print(f" - {len(complaints)} Complaints (2 years)")
# print(f" - {len(inquiries)} Inquiries (2 years)")
# print(f" - {len(feedbacks)} Feedback Items")
# print(f" - {len(actions)} PX Actions")
# print(f" - {len(journey_instances)} Journey Instances")
# print(f" - {len(survey_instances)} Survey Instances")
# print(f" - {len(call_interactions)} Call Center Interactions")
# print(f" - {len(social_mentions)} Social Media Mentions")
# print(f" - {len(projects)} QI Projects")
# print(f" - {len(staff_ratings)} Staff Monthly Ratings")
# print(f" - {len(appreciations)} Appreciations (2 years)")
# print(f" - {len(user_badges)} Badges Awarded")
# print(f" - {len(appreciation_stats)} Appreciation Statistics")
# print(f" - {len(observations)} Observations (2 years)")
print(f" - {len(users_list)} Users")
print(f" - {len(complaints)} Complaints (2 years)")
print(f" - {len(inquiries)} Inquiries (2 years)")
print(f" - {len(feedbacks)} Feedback Items")
print(f" - {len(actions)} PX Actions")
print(f" - {len(journey_instances)} Journey Instances")
print(f" - {len(survey_instances)} Survey Instances")
print(f" - {len(call_interactions)} Call Center Interactions")
print(f" - {len(social_mentions)} Social Media Mentions")
print(f" - {len(projects)} QI Projects")
print(f" - {len(staff_ratings)} Staff Monthly Ratings")
print(f" - {len(appreciations)} Appreciations (2 years)")
print(f" - {len(user_badges)} Badges Awarded")
print(f" - {len(appreciation_stats)} Appreciation Statistics")
print(f" - {len(observations)} Observations (2 years)")
print(f"\nYou can now login with:")
print(f" Username: px_admin")
print(f" Password: admin123")
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"\nAccess the system at: http://127.0.0.1:8000/")
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 %}
<style>
.action-header {
background: linear-gradient(135deg, #f57c00 0%, #ff6f00 100%);
color: white;
padding: 30px;
border-radius: 12px;
{#background: linear-gradient(135deg, #616161 0%, #616161 100%);#}
{#color: white;#}
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.sla-progress-card {
@ -156,7 +156,7 @@
</div>
<!-- Action Header -->
<div class="action-header">
<div class="card p-2">
<div class="row align-items-center">
<div class="col-md-8">
<div class="d-flex align-items-center mb-2">

View File

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

View File

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

View File

@ -151,7 +151,7 @@
</thead>
<tbody>
{% 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>
{% if forloop.counter == 1 %}
<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">
<!-- Command Center -->
<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 -->
<li class="nav-item">
<a class="nav-link {% if 'actions' in request.path %}active{% endif %}"
@ -165,7 +279,11 @@
{% if user.is_px_admin %}
<li class="nav-item">
<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>
{% trans "Settings" %}
<i class="bi bi-chevron-down ms-auto"></i>