update
This commit is contained in:
320
gnx-react/backend/support/models.py
Normal file
320
gnx-react/backend/support/models.py
Normal file
@@ -0,0 +1,320 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
from django.core.validators import EmailValidator
|
||||
import random
|
||||
import string
|
||||
|
||||
class TicketStatus(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
color = models.CharField(max_length=7, default='#667eea', help_text='Hex color code')
|
||||
description = models.TextField(blank=True)
|
||||
is_closed = models.BooleanField(default=False, help_text='Whether this status represents a closed ticket')
|
||||
is_active = models.BooleanField(default=True)
|
||||
display_order = models.PositiveIntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['display_order', 'name']
|
||||
verbose_name_plural = 'Ticket Statuses'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class TicketPriority(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
level = models.PositiveIntegerField(unique=True, help_text='Lower number = higher priority')
|
||||
color = models.CharField(max_length=7, default='#667eea', help_text='Hex color code')
|
||||
description = models.TextField(blank=True)
|
||||
sla_hours = models.PositiveIntegerField(default=24, help_text='SLA response time in hours')
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['level']
|
||||
verbose_name_plural = 'Ticket Priorities'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class TicketCategory(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
color = models.CharField(max_length=7, default='#667eea', help_text='Hex color code')
|
||||
icon = models.CharField(max_length=50, default='fa-question-circle', help_text='FontAwesome icon class')
|
||||
is_active = models.BooleanField(default=True)
|
||||
display_order = models.PositiveIntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['display_order', 'name']
|
||||
verbose_name_plural = 'Ticket Categories'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class SupportTicket(models.Model):
|
||||
TICKET_TYPES = [
|
||||
('technical', 'Technical Issue'),
|
||||
('billing', 'Billing Question'),
|
||||
('feature_request', 'Feature Request'),
|
||||
('bug_report', 'Bug Report'),
|
||||
('general', 'General Inquiry'),
|
||||
('account', 'Account Issue'),
|
||||
]
|
||||
|
||||
ticket_number = models.CharField(max_length=20, unique=True, editable=False)
|
||||
title = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
ticket_type = models.CharField(max_length=20, choices=TICKET_TYPES, default='general')
|
||||
|
||||
# User information
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='support_tickets', null=True, blank=True)
|
||||
user_name = models.CharField(max_length=100)
|
||||
user_email = models.EmailField()
|
||||
user_phone = models.CharField(max_length=20, blank=True)
|
||||
company = models.CharField(max_length=100, blank=True)
|
||||
|
||||
# Ticket management
|
||||
category = models.ForeignKey(TicketCategory, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
priority = models.ForeignKey(TicketPriority, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
status = models.ForeignKey(TicketStatus, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_tickets')
|
||||
assigned_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
closed_at = models.DateTimeField(null=True, blank=True)
|
||||
last_activity = models.DateTimeField(auto_now=True)
|
||||
first_response_at = models.DateTimeField(null=True, blank=True)
|
||||
sla_deadline = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Additional fields
|
||||
tags = models.CharField(max_length=500, blank=True, help_text='Comma-separated tags')
|
||||
internal_notes = models.TextField(blank=True, help_text='Internal notes visible only to staff')
|
||||
is_escalated = models.BooleanField(default=False)
|
||||
escalation_reason = models.TextField(blank=True)
|
||||
attachments = models.JSONField(default=list, blank=True, help_text='List of file paths')
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['ticket_number']),
|
||||
models.Index(fields=['user_email']),
|
||||
models.Index(fields=['status']),
|
||||
models.Index(fields=['priority']),
|
||||
models.Index(fields=['assigned_to']),
|
||||
models.Index(fields=['created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.ticket_number} - {self.title}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.ticket_number:
|
||||
self.ticket_number = self.generate_ticket_number()
|
||||
|
||||
# Set SLA deadline based on priority
|
||||
if not self.sla_deadline and self.priority:
|
||||
self.sla_deadline = timezone.now() + timezone.timedelta(hours=self.priority.sla_hours)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def generate_ticket_number():
|
||||
"""Generate a unique ticket number in format: TKT-YYYYMMDD-XXXXX"""
|
||||
today = timezone.now().strftime('%Y%m%d')
|
||||
random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=5))
|
||||
ticket_number = f'TKT-{today}-{random_str}'
|
||||
|
||||
# Ensure uniqueness
|
||||
while SupportTicket.objects.filter(ticket_number=ticket_number).exists():
|
||||
random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=5))
|
||||
ticket_number = f'TKT-{today}-{random_str}'
|
||||
|
||||
return ticket_number
|
||||
|
||||
|
||||
class TicketMessage(models.Model):
|
||||
MESSAGE_TYPES = [
|
||||
('user_message', 'User Message'),
|
||||
('agent_response', 'Agent Response'),
|
||||
('system_note', 'System Note'),
|
||||
('status_change', 'Status Change'),
|
||||
('assignment_change', 'Assignment Change'),
|
||||
('escalation', 'Escalation'),
|
||||
]
|
||||
|
||||
ticket = models.ForeignKey(SupportTicket, on_delete=models.CASCADE, related_name='messages')
|
||||
message_type = models.CharField(max_length=20, choices=MESSAGE_TYPES, default='user_message')
|
||||
content = models.TextField()
|
||||
|
||||
# Author information
|
||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='ticket_messages')
|
||||
author_name = models.CharField(max_length=100, blank=True)
|
||||
author_email = models.EmailField(blank=True)
|
||||
|
||||
# Message metadata
|
||||
is_internal = models.BooleanField(default=False, help_text='Internal message not visible to user')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
attachments = models.JSONField(default=list, blank=True, help_text='List of file paths')
|
||||
|
||||
# Read status
|
||||
is_read = models.BooleanField(default=False)
|
||||
read_at = models.DateTimeField(null=True, blank=True)
|
||||
read_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='read_messages')
|
||||
|
||||
class Meta:
|
||||
ordering = ['created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['ticket', 'created_at']),
|
||||
models.Index(fields=['author']),
|
||||
models.Index(fields=['message_type']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Message on {self.ticket.ticket_number} at {self.created_at}"
|
||||
|
||||
|
||||
class TicketActivity(models.Model):
|
||||
ACTIVITY_TYPES = [
|
||||
('created', 'Ticket Created'),
|
||||
('updated', 'Ticket Updated'),
|
||||
('status_changed', 'Status Changed'),
|
||||
('assigned', 'Ticket Assigned'),
|
||||
('message_added', 'Message Added'),
|
||||
('escalated', 'Ticket Escalated'),
|
||||
('closed', 'Ticket Closed'),
|
||||
('reopened', 'Ticket Reopened'),
|
||||
]
|
||||
|
||||
ticket = models.ForeignKey(SupportTicket, on_delete=models.CASCADE, related_name='activities')
|
||||
activity_type = models.CharField(max_length=20, choices=ACTIVITY_TYPES)
|
||||
description = models.TextField()
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='ticket_activities')
|
||||
user_name = models.CharField(max_length=100, blank=True)
|
||||
old_value = models.TextField(blank=True)
|
||||
new_value = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['ticket', 'created_at']),
|
||||
models.Index(fields=['activity_type']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.activity_type} - {self.ticket.ticket_number}"
|
||||
|
||||
|
||||
class KnowledgeBaseCategory(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
slug = models.SlugField(max_length=120, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
icon = models.CharField(max_length=50, default='fa-book', help_text='FontAwesome icon class')
|
||||
color = models.CharField(max_length=7, default='#667eea', help_text='Hex color code')
|
||||
is_active = models.BooleanField(default=True)
|
||||
display_order = models.PositiveIntegerField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['display_order', 'name']
|
||||
verbose_name_plural = 'Knowledge Base Categories'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class KnowledgeBaseArticle(models.Model):
|
||||
title = models.CharField(max_length=200)
|
||||
slug = models.SlugField(max_length=220, unique=True)
|
||||
category = models.ForeignKey(KnowledgeBaseCategory, on_delete=models.SET_NULL, null=True, related_name='articles')
|
||||
content = models.TextField()
|
||||
summary = models.TextField(blank=True, help_text='Short summary of the article')
|
||||
|
||||
# SEO and metadata
|
||||
meta_description = models.CharField(max_length=160, blank=True)
|
||||
keywords = models.CharField(max_length=500, blank=True, help_text='Comma-separated keywords')
|
||||
|
||||
# Article management
|
||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='kb_articles')
|
||||
is_published = models.BooleanField(default=False)
|
||||
is_featured = models.BooleanField(default=False)
|
||||
view_count = models.PositiveIntegerField(default=0)
|
||||
helpful_count = models.PositiveIntegerField(default=0)
|
||||
not_helpful_count = models.PositiveIntegerField(default=0)
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
published_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['slug']),
|
||||
models.Index(fields=['category']),
|
||||
models.Index(fields=['is_published']),
|
||||
models.Index(fields=['created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class SupportSettings(models.Model):
|
||||
setting_name = models.CharField(max_length=100, unique=True)
|
||||
setting_value = models.TextField()
|
||||
description = models.TextField(blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = 'Support Settings'
|
||||
|
||||
def __str__(self):
|
||||
return self.setting_name
|
||||
|
||||
|
||||
class RegisteredEmail(models.Model):
|
||||
"""
|
||||
Email addresses that are authorized to submit support tickets
|
||||
Only admins can add/remove emails from this list
|
||||
"""
|
||||
email = models.EmailField(unique=True, validators=[EmailValidator()])
|
||||
company_name = models.CharField(max_length=200, blank=True, help_text='Company or organization name')
|
||||
contact_name = models.CharField(max_length=200, blank=True, help_text='Primary contact name')
|
||||
notes = models.TextField(blank=True, help_text='Internal notes about this registration')
|
||||
is_active = models.BooleanField(default=True, help_text='Whether this email can submit tickets')
|
||||
added_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='registered_emails')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
last_ticket_date = models.DateTimeField(null=True, blank=True, help_text='Last time this email submitted a ticket')
|
||||
ticket_count = models.PositiveIntegerField(default=0, help_text='Total number of tickets submitted')
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
verbose_name = 'Registered Email'
|
||||
verbose_name_plural = 'Registered Emails'
|
||||
indexes = [
|
||||
models.Index(fields=['email']),
|
||||
models.Index(fields=['is_active']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.email} ({self.company_name or 'No company'})"
|
||||
|
||||
def increment_ticket_count(self):
|
||||
"""Increment ticket count and update last ticket date"""
|
||||
self.ticket_count += 1
|
||||
self.last_ticket_date = timezone.now()
|
||||
self.save(update_fields=['ticket_count', 'last_ticket_date'])
|
||||
Reference in New Issue
Block a user