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