The Complaint Management module handles grievance registration, tracking, resolution, and feedback for various complaint categories across the institute - including academic, hostel, mess, infrastructure, and administrative complaints.
from django.db import models
from django.contrib.auth.models import User
from applications.globals.models import ExtraInfo, Faculty, Staff, DepartmentInfo, Designation, HoldsDesignation
from applications.academic_information.models import Student
# ==================== COMPLAINT CONFIGURATION ====================
class ComplaintCategory(models.Model):
"""Categories of complaints"""
name = models.CharField(max_length=100)
code = models.CharField(max_length=20, unique=True)
description = models.TextField(blank=True)
# Routing
default_handler_designation = models.ForeignKey(Designation, on_delete=models.SET_NULL, null=True, blank=True)
default_department = models.ForeignKey(DepartmentInfo, on_delete=models.SET_NULL, null=True, blank=True)
# SLA
resolution_days = models.IntegerField(default=7) # Expected resolution time
escalation_days = models.IntegerField(default=3) # Escalate after these days
is_active = models.BooleanField(default=True)
class Meta:
verbose_name_plural = "Complaint Categories"
class ComplaintSubCategory(models.Model):
"""Sub-categories of complaints"""
category = models.ForeignKey(ComplaintCategory, on_delete=models.CASCADE, related_name='subcategories')
name = models.CharField(max_length=100)
code = models.CharField(max_length=30)
description = models.TextField(blank=True)
# Specific routing
handler_designation = models.ForeignKey(Designation, on_delete=models.SET_NULL, null=True, blank=True)
is_active = models.BooleanField(default=True)
class Meta:
unique_together = ['category', 'code']
# ==================== COMPLAINT MANAGEMENT ====================
class Complaint(models.Model):
"""Main complaint records"""
STATUS = [
('SUBMITTED', 'Submitted'),
('ACKNOWLEDGED', 'Acknowledged'),
('IN_PROGRESS', 'In Progress'),
('ON_HOLD', 'On Hold'),
('RESOLVED', 'Resolved'),
('CLOSED', 'Closed'),
('REOPENED', 'Reopened'),
('REJECTED', 'Rejected'),
]
PRIORITY = [
('LOW', 'Low'),
('MEDIUM', 'Medium'),
('HIGH', 'High'),
('CRITICAL', 'Critical'),
]
# Identification
complaint_number = models.CharField(max_length=50, unique=True)
# Complainant (from globals)
complainant = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='complaints')
complainant_department = models.ForeignKey(DepartmentInfo, on_delete=models.SET_NULL, null=True, blank=True)
# Category
category = models.ForeignKey(ComplaintCategory, on_delete=models.PROTECT)
subcategory = models.ForeignKey(ComplaintSubCategory, on_delete=models.SET_NULL, null=True, blank=True)
# Details
subject = models.CharField(max_length=300)
description = models.TextField()
# Location (if applicable)
location = models.CharField(max_length=200, blank=True)
# Priority
priority = models.CharField(max_length=20, choices=PRIORITY, default='MEDIUM')
# Status
status = models.CharField(max_length=20, choices=STATUS, default='SUBMITTED')
# Assignment
assigned_to = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_complaints')
assigned_department = models.ForeignKey(DepartmentInfo, on_delete=models.SET_NULL, null=True, blank=True, related_name='department_complaints')
# Timeline
submitted_at = models.DateTimeField(auto_now_add=True)
acknowledged_at = models.DateTimeField(null=True, blank=True)
resolved_at = models.DateTimeField(null=True, blank=True)
closed_at = models.DateTimeField(null=True, blank=True)
# SLA
expected_resolution = models.DateTimeField(null=True, blank=True)
is_sla_breached = models.BooleanField(default=False)
# Escalation
escalation_level = models.IntegerField(default=0)
# Anonymous option
is_anonymous = models.BooleanField(default=False)
class Meta:
ordering = ['-submitted_at']
class ComplaintAttachment(models.Model):
"""Attachments for complaints"""
complaint = models.ForeignKey(Complaint, on_delete=models.CASCADE, related_name='attachments')
file = models.FileField(upload_to='complaints/attachments/')
file_name = models.CharField(max_length=200)
uploaded_at = models.DateTimeField(auto_now_add=True)
uploaded_by = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True)
# ==================== WORKFLOW ====================
class ComplaintStatusHistory(models.Model):
"""Track status changes"""
complaint = models.ForeignKey(Complaint, on_delete=models.CASCADE, related_name='status_history')
from_status = models.CharField(max_length=20)
to_status = models.CharField(max_length=20)
changed_by = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True)
changed_at = models.DateTimeField(auto_now_add=True)
remarks = models.TextField(blank=True)
class ComplaintComment(models.Model):
"""Comments/Notes on complaints"""
COMMENT_TYPE = [
('PUBLIC', 'Public'), # Visible to complainant
('INTERNAL', 'Internal'), # Staff only
]
complaint = models.ForeignKey(Complaint, on_delete=models.CASCADE, related_name='comments')
comment_type = models.CharField(max_length=20, choices=COMMENT_TYPE, default='PUBLIC')
content = models.TextField()
commented_by = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True)
commented_at = models.DateTimeField(auto_now_add=True)
# Attachment
attachment = models.FileField(upload_to='complaints/comments/', blank=True)
class ComplaintAssignmentHistory(models.Model):
"""Track assignment changes"""
complaint = models.ForeignKey(Complaint, on_delete=models.CASCADE, related_name='assignment_history')
from_assignee = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True, blank=True, related_name='reassigned_from')
to_assignee = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True, blank=True, related_name='reassigned_to')
assigned_by = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True, related_name='assignments_made')
assigned_at = models.DateTimeField(auto_now_add=True)
reason = models.TextField(blank=True)
# ==================== RESOLUTION ====================
class ComplaintResolution(models.Model):
"""Complaint resolution details"""
complaint = models.OneToOneField(Complaint, on_delete=models.CASCADE, related_name='resolution')
resolution_summary = models.TextField()
action_taken = models.TextField()
resolved_by = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True)
resolved_at = models.DateTimeField(auto_now_add=True)
# Root cause
root_cause = models.TextField(blank=True)
preventive_measures = models.TextField(blank=True)
# Supporting documents
resolution_document = models.FileField(upload_to='complaints/resolutions/', blank=True)
# ==================== FEEDBACK ====================
class ComplaintFeedback(models.Model):
"""Complainant feedback after resolution"""
complaint = models.OneToOneField(Complaint, on_delete=models.CASCADE, related_name='feedback')
# Ratings (1-5)
response_time_rating = models.IntegerField()
resolution_quality_rating = models.IntegerField()
staff_behavior_rating = models.IntegerField()
overall_rating = models.IntegerField()
is_satisfied = models.BooleanField()
comments = models.TextField(blank=True)
submitted_at = models.DateTimeField(auto_now_add=True)
# ==================== ESCALATION ====================
class EscalationMatrix(models.Model):
"""Escalation rules"""
category = models.ForeignKey(ComplaintCategory, on_delete=models.CASCADE, related_name='escalation_matrix')
level = models.IntegerField() # 1, 2, 3, etc.
escalate_after_days = models.IntegerField()
escalate_to = models.ForeignKey(Designation, on_delete=models.CASCADE)
notification_required = models.BooleanField(default=True)
class Meta:
unique_together = ['category', 'level']
ordering = ['category', 'level']
class EscalationRecord(models.Model):
"""Track escalations"""
complaint = models.ForeignKey(Complaint, on_delete=models.CASCADE, related_name='escalations')
escalation_level = models.IntegerField()
escalated_to = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True)
escalated_at = models.DateTimeField(auto_now_add=True)
reason = models.CharField(max_length=200) # SLA breach, Priority increase, etc.
action_taken = models.TextField(blank=True)
action_taken_at = models.DateTimeField(null=True, blank=True)
# ==================== REPORTS ====================
class ComplaintReport(models.Model):
"""Periodic complaint reports"""
REPORT_TYPE = [
('DAILY', 'Daily'),
('WEEKLY', 'Weekly'),
('MONTHLY', 'Monthly'),
('QUARTERLY', 'Quarterly'),
]
report_type = models.CharField(max_length=20, choices=REPORT_TYPE)
period_start = models.DateField()
period_end = models.DateField()
# Statistics
total_received = models.IntegerField(default=0)
total_resolved = models.IntegerField(default=0)
total_pending = models.IntegerField(default=0)
total_sla_breached = models.IntegerField(default=0)
average_resolution_time = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) # hours
# Category-wise breakdown
category_breakdown = models.TextField(blank=True) # JSON
generated_at = models.DateTimeField(auto_now_add=True)
generated_by = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True)
# ==================== QUICK COMPLAINT TYPES ====================
class MaintenanceComplaint(models.Model):
"""Infrastructure/Maintenance complaints"""
AREA_TYPE = [
('ACADEMIC', 'Academic Building'),
('HOSTEL', 'Hostel'),
('MESS', 'Mess'),
('LIBRARY', 'Library'),
('GROUND', 'Sports Ground'),
('ROAD', 'Roads'),
('COMMON', 'Common Area'),
('OTHER', 'Other'),
]
ISSUE_TYPE = [
('ELECTRICAL', 'Electrical'),
('PLUMBING', 'Plumbing'),
('FURNITURE', 'Furniture'),
('CIVIL', 'Civil Work'),
('HOUSEKEEPING', 'Housekeeping'),
('OTHER', 'Other'),
]
complaint = models.OneToOneField(Complaint, on_delete=models.CASCADE, related_name='maintenance_details')
area_type = models.CharField(max_length=20, choices=AREA_TYPE)
issue_type = models.CharField(max_length=20, choices=ISSUE_TYPE)
building_name = models.CharField(max_length=100, blank=True)
room_number = models.CharField(max_length=50, blank=True)
floor = models.CharField(max_length=20, blank=True)
class AcademicComplaint(models.Model):
"""Academic-related complaints"""
ISSUE_TYPE = [
('RESULT', 'Result/Grades'),
('ATTENDANCE', 'Attendance'),
('SYLLABUS', 'Syllabus'),
('TEACHING', 'Teaching Quality'),
('EXAM', 'Examination'),
('REGISTRATION', 'Registration'),
('OTHER', 'Other'),
]
complaint = models.OneToOneField(Complaint, on_delete=models.CASCADE, related_name='academic_details')
issue_type = models.CharField(max_length=20, choices=ISSUE_TYPE)
course_code = models.CharField(max_length=20, blank=True)
course_name = models.CharField(max_length=200, blank=True)
semester = models.CharField(max_length=20, blank=True)