"use client"; import { useEffect, useMemo, useState } from "react"; import { useTickets, useAvailability, useSdk, useFeatureFlags, type Ticket, type TicketState, type TicketPriority, } from "@lynkd/messaging-inbox-sdk"; import { useUi } from "@/lib/ui-state"; import { Header } from "@/components/nav/Header"; import { Avatar, Chip } from "@/components/ui/primitives"; import { RichText } from "@/lib/rich-text"; import { clockTime, relativeTime } from "@/lib/format"; import clsx from "clsx"; import { LifeBuoy, AlertTriangle, Circle, Send, ArrowUpCircle, CheckCircle2, PhoneCall, Tag, Clock, Search, ChevronLeft, } from "lucide-react"; const stateTone: Record[0]["tone"]> = { NEW: "info", OPEN: "accent", PENDING: "warning", RESOLVED: "success", CLOSED: "neutral", }; const priTone: Record[0]["tone"]> = { LOW: "neutral", NORMAL: "info", HIGH: "warning", URGENT: "danger", }; export function SupportView({ initialTicketId }: { initialTicketId?: string }) { const { tickets, loading, patch, reply } = useTickets("all"); const { toast, openModal } = useUi(); const [selected, setSelected] = useState(initialTicketId ?? null); const [scope, setScope] = useState("ALL"); const [q, setQ] = useState(""); useEffect(() => { if (initialTicketId) setSelected(initialTicketId); }, [initialTicketId]); // Desktop shows both panes, so default-select the first ticket. On mobile, // don't auto-reselect — that would defeat the "Back to list" button. useEffect(() => { if (!selected && tickets.length && typeof window !== "undefined" && window.matchMedia("(min-width: 768px)").matches) { setSelected(tickets[0].id); } }, [tickets, selected]); const filtered = useMemo( () => tickets.filter((t) => (scope === "ALL" || t.state === scope) && (!q || t.subject.toLowerCase().includes(q.toLowerCase()) || t.ref.toLowerCase().includes(q.toLowerCase()))), [tickets, scope, q], ); const selectedTicket = tickets.find((t) => t.id === selected) ?? null; const open = tickets.filter((t) => t.state === "OPEN" || t.state === "NEW").length; const breached = tickets.filter((t) => t.slaBreached).length; const resolved7d = tickets.filter((t) => (t.state === "RESOLVED" || t.state === "CLOSED")).length; return (
{/* list */}
setQ(e.target.value)} placeholder="Search tickets" className="w-full bg-transparent text-[13px] outline-none placeholder:text-dim" />
{(["ALL", "NEW", "OPEN", "PENDING", "RESOLVED", "CLOSED"] as const).map((s) => ( ))}
{loading &&
Loading…
} {filtered.map((t) => setSelected(t.id)} />)} {!loading && filtered.length === 0 &&
No tickets match.
}
{/* detail */}
{selectedTicket ? ( setSelected(null)} onSetState={(s) => patch(selectedTicket.id, s)} onReply={(b) => reply(selectedTicket.id, b)} onEscalate={() => { patch(selectedTicket.id, "OPEN"); toast(`${selectedTicket.ref} escalated to Tier 2`, "success"); }} onCallback={() => openModal("callback")} /> ) : }
); } function TicketRow({ ticket, active, onClick }: { ticket: Ticket; active: boolean; onClick: () => void }) { const { userById } = useSdk(); const requester = userById(ticket.requesterId); return ( ); } function TicketDetail({ ticket, onBack, onSetState, onReply, onEscalate, onCallback, }: { ticket: Ticket; onBack: () => void; onSetState: (s: TicketState) => void; onReply: (body: string) => void; onEscalate: () => void; onCallback: () => void; }) { const { userById } = useSdk(); const flags = useFeatureFlags(); const [draft, setDraft] = useState(""); const requester = userById(ticket.requesterId); const assignee = ticket.assigneeId ? userById(ticket.assigneeId) : undefined; const send = () => { if (draft.trim()) { onReply(draft.trim()); setDraft(""); } }; return (
{ticket.ref} {ticket.state} {ticket.priority} {ticket.slaBreached && SLA breached}

{ticket.subject}

{requester && {requester.name}} {assignee && · Assigned to {assignee.name}} {ticket.category && {ticket.category}} · opened {relativeTime(ticket.createdTs)}
{flags.supportEscalate && } onSetState("RESOLVED")} /> onSetState("PENDING")} /> {flags.supportCallback && }
{ticket.messages?.map((m) => { const author = userById(m.authorId); const mine = m.authorId === "u_me"; return (
{author && }
{author?.name}{clockTime(m.ts)}
); })}
{ticket.state !== "CLOSED" && (