Files
GNX-WEB/gnx-react/backend/support/models.py
Iliyan Angelov d48c54e2c5 update
2025-10-07 22:10:27 +03:00

321 lines
13 KiB
Python

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'])