standards app done

This commit is contained in:
Faheed 2026-01-13 18:05:54 +03:00 committed by ismail
parent 7d6d75b10b
commit 4dd3c3e505
34 changed files with 398 additions and 82 deletions

View File

@ -35,8 +35,8 @@ class UserAdmin(BaseUserAdmin):
ordering = ['-date_joined']
fieldsets = (
(None, {'fields': ('email', 'username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'phone', 'employee_id')}),
(None, {'fields': ('email', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'username', 'phone', 'employee_id')}),
(_('Organization'), {'fields': ('hospital', 'department')}),
(_('Profile'), {'fields': ('avatar', 'bio', 'language')}),
(_('Permissions'), {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,11 +39,11 @@ class ComplaintAdmin(admin.ModelAdmin):
list_display = [
'title_preview', 'patient', 'hospital', 'category',
'severity_badge', 'status_badge', 'sla_indicator',
'assigned_to', 'created_at'
'created_by', 'assigned_to', 'created_at'
]
list_filter = [
'status', 'severity', 'priority', 'category', 'source',
'is_overdue', 'hospital', 'created_at'
'is_overdue', 'hospital', 'created_by', 'created_at'
]
search_fields = [
'title', 'description', 'patient__mrn',
@ -66,6 +66,9 @@ class ComplaintAdmin(admin.ModelAdmin):
('Classification', {
'fields': ('priority', 'severity', 'source')
}),
('Creator Tracking', {
'fields': ('created_by',)
}),
('Status & Assignment', {
'fields': ('status', 'assigned_to', 'assigned_at')
}),
@ -94,7 +97,8 @@ class ComplaintAdmin(admin.ModelAdmin):
qs = super().get_queryset(request)
return qs.select_related(
'patient', 'hospital', 'department', 'staff',
'assigned_to', 'resolved_by', 'closed_by', 'resolution_survey'
'assigned_to', 'resolved_by', 'closed_by', 'resolution_survey',
'created_by'
)
def title_preview(self, obj):
@ -219,9 +223,9 @@ class InquiryAdmin(admin.ModelAdmin):
"""Inquiry admin"""
list_display = [
'subject_preview', 'patient', 'contact_name',
'hospital', 'category', 'status', 'assigned_to', 'created_at'
'hospital', 'category', 'status', 'created_by', 'assigned_to', 'created_at'
]
list_filter = ['status', 'category', 'source', 'hospital', 'created_at']
list_filter = ['status', 'category', 'source', 'hospital', 'created_by', 'created_at']
search_fields = [
'subject', 'message', 'contact_name', 'contact_phone',
'patient__mrn', 'patient__first_name', 'patient__last_name'
@ -242,6 +246,9 @@ class InquiryAdmin(admin.ModelAdmin):
('Inquiry Details', {
'fields': ('subject', 'message', 'category', 'source')
}),
('Creator Tracking', {
'fields': ('created_by',)
}),
('Status & Assignment', {
'fields': ('status', 'assigned_to')
}),
@ -259,7 +266,7 @@ class InquiryAdmin(admin.ModelAdmin):
qs = super().get_queryset(request)
return qs.select_related(
'patient', 'hospital', 'department',
'assigned_to', 'responded_by'
'assigned_to', 'responded_by', 'created_by'
)
def subject_preview(self, obj):

View File

@ -1,4 +1,5 @@
# Generated by Django 6.0.1 on 2026-01-15 12:02
# Generated by Django 6.0 on 2026-01-12 09:50
import django.db.models.deletion
import uuid
@ -64,10 +65,6 @@ class Migration(migrations.Migration):
('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)),
('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={
'verbose_name': 'Complaint Explanation',
@ -168,25 +165,6 @@ class Migration(migrations.Migration):
'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(
name='Inquiry',
fields=[

View File

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

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-15 12:02
# Generated by Django 6.0 on 2026-01-12 09:50
import django.db.models.deletion
from django.conf import settings
@ -57,11 +57,6 @@ class Migration(migrations.Migration):
name='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(
model_name='complaintexplanation',
name='requested_by',
@ -107,11 +102,6 @@ class Migration(migrations.Migration):
name='explanation',
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(
model_name='inquiry',
name='assigned_to',
@ -214,10 +204,6 @@ class Migration(migrations.Migration):
model_name='escalationrule',
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(
model_name='inquiry',
index=models.Index(fields=['status', '-created_at'], name='complaints__status_3d0678_idx'),

View File

@ -180,6 +180,7 @@ class Complaint(UUIDModel, TimeStampedModel):
blank=True,
related_name='created_complaints',
help_text="User who created this complaint (SourceUser or Patient)"
help_text="Source of complaint"
)
# Status and workflow
@ -760,17 +761,7 @@ class Inquiry(UUIDModel, TimeStampedModel):
related_name="inquiries",
null=True,
blank=True,
help_text="Source of inquiry",
)
# Creator tracking
created_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_inquiries',
help_text="User who created this inquiry (SourceUser or Patient)"
help_text="Source of inquiry"
)
# Status

View File

@ -1029,8 +1029,13 @@ def inquiry_create(request):
from .models import Inquiry
from .forms import InquiryForm
from apps.organizations.models import Patient
if request.method == "POST":
# Determine base layout based on user type
from apps.px_sources.models import SourceUser
source_user = SourceUser.objects.filter(user=request.user).first()
base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html'
if request.method == 'POST':
try:
# Get form data
patient_id = request.POST.get("patient_id", None)
@ -1086,7 +1091,9 @@ def inquiry_create(request):
hospitals = hospitals.filter(id=request.user.hospital.id)
context = {
"hospitals": hospitals,
'hospitals': hospitals,
'base_layout': base_layout,
'source_user': source_user,
}
return render(request, "complaints/inquiry_form.html", context)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,38 @@
# Generated by Django 6.0.1 on 2026-01-20 12:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('journeys', '0002_initial'),
('surveys', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='patientjourneystagetemplate',
name='auto_send_survey',
),
migrations.RemoveField(
model_name='patientjourneystagetemplate',
name='survey_delay_hours',
),
migrations.AddField(
model_name='patientjourneytemplate',
name='post_discharge_survey_delay_hours',
field=models.IntegerField(default=1, help_text='Hours after discharge to send the survey'),
),
migrations.AddField(
model_name='patientjourneytemplate',
name='send_post_discharge_survey',
field=models.BooleanField(default=False, help_text='Send a comprehensive survey after patient discharge'),
),
migrations.AlterField(
model_name='patientjourneystagetemplate',
name='survey_template',
field=models.ForeignKey(blank=True, help_text='Survey template containing questions for this stage (merged into post-discharge survey)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='journey_stages', to='surveys.surveytemplate'),
),
]

View File

@ -0,0 +1,37 @@
# Generated by Django 6.0.1 on 2026-01-20 13:49
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('journeys', '0003_remove_patientjourneystagetemplate_auto_send_survey_and_more'),
]
operations = [
migrations.RemoveField(
model_name='patientjourneystageinstance',
name='completed_by_event',
),
migrations.RemoveField(
model_name='patientjourneystageinstance',
name='survey_instance',
),
migrations.RemoveField(
model_name='patientjourneystageinstance',
name='survey_sent_at',
),
migrations.RemoveField(
model_name='patientjourneystagetemplate',
name='description',
),
migrations.RemoveField(
model_name='patientjourneystagetemplate',
name='requires_department',
),
migrations.RemoveField(
model_name='patientjourneystagetemplate',
name='requires_physician',
),
]

View File

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

View File

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

View File

@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-01-15 12:02
# Generated by Django 6.0 on 2026-01-12 09:50
import django.db.models.deletion
import uuid
@ -129,7 +129,6 @@ class Migration(migrations.Migration):
('license_number', models.CharField(blank=True, max_length=100, null=True, unique=True)),
('specialization', models.CharField(blank=True, max_length=200)),
('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)),
('name', models.CharField(blank=True, max_length=300, verbose_name='Full Name (Original)')),
('country', models.CharField(blank=True, max_length=100, verbose_name='Country')),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,57 @@
# Generated by Django 6.0.1 on 2026-01-20 13:49
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('surveys', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='surveyinstance',
name='journey_stage_instance',
),
migrations.RemoveField(
model_name='surveyinstance',
name='satisfaction_feedback_sent',
),
migrations.RemoveField(
model_name='surveyinstance',
name='satisfaction_feedback_sent_at',
),
migrations.RemoveField(
model_name='surveyquestion',
name='branch_logic',
),
migrations.RemoveField(
model_name='surveyquestion',
name='help_text',
),
migrations.RemoveField(
model_name='surveyquestion',
name='help_text_ar',
),
migrations.RemoveField(
model_name='surveyquestion',
name='weight',
),
migrations.RemoveField(
model_name='surveyresponse',
name='response_time_seconds',
),
migrations.RemoveField(
model_name='surveytemplate',
name='description',
),
migrations.RemoveField(
model_name='surveytemplate',
name='description_ar',
),
migrations.RemoveField(
model_name='surveytemplate',
name='version',
),
]

View File

@ -0,0 +1,96 @@
# Generated migration for survey tracking features
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('surveys', '0002_remove_surveyinstance_journey_stage_instance_and_more'),
]
operations = [
# Add tracking fields to SurveyInstance
migrations.AddField(
model_name='surveyinstance',
name='open_count',
field=models.PositiveIntegerField(default=0, help_text='Number of times the survey link was opened'),
),
migrations.AddField(
model_name='surveyinstance',
name='last_opened_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='surveyinstance',
name='time_spent_seconds',
field=models.PositiveIntegerField(default=0, help_text='Total time spent on survey in seconds'),
),
# Update status field choices
migrations.AlterField(
model_name='surveyinstance',
name='status',
field=models.CharField(
choices=[
('sent', 'Sent'),
('viewed', 'Viewed'),
('in_progress', 'In Progress'),
('completed', 'Completed'),
('abandoned', 'Abandoned'),
('expired', 'Expired'),
('cancelled', 'Cancelled'),
],
default='sent',
max_length=20,
help_text='Current status of the survey instance'
),
),
# Create SurveyTracking model
migrations.CreateModel(
name='SurveyTracking',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('event_type', models.CharField(
choices=[
('page_view', 'Page View'),
('survey_started', 'Survey Started'),
('question_answered', 'Question Answered'),
('survey_completed', 'Survey Completed'),
('survey_abandoned', 'Survey Abandoned'),
('reminder_sent', 'Reminder Sent'),
],
default='page_view',
max_length=20,
help_text='Type of tracking event'
)),
('time_on_page', models.PositiveIntegerField(blank=True, null=True, help_text='Time spent on current page in seconds')),
('total_time_spent', models.PositiveIntegerField(default=0, help_text='Total time spent in survey in seconds')),
('current_question', models.PositiveIntegerField(blank=True, null=True, help_text='Current question number being viewed')),
('user_agent', models.TextField(blank=True, help_text='Browser user agent string')),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('device_type', models.CharField(blank=True, max_length=50)),
('browser', models.CharField(blank=True, max_length=50)),
('country', models.CharField(blank=True, max_length=100)),
('city', models.CharField(blank=True, max_length=100)),
('metadata', models.JSONField(blank=True, null=True, default=dict, help_text='Additional tracking metadata')),
('survey_instance', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='tracking_events',
to='surveys.surveyinstance'
)),
],
options={
'verbose_name': 'Survey Tracking',
'verbose_name_plural': 'Survey Tracking Events',
'ordering': ['-created_at'],
'indexes': [
models.Index(fields=['survey_instance', '-created_at'], name='idx_survey_instance_created'),
models.Index(fields=['event_type', '-created_at'], name='idx_event_type_created'),
models.Index(fields=['ip_address'], name='idx_ip_address'),
],
},
),
]

View File

@ -0,0 +1,110 @@
# Generated by Django 6.0.1 on 2026-01-21 13:54
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('surveys', '0003_add_survey_tracking'),
]
operations = [
migrations.AlterModelOptions(
name='surveytracking',
options={'ordering': ['survey_instance', 'created_at']},
),
migrations.RemoveIndex(
model_name='surveytracking',
name='idx_survey_instance_created',
),
migrations.RemoveIndex(
model_name='surveytracking',
name='idx_ip_address',
),
migrations.RenameIndex(
model_name='surveytracking',
new_name='surveys_sur_event_t_885d23_idx',
old_name='idx_event_type_created',
),
migrations.AddField(
model_name='surveytracking',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name='surveyinstance',
name='last_opened_at',
field=models.DateTimeField(blank=True, help_text='Most recent time survey was opened', null=True),
),
migrations.AlterField(
model_name='surveyinstance',
name='open_count',
field=models.IntegerField(default=0, help_text='Number of times survey link was opened'),
),
migrations.AlterField(
model_name='surveyinstance',
name='status',
field=models.CharField(choices=[('sent', 'Sent (Not Opened)'), ('viewed', 'Viewed (Opened, Not Started)'), ('in_progress', 'In Progress (Started, Not Completed)'), ('completed', 'Completed'), ('abandoned', 'Abandoned (Started but Left)'), ('expired', 'Expired'), ('cancelled', 'Cancelled')], db_index=True, default='sent', max_length=20),
),
migrations.AlterField(
model_name='surveyinstance',
name='time_spent_seconds',
field=models.IntegerField(blank=True, help_text='Total time spent on survey in seconds', null=True),
),
migrations.AlterField(
model_name='surveytracking',
name='browser',
field=models.CharField(blank=True, max_length=100),
),
migrations.AlterField(
model_name='surveytracking',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True),
),
migrations.AlterField(
model_name='surveytracking',
name='current_question',
field=models.IntegerField(blank=True, help_text='Question number when event occurred', null=True),
),
migrations.AlterField(
model_name='surveytracking',
name='device_type',
field=models.CharField(blank=True, help_text='mobile, tablet, desktop', max_length=50),
),
migrations.AlterField(
model_name='surveytracking',
name='event_type',
field=models.CharField(choices=[('page_view', 'Page View'), ('survey_started', 'Survey Started'), ('question_answered', 'Question Answered'), ('survey_abandoned', 'Survey Abandoned'), ('survey_completed', 'Survey Completed'), ('reminder_sent', 'Reminder Sent')], db_index=True, max_length=50),
),
migrations.AlterField(
model_name='surveytracking',
name='id',
field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='surveytracking',
name='metadata',
field=models.JSONField(blank=True, default=dict),
),
migrations.AlterField(
model_name='surveytracking',
name='time_on_page',
field=models.IntegerField(blank=True, help_text='Time spent on page in seconds', null=True),
),
migrations.AlterField(
model_name='surveytracking',
name='total_time_spent',
field=models.IntegerField(blank=True, help_text='Total time spent on survey so far in seconds', null=True),
),
migrations.AlterField(
model_name='surveytracking',
name='user_agent',
field=models.TextField(blank=True),
),
migrations.AddIndex(
model_name='surveytracking',
index=models.Index(fields=['survey_instance', 'event_type', '-created_at'], name='surveys_sur_survey__9743a1_idx'),
),
]

View File

@ -1,4 +1,5 @@
{% extends base_layout %}
{% extends base_layout %}
{% load i18n %}
{% load static %}

View File

@ -156,6 +156,15 @@
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Due Date -->
<div class="mb-3">
<label class="form-label">{% trans "Due Date" %}</label>
<input type="datetime-local" name="due_date" class="form-control"
placeholder="{% trans 'Optional due date' %}">
<small class="form-text text-muted">
{{ _("Leave empty for default")}}
</small>
</div>
<!-- Help Information -->
<div class="alert alert-info">