diff --git a/oudelaa_dashboard/app/(dashboard)/dashboard/support/page.tsx b/oudelaa_dashboard/app/(dashboard)/dashboard/support/page.tsx new file mode 100644 index 0000000..a217fc2 --- /dev/null +++ b/oudelaa_dashboard/app/(dashboard)/dashboard/support/page.tsx @@ -0,0 +1,470 @@ +"use client"; + +import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import Image from "next/image"; +import { ImageIcon, RefreshCcw, Send, UserRoundCheck } from "lucide-react"; + +import { NoPermissionState } from "@/components/auth/no-permission-state"; +import { useSuperAdminSession } from "@/components/auth/session-context"; +import { PageHeader } from "@/components/dashboard/page-header"; +import { PaginationControls } from "@/components/dashboard/pagination-controls"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { EmptyState } from "@/components/ui/empty-state"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Textarea } from "@/components/ui/textarea"; +import { useToast } from "@/components/ui/toast"; +import { getItems, getPagination } from "@/lib/api/core"; +import { + assignSupportTicket, + getSupportTicket, + listSupportTickets, + replyToSupportTicket, + updateSupportTicketStatus, +} from "@/lib/api/support"; +import { formatDateTime } from "@/lib/format"; +import { SUPERADMIN_PERMISSIONS, hasPermission } from "@/lib/permissions"; +import type { + ApiUser, + SupportTicket, + SupportTicketDetailResponse, + SupportTicketStatus, + SupportTicketsResponse, +} from "@/types/api"; + +const statuses: SupportTicketStatus[] = ["open", "pending", "in_progress", "resolved", "closed"]; +const categories = ["account", "technical", "media", "payment", "collaboration", "other"]; +const priorities = ["low", "normal", "high", "urgent"]; + +function userLabel(user?: ApiUser | null) { + if (!user) return "-"; + return user.stageName || user.name || user.username || user.email || user._id; +} + +function statusVariant(status: SupportTicketStatus): "success" | "muted" | "warning" | "danger" { + if (status === "resolved" || status === "closed") return "success"; + if (status === "in_progress") return "warning"; + if (status === "pending") return "muted"; + return "danger"; +} + +export default function SupportPage() { + const { permissions } = useSuperAdminSession(); + const canManageSupport = hasPermission(permissions, SUPERADMIN_PERMISSIONS.CASES_MANAGE); + const { toast } = useToast(); + + const [statusFilter, setStatusFilter] = useState("open"); + const [categoryFilter, setCategoryFilter] = useState("all"); + const [priorityFilter, setPriorityFilter] = useState("all"); + const [page, setPage] = useState(1); + const [response, setResponse] = useState(null); + const [selected, setSelected] = useState(null); + const [reply, setReply] = useState(""); + const [replyImage, setReplyImage] = useState(null); + const [assignAdminId, setAssignAdminId] = useState(""); + const [loading, setLoading] = useState(true); + const [updating, setUpdating] = useState(false); + const filtersRef = useRef({ statusFilter, categoryFilter, priorityFilter }); + + filtersRef.current = { statusFilter, categoryFilter, priorityFilter }; + + const loadTickets = useCallback(async () => { + if (!canManageSupport) { + setLoading(false); + return; + } + + setLoading(true); + try { + const current = filtersRef.current; + const tickets = await listSupportTickets({ + page, + limit: 15, + status: current.statusFilter === "all" ? undefined : current.statusFilter, + category: current.categoryFilter === "all" ? undefined : current.categoryFilter, + priority: current.priorityFilter === "all" ? undefined : current.priorityFilter, + sortOrder: "desc", + }); + setResponse(tickets); + const items = getItems(tickets) as SupportTicket[]; + const currentSelectedId = selected?.ticket._id; + const nextTicket = currentSelectedId + ? items.find((item) => item._id === currentSelectedId) ?? items[0] + : items[0]; + if (nextTicket) { + const detail = await getSupportTicket(nextTicket._id); + setSelected(detail); + setAssignAdminId(detail.ticket.assignedAdminId ?? ""); + } else { + setSelected(null); + setAssignAdminId(""); + } + } catch (error) { + toast({ title: "Failed to load support tickets", description: String(error), variant: "danger" }); + } finally { + setLoading(false); + } + }, [canManageSupport, page, selected?.ticket._id, toast]); + + useEffect(() => { + void loadTickets(); + }, [loadTickets]); + + const tickets = getItems(response) as SupportTicket[]; + const summary = useMemo( + () => ({ + open: tickets.filter((ticket) => ticket.status === "open").length, + inProgress: tickets.filter((ticket) => ticket.status === "in_progress").length, + resolved: tickets.filter((ticket) => ticket.status === "resolved").length, + closed: tickets.filter((ticket) => ticket.status === "closed").length, + }), + [tickets], + ); + + const selectTicket = async (ticketId: string) => { + setUpdating(true); + try { + const detail = await getSupportTicket(ticketId); + setSelected(detail); + setAssignAdminId(detail.ticket.assignedAdminId ?? ""); + } catch (error) { + toast({ title: "Failed to load ticket", description: String(error), variant: "danger" }); + } finally { + setUpdating(false); + } + }; + + const applyFilters = () => { + if (page === 1) { + void loadTickets(); + return; + } + setPage(1); + }; + + const changeStatus = async (status: SupportTicketStatus) => { + if (!selected) return; + setUpdating(true); + try { + await updateSupportTicketStatus(selected.ticket._id, status); + await selectTicket(selected.ticket._id); + await loadTickets(); + toast({ title: "Ticket status updated", description: status, variant: "success" }); + } catch (error) { + toast({ title: "Status update failed", description: String(error), variant: "danger" }); + } finally { + setUpdating(false); + } + }; + + const assignTicket = async () => { + if (!selected) return; + setUpdating(true); + try { + await assignSupportTicket(selected.ticket._id, assignAdminId.trim()); + await selectTicket(selected.ticket._id); + toast({ title: "Assignment updated", variant: "success" }); + } catch (error) { + toast({ title: "Assignment failed", description: String(error), variant: "danger" }); + } finally { + setUpdating(false); + } + }; + + const sendReply = async () => { + if (!selected || (!reply.trim() && !replyImage)) return; + setUpdating(true); + try { + await replyToSupportTicket(selected.ticket._id, reply.trim(), replyImage); + setReply(""); + setReplyImage(null); + await selectTicket(selected.ticket._id); + await loadTickets(); + toast({ title: "Reply sent", variant: "success" }); + } catch (error) { + toast({ title: "Reply failed", description: String(error), variant: "danger" }); + } finally { + setUpdating(false); + } + }; + + const onImageChange = (event: ChangeEvent) => { + setReplyImage(event.target.files?.[0] ?? null); + }; + + if (!canManageSupport) { + return ( +
+ + +
+ ); + } + + return ( +
+ void loadTickets()} disabled={loading}> + + Refresh + + } + /> + +
+ + + Open + + {summary.open} + + + + In progress + + {summary.inProgress} + + + + Resolved + + {summary.resolved} + + + + Closed + + {summary.closed} + +
+ + + + Filters + + + + + + + + + +
+ + + Ticket queue + + + {!tickets.length && !loading ? ( + + ) : ( + + + + User + Subject + Priority + Status + Last message + + + + {tickets.map((ticket) => ( + void selectTicket(ticket._id)} + > + +
{userLabel(ticket.user)}
+
{ticket.user?.email ?? ticket.userId}
+
+ +
{ticket.subject}
+
{ticket.category}
+
+ + + {ticket.priority} + + + + {ticket.status} + + {formatDateTime(ticket.lastMessageAt)} +
+ ))} +
+
+ )} + +
+
+ + + + Ticket details + + + {!selected ? ( + + ) : ( + <> +
+
+ {selected.ticket.user?.avatar ? ( + {userLabel(selected.ticket.user)} + ) : ( +
+ +
+ )} +
+
{userLabel(selected.ticket.user)}
+
{selected.ticket.user?.username ?? "-"}
+
{selected.ticket.user?.email ?? "-"}
+
+ + {selected.ticket.user?.isDisabled ? "disabled" : "active"} + +
+
+
{selected.ticket.subject}
+
+ {selected.ticket.category} / {selected.ticket.priority} / {formatDateTime(selected.ticket.createdAt)} +
+
+
+ +
+ +
+ setAssignAdminId(event.target.value)} + /> + +
+
+ +
+ {selected.messages.map((message) => ( +
+
+ {message.senderRole} + {formatDateTime(message.createdAt)} +
+ {message.message ?

{message.message}

: null} + {message.attachmentUrl ? ( + + Support attachment + + ) : null} +
+ ))} +
+ +
+