PageSourceSearch

https://www.hamienet.com/assets/TicketSupport-BJw7vygr.js

js hamienet.com collected 2026-10-02 05:02:24 UTC 20,038 bytes, 333 lines download raw bytes

1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js";
2import { a as api } from "./index-w5lXls15.js";
3import { a as DataTable, b as Drawer } from "./AdminShared-BP_vxG_A.js";
4import { m as formatTicketDate } from "./utils-CEVb7nN1.js";
5import { dQ as LifeBuoy, b9 as Settings, cd as Save, bv as CircleCheckBig, b1 as CircleAlert, b5 as Trash2, bx as Send } from "./vendor-D0P4XgNj.js";
6import "./vendor-i18n-BC7ZMXWQ.js";
7const TicketSupportSettings = () => {
8  const [limit, setLimit] = reactExports.useState(3);
9  const [loading, setLoading] = reactExports.useState(true);
10  const [saving, setSaving] = reactExports.useState(false);
11  const [feedback, setFeedback] = reactExports.useState(null);
12  reactExports.useEffect(() => {
13    api.getSystemSetting("support_config").then((res) => {
14      if (res && res.value && res.value.max_open_tickets) {
15        setLimit(res.value.max_open_tickets);
16      }
17      setLoading(false);
18    });
19  }, []);
20  const handleSave = async () => {
21    setSaving(true);
22    setFeedback(null);
23    try {
24      await api.saveSystemSetting("support_config", { max_open_tickets: limit });
25      setFeedback({ type: "success", message: "Settings saved successfully" });
26      setTimeout(() => setFeedback(null), 3e3);
27    } catch (e) {
28      setFeedback({ type: "error", message: e.message || "Failed to save settings" });
29    } finally {
30      setSaving(false);
31    }
32  };
33  if (loading) return null;
34  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl p-4 mb-4 shadow-sm flex flex-wrap items-center justify-between gap-4", children: [
35    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
36      /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-2 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg text-indigo-600 dark:text-indigo-400 shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 18 }) }),
37      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
38        /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-sm font-bold text-zinc-900 dark:text-white", children: "Support Configuration" }),
39        /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500", children: "Manage global ticket limits and support behavior." })
40      ] })
41    ] }),
42    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 flex-wrap", children: [
43      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
44        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-500 uppercase tracking-wider whitespace-nowrap", children: "Max Open Tickets:" }),
45        /* @__PURE__ */ jsxRuntimeExports.jsx(
46          "input",
47          {
48            type: "number",
49            min: "1",
50            max: "50",
51            value: limit,
52            onChange: (e) => setLimit(parseInt(e.target.value) || 1),
53            className: "w-20 bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 rounded-lg px-3 py-1.5 outline-none focus:ring-2 focus:ring-indigo-500 text-zinc-900 dark:text-white transition-all text-sm text-center"
54          }
55        ),
56        /* @__PURE__ */ jsxRuntimeExports.jsxs(
57          "button",
58          {
59            onClick: handleSave,
60            disabled: saving,
61            className: "px-4 py-1.5 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all shadow-md shadow-indigo-500/10",
62            children: [
63              saving ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { size: 14 }),
64              "Save"
65            ]
66          }
67        )
68      ] }),
69      feedback && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center gap-1.5 text-xs font-semibold animate-in fade-in slide-in-from-left-2 ${feedback.type === "success" ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400"}`, children: [
70        feedback.type === "success" ? /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheckBig, { size: 14 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 14 }),
71        feedback.message
72      ] })
73    ] })
74  ] });
75};
76const TicketChat = ({ ticket, onClose, onRefresh }) => {
77  const [currentTicket, setCurrentTicket] = reactExports.useState(ticket);
78  const [reply, setReply] = reactExports.useState("");
79  const [sendEmail, setSendEmail] = reactExports.useState(false);
80  const [sending, setSending] = reactExports.useState(false);
81  const [error, setError] = reactExports.useState(null);
82  const scrollRef = reactExports.useRef(null);
83  reactExports.useEffect(() => {
84    setCurrentTicket(ticket);
85  }, [ticket]);
86  reactExports.useEffect(() => {
87    if (scrollRef.current) {
88      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
89    }
90  }, [currentTicket == null ? void 0 : currentTicket.messages]);
91  const handleSend = async () => {
92    if (!reply || sending) return;
93    setSending(true);
94    setError(null);
95    try {
96      await api.replyToTicket(currentTicket.id, reply, sendEmail);
97      setReply("");
98      try {
99        const updated = await api.getTicketDetails(currentTicket.id);
100        setCurrentTicket(updated);
101        onRefresh(updated);
102      } catch (e) {
103        console.error("Failed to load updated ticket details", e);
104        onRefresh();
105      }
106    } catch (err) {
107      console.error("Failed to send ticket reply", err);
108      setError(err.message || "Failed to send reply");
109    } finally {
110      setSending(false);
111    }
112  };
113  const handleResolve = () => {
114    onClose();
115    api.resolveTicket(currentTicket.id).then(() => onRefresh());
116  };
117  if (!currentTicket) return null;
118  return /* @__PURE__ */ jsxRuntimeExports.jsx(Drawer, { isOpen: !!currentTicket, onClose, title: `Ticket #${currentTicket.id.split("-")[1]}`, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full", children: [
119    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4", children: [
120      /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-lg text-zinc-900 dark:text-white", children: currentTicket.subject }),
121      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-xs mt-1", children: [
122        /* @__PURE__ */ jsxRuntimeExports.jsxs(
123          "select",
124          {
125            value: currentTicket.status,
126            onChange: async (e) => {
127              const newStatus = e.target.value;
128              const previousStatus = currentTicket.status;
129              setCurrentTicket({ ...currentTicket, status: newStatus });
130              try {
131                await api.updateTicketStatus(currentTicket.id, newStatus);
132                const updated = await api.getTicketDetails(currentTicket.id);
133                setCurrentTicket(updated);
134                onRefresh(updated);
135              } catch (err) {
136                console.error("Failed to update ticket status", err);
137                setCurrentTicket({ ...currentTicket, status: previousStatus });
138                onRefresh();
139              }
140            },
141            className: `px-2 py-1 rounded-lg capitalize font-bold outline-none cursor-pointer border transition-colors ${currentTicket.status === "open" ? "bg-indigo-50 dark:bg-indigo-950/60 text-indigo-600 dark:text-indigo-400 border-indigo-200 dark:border-indigo-800" : currentTicket.status === "on hold" ? "bg-amber-50 dark:bg-amber-950/60 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-800" : "bg-zinc-100 dark:bg-zinc-800/60 text-zinc-600 dark:text-zinc-400 border-zinc-200 dark:border-zinc-700"}`,
142            children: [
143              /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "open", className: "bg-white dark:bg-zinc-900 text-indigo-600 dark:text-indigo-400 font-semibold", children: "open" }),
144              /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "on hold", className: "bg-white dark:bg-zinc-900 text-amber-600 dark:text-amber-400 font-semibold", children: "on hold" }),
145              /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "resolved", className: "bg-white dark:bg-zinc-900 text-zinc-600 dark:text-zinc-400 font-semibold", children: "resolved" })
146            ]
147          }
148        ),
149        /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-500", children: [
150          "via ",
151          currentTicket.userName
152        ] })
153      ] })
154    ] }),
155    /* @__PURE__ */ jsxRuntimeExports.jsx(
156      "div",
157      {
158        ref: scrollRef,
159        className: "flex-1 overflow-y-auto space-y-4 mb-4 bg-zinc-50 dark:bg-zinc-900 p-4 rounded-lg",
160        children: currentTicket.messages.map((msg) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex flex-col ${msg.sender === "support" ? "items-end" : "items-start"} group/msg`, children: [
161          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center gap-2 ${msg.sender === "support" ? "flex-row-reverse" : "flex-row"}`, children: [
162            /* @__PURE__ */ jsxRuntimeExports.jsx(
163              "div",
164              {
165                className: `max-w-[280px] sm:max-w-md md:max-w-xl p-3 rounded-lg text-sm ${msg.sender === "support" ? "bg-indigo-600 text-white rounded-br-none" : "bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 rounded-bl-none border border-zinc-200 dark:border-zinc-800"}`,
166                style: { whiteSpace: "pre-wrap" },
167                dangerouslySetInnerHTML: { __html: msg.text }
168              }
169            ),
170            /* @__PURE__ */ jsxRuntimeExports.jsx(
171              "button",
172              {
173                onClick: async () => {
174                  if (window.confirm("Are you sure you want to delete this message?")) {
175                    try {
176                      await api.deleteTicketMessage(currentTicket.id, msg.id);
177                      const updated = await api.getTicketDetails(currentTicket.id);
178                      setCurrentTicket(updated);
179                      onRefresh(updated);
180                    } catch (err) {
181                      console.error("Failed to delete message", err);
182                      alert("Failed to delete message");
183                    }
184                  }
185                },
186                className: "p-1 text-zinc-400 hover:text-red-500 rounded opacity-0 group-hover/msg:opacity-100 transition-opacity",
187                title: "Delete message",
188                children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 14 })
189              }
190            )
191          ] }),
192          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-400 mt-1", children: formatTicketDate(msg.timestamp) })
193        ] }, msg.id))
194      }
195    ),
196    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-t border-zinc-200 dark:border-zinc-800 pt-4", children: [
197      error && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-3 p-2 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900/30 rounded-lg flex items-center gap-2 text-xs text-red-600 dark:text-red-400 font-semibold", children: [
198        /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 14, className: "shrink-0" }),
199        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: error })
200      ] }),
201      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
202        /* @__PURE__ */ jsxRuntimeExports.jsx(
203          "input",
204          {
205            type: "text",
206            value: reply,
207            disabled: sending,
208            onChange: (e) => setReply(e.target.value),
209            onKeyDown: (e) => {
210              if (e.key === "Enter") {
211                handleSend();
212              }
213            },
214            placeholder: sending ? "Sending..." : "Type a reply...",
215            className: "flex-1 bg-zinc-100 dark:bg-zinc-800 border-none rounded-lg px-4 py-2 text-sm outline-none text-zinc-900 dark:text-white disabled:opacity-50"
216          }
217        ),
218        /* @__PURE__ */ jsxRuntimeExports.jsx(
219          "button",
220          {
221            onClick: handleSend,
222            disabled: sending || !reply.trim(),
223            className: "p-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center justify-center min-w-[36px]",
224            children: sending ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Send, { size: 18 })
225          }
226        )
227      ] }),
228      /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2 mt-3 mb-1 px-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "relative flex items-center cursor-pointer select-none", children: [
229        /* @__PURE__ */ jsxRuntimeExports.jsx(
230          "input",
231          {
232            type: "checkbox",
233            checked: sendEmail,
234            onChange: (e) => setSendEmail(e.target.checked),
235            className: "sr-only peer"
236          }
237        ),
238        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-4 bg-zinc-200 dark:bg-zinc-800 rounded-full peer peer-focus:ring-2 peer-focus:ring-indigo-500/20 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-3 after:w-3 after:transition-all peer-checked:bg-indigo-600 transition-all" }),
239        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ml-2 text-xs font-semibold text-zinc-500 dark:text-zinc-400", children: "Send response via email" })
240      ] }) }),
241      currentTicket.status === "open" && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleResolve, className: "w-full mt-3 py-2 bg-emerald-100 hover:bg-emerald-200 text-emerald-700 rounded-lg text-xs font-bold uppercase transition-colors", children: "Mark as Resolved" })
242    ] })
243  ] }) });
244};
245const getCategoryBadge = (category) => {
246  switch (category) {
247    case "billing":
248      return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "rounded px-2 py-0.5 text-xs font-bold bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 border border-amber-200 dark:border-amber-800/30 capitalize whitespace-nowrap", children: "billing" });
249    case "bug":
250    case "bug_report":
251      return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "rounded px-2 py-0.5 text-xs font-bold bg-rose-50 dark:bg-rose-900/20 text-rose-600 dar
251k:text-rose-400 border border-rose-200 dark:border-rose-800/30 capitalize whitespace-nowrap", children: category === "bug_report" ? "bug (sidebar)" : "bug" });
252    case "account":
253      return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "rounded px-2 py-0.5 text-xs font-bold bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 border border-sky-200 dark:border-sky-800/30 capitalize whitespace-nowrap", children: "account" });
254    case "copyright":
255      return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "rounded px-2 py-0.5 text-xs font-bold bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400 border border-orange-200 dark:border-orange-800/30 capitalize whitespace-nowrap", children: "copyright" });
256    default:
257      return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "rounded px-2 py-0.5 text-xs font-bold bg-zinc-50 dark:bg-zinc-800/30 text-zinc-600 dark:text-zinc-400 border border-zinc-200 dark:border-zinc-700/30 capitalize whitespace-nowrap", children: category });
258  }
259};
260const TicketSupport = ({ refreshTrigger }) => {
261  const [inspectingTicket, setInspectingTicket] = reactExports.useState(null);
262  const [localRefresh, setLocalRefresh] = reactExports.useState(0);
263  const refresh = (updatedTicket) => {
264    if (updatedTicket) {
265      setInspectingTicket(updatedTicket);
266    }
267    setLocalRefresh((p) => p + 1);
268  };
269  const fetcher = reactExports.useCallback((params) => api.getTickets(params), []);
270  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 md:p-6 space-y-4", children: [
271    /* @__PURE__ */ jsxRuntimeExports.jsx(TicketSupportSettings, {}),
272    /* @__PURE__ */ jsxRuntimeExports.jsx(
273      DataTable,
274      {
275        title: "Support Tickets",
276        icon: LifeBuoy,
277        placeholder: "Search subject or user...",
278        filters: [
279          {
280            key: "category",
281            label: "Category",
282            options: [
283              { label: "General Inquiry", value: "general" },
284              { label: "Account Issue", value: "account" },
285              { label: "Copyright Claim", value: "copyright" },
286              { label: "Bug Report", value: "bug" },
287              { label: "Bug Report (Sidebar)", value: "bug_report" },
288              { label: "Billing", value: "billing" }
289            ]
290          },
291          {
292            key: "status",
293            label: "Status",
294            options: [
295              { label: "Open", value: "open" },
296              { label: "On Hold", value: "on hold" },
297              { label: "Resolved", value: "resolved" }
298            ]
299          }
300        ],
301        onFetch: fetcher,
302        reloadTrigger: refreshTrigger + localRefresh,
303        onRowClick: setInspectingTicket,
304        dense: true,
305        columnHeaders: [
306          { label: "Subject", key: "subject" },
307          { label: "User", key: "username" },
308          { label: "Category", key: "category" },
309          { label: "Message", key: "message" },
310          { label: "Submitted", key: "createdAt" },
311          { label: "Status", key: "status" }
312        ],
313        columns: (ticket) => {
314          var _a, _b;
315          const firstMsg = ((_b = (_a = ticket.messages) == null ? void 0 : _a[0]) == null ? void 0 : _b.text) || "";
316          const truncatedMsg = firstMsg.length > 50 ? firstMsg.substring(0, 50) + "..." : firstMsg;
317          return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
318            /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2 font-bold text-zinc-900 dark:text-white", children: ticket.subject }),
319            /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2 text-zinc-600 dark:text-zinc-400", children: ticket.userName }),
320            /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2", children: getCategoryBadge(ticket.category) }),
321            /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2 text-xs text-zinc-500 max-w-[250px] truncate", title: firstMsg, children: truncatedMsg }),
322            /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2 text-xs text-zinc-400 font-mono whitespace-nowrap", children: formatTicketDate(ticket.createdAt) }),
323            /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `rounded px-2 py-0.5 text-xs capitalize font-bold border ${ticket.status === "open" ? "bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 border-indigo-200 dark:border-indigo-800/30" : ticket.status === "on hold" ? "bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-800/30" : "bg-zinc-100 dark:bg-zinc-800 text-zinc-500 border border-zinc-200 dark:border-zinc-700/30"}`, children: ticket.status }) })
324          ] });
325        }
326      }
327    ),
328    inspectingTicket && /* @__PURE__ */ jsxRuntimeExports.jsx(TicketChat, { ticket: inspectingTicket, onClose: () => setInspectingTicket(null), onRefresh: refresh })
329  ] });
330};
331export {
332  TicketSupport as default
333};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.