1import { n as Coins } from "./Header-DQPPT6P1.js"; 2import { t as AdminLayout } from "./AdminLayout-HxnHZjbV.js"; 3import { t as Ticket } from "./ticket-C3w8wBvv.js"; 4import { A as SelectValue, Ar as require_jsx_runtime, Bn as Eye, C as DropdownMenuTrigger, Cr as useAuth, Ct as useToast, D as SelectContent, Dr as useMutation, Dt as CardHeader, E as Select, Hn as EllipsisVertical, Hr as __toESM, O as SelectItem, Or as useQuery, Ot as CardTitle, S as DropdownMenuSeparator, St as TabsTrigger, Tn as LoaderCircle, Tt as CardContent, _n as Plus, at as Textarea, b as DropdownMenuContent, br as createLucideIcon, bt as TabsContent, dt as DialogDescription, ft as DialogFooter, gt as DialogTitle, j as Label, k as SelectTrigger, kr as useQueryClient, kt as Button, lt as Dialog, ot as useAuthReady, pn as Search, pt as DialogHeader, rn as Trash2, st as useBackend, tt as Input, ut as DialogContent, v as DropdownMenu, vt as Badge, wt as Card, x as DropdownMenuItem, xt as TabsList, yt as Tabs, zr as require_react } from "./index-Cqycx31B.js"; 5import { i as DrawerTitle, n as DrawerContent, r as DrawerHeader, t as Drawer } from "./drawer-gCXhKnFK.js"; 6import { a as TableHeader, i as TableHead, n as TableBody, o as TableRow, r as TableCell, t as Table } from "./table-CnLCOrpC.js"; 7/** 8* @license lucide-react v0.484.0 - ISC 9* 10* This source code is licensed under the ISC license. 11* See the LICENSE file in the root directory of this source tree. 12*/ 13var ToggleLeft = createLucideIcon("toggle-left", [["rect", { 14 width: "20", 15 height: "12", 16 x: "2", 17 y: "6", 18 rx: "6", 19 ry: "6", 20 key: "f2vt7d" 21}], ["circle", { 22 cx: "8", 23 cy: "12", 24 r: "2", 25 key: "1nvbw3" 26}]]); 27/** 28* @license lucide-react v0.484.0 - ISC 29* 30* This source code is licensed under the ISC license. 31* See the LICENSE file in the root directory of this source tree. 32*/ 33var ToggleRight = createLucideIcon("toggle-right", [["rect", { 34 width: "20", 35 height: "12", 36 x: "2", 37 y: "6", 38 rx: "6", 39 ry: "6", 40 key: "f2vt7d" 41}], ["circle", { 42 cx: "16", 43 cy: "12", 44 r: "2", 45 key: "4ma0v8" 46}]]); 47//#endregion 48//#region pages/AdminCoupons.tsx 49var import_react = /* @__PURE__ */ __toESM(require_react(), 1); 50var import_jsx_runtime = require_jsx_runtime(); 51function AdminCoupons() { 52 const backend = useBackend(); 53 const queryClient = useQueryClient(); 54 const { isLoaded, isSignedIn } = useAuth(); 55 const isAuthReady = useAuthReady(); 56 const { toast } = useToast(); 57 const [search, setSearch] = (0, import_react.useState)(""); 58 const [page, setPage] = (0, import_react.useState)(0); 59 const pageSize = 50; 60 const [selectedCouponId, setSelectedCouponId] = (0, import_react.useState)(null); 61 const [createCouponDialog, setCreateCouponDialog] = (0, import_react.useState)({ 62 open: false, 63 code: "", 64 name: "", 65 description: "", 66 couponType: "credit_grant", 67 creditAmount: "", 68 tierId: "", 69 discountPercentage: "", 70 discountFixedCents: "", 71 maxRedemptions: "", 72 maxPerUser: "1", 73 validUntil: "", 74 notes: "" 75 }); 76 const { data: adminAccessData } = useQuery({ 77 queryKey: ["admin", "health-check"], 78 queryFn: async () => backend.admin.checkAdminAccess(), 79 enabled: isLoaded && isSignedIn && isAuthReady 80 }); 81 const { data: couponsData, isLoading } = useQuery({ 82 queryKey: [ 83 "admin", 84 "coupons", 85 search, 86 page 87 ], 88 queryFn: async () => backend.admin.listCoupons({ 89 search, 90 limit: pageSize, 91 offset: page * pageSize 92 }), 93 enabled: isLoaded && isSignedIn && isAuthReady && !!adminAccessData 94 }); 95 const { data: selectedCouponDetails } = useQuery({ 96 queryKey: [ 97 "admin", 98 "coupon-details", 99 selectedCouponId 100 ], 101 queryFn: async () => backend.admin.getCouponDetails({ couponId: selectedCouponId }), 102 enabled: !!selectedCouponId 103 }); 104 const { data: redemptionAttempts } = useQuery({ 105 queryKey: [ 106 "admin", 107 "redemption-attempts", 108 selectedCouponId 109 ], 110 queryFn: async () => backend.admin.queryRedemptionAttempts({ 111 couponId: selectedCouponId, 112 limit: 50 113 }), 114 enabled: !!selectedCouponId 115 }); 116 const createCouponMutation = useMutation({ 117 mutationFn: async (params) => backend.admin.createCoupon(params), 118 onSuccess: (data) => { 119 toast({ 120 title: "Coupon Created", 121 description: `Successfully created coupon: ${data.code}` 122 });
123 queryClient.invalidateQueries({ queryKey: ["admin", "coupons"] }); 124 setCreateCouponDialog({ 125 open: false, 126 code: "", 127 name: "", 128 description: "", 129 couponType: "credit_grant", 130 creditAmount: "", 131 tierId: "", 132 discountPercentage: "", 133 discountFixedCents: "", 134 maxRedemptions: "", 135 maxPerUser: "1", 136 validUntil: "", 137 notes: "" 138 }); 139 }, 140 onError: (error) => { 141 toast({ 142 title: "Failed to Create Coupon", 143 description: error.message, 144 variant: "destructive" 145 }); 146 } 147 }); 148 const toggleCouponMutation = useMutation({ 149 mutationFn: async (params) => backend.admin.updateCoupon({ 150 couponId: params.couponId, 151 isActive: params.isActive 152 }), 153 onSuccess: (_, variables) => { 154 toast({ 155 title: variables.isActive ? "Coupon Activated" : "Coupon Deactivated", 156 description: "Coupon status updated successfully" 157 }); 158 queryClient.invalidateQueries({ queryKey: ["admin", "coupons"] }); 159 queryClient.invalidateQueries({ queryKey: [ 160 "admin", 161 "coupon-details", 162 variables.couponId 163 ] }); 164 }, 165 onError: (error) => { 166 toast({ 167 title: "Failed to Update Coupon", 168 description: error.message, 169 variant: "destructive" 170 }); 171 } 172 }); 173 const deleteCouponMutation = useMutation({ 174 mutationFn: async (couponId) => backend.admin.deleteCoupon({ couponId }), 175 onSuccess: (data) => { 176 toast({ 177 title: "Coupon Deleted", 178 description: data.message 179 }); 180 queryClient.invalidateQueries({ queryKey: ["admin", "coupons"] }); 181 setSelectedCouponId(null); 182 }, 183 onError: (error) => { 184 toast({ 185 title: "Failed to Delete Coupon", 186 description: error.message, 187 variant: "destructive" 188 }); 189 } 190 }); 191 const coupons = couponsData?.coupons || []; 192 const total = couponsData?.total || 0; 193 const totalPages = Math.ceil(total / pageSize); 194 if (!isLoaded || !isSignedIn || !isAuthReady || !adminAccessData) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdminLayout, { 195 adminRole: adminAccessData?.role ?? void 0, 196 adminEmail: adminAccessData?.email ?? void 0, 197 children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 198 className: "flex items-center justify-center py-32", 199 children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "w-12 h-12 animate-spin text-primary" }) 200 }) 201 }); 202 return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AdminLayout, { 203 adminRole: adminAccessData.role ?? void 0, 204 adminEmail: adminAccessData.email ?? void 0, 205 children: [ 206 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 207 className: "space-y-6", 208 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 209 className: "flex items-center justify-between", 210 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 211 className: "flex items-center gap-3", 212 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 213 className: "w-12 h-12 rounded-2xl bg-amber-500/10 flex items-center justify-center", 214 children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Ticket, { className: "w-6 h-6 text-amber-500" }) 215 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { 216 className: "text-3xl font-bold", 217 children: "Coupons" 218 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 219 className: "text-muted-foreground", 220 children: "Manage promotional codes and discounts" 221 })] })] 222 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, { 223 onClick: () => setCreateCouponDialog({ 224 ...createCouponDialog, 225 open: true 226 }), 227 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { className: "w-4 h-4 mr-2" }), "Create Coupon"] 228 })] 229 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardTitle, { 230 className: "text-sm flex items-center justify-between", 231 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [ 232 "All Coupons (",
233 total.toLocaleString(), 234 ")" 235 ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 236 className: "relative", 237 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Search, { className: "absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, { 238 placeholder: "Search coupons...", 239 value: search, 240 onChange: (e) => { 241 setSearch(e.target.value); 242 setPage(0); 243 }, 244 className: "pl-9 w-80" 245 })] 246 })] 247 }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 248 className: "flex items-center justify-center py-12", 249 children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "w-8 h-8 animate-spin text-muted-foreground" }) 250 }) : coupons.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 251 className: "flex flex-col items-center justify-center py-12 text-center", 252 children: [ 253 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 254 className: "w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4", 255 children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Ticket, { className: "w-8 h-8 text-muted-foreground" }) 256 }), 257 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { 258 className: "text-lg font-semibold mb-1", 259 children: "No coupons found" 260 }), 261 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 262 className: "text-sm text-muted-foreground", 263 children: search ? "Try adjusting your search criteria" : "Create your first coupon to get started" 264 }) 265 ] 266 }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 267 className: "overflow-x-auto rounded-md border", 268 children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Table, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableRow, { children: [ 269 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Code" }), 270 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Name" }), 271 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Type" }), 272 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Status" }), 273 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { 274 className: "text-right", 275 children: "Value" 276 }), 277 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { 278 className: "text-right", 279 children: "Redemptions" 280 }), 281 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { children: "Expires" }), 282 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableHead, { className: "w-12" }) 283 ] }) }
283), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableBody, { children: coupons.map((coupon) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableRow, { 284 className: "group", 285 children: [ 286 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 287 className: "font-mono font-bold", 288 children: coupon.code 289 }) }), 290 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { 291 className: "font-medium", 292 children: coupon.name 293 }), 294 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { 295 variant: "outline", 296 children: coupon.couponType.replaceAll("_", " ") 297 }) }), 298 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: coupon.isActive ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { 299 className: "bg-green-500", 300 children: "Active" 301 }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { 302 variant: "outline", 303 children: "Inactive" 304 }) }), 305 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableCell, { 306 className: "text-right", 307 children: [ 308 coupon.creditAmount && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 309 className: "flex items-center justify-end gap-1", 310 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Coins, { className: "w-3 h-3 text-amber-500" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: coupon.creditAmount.toLocaleString() })] 311 }), 312 coupon.discountPercentage && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [coupon.discountPercentage, "% off"] }), 313 coupon.discountFixedCents && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [ 314 "$", 315 (coupon.discountFixedCents / 100).toFixed(2), 316 " off" 317 ] }) 318 ] 319 }), 320 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { 321 className: "text-right", 322 children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [ 323 coupon.redemptionsCount, 324 " / ", 325 coupon.maxRedemptions || "â" 326 ] }) 327 }), 328 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { 329 className: "text-sm text-muted-foreground", 330 children: coupon.validUntil ? new Date(coupon.validUntil).toLocaleDateString() : "Never" 331 }), 332 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenu, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DropdownMenuTrigger, { 333 asChild: true, 334 children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, { 335 variant: "ghost", 336 size: "sm", 337 className: "h-8 w-8 p-0", 338 children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EllipsisVertical, { className: "w-4 h-4" }) 339 }) 340 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuContent, { 341 align: "end", 342 children: [ 343 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuItem, { 344 onClick: () => setSelectedCouponId(coupon.id), 345 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Eye, { className: "w-4 h-4 mr-2" }), "View Details"] 346 }), 347 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DropdownMenuSeparator, {}), 348 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DropdownMenuItem, { 349 onClick: () => toggleCouponMutation.mutate({ 350 couponId: coupon.id, 351 isActive: !coupon.isActive 352 }), 353 children: coupon.isActive ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToggleLeft, { className: "w-4 h-4 mr-2" }), "Deactivate"] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToggleRight, { className: "w-4 h-4 mr-2 text-green-500" }), "Activate"] }) 354 }),
355 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenuItem, { 356 onClick: () => { 357 if (confirm(`Delete coupon "${coupon.code}"?`)) deleteCouponMutation.mutate(coupon.id); 358 }, 359 className: "text-red-500", 360 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2, { className: "w-4 h-4 mr-2" }), "Delete"] 361 }) 362 ] 363 })] }) }) 364 ] 365 }, coupon.id)) })] }) 366 }), totalPages > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 367 className: "flex items-center justify-between mt-4", 368 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { 369 className: "text-sm text-muted-foreground", 370 children: [ 371 "Showing ", 372 page * pageSize + 1, 373 "-", 374 Math.min((page + 1) * pageSize, total), 375 " of ", 376 total 377 ] 378 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 379 className: "flex items-center gap-2", 380 children: [ 381 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, { 382 variant: "outline", 383 size: "sm", 384 onClick: () => setPage(Math.max(0, page - 1)), 385 disabled: page === 0, 386 children: "Previous" 387 }), 388 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { 389 className: "text-sm text-muted-foreground", 390 children: [ 391 "Page ", 392 page + 1, 393 " of ", 394 totalPages 395 ] 396 }), 397 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, { 398 variant: "outline", 399 size: "sm", 400 onClick: () => setPage(Math.min(totalPages - 1, page + 1)), 401 disabled: page >= totalPages - 1, 402 children: "Next" 403 }) 404 ] 405 })] 406 })] }) })] })] 407 }), 408 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Drawer, { 409 open: !!selectedCouponId, 410 onOpenChange: (open) => !open && setSelectedCouponId(null), 411 children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerContent, { 412 className: "max-h-[90vh]", 413 children: selectedCouponDetails && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DrawerTitle, { 414 className: "flex items-center justify-between", 415 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: ["Coupon: ", selectedCouponDetails.coupon.code] }
415), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, { 416 variant: "destructive", 417 size: "sm", 418 onClick: () => { 419 if (confirm(`Delete coupon "${selectedCouponDetails.coupon.code}"?`)) deleteCouponMutation.mutate(selectedCouponDetails.coupon.id); 420 }, 421 disabled: deleteCouponMutation.isPending, 422 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2, { className: "w-4 h-4 mr-1" }), "Delete"] 423 })] 424 }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 425 className: "p-6 overflow-y-auto", 426 children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 427 className: "space-y-4", 428 children: [ 429 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 430 className: "grid grid-cols-2 gap-4", 431 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 432 className: "text-sm text-muted-foreground", 433 children: "Information" 434 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 435 className: "mt-2 space-y-2 text-sm", 436 children: [ 437 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ 438 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 439 className: "font-medium", 440 children: "Code:" 441 }), 442 " ", 443 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 444 className: "font-mono", 445 children: selectedCouponDetails.coupon.code 446 }) 447 ] }), 448 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ 449 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 450 className: "font-medium", 451 children: "Name:" 452 }), 453 " ", 454 selectedCouponDetails.coupon.name 455 ] }), 456 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ 457 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 458 className: "font-medium", 459 children: "Type:" 460 }), 461 " ", 462 selectedCouponDetails.coupon.couponType.replace("_", " ") 463 ] }), 464 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ 465 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 466 className: "font-medium", 467 children: "Status:" 468 }), 469 " ", 470 selectedCouponDetails.coupon.isActive ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { 471 className: "bg-green-500", 472 children: "Active" 473 }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { 474 variant: "outline", 475 children: "Inactive" 476 }) 477 ] }) 478 ] 479 })] }
479), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 480 className: "text-sm text-muted-foreground", 481 children: "Stats" 482 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 483 className: "mt-2 space-y-2 text-sm", 484 children: [ 485 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ 486 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 487 className: "font-medium", 488 children: "Total Redemptions:" 489 }), 490 " ", 491 selectedCouponDetails.stats.totalRedemptions 492 ] }), 493 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ 494 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 495 className: "font-medium", 496 children: "Unique Users:" 497 }), 498 " ", 499 selectedCouponDetails.stats.uniqueUsers 500 ] }), 501 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ 502 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 503 className: "font-medium", 504 children: "Remaining:" 505 }), 506 " ", 507 selectedCouponDetails.stats.remainingRedemptions ?? "â" 508 ] }), 509 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ 510 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 511 className: "font-medium", 512 children: "Valid Until:" 513 }), 514 " ", 515 selectedCouponDetails.coupon.validUntil ? new Date(selectedCouponDetails.coupon.validUntil).toLocaleDateString() : "Never" 516 ] }) 517 ] 518 })] })] 519 }),
520 selectedCouponDetails.coupon.description && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 521 className: "text-sm text-muted-foreground mb-1", 522 children: "Description" 523 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 524 className: "text-sm", 525 children: selectedCouponDetails.coupon.description 526 })] }), 527 selectedCouponDetails.coupon.notes && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 528 className: "text-sm text-muted-foreground mb-1", 529 children: "Admin Notes" 530 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 531 className: "text-sm bg-muted/50 p-3 rounded", 532 children: selectedCouponDetails.coupon.notes 533 })] }), 534 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tabs, { 535 defaultValue: "redemptions", 536 className: "mt-4", 537 children: [ 538 /* @__P
538URE__ */ (0, import_jsx_runtime.jsxs)(TabsList, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TabsTrigger, { 539 value: "redemptions", 540 children: [ 541 "Successful Redemptions (", 542 selectedCouponDetails.redemptions.length, 543 ")" 544 ] 545 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TabsTrigger, { 546 value: "attempts", 547 children: [ 548 "All Attempts (", 549 redemptionAttempts?.total || 0, 550 ")" 551 ] 552 })] }), 553 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TabsContent, { 554 value: "redemptions", 555 className: "space-y-2 mt-4", 556 children: selectedCouponDetails.redemptions.length > 0 ? selectedCouponDetails.redemptions.map((redemption) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 557 className: "p-3 bg-muted/50 rounded text-sm", 558 children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 559 className: "flex items-center justify-between", 560 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 561 className: "font-medium", 562 children: redemption.userId 563 }), redemption.organizationId && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { 564 className: "text-xs text-muted-foreground", 565 children: ["Org: ", redemption.organizationId] 566 })] }
566), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 567 className: "text-right", 568 children: [redemption.creditsGranted && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { 569 className: "text-xs", 570 children: [redemption.creditsGranted.toLocaleString(), " credits"] 571 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 572 className: "text-xs text-muted-foreground", 573 children: new Date(redemption.redeemedAt).toLocaleDateString() 574 })] 575 })] 576 }) 577 }, redemption.id)) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 578 className: "text-sm text-muted-foreground text-center py-8", 579 children: "No successful redemptions yet" 580 }) 581 }), 582 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TabsContent, { 583 value: "attempts", 584 className: "space-y-2 mt-4", 585 children: redemptionAttempts && redemptionAttempts.attempts.length > 0 ? redemptionAttempts.attempts.map((attempt) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 586 className: `p-3 rounded text-sm ${attempt.success ? "bg-green-50 border border-green-200" : "bg-red-50 border border-red-200"}`, 587 children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 588 className: "flex items-center justify-between", 589 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ 590 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 591 className: "flex items-center gap-2", 592 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 593 className: "font-medium font-mono text-xs", 594 children: attempt.userId 595 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { 596 variant: attempt.success ? "default" : "destructive", 597 children: attempt.success ? "Success" : "Failed" 598 })] 599 }), 600 attempt.organizationId && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { 601 className: "text-xs text-muted-foreground mt-1", 602 children: ["Org: ", attempt.organizationId] 603 }), 604 !attempt.success && attempt.failureReason && /* @__P
604URE__ */ (0, import_jsx_runtime.jsxs)("p", { 605 className: "text-xs text-red-600 mt-1", 606 children: [ 607 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Reason:" }), 608 " ", 609 attempt.failureReason 610 ] 611 }) 612 ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 613 className: "text-right", 614 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 615 className: "text-xs text-muted-foreground", 616 children: new Date(attempt.attemptedAt).toLocaleDateString() 617 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 618 className: "text-xs text-muted-foreground", 619 children: new Date(attempt.attemptedAt).toLocaleTimeString() 620 })] 621 })] 622 }) 623 }, attempt.id)) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 624 className: "text-sm text-muted-foreground text-center py-8", 625 children: "No redemption attempts yet" 626 }) 627 }) 628 ] 629 }) 630 ] 631 }) 632 })] }) 633 }) 634 }), 635 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog, { 636 open: createCouponDialog.open, 637 onOpenChange: (open) => setCreateCouponDialog({ 638 ...createCouponDialog, 639 open 640 }), 641 children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogContent, { 642 className: "max-w-2xl max-h-[90vh] overflow-y-auto", 643 children: [ 644 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogTitle, { children: "Create New Coupon" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogDescription, { children: "Create a new coupon code for credits, discounts, or tier upgrades" })] }), 645 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 646 className: "space-y-4 py-4", 647 children: [ 648 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 649 className: "grid grid-cols-2 gap-4", 650 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 651 htmlFor: "coupon-code", 652 children: "Code (required)" 653 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, { 654 id: "coupon-code", 655 value: createCouponDialog.code, 656 onChange: (e) => setCreateCouponDialog({ 657 ...createCouponDialog, 658 code: e.target.value.toUpperCase() 659 }), 660 placeholder: "e.g. WELCOME2024", 661 className: "font-mono" 662 })] }
662), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 663 htmlFor: "coupon-name", 664 children: "Name (required)" 665 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, { 666 id: "coupon-name", 667 value: createCouponDialog.name, 668 onChange: (e) => setCreateCouponDialog({ 669 ...createCouponDialog, 670 name: e.target.value 671 }), 672 placeholder: "e.g. Welcome Offer" 673 })] })] 674 }), 675 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 676 htmlFor: "coupon-type", 677 children: "Coupon Type" 678 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select, { 679 value: createCouponDialog.couponType, 680 onValueChange: (value) => setCreateCouponDialog({ 681 ...createCouponDialog, 682 couponType: value 683 }), 684 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, {}) }
684), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectContent, { children: [ 685 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, { 686 value: "credit_grant", 687 children: "Credit Grant" 688 }), 689 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, { 690 value: "tier_upgrade", 691 children: "Tier Upgrade (Trial)" 692 }), 693 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, { 694 value: "percentage_discount", 695 children: "Percentage Discount" 696 }), 697 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, { 698 value: "fixed_discount", 699 children: "Fixed Discount" 700 }) 701 ] })] 702 })] }), 703 createCouponDialog.couponType === "credit_grant" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 704 htmlFor: "credit-amount", 705 children: "Credit Amount (required)" 706 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, { 707 id: "credit-amount", 708 type: "number", 709 min: "1", 710 max: "1000000", 711 value: createCouponDialog.creditAmount, 712 onChange: (e) => setCreateCouponDialog({ 713 ...createCouponDialog, 714 creditAmount: e.target.value 715 }), 716 placeholder: "e.g. 10000" 717 })] }), 718 createCouponDialog.couponType === "tier_upgrade" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 719 htmlFor: "tier-id", 720 children: "Tier ID (required)" 721 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select, { 722 value: createCouponDialog.tierId, 723 onValueChange: (value) => setCreateCouponDialog({ 724 ...createCouponDialog, 725 tierId: value 726 }), 727 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, { placeholder: "Select tier" }) }
727), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectContent, { children: [ 728 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, { 729 value: "starter", 730 children: "Starter" 731 }), 732 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, { 733 value: "professional", 734 children: "Professional" 735 }), 736 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, { 737 value: "business", 738 children: "Business" 739 }), 740 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, { 741 value: "enterprise", 742 children: "Enterprise" 743 }) 744 ] })] 745 })] }), 746 createCouponDialog.couponType === "percentage_discount" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 747 htmlFor: "discount-percentage", 748 children: "Discount Percentage (required)" 749 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, { 750 id: "discount-percentage", 751 type: "number", 752 min: "1", 753 max: "100", 754 value: createCouponDialog.discountPercentage, 755 onChange: (e) => setCreateCouponDialog({ 756 ...createCouponDialog, 757 discountPercentage: e.target.value 758 }), 759 placeholder: "e.g. 20" 760 })] }), 761 createCouponDialog.couponType === "fixed_discount" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 762 htmlFor: "discount-fixed", 763 children: "Fixed Discount (cents, required)" 764 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, { 765 id: "discount-fixed", 766 type: "number", 767 min: "1", 768 value: createCouponDialog.discountFixedCents, 769 onChange: (e) => setCreateCouponDialog({ 770 ...createCouponDialog, 771 discountFixedCents: e.target.value 772 }), 773 placeholder: "e.g. 1000 (= $10.00)" 774 })] }), 775 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 776 htmlFor: "description", 777 children: "Description" 778 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, { 779 id: "description", 780 value: createCouponDialog.description, 781 onChange: (e) => setCreateCouponDialog({ 782 ...createCouponDialog, 783 description: e.target.value 784 }), 785 placeholder: "User-facing description of the coupon", 786 rows: 2 787 })] }), 788 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 789 className: "grid grid-cols-2 gap-4", 790 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 791 htmlFor: "max-redemptions", 792 children: "Max Redemptions (optional)" 793 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, { 794 id: "max-redemptions", 795 type: "number", 796 min: "1", 797 value: createCouponDialog.maxRedemptions, 798 onChange: (e) => setCreateCouponDialog({ 799 ...createCouponDialog, 800 maxRedemptions: e.target.value 801 }), 802 placeholder: "Leave empty for unlimited" 803 })] }
803), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 804 htmlFor: "max-per-user", 805 children: "Max Per User" 806 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, { 807 id: "max-per-user", 808 type: "number", 809 min: "1", 810 value: createCouponDialog.maxPerUser, 811 onChange: (e) => setCreateCouponDialog({ 812 ...createCouponDialog, 813 maxPerUser: e.target.value 814 }), 815 placeholder: "Default: 1" 816 })] })] 817 }), 818 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 819 htmlFor: "valid-until", 820 children: "Valid Until (optional)" 821 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, { 822 id: "valid-until", 823 type: "date", 824 value: createCouponDialog.validUntil, 825 onChange: (e) => setCreateCouponDialog({ 826 ...createCouponDialog, 827 validUntil: e.target.value 828 }) 829 })] }), 830 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { 831 htmlFor: "notes", 832 children: "Admin Notes (optional)" 833 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, { 834 id: "notes", 835 value: createCouponDialog.notes, 836 onChange: (e) => setCreateCouponDialog({ 837 ...createCouponDialog, 838 notes: e.target.value 839 }), 840 placeholder: "Internal notes (not shown to users)", 841 rows: 2 842 })] }) 843 ] 844 }), 845 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, { 846 variant: "outline", 847 onClick: () => setCreateCouponDialog({ 848 ...createCouponDialog, 849 open: false 850 }), 851 children: "Cancel" 852 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, { 853 onClick: () => { 854 if (!createCouponDialog.code || createCouponDialog.code.length < 3) { 855 toast({ 856 title: "Code must be at least 3 characters", 857 variant: "destructive" 858 }); 859 return; 860 } 861 if (!createCouponDialog.name || createCouponDialog.name.length < 3) { 862 toast({ 863 title: "Name must be at least 3 characters", 864 variant: "destructive" 865 }); 866 return; 867 } 868 const payload = { 869 code: createCouponDialog.code, 870 name: createCouponDialog.name, 871 description: createCouponDialog.description || void 0, 872 couponType: createCouponDialog.couponType, 873 notes: createCouponDialog.notes || void 0, 874 maxRedemptions: createCouponDialog.maxRedemptions ? parseInt(createCouponDialog.maxRedemptions) : void 0, 875 maxPerUser: createCouponDialog.maxPerUser ? parseInt(createCouponDialog.maxPerUser) : 1, 876 validUntil: createCouponDialog.validUntil ? new Date(createCouponDialog.validUntil) : void 0 877 }; 878 if (createCouponDialog.couponType === "credit_grant") { 879 const amount = parseInt(createCouponDialog.creditAmount); 880 if (isNaN(amount) || amount <= 0) { 881 toast({ 882 title: "Credit amount must be positive", 883 variant: "destructive" 884 }); 885 return; 886 } 887 payload.creditAmount = amount; 888 } else if (createCouponDialog.couponType === "tier_upgrade") { 889 if (!createCouponDialog.tierId) { 890 toast({ 891 title: "Please select a tier", 892 variant: "destructive" 893 }); 894 return; 895 } 896 payload.tierId = createCouponDialog.tierId; 897 } else if (createCouponDialog.couponType === "percentage_discount") { 898 const pct = parseInt(createCouponDialog.discountPercentage); 899 if (isNaN(pct) || pct < 1 || pct > 100) { 900 toast({ 901 title: "Discount percentage must be 1-100", 902 variant: "destructive" 903 }); 904 return; 905 } 906 payload.discountPercentage = pct; 907 } else if (createCouponDialog.couponType === "fixed_discount") { 908 const cents = parseInt(createCouponDialog.discountFixedCents); 909 if (isNaN(cents) || cents <= 0) { 910 toast({ 911 title: "Fixed discount must be positive", 912 variant: "destructive" 913 }); 914 return; 915 } 916 payload.discountFixedCents = cents; 917 } 918 createCouponMutation.mutate(payload); 919 }, 920 disabled: createCouponMutation.isPending,
921 children: createCouponMutation.isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "w-4 h-4 mr-2 animate-spin" }), "Creating..."] }) : "Create Coupon" 922 })] }) 923 ] 924 }) 925 }) 926 ] 927 }); 928} 929//#endregion 930export { AdminCoupons as default };
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.