321 lines
13 KiB
Python
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'])
|