Support Ticket System — Design Specification
DerinDate: 2026-04-01 Status: Approved Approach: Monolithic — fully integrated into existing Next.js application
1. Overview
Bölüm başlığı “1. Overview”A ticket-based support system between customers and admins within the ECU Tuning Portal. Both customers and admins can create tickets. The system includes admin-managed categories, priority levels, file attachments, email + in-app notifications, and optional relation to existing resources (File, Invoice, DevelopmentRequest).
2. Requirements Summary
Bölüm başlığı “2. Requirements Summary”| Area | Decision |
|---|---|
| Type | Ticket-based support (Zendesk-style) |
| Who can create | Both customers and admins |
| Categories | Admin-managed CRUD (multilingual JSON names) |
| Priority | Low / Normal / High / Urgent |
| File attachments | Both customer and admin can attach files to messages |
| Notifications | Email + in-app (bell icon + badge) |
| Resource linking | Optional: File, Invoice, DevelopmentRequest |
| Assignment | No assignment — all admins see all tickets |
3. Database Models (Prisma)
Bölüm başlığı “3. Database Models (Prisma)”3.1 TicketCategory
Bölüm başlığı “3.1 TicketCategory”Admin-managed category table with multilingual names.
model TicketCategory { id String @id @default(cuid()) name Json // {"tr": "Teknik Destek", "en": "Technical Support", ...} slug String @unique color String? // Badge color hex: "#ef4444" sortOrder Int @default(0) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
tickets Ticket[]}3.2 Ticket
Bölüm başlığı “3.2 Ticket”Main ticket record with status workflow, priority, and optional resource relations.
model Ticket { id String @id @default(cuid()) ticketNumber String @unique // Auto-generated: "TKT-00001" subject String status String @default("open") // open, awaiting_customer, awaiting_admin, resolved, closed priority String @default("normal") // low, normal, high, urgent
customerId String createdById String // ID of the person who created the ticket createdByType String // "customer" | "admin"
categoryId String?
relatedFileId String? relatedInvoiceId String? relatedDevRequestId String?
lastMessageAt DateTime @default(now()) closedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
messages TicketMessage[] notifications TicketNotification[]
customer Customer @relation(fields: [customerId], references: [id]) category TicketCategory? @relation(fields: [categoryId], references: [id]) relatedFile File? @relation(fields: [relatedFileId], references: [id]) relatedInvoice Invoice? @relation(fields: [relatedInvoiceId], references: [id]) relatedDevRequest DevelopmentRequest? @relation(fields: [relatedDevRequestId], references: [id])}Status flow:
open --> awaiting_customer (admin replied) --> awaiting_admin (customer replied) --> resolved (marked as resolved by either side) --> closed (after resolved, or direct close)3.3 TicketMessage
Bölüm başlığı “3.3 TicketMessage”Individual messages within a ticket thread.
model TicketMessage { id String @id @default(cuid()) ticketId String content String senderType String // "customer" | "admin" senderId String // Customer.id or AdminUser.id isInternal Boolean @default(false) // Admin-only internal note createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
ticket Ticket @relation(fields: [ticketId], references: [id]) attachments TicketAttachment[]}3.4 TicketAttachment
Bölüm başlığı “3.4 TicketAttachment”Files attached to messages.
model TicketAttachment { id String @id @default(cuid()) messageId String fileName String // Original file name filePath String // Server storage path fileSize Int // Bytes mimeType String createdAt DateTime @default(now())
message TicketMessage @relation(fields: [messageId], references: [id])}3.5 TicketNotification
Bölüm başlığı “3.5 TicketNotification”In-app notification records.
model TicketNotification { id String @id @default(cuid()) ticketId String recipientType String // "customer" | "admin" recipientId String type String // "new_ticket" | "new_message" | "status_changed" | "ticket_closed" title String body String isRead Boolean @default(false) createdAt DateTime @default(now())
ticket Ticket @relation(fields: [ticketId], references: [id])}4. API Routes
Bölüm başlığı “4. API Routes”4.1 Admin API
Bölüm başlığı “4.1 Admin API”GET /api/admin/tickets → List all tickets (filter: status, priority, category, customer, search)POST /api/admin/tickets → Create ticket on behalf of customerGET /api/admin/tickets/[id] → Ticket detail + messagesPATCH /api/admin/tickets/[id] → Update ticket (status, priority, category)POST /api/admin/tickets/[id]/messages → Send message (isInternal optional)POST /api/admin/tickets/[id]/close → Close ticketGET /api/admin/tickets/stats → Dashboard stats (open, pending, resolved counts)
GET /api/admin/ticket-categories → List all categoriesPOST /api/admin/ticket-categories → Create categoryPATCH /api/admin/ticket-categories/[id] → Update categoryDELETE /api/admin/ticket-categories/[id] → Soft delete (isActive=false)
GET /api/admin/notifications → Admin notifications (with unread count)PATCH /api/admin/notifications/[id]/read → Mark single as readPATCH /api/admin/notifications/read-all → Mark all as read4.2 Customer API
Bölüm başlığı “4.2 Customer API”GET /api/customer/tickets → Customer's own ticketsPOST /api/customer/tickets → Create new ticketGET /api/customer/tickets/[id] → Ticket detail + messages (isInternal=false only)POST /api/customer/tickets/[id]/messages → Send messagePOST /api/customer/tickets/[id]/close → Close own ticket
GET /api/customer/notifications → Customer notificationsPATCH /api/customer/notifications/[id]/read → Mark single as readPATCH /api/customer/notifications/read-all → Mark all as read4.3 File Upload
Bölüm başlığı “4.3 File Upload”POST /api/upload/ticket-attachment → Upload file (multipart/form-data)4.4 Attachment Download
Bölüm başlığı “4.4 Attachment Download”GET /api/admin/tickets/[id]/attachments/[attachmentId] → Download (with auth check)GET /api/customer/tickets/[id]/attachments/[attachmentId] → Download (with auth + ownership check)5. Page Structure
Bölüm başlığı “5. Page Structure”5.1 Admin Panel Pages
Bölüm başlığı “5.1 Admin Panel Pages”/[locale]/admin/(panel)/tickets/page.tsx → Ticket list (table with filters, search, sorting)/[locale]/admin/(panel)/tickets/[id]/page.tsx → Ticket detail (message thread + status controls)/[locale]/admin/(panel)/ticket-categories/page.tsx → Category CRUD (inline table or modal)/[locale]/admin/(panel)/dashboard/page.tsx → (existing) + Ticket summary widget added5.2 Customer Portal Pages
Bölüm başlığı “5.2 Customer Portal Pages”/[locale]/customer/(portal)/tickets/page.tsx → Customer's ticket list/[locale]/customer/(portal)/tickets/[id]/page.tsx → Ticket detail (message thread)/[locale]/customer/(portal)/tickets/new/page.tsx → New ticket form/[locale]/customer/(portal)/dashboard/page.tsx → (existing) + Ticket summary widget added6. UI Components
Bölüm başlığı “6. UI Components”/components/tickets/├── TicketList.tsx → Table: ticketNumber, subject, status badge, priority badge, category, date├── TicketDetail.tsx → Message thread + reply form + status controls├── TicketMessageBubble.tsx → Single message bubble (admin/customer different colors)├── TicketForm.tsx → New ticket form (subject, category, priority, related resource, message, file)├── TicketStatusBadge.tsx → Color-coded status badge├── TicketPriorityBadge.tsx → Color-coded priority badge├── TicketFilters.tsx → Admin filter bar (status, priority, category, search)├── TicketAttachmentUpload.tsx → File upload dropzone├── TicketRelationSelect.tsx → Related File/Invoice/DevRequest picker (search + dropdown)└── InternalNoteToggle.tsx → Admin: "Internal note" toggle switch
/components/notifications/├── NotificationBell.tsx → Header bell icon + unread count badge├── NotificationDropdown.tsx → Dropdown notification list on click└── NotificationItem.tsx → Single notification row (icon, title, time, read/unread)6.1 Design Language
Bölüm başlığı “6.1 Design Language”Following existing admin panel glass morphism + dark theme:
- Message bubbles: Admin messages left-aligned (brand-panel background), customer messages right-aligned (brand-red tint)
- Internal notes: Amber/yellow border, “Internal Note” label — visible only to admins
- Status badges:
open→ blue,awaiting_customer→ orange,awaiting_admin→ purple,resolved→ green,closed→ gray - Priority badges:
low→ gray,normal→ blue,high→ orange,urgent→ red with pulse animation - Notification bell: Right side of header, red badge + pulse animation when unread exists
7. Notification System
Bölüm başlığı “7. Notification System”7.1 In-App Notifications (Polling)
Bölüm başlığı “7.1 In-App Notifications (Polling)”- NotificationBell component polls
GET /api/[role]/notifications?unreadOnly=trueevery 30 seconds - First load fetches immediately, then interval starts
- Click on bell opens dropdown with last 20 notifications
- Click on notification navigates to the related ticket + marks as read
7.2 Notification Trigger Rules
Bölüm başlığı “7.2 Notification Trigger Rules”| Event | Notified | Type |
|---|---|---|
| Customer creates ticket | All admins | new_ticket |
| Admin creates ticket (on behalf) | Related customer | new_ticket |
| Customer sends message | All admins | new_message |
| Admin sends message (not internal) | Related customer | new_message |
| Ticket status changes | Related customer | status_changed |
| Ticket closed | Other party | ticket_closed |
Internal notes (isInternal=true) NEVER trigger customer notifications.
7.3 Email Notifications
Bölüm başlığı “7.3 Email Notifications”New email templates using existing @react-email/components + base-layout.tsx:
/lib/mail/templates/├── ticket-new.tsx → "Your support ticket has been created: TKT-00042"├── ticket-reply.tsx → "New reply on your support ticket: TKT-00042"├── ticket-status-changed.tsx → "Your ticket status has been updated: TKT-00042"└── ticket-closed.tsx → "Support ticket closed: TKT-00042"Each template:
- Uses existing
base-layout.tsx(co-branded header/footer) - Translated to customer’s locale via
next-intl - Includes ticket number, subject, message preview, and “View Ticket” CTA button
- Applies existing anti-spam headers
7.4 Notification Service
Bölüm başlığı “7.4 Notification Service”/lib/ticket-notifications.ts├── createTicketNotification() → Writes TicketNotification to DB + sends email├── notifyNewTicket() → New ticket notification├── notifyNewMessage() → New message notification├── notifyStatusChange() → Status change notification└── notifyTicketClosed() → Closure notification8. Security
Bölüm başlığı “8. Security”8.1 Authorization
Bölüm başlığı “8.1 Authorization”- Admin routes protected by existing
requireAdmin()middleware - Customer routes protected by existing
validateCustomerSession() - Customer can only access own tickets —
customerIdfilter enforced on every query isInternal=truemessages filtered out at query level for customer API
8.2 File Upload Security
Bölüm başlığı “8.2 File Upload Security”- Max file size: 10MB
- Allowed MIME types:
image/*,application/pdf,.bin,.zip,.rar,.7z - File names sanitized (special characters removed)
- Files stored in
/uploads/tickets/{ticketId}/— not publicly accessible - File downloads served through API with auth check
8.3 Rate Limiting
Bölüm başlığı “8.3 Rate Limiting”- Ticket creation: max 10 per customer per hour
- Message sending: max 5 per person per minute
- Uses existing in-memory rate limiter pattern
8.4 Input Validation
Bölüm başlığı “8.4 Input Validation”subject: max 200 characterscontent(message body): max 5000 characters- XSS protection: React escapes content by default on render
9. i18n
Bölüm başlığı “9. i18n”Uses existing next-intl infrastructure across all 24 supported locales.
Translation keys added to /messages/{locale}.json:
{ "tickets": { "title": "Support Tickets", "newTicket": "New Ticket", "subject": "Subject", "status": { "open": "Open", "awaiting_customer": "Awaiting Your Reply", "awaiting_admin": "Awaiting Support Reply", "resolved": "Resolved", "closed": "Closed" }, "priority": { "low": "Low", "normal": "Normal", "high": "High", "urgent": "Urgent" }, "messages": { "reply": "Reply", "internalNote": "Internal Note", "sendMessage": "Send Message", "attachFile": "Attach File" }, "notifications": { "newTicket": "New support ticket", "newMessage": "New reply on ticket", "statusChanged": "Ticket status updated", "ticketClosed": "Ticket closed" } }}- Category names stored as JSON with per-locale translations
- Email templates rendered in customer’s
locale - Admin panel uses admin’s browser locale
10. File Storage Structure
Bölüm başlığı “10. File Storage Structure”/uploads/└── tickets/ └── {ticketId}/ └── {messageId}/ ├── screenshot.png ├── ecufile.bin └── ...11. Ticket Number Generation
Bölüm başlığı “11. Ticket Number Generation”Auto-incrementing format: TKT-00001, TKT-00002, etc.
Implementation: Query the latest ticket’s ticketNumber, parse the numeric part, increment by 1, and pad to 5 digits. Use a database transaction to prevent race conditions.