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.