update
This commit is contained in:
193
backEnd/support/README.md
Normal file
193
backEnd/support/README.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Support Center Module
|
||||
|
||||
Enterprise Support Center for handling customer support tickets, knowledge base articles, and support settings.
|
||||
|
||||
## Features
|
||||
|
||||
- **Ticket Management**
|
||||
- Create and track support tickets
|
||||
- Multiple ticket types (technical, billing, feature requests, etc.)
|
||||
- Priority and status management
|
||||
- Ticket categories and tags
|
||||
- SLA deadline tracking
|
||||
- Message and activity history
|
||||
|
||||
- **Knowledge Base**
|
||||
- Categorized articles
|
||||
- Search functionality
|
||||
- Featured articles
|
||||
- Article feedback (helpful/not helpful)
|
||||
- View count tracking
|
||||
- Rich content support
|
||||
|
||||
- **Public API**
|
||||
- Create tickets without authentication
|
||||
- Check ticket status by ticket number
|
||||
- Browse knowledge base articles
|
||||
- Search articles
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Run Migrations
|
||||
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### 2. Populate Initial Data
|
||||
|
||||
```bash
|
||||
python manage.py populate_support_data
|
||||
```
|
||||
|
||||
This will create:
|
||||
- 5 ticket statuses (Open, In Progress, Pending Response, Resolved, Closed)
|
||||
- 4 ticket priorities (Low, Medium, High, Critical)
|
||||
- 6 ticket categories
|
||||
- 6 knowledge base categories
|
||||
- 6 sample knowledge base articles
|
||||
|
||||
### 3. Admin Access
|
||||
|
||||
Access the Django admin panel to manage:
|
||||
- Support tickets
|
||||
- Ticket categories, statuses, and priorities
|
||||
- Knowledge base categories and articles
|
||||
- Support settings
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Tickets
|
||||
|
||||
- `GET /api/support/tickets/` - List all tickets
|
||||
- `POST /api/support/tickets/` - Create a new ticket
|
||||
- `GET /api/support/tickets/{id}/` - Get ticket details
|
||||
- `POST /api/support/tickets/check-status/` - Check ticket status by ticket number
|
||||
- `POST /api/support/tickets/{id}/add-message/` - Add a message to a ticket
|
||||
|
||||
### Categories
|
||||
|
||||
- `GET /api/support/categories/` - List all ticket categories
|
||||
- `GET /api/support/statuses/` - List all ticket statuses
|
||||
- `GET /api/support/priorities/` - List all ticket priorities
|
||||
|
||||
### Knowledge Base
|
||||
|
||||
- `GET /api/support/knowledge-base/` - List all published articles
|
||||
- `GET /api/support/knowledge-base/{slug}/` - Get article details
|
||||
- `GET /api/support/knowledge-base/featured/` - Get featured articles
|
||||
- `GET /api/support/knowledge-base/by-category/{category_slug}/` - Get articles by category
|
||||
- `POST /api/support/knowledge-base/{slug}/mark-helpful/` - Mark article as helpful/not helpful
|
||||
- `GET /api/support/knowledge-base-categories/` - List all KB categories
|
||||
|
||||
### Settings
|
||||
|
||||
- `GET /api/support/settings/` - List all active support settings
|
||||
- `GET /api/support/settings/{setting_name}/` - Get specific setting
|
||||
|
||||
## Models
|
||||
|
||||
### SupportTicket
|
||||
Main model for support tickets with full tracking capabilities.
|
||||
|
||||
### TicketStatus
|
||||
Ticket status options (Open, In Progress, Resolved, etc.)
|
||||
|
||||
### TicketPriority
|
||||
Priority levels with SLA hours (Low, Medium, High, Critical)
|
||||
|
||||
### TicketCategory
|
||||
Categorize tickets for better organization
|
||||
|
||||
### TicketMessage
|
||||
Messages and updates on tickets
|
||||
|
||||
### TicketActivity
|
||||
Audit trail of all ticket changes
|
||||
|
||||
### KnowledgeBaseCategory
|
||||
Categories for knowledge base articles
|
||||
|
||||
### KnowledgeBaseArticle
|
||||
Knowledge base articles with rich content
|
||||
|
||||
### SupportSettings
|
||||
Configurable support center settings
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Create a Ticket
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
data = {
|
||||
"title": "Cannot login to my account",
|
||||
"description": "I've been trying to login but getting error 500",
|
||||
"ticket_type": "technical",
|
||||
"user_name": "John Doe",
|
||||
"user_email": "john@example.com",
|
||||
"user_phone": "+1234567890",
|
||||
"company": "Acme Corp",
|
||||
"category": 1 # Technical Support category ID
|
||||
}
|
||||
|
||||
response = requests.post('http://localhost:8000/api/support/tickets/', json=data)
|
||||
ticket = response.json()
|
||||
print(f"Ticket created: {ticket['ticket_number']}")
|
||||
```
|
||||
|
||||
### Check Ticket Status
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
data = {
|
||||
"ticket_number": "TKT-20231015-ABCDE"
|
||||
}
|
||||
|
||||
response = requests.post('http://localhost:8000/api/support/tickets/check-status/', json=data)
|
||||
ticket = response.json()
|
||||
print(f"Status: {ticket['status_name']}")
|
||||
```
|
||||
|
||||
### Search Knowledge Base
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.get('http://localhost:8000/api/support/knowledge-base/', params={'search': 'login'})
|
||||
articles = response.json()
|
||||
for article in articles:
|
||||
print(f"- {article['title']}")
|
||||
```
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
The support center is integrated with the Next.js frontend at `/support-center` with:
|
||||
- Ticket submission form
|
||||
- Knowledge base browser with search
|
||||
- Ticket status checker
|
||||
- Modern, responsive UI
|
||||
|
||||
## Email Notifications
|
||||
|
||||
To enable email notifications for tickets, configure email settings in `settings.py` and implement email templates in `support/templates/support/`.
|
||||
|
||||
## Security
|
||||
|
||||
- All endpoints are public (AllowAny permission)
|
||||
- Ticket numbers are randomly generated and hard to guess
|
||||
- Internal notes and messages are hidden from public API
|
||||
- Rate limiting recommended for production
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Live chat integration
|
||||
- [ ] File attachments for tickets
|
||||
- [ ] Email notifications
|
||||
- [ ] Ticket assignment and routing
|
||||
- [ ] SLA breach alerts
|
||||
- [ ] Advanced analytics dashboard
|
||||
- [ ] Webhook notifications
|
||||
|
||||
0
backEnd/support/__init__.py
Normal file
0
backEnd/support/__init__.py
Normal file
BIN
backEnd/support/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/support/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/support/__pycache__/admin.cpython-312.pyc
Normal file
BIN
backEnd/support/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/support/__pycache__/apps.cpython-312.pyc
Normal file
BIN
backEnd/support/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/support/__pycache__/email_service.cpython-312.pyc
Normal file
BIN
backEnd/support/__pycache__/email_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/support/__pycache__/models.cpython-312.pyc
Normal file
BIN
backEnd/support/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/support/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
backEnd/support/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/support/__pycache__/signals.cpython-312.pyc
Normal file
BIN
backEnd/support/__pycache__/signals.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/support/__pycache__/urls.cpython-312.pyc
Normal file
BIN
backEnd/support/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backEnd/support/__pycache__/views.cpython-312.pyc
Normal file
BIN
backEnd/support/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
182
backEnd/support/admin.py
Normal file
182
backEnd/support/admin.py
Normal file
@@ -0,0 +1,182 @@
|
||||
from django.contrib import admin
|
||||
from .models import (
|
||||
SupportTicket, TicketStatus, TicketPriority, TicketCategory,
|
||||
TicketMessage, TicketActivity, KnowledgeBaseCategory,
|
||||
KnowledgeBaseArticle, SupportSettings, RegisteredEmail
|
||||
)
|
||||
|
||||
|
||||
@admin.register(TicketStatus)
|
||||
class TicketStatusAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'color', 'is_closed', 'is_active', 'display_order']
|
||||
list_filter = ['is_closed', 'is_active']
|
||||
search_fields = ['name']
|
||||
ordering = ['display_order', 'name']
|
||||
|
||||
|
||||
@admin.register(TicketPriority)
|
||||
class TicketPriorityAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'level', 'color', 'sla_hours', 'is_active']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['name']
|
||||
ordering = ['level']
|
||||
|
||||
|
||||
@admin.register(TicketCategory)
|
||||
class TicketCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'icon', 'color', 'is_active', 'display_order']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['name', 'description']
|
||||
ordering = ['display_order', 'name']
|
||||
|
||||
|
||||
class TicketMessageInline(admin.TabularInline):
|
||||
model = TicketMessage
|
||||
extra = 0
|
||||
fields = ['message_type', 'content', 'author_name', 'created_at', 'is_internal']
|
||||
readonly_fields = ['created_at']
|
||||
|
||||
|
||||
class TicketActivityInline(admin.TabularInline):
|
||||
model = TicketActivity
|
||||
extra = 0
|
||||
fields = ['activity_type', 'description', 'user_name', 'created_at']
|
||||
readonly_fields = ['created_at']
|
||||
|
||||
|
||||
@admin.register(SupportTicket)
|
||||
class SupportTicketAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'ticket_number', 'title', 'user_name', 'user_email',
|
||||
'status', 'priority', 'category', 'created_at', 'is_escalated'
|
||||
]
|
||||
list_filter = ['status', 'priority', 'category', 'ticket_type', 'is_escalated', 'created_at']
|
||||
search_fields = ['ticket_number', 'title', 'user_name', 'user_email', 'description']
|
||||
readonly_fields = ['ticket_number', 'created_at', 'updated_at', 'last_activity']
|
||||
inlines = [TicketMessageInline, TicketActivityInline]
|
||||
|
||||
fieldsets = (
|
||||
('Ticket Information', {
|
||||
'fields': ('ticket_number', 'title', 'description', 'ticket_type')
|
||||
}),
|
||||
('User Information', {
|
||||
'fields': ('user_name', 'user_email', 'user_phone', 'company')
|
||||
}),
|
||||
('Ticket Management', {
|
||||
'fields': ('category', 'priority', 'status', 'assigned_to', 'assigned_at')
|
||||
}),
|
||||
('Escalation', {
|
||||
'fields': ('is_escalated', 'escalation_reason'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at', 'closed_at', 'last_activity', 'first_response_at', 'sla_deadline')
|
||||
}),
|
||||
('Additional Information', {
|
||||
'fields': ('tags', 'internal_notes', 'attachments'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
ordering = ['-created_at']
|
||||
|
||||
|
||||
@admin.register(TicketMessage)
|
||||
class TicketMessageAdmin(admin.ModelAdmin):
|
||||
list_display = ['ticket', 'message_type', 'author_name', 'created_at', 'is_internal', 'is_read']
|
||||
list_filter = ['message_type', 'is_internal', 'is_read', 'created_at']
|
||||
search_fields = ['ticket__ticket_number', 'content', 'author_name', 'author_email']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
ordering = ['-created_at']
|
||||
|
||||
|
||||
@admin.register(TicketActivity)
|
||||
class TicketActivityAdmin(admin.ModelAdmin):
|
||||
list_display = ['ticket', 'activity_type', 'user_name', 'created_at']
|
||||
list_filter = ['activity_type', 'created_at']
|
||||
search_fields = ['ticket__ticket_number', 'description', 'user_name']
|
||||
readonly_fields = ['created_at']
|
||||
ordering = ['-created_at']
|
||||
|
||||
|
||||
@admin.register(KnowledgeBaseCategory)
|
||||
class KnowledgeBaseCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'icon', 'color', 'is_active', 'display_order']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['name', 'description']
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
ordering = ['display_order', 'name']
|
||||
|
||||
|
||||
@admin.register(KnowledgeBaseArticle)
|
||||
class KnowledgeBaseArticleAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'title', 'category', 'is_published', 'is_featured',
|
||||
'view_count', 'helpful_count', 'created_at'
|
||||
]
|
||||
list_filter = ['is_published', 'is_featured', 'category', 'created_at']
|
||||
search_fields = ['title', 'content', 'summary', 'keywords']
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
readonly_fields = ['view_count', 'helpful_count', 'not_helpful_count', 'created_at', 'updated_at']
|
||||
|
||||
fieldsets = (
|
||||
('Article Information', {
|
||||
'fields': ('title', 'slug', 'category', 'content', 'summary')
|
||||
}),
|
||||
('SEO & Metadata', {
|
||||
'fields': ('meta_description', 'keywords')
|
||||
}),
|
||||
('Publishing', {
|
||||
'fields': ('author', 'is_published', 'is_featured', 'published_at')
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('view_count', 'helpful_count', 'not_helpful_count'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
ordering = ['-created_at']
|
||||
|
||||
|
||||
@admin.register(SupportSettings)
|
||||
class SupportSettingsAdmin(admin.ModelAdmin):
|
||||
list_display = ['setting_name', 'is_active', 'created_at', 'updated_at']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['setting_name', 'setting_value', 'description']
|
||||
ordering = ['setting_name']
|
||||
|
||||
|
||||
@admin.register(RegisteredEmail)
|
||||
class RegisteredEmailAdmin(admin.ModelAdmin):
|
||||
list_display = ['email', 'company_name', 'contact_name', 'is_active', 'ticket_count', 'last_ticket_date', 'created_at']
|
||||
list_filter = ['is_active', 'created_at', 'last_ticket_date']
|
||||
search_fields = ['email', 'company_name', 'contact_name', 'notes']
|
||||
readonly_fields = ['added_by', 'created_at', 'updated_at', 'last_ticket_date', 'ticket_count']
|
||||
ordering = ['-created_at']
|
||||
|
||||
fieldsets = (
|
||||
('Email Information', {
|
||||
'fields': ('email', 'is_active')
|
||||
}),
|
||||
('Contact Details', {
|
||||
'fields': ('company_name', 'contact_name', 'notes')
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('ticket_count', 'last_ticket_date'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('System Information', {
|
||||
'fields': ('added_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Automatically set added_by to current user if creating new record"""
|
||||
if not change: # If creating new object
|
||||
obj.added_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
10
backEnd/support/apps.py
Normal file
10
backEnd/support/apps.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SupportConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'support'
|
||||
|
||||
def ready(self):
|
||||
"""Import signal handlers when app is ready"""
|
||||
import support.signals
|
||||
161
backEnd/support/email_service.py
Normal file
161
backEnd/support/email_service.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Email Service for Support Tickets
|
||||
Handles sending email notifications for ticket creation and updates
|
||||
"""
|
||||
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.conf import settings
|
||||
from django.utils.html import strip_tags
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SupportEmailService:
|
||||
"""Service for sending support ticket related emails"""
|
||||
|
||||
@staticmethod
|
||||
def send_ticket_confirmation_to_user(ticket):
|
||||
"""
|
||||
Send ticket confirmation email to the user who created the ticket
|
||||
|
||||
Args:
|
||||
ticket: SupportTicket instance
|
||||
"""
|
||||
try:
|
||||
subject = f'Ticket Created: {ticket.ticket_number}'
|
||||
|
||||
# Context for email template
|
||||
context = {
|
||||
'ticket': ticket,
|
||||
'ticket_number': ticket.ticket_number,
|
||||
'user_name': ticket.user_name,
|
||||
'title': ticket.title,
|
||||
'description': ticket.description,
|
||||
'ticket_type': ticket.get_ticket_type_display(),
|
||||
'category': ticket.category.name if ticket.category else 'General',
|
||||
'priority': ticket.priority.name if ticket.priority else 'Medium',
|
||||
'status': ticket.status.name if ticket.status else 'Open',
|
||||
'created_at': ticket.created_at.strftime('%B %d, %Y at %I:%M %p'),
|
||||
'support_url': f'{settings.SITE_URL}/support-center',
|
||||
}
|
||||
|
||||
# Render HTML email
|
||||
html_message = render_to_string(
|
||||
'support/ticket_confirmation_user.html',
|
||||
context
|
||||
)
|
||||
|
||||
# Create plain text version
|
||||
text_message = render_to_string(
|
||||
'support/ticket_confirmation_user.txt',
|
||||
context
|
||||
)
|
||||
|
||||
# Create email
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[ticket.user_email],
|
||||
)
|
||||
|
||||
# Attach HTML version
|
||||
email.attach_alternative(html_message, "text/html")
|
||||
|
||||
# Send email
|
||||
email.send(fail_silently=False)
|
||||
|
||||
logger.info(f'Ticket confirmation email sent to user: {ticket.user_email} for ticket {ticket.ticket_number}')
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send ticket confirmation email to user: {str(e)}')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_ticket_notification_to_company(ticket):
|
||||
"""
|
||||
Send ticket notification email to company/support team
|
||||
|
||||
Args:
|
||||
ticket: SupportTicket instance
|
||||
"""
|
||||
try:
|
||||
subject = f'New Support Ticket: {ticket.ticket_number} - {ticket.title}'
|
||||
|
||||
# Get company email from settings
|
||||
company_email = getattr(settings, 'SUPPORT_EMAIL', settings.DEFAULT_FROM_EMAIL)
|
||||
|
||||
# Context for email template
|
||||
context = {
|
||||
'ticket': ticket,
|
||||
'ticket_number': ticket.ticket_number,
|
||||
'user_name': ticket.user_name,
|
||||
'user_email': ticket.user_email,
|
||||
'user_phone': ticket.user_phone,
|
||||
'company': ticket.company,
|
||||
'title': ticket.title,
|
||||
'description': ticket.description,
|
||||
'ticket_type': ticket.get_ticket_type_display(),
|
||||
'category': ticket.category.name if ticket.category else 'General',
|
||||
'priority': ticket.priority.name if ticket.priority else 'Medium',
|
||||
'status': ticket.status.name if ticket.status else 'Open',
|
||||
'created_at': ticket.created_at.strftime('%B %d, %Y at %I:%M %p'),
|
||||
'admin_url': f'{settings.SITE_URL}/admin/support/supportticket/{ticket.id}/change/',
|
||||
}
|
||||
|
||||
# Render HTML email
|
||||
html_message = render_to_string(
|
||||
'support/ticket_notification_company.html',
|
||||
context
|
||||
)
|
||||
|
||||
# Create plain text version
|
||||
text_message = render_to_string(
|
||||
'support/ticket_notification_company.txt',
|
||||
context
|
||||
)
|
||||
|
||||
# Create email
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[company_email],
|
||||
reply_to=[ticket.user_email],
|
||||
)
|
||||
|
||||
# Attach HTML version
|
||||
email.attach_alternative(html_message, "text/html")
|
||||
|
||||
# Send email
|
||||
email.send(fail_silently=False)
|
||||
|
||||
logger.info(f'Ticket notification email sent to company: {company_email} for ticket {ticket.ticket_number}')
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send ticket notification email to company: {str(e)}')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_ticket_created_emails(ticket):
|
||||
"""
|
||||
Send both user confirmation and company notification emails
|
||||
|
||||
Args:
|
||||
ticket: SupportTicket instance
|
||||
|
||||
Returns:
|
||||
dict with status of both emails
|
||||
"""
|
||||
user_email_sent = SupportEmailService.send_ticket_confirmation_to_user(ticket)
|
||||
company_email_sent = SupportEmailService.send_ticket_notification_to_company(ticket)
|
||||
|
||||
return {
|
||||
'user_email_sent': user_email_sent,
|
||||
'company_email_sent': company_email_sent,
|
||||
}
|
||||
|
||||
0
backEnd/support/management/__init__.py
Normal file
0
backEnd/support/management/__init__.py
Normal file
BIN
backEnd/support/management/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/support/management/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
0
backEnd/support/management/commands/__init__.py
Normal file
0
backEnd/support/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
57
backEnd/support/management/commands/add_registered_emails.py
Normal file
57
backEnd/support/management/commands/add_registered_emails.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Management command to add registered emails for testing
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth.models import User
|
||||
from support.models import RegisteredEmail
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Add sample registered emails for testing'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# Get the admin user to set as added_by
|
||||
admin_user = User.objects.filter(is_superuser=True).first()
|
||||
|
||||
emails_to_add = [
|
||||
{
|
||||
'email': 'admin@gnxsoft.com',
|
||||
'company_name': 'GNX Software',
|
||||
'contact_name': 'Admin User',
|
||||
'notes': 'Primary admin email',
|
||||
},
|
||||
{
|
||||
'email': 'support@gnxsoft.com',
|
||||
'company_name': 'GNX Software',
|
||||
'contact_name': 'Support Team',
|
||||
'notes': 'General support email',
|
||||
},
|
||||
]
|
||||
|
||||
created_count = 0
|
||||
for email_data in emails_to_add:
|
||||
registered_email, created = RegisteredEmail.objects.get_or_create(
|
||||
email=email_data['email'],
|
||||
defaults={
|
||||
'company_name': email_data['company_name'],
|
||||
'contact_name': email_data['contact_name'],
|
||||
'notes': email_data['notes'],
|
||||
'added_by': admin_user,
|
||||
'is_active': True,
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
created_count += 1
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'✓ Created registered email: {email_data["email"]}')
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'- Email already exists: {email_data["email"]}')
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'\n✓ Added {created_count} new registered emails')
|
||||
)
|
||||
|
||||
268
backEnd/support/management/commands/populate_support_data.py
Normal file
268
backEnd/support/management/commands/populate_support_data.py
Normal file
@@ -0,0 +1,268 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.text import slugify
|
||||
from support.models import (
|
||||
TicketStatus, TicketPriority, TicketCategory,
|
||||
KnowledgeBaseCategory, KnowledgeBaseArticle
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Populate support center with initial data'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.stdout.write(self.style.SUCCESS('Starting to populate support data...'))
|
||||
|
||||
# Create Ticket Statuses
|
||||
self.create_ticket_statuses()
|
||||
|
||||
# Create Ticket Priorities
|
||||
self.create_ticket_priorities()
|
||||
|
||||
# Create Ticket Categories
|
||||
self.create_ticket_categories()
|
||||
|
||||
# Create Knowledge Base Categories
|
||||
self.create_kb_categories()
|
||||
|
||||
# Create Knowledge Base Articles
|
||||
self.create_kb_articles()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Successfully populated support data!'))
|
||||
|
||||
def create_ticket_statuses(self):
|
||||
self.stdout.write('Creating ticket statuses...')
|
||||
|
||||
statuses = [
|
||||
{'name': 'Open', 'color': '#3b82f6', 'description': 'Ticket has been opened', 'is_closed': False, 'display_order': 1},
|
||||
{'name': 'In Progress', 'color': '#f59e0b', 'description': 'Ticket is being worked on', 'is_closed': False, 'display_order': 2},
|
||||
{'name': 'Pending Response', 'color': '#8b5cf6', 'description': 'Waiting for customer response', 'is_closed': False, 'display_order': 3},
|
||||
{'name': 'Resolved', 'color': '#10b981', 'description': 'Ticket has been resolved', 'is_closed': True, 'display_order': 4},
|
||||
{'name': 'Closed', 'color': '#6b7280', 'description': 'Ticket has been closed', 'is_closed': True, 'display_order': 5},
|
||||
]
|
||||
|
||||
for status_data in statuses:
|
||||
status, created = TicketStatus.objects.get_or_create(
|
||||
name=status_data['name'],
|
||||
defaults=status_data
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(f' ✓ Created status: {status.name}'))
|
||||
else:
|
||||
self.stdout.write(f' - Status already exists: {status.name}')
|
||||
|
||||
def create_ticket_priorities(self):
|
||||
self.stdout.write('Creating ticket priorities...')
|
||||
|
||||
priorities = [
|
||||
{'name': 'Low', 'level': 4, 'color': '#6b7280', 'description': 'Low priority issue', 'sla_hours': 72},
|
||||
{'name': 'Medium', 'level': 3, 'color': '#3b82f6', 'description': 'Medium priority issue', 'sla_hours': 48},
|
||||
{'name': 'High', 'level': 2, 'color': '#f59e0b', 'description': 'High priority issue', 'sla_hours': 24},
|
||||
{'name': 'Critical', 'level': 1, 'color': '#ef4444', 'description': 'Critical issue requiring immediate attention', 'sla_hours': 4},
|
||||
]
|
||||
|
||||
for priority_data in priorities:
|
||||
priority, created = TicketPriority.objects.get_or_create(
|
||||
name=priority_data['name'],
|
||||
defaults=priority_data
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(f' ✓ Created priority: {priority.name}'))
|
||||
else:
|
||||
self.stdout.write(f' - Priority already exists: {priority.name}')
|
||||
|
||||
def create_ticket_categories(self):
|
||||
self.stdout.write('Creating ticket categories...')
|
||||
|
||||
categories = [
|
||||
{'name': 'Technical Support', 'description': 'Technical issues and troubleshooting', 'color': '#3b82f6', 'icon': 'fa-wrench', 'display_order': 1},
|
||||
{'name': 'Billing & Payments', 'description': 'Billing questions and payment issues', 'color': '#10b981', 'icon': 'fa-credit-card', 'display_order': 2},
|
||||
{'name': 'Account Management', 'description': 'Account settings and access issues', 'color': '#8b5cf6', 'icon': 'fa-user-cog', 'display_order': 3},
|
||||
{'name': 'Product Inquiry', 'description': 'Questions about products and features', 'color': '#f59e0b', 'icon': 'fa-box', 'display_order': 4},
|
||||
{'name': 'Bug Reports', 'description': 'Report software bugs and issues', 'color': '#ef4444', 'icon': 'fa-bug', 'display_order': 5},
|
||||
{'name': 'Feature Requests', 'description': 'Request new features or improvements', 'color': '#06b6d4', 'icon': 'fa-lightbulb', 'display_order': 6},
|
||||
]
|
||||
|
||||
for category_data in categories:
|
||||
category, created = TicketCategory.objects.get_or_create(
|
||||
name=category_data['name'],
|
||||
defaults=category_data
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(f' ✓ Created category: {category.name}'))
|
||||
else:
|
||||
self.stdout.write(f' - Category already exists: {category.name}')
|
||||
|
||||
def create_kb_categories(self):
|
||||
self.stdout.write('Creating knowledge base categories...')
|
||||
|
||||
categories = [
|
||||
{'name': 'Getting Started', 'slug': 'getting-started', 'description': 'Learn the basics and get started quickly', 'icon': 'fa-rocket', 'color': '#3b82f6', 'display_order': 1},
|
||||
{'name': 'Account & Billing', 'slug': 'account-billing', 'description': 'Manage your account and billing information', 'icon': 'fa-user-circle', 'color': '#10b981', 'display_order': 2},
|
||||
{'name': 'Technical Documentation', 'slug': 'technical-docs', 'description': 'Technical guides and API documentation', 'icon': 'fa-code', 'color': '#8b5cf6', 'display_order': 3},
|
||||
{'name': 'Troubleshooting', 'slug': 'troubleshooting', 'description': 'Common issues and how to resolve them', 'icon': 'fa-tools', 'color': '#f59e0b', 'display_order': 4},
|
||||
{'name': 'Security & Privacy', 'slug': 'security-privacy', 'description': 'Security features and privacy settings', 'icon': 'fa-shield-alt', 'color': '#ef4444', 'display_order': 5},
|
||||
{'name': 'Best Practices', 'slug': 'best-practices', 'description': 'Tips and best practices for optimal use', 'icon': 'fa-star', 'color': '#daa520', 'display_order': 6},
|
||||
]
|
||||
|
||||
for category_data in categories:
|
||||
category, created = KnowledgeBaseCategory.objects.get_or_create(
|
||||
slug=category_data['slug'],
|
||||
defaults=category_data
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(f' ✓ Created KB category: {category.name}'))
|
||||
else:
|
||||
self.stdout.write(f' - KB category already exists: {category.name}')
|
||||
|
||||
def create_kb_articles(self):
|
||||
self.stdout.write('Creating knowledge base articles...')
|
||||
|
||||
# Get categories
|
||||
getting_started = KnowledgeBaseCategory.objects.filter(slug='getting-started').first()
|
||||
account_billing = KnowledgeBaseCategory.objects.filter(slug='account-billing').first()
|
||||
technical = KnowledgeBaseCategory.objects.filter(slug='technical-docs').first()
|
||||
troubleshooting = KnowledgeBaseCategory.objects.filter(slug='troubleshooting').first()
|
||||
|
||||
articles = [
|
||||
{
|
||||
'title': 'How to Get Started with Our Platform',
|
||||
'slug': 'how-to-get-started',
|
||||
'category': getting_started,
|
||||
'summary': 'A comprehensive guide to help you get started with our platform quickly and easily.',
|
||||
'content': '''<h2>Welcome to Our Platform!</h2>
|
||||
<p>This guide will help you get started with our platform in just a few simple steps.</p>
|
||||
|
||||
<h3>Step 1: Create Your Account</h3>
|
||||
<p>Visit our sign-up page and create your account using your email address or social login.</p>
|
||||
|
||||
<h3>Step 2: Complete Your Profile</h3>
|
||||
<p>Add your company information and customize your profile settings.</p>
|
||||
|
||||
<h3>Step 3: Explore the Dashboard</h3>
|
||||
<p>Familiarize yourself with the main dashboard and available features.</p>
|
||||
|
||||
<h3>Step 4: Start Using Our Services</h3>
|
||||
<p>Begin using our services and tools to achieve your business goals.</p>
|
||||
|
||||
<p>If you need any help, our support team is always here to assist you!</p>''',
|
||||
'is_published': True,
|
||||
'is_featured': True,
|
||||
},
|
||||
{
|
||||
'title': 'Understanding Your Billing Cycle',
|
||||
'slug': 'understanding-billing-cycle',
|
||||
'category': account_billing,
|
||||
'summary': 'Learn how our billing cycle works and how to manage your payments.',
|
||||
'content': '''<h2>Billing Cycle Overview</h2>
|
||||
<p>Understanding your billing cycle is important for managing your subscription effectively.</p>
|
||||
|
||||
<h3>Monthly Billing</h3>
|
||||
<p>For monthly subscriptions, you'll be charged on the same date each month.</p>
|
||||
|
||||
<h3>Annual Billing</h3>
|
||||
<p>Annual subscriptions offer a discount and are billed once per year.</p>
|
||||
|
||||
<h3>Managing Your Subscription</h3>
|
||||
<p>You can upgrade, downgrade, or cancel your subscription at any time from your account settings.</p>''',
|
||||
'is_published': True,
|
||||
'is_featured': True,
|
||||
},
|
||||
{
|
||||
'title': 'API Documentation Overview',
|
||||
'slug': 'api-documentation-overview',
|
||||
'category': technical,
|
||||
'summary': 'Complete guide to our API endpoints and authentication.',
|
||||
'content': '''<h2>API Documentation</h2>
|
||||
<p>Our API provides programmatic access to all platform features.</p>
|
||||
|
||||
<h3>Authentication</h3>
|
||||
<p>All API requests require authentication using an API key.</p>
|
||||
<code>Authorization: Bearer YOUR_API_KEY</code>
|
||||
|
||||
<h3>Rate Limits</h3>
|
||||
<p>Standard accounts are limited to 1000 requests per hour.</p>
|
||||
|
||||
<h3>Response Format</h3>
|
||||
<p>All responses are returned in JSON format.</p>''',
|
||||
'is_published': True,
|
||||
'is_featured': True,
|
||||
},
|
||||
{
|
||||
'title': 'Common Login Issues and Solutions',
|
||||
'slug': 'common-login-issues',
|
||||
'category': troubleshooting,
|
||||
'summary': 'Troubleshoot common login problems and learn how to resolve them.',
|
||||
'content': '''<h2>Login Troubleshooting</h2>
|
||||
<p>Having trouble logging in? Here are some common issues and solutions.</p>
|
||||
|
||||
<h3>Forgot Password</h3>
|
||||
<p>Click "Forgot Password" on the login page to reset your password via email.</p>
|
||||
|
||||
<h3>Account Locked</h3>
|
||||
<p>After multiple failed login attempts, your account may be temporarily locked for security.</p>
|
||||
|
||||
<h3>Browser Issues</h3>
|
||||
<p>Clear your browser cache and cookies, or try a different browser.</p>
|
||||
|
||||
<h3>Still Having Issues?</h3>
|
||||
<p>Contact our support team for personalized assistance.</p>''',
|
||||
'is_published': True,
|
||||
'is_featured': False,
|
||||
},
|
||||
{
|
||||
'title': 'How to Update Your Payment Method',
|
||||
'slug': 'update-payment-method',
|
||||
'category': account_billing,
|
||||
'summary': 'Step-by-step guide to updating your payment information.',
|
||||
'content': '''<h2>Updating Payment Information</h2>
|
||||
<p>Keep your payment method up to date to avoid service interruptions.</p>
|
||||
|
||||
<h3>Steps to Update</h3>
|
||||
<ol>
|
||||
<li>Go to Account Settings</li>
|
||||
<li>Click on "Billing & Payments"</li>
|
||||
<li>Select "Update Payment Method"</li>
|
||||
<li>Enter your new payment details</li>
|
||||
<li>Click "Save Changes"</li>
|
||||
</ol>
|
||||
|
||||
<h3>Supported Payment Methods</h3>
|
||||
<p>We accept all major credit cards, PayPal, and bank transfers.</p>''',
|
||||
'is_published': True,
|
||||
'is_featured': False,
|
||||
},
|
||||
{
|
||||
'title': 'Security Best Practices',
|
||||
'slug': 'security-best-practices',
|
||||
'category': KnowledgeBaseCategory.objects.filter(slug='security-privacy').first(),
|
||||
'summary': 'Essential security practices to keep your account safe.',
|
||||
'content': '''<h2>Account Security</h2>
|
||||
<p>Follow these best practices to keep your account secure.</p>
|
||||
|
||||
<h3>Use Strong Passwords</h3>
|
||||
<p>Create complex passwords with a mix of letters, numbers, and symbols.</p>
|
||||
|
||||
<h3>Enable Two-Factor Authentication</h3>
|
||||
<p>Add an extra layer of security with 2FA.</p>
|
||||
|
||||
<h3>Regular Security Audits</h3>
|
||||
<p>Review your account activity regularly for any suspicious behavior.</p>
|
||||
|
||||
<h3>Keep Software Updated</h3>
|
||||
<p>Always use the latest version of our software for the best security.</p>''',
|
||||
'is_published': True,
|
||||
'is_featured': True,
|
||||
},
|
||||
]
|
||||
|
||||
for article_data in articles:
|
||||
if article_data['category']:
|
||||
article, created = KnowledgeBaseArticle.objects.get_or_create(
|
||||
slug=article_data['slug'],
|
||||
defaults=article_data
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(f' ✓ Created article: {article.title}'))
|
||||
else:
|
||||
self.stdout.write(f' - Article already exists: {article.title}')
|
||||
|
||||
183
backEnd/support/migrations/0001_initial.py
Normal file
183
backEnd/support/migrations/0001_initial.py
Normal file
@@ -0,0 +1,183 @@
|
||||
# Generated by Django 4.2.7 on 2025-10-07 15:42
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SupportSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('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)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Support Settings',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('color', models.CharField(default='#667eea', help_text='Hex color code', max_length=7)),
|
||||
('icon', models.CharField(default='fa-question-circle', help_text='FontAwesome icon class', max_length=50)),
|
||||
('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)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Ticket Categories',
|
||||
'ordering': ['display_order', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketPriority',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('level', models.PositiveIntegerField(help_text='Lower number = higher priority', unique=True)),
|
||||
('color', models.CharField(default='#667eea', help_text='Hex color code', max_length=7)),
|
||||
('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)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Ticket Priorities',
|
||||
'ordering': ['level'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketStatus',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('color', models.CharField(default='#667eea', help_text='Hex color code', max_length=7)),
|
||||
('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)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Ticket Statuses',
|
||||
'ordering': ['display_order', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SupportTicket',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ticket_number', models.CharField(editable=False, max_length=20, unique=True)),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('description', models.TextField()),
|
||||
('ticket_type', models.CharField(choices=[('technical', 'Technical Issue'), ('billing', 'Billing Question'), ('feature_request', 'Feature Request'), ('bug_report', 'Bug Report'), ('general', 'General Inquiry'), ('account', 'Account Issue')], default='general', max_length=20)),
|
||||
('user_name', models.CharField(max_length=100)),
|
||||
('user_email', models.EmailField(max_length=254)),
|
||||
('user_phone', models.CharField(blank=True, max_length=20)),
|
||||
('company', models.CharField(blank=True, max_length=100)),
|
||||
('assigned_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('closed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('last_activity', models.DateTimeField(auto_now=True)),
|
||||
('first_response_at', models.DateTimeField(blank=True, null=True)),
|
||||
('sla_deadline', models.DateTimeField(blank=True, null=True)),
|
||||
('tags', models.CharField(blank=True, help_text='Comma-separated tags', max_length=500)),
|
||||
('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(blank=True, default=list, help_text='List of file paths')),
|
||||
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_tickets', to=settings.AUTH_USER_MODEL)),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='support.ticketcategory')),
|
||||
('priority', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='support.ticketpriority')),
|
||||
('status', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='support.ticketstatus')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='support_tickets', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketMessage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message_type', models.CharField(choices=[('user_message', 'User Message'), ('agent_response', 'Agent Response'), ('system_note', 'System Note'), ('status_change', 'Status Change'), ('assignment_change', 'Assignment Change'), ('escalation', 'Escalation')], default='user_message', max_length=20)),
|
||||
('content', models.TextField()),
|
||||
('author_name', models.CharField(blank=True, max_length=100)),
|
||||
('author_email', models.EmailField(blank=True, max_length=254)),
|
||||
('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(blank=True, default=list, help_text='List of file paths')),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('read_at', models.DateTimeField(blank=True, null=True)),
|
||||
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_messages', to=settings.AUTH_USER_MODEL)),
|
||||
('read_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='read_messages', to=settings.AUTH_USER_MODEL)),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='support.supportticket')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created_at'],
|
||||
'indexes': [models.Index(fields=['ticket', 'created_at'], name='support_tic_ticket__0cd9bd_idx'), models.Index(fields=['author'], name='support_tic_author__503d4b_idx'), models.Index(fields=['message_type'], name='support_tic_message_6220bd_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketActivity',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('activity_type', models.CharField(choices=[('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')], max_length=20)),
|
||||
('description', models.TextField()),
|
||||
('user_name', models.CharField(blank=True, max_length=100)),
|
||||
('old_value', models.TextField(blank=True)),
|
||||
('new_value', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='support.supportticket')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_activities', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['ticket', 'created_at'], name='support_tic_ticket__4097ca_idx'), models.Index(fields=['activity_type'], name='support_tic_activit_9c98a0_idx')],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='supportticket',
|
||||
index=models.Index(fields=['ticket_number'], name='support_sup_ticket__4a7d4b_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='supportticket',
|
||||
index=models.Index(fields=['user_email'], name='support_sup_user_em_c518a8_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='supportticket',
|
||||
index=models.Index(fields=['status'], name='support_sup_status__7b4480_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='supportticket',
|
||||
index=models.Index(fields=['priority'], name='support_sup_priorit_5d48ff_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='supportticket',
|
||||
index=models.Index(fields=['assigned_to'], name='support_sup_assigne_53b075_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='supportticket',
|
||||
index=models.Index(fields=['created_at'], name='support_sup_created_83a137_idx'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 4.2.7 on 2025-10-07 18:06
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('support', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='KnowledgeBaseCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=120, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('icon', models.CharField(default='fa-book', help_text='FontAwesome icon class', max_length=50)),
|
||||
('color', models.CharField(default='#667eea', help_text='Hex color code', max_length=7)),
|
||||
('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)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Knowledge Base Categories',
|
||||
'ordering': ['display_order', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='KnowledgeBaseArticle',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('slug', models.SlugField(max_length=220, unique=True)),
|
||||
('content', models.TextField()),
|
||||
('summary', models.TextField(blank=True, help_text='Short summary of the article')),
|
||||
('meta_description', models.CharField(blank=True, max_length=160)),
|
||||
('keywords', models.CharField(blank=True, help_text='Comma-separated keywords', max_length=500)),
|
||||
('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)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('published_at', models.DateTimeField(blank=True, null=True)),
|
||||
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='kb_articles', to=settings.AUTH_USER_MODEL)),
|
||||
('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='articles', to='support.knowledgebasecategory')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['slug'], name='support_kno_slug_0c7b3a_idx'), models.Index(fields=['category'], name='support_kno_categor_733ba1_idx'), models.Index(fields=['is_published'], name='support_kno_is_publ_402a55_idx'), models.Index(fields=['created_at'], name='support_kno_created_ef91a5_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
39
backEnd/support/migrations/0003_registeredemail.py
Normal file
39
backEnd/support/migrations/0003_registeredemail.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# Generated by Django 4.2.7 on 2025-10-07 18:58
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('support', '0002_knowledgebasecategory_knowledgebasearticle'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RegisteredEmail',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(max_length=254, unique=True, validators=[django.core.validators.EmailValidator()])),
|
||||
('company_name', models.CharField(blank=True, help_text='Company or organization name', max_length=200)),
|
||||
('contact_name', models.CharField(blank=True, help_text='Primary contact name', max_length=200)),
|
||||
('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')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('last_ticket_date', models.DateTimeField(blank=True, help_text='Last time this email submitted a ticket', null=True)),
|
||||
('ticket_count', models.PositiveIntegerField(default=0, help_text='Total number of tickets submitted')),
|
||||
('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='registered_emails', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Registered Email',
|
||||
'verbose_name_plural': 'Registered Emails',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['email'], name='support_reg_email_ee78ad_idx'), models.Index(fields=['is_active'], name='support_reg_is_acti_fadc86_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backEnd/support/migrations/__init__.py
Normal file
0
backEnd/support/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backEnd/support/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backEnd/support/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
320
backEnd/support/models.py
Normal file
320
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'])
|
||||
211
backEnd/support/serializers.py
Normal file
211
backEnd/support/serializers.py
Normal file
@@ -0,0 +1,211 @@
|
||||
from rest_framework import serializers
|
||||
from .models import (
|
||||
SupportTicket, TicketStatus, TicketPriority, TicketCategory,
|
||||
TicketMessage, TicketActivity, KnowledgeBaseCategory,
|
||||
KnowledgeBaseArticle, SupportSettings
|
||||
)
|
||||
from .email_service import SupportEmailService
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TicketStatusSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TicketStatus
|
||||
fields = ['id', 'name', 'color', 'description', 'is_closed', 'display_order']
|
||||
|
||||
|
||||
class TicketPrioritySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TicketPriority
|
||||
fields = ['id', 'name', 'level', 'color', 'description', 'sla_hours']
|
||||
|
||||
|
||||
class TicketCategorySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TicketCategory
|
||||
fields = ['id', 'name', 'description', 'color', 'icon', 'display_order']
|
||||
|
||||
|
||||
class TicketMessageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TicketMessage
|
||||
fields = [
|
||||
'id', 'ticket', 'message_type', 'content', 'author_name',
|
||||
'author_email', 'is_internal', 'created_at', 'updated_at',
|
||||
'attachments', 'is_read'
|
||||
]
|
||||
read_only_fields = ['created_at', 'updated_at']
|
||||
|
||||
|
||||
class TicketActivitySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TicketActivity
|
||||
fields = [
|
||||
'id', 'activity_type', 'description', 'user_name',
|
||||
'old_value', 'new_value', 'created_at'
|
||||
]
|
||||
read_only_fields = ['created_at']
|
||||
|
||||
|
||||
class SupportTicketSerializer(serializers.ModelSerializer):
|
||||
status_name = serializers.CharField(source='status.name', read_only=True)
|
||||
status_color = serializers.CharField(source='status.color', read_only=True)
|
||||
priority_name = serializers.CharField(source='priority.name', read_only=True)
|
||||
priority_color = serializers.CharField(source='priority.color', read_only=True)
|
||||
category_name = serializers.CharField(source='category.name', read_only=True)
|
||||
messages = TicketMessageSerializer(many=True, read_only=True)
|
||||
activities = TicketActivitySerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = SupportTicket
|
||||
fields = [
|
||||
'id', 'ticket_number', 'title', 'description', 'ticket_type',
|
||||
'user_name', 'user_email', 'user_phone', 'company',
|
||||
'category', 'category_name', 'priority', 'priority_name', 'priority_color',
|
||||
'status', 'status_name', 'status_color', 'assigned_to', 'assigned_at',
|
||||
'created_at', 'updated_at', 'closed_at', 'last_activity',
|
||||
'first_response_at', 'sla_deadline', 'tags', 'is_escalated',
|
||||
'escalation_reason', 'attachments', 'messages', 'activities'
|
||||
]
|
||||
read_only_fields = [
|
||||
'ticket_number', 'created_at', 'updated_at', 'last_activity',
|
||||
'assigned_at', 'closed_at', 'first_response_at', 'sla_deadline'
|
||||
]
|
||||
|
||||
|
||||
class SupportTicketCreateSerializer(serializers.ModelSerializer):
|
||||
"""Simplified serializer for creating tickets from public form"""
|
||||
ticket_number = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = SupportTicket
|
||||
fields = [
|
||||
'ticket_number', 'title', 'description', 'ticket_type', 'user_name',
|
||||
'user_email', 'user_phone', 'company', 'category'
|
||||
]
|
||||
read_only_fields = ['ticket_number']
|
||||
|
||||
def validate_user_email(self, value):
|
||||
"""
|
||||
Validate email format and optionally check if registered
|
||||
"""
|
||||
from .models import RegisteredEmail
|
||||
from django.core.validators import validate_email
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
|
||||
# Basic email format validation
|
||||
try:
|
||||
validate_email(value)
|
||||
except DjangoValidationError:
|
||||
raise serializers.ValidationError("Please enter a valid email address.")
|
||||
|
||||
# Optional: Check if email is registered (for analytics/tracking)
|
||||
# But don't block ticket creation if not registered
|
||||
try:
|
||||
registered_email = RegisteredEmail.objects.get(email=value)
|
||||
if not registered_email.is_active:
|
||||
# Log this but don't block the ticket
|
||||
logger.warning(f'Ticket submitted by deactivated registered email: {value}')
|
||||
except RegisteredEmail.DoesNotExist:
|
||||
# This is fine - unregistered users can submit tickets
|
||||
logger.info(f'Ticket submitted by unregistered email: {value}')
|
||||
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
from .models import RegisteredEmail
|
||||
|
||||
# Set default status and priority if not set
|
||||
if not validated_data.get('status'):
|
||||
default_status = TicketStatus.objects.filter(name='Open').first()
|
||||
if default_status:
|
||||
validated_data['status'] = default_status
|
||||
|
||||
if not validated_data.get('priority'):
|
||||
default_priority = TicketPriority.objects.filter(name='Medium').first()
|
||||
if default_priority:
|
||||
validated_data['priority'] = default_priority
|
||||
|
||||
ticket = SupportTicket.objects.create(**validated_data)
|
||||
|
||||
# Create initial activity
|
||||
TicketActivity.objects.create(
|
||||
ticket=ticket,
|
||||
activity_type='created',
|
||||
description=f'Ticket created by {ticket.user_name}',
|
||||
user_name=ticket.user_name
|
||||
)
|
||||
|
||||
# Update registered email statistics (only if email is registered)
|
||||
try:
|
||||
registered_email = RegisteredEmail.objects.get(email=validated_data['user_email'])
|
||||
registered_email.increment_ticket_count()
|
||||
logger.info(f'Updated ticket count for registered email: {validated_data["user_email"]}')
|
||||
except RegisteredEmail.DoesNotExist:
|
||||
# This is normal now - unregistered users can submit tickets
|
||||
logger.info(f'Ticket created by unregistered email: {validated_data["user_email"]}')
|
||||
|
||||
# Send email notifications
|
||||
try:
|
||||
email_results = SupportEmailService.send_ticket_created_emails(ticket)
|
||||
logger.info(f'Email notifications sent for ticket {ticket.ticket_number}: {email_results}')
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send email notifications for ticket {ticket.ticket_number}: {str(e)}')
|
||||
# Don't fail ticket creation if emails fail
|
||||
|
||||
return ticket
|
||||
|
||||
|
||||
class TicketStatusCheckSerializer(serializers.Serializer):
|
||||
"""Serializer for checking ticket status by ticket number"""
|
||||
ticket_number = serializers.CharField(max_length=20)
|
||||
|
||||
|
||||
class KnowledgeBaseCategorySerializer(serializers.ModelSerializer):
|
||||
article_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = KnowledgeBaseCategory
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'description', 'icon', 'color',
|
||||
'display_order', 'article_count'
|
||||
]
|
||||
|
||||
def get_article_count(self, obj):
|
||||
return obj.articles.filter(is_published=True).count()
|
||||
|
||||
|
||||
class KnowledgeBaseArticleListSerializer(serializers.ModelSerializer):
|
||||
category_name = serializers.CharField(source='category.name', read_only=True)
|
||||
category_slug = serializers.CharField(source='category.slug', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = KnowledgeBaseArticle
|
||||
fields = [
|
||||
'id', 'title', 'slug', 'category', 'category_name', 'category_slug',
|
||||
'summary', 'is_featured', 'view_count', 'helpful_count',
|
||||
'not_helpful_count', 'created_at', 'updated_at', 'published_at'
|
||||
]
|
||||
|
||||
|
||||
class KnowledgeBaseArticleDetailSerializer(serializers.ModelSerializer):
|
||||
category_name = serializers.CharField(source='category.name', read_only=True)
|
||||
category_slug = serializers.CharField(source='category.slug', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = KnowledgeBaseArticle
|
||||
fields = [
|
||||
'id', 'title', 'slug', 'category', 'category_name', 'category_slug',
|
||||
'content', 'summary', 'meta_description', 'keywords',
|
||||
'is_featured', 'view_count', 'helpful_count', 'not_helpful_count',
|
||||
'created_at', 'updated_at', 'published_at'
|
||||
]
|
||||
|
||||
|
||||
class SupportSettingsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SupportSettings
|
||||
fields = ['id', 'setting_name', 'setting_value', 'description', 'is_active']
|
||||
|
||||
235
backEnd/support/signals.py
Normal file
235
backEnd/support/signals.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
Django signals for Support app
|
||||
Handles automatic notifications when tickets are updated
|
||||
"""
|
||||
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.conf import settings
|
||||
import logging
|
||||
|
||||
from .models import SupportTicket, TicketMessage, TicketActivity
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TicketUpdateNotifier:
|
||||
"""Service for sending ticket update notifications"""
|
||||
|
||||
@staticmethod
|
||||
def send_status_change_notification(ticket, old_status, new_status):
|
||||
"""Send email when ticket status changes"""
|
||||
try:
|
||||
subject = f'Ticket Status Updated: {ticket.ticket_number}'
|
||||
|
||||
context = {
|
||||
'ticket': ticket,
|
||||
'ticket_number': ticket.ticket_number,
|
||||
'user_name': ticket.user_name,
|
||||
'title': ticket.title,
|
||||
'old_status': old_status,
|
||||
'new_status': new_status,
|
||||
'status_color': ticket.status.color if ticket.status else '#3b82f6',
|
||||
'updated_at': ticket.updated_at.strftime('%B %d, %Y at %I:%M %p'),
|
||||
'support_url': f'{settings.SITE_URL}/support-center',
|
||||
}
|
||||
|
||||
# Render HTML email
|
||||
html_message = render_to_string(
|
||||
'support/ticket_status_update.html',
|
||||
context
|
||||
)
|
||||
|
||||
# Create plain text version
|
||||
text_message = render_to_string(
|
||||
'support/ticket_status_update.txt',
|
||||
context
|
||||
)
|
||||
|
||||
# Create email
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[ticket.user_email],
|
||||
)
|
||||
|
||||
email.attach_alternative(html_message, "text/html")
|
||||
email.send(fail_silently=False)
|
||||
|
||||
logger.info(f'Status change notification sent for ticket {ticket.ticket_number}')
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send status change notification: {str(e)}')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_message_notification(ticket, message):
|
||||
"""Send email when a new message is added to the ticket"""
|
||||
try:
|
||||
# Only send if it's an agent response (not user message)
|
||||
if message.message_type != 'agent_response':
|
||||
return False
|
||||
|
||||
subject = f'New Response on Ticket: {ticket.ticket_number}'
|
||||
|
||||
context = {
|
||||
'ticket': ticket,
|
||||
'ticket_number': ticket.ticket_number,
|
||||
'user_name': ticket.user_name,
|
||||
'title': ticket.title,
|
||||
'message': message.content,
|
||||
'message_author': message.author_name or 'Support Team',
|
||||
'created_at': message.created_at.strftime('%B %d, %Y at %I:%M %p'),
|
||||
'support_url': f'{settings.SITE_URL}/support-center',
|
||||
}
|
||||
|
||||
# Render HTML email
|
||||
html_message = render_to_string(
|
||||
'support/ticket_message_notification.html',
|
||||
context
|
||||
)
|
||||
|
||||
# Create plain text version
|
||||
text_message = render_to_string(
|
||||
'support/ticket_message_notification.txt',
|
||||
context
|
||||
)
|
||||
|
||||
# Create email
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[ticket.user_email],
|
||||
)
|
||||
|
||||
email.attach_alternative(html_message, "text/html")
|
||||
email.send(fail_silently=False)
|
||||
|
||||
logger.info(f'Message notification sent for ticket {ticket.ticket_number}')
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send message notification: {str(e)}')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_assignment_notification(ticket, assigned_to):
|
||||
"""Send email when ticket is assigned"""
|
||||
try:
|
||||
subject = f'Ticket Assigned: {ticket.ticket_number}'
|
||||
|
||||
context = {
|
||||
'ticket': ticket,
|
||||
'ticket_number': ticket.ticket_number,
|
||||
'user_name': ticket.user_name,
|
||||
'title': ticket.title,
|
||||
'assigned_to': assigned_to.get_full_name() or assigned_to.username,
|
||||
'updated_at': ticket.updated_at.strftime('%B %d, %Y at %I:%M %p'),
|
||||
'support_url': f'{settings.SITE_URL}/support-center',
|
||||
}
|
||||
|
||||
# Render HTML email
|
||||
html_message = render_to_string(
|
||||
'support/ticket_assigned_notification.html',
|
||||
context
|
||||
)
|
||||
|
||||
# Create plain text version
|
||||
text_message = render_to_string(
|
||||
'support/ticket_assigned_notification.txt',
|
||||
context
|
||||
)
|
||||
|
||||
# Create email
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[ticket.user_email],
|
||||
)
|
||||
|
||||
email.attach_alternative(html_message, "text/html")
|
||||
email.send(fail_silently=False)
|
||||
|
||||
logger.info(f'Assignment notification sent for ticket {ticket.ticket_number}')
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send assignment notification: {str(e)}')
|
||||
return False
|
||||
|
||||
|
||||
# Store original values before save
|
||||
@receiver(pre_save, sender=SupportTicket)
|
||||
def store_ticket_original_values(sender, instance, **kwargs):
|
||||
"""Store original values before ticket is updated"""
|
||||
if instance.pk:
|
||||
try:
|
||||
original = SupportTicket.objects.get(pk=instance.pk)
|
||||
instance._original_status = original.status
|
||||
instance._original_assigned_to = original.assigned_to
|
||||
except SupportTicket.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
# Send notifications after ticket is saved
|
||||
@receiver(post_save, sender=SupportTicket)
|
||||
def send_ticket_update_notifications(sender, instance, created, **kwargs):
|
||||
"""Send notifications when ticket is updated"""
|
||||
if created:
|
||||
# Ticket creation is handled in serializer
|
||||
return
|
||||
|
||||
# Check for status change
|
||||
if hasattr(instance, '_original_status') and instance._original_status != instance.status:
|
||||
old_status = instance._original_status.name if instance._original_status else 'Unknown'
|
||||
new_status = instance.status.name if instance.status else 'Unknown'
|
||||
|
||||
TicketUpdateNotifier.send_status_change_notification(
|
||||
instance,
|
||||
old_status,
|
||||
new_status
|
||||
)
|
||||
|
||||
# Create activity log
|
||||
TicketActivity.objects.create(
|
||||
ticket=instance,
|
||||
activity_type='status_changed',
|
||||
description=f'Status changed from {old_status} to {new_status}',
|
||||
old_value=old_status,
|
||||
new_value=new_status
|
||||
)
|
||||
|
||||
# Check for assignment change
|
||||
if hasattr(instance, '_original_assigned_to') and instance._original_assigned_to != instance.assigned_to:
|
||||
if instance.assigned_to:
|
||||
TicketUpdateNotifier.send_assignment_notification(
|
||||
instance,
|
||||
instance.assigned_to
|
||||
)
|
||||
|
||||
# Create activity log
|
||||
TicketActivity.objects.create(
|
||||
ticket=instance,
|
||||
activity_type='assigned',
|
||||
description=f'Ticket assigned to {instance.assigned_to.get_full_name() or instance.assigned_to.username}',
|
||||
new_value=str(instance.assigned_to)
|
||||
)
|
||||
|
||||
|
||||
# Send notification when message is added
|
||||
@receiver(post_save, sender=TicketMessage)
|
||||
def send_message_notification(sender, instance, created, **kwargs):
|
||||
"""Send notification when a new message is added"""
|
||||
if created and not instance.is_internal:
|
||||
# Only send if it's a new message that's not internal
|
||||
TicketUpdateNotifier.send_message_notification(
|
||||
instance.ticket,
|
||||
instance
|
||||
)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ticket Assigned</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.email-header {
|
||||
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||
color: #ffffff;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.email-header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.email-body {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.ticket-info {
|
||||
background: linear-gradient(135deg, rgba(218, 165, 32, 0.1) 0%, rgba(218, 165, 32, 0.05) 100%);
|
||||
border-left: 4px solid #daa520;
|
||||
padding: 20px;
|
||||
margin: 25px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.assignment-box {
|
||||
background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(139, 92, 246, 0.05) 100%);
|
||||
border-left: 4px solid #8b5cf6;
|
||||
padding: 20px;
|
||||
margin: 25px 0;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #daa520, #d4af37);
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 14px 32px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
background-color: #f8f8f8;
|
||||
padding: 25px 30px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h1>👤 Ticket Assigned</h1>
|
||||
</div>
|
||||
|
||||
<div class="email-body">
|
||||
<p>Dear {{ user_name }},</p>
|
||||
|
||||
<p>Your support ticket has been assigned to a team member who will be assisting you.</p>
|
||||
|
||||
<div class="ticket-info">
|
||||
<strong>Ticket:</strong> {{ ticket_number }}<br>
|
||||
<strong>Subject:</strong> {{ title }}
|
||||
</div>
|
||||
|
||||
<div class="assignment-box">
|
||||
<h3 style="margin-top: 0;">Assigned To</h3>
|
||||
<div style="font-size: 18px; font-weight: 700; color: #8b5cf6;">{{ assigned_to }}</div>
|
||||
<p style="margin-top: 10px; color: #666; font-size: 14px;">{{ updated_at }}</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{ support_url }}" class="cta-button">View Ticket Details</a>
|
||||
</div>
|
||||
|
||||
<p style="color: #666;">
|
||||
Your assigned support agent will review your ticket and respond as soon as possible.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
<strong>GNX Software Solutions</strong><br>
|
||||
Enterprise Support Center
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 12px;">
|
||||
This is an automated notification. For assistance, visit our <a href="{{ support_url }}" style="color: #daa520;">Support Center</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
TICKET ASSIGNED
|
||||
============================================================
|
||||
|
||||
Dear {{ user_name }},
|
||||
|
||||
Your support ticket has been assigned to a team member who will be assisting you.
|
||||
|
||||
Ticket: {{ ticket_number }}
|
||||
Subject: {{ title }}
|
||||
|
||||
------------------------------------------------------------
|
||||
ASSIGNED TO
|
||||
------------------------------------------------------------
|
||||
|
||||
{{ assigned_to }}
|
||||
|
||||
{{ updated_at }}
|
||||
|
||||
------------------------------------------------------------
|
||||
|
||||
Your assigned support agent will review your ticket and respond as soon as possible.
|
||||
|
||||
View ticket details:
|
||||
{{ support_url }}
|
||||
|
||||
============================================================
|
||||
GNX Software Solutions - Enterprise Support Center
|
||||
|
||||
This is an automated notification.
|
||||
For assistance, visit our Support Center at {{ support_url }}
|
||||
|
||||
186
backEnd/support/templates/support/ticket_confirmation_user.html
Normal file
186
backEnd/support/templates/support/ticket_confirmation_user.html
Normal file
@@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Support Ticket Confirmation</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.email-header {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
color: #ffffff;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.email-header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.email-body {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.ticket-number-box {
|
||||
background: linear-gradient(135deg, rgba(218, 165, 32, 0.1) 0%, rgba(218, 165, 32, 0.05) 100%);
|
||||
border-left: 4px solid #daa520;
|
||||
padding: 20px;
|
||||
margin: 25px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ticket-number {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #daa520;
|
||||
font-family: 'Courier New', monospace;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.info-section {
|
||||
margin: 25px 0;
|
||||
}
|
||||
.info-row {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #555555;
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
.info-value {
|
||||
color: #333333;
|
||||
}
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #daa520, #d4af37);
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 14px 32px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.footer {
|
||||
background-color: #f8f8f8;
|
||||
padding: 25px 30px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<!-- Header -->
|
||||
<div class="email-header">
|
||||
<h1>✓ Support Ticket Created</h1>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="email-body">
|
||||
<p>Dear {{ user_name }},</p>
|
||||
|
||||
<p>Thank you for contacting our support team. We've received your support request and our team will respond as soon as possible.</p>
|
||||
|
||||
<!-- Ticket Number Box -->
|
||||
<div class="ticket-number-box">
|
||||
<div style="font-size: 14px; color: #666; margin-bottom: 8px;">Your Ticket Number</div>
|
||||
<div class="ticket-number">{{ ticket_number }}</div>
|
||||
<div style="font-size: 13px; color: #666; margin-top: 8px;">Please save this number for future reference</div>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Details -->
|
||||
<div class="info-section">
|
||||
<h3 style="color: #0f172a; margin-bottom: 15px;">Ticket Details</h3>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Subject:</span>
|
||||
<span class="info-value">{{ title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Type:</span>
|
||||
<span class="info-value">{{ ticket_type }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Category:</span>
|
||||
<span class="info-value">{{ category }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Priority:</span>
|
||||
<span class="info-value">{{ priority }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Status:</span>
|
||||
<span class="status-badge">{{ status }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Created:</span>
|
||||
<span class="info-value">{{ created_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="info-section">
|
||||
<h3 style="color: #0f172a; margin-bottom: 15px;">Description</h3>
|
||||
<div style="background-color: #f8f8f8; padding: 15px; border-radius: 4px; white-space: pre-wrap;">{{ description }}</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA Button -->
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{ support_url }}" class="cta-button">Check Ticket Status</a>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 30px; color: #666;">
|
||||
<strong>What happens next?</strong><br>
|
||||
Our support team will review your ticket and respond within our standard SLA timeframe. You'll receive an email notification when there's an update.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="footer">
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
<strong>GNX Software Solutions</strong><br>
|
||||
Enterprise Support Center
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 12px;">
|
||||
This is an automated message. Please do not reply directly to this email.<br>
|
||||
For assistance, please visit our <a href="{{ support_url }}" style="color: #daa520;">Support Center</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
SUPPORT TICKET CREATED
|
||||
============================================================
|
||||
|
||||
Dear {{ user_name }},
|
||||
|
||||
Thank you for contacting our support team. We've received your support request and our team will respond as soon as possible.
|
||||
|
||||
YOUR TICKET NUMBER: {{ ticket_number }}
|
||||
Please save this number for future reference.
|
||||
|
||||
------------------------------------------------------------
|
||||
TICKET DETAILS
|
||||
------------------------------------------------------------
|
||||
|
||||
Subject: {{ title }}
|
||||
Type: {{ ticket_type }}
|
||||
Category: {{ category }}
|
||||
Priority: {{ priority }}
|
||||
Status: {{ status }}
|
||||
Created: {{ created_at }}
|
||||
|
||||
------------------------------------------------------------
|
||||
DESCRIPTION
|
||||
------------------------------------------------------------
|
||||
|
||||
{{ description }}
|
||||
|
||||
------------------------------------------------------------
|
||||
|
||||
WHAT HAPPENS NEXT?
|
||||
Our support team will review your ticket and respond within our standard SLA timeframe. You'll receive an email notification when there's an update.
|
||||
|
||||
To check your ticket status, visit:
|
||||
{{ support_url }}
|
||||
|
||||
============================================================
|
||||
GNX Software Solutions - Enterprise Support Center
|
||||
|
||||
This is an automated message. Please do not reply directly to this email.
|
||||
For assistance, please visit our Support Center at {{ support_url }}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>New Response on Your Ticket</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.email-header {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
color: #ffffff;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.email-header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.email-body {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.ticket-info {
|
||||
background: linear-gradient(135deg, rgba(218, 165, 32, 0.1) 0%, rgba(218, 165, 32, 0.05) 100%);
|
||||
border-left: 4px solid #daa520;
|
||||
padding: 20px;
|
||||
margin: 25px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.message-box {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #10b981;
|
||||
padding: 20px;
|
||||
margin: 25px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.message-author {
|
||||
font-weight: 700;
|
||||
color: #10b981;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.message-content {
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #daa520, #d4af37);
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 14px 32px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
background-color: #f8f8f8;
|
||||
padding: 25px 30px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h1>💬 New Response on Your Ticket</h1>
|
||||
</div>
|
||||
|
||||
<div class="email-body">
|
||||
<p>Dear {{ user_name }},</p>
|
||||
|
||||
<p>Our support team has responded to your ticket.</p>
|
||||
|
||||
<div class="ticket-info">
|
||||
<strong>Ticket:</strong> {{ ticket_number }}<br>
|
||||
<strong>Subject:</strong> {{ title }}
|
||||
</div>
|
||||
|
||||
<div class="message-box">
|
||||
<div class="message-author">{{ message_author }} replied:</div>
|
||||
<div class="message-content">{{ message }}</div>
|
||||
<div style="margin-top: 15px; color: #666; font-size: 13px;">{{ created_at }}</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{ support_url }}" class="cta-button">View Full Conversation</a>
|
||||
</div>
|
||||
|
||||
<p style="color: #666;">
|
||||
You can view the complete ticket history and reply to this message in the Support Center.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
<strong>GNX Software Solutions</strong><br>
|
||||
Enterprise Support Center
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 12px;">
|
||||
This is an automated notification. For assistance, visit our <a href="{{ support_url }}" style="color: #daa520;">Support Center</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
NEW RESPONSE ON YOUR TICKET
|
||||
============================================================
|
||||
|
||||
Dear {{ user_name }},
|
||||
|
||||
Our support team has responded to your ticket.
|
||||
|
||||
Ticket: {{ ticket_number }}
|
||||
Subject: {{ title }}
|
||||
|
||||
------------------------------------------------------------
|
||||
{{ message_author }} replied:
|
||||
------------------------------------------------------------
|
||||
|
||||
{{ message }}
|
||||
|
||||
{{ created_at }}
|
||||
|
||||
------------------------------------------------------------
|
||||
|
||||
View the full conversation and reply:
|
||||
{{ support_url }}
|
||||
|
||||
============================================================
|
||||
GNX Software Solutions - Enterprise Support Center
|
||||
|
||||
This is an automated notification.
|
||||
For assistance, visit our Support Center at {{ support_url }}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>New Support Ticket</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 700px;
|
||||
margin: 20px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.email-header {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
color: #ffffff;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.email-header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.alert-badge {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.email-body {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.ticket-number-box {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(239, 68, 68, 0.05) 100%);
|
||||
border-left: 4px solid #ef4444;
|
||||
padding: 20px;
|
||||
margin: 25px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ticket-number {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
font-family: 'Courier New', monospace;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.customer-info {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
margin: 25px 0;
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #555555;
|
||||
}
|
||||
.info-value {
|
||||
color: #333333;
|
||||
}
|
||||
.priority-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.priority-high {
|
||||
background-color: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
.priority-medium {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
.priority-low {
|
||||
background-color: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
.priority-critical {
|
||||
background-color: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #0f172a, #1e293b);
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 14px 32px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
background-color: #f8f8f8;
|
||||
padding: 25px 30px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
.description-box {
|
||||
background-color: #f8f8f8;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #daa520;
|
||||
margin: 20px 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<!-- Header -->
|
||||
<div class="email-header">
|
||||
<h1>🎫 New Support Ticket</h1>
|
||||
<div class="alert-badge">Requires Attention</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="email-body">
|
||||
<p><strong>A new support ticket has been submitted and requires your attention.</strong></p>
|
||||
|
||||
<!-- Ticket Number Box -->
|
||||
<div class="ticket-number-box">
|
||||
<div style="font-size: 14px; color: #666; margin-bottom: 8px;">Ticket Number</div>
|
||||
<div class="ticket-number">{{ ticket_number }}</div>
|
||||
<div style="margin-top: 15px;">
|
||||
<span class="info-label">Created:</span> {{ created_at }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer Information -->
|
||||
<div class="customer-info">
|
||||
<h3 style="margin-top: 0; color: #0f172a;">Customer Information</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-label">Name:</div>
|
||||
<div class="info-value">{{ user_name }}</div>
|
||||
|
||||
<div class="info-label">Email:</div>
|
||||
<div class="info-value"><a href="mailto:{{ user_email }}" style="color: #3b82f6;">{{ user_email }}</a></div>
|
||||
|
||||
{% if user_phone %}
|
||||
<div class="info-label">Phone:</div>
|
||||
<div class="info-value"><a href="tel:{{ user_phone }}" style="color: #3b82f6;">{{ user_phone }}</a></div>
|
||||
{% endif %}
|
||||
|
||||
{% if company %}
|
||||
<div class="info-label">Company:</div>
|
||||
<div class="info-value">{{ company }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Details -->
|
||||
<div style="margin: 25px 0;">
|
||||
<h3 style="color: #0f172a;">Ticket Details</h3>
|
||||
|
||||
<div style="margin: 15px 0;">
|
||||
<div class="info-label" style="display: block; margin-bottom: 8px;">Subject:</div>
|
||||
<div style="font-size: 18px; font-weight: 600; color: #0f172a;">{{ title }}</div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid" style="margin-top: 20px;">
|
||||
<div class="info-label">Type:</div>
|
||||
<div class="info-value">{{ ticket_type }}</div>
|
||||
|
||||
<div class="info-label">Category:</div>
|
||||
<div class="info-value">{{ category }}</div>
|
||||
|
||||
<div class="info-label">Priority:</div>
|
||||
<div>
|
||||
{% if priority == 'Critical' %}
|
||||
<span class="priority-badge priority-critical">{{ priority }}</span>
|
||||
{% elif priority == 'High' %}
|
||||
<span class="priority-badge priority-high">{{ priority }}</span>
|
||||
{% elif priority == 'Low' %}
|
||||
<span class="priority-badge priority-low">{{ priority }}</span>
|
||||
{% else %}
|
||||
<span class="priority-badge priority-medium">{{ priority }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="info-label">Status:</div>
|
||||
<div class="info-value">{{ status }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<h3 style="color: #0f172a;">Description</h3>
|
||||
<div class="description-box">{{ description }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div style="text-align: center; margin: 40px 0 20px;">
|
||||
<a href="{{ admin_url }}" class="cta-button" style="margin-right: 10px;">View in Admin Panel</a>
|
||||
<a href="mailto:{{ user_email }}" class="cta-button" style="background: linear-gradient(135deg, #daa520, #d4af37);">Reply to Customer</a>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; border-radius: 4px; margin-top: 30px;">
|
||||
<strong>⚠️ Action Required</strong><br>
|
||||
Please review and respond to this ticket according to the SLA requirements for {{ priority }} priority tickets.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="footer">
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
<strong>GNX Software Solutions</strong><br>
|
||||
Internal Support Notification
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 12px;">
|
||||
This is an automated notification for new support tickets.<br>
|
||||
Manage tickets in the <a href="{{ admin_url }}" style="color: #3b82f6;">Admin Panel</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
NEW SUPPORT TICKET - REQUIRES ATTENTION
|
||||
======================================================================
|
||||
|
||||
A new support ticket has been submitted and requires your attention.
|
||||
|
||||
TICKET NUMBER: {{ ticket_number }}
|
||||
Created: {{ created_at }}
|
||||
|
||||
----------------------------------------------------------------------
|
||||
CUSTOMER INFORMATION
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Name: {{ user_name }}
|
||||
Email: {{ user_email }}
|
||||
{% if user_phone %}Phone: {{ user_phone }}{% endif %}
|
||||
{% if company %}Company: {{ company }}{% endif %}
|
||||
|
||||
----------------------------------------------------------------------
|
||||
TICKET DETAILS
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Subject: {{ title }}
|
||||
Type: {{ ticket_type }}
|
||||
Category: {{ category }}
|
||||
Priority: {{ priority }}
|
||||
Status: {{ status }}
|
||||
|
||||
----------------------------------------------------------------------
|
||||
DESCRIPTION
|
||||
----------------------------------------------------------------------
|
||||
|
||||
{{ description }}
|
||||
|
||||
----------------------------------------------------------------------
|
||||
ACTION REQUIRED
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Please review and respond to this ticket according to the SLA requirements for {{ priority }} priority tickets.
|
||||
|
||||
View ticket in admin panel:
|
||||
{{ admin_url }}
|
||||
|
||||
Reply to customer:
|
||||
mailto:{{ user_email }}
|
||||
|
||||
======================================================================
|
||||
GNX Software Solutions - Internal Support Notification
|
||||
|
||||
This is an automated notification for new support tickets.
|
||||
|
||||
132
backEnd/support/templates/support/ticket_status_update.html
Normal file
132
backEnd/support/templates/support/ticket_status_update.html
Normal file
@@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ticket Status Updated</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.email-header {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
color: #ffffff;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.email-header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.email-body {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.status-change-box {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 25px;
|
||||
margin: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin: 0 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.arrow {
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.ticket-info {
|
||||
background: linear-gradient(135deg, rgba(218, 165, 32, 0.1) 0%, rgba(218, 165, 32, 0.05) 100%);
|
||||
border-left: 4px solid #daa520;
|
||||
padding: 20px;
|
||||
margin: 25px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #daa520, #d4af37);
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 14px 32px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
background-color: #f8f8f8;
|
||||
padding: 25px 30px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h1>🔔 Ticket Status Updated</h1>
|
||||
</div>
|
||||
|
||||
<div class="email-body">
|
||||
<p>Dear {{ user_name }},</p>
|
||||
|
||||
<p>Your support ticket status has been updated.</p>
|
||||
|
||||
<div class="ticket-info">
|
||||
<strong>Ticket:</strong> {{ ticket_number }}<br>
|
||||
<strong>Subject:</strong> {{ title }}
|
||||
</div>
|
||||
|
||||
<div class="status-change-box">
|
||||
<h3 style="margin-top: 0;">Status Change</h3>
|
||||
<div style="display: flex; align-items: center; justify-content: center; flex-wrap: wrap;">
|
||||
<span class="status-badge" style="background-color: #94a3b8;">{{ old_status }}</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="status-badge" style="background-color: {{ status_color }};">{{ new_status }}</span>
|
||||
</div>
|
||||
<p style="margin-top: 15px; color: #666; font-size: 14px;">Updated: {{ updated_at }}</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{ support_url }}" class="cta-button">View Ticket Details</a>
|
||||
</div>
|
||||
|
||||
<p style="color: #666;">
|
||||
You can check the full details of your ticket and any new messages in the Support Center.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
<strong>GNX Software Solutions</strong><br>
|
||||
Enterprise Support Center
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 12px;">
|
||||
This is an automated notification. For assistance, visit our <a href="{{ support_url }}" style="color: #daa520;">Support Center</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
29
backEnd/support/templates/support/ticket_status_update.txt
Normal file
29
backEnd/support/templates/support/ticket_status_update.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
TICKET STATUS UPDATED
|
||||
============================================================
|
||||
|
||||
Dear {{ user_name }},
|
||||
|
||||
Your support ticket status has been updated.
|
||||
|
||||
Ticket: {{ ticket_number }}
|
||||
Subject: {{ title }}
|
||||
|
||||
------------------------------------------------------------
|
||||
STATUS CHANGE
|
||||
------------------------------------------------------------
|
||||
|
||||
{{ old_status }} → {{ new_status }}
|
||||
|
||||
Updated: {{ updated_at }}
|
||||
|
||||
------------------------------------------------------------
|
||||
|
||||
You can check the full details of your ticket and any new messages in the Support Center:
|
||||
{{ support_url }}
|
||||
|
||||
============================================================
|
||||
GNX Software Solutions - Enterprise Support Center
|
||||
|
||||
This is an automated notification.
|
||||
For assistance, visit our Support Center at {{ support_url }}
|
||||
|
||||
3
backEnd/support/tests.py
Normal file
3
backEnd/support/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
17
backEnd/support/urls.py
Normal file
17
backEnd/support/urls.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from . import views
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'tickets', views.SupportTicketViewSet, basename='ticket')
|
||||
router.register(r'categories', views.TicketCategoryViewSet, basename='category')
|
||||
router.register(r'statuses', views.TicketStatusViewSet, basename='status')
|
||||
router.register(r'priorities', views.TicketPriorityViewSet, basename='priority')
|
||||
router.register(r'knowledge-base-categories', views.KnowledgeBaseCategoryViewSet, basename='kb-category')
|
||||
router.register(r'knowledge-base', views.KnowledgeBaseArticleViewSet, basename='kb-article')
|
||||
router.register(r'settings', views.SupportSettingsViewSet, basename='settings')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
216
backEnd/support/views.py
Normal file
216
backEnd/support/views.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from rest_framework import viewsets, status, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import (
|
||||
SupportTicket, TicketStatus, TicketPriority, TicketCategory,
|
||||
TicketMessage, KnowledgeBaseCategory, KnowledgeBaseArticle, SupportSettings
|
||||
)
|
||||
from .serializers import (
|
||||
SupportTicketSerializer, SupportTicketCreateSerializer,
|
||||
TicketStatusSerializer, TicketPrioritySerializer, TicketCategorySerializer,
|
||||
TicketMessageSerializer, TicketStatusCheckSerializer,
|
||||
KnowledgeBaseCategorySerializer, KnowledgeBaseArticleListSerializer,
|
||||
KnowledgeBaseArticleDetailSerializer, SupportSettingsSerializer
|
||||
)
|
||||
|
||||
|
||||
class SupportTicketViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for managing support tickets.
|
||||
Public endpoint for creating tickets and checking status.
|
||||
"""
|
||||
queryset = SupportTicket.objects.all()
|
||||
permission_classes = [AllowAny]
|
||||
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
|
||||
search_fields = ['ticket_number', 'title', 'user_email', 'user_name']
|
||||
ordering_fields = ['created_at', 'updated_at', 'priority']
|
||||
ordering = ['-created_at']
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create':
|
||||
return SupportTicketCreateSerializer
|
||||
return SupportTicketSerializer
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='check-status')
|
||||
def check_status(self, request):
|
||||
"""
|
||||
Check the status of a ticket by ticket number.
|
||||
POST /api/support/tickets/check-status/
|
||||
Body: {"ticket_number": "TKT-20231015-XXXXX"}
|
||||
"""
|
||||
serializer = TicketStatusCheckSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
ticket_number = serializer.validated_data['ticket_number']
|
||||
|
||||
try:
|
||||
ticket = SupportTicket.objects.get(ticket_number=ticket_number)
|
||||
ticket_serializer = SupportTicketSerializer(ticket)
|
||||
return Response(ticket_serializer.data)
|
||||
except SupportTicket.DoesNotExist:
|
||||
return Response(
|
||||
{'error': 'Ticket not found. Please check your ticket number.'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='add-message')
|
||||
def add_message(self, request, pk=None):
|
||||
"""
|
||||
Add a message to a ticket.
|
||||
POST /api/support/tickets/{id}/add-message/
|
||||
Body: {
|
||||
"content": "Message content",
|
||||
"author_name": "User Name",
|
||||
"author_email": "user@example.com"
|
||||
}
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
|
||||
message_data = {
|
||||
'ticket': ticket.id,
|
||||
'content': request.data.get('content'),
|
||||
'author_name': request.data.get('author_name', ticket.user_name),
|
||||
'author_email': request.data.get('author_email', ticket.user_email),
|
||||
'message_type': 'user_message'
|
||||
}
|
||||
|
||||
serializer = TicketMessageSerializer(data=message_data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
# Update ticket's last activity
|
||||
ticket.last_activity = timezone.now()
|
||||
ticket.save()
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class TicketCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for ticket categories.
|
||||
Public read-only access to categories.
|
||||
"""
|
||||
queryset = TicketCategory.objects.filter(is_active=True)
|
||||
serializer_class = TicketCategorySerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
|
||||
class TicketStatusViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for ticket statuses.
|
||||
Public read-only access to statuses.
|
||||
"""
|
||||
queryset = TicketStatus.objects.filter(is_active=True)
|
||||
serializer_class = TicketStatusSerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
|
||||
class TicketPriorityViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for ticket priorities.
|
||||
Public read-only access to priorities.
|
||||
"""
|
||||
queryset = TicketPriority.objects.filter(is_active=True)
|
||||
serializer_class = TicketPrioritySerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
|
||||
class KnowledgeBaseCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for knowledge base categories.
|
||||
Public read-only access.
|
||||
"""
|
||||
queryset = KnowledgeBaseCategory.objects.filter(is_active=True)
|
||||
serializer_class = KnowledgeBaseCategorySerializer
|
||||
permission_classes = [AllowAny]
|
||||
lookup_field = 'slug'
|
||||
|
||||
|
||||
class KnowledgeBaseArticleViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for knowledge base articles.
|
||||
Public read-only access to published articles.
|
||||
"""
|
||||
queryset = KnowledgeBaseArticle.objects.filter(is_published=True)
|
||||
permission_classes = [AllowAny]
|
||||
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
|
||||
search_fields = ['title', 'content', 'summary', 'keywords']
|
||||
ordering_fields = ['created_at', 'view_count', 'helpful_count']
|
||||
ordering = ['-created_at']
|
||||
lookup_field = 'slug'
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve':
|
||||
return KnowledgeBaseArticleDetailSerializer
|
||||
return KnowledgeBaseArticleListSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
# Increment view count
|
||||
instance.view_count += 1
|
||||
instance.save(update_fields=['view_count'])
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='mark-helpful')
|
||||
def mark_helpful(self, request, slug=None):
|
||||
"""
|
||||
Mark an article as helpful.
|
||||
POST /api/support/knowledge-base/{slug}/mark-helpful/
|
||||
Body: {"helpful": true/false}
|
||||
"""
|
||||
article = self.get_object()
|
||||
is_helpful = request.data.get('helpful', True)
|
||||
|
||||
if is_helpful:
|
||||
article.helpful_count += 1
|
||||
else:
|
||||
article.not_helpful_count += 1
|
||||
|
||||
article.save()
|
||||
|
||||
return Response({
|
||||
'helpful_count': article.helpful_count,
|
||||
'not_helpful_count': article.not_helpful_count
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='featured')
|
||||
def featured(self, request):
|
||||
"""
|
||||
Get featured articles.
|
||||
GET /api/support/knowledge-base/featured/
|
||||
"""
|
||||
featured_articles = self.queryset.filter(is_featured=True)[:6]
|
||||
serializer = self.get_serializer(featured_articles, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='by-category/(?P<category_slug>[^/.]+)')
|
||||
def by_category(self, request, category_slug=None):
|
||||
"""
|
||||
Get articles by category slug.
|
||||
GET /api/support/knowledge-base/by-category/{category_slug}/
|
||||
"""
|
||||
articles = self.queryset.filter(category__slug=category_slug)
|
||||
page = self.paginate_queryset(articles)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(articles, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class SupportSettingsViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for support settings.
|
||||
Public read-only access to active settings.
|
||||
"""
|
||||
queryset = SupportSettings.objects.filter(is_active=True)
|
||||
serializer_class = SupportSettingsSerializer
|
||||
permission_classes = [AllowAny]
|
||||
lookup_field = 'setting_name'
|
||||
Reference in New Issue
Block a user