@@ -167,7 +205,7 @@ export function EscalationForm({
{callbackRequired && (
-
+
- ASAP
+ ASAP (Urgent)
Morning (9am - 12pm)
Afternoon (1pm - 5pm)
@@ -192,17 +230,19 @@ export function EscalationForm({
)}
+ {/* Assignee */}
@@ -210,9 +250,13 @@ export function EscalationForm({
-
diff --git a/src/features/users/components/tabs/SupportTab.tsx b/src/features/users/components/tabs/SupportTab.tsx
index 98c6d6c..e02b7d1 100644
--- a/src/features/users/components/tabs/SupportTab.tsx
+++ b/src/features/users/components/tabs/SupportTab.tsx
@@ -1,10 +1,9 @@
'use client';
-import { useTransition, useState } from 'react';
+import { useState } from 'react';
import type { User } from '@/lib/types/user';
import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
+import { Badge } from '@/components/ui/badge';
import { EscalationForm } from '../dialogs/EscalationForm';
import {
MessageSquare,
@@ -12,58 +11,175 @@ import {
Plus,
UserCircle2,
Headphones,
- Loader2
+ Loader2,
+ Phone,
+ AlertTriangle,
+ Shield,
} from 'lucide-react';
import { toast } from 'sonner';
+import { cn } from '@/lib/utils';
+import { formatDistanceToNow } from 'date-fns';
interface SupportTabProps {
user: User;
}
+// Mock Data for demonstration matching the "Ticket List" requirements
+const MOCK_TICKETS = [
+ {
+ id: 'tkt-001',
+ type: 'Refund',
+ subject: 'Refund requested for "Tech Summit"',
+ priority: 'High',
+ status: 'Open',
+ callbackRequired: true,
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(), // 2 hours ago
+ author: 'User',
+ },
+ {
+ id: 'tkt-002',
+ type: 'Technical',
+ subject: 'Login issue on mobile app',
+ priority: 'Normal',
+ status: 'Resolved',
+ callbackRequired: false,
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3).toISOString(), // 3 days ago
+ author: 'User',
+ },
+ {
+ id: 'fraud-001',
+ type: 'Fraud Alert',
+ subject: 'Suspicious login attempt detected',
+ priority: 'Critical',
+ status: 'Closed',
+ callbackRequired: false,
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5).toISOString(), // 5 days ago
+ author: 'System',
+ }
+];
+
export function SupportTab({ user }: SupportTabProps) {
const [createDialogOpen, setCreateDialogOpen] = useState(false);
+ const [tickets, setTickets] = useState(MOCK_TICKETS);
- // Filter state could go here in future
- // const [filter, setFilter] = useState('all');
+ const getPriorityColor = (priority: string) => {
+ switch (priority) {
+ case 'Critical': return 'bg-red-100 text-red-700 border-red-200';
+ case 'High': return 'bg-orange-100 text-orange-700 border-orange-200';
+ case 'Normal': return 'bg-blue-50 text-blue-700 border-blue-200';
+ default: return 'bg-slate-100 text-slate-600 border-slate-200';
+ }
+ };
+
+ const getTypeBadge = (type: string) => {
+ switch (type) {
+ case 'Refund': return
Refund;
+ case 'Technical': return
Tech;
+ case 'Fraud Alert': return
Fraud;
+ default: return
{type};
+ }
+ };
+
+ const handleTicketCreated = (newTicket: any) => {
+ setTickets([newTicket, ...tickets]);
+ };
+
+ const handleViewAllHistory = () => {
+ // In a real app, this might navigate to a dedicated page or load more items.
+ // For this mock, we'll simulate loading more history.
+ toast.info("Loading full ticket history...", {
+ duration: 1500,
+ });
+
+ // Simulate expanded view (optional enhancement)
+ // setLimit(50);
+ };
return (
-
+
-
Communication Timeline
+
+
Support & Escalations
+
Manage tickets and communication history.
+
+
+
View All History
+
setCreateDialogOpen(true)}>
+
+ Create Escalation
+
+
- {/* Timeline Stream */}
-
- {[
- { id: 1, type: 'ticket', title: 'Refund requested for "Tech Summit"', status: 'Open', date: '2 hours ago', icon: Headphones, color: 'text-blue-600 bg-blue-50' },
- { id: 2, type: 'email', title: 'Sent: "Your tickets are ready!"', status: 'Delivered', date: 'Yesterday', icon: Mail, color: 'text-slate-600 bg-slate-50' },
- { id: 3, type: 'ticket', title: 'Login issue reported', status: 'Resolved', date: '3 days ago', icon: MessageSquare, color: 'text-emerald-600 bg-emerald-50' },
- ].map((item) => (
-
-
-
-
-
-
-
{item.title}
-
{item.date}
+ {/* Ticket List / Timeline */}
+
+ {tickets.length > 0 ? (
+
+ {tickets.map((ticket) => (
+
+
+
+ {ticket.priority === 'Critical' ?
:
+ ticket.type === 'Fraud Alert' ?
:
+
}
+
+
+
+ {ticket.subject}
+ #{ticket.id}
+
+
+ {getTypeBadge(ticket.type)}
+
+ {ticket.priority}
+
+ • {ticket.status}
+ • {formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true })}
+
+
+
+
+
+ {ticket.callbackRequired && (
+
+ )}
+
+ View Details
+
+
-
-
- {item.type === 'ticket' ? 'Ticket #1234' : 'Email System'}
-
- • {item.status}
-
-
+ ))}
- ))}
+ ) : (
+ // Empty State
+
+
+
+
+
No active escalations
+
+ User is happy! There are no open tickets or escalations for this account.
+
+
+ )}
);
diff --git a/src/lib/actions/support.ts b/src/lib/actions/support.ts
new file mode 100644
index 0000000..25c738a
--- /dev/null
+++ b/src/lib/actions/support.ts
@@ -0,0 +1,80 @@
+
+import { z } from 'zod';
+import { createEscalationSchema } from '@/lib/validations/support';
+import { logAdminAction } from '@/lib/audit-logger';
+
+// Mock DB or Queue
+const MOCK_TICKET_DB: any[] = [];
+
+/**
+ * Creates a support escalation ticket.
+ * Simulates server-side logic including SLA routing and notifications.
+ */
+export async function createEscalation(data: z.infer
) {
+ try {
+ // 1. Validation
+ const validated = createEscalationSchema.safeParse(data);
+ if (!validated.success) {
+ return {
+ success: false,
+ message: "Validation failed: " + validated.error.errors.map(e => e.message).join(', ')
+ };
+ }
+
+ const input = validated.data;
+
+ // 2. Simulate Server Processing Delay
+ await new Promise(resolve => setTimeout(resolve, 800));
+
+ // 3. Auto-Routing Logic
+ let routedTo = input.assigneeId || 'support-general';
+ if (input.priority === 'Critical') {
+ // Logic: Critical tickets trigger PagerDuty/Slack
+ console.log(`[SERVER_ACT] 🚨 CRITICAL TICKET - Alerting #urgent-support webhook...`);
+ routedTo = 'incident-response-team';
+ } else if (input.type === 'Fraud Alert') {
+ routedTo = 'fraud-team';
+ }
+
+ // 4. Create Ticket Record
+ const ticketId = `tkt-${Math.floor(Math.random() * 10000)}`;
+ const newTicket = {
+ id: ticketId,
+ ...input,
+ status: 'Open',
+ assignedTo: routedTo,
+ createdAt: new Date().toISOString(),
+ };
+ MOCK_TICKET_DB.push(newTicket);
+
+ // 5. Handle Callback Queue
+ if (input.callbackRequired) {
+ console.log(`[SERVER_ACT] 📞 Adding ticket ${ticketId} to Callback Queue (Phone: ${input.callbackPhone})`);
+ }
+
+ // 6. Audit Logging
+ await logAdminAction({
+ actorId: 'current-admin-id', // In real app, get from session
+ action: 'escalation_created',
+ targetId: input.userId,
+ details: {
+ ticketId,
+ priority: input.priority,
+ type: input.type,
+ routedTo
+ }
+ });
+
+ return {
+ success: true,
+ message: input.priority === 'Critical'
+ ? "Critical Escalation Created & Alert Sent!"
+ : "Escalation ticket created successfully.",
+ ticket: { ...newTicket, author: 'Admin' }
+ };
+
+ } catch (error) {
+ console.error("createEscalation error:", error);
+ return { success: false, message: "Failed to create escalation." };
+ }
+}
diff --git a/src/lib/validations/support.ts b/src/lib/validations/support.ts
new file mode 100644
index 0000000..b5fbafc
--- /dev/null
+++ b/src/lib/validations/support.ts
@@ -0,0 +1,67 @@
+import { z } from 'zod';
+
+export const SupportTicketType = {
+ REFUND: 'Refund',
+ PAYMENT_FAILURE: 'Payment Failure',
+ ACCOUNT_ACCESS: 'Account access',
+ FRAUD_ALERT: 'Fraud Alert',
+ EVENT_ISSUE: 'Event issue',
+ OTHER: 'Other',
+} as const;
+
+export const SupportTicketPriority = {
+ LOW: 'Low',
+ MEDIUM: 'Medium',
+ HIGH: 'High',
+ CRITICAL: 'Critical',
+} as const;
+
+export const SupportTicketStatus = {
+ OPEN: 'Open',
+ IN_PROGRESS: 'In Progress',
+ RESOLVED: 'Resolved',
+ CLOSED: 'Closed',
+} as const;
+
+
+export const createEscalationSchema = z.object({
+ userId: z.string().min(1, "User ID is required"),
+ type: z.enum([
+ SupportTicketType.REFUND,
+ SupportTicketType.PAYMENT_FAILURE,
+ SupportTicketType.ACCOUNT_ACCESS,
+ SupportTicketType.FRAUD_ALERT,
+ SupportTicketType.EVENT_ISSUE,
+ SupportTicketType.OTHER
+ ]),
+ priority: z.enum([
+ SupportTicketPriority.LOW,
+ SupportTicketPriority.MEDIUM,
+ SupportTicketPriority.HIGH,
+ SupportTicketPriority.CRITICAL
+ ]),
+ subject: z.string().min(5, "Subject must be at least 5 characters"),
+ description: z.string().min(10, "Description must be at least 10 characters"),
+ callbackRequired: z.boolean().default(false),
+ callbackPhone: z.string().optional(),
+ callbackTime: z.string().optional(),
+ assigneeId: z.string().optional(),
+}).refine((data) => {
+ if (data.priority === SupportTicketPriority.CRITICAL && data.description.length < 20) {
+ return false;
+ }
+ return true;
+}, {
+ message: "Critical priority tickets require a description of at least 20 characters.",
+ path: ["description"],
+}).refine((data) => {
+ if (data.callbackRequired && (!data.callbackPhone || data.callbackPhone.length < 5)) {
+ return false;
+ }
+ return true;
+}, {
+ message: "Phone number is required when callback is requested.",
+ path: ["callbackPhone"],
+});
+
+export type CreateEscalationInput = z.infer;