update-po-file

This commit is contained in:
Marwan Alwali 2026-01-18 14:04:23 +03:00
parent 1f9d8a7198
commit 524efbead9
41 changed files with 3269 additions and 686 deletions

View File

@ -1,24 +1,42 @@
""" """
Accounts admin Accounts admin
""" """
from django import forms
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import Role, User from .models import Role, User
class UserChangeForm(BaseUserChangeForm):
"""Custom user change form that handles nullable username field."""
# Override username field to use a regular CharField that handles None
username = forms.CharField(
max_length=150,
required=False,
help_text=_('Optional. 150 characters or fewer.'),
)
class Meta(BaseUserChangeForm.Meta):
model = User
@admin.register(User) @admin.register(User)
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
"""Custom User admin""" """Custom User admin"""
form = UserChangeForm
list_display = ['email', 'username', 'first_name', 'last_name', 'hospital', 'department', 'is_active', 'is_staff'] list_display = ['email', 'username', 'first_name', 'last_name', 'hospital', 'department', 'is_active', 'is_staff']
list_filter = ['is_active', 'is_staff', 'is_superuser', 'groups', 'hospital', 'department'] list_filter = ['is_active', 'is_staff', 'is_superuser', 'groups', 'hospital', 'department']
search_fields = ['email', 'username', 'first_name', 'last_name', 'employee_id'] search_fields = ['email', 'username', 'first_name', 'last_name', 'employee_id']
ordering = ['-date_joined'] ordering = ['-date_joined']
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('email', 'username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'phone', 'employee_id')}), (_('Personal info'), {'fields': ('first_name', 'last_name', 'phone', 'employee_id')}),
(_('Organization'), {'fields': ('hospital', 'department')}), (_('Organization'), {'fields': ('hospital', 'department')}),
(_('Profile'), {'fields': ('avatar', 'bio', 'language')}), (_('Profile'), {'fields': ('avatar', 'bio', 'language')}),
(_('Permissions'), { (_('Permissions'), {

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.utils.timezone import django.utils.timezone
import uuid import uuid
@ -33,6 +33,10 @@ class Migration(migrations.Migration):
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')), ('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
('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)),
('notification_email_enabled', models.BooleanField(default=True, help_text='Enable email notifications')),
('notification_sms_enabled', models.BooleanField(default=False, help_text='Enable SMS notifications')),
('preferred_notification_channel', models.CharField(choices=[('email', 'Email'), ('sms', 'SMS'), ('both', 'Both')], default='email', help_text='Preferred notification channel for general notifications', max_length=10)),
('explanation_notification_channel', models.CharField(choices=[('email', 'Email'), ('sms', 'SMS'), ('both', 'Both')], default='email', help_text='Preferred channel for explanation requests', max_length=10)),
('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')), ('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_token', models.CharField(blank=True, help_text='Token for account activation', max_length=100, null=True, unique=True)),

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings

View File

@ -1,33 +0,0 @@
# Generated by Django 6.0.1 on 2026-01-12 18:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='explanation_notification_channel',
field=models.CharField(choices=[('email', 'Email'), ('sms', 'SMS'), ('both', 'Both')], default='email', help_text='Preferred channel for explanation requests', max_length=10),
),
migrations.AddField(
model_name='user',
name='notification_email_enabled',
field=models.BooleanField(default=True, help_text='Enable email notifications'),
),
migrations.AddField(
model_name='user',
name='notification_sms_enabled',
field=models.BooleanField(default=False, help_text='Enable SMS notifications'),
),
migrations.AddField(
model_name='user',
name='preferred_notification_channel',
field=models.CharField(choices=[('email', 'Email'), ('sms', 'SMS'), ('both', 'Both')], default='email', help_text='Preferred notification channel for general notifications', max_length=10),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -229,8 +229,9 @@ class UnifiedAnalyticsService:
'avg_survey_score': float(surveys_qs.aggregate(avg=Avg('total_score'))['avg'] or 0), 'avg_survey_score': float(surveys_qs.aggregate(avg=Avg('total_score'))['avg'] or 0),
# Social Media KPIs # Social Media KPIs
# Sentiment is stored in ai_analysis JSON field as ai_analysis.sentiment
'negative_social_comments': int(SocialMediaComment.objects.filter( 'negative_social_comments': int(SocialMediaComment.objects.filter(
sentiment='negative', ai_analysis__sentiment='negative',
published_at__gte=start_date, published_at__gte=start_date,
published_at__lte=end_date published_at__lte=end_date
).count()), ).count()),

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -49,8 +49,7 @@ def send_appreciation_notification(appreciation):
Uses the notification system to send email/SMS/WhatsApp. Uses the notification system to send email/SMS/WhatsApp.
""" """
try: try:
from apps.notifications.models import NotificationLog, NotificationChannel, NotificationStatus from apps.notifications.services import send_email, send_sms
from apps.notifications.services import send_notification
# Get recipient details # Get recipient details
recipient_email = appreciation.get_recipient_email() recipient_email = appreciation.get_recipient_email()
@ -73,12 +72,11 @@ def send_appreciation_notification(appreciation):
# Send email if available # Send email if available
if recipient_email: if recipient_email:
try: try:
send_notification( send_email(
channel=NotificationChannel.EMAIL, email=recipient_email,
recipient=recipient_email,
subject=f"New Appreciation Received - {appreciation.hospital.name}", subject=f"New Appreciation Received - {appreciation.hospital.name}",
message=message_en, message=message_en,
content_object=appreciation, related_object=appreciation,
) )
except Exception as e: except Exception as e:
# Log error but don't fail # Log error but don't fail
@ -87,11 +85,10 @@ def send_appreciation_notification(appreciation):
# Send SMS if available # Send SMS if available
if recipient_phone: if recipient_phone:
try: try:
send_notification( send_sms(
channel=NotificationChannel.SMS, phone=recipient_phone,
recipient=recipient_phone,
message=message_en, message=message_en,
content_object=appreciation, related_object=appreciation,
) )
except Exception as e: except Exception as e:
# Log error but don't fail # Log error but don't fail

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
@ -64,6 +64,10 @@ class Migration(migrations.Migration):
('email_sent_at', models.DateTimeField(blank=True, help_text='When the explanation request email was sent', null=True)), ('email_sent_at', models.DateTimeField(blank=True, help_text='When the explanation request email was sent', null=True)),
('responded_at', models.DateTimeField(blank=True, help_text='When the explanation was submitted', null=True)), ('responded_at', models.DateTimeField(blank=True, help_text='When the explanation was submitted', null=True)),
('request_message', models.TextField(blank=True, help_text='Optional message sent with the explanation request')), ('request_message', models.TextField(blank=True, help_text='Optional message sent with the explanation request')),
('sla_due_at', models.DateTimeField(blank=True, db_index=True, help_text='SLA deadline for staff to submit explanation', null=True)),
('is_overdue', models.BooleanField(db_index=True, default=False, help_text='Explanation request is overdue')),
('reminder_sent_at', models.DateTimeField(blank=True, help_text='Reminder sent to staff about overdue explanation', null=True)),
('escalated_at', models.DateTimeField(blank=True, help_text='When explanation was escalated to manager', null=True)),
], ],
options={ options={
'verbose_name': 'Complaint Explanation', 'verbose_name': 'Complaint Explanation',
@ -80,7 +84,10 @@ class Migration(migrations.Migration):
('severity', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Severity level for this SLA', max_length=20)), ('severity', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Severity level for this SLA', max_length=20)),
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Priority level for this SLA', max_length=20)), ('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Priority level for this SLA', max_length=20)),
('sla_hours', models.IntegerField(help_text='Number of hours until SLA deadline')), ('sla_hours', models.IntegerField(help_text='Number of hours until SLA deadline')),
('reminder_hours_before', models.IntegerField(default=24, help_text='Send reminder X hours before deadline')), ('reminder_hours_before', models.IntegerField(default=24, help_text='Send first reminder X hours before deadline')),
('second_reminder_enabled', models.BooleanField(default=False, help_text='Enable sending a second reminder')),
('second_reminder_hours_before', models.IntegerField(default=6, help_text='Send second reminder X hours before deadline')),
('thank_you_email_enabled', models.BooleanField(default=False, help_text='Send thank you email when complaint is closed')),
('is_active', models.BooleanField(default=True)), ('is_active', models.BooleanField(default=True)),
], ],
options={ options={
@ -127,9 +134,13 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=200)), ('name', models.CharField(max_length=200)),
('description', models.TextField(blank=True)), ('description', models.TextField(blank=True)),
('escalation_level', models.IntegerField(default=1, help_text='Escalation level (1 = first level, 2 = second, etc.)')),
('max_escalation_level', models.IntegerField(default=3, help_text='Maximum escalation level before stopping (default: 3)')),
('trigger_on_overdue', models.BooleanField(default=True, help_text='Trigger when complaint is overdue')), ('trigger_on_overdue', models.BooleanField(default=True, help_text='Trigger when complaint is overdue')),
('trigger_hours_overdue', models.IntegerField(default=0, help_text='Trigger X hours after overdue (0 = immediately)')), ('trigger_hours_overdue', models.IntegerField(default=0, help_text='Trigger X hours after overdue (0 = immediately)')),
('escalate_to_role', models.CharField(choices=[('department_manager', 'Department Manager'), ('hospital_admin', 'Hospital Admin'), ('px_admin', 'PX Admin'), ('specific_user', 'Specific User')], help_text='Role to escalate to', max_length=50)), ('reminder_escalation_enabled', models.BooleanField(default=False, help_text='Enable escalation after reminder if no action taken')),
('reminder_escalation_hours', models.IntegerField(default=24, help_text='Escalate X hours after reminder if no action')),
('escalate_to_role', models.CharField(choices=[('department_manager', 'Department Manager'), ('hospital_admin', 'Hospital Admin'), ('px_admin', 'PX Admin'), ('ceo', 'CEO'), ('specific_user', 'Specific User')], help_text='Role to escalate to', max_length=50)),
('severity_filter', models.CharField(blank=True, choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Only escalate complaints with this severity (blank = all)', max_length=20)), ('severity_filter', models.CharField(blank=True, choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Only escalate complaints with this severity (blank = all)', max_length=20)),
('priority_filter', models.CharField(blank=True, choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Only escalate complaints with this priority (blank = all)', max_length=20)), ('priority_filter', models.CharField(blank=True, choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Only escalate complaints with this priority (blank = all)', max_length=20)),
('order', models.IntegerField(default=0, help_text='Escalation order (lower = first)')), ('order', models.IntegerField(default=0, help_text='Escalation order (lower = first)')),
@ -157,6 +168,25 @@ class Migration(migrations.Migration):
'ordering': ['-created_at'], 'ordering': ['-created_at'],
}, },
), ),
migrations.CreateModel(
name='ExplanationSLAConfig',
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)),
('response_hours', models.IntegerField(default=48, help_text='Hours staff has to submit explanation')),
('reminder_hours_before', models.IntegerField(default=12, help_text='Send reminder X hours before deadline')),
('auto_escalate_enabled', models.BooleanField(default=True, help_text='Automatically escalate to manager if no response')),
('escalation_hours_overdue', models.IntegerField(default=0, help_text='Escalate X hours after overdue (0 = immediately)')),
('max_escalation_levels', models.IntegerField(default=3, help_text='Maximum levels to escalate up staff hierarchy')),
('is_active', models.BooleanField(default=True)),
],
options={
'verbose_name': 'Explanation SLA Config',
'verbose_name_plural': 'Explanation SLA Configs',
'ordering': ['hospital'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='Inquiry', name='Inquiry',
fields=[ fields=[
@ -230,7 +260,8 @@ class Migration(migrations.Migration):
('assigned_at', models.DateTimeField(blank=True, null=True)), ('assigned_at', models.DateTimeField(blank=True, null=True)),
('due_at', models.DateTimeField(db_index=True, help_text='SLA deadline')), ('due_at', models.DateTimeField(db_index=True, help_text='SLA deadline')),
('is_overdue', models.BooleanField(db_index=True, default=False)), ('is_overdue', models.BooleanField(db_index=True, default=False)),
('reminder_sent_at', models.DateTimeField(blank=True, null=True)), ('reminder_sent_at', models.DateTimeField(blank=True, help_text='First SLA reminder timestamp', null=True)),
('second_reminder_sent_at', models.DateTimeField(blank=True, help_text='Second SLA reminder timestamp', null=True)),
('escalated_at', models.DateTimeField(blank=True, null=True)), ('escalated_at', models.DateTimeField(blank=True, null=True)),
('resolution', models.TextField(blank=True)), ('resolution', models.TextField(blank=True)),
('resolved_at', models.DateTimeField(blank=True, null=True)), ('resolved_at', models.DateTimeField(blank=True, null=True)),

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
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 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
@ -57,6 +57,11 @@ class Migration(migrations.Migration):
name='complaint', name='complaint',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='explanations', to='complaints.complaint'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='explanations', to='complaints.complaint'),
), ),
migrations.AddField(
model_name='complaintexplanation',
name='escalated_to_manager',
field=models.ForeignKey(blank=True, help_text="Escalated to this explanation (manager's explanation request)", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='escalated_from_staff', to='complaints.complaintexplanation'),
),
migrations.AddField( migrations.AddField(
model_name='complaintexplanation', model_name='complaintexplanation',
name='requested_by', name='requested_by',
@ -102,6 +107,11 @@ class Migration(migrations.Migration):
name='explanation', name='explanation',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='complaints.complaintexplanation'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='complaints.complaintexplanation'),
), ),
migrations.AddField(
model_name='explanationslaconfig',
name='hospital',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='explanation_sla_configs', to='organizations.hospital'),
),
migrations.AddField( migrations.AddField(
model_name='inquiry', model_name='inquiry',
name='assigned_to', name='assigned_to',
@ -204,6 +214,10 @@ class Migration(migrations.Migration):
model_name='escalationrule', model_name='escalationrule',
index=models.Index(fields=['hospital', 'is_active'], name='complaints__hospita_3c8bac_idx'), index=models.Index(fields=['hospital', 'is_active'], name='complaints__hospita_3c8bac_idx'),
), ),
migrations.AddIndex(
model_name='explanationslaconfig',
index=models.Index(fields=['hospital', 'is_active'], name='complaints__hospita_fe4ec5_idx'),
),
migrations.AddIndex( migrations.AddIndex(
model_name='inquiry', model_name='inquiry',
index=models.Index(fields=['status', '-created_at'], name='complaints__status_3d0678_idx'), index=models.Index(fields=['status', '-created_at'], name='complaints__status_3d0678_idx'),

View File

@ -1,68 +0,0 @@
# Generated by Django 6.0.1 on 2026-01-13 20:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('complaints', '0003_initial'),
]
operations = [
migrations.AddField(
model_name='complaint',
name='second_reminder_sent_at',
field=models.DateTimeField(blank=True, help_text='Second SLA reminder timestamp', null=True),
),
migrations.AddField(
model_name='complaintslaconfig',
name='second_reminder_enabled',
field=models.BooleanField(default=False, help_text='Enable sending a second reminder'),
),
migrations.AddField(
model_name='complaintslaconfig',
name='second_reminder_hours_before',
field=models.IntegerField(default=6, help_text='Send second reminder X hours before deadline'),
),
migrations.AddField(
model_name='complaintslaconfig',
name='thank_you_email_enabled',
field=models.BooleanField(default=False, help_text='Send thank you email when complaint is closed'),
),
migrations.AddField(
model_name='escalationrule',
name='escalation_level',
field=models.IntegerField(default=1, help_text='Escalation level (1 = first level, 2 = second, etc.)'),
),
migrations.AddField(
model_name='escalationrule',
name='max_escalation_level',
field=models.IntegerField(default=3, help_text='Maximum escalation level before stopping (default: 3)'),
),
migrations.AddField(
model_name='escalationrule',
name='reminder_escalation_enabled',
field=models.BooleanField(default=False, help_text='Enable escalation after reminder if no action taken'),
),
migrations.AddField(
model_name='escalationrule',
name='reminder_escalation_hours',
field=models.IntegerField(default=24, help_text='Escalate X hours after reminder if no action'),
),
migrations.AlterField(
model_name='complaint',
name='reminder_sent_at',
field=models.DateTimeField(blank=True, help_text='First SLA reminder timestamp', null=True),
),
migrations.AlterField(
model_name='complaintslaconfig',
name='reminder_hours_before',
field=models.IntegerField(default=24, help_text='Send first reminder X hours before deadline'),
),
migrations.AlterField(
model_name='escalationrule',
name='escalate_to_role',
field=models.CharField(choices=[('department_manager', 'Department Manager'), ('hospital_admin', 'Hospital Admin'), ('px_admin', 'PX Admin'), ('ceo', 'CEO'), ('specific_user', 'Specific User')], help_text='Role to escalate to', max_length=50),
),
]

View File

@ -1,62 +0,0 @@
# Generated by Django 6.0.1 on 2026-01-14 12:36
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('complaints', '0004_add_second_reminder_sent_at'),
('organizations', '0004_staff_location_staff_name_staff_phone'),
]
operations = [
migrations.AddField(
model_name='complaintexplanation',
name='escalated_at',
field=models.DateTimeField(blank=True, help_text='When explanation was escalated to manager', null=True),
),
migrations.AddField(
model_name='complaintexplanation',
name='escalated_to_manager',
field=models.ForeignKey(blank=True, help_text="Escalated to this explanation (manager's explanation request)", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='escalated_from_staff', to='complaints.complaintexplanation'),
),
migrations.AddField(
model_name='complaintexplanation',
name='is_overdue',
field=models.BooleanField(db_index=True, default=False, help_text='Explanation request is overdue'),
),
migrations.AddField(
model_name='complaintexplanation',
name='reminder_sent_at',
field=models.DateTimeField(blank=True, help_text='Reminder sent to staff about overdue explanation', null=True),
),
migrations.AddField(
model_name='complaintexplanation',
name='sla_due_at',
field=models.DateTimeField(blank=True, db_index=True, help_text='SLA deadline for staff to submit explanation', null=True),
),
migrations.CreateModel(
name='ExplanationSLAConfig',
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)),
('response_hours', models.IntegerField(default=48, help_text='Hours staff has to submit explanation')),
('reminder_hours_before', models.IntegerField(default=12, help_text='Send reminder X hours before deadline')),
('auto_escalate_enabled', models.BooleanField(default=True, help_text='Automatically escalate to manager if no response')),
('escalation_hours_overdue', models.IntegerField(default=0, help_text='Escalate X hours after overdue (0 = immediately)')),
('max_escalation_levels', models.IntegerField(default=3, help_text='Maximum levels to escalate up staff hierarchy')),
('is_active', models.BooleanField(default=True)),
('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='explanation_sla_configs', to='organizations.hospital')),
],
options={
'verbose_name': 'Explanation SLA Config',
'verbose_name_plural': 'Explanation SLA Configs',
'ordering': ['hospital'],
'indexes': [models.Index(fields=['hospital', 'is_active'], name='complaints__hospita_fe4ec5_idx')],
},
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
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 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
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 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
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 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -565,3 +565,8 @@ def send_whatsapp(phone, message, **kwargs):
def send_email(email, subject, message, **kwargs): def send_email(email, subject, message, **kwargs):
"""Send Email notification""" """Send Email notification"""
return NotificationService.send_email(email, subject, message, **kwargs) return NotificationService.send_email(email, subject, message, **kwargs)
def send_notification(recipient, title, message, **kwargs):
"""Send generic notification to a user"""
return NotificationService.send_notification(recipient, title, message, **kwargs)

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import apps.observations.models import apps.observations.models
import django.db.models.deletion import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
@ -15,29 +15,6 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel(
name='Hospital',
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)),
('name', models.CharField(max_length=200)),
('name_ar', models.CharField(blank=True, max_length=200, verbose_name='Name (Arabic)')),
('code', models.CharField(db_index=True, max_length=50, unique=True)),
('address', models.TextField(blank=True)),
('city', models.CharField(blank=True, max_length=100)),
('phone', models.CharField(blank=True, max_length=20)),
('email', models.EmailField(blank=True, max_length=254)),
('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)),
],
options={
'verbose_name_plural': 'Hospitals',
'ordering': ['name'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='Organization', name='Organization',
fields=[ fields=[
@ -62,6 +39,34 @@ class Migration(migrations.Migration):
'ordering': ['name'], 'ordering': ['name'],
}, },
), ),
migrations.CreateModel(
name='Hospital',
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)),
('name', models.CharField(max_length=200)),
('name_ar', models.CharField(blank=True, max_length=200, verbose_name='Name (Arabic)')),
('code', models.CharField(db_index=True, max_length=50, unique=True)),
('address', models.TextField(blank=True)),
('city', models.CharField(blank=True, max_length=100)),
('phone', models.CharField(blank=True, max_length=20)),
('email', models.EmailField(blank=True, max_length=254)),
('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')),
('ceo', models.ForeignKey(blank=True, help_text='Chief Executive Officer', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hospitals_as_ceo', to=settings.AUTH_USER_MODEL, verbose_name='CEO')),
('cfo', models.ForeignKey(blank=True, help_text='Chief Financial Officer', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hospitals_as_cfo', to=settings.AUTH_USER_MODEL, verbose_name='CFO')),
('coo', models.ForeignKey(blank=True, help_text='Chief Operating Officer', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hospitals_as_coo', to=settings.AUTH_USER_MODEL, verbose_name='COO')),
('medical_director', models.ForeignKey(blank=True, help_text='Medical Director', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hospitals_as_medical_director', to=settings.AUTH_USER_MODEL, verbose_name='Medical Director')),
('organization', models.ForeignKey(blank=True, help_text='Parent organization (null for backward compatibility)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='hospitals', to='organizations.organization')),
],
options={
'verbose_name_plural': 'Hospitals',
'ordering': ['name'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='Department', name='Department',
fields=[ fields=[
@ -84,11 +89,6 @@ class Migration(migrations.Migration):
'unique_together': {('hospital', 'code')}, 'unique_together': {('hospital', 'code')},
}, },
), ),
migrations.AddField(
model_name='hospital',
name='organization',
field=models.ForeignKey(blank=True, help_text='Parent organization (null for backward compatibility)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='hospitals', to='organizations.organization'),
),
migrations.CreateModel( migrations.CreateModel(
name='Patient', name='Patient',
fields=[ fields=[
@ -129,10 +129,19 @@ class Migration(migrations.Migration):
('license_number', models.CharField(blank=True, max_length=100, null=True, unique=True)), ('license_number', models.CharField(blank=True, max_length=100, null=True, unique=True)),
('specialization', models.CharField(blank=True, max_length=200)), ('specialization', models.CharField(blank=True, max_length=200)),
('email', models.EmailField(blank=True, max_length=254)), ('email', models.EmailField(blank=True, max_length=254)),
('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone Number')),
('employee_id', models.CharField(db_index=True, max_length=50, unique=True)), ('employee_id', models.CharField(db_index=True, max_length=50, unique=True)),
('name', models.CharField(blank=True, max_length=300, verbose_name='Full Name (Original)')),
('country', models.CharField(blank=True, max_length=100, verbose_name='Country')),
('location', models.CharField(blank=True, max_length=200, verbose_name='Location')),
('gender', models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female'), ('other', 'Other')], max_length=10)),
('department_name', models.CharField(blank=True, max_length=200, verbose_name='Department (Original)')),
('section', models.CharField(blank=True, max_length=200, verbose_name='Section')),
('subsection', models.CharField(blank=True, max_length=200, verbose_name='Subsection')),
('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, related_name='staff', 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')),
('report_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='direct_reports', to='organizations.staff', verbose_name='Reports To')),
('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)),
], ],
options={ options={

View File

@ -1,41 +0,0 @@
# Generated by Django 6.0.1 on 2026-01-13 13:07
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organizations', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='hospital',
name='ceo',
field=models.ForeignKey(blank=True, help_text='Chief Executive Officer', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hospitals_as_ceo', to=settings.AUTH_USER_MODEL, verbose_name='CEO'),
),
migrations.AddField(
model_name='hospital',
name='cfo',
field=models.ForeignKey(blank=True, help_text='Chief Financial Officer', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hospitals_as_cfo', to=settings.AUTH_USER_MODEL, verbose_name='CFO'),
),
migrations.AddField(
model_name='hospital',
name='coo',
field=models.ForeignKey(blank=True, help_text='Chief Operating Officer', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hospitals_as_coo', to=settings.AUTH_USER_MODEL, verbose_name='COO'),
),
migrations.AddField(
model_name='hospital',
name='medical_director',
field=models.ForeignKey(blank=True, help_text='Medical Director', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hospitals_as_medical_director', to=settings.AUTH_USER_MODEL, verbose_name='Medical Director'),
),
migrations.AlterField(
model_name='hospital',
name='metadata',
field=models.JSONField(blank=True, default=dict, help_text='Hospital configuration settings'),
),
]

View File

@ -1,44 +0,0 @@
# Generated by Django 6.0.1 on 2026-01-13 13:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organizations', '0002_hospital_ceo_hospital_cfo_hospital_coo_and_more'),
]
operations = [
migrations.AddField(
model_name='staff',
name='country',
field=models.CharField(blank=True, max_length=100, verbose_name='Country'),
),
migrations.AddField(
model_name='staff',
name='department_name',
field=models.CharField(blank=True, max_length=200, verbose_name='Department (Original)'),
),
migrations.AddField(
model_name='staff',
name='gender',
field=models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female'), ('other', 'Other')], max_length=10),
),
migrations.AddField(
model_name='staff',
name='report_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='direct_reports', to='organizations.staff', verbose_name='Reports To'),
),
migrations.AddField(
model_name='staff',
name='section',
field=models.CharField(blank=True, max_length=200, verbose_name='Section'),
),
migrations.AddField(
model_name='staff',
name='subsection',
field=models.CharField(blank=True, max_length=200, verbose_name='Subsection'),
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 6.0.1 on 2026-01-13 13:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organizations', '0003_staff_country_staff_department_name_staff_gender_and_more'),
]
operations = [
migrations.AddField(
model_name='staff',
name='location',
field=models.CharField(blank=True, max_length=200, verbose_name='Location'),
),
migrations.AddField(
model_name='staff',
name='name',
field=models.CharField(blank=True, max_length=300, verbose_name='Full Name (Original)'),
),
migrations.AddField(
model_name='staff',
name='phone',
field=models.CharField(blank=True, max_length=20, verbose_name='Phone Number'),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
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 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import apps.references.models import apps.references.models
import django.db.models.deletion import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-12 09:50 # Generated by Django 6.0.1 on 2026-01-15 12:02
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
</div> </div>
{% if user.is_px_admin or user.is_hospital_admin %} {% if user.is_px_admin or user.is_hospital_admin %}
<a href="{% url 'organizations:staff_create' %}" class="btn btn-primary"> <a href="{% url 'organizations:staff_create' %}" class="btn btn-primary">
<i class="fas fa-plus"></i> {% trans "Add New Staff" %} <i class="bi bi-plus"></i> {% trans "Add New Staff" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
@ -58,7 +58,7 @@
</div> </div>
<div class="col-md-2 d-flex align-items-end"> <div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100"> <button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search"></i> {% trans "Search" %} <i class="bi bi-search"></i> {% trans "Search" %}
</button> </button>
</div> </div>
</form> </form>
@ -122,12 +122,12 @@
<td> <td>
{% if staff_member.user %} {% if staff_member.user %}
<span class="badge bg-success"> <span class="badge bg-success">
<i class="fas fa-check"></i> {% trans "Yes" %} <i class="bi bi-check"></i> {% trans "Yes" %}
</span> </span>
<br><small class="text-muted">{{ staff_member.user.username }}</small> <br><small class="text-muted">{{ staff_member.user.username }}</small>
{% else %} {% else %}
<span class="badge bg-danger"> <span class="badge bg-danger">
<i class="fas fa-times"></i> {% trans "No" %} <i class="bi bi-times"></i> {% trans "No" %}
</span> </span>
{% endif %} {% endif %}
</td> </td>
@ -141,12 +141,12 @@
<td> <td>
<div class="btn-group"> <div class="btn-group">
<a href="{% url 'organizations:staff_detail' staff_member.id %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Details' %}"> <a href="{% url 'organizations:staff_detail' staff_member.id %}" class="btn btn-sm btn-outline-primary" title="{% trans 'View Details' %}">
<i class="fas fa-eye"></i> <i class="bi bi-eye"></i>
</a> </a>
{% if user.is_px_admin or user.is_hospital_admin %} {% if user.is_px_admin or user.is_hospital_admin %}
{% if not staff_member.user and staff_member.email %} {% if not staff_member.user and staff_member.email %}
<button type="button" class="btn btn-sm btn-outline-success" onclick="createUserAccount('{{ staff_member.id }}', '{{ staff_member.get_full_name }}')" title="{% trans 'Create User Account' %}"> <button type="button" class="btn btn-sm btn-outline-success" onclick="createUserAccount('{{ staff_member.id }}', '{{ staff_member.get_full_name }}')" title="{% trans 'Create User Account' %}">
<i class="fas fa-user-plus"></i> <i class="bi bi-person-fill-add"></i>
</button> </button>
{% endif %} {% endif %}
{% if staff_member.user %} {% if staff_member.user %}
@ -180,12 +180,12 @@
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page=1{% for key, value in filters.items %}{% if value %}&{{ key }}={{ value }}{% endif %}{% endfor %}"> <a class="page-link" href="?page=1{% for key, value in filters.items %}{% if value %}&{{ key }}={{ value }}{% endif %}{% endfor %}">
<i class="fas fa-angle-double-left"></i> <i class="bi bi-chevron-double-left"></i>
</a> </a>
</li> </li>
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in filters.items %}{% if value %}&{{ key }}={{ value }}{% endif %}{% endfor %}"> <a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in filters.items %}{% if value %}&{{ key }}={{ value }}{% endif %}{% endfor %}">
<i class="fas fa-angle-left"></i> <i class="bi bi-chevron-left"></i>
</a> </a>
</li> </li>
{% endif %} {% endif %}
@ -197,12 +197,12 @@
{% if page_obj.has_next %} {% if page_obj.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% for key, value in filters.items %}{% if value %}&{{ key }}={{ value }}{% endif %}{% endfor %}"> <a class="page-link" href="?page={{ page_obj.next_page_number }}{% for key, value in filters.items %}{% if value %}&{{ key }}={{ value }}{% endif %}{% endfor %}">
<i class="fas fa-angle-right"></i> <i class="bi bi-chevron-right"></i>
</a> </a>
</li> </li>
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in filters.items %}{% if value %}&{{ key }}={{ value }}{% endif %}{% endfor %}"> <a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in filters.items %}{% if value %}&{{ key }}={{ value }}{% endif %}{% endfor %}">
<i class="fas fa-angle-double-right"></i> <i class="bi bi-chevron-double-right"></i>
</a> </a>
</li> </li>
{% endif %} {% endif %}