HH/apps/complaints/migrations/0001_initial.py
ismail fd19216b0d
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m5s
track migrations in git, regenerate fresh initial migrations, add staging-test compose, fix .gitignore
2026-05-11 23:34:39 +03:00

710 lines
62 KiB
Python

# Generated by Django 6.0.1 on 2026-05-11 20:32
import apps.core.encryption
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('organizations', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ComplaintAdverseAction',
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)),
('action_type', models.CharField(choices=[('refused_service', 'Refused Service'), ('delayed_treatment', 'Delayed Treatment'), ('verbal_abuse', 'Verbal Abuse / Hostility'), ('increased_wait', 'Increased Wait Time'), ('unnecessary_procedure', 'Unnecessary Procedure'), ('dismissed_from_care', 'Dismissed from Care'), ('poor_treatment', 'Poor Treatment Quality'), ('discrimination', 'Discrimination'), ('retaliation', 'Retaliation'), ('other', 'Other')], default='other', help_text='Type of adverse action', max_length=30)),
('severity', models.CharField(choices=[('low', 'Low - Minor inconvenience'), ('medium', 'Medium - Moderate impact'), ('high', 'High - Significant harm'), ('critical', 'Critical - Severe harm / Life-threatening')], default='medium', help_text='Severity level of the adverse action', max_length=10)),
('description', models.TextField(help_text='Detailed description of what happened to the patient')),
('incident_date', models.DateTimeField(help_text='Date and time when the adverse action occurred')),
('location', models.CharField(blank=True, help_text='Location where the incident occurred (e.g., Emergency Room, Clinic B)', max_length=200)),
('patient_impact', models.TextField(blank=True, help_text='Description of the impact on the patient (physical, emotional, financial)')),
('status', models.CharField(choices=[('reported', 'Reported - Awaiting Review'), ('under_investigation', 'Under Investigation'), ('verified', 'Verified'), ('unfounded', 'Unfounded'), ('resolved', 'Resolved')], default='reported', help_text='Current status of the adverse action report', max_length=30)),
('investigation_notes', models.TextField(blank=True, help_text='Notes from the investigation')),
('investigated_at', models.DateTimeField(blank=True, help_text='When the investigation was completed', null=True)),
('resolution', models.TextField(blank=True, help_text='How the adverse action was resolved')),
('resolved_at', models.DateTimeField(blank=True, help_text='When the adverse action was resolved', null=True)),
('is_escalated', models.BooleanField(default=False, help_text='Whether this adverse action has been escalated to management')),
('escalated_at', models.DateTimeField(blank=True, help_text='When the adverse action was escalated', null=True)),
],
options={
'verbose_name': 'Complaint Adverse Action',
'verbose_name_plural': 'Complaint Adverse Actions',
'ordering': ['-incident_date', '-created_at'],
},
),
migrations.CreateModel(
name='ComplaintAdverseActionAttachment',
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(help_text='Attachment file (image, document, audio recording, etc.)', upload_to='complaints/adverse_actions/%Y/%m/%d/')),
('filename', models.CharField(max_length=255)),
('file_type', models.CharField(blank=True, max_length=100)),
('file_size', models.IntegerField(help_text='File size in bytes')),
('description', models.TextField(blank=True, help_text='Description of what this attachment shows')),
],
options={
'verbose_name': 'Adverse Action Attachment',
'verbose_name_plural': 'Adverse Action Attachments',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='ComplaintAttachment',
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='complaints/%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='ComplaintCategory',
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)),
('code', models.CharField(help_text='Unique code for this category', max_length=100)),
('name_en', models.CharField(max_length=200)),
('name_ar', models.CharField(blank=True, max_length=200)),
('description_en', models.TextField(blank=True)),
('description_ar', models.TextField(blank=True)),
('level', models.IntegerField(choices=[(1, 'Domain'), (2, 'Category'), (3, 'Subcategory'), (4, 'Classification')], help_text='Hierarchy level (1=Domain, 2=Category, 3=Subcategory, 4=Classification)')),
('domain_type', models.CharField(blank=True, choices=[('CLINICAL', 'Clinical'), ('MANAGEMENT', 'Management'), ('RELATIONSHIPS', 'Relationships')], help_text='Domain type for top-level categories', max_length=20)),
('order', models.IntegerField(default=0, help_text='Display order')),
('is_active', models.BooleanField(default=True)),
],
options={
'verbose_name_plural': 'Complaint Categories',
'ordering': ['order', 'name_en'],
},
),
migrations.CreateModel(
name='ComplaintCommunication',
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)),
('communication_type', models.CharField(choices=[('phone_call', 'Phone Call'), ('email', 'Email'), ('sms', 'SMS'), ('meeting', 'Meeting'), ('letter', 'Letter'), ('other', 'Other')], help_text='Type of communication', max_length=20)),
('direction', models.CharField(choices=[('inbound', 'Inbound'), ('outbound', 'Outbound')], help_text='Direction of communication', max_length=20)),
('contacted_person', models.CharField(help_text='Name of person contacted', max_length=200)),
('contacted_role', models.CharField(blank=True, help_text='Role/relation (e.g., Complainant, Patient, Staff)', max_length=100)),
('contacted_phone', models.CharField(blank=True, help_text='Phone number', max_length=20)),
('contacted_email', models.EmailField(blank=True, help_text='Email address', max_length=254)),
('subject', models.CharField(blank=True, help_text='Subject/summary of communication', max_length=500)),
('notes', models.TextField(help_text='Details of what was discussed')),
('requires_followup', models.BooleanField(default=False, help_text='Whether this communication requires follow-up')),
('followup_date', models.DateField(blank=True, help_text='Date when follow-up is needed', null=True)),
('followup_notes', models.TextField(blank=True, help_text='Notes from follow-up')),
('attachment', models.FileField(blank=True, help_text='Attached document (email export, letter, etc.)', null=True, upload_to='complaints/communications/%Y/%m/%d/')),
],
options={
'verbose_name': 'Complaint Communication',
'verbose_name_plural': 'Complaint Communications',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='ComplaintExplanation',
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)),
('explanation', models.TextField(help_text="Staff's explanation about the complaint")),
('token', models.CharField(help_text='Unique access token for explanation submission', max_length=64, unique=True)),
('is_used', models.BooleanField(db_index=True, default=False, help_text='Token expiry tracking - becomes True after submission')),
('submitted_via', models.CharField(choices=[('email_link', 'Email Link'), ('direct', 'Direct Entry')], default='email_link', help_text='How the explanation was submitted', max_length=20)),
('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='First reminder sent to staff about overdue explanation', null=True)),
('second_reminder_sent_at', models.DateTimeField(blank=True, help_text='Second 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)),
('acceptance_status', models.CharField(choices=[('pending', 'Pending Review'), ('acceptable', 'Acceptable'), ('not_acceptable', 'Not Acceptable')], default='pending', help_text='Review status of the explanation', max_length=20)),
('accepted_at', models.DateTimeField(blank=True, help_text='When the explanation was reviewed', null=True)),
('acceptance_notes', models.TextField(blank=True, help_text='Notes about the acceptance decision')),
],
options={
'verbose_name': 'Complaint Explanation',
'verbose_name_plural': 'Complaint Explanations',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='ComplaintInvolvedDepartment',
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(choices=[('primary', 'Primary Department'), ('secondary', 'Secondary/Supporting'), ('coordination', 'Coordination Only'), ('investigating', 'Investigating')], default='secondary', help_text='Role of this department in the complaint resolution', max_length=20)),
('is_primary', models.BooleanField(default=False, help_text='Mark as the primary responsible department')),
('notes', models.TextField(blank=True, help_text="Additional notes about this department's involvement")),
('assigned_at', models.DateTimeField(blank=True, null=True)),
('response_submitted', models.BooleanField(default=False, help_text='Whether this department has submitted their response')),
('response_submitted_at', models.DateTimeField(blank=True, null=True)),
('response_notes', models.TextField(blank=True, help_text="Department's response/feedback on the complaint")),
('forwarded_at', models.DateTimeField(blank=True, help_text='When complaint was sent to this department', null=True)),
('first_reminder_sent_at', models.DateTimeField(blank=True, help_text='When first reminder was sent to this department', null=True)),
('second_reminder_sent_at', models.DateTimeField(blank=True, help_text='When second reminder was sent to this department', null=True)),
('delay_reason', models.TextField(blank=True, help_text='Reason for department delay in response')),
('delayed_person', models.CharField(blank=True, help_text='Name of person responsible for delay', max_length=200)),
],
options={
'verbose_name': 'Complaint Involved Department',
'verbose_name_plural': 'Complaint Involved Departments',
'ordering': ['-is_primary', '-created_at'],
},
),
migrations.CreateModel(
name='ComplaintInvolvedStaff',
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(choices=[('accused', 'Accused/Involved'), ('witness', 'Witness'), ('responsible', 'Responsible for Resolution'), ('investigator', 'Investigator'), ('support', 'Support Staff'), ('px_management', 'PX Management')], default='accused', help_text='Role of this staff member in the complaint', max_length=20)),
('notes', models.TextField(blank=True, help_text="Additional notes about this staff member's involvement")),
('explanation_requested', models.BooleanField(default=False, help_text='Whether an explanation has been requested from this staff')),
('explanation_requested_at', models.DateTimeField(blank=True, null=True)),
('explanation_received', models.BooleanField(default=False, help_text='Whether an explanation has been received')),
('explanation_received_at', models.DateTimeField(blank=True, null=True)),
('explanation', models.TextField(blank=True, help_text="The staff member's explanation")),
],
options={
'verbose_name': 'Complaint Involved Staff',
'verbose_name_plural': 'Complaint Involved Staff',
'ordering': ['role', '-created_at'],
},
),
migrations.CreateModel(
name='ComplaintMeeting',
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)),
('meeting_date', models.DateTimeField(help_text='Date and time of the meeting')),
('meeting_type', models.CharField(choices=[('management_intervention', 'Management Intervention'), ('pr_follow_up', 'PR Follow-up'), ('department_review', 'Department Review'), ('other', 'Other')], default='management_intervention', help_text='Type of meeting', max_length=50)),
('outcome', models.TextField(blank=True, help_text='Meeting outcome and agreed resolution')),
('notes', models.TextField(blank=True, help_text='Additional meeting notes')),
],
options={
'verbose_name': 'Complaint Meeting',
'verbose_name_plural': 'Complaint Meetings',
'ordering': ['-meeting_date'],
},
),
migrations.CreateModel(
name='ComplaintPRInteraction',
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)),
('contact_date', models.DateTimeField(help_text='Date and time of PR contact with complainant')),
('contact_method', models.CharField(choices=[('phone', 'Phone'), ('in_person', 'In Person'), ('email', 'Email'), ('other', 'Other')], default='in_person', help_text='Method of contact', max_length=20)),
('statement_text', models.TextField(blank=True, help_text='Formal statement taken from the complainant')),
('procedure_explained', models.BooleanField(default=False, help_text='Whether complaint procedure was explained to the complainant')),
('notes', models.TextField(blank=True, help_text='Additional notes from the PR interaction')),
],
options={
'verbose_name': 'PR Interaction',
'verbose_name_plural': 'PR Interactions',
'ordering': ['-contact_date'],
},
),
migrations.CreateModel(
name='ComplaintSLAConfig',
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)),
('severity', models.CharField(blank=True, choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Severity level for this SLA (optional if source is specified)', max_length=20, null=True)),
('priority', models.CharField(blank=True, choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Priority level for this SLA (optional if source is specified)', max_length=20, null=True)),
('sla_hours', models.IntegerField(help_text='Number of hours until SLA deadline')),
('first_reminder_hours_after', models.IntegerField(default=0, help_text='Send 1st reminder X hours after complaint creation (0 = use reminder_hours_before)')),
('second_reminder_hours_after', models.IntegerField(default=0, help_text='Send 2nd reminder X hours after complaint creation (0 = use second_reminder_hours_before)')),
('escalation_hours_after', models.IntegerField(default=0, help_text='Escalate complaint X hours after creation if unresolved (0 = use overdue logic)')),
('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)),
],
options={
'ordering': ['hospital', 'source', 'severity', 'priority'],
},
),
migrations.CreateModel(
name='ComplaintTemplate',
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(help_text="Template name (e.g., 'Long Wait Time', 'Rude Staff')", max_length=200)),
('description', models.TextField(help_text='Default description template with placeholders')),
('default_severity', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], default='medium', help_text='Default severity level', max_length=20)),
('default_priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], default='medium', help_text='Default priority level', max_length=20)),
('usage_count', models.IntegerField(default=0, editable=False, help_text='Number of times this template has been used')),
('placeholders', models.JSONField(blank=True, default=list, help_text='List of placeholder names used in description')),
('is_active', models.BooleanField(db_index=True, default=True, help_text='Whether this template is available for selection')),
],
options={
'verbose_name': 'Complaint Template',
'verbose_name_plural': 'Complaint Templates',
'ordering': ['-usage_count', 'name'],
},
),
migrations.CreateModel(
name='ComplaintThreshold',
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)),
('threshold_type', models.CharField(choices=[('resolution_survey_score', 'Resolution Survey Score'), ('response_time', 'Response Time'), ('resolution_time', 'Resolution Time')], help_text='Type of threshold', max_length=50)),
('threshold_value', models.FloatField(help_text='Threshold value (e.g., 50 for 50% score)')),
('comparison_operator', models.CharField(choices=[('lt', 'Less Than'), ('lte', 'Less Than or Equal'), ('gt', 'Greater Than'), ('gte', 'Greater Than or Equal'), ('eq', 'Equal')], default='lt', help_text='How to compare against threshold', max_length=10)),
('action_type', models.CharField(choices=[('create_px_action', 'Create PX Action'), ('send_notification', 'Send Notification'), ('escalate', 'Escalate')], help_text='Action to take when threshold is breached', max_length=50)),
('is_active', models.BooleanField(default=True)),
],
options={
'ordering': ['hospital', 'threshold_type'],
},
),
migrations.CreateModel(
name='ComplaintUpdate',
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'), ('resolution', 'Resolution'), ('escalation', 'Escalation'), ('communication', 'Communication')], db_index=True, max_length=50)),
('message', models.TextField()),
('old_status', models.CharField(blank=True, max_length=25)),
('new_status', models.CharField(blank=True, max_length=25)),
('metadata', models.JSONField(blank=True, default=dict)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='EscalationRule',
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)),
('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_hours_overdue', models.IntegerField(default=0, help_text='Trigger X hours after overdue (0 = immediately)')),
('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'), ('medical_director', 'Medical Director'), ('admin_director', 'Administrative Director'), ('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)),
('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)')),
('is_active', models.BooleanField(default=True)),
],
options={
'ordering': ['hospital', 'order'],
},
),
migrations.CreateModel(
name='ExplanationAttachment',
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='explanation_attachments/%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={
'verbose_name': 'Explanation Attachment',
'verbose_name_plural': 'Explanation Attachments',
'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 first reminder X hours before deadline')),
('second_reminder_enabled', models.BooleanField(default=True, help_text='Enable sending a second reminder before escalation')),
('second_reminder_hours_before', models.IntegerField(default=4, help_text='Send second 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='GovernmentTicket',
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)),
('ticket_number', models.CharField(db_index=True, help_text='Ticket number from source system (e.g., B2022807)', max_length=50, unique=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed')], db_index=True, default='pending', max_length=20)),
('complainant_name', models.CharField(max_length=200)),
('national_id', models.CharField(blank=True, max_length=20)),
('contact_number', models.CharField(blank=True, max_length=20)),
('received_date', models.DateTimeField(help_text='Date/time the ticket was received from source')),
('classification', models.CharField(blank=True, max_length=100)),
('content', models.TextField()),
('converted_to_complaint', models.BooleanField(db_index=True, default=False)),
],
options={
'verbose_name': 'Government Ticket',
'verbose_name_plural': 'Government Tickets',
'ordering': ['-received_date'],
},
),
migrations.CreateModel(
name='Inquiry',
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_deleted', models.BooleanField(db_index=True, default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('contact_name', models.CharField(blank=True, max_length=200)),
('contact_phone', models.CharField(blank=True, max_length=20)),
('contact_email', models.EmailField(blank=True, max_length=254)),
('reference_number', models.CharField(blank=True, max_length=50, unique=True)),
('subject', models.CharField(max_length=500)),
('message', models.TextField()),
('category', models.CharField(choices=[('appointment', 'Appointment'), ('billing', 'Billing'), ('medical_records', 'Medical Records'), ('general', 'General Information'), ('other', 'Other')], max_length=100)),
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], db_index=True, default='low', max_length=20)),
('ai_brief_en', models.CharField(blank=True, help_text='AI-generated brief summary (English)', max_length=500)),
('ai_brief_ar', models.CharField(blank=True, help_text='AI-generated brief summary (Arabic)', max_length=500)),
('status', models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('contacted', 'Contacted'), ('contacted_no_response', 'Contacted, No Response')], db_index=True, default='open', max_length=25)),
('assigned_at', models.DateTimeField(blank=True, null=True)),
('activated_at', models.DateTimeField(blank=True, db_index=True, help_text='Timestamp when inquiry was first activated (moved from OPEN to IN_PROGRESS)', null=True)),
('due_at', models.DateTimeField(blank=True, db_index=True, help_text='SLA deadline', null=True)),
('is_overdue', models.BooleanField(db_index=True, default=False)),
('breached_at', models.DateTimeField(blank=True, db_index=True, help_text='Timestamp when inquiry first breached SLA', 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)),
('response', models.TextField(blank=True)),
('response_en', models.TextField(blank=True, help_text='Response text (English)')),
('response_ar', models.TextField(blank=True, help_text='Response text (Arabic)')),
('response_sent_at', models.DateTimeField(blank=True, help_text='When response was sent to inquirer', null=True)),
('responded_at', models.DateTimeField(blank=True, null=True)),
('metadata', models.JSONField(blank=True, default=dict)),
('is_straightforward', models.BooleanField(default=True, help_text='Direct resolution (no department coordination needed)', verbose_name='Is Straightforward')),
('is_outgoing', models.BooleanField(default=False, help_text='Inquiry sent to external department for response', verbose_name='Is Outgoing')),
('transferred_at', models.DateTimeField(blank=True, db_index=True, help_text='When the inquiry was transferred to a department', null=True)),
('transfer_count', models.PositiveIntegerField(default=0, help_text='Number of times this inquiry has been transferred')),
('department_response_en', models.TextField(blank=True, verbose_name='Department Response (English)')),
('department_response_ar', models.TextField(blank=True, verbose_name='Department Response (Arabic)')),
('department_response_summary_en', models.TextField(blank=True, verbose_name='AI Summary of Dept Response (EN)')),
('department_response_summary_ar', models.TextField(blank=True, verbose_name='AI Summary of Dept Response (AR)')),
('department_responded_at', models.DateTimeField(blank=True, null=True)),
('dept_response_sla_due_at', models.DateTimeField(blank=True, db_index=True, help_text='SLA deadline for department response', null=True)),
('dept_response_is_overdue', models.BooleanField(db_index=True, default=False, help_text='Department response is overdue')),
('dept_response_reminder_sent_at', models.DateTimeField(blank=True, help_text='First SLA reminder for dept response', null=True)),
('dept_response_second_reminder_sent_at', models.DateTimeField(blank=True, help_text='Second SLA reminder for dept response', null=True)),
('dept_response_escalated_at', models.DateTimeField(blank=True, help_text='When dept response was escalated to manager', null=True)),
('dept_response_acceptance_status', models.CharField(choices=[('pending', 'Pending Review'), ('acceptable', 'Acceptable'), ('not_acceptable', 'Not Acceptable')], default='pending', help_text='Review status of the department response', max_length=20)),
('dept_response_accepted_at', models.DateTimeField(blank=True, help_text='When the department response was reviewed', null=True)),
('dept_response_acceptance_notes', models.TextField(blank=True, help_text='Notes about the acceptance decision')),
('contacted_nr_at', models.DateTimeField(blank=True, null=True, verbose_name='Contacted NR Date')),
('contacted_nr_time', models.TimeField(blank=True, null=True, verbose_name='Contacted NR Time')),
('contacted_nr_duration', models.DurationField(blank=True, null=True, verbose_name='Contacted NR Duration')),
('under_process_at', models.DateTimeField(blank=True, null=True, verbose_name='Under Process Date')),
('under_process_time', models.TimeField(blank=True, null=True, verbose_name='Under Process Time')),
('under_process_duration', models.DurationField(blank=True, null=True, verbose_name='Under Process Duration')),
('contacted_at', models.DateTimeField(blank=True, null=True, verbose_name='Contacted Date')),
('contacted_time', models.TimeField(blank=True, null=True, verbose_name='Contacted Time')),
('contacted_duration', models.DurationField(blank=True, null=True, verbose_name='Contacted Duration')),
('timeline_sla', models.CharField(blank=True, choices=[('24_hours', '24 Hours'), ('48_hours', '48 Hours'), ('72_hours', '72 Hours'), ('more_than_72_hours', 'More than 72 hours')], help_text='Response deadline category', max_length=50, verbose_name='Timeline SLA')),
('staff_notes', models.TextField(blank=True, verbose_name='Staff Notes')),
('supervisor_notes', models.TextField(blank=True, verbose_name='Supervisor Notes')),
('requires_follow_up', models.BooleanField(db_index=True, default=False, help_text='This inquiry requires follow-up call')),
('follow_up_due_at', models.DateTimeField(blank=True, db_index=True, help_text='Due date for follow-up call to inquirer', null=True)),
('follow_up_completed_at', models.DateTimeField(blank=True, help_text='When follow-up call was completed', null=True)),
('follow_up_notes', models.TextField(blank=True, help_text='Notes from follow-up call')),
('follow_up_reminder_sent_at', models.DateTimeField(blank=True, help_text='When reminder was sent for follow-up', null=True)),
],
options={
'verbose_name_plural': 'Inquiries',
'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='InquiryExplanation',
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)),
('explanation', models.TextField(blank=True, help_text="Staff's response to the inquiry")),
('token', models.CharField(help_text='Unique access token for response submission', max_length=64, unique=True)),
('is_used', models.BooleanField(db_index=True, default=False, help_text='Token expiry tracking - becomes True after submission')),
('submitted_via', models.CharField(choices=[('email_link', 'Email Link'), ('direct', 'Direct Entry')], default='email_link', max_length=20)),
('email_sent_at', models.DateTimeField(blank=True, help_text='When the response request email was sent', null=True)),
('responded_at', models.DateTimeField(blank=True, help_text='When the response was submitted', null=True)),
('request_message', models.TextField(blank=True, help_text='Optional message sent with the response request')),
('sla_due_at', models.DateTimeField(blank=True, db_index=True, help_text='SLA deadline for staff to submit response', null=True)),
('is_overdue', models.BooleanField(db_index=True, default=False)),
('reminder_sent_at', models.DateTimeField(blank=True, null=True)),
('second_reminder_sent_at', models.DateTimeField(blank=True, null=True)),
('escalated_at', models.DateTimeField(blank=True, null=True)),
],
options={
'verbose_name': 'Inquiry Explanation',
'verbose_name_plural': 'Inquiry Explanations',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='InquiryExplanationAttachment',
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='inquiry_explanation_attachments/%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={
'verbose_name': 'Inquiry Explanation Attachment',
'verbose_name_plural': 'Inquiry Explanation Attachments',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='InquirySLAConfig',
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)),
('sla_hours', models.IntegerField(help_text='Number of hours until SLA deadline')),
('first_reminder_hours_after', models.IntegerField(default=0, help_text='Send 1st reminder X hours after inquiry creation (0 = use reminder_hours_before)')),
('second_reminder_hours_after', models.IntegerField(default=0, help_text='Send 2nd reminder X hours after inquiry creation (0 = use second_reminder_hours_before)')),
('escalation_hours_after', models.IntegerField(default=0, help_text='Escalate inquiry X hours after creation if unresolved (0 = use overdue logic)')),
('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')),
('is_active', models.BooleanField(default=True)),
('dept_response_hours', models.IntegerField(default=48, help_text='Hours for department to submit a response')),
('dept_response_reminder_hours_before', models.IntegerField(default=12, help_text='Send 1st reminder X hours before dept response deadline')),
('dept_response_second_reminder_enabled', models.BooleanField(default=True, help_text='Enable sending a second reminder for dept response')),
('dept_response_second_reminder_hours_before', models.IntegerField(default=4, help_text='Send 2nd reminder X hours before dept response deadline')),
('dept_response_auto_escalate_enabled', models.BooleanField(default=True, help_text='Auto-escalate to department manager if response overdue')),
('dept_response_escalation_hours_overdue', models.IntegerField(default=0, help_text='Escalate X hours after dept response deadline (0 = immediately)')),
],
options={
'verbose_name': 'Inquiry SLA Config',
'verbose_name_plural': 'Inquiry SLA Configs',
'ordering': ['hospital', 'source'],
},
),
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'), ('department_response', 'Department Response'), ('sent_to_department', 'Sent to Department'), ('transferred_to_department', 'Transferred to Department')], db_index=True, max_length=50)),
('message', models.TextField()),
('old_status', models.CharField(blank=True, max_length=25)),
('new_status', models.CharField(blank=True, max_length=25)),
('metadata', models.JSONField(blank=True, default=dict)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='OnCallAdmin',
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)),
('start_date', models.DateField(blank=True, help_text='Start date for this on-call assignment (optional)', null=True)),
('end_date', models.DateField(blank=True, help_text='End date for this on-call assignment (optional)', null=True)),
('notification_priority', models.PositiveIntegerField(default=1, help_text='Priority for notifications (1 = highest)')),
('is_active', models.BooleanField(default=True, help_text='Whether this on-call assignment is currently active')),
('notify_email', models.BooleanField(default=True, help_text='Send email notifications')),
('notify_sms', models.BooleanField(default=False, help_text='Send SMS notifications')),
('sms_phone', models.CharField(blank=True, help_text='Custom phone number for SMS notifications (optional)', max_length=20)),
],
options={
'verbose_name': 'On-Call Admin',
'verbose_name_plural': 'On-Call Admins',
'ordering': ['notification_priority', '-created_at'],
},
),
migrations.CreateModel(
name='OnCallAdminSchedule',
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)),
('working_days', models.JSONField(default=list, help_text='List of working days (0=Monday, 6=Sunday). Default: [0,1,2,3,4] (Mon-Fri)')),
('work_start_time', models.TimeField(default='08:00', help_text='Start of working hours (e.g., 08:00)')),
('work_end_time', models.TimeField(default='17:00', help_text='End of working hours (e.g., 17:00)')),
('timezone', models.CharField(default='Asia/Riyadh', help_text='Timezone for working hours calculation (e.g., Asia/Riyadh)', max_length=50)),
('is_active', models.BooleanField(default=True, help_text='Whether this on-call schedule is active')),
],
options={
'verbose_name': 'On-Call Admin Schedule',
'verbose_name_plural': 'On-Call Admin Schedules',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='PatientComplaintSession',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('token', models.CharField(max_length=64, unique=True)),
('expires_at', models.DateTimeField(db_index=True)),
('is_active', models.BooleanField(db_index=True, default=True)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Complaint',
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_deleted', models.BooleanField(db_index=True, default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('contact_name', models.CharField(blank=True, max_length=200)),
('contact_phone', models.CharField(blank=True, max_length=20)),
('contact_email', models.EmailField(blank=True, max_length=254)),
('relation_to_patient', models.CharField(blank=True, choices=[('patient', 'Patient'), ('relative', 'Relative'), ('friend', 'Friend'), ('other', 'Other')], help_text="Complainant's relationship to the patient", max_length=20, verbose_name='Relation to Patient')),
('patient_name', models.CharField(blank=True, help_text='Name of the patient involved', max_length=200, verbose_name='Patient Name')),
('national_id', apps.core.encryption.EncryptedCharField(blank=True, help_text='Saudi National ID or Iqama number', max_length=512, verbose_name='National ID/Iqama No.')),
('national_id_hash', models.CharField(blank=True, db_index=True, max_length=64)),
('incident_date', models.DateField(blank=True, help_text='Date when the incident occurred', null=True, verbose_name='Incident Date')),
('staff_name', models.CharField(blank=True, help_text='Name of staff member involved (if known)', max_length=200, verbose_name='Staff Involved')),
('expected_result', models.TextField(blank=True, help_text='What the complainant expects as a resolution', verbose_name='Expected Complaint Result')),
('reference_number', models.CharField(blank=True, help_text='Unique reference number for patient tracking', max_length=50, null=True, unique=True)),
('encounter_id', models.CharField(blank=True, db_index=True, help_text='Related encounter ID if applicable', max_length=100)),
('title', models.CharField(max_length=500)),
('description', models.TextField()),
('ai_brief_en', models.CharField(blank=True, db_index=True, help_text='AI-generated 2-3 word summary in English', max_length=100)),
('ai_brief_ar', models.CharField(blank=True, help_text='AI-generated 2-3 word summary in Arabic', max_length=100)),
('subcategory', models.CharField(blank=True, help_text='Level 3: Subcategory code (legacy)', max_length=100)),
('classification', models.CharField(blank=True, help_text='Level 4: Classification code (legacy)', max_length=100)),
('complaint_type', models.CharField(choices=[('complaint', 'Complaint'), ('appreciation', 'Appreciation')], db_index=True, default='complaint', help_text='Type of feedback (complaint vs appreciation)', max_length=20)),
('complaint_source_type', models.CharField(choices=[('internal', 'Internal'), ('external', 'External')], db_index=True, default='external', help_text='Source type (Internal = staff-generated, External = patient/public-generated)', max_length=20)),
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], db_index=True, default='medium', max_length=20)),
('severity', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], db_index=True, default='medium', max_length=20)),
('status', models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('partially_resolved', 'Partially Resolved'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('cancelled', 'Cancelled'), ('contacted', 'Contacted'), ('contacted_no_response', 'Contacted, No Response'), ('pending_external', 'Pending External'), ('ovr_pending', 'OVR Pending Approval')], db_index=True, default='open', max_length=25)),
('assigned_at', models.DateTimeField(blank=True, null=True)),
('activated_at', models.DateTimeField(blank=True, db_index=True, help_text='Timestamp when complaint was first activated (moved from OPEN to IN_PROGRESS)', null=True)),
('due_at', models.DateTimeField(db_index=True, help_text='SLA deadline')),
('is_overdue', models.BooleanField(db_index=True, default=False)),
('breached_at', models.DateTimeField(blank=True, db_index=True, help_text='Timestamp when complaint first breached SLA', 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)),
('explanation_requested', models.BooleanField(default=False, help_text='Whether an explanation has been requested from staff')),
('explanation_requested_at', models.DateTimeField(blank=True, help_text='When explanation request was first sent to staff', null=True)),
('explanation_received_at', models.DateTimeField(blank=True, help_text='When explanation was received from staff', null=True)),
('explanation_delay_reason', models.TextField(blank=True, help_text='Reason for delay in receiving staff explanation')),
('resolution', models.TextField(blank=True)),
('resolution_sent_at', models.DateTimeField(blank=True, null=True)),
('resolution_category', models.CharField(blank=True, choices=[('full_action_taken', 'Full Action Taken'), ('partial_action_taken', 'Partial Action Taken'), ('no_action_needed', 'No Action Needed'), ('cannot_resolve', 'Cannot Resolve'), ('patient_withdrawn', 'Patient Withdrawn')], db_index=True, help_text='Category of resolution', max_length=50)),
('resolution_outcome', models.CharField(blank=True, choices=[('patient', 'Patient'), ('hospital', 'Hospital'), ('other', 'Other — please specify')], db_index=True, help_text='Who was in wrong/right (Patient / Hospital / Other)', max_length=20)),
('resolution_outcome_other', models.TextField(blank=True, help_text='Specify if Other was selected for resolution outcome')),
('resolved_at', models.DateTimeField(blank=True, null=True)),
('closed_at', models.DateTimeField(blank=True, null=True)),
('reopened_at', models.DateTimeField(blank=True, null=True)),
('resolution_survey_sent_at', models.DateTimeField(blank=True, null=True)),
('satisfaction', models.CharField(blank=True, choices=[('satisfied', 'Satisfied'), ('neutral', 'Neutral'), ('dissatisfied', 'Dissatisfied'), ('no_response', 'No Response')], db_index=True, help_text='Direct satisfaction feedback from patient follow-up call', max_length=20)),
('satisfaction_set_at', models.DateTimeField(blank=True, help_text='When satisfaction was last set', null=True)),
('moh_reference', models.CharField(blank=True, help_text='Ministry of Health reference number', max_length=100)),
('moh_reference_date', models.DateField(blank=True, help_text='MOH reference date', null=True)),
('chi_reference', models.CharField(blank=True, help_text='Council of Health Insurance reference number', max_length=100)),
('chi_reference_date', models.DateField(blank=True, help_text='CHI reference date', null=True)),
('file_number', models.CharField(blank=True, db_index=True, help_text='Patient file/MRN number', max_length=100)),
('form_sent_at', models.DateTimeField(blank=True, help_text='When complaint form was sent to the complained department', null=True)),
('forwarded_to_dept_at', models.DateTimeField(blank=True, help_text='When complaint was forwarded to the involved department', null=True)),
('response_date', models.DateField(blank=True, help_text='Date when response was received', null=True)),
('complaint_subject', models.CharField(blank=True, help_text='Main complaint subject (from Excel classification)', max_length=500)),
('action_taken_by_dept', models.TextField(blank=True, help_text='Action taken by the responsible department')),
('action_result', models.TextField(blank=True, help_text='Result of the action/investigation taken')),
('recommendation_action_plan', models.TextField(blank=True, help_text='Solutions, suggestions, and action plan')),
('delay_reason_closure', models.CharField(blank=True, choices=[('department_no_response', 'Department No Response'), ('escalated', 'Escalated'), ('patient_not_satisfied', 'Patient Not Satisfied with Complaint Resolution')], help_text='Reason for not closing the complaint within 72 hours', max_length=50)),
('was_pending_external', models.BooleanField(default=False, help_text='Whether this complaint was ever in pending_external status')),
('pending_external_set_at', models.DateTimeField(blank=True, help_text='When complaint was set to pending_external status', null=True)),
('is_escalated_ovr', models.BooleanField(db_index=True, default=False, help_text='Whether complaint is escalated as OVR')),
('escalated_ovr_at', models.DateTimeField(blank=True, help_text='When OVR escalation was set', null=True)),
('metadata', models.JSONField(blank=True, default=dict)),
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_complaints', to=settings.AUTH_USER_MODEL)),
('closed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='closed_complaints', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(blank=True, help_text='User who created this complaint (SourceUser or Patient)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_complaints', to=settings.AUTH_USER_MODEL)),
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_%(class)s_set', to=settings.AUTH_USER_MODEL)),
('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='complaints', to='organizations.department')),
('escalated_ovr_by', models.ForeignKey(blank=True, help_text='User who escalated as OVR', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='escalated_ovr_complaints', to=settings.AUTH_USER_MODEL)),
('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complaints', to='organizations.hospital')),
('location', models.ForeignKey(blank=True, help_text='Location (e.g., Riyadh, Jeddah)', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.location')),
('main_section', models.ForeignKey(blank=True, help_text='Section/Department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.mainsection')),
('patient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='complaints', to='organizations.patient')),
('reopened_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reopened_complaints', to=settings.AUTH_USER_MODEL)),
('reopened_from', models.ForeignKey(blank=True, help_text='Original complaint this was reopened from', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reopenings', to='complaints.complaint')),
],
options={
'ordering': ['-created_at'],
},
),
]