@infuro/cms-core 1.0.72 → 1.0.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/admin.cjs +543 -120
  2. package/dist/admin.js +545 -122
  3. package/dist/api.cjs +37 -37
  4. package/dist/api.d.cts +13 -6
  5. package/dist/api.d.ts +13 -6
  6. package/dist/api.js +5 -5
  7. package/dist/auth.cjs +11 -11
  8. package/dist/auth.d.cts +15 -0
  9. package/dist/auth.d.ts +15 -0
  10. package/dist/auth.js +1 -1
  11. package/dist/{chunk-BQQMTQ4C.cjs → chunk-25T6HP5T.cjs} +2 -2
  12. package/dist/{chunk-TTOEY7AH.cjs → chunk-2JV4EDY3.cjs} +3 -3
  13. package/dist/{chunk-LJ5C4RBF.js → chunk-6TRH3VY7.js} +1 -1
  14. package/dist/{chunk-4MNVIGVJ.cjs → chunk-D3SR6CYU.cjs} +3 -3
  15. package/dist/{chunk-PQJIJ4ZX.js → chunk-DAUBLBTP.js} +1 -1
  16. package/dist/{chunk-SCE2GIKH.js → chunk-DEGN4BTH.js} +9 -0
  17. package/dist/{chunk-CKTRA3Q2.cjs → chunk-F3FKBNMQ.cjs} +9 -0
  18. package/dist/{chunk-SRGDHASW.cjs → chunk-H4UIIBFD.cjs} +2571 -2096
  19. package/dist/{chunk-YHVA5NHI.js → chunk-LUSJITTI.js} +43 -6
  20. package/dist/{chunk-43UVXBEH.js → chunk-OQOO2JMB.js} +2529 -2054
  21. package/dist/{chunk-HZZTDM5J.js → chunk-R5HLPH6P.js} +1 -1
  22. package/dist/{chunk-3LF4RYAJ.js → chunk-X3IJG25M.js} +1 -1
  23. package/dist/{chunk-2JHGIWH7.cjs → chunk-X5ILLSTD.cjs} +43 -6
  24. package/dist/{chunk-NIBEGDD4.cjs → chunk-ZBSH4GWB.cjs} +1 -1
  25. package/dist/{email-queue-MWP4R67H.js → email-queue-5TOJJT3P.js} +3 -3
  26. package/dist/{email-queue-BJDH2ID7.cjs → email-queue-X2A46VD3.cjs} +7 -7
  27. package/dist/{emit-order-notification-trigger-TBXAZAPS.js → emit-order-notification-trigger-3RAVWKLG.js} +1 -1
  28. package/dist/{emit-order-notification-trigger-I4HQSTEN.cjs → emit-order-notification-trigger-MY3KP7CD.cjs} +3 -3
  29. package/dist/{erp-order-invoice-5O42ZKC5.cjs → erp-order-invoice-23BDWC2H.cjs} +6 -6
  30. package/dist/{erp-order-invoice-JEQKFXG4.js → erp-order-invoice-SEZZUDMW.js} +2 -2
  31. package/dist/generate-local-invoice-pdf-V5FHS23Z.js +3 -0
  32. package/dist/{generate-local-invoice-pdf-2NCA2WBL.cjs → generate-local-invoice-pdf-XYBVKHNP.cjs} +2 -2
  33. package/dist/index.cjs +243 -243
  34. package/dist/index.d.cts +31 -9
  35. package/dist/index.d.ts +31 -9
  36. package/dist/index.js +10 -10
  37. package/dist/migrations/1783400000000-MakeProductConfigTaxIdNullable.ts +40 -0
  38. package/dist/{order-completion-otp-handlers-KHLZQQJX.js → order-completion-otp-handlers-2WOFSORP.js} +4 -4
  39. package/dist/{order-completion-otp-handlers-2ZKRK5SN.cjs → order-completion-otp-handlers-EWY47XUA.cjs} +6 -6
  40. package/dist/{order-notification-dispatcher-Z3MXNF4U.js → order-notification-dispatcher-COR2ZHM3.js} +4 -4
  41. package/dist/{order-notification-dispatcher-37KDVHBM.cjs → order-notification-dispatcher-KTOAMMSD.cjs} +9 -9
  42. package/package.json +1 -1
  43. package/dist/generate-local-invoice-pdf-XUH6GYEJ.js +0 -3
package/dist/admin.cjs CHANGED
@@ -381,9 +381,8 @@ var init_vendor_scope = __esm({
381
381
  }
382
382
  });
383
383
  function AdminHeader() {
384
- const { data: session } = react.useSession();
384
+ const { data: session, update } = react.useSession();
385
385
  const sessionUser = session?.user;
386
- const roleLabel = sessionUser?.groupName?.trim() || (sessionUser?.isRBACAdmin ? "Administrator" : "Admin user");
387
386
  const isVendor = isVendorPortalUser(sessionUser);
388
387
  const isMobile = useIsMobile();
389
388
  const configuredLogo = process.env.NEXT_PUBLIC_ADMIN_LOGO_URL || DEFAULT_ADMIN_LOGO;
@@ -392,9 +391,125 @@ function AdminHeader() {
392
391
  const isDataLogo = React26.useMemo(() => logoSrc.startsWith("data:"), [
393
392
  logoSrc
394
393
  ]);
394
+ const [isSwitching, setIsSwitching] = React26.useState(false);
395
+ const [cookieVendorId, setCookieVendorId] = React26.useState(() => {
396
+ if (typeof window === "undefined") return null;
397
+ try {
398
+ const match = document.cookie.match(/(?:^|;\s*)infuro_active_vendor_id=(\d+)/);
399
+ if (match && match[1]) return Number(match[1]);
400
+ const stored = localStorage.getItem("infuro_active_vendor_id");
401
+ if (stored) return Number(stored);
402
+ } catch {
403
+ }
404
+ return null;
405
+ });
406
+ const [fetchedStores, setFetchedStores] = React26.useState(null);
407
+ const [fetchedRoleName, setFetchedRoleName] = React26.useState(null);
408
+ const [fetchedActiveVendorId, setFetchedActiveVendorId] = React26.useState(null);
409
+ React26.useEffect(() => {
410
+ if (sessionUser?.vendorStores && sessionUser.vendorStores.length > 1 && !cookieVendorId) {
411
+ return;
412
+ }
413
+ let isMounted = true;
414
+ fetch("/api/admin/vendor/context").then((res) => res.ok ? res.json() : null).then((data) => {
415
+ if (!isMounted || !data) return;
416
+ if (Array.isArray(data.vendorStores)) {
417
+ setFetchedStores(data.vendorStores);
418
+ }
419
+ if (data.vendorRoleName) {
420
+ setFetchedRoleName(data.vendorRoleName);
421
+ }
422
+ if (typeof data.activeVendorId === "number") {
423
+ setFetchedActiveVendorId(data.activeVendorId);
424
+ }
425
+ try {
426
+ localStorage.setItem("infuro_active_vendor_context", JSON.stringify(data));
427
+ window.dispatchEvent(new CustomEvent("infuro_vendor_context_updated", {
428
+ detail: data
429
+ }));
430
+ } catch {
431
+ }
432
+ }).catch(() => {
433
+ });
434
+ return () => {
435
+ isMounted = false;
436
+ };
437
+ }, [
438
+ sessionUser?.email,
439
+ sessionUser?.vendorStores,
440
+ cookieVendorId
441
+ ]);
442
+ const stores = React26.useMemo(() => {
443
+ if (sessionUser?.vendorStores && sessionUser.vendorStores.length > 0) {
444
+ return sessionUser.vendorStores;
445
+ }
446
+ return fetchedStores || [];
447
+ }, [
448
+ sessionUser?.vendorStores,
449
+ fetchedStores
450
+ ]);
451
+ const effectiveActiveVendorId = cookieVendorId ?? sessionUser?.activeVendorId ?? fetchedActiveVendorId;
452
+ const activeStore = React26.useMemo(() => {
453
+ return stores.find((s) => s.id === effectiveActiveVendorId) || (stores.length > 0 ? stores[0] : null);
454
+ }, [
455
+ stores,
456
+ effectiveActiveVendorId
457
+ ]);
458
+ const effectiveRoleName = activeStore?.roleName || sessionUser?.vendorRoleName || fetchedRoleName;
459
+ const roleLabel = ((isVendor || stores.length > 0) && effectiveRoleName ? effectiveRoleName : null) || sessionUser?.groupName?.trim() || (sessionUser?.isRBACAdmin ? "Administrator" : "Admin user");
395
460
  const handleLogout = /* @__PURE__ */ __name(() => {
396
461
  void adminSignOut();
397
462
  }, "handleLogout");
463
+ const handleSwitchStore = /* @__PURE__ */ __name(async (vendorId) => {
464
+ if (vendorId === activeStore?.id || isSwitching) return;
465
+ try {
466
+ setIsSwitching(true);
467
+ document.cookie = `infuro_active_vendor_id=${vendorId}; path=/; max-age=31536000; SameSite=Lax`;
468
+ try {
469
+ localStorage.setItem("infuro_active_vendor_id", String(vendorId));
470
+ } catch {
471
+ }
472
+ setCookieVendorId(vendorId);
473
+ const res = await fetch("/api/admin/vendor/switch", {
474
+ method: "POST",
475
+ headers: {
476
+ "Content-Type": "application/json"
477
+ },
478
+ body: JSON.stringify({
479
+ vendorId
480
+ })
481
+ });
482
+ if (res.ok) {
483
+ const data = await res.json();
484
+ try {
485
+ localStorage.setItem("infuro_active_vendor_context", JSON.stringify(data));
486
+ window.dispatchEvent(new CustomEvent("infuro_vendor_context_updated", {
487
+ detail: data
488
+ }));
489
+ } catch {
490
+ }
491
+ if (typeof update === "function") {
492
+ try {
493
+ await update({
494
+ activeVendorId: vendorId,
495
+ vendorRole: data.vendorRole,
496
+ vendorRoleId: data.vendorRoleId,
497
+ vendorRoleName: data.vendorRoleName,
498
+ isVendorRoleOwner: data.isVendorRoleOwner,
499
+ vendorEntityPerms: data.vendorEntityPerms,
500
+ vendorStores: data.vendorStores
501
+ });
502
+ } catch {
503
+ }
504
+ }
505
+ window.location.reload();
506
+ }
507
+ } catch (err) {
508
+ console.error("Failed to switch store:", err);
509
+ } finally {
510
+ setIsSwitching(false);
511
+ }
512
+ }, "handleSwitchStore");
398
513
  return /* @__PURE__ */ React.createElement("header", {
399
514
  className: "bg-white border-b border-gray-200 px-4 py-2"
400
515
  }, /* @__PURE__ */ React.createElement("div", {
@@ -427,7 +542,46 @@ function AdminHeader() {
427
542
  className: "text-sm font-semibold text-gray-800"
428
543
  }, "Infuro"))), /* @__PURE__ */ React.createElement("div", {
429
544
  className: "flex items-center space-x-3"
430
- }, /* @__PURE__ */ React.createElement(DropdownMenu, null, /* @__PURE__ */ React.createElement(DropdownMenuTrigger, {
545
+ }, stores.length > 1 && /* @__PURE__ */ React.createElement(DropdownMenu, null, /* @__PURE__ */ React.createElement(DropdownMenuTrigger, {
546
+ asChild: true
547
+ }, /* @__PURE__ */ React.createElement(Button, {
548
+ variant: "outline",
549
+ size: "sm",
550
+ disabled: isSwitching,
551
+ className: "h-8 px-2.5 flex items-center gap-1.5 border-gray-200 bg-gray-50/80 hover:bg-gray-100 text-gray-800 text-xs font-medium rounded-lg shadow-none transition-colors"
552
+ }, isSwitching ? /* @__PURE__ */ React.createElement(LucideIcons.Loader2, {
553
+ className: "h-3.5 w-3.5 animate-spin text-gray-500 shrink-0"
554
+ }) : /* @__PURE__ */ React.createElement(LucideIcons.Store, {
555
+ className: "h-3.5 w-3.5 text-gray-500 shrink-0"
556
+ }), /* @__PURE__ */ React.createElement("span", {
557
+ className: "max-w-[130px] sm:max-w-[180px] truncate font-semibold"
558
+ }, activeStore?.name || "Select Store"), activeStore?.roleName && /* @__PURE__ */ React.createElement("span", {
559
+ className: "hidden sm:inline-block px-1.5 py-0.5 text-[10px] font-medium bg-gray-200/80 text-gray-700 rounded"
560
+ }, activeStore.roleName), /* @__PURE__ */ React.createElement(LucideIcons.ChevronDown, {
561
+ className: "h-3 w-3 text-gray-400 shrink-0"
562
+ }))), /* @__PURE__ */ React.createElement(DropdownMenuContent, {
563
+ align: "end",
564
+ className: "w-56 p-1"
565
+ }, /* @__PURE__ */ React.createElement(DropdownMenuLabel, {
566
+ className: "text-[11px] font-semibold text-gray-500 uppercase px-2 py-1"
567
+ }, "Switch Store"), /* @__PURE__ */ React.createElement(DropdownMenuSeparator, {
568
+ className: "my-1"
569
+ }), stores.map((st) => {
570
+ const isSelected = st.id === activeStore?.id;
571
+ return /* @__PURE__ */ React.createElement(DropdownMenuItem, {
572
+ key: st.id,
573
+ onClick: /* @__PURE__ */ __name(() => handleSwitchStore(st.id), "onClick"),
574
+ className: "flex items-center justify-between px-2.5 py-2 cursor-pointer text-xs rounded-md"
575
+ }, /* @__PURE__ */ React.createElement("div", {
576
+ className: "flex flex-col gap-0.5 truncate pr-2"
577
+ }, /* @__PURE__ */ React.createElement("span", {
578
+ className: `font-medium ${isSelected ? "text-primary font-semibold" : "text-gray-800"}`
579
+ }, st.name), st.roleName && /* @__PURE__ */ React.createElement("span", {
580
+ className: "text-[10px] text-gray-400"
581
+ }, st.roleName)), isSelected && /* @__PURE__ */ React.createElement(LucideIcons.Check, {
582
+ className: "h-3.5 w-3.5 text-primary shrink-0"
583
+ }));
584
+ }))), /* @__PURE__ */ React.createElement(DropdownMenu, null, /* @__PURE__ */ React.createElement(DropdownMenuTrigger, {
431
585
  asChild: true
432
586
  }, /* @__PURE__ */ React.createElement(Button, {
433
587
  variant: "ghost",
@@ -895,7 +1049,7 @@ var init_admin_config_context = __esm({
895
1049
  var CMS_VERSION;
896
1050
  var init_cms_version = __esm({
897
1051
  "src/lib/cms-version.ts"() {
898
- CMS_VERSION = "1.0.72" ;
1052
+ CMS_VERSION = "1.0.74" ;
899
1053
  }
900
1054
  });
901
1055
  function useCatalogCategories(enabled = true) {
@@ -1003,8 +1157,54 @@ function AdminSidebar({ variant = "sidebar" }) {
1003
1157
  const searchParams = navigation.useSearchParams();
1004
1158
  const { data: session } = react.useSession();
1005
1159
  const sessionUser = session?.user;
1006
- const showVendorOnboard = canOnboardVendors(sessionUser);
1007
- const vendorPortal = isVendorPortalUser(sessionUser);
1160
+ const [vendorContext, setVendorContext] = React26.useState(() => {
1161
+ if (typeof window === "undefined") return null;
1162
+ try {
1163
+ const stored = localStorage.getItem("infuro_active_vendor_context");
1164
+ if (stored) return JSON.parse(stored);
1165
+ } catch {
1166
+ }
1167
+ return null;
1168
+ });
1169
+ React26.useEffect(() => {
1170
+ const onContextUpdated = /* @__PURE__ */ __name((e) => {
1171
+ const detail = e.detail;
1172
+ if (detail) setVendorContext(detail);
1173
+ }, "onContextUpdated");
1174
+ window.addEventListener("infuro_vendor_context_updated", onContextUpdated);
1175
+ fetch("/api/admin/vendor/context").then((r) => r.ok ? r.json() : null).then((data) => {
1176
+ if (data) {
1177
+ setVendorContext(data);
1178
+ try {
1179
+ localStorage.setItem("infuro_active_vendor_context", JSON.stringify(data));
1180
+ } catch {
1181
+ }
1182
+ }
1183
+ }).catch(() => {
1184
+ });
1185
+ return () => {
1186
+ window.removeEventListener("infuro_vendor_context_updated", onContextUpdated);
1187
+ };
1188
+ }, []);
1189
+ const effectiveSessionUser = React26.useMemo(() => {
1190
+ if (!sessionUser) return null;
1191
+ if (!vendorContext) return sessionUser;
1192
+ return {
1193
+ ...sessionUser,
1194
+ activeVendorId: vendorContext.activeVendorId ?? sessionUser.activeVendorId,
1195
+ vendorRole: vendorContext.vendorRole ?? sessionUser.vendorRole,
1196
+ vendorRoleId: vendorContext.vendorRoleId ?? sessionUser.vendorRoleId,
1197
+ vendorRoleName: vendorContext.vendorRoleName ?? sessionUser.vendorRoleName,
1198
+ isVendorRoleOwner: vendorContext.isVendorRoleOwner ?? sessionUser.isVendorRoleOwner,
1199
+ vendorEntityPerms: vendorContext.vendorEntityPerms ?? sessionUser.vendorEntityPerms,
1200
+ vendorStores: vendorContext.vendorStores ?? sessionUser.vendorStores
1201
+ };
1202
+ }, [
1203
+ sessionUser,
1204
+ vendorContext
1205
+ ]);
1206
+ const showVendorOnboard = canOnboardVendors(effectiveSessionUser);
1207
+ const vendorPortal = isVendorPortalUser(effectiveSessionUser);
1008
1208
  const { customNavItems, customNavSections = [], storeEnabled, multiVendorEnabled, eventsEnabled, vendorCanCreateCategories, vendorCanCreateCollections, vendorCanCreateBrands } = React26.useContext(exports.AdminConfigContext);
1009
1209
  const showStoreNav = storeEnabled || vendorPortal;
1010
1210
  const showPlatformNav = !vendorPortal;
@@ -1031,8 +1231,8 @@ function AdminSidebar({ variant = "sidebar" }) {
1031
1231
  const headingCls = "text-[11px] font-semibold text-gray-400 uppercase tracking-wider px-2.5 mb-1.5";
1032
1232
  const asideCls = isDrawer ? "w-full h-full min-h-0 bg-white flex flex-col overflow-hidden" : "w-52 h-full min-h-0 bg-white border-r border-gray-200 flex-shrink-0 flex flex-col overflow-hidden";
1033
1233
  const canReadEntity = /* @__PURE__ */ __name((entity) => {
1034
- if (!sessionUser) return true;
1035
- return sessionHasEntityAccessFromExplanation(sessionUser, entity, "read");
1234
+ if (!effectiveSessionUser) return true;
1235
+ return sessionHasEntityAccessFromExplanation(effectiveSessionUser, entity, "read");
1036
1236
  }, "canReadEntity");
1037
1237
  return /* @__PURE__ */ React.createElement("aside", {
1038
1238
  className: asideCls
@@ -1242,12 +1442,12 @@ function AdminSidebar({ variant = "sidebar" }) {
1242
1442
  className: `${linkCls} ${isActive("/admin/vendor-profile") ? linkActive : linkInactive}`
1243
1443
  }, /* @__PURE__ */ React.createElement(LucideIcons.User, {
1244
1444
  className: `h-4 w-4 mr-2 ${isActive("/admin/vendor-profile") ? iconActive : iconInactive}`
1245
- }), "Profile")), canManageVendorTeam(sessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2__default.default, {
1445
+ }), "Profile")), canManageVendorTeam(effectiveSessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2__default.default, {
1246
1446
  href: "/admin/vendor-team",
1247
1447
  className: `${linkCls} ${isActive("/admin/vendor-team") ? linkActive : linkInactive}`
1248
1448
  }, /* @__PURE__ */ React.createElement(LucideIcons.Users, {
1249
1449
  className: `h-4 w-4 mr-2 ${isActive("/admin/vendor-team") ? iconActive : iconInactive}`
1250
- }), "Team")), canManageVendorRoles(sessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2__default.default, {
1450
+ }), "Team")), canManageVendorRoles(effectiveSessionUser) && /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(Link2__default.default, {
1251
1451
  href: "/admin/vendor-roles",
1252
1452
  className: `${linkCls} ${isActive("/admin/vendor-roles") ? linkActive : linkInactive}`
1253
1453
  }, /* @__PURE__ */ React.createElement(LucideIcons.Shield, {
@@ -6438,6 +6638,20 @@ var init_FieldConfiguration = __esm({
6438
6638
  }
6439
6639
  });
6440
6640
 
6641
+ // src/lib/slug-sanitizer.ts
6642
+ function sanitizeSlugInput(value) {
6643
+ return value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-");
6644
+ }
6645
+ function normalizeSlug(value) {
6646
+ return sanitizeSlugInput(value).replace(/^-+|-+$/g, "");
6647
+ }
6648
+ var init_slug_sanitizer = __esm({
6649
+ "src/lib/slug-sanitizer.ts"() {
6650
+ __name(sanitizeSlugInput, "sanitizeSlugInput");
6651
+ __name(normalizeSlug, "normalizeSlug");
6652
+ }
6653
+ });
6654
+
6441
6655
  // src/components/Admin/FormBuilder.tsx
6442
6656
  var FormBuilder_exports = {};
6443
6657
  __export(FormBuilder_exports, {
@@ -6516,18 +6730,6 @@ function FormBuilder({ formId, duplicateFromId }) {
6516
6730
  loadId,
6517
6731
  loadFormData
6518
6732
  ]);
6519
- React26.useEffect(() => {
6520
- if (formData.name && !formData.slug) {
6521
- const generatedSlug = formData.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
6522
- setFormData((prev) => ({
6523
- ...prev,
6524
- slug: generatedSlug
6525
- }));
6526
- }
6527
- }, [
6528
- formData.name,
6529
- formData.slug
6530
- ]);
6531
6733
  const addField = /* @__PURE__ */ __name(() => {
6532
6734
  const newField = {
6533
6735
  id: `field_${Date.now()}`,
@@ -6632,6 +6834,7 @@ function FormBuilder({ formId, duplicateFromId }) {
6632
6834
  setErrors([]);
6633
6835
  const payload = {
6634
6836
  ...formData,
6837
+ slug: normalizeSlug(formData.slug),
6635
6838
  published: isPublishing ? true : formData.published,
6636
6839
  fields: formData.fields.map((field) => {
6637
6840
  const numericId = typeof field.id === "number" ? field.id : /^\d+$/.test(String(field.id)) ? parseInt(String(field.id), 10) : void 0;
@@ -6762,8 +6965,12 @@ function FormBuilder({ formId, duplicateFromId }) {
6762
6965
  value: formData.slug,
6763
6966
  onChange: /* @__PURE__ */ __name((e) => setFormData((prev) => ({
6764
6967
  ...prev,
6765
- slug: e.target.value
6968
+ slug: sanitizeSlugInput(e.target.value)
6766
6969
  })), "onChange"),
6970
+ onBlur: /* @__PURE__ */ __name(() => setFormData((prev) => ({
6971
+ ...prev,
6972
+ slug: normalizeSlug(prev.slug)
6973
+ })), "onBlur"),
6767
6974
  placeholder: "form-slug"
6768
6975
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
6769
6976
  className: "block text-sm font-medium text-gray-700 mb-1"
@@ -6952,6 +7159,7 @@ var init_FormBuilder = __esm({
6952
7159
  init_switch();
6953
7160
  init_badge();
6954
7161
  init_FieldConfiguration();
7162
+ init_slug_sanitizer();
6955
7163
  __name(FormBuilder, "FormBuilder");
6956
7164
  }
6957
7165
  });
@@ -8001,9 +8209,10 @@ function BlogEditor({ existingBlog, duplicateSource }) {
8001
8209
  setIsSaving(false);
8002
8210
  return;
8003
8211
  }
8212
+ const normalizedSlug = normalizeSlug(slug);
8004
8213
  const body = {
8005
8214
  title,
8006
- slug,
8215
+ slug: normalizedSlug,
8007
8216
  content,
8008
8217
  published: isPublishing,
8009
8218
  tags,
@@ -8273,7 +8482,8 @@ function BlogEditor({ existingBlog, duplicateSource }) {
8273
8482
  type: "text",
8274
8483
  placeholder: "blog-post-url",
8275
8484
  value: slug,
8276
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
8485
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
8486
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
8277
8487
  className: "h-8 text-sm"
8278
8488
  }), /* @__PURE__ */ React.createElement("p", {
8279
8489
  className: "text-xs text-gray-500 mt-1"
@@ -8366,6 +8576,7 @@ var init_BlogEditorPage = __esm({
8366
8576
  init_JoditRichText();
8367
8577
  init_CategoryAutocomplete();
8368
8578
  init_UserAutocomplete();
8579
+ init_slug_sanitizer();
8369
8580
  __name(BlogEditor, "BlogEditor");
8370
8581
  }
8371
8582
  });
@@ -25967,7 +26178,9 @@ function OrderDiscountPanel({ orderLines, currency, contactId, appliedCoupons, o
25967
26178
  if (!res.ok) return;
25968
26179
  const data = await res.json();
25969
26180
  const rows = Array.isArray(data?.data) ? data.data : [];
25970
- if (!cancelled) setDiscounts(rows);
26181
+ const now = Date.now();
26182
+ const activeRows = rows.filter((d) => !d.validUntil || new Date(d.validUntil).getTime() > now);
26183
+ if (!cancelled) setDiscounts(activeRows);
25971
26184
  } catch {
25972
26185
  } finally {
25973
26186
  if (!cancelled) setLoading(false);
@@ -26433,6 +26646,7 @@ function OrderPlacementPage({ editOrderId }) {
26433
26646
  const listReturnUrl = safeAdminListReturnUrl(searchParams.get("from")) ?? "/admin/orders";
26434
26647
  const [customers, setCustomers] = React26.useState([]);
26435
26648
  const [customersLoading, setCustomersLoading] = React26.useState(true);
26649
+ const [selectedRowId, setSelectedRowId] = React26.useState(null);
26436
26650
  const [contactId, setContactId] = React26.useState(null);
26437
26651
  const [selectedCustomerId, setSelectedCustomerId] = React26.useState(null);
26438
26652
  const [contactName, setContactName] = React26.useState("");
@@ -26736,9 +26950,11 @@ function OrderPlacementPage({ editOrderId }) {
26736
26950
  const rows = (Array.isArray(data?.data) ? data.data : []).map((row) => {
26737
26951
  const id = Number(row.id);
26738
26952
  if (!Number.isFinite(id)) return null;
26739
- const rawCustomerId = Number(row.customerId);
26953
+ const rawContactId = Number(row.contactId ?? row.contact?.id);
26954
+ const rawCustomerId = Number(row.customerId ?? row.customer?.id);
26740
26955
  return {
26741
26956
  id,
26957
+ contactId: Number.isFinite(rawContactId) && rawContactId > 0 ? rawContactId : null,
26742
26958
  customerId: Number.isFinite(rawCustomerId) && rawCustomerId > 0 ? rawCustomerId : null,
26743
26959
  name: row.name ?? null,
26744
26960
  email: row.email ?? null,
@@ -26756,6 +26972,15 @@ function OrderPlacementPage({ editOrderId }) {
26756
26972
  cancelled = true;
26757
26973
  };
26758
26974
  }, []);
26975
+ React26.useEffect(() => {
26976
+ if (contactId == null && !contactEmail) return;
26977
+ const match = customers.find((c) => contactId != null && c.contactId === contactId || contactEmail && c.email?.toLowerCase() === contactEmail.toLowerCase());
26978
+ if (match) setSelectedRowId(match.id);
26979
+ }, [
26980
+ contactId,
26981
+ contactEmail,
26982
+ customers
26983
+ ]);
26759
26984
  async function loadCustomerContacts(customerId) {
26760
26985
  setCustomerContactsLoading(true);
26761
26986
  setCustomerContacts([]);
@@ -26836,6 +27061,7 @@ function OrderPlacementPage({ editOrderId }) {
26836
27061
  __name(loadContactAddresses, "loadContactAddresses");
26837
27062
  function handleCustomerSelect(value) {
26838
27063
  if (value === MANUAL_CUSTOMER) {
27064
+ setSelectedRowId(null);
26839
27065
  setContactId(null);
26840
27066
  setSelectedCustomerId(null);
26841
27067
  setCustomerContacts([]);
@@ -26843,6 +27069,9 @@ function OrderPlacementPage({ editOrderId }) {
26843
27069
  setSubContactName("");
26844
27070
  setSubContactEmail("");
26845
27071
  setSubContactPhone("");
27072
+ setContactName("");
27073
+ setContactEmail("");
27074
+ setContactPhone("");
26846
27075
  setBillingAddress({
26847
27076
  ...emptyAddress
26848
27077
  });
@@ -26851,11 +27080,12 @@ function OrderPlacementPage({ editOrderId }) {
26851
27080
  });
26852
27081
  return;
26853
27082
  }
26854
- const id = Number(value);
26855
- if (!Number.isFinite(id)) return;
26856
- const customer = customers.find((c) => c.id === id);
27083
+ const rowId = Number(value);
27084
+ if (!Number.isFinite(rowId)) return;
27085
+ const customer = customers.find((c) => c.id === rowId);
26857
27086
  if (!customer) return;
26858
- setContactId(id);
27087
+ setSelectedRowId(rowId);
27088
+ setContactId(customer.contactId ?? null);
26859
27089
  setSelectedCustomerId(customer.customerId ?? null);
26860
27090
  setContactName(customer.name ?? "");
26861
27091
  setContactEmail(customer.email ?? "");
@@ -26866,7 +27096,9 @@ function OrderPlacementPage({ editOrderId }) {
26866
27096
  setSubContactPhone("");
26867
27097
  setPlaceForSomeoneElse(false);
26868
27098
  if (customer.customerId != null) loadCustomerContacts(customer.customerId);
26869
- if (customer.email) {
27099
+ if (customer.contactId != null) {
27100
+ loadContactAddresses(customer.contactId);
27101
+ } else if (customer.email) {
26870
27102
  loadContactAddressesByEmail(customer.email);
26871
27103
  } else {
26872
27104
  setBillingAddress({
@@ -26952,14 +27184,55 @@ function OrderPlacementPage({ editOrderId }) {
26952
27184
  status: "available"
26953
27185
  });
26954
27186
  if (q.length >= 2) params.set("search", q);
26955
- const res = await fetch(`/api/products?${params}`);
27187
+ const [res, epRes, evRes] = await Promise.all([
27188
+ fetch(`/api/products?${params}`),
27189
+ fetch("/api/event_products?limit=500").catch(() => null),
27190
+ fetch("/api/events?limit=500").catch(() => null)
27191
+ ]);
26956
27192
  if (!res.ok) {
26957
27193
  setProductHits([]);
26958
27194
  return;
26959
27195
  }
26960
27196
  const body = await res.json();
26961
27197
  const hits = Array.isArray(body.data) ? body.data : [];
26962
- hits.forEach((p) => productCache.current.set(p.id, p));
27198
+ const eventMap = /* @__PURE__ */ new Map();
27199
+ if (evRes && evRes.ok) {
27200
+ const evBody = await evRes.json().catch(() => ({}));
27201
+ const evList = Array.isArray(evBody.data) ? evBody.data : [];
27202
+ for (const ev of evList) {
27203
+ if (ev && ev.id) eventMap.set(Number(ev.id), ev);
27204
+ }
27205
+ }
27206
+ const productEventLimits = /* @__PURE__ */ new Map();
27207
+ if (epRes && epRes.ok) {
27208
+ const epBody = await epRes.json().catch(() => ({}));
27209
+ const epList = Array.isArray(epBody.data) ? epBody.data : [];
27210
+ for (const ep of epList) {
27211
+ const ev = eventMap.get(Number(ep.eventId));
27212
+ if (ev) {
27213
+ const maxGroup = typeof ev.maxGroupPurchaseQuantity === "number" && Number.isFinite(ev.maxGroupPurchaseQuantity) && ev.maxGroupPurchaseQuantity > 0 ? Math.floor(ev.maxGroupPurchaseQuantity) : null;
27214
+ const allowsGroup = ev.allowGroupOrders !== false;
27215
+ let cap;
27216
+ if (maxGroup !== null && maxGroup > 1) {
27217
+ cap = maxGroup;
27218
+ } else if (!allowsGroup) {
27219
+ cap = 1;
27220
+ } else if (maxGroup !== null) {
27221
+ cap = maxGroup;
27222
+ } else {
27223
+ cap = 999;
27224
+ }
27225
+ productEventLimits.set(Number(ep.productId), cap);
27226
+ }
27227
+ }
27228
+ }
27229
+ hits.forEach((p) => {
27230
+ const cap = productEventLimits.get(p.id);
27231
+ if (cap !== void 0) {
27232
+ p.maxPurchaseLimit = cap;
27233
+ }
27234
+ productCache.current.set(p.id, p);
27235
+ });
26963
27236
  setProductHits(hits);
26964
27237
  } catch {
26965
27238
  setProductHits([]);
@@ -27236,6 +27509,7 @@ function OrderPlacementPage({ editOrderId }) {
27236
27509
  sonner.toast.message("Select one or more products in the list first");
27237
27510
  return;
27238
27511
  }
27512
+ let reachedLimit = false;
27239
27513
  setLines((prev) => {
27240
27514
  const next = [
27241
27515
  ...prev
@@ -27243,29 +27517,47 @@ function OrderPlacementPage({ editOrderId }) {
27243
27517
  for (const id of stagedIds) {
27244
27518
  const hit = productCache.current.get(id);
27245
27519
  if (!hit) continue;
27520
+ const maxAllowed = typeof hit.maxPurchaseLimit === "number" && hit.maxPurchaseLimit > 0 ? hit.maxPurchaseLimit : 99999;
27246
27521
  const idx = next.findIndex((l) => l.productId === id);
27247
27522
  if (idx >= 0) {
27248
27523
  const row = next[idx];
27249
- next[idx] = {
27250
- ...row,
27251
- quantity: row.quantity + 1
27252
- };
27253
- } else next.push({
27254
- key: String(id),
27255
- productId: id,
27256
- label: hit.name ?? `Product #${id}`,
27257
- sku: hit.sku,
27258
- quantity: 1
27259
- });
27524
+ if (row.quantity >= maxAllowed) {
27525
+ reachedLimit = true;
27526
+ } else {
27527
+ next[idx] = {
27528
+ ...row,
27529
+ quantity: Math.min(maxAllowed, row.quantity + 1)
27530
+ };
27531
+ }
27532
+ } else {
27533
+ next.push({
27534
+ key: String(id),
27535
+ productId: id,
27536
+ label: hit.name ?? `Product #${id}`,
27537
+ sku: hit.sku,
27538
+ quantity: 1
27539
+ });
27540
+ }
27260
27541
  }
27261
27542
  return next;
27262
27543
  });
27263
27544
  setStagedIds(/* @__PURE__ */ new Set());
27264
- sonner.toast.success("Added to cart");
27545
+ if (reachedLimit) {
27546
+ sonner.toast.error("One or more items reached their maximum purchase limit");
27547
+ } else {
27548
+ sonner.toast.success("Added to cart");
27549
+ }
27265
27550
  }
27266
27551
  __name(addStagedToCart, "addStagedToCart");
27267
27552
  function updateQty(key, quantity) {
27268
- const q = Math.min(99999, Math.max(1, Math.floor(quantity) || 1));
27553
+ const line = lines.find((l) => l.key === key);
27554
+ const hit = line ? productCache.current.get(line.productId) : null;
27555
+ const maxAllowed = typeof hit?.maxPurchaseLimit === "number" && hit.maxPurchaseLimit > 0 ? hit.maxPurchaseLimit : 99999;
27556
+ const parsed = Math.max(1, Math.floor(quantity) || 1);
27557
+ const q = Math.min(maxAllowed, parsed);
27558
+ if (parsed > maxAllowed) {
27559
+ sonner.toast.error(`Maximum purchase limit for "${line?.label ?? "this product"}" is ${maxAllowed}`);
27560
+ }
27269
27561
  setLines((prev) => prev.map((l) => l.key === key ? {
27270
27562
  ...l,
27271
27563
  quantity: q
@@ -27315,6 +27607,13 @@ function OrderPlacementPage({ editOrderId }) {
27315
27607
  sonner.toast.error("Add at least one product to the cart");
27316
27608
  return;
27317
27609
  }
27610
+ for (const line of lines) {
27611
+ const hit = productCache.current.get(line.productId);
27612
+ if (hit?.maxPurchaseLimit && line.quantity > hit.maxPurchaseLimit) {
27613
+ sonner.toast.error(`Quantity for "${line.label}" cannot exceed maximum allowed limit of ${hit.maxPurchaseLimit}`);
27614
+ return;
27615
+ }
27616
+ }
27318
27617
  const unknown = preview?.lines.filter((l) => !l.found) ?? [];
27319
27618
  if (unknown.length > 0) {
27320
27619
  sonner.toast.error(`One or more items could not be validated: ${unknown.map((u) => u.productName).join(", ")}`);
@@ -27344,6 +27643,9 @@ function OrderPlacementPage({ editOrderId }) {
27344
27643
  const orderContactId = resolvedContactId;
27345
27644
  const updatedMetadata = {
27346
27645
  ...orderMetadata,
27646
+ customerName: effectiveName || void 0,
27647
+ customerEmail: effectiveEmail || void 0,
27648
+ customerPhone: effectivePhone || void 0,
27347
27649
  orderDate: orderDate || void 0,
27348
27650
  bookingDate: bookingDate || void 0,
27349
27651
  serviceDate: bookingDate || void 0,
@@ -27372,13 +27674,14 @@ function OrderPlacementPage({ editOrderId }) {
27372
27674
  })
27373
27675
  };
27374
27676
  if (orderContactId != null) payload.contactId = orderContactId;
27375
- if (!placeForSomeoneElse && contactId != null) payload.customerId = contactId;
27677
+ if (!placeForSomeoneElse && selectedCustomerId != null) payload.customerId = selectedCustomerId;
27376
27678
  if (placeForSomeoneElse && selectedCustomerId != null) {
27377
27679
  payload.accountCustomerId = selectedCustomerId;
27378
27680
  }
27379
27681
  if (coupons.length > 0) payload.discountId = coupons[0].discountId;
27380
- if (!editOrderId && orderDate) {
27381
- payload.createdAt = `${orderDate}T06:30:00.000Z`;
27682
+ const todayIso = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
27683
+ if (!editOrderId && orderDate && orderDate !== todayIso) {
27684
+ payload.createdAt = `${orderDate}T${(/* @__PURE__ */ new Date()).toISOString().slice(11)}`;
27382
27685
  }
27383
27686
  const url = editOrderId ? `/api/orders/${editOrderId}` : "/api/orders";
27384
27687
  const method = editOrderId ? "PUT" : "POST";
@@ -27465,7 +27768,7 @@ function OrderPlacementPage({ editOrderId }) {
27465
27768
  }
27466
27769
  __name(handleSubmit, "handleSubmit");
27467
27770
  const allVisibleStaged = productHits.length > 0 && productHits.every((p) => stagedIds.has(p.id));
27468
- const customerSelectValue = contactId != null ? String(contactId) : MANUAL_CUSTOMER;
27771
+ const customerSelectValue = selectedRowId != null ? String(selectedRowId) : MANUAL_CUSTOMER;
27469
27772
  const subContactSelectValue = subContactId != null ? String(subContactId) : MANUAL_CONTACT;
27470
27773
  if (editOrderId && loadingOrder) {
27471
27774
  return /* @__PURE__ */ React.createElement("div", {
@@ -27977,6 +28280,7 @@ function OrderPlacementPage({ editOrderId }) {
27977
28280
  }, /* @__PURE__ */ React.createElement(Input, {
27978
28281
  type: "number",
27979
28282
  min: 1,
28283
+ max: productCache.current.get(line.productId)?.maxPurchaseLimit ?? void 0,
27980
28284
  className: "h-8 w-14 ml-auto text-right",
27981
28285
  value: line.quantity,
27982
28286
  onChange: /* @__PURE__ */ __name((e) => updateQty(line.key, Number(e.target.value)), "onChange"),
@@ -30577,9 +30881,10 @@ function SaveButton({ pageId, pageData, existingSeoId, onSeoIdChange, onSaved, c
30577
30881
  }
30578
30882
  }
30579
30883
  }
30884
+ const normalizedSlug = normalizeSlug(pageData.slug);
30580
30885
  const payload = {
30581
30886
  title: pageData.title,
30582
- slug: pageData.slug,
30887
+ slug: normalizedSlug,
30583
30888
  content,
30584
30889
  published: pageData.published
30585
30890
  };
@@ -30819,7 +31124,8 @@ function PageBuilderPage({ pageId }) {
30819
31124
  className: "block text-xs font-medium text-gray-600 mb-1"
30820
31125
  }, "Slug *"), /* @__PURE__ */ React26__namespace.default.createElement(Input, {
30821
31126
  value: slug,
30822
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
31127
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
31128
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
30823
31129
  placeholder: "page-url-slug",
30824
31130
  className: "h-8 text-sm"
30825
31131
  })))), /* @__PURE__ */ React26__namespace.default.createElement(RightSidebar, {
@@ -30856,6 +31162,7 @@ var init_PageBuilderPage = __esm({
30856
31162
  init_admin_config_context();
30857
31163
  init_registry();
30858
31164
  init_ImageOrUrlField();
31165
+ init_slug_sanitizer();
30859
31166
  __name(createSelectable, "createSelectable");
30860
31167
  __name(buildEditorResolver, "buildEditorResolver");
30861
31168
  __name(getIcon, "getIcon");
@@ -31089,7 +31396,8 @@ function BrandEditPage({ brandId }) {
31089
31396
  ]);
31090
31397
  return;
31091
31398
  }
31092
- if (!slug.trim()) {
31399
+ const normalizedSlug = normalizeSlug(slug);
31400
+ if (!normalizedSlug) {
31093
31401
  setErrors([
31094
31402
  "Slug is required"
31095
31403
  ]);
@@ -31097,10 +31405,10 @@ function BrandEditPage({ brandId }) {
31097
31405
  }
31098
31406
  setSaving(true);
31099
31407
  try {
31100
- const savedSeoId = await saveSeo(seo, slug.trim(), seoId);
31408
+ const savedSeoId = await saveSeo(seo, normalizedSlug, seoId);
31101
31409
  const payload = {
31102
31410
  name: name.trim(),
31103
- slug: slug.trim(),
31411
+ slug: normalizedSlug,
31104
31412
  description: description || null,
31105
31413
  logo: logo || null,
31106
31414
  active,
@@ -31190,7 +31498,9 @@ function BrandEditPage({ brandId }) {
31190
31498
  }, "Slug *"), /* @__PURE__ */ React.createElement("input", {
31191
31499
  type: "text",
31192
31500
  value: slug,
31193
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
31501
+ placeholder: "brand-slug",
31502
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
31503
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
31194
31504
  className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm",
31195
31505
  required: true
31196
31506
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
@@ -31250,6 +31560,7 @@ var init_BrandEditPage = __esm({
31250
31560
  init_DetailPageHeader();
31251
31561
  init_vendor_scope();
31252
31562
  init_ImageOrUrlField();
31563
+ init_slug_sanitizer();
31253
31564
  isCreate = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
31254
31565
  __name(BrandEditPage, "BrandEditPage");
31255
31566
  }
@@ -32714,11 +33025,21 @@ function ProductEditPage({ productId }) {
32714
33025
  const pcData = await pcRes.json();
32715
33026
  const pcs = Array.isArray(pcData.data) ? pcData.data : [];
32716
33027
  if (pcs.length > 0) {
32717
- setTaxRows(pcs.map((p) => ({
32718
- taxId: p.taxId,
32719
- rate: p.rate != null && p.rate !== "" ? String(p.rate) : ""
32720
- })));
32721
- const firstRefund = pcs[0].refundPolicyId;
33028
+ const validTaxPcs = pcs.filter((p) => p.taxId != null && Number(p.taxId) > 0);
33029
+ if (validTaxPcs.length > 0) {
33030
+ setTaxRows(validTaxPcs.map((p) => ({
33031
+ taxId: p.taxId,
33032
+ rate: p.rate != null && p.rate !== "" ? String(p.rate) : ""
33033
+ })));
33034
+ } else if (!cancelled) {
33035
+ setTaxRows([
33036
+ {
33037
+ taxId: "",
33038
+ rate: ""
33039
+ }
33040
+ ]);
33041
+ }
33042
+ const firstRefund = pcs.find((p) => p.refundPolicyId != null)?.refundPolicyId;
32722
33043
  setRefundPolicyId(firstRefund != null ? Number(firstRefund) : null);
32723
33044
  } else if (!cancelled) {
32724
33045
  setTaxRows([
@@ -32727,6 +33048,7 @@ function ProductEditPage({ productId }) {
32727
33048
  rate: ""
32728
33049
  }
32729
33050
  ]);
33051
+ setRefundPolicyId(null);
32730
33052
  }
32731
33053
  }
32732
33054
  const paRes = await fetch(`/api/product_attributes?productId=${sourceProductId}&limit=100`);
@@ -33103,14 +33425,7 @@ function ProductEditPage({ productId }) {
33103
33425
  if (a == null || b == null) return true;
33104
33426
  return Math.abs(a - b) > 1e-6;
33105
33427
  }, "ratesDiffer");
33106
- const wantedConfig = /* @__PURE__ */ new Map();
33107
- for (const row of taxRows) {
33108
- if (row.taxId === "") continue;
33109
- wantedConfig.set(row.taxId, {
33110
- rate: parseTaxRate(row.rate),
33111
- refundPolicyId
33112
- });
33113
- }
33428
+ const validTaxRows = taxRows.filter((row) => row.taxId !== "");
33114
33429
  console.log("[product_config rows to save]:", taxRows, "refundPolicyId:", refundPolicyId);
33115
33430
  const pcListRes = await fetch(`/api/product_config?productId=${savedId}&limit=200`);
33116
33431
  const pcListData = pcListRes.ok ? await pcListRes.json() : {
@@ -33118,17 +33433,60 @@ function ProductEditPage({ productId }) {
33118
33433
  };
33119
33434
  console.log("[product existing pc rows]:", pcListData);
33120
33435
  const existingPc = Array.isArray(pcListData.data) ? pcListData.data : [];
33121
- for (const ep of existingPc) {
33122
- if (!wantedConfig.has(ep.taxId)) {
33123
- await fetch(`/api/product_config/${ep.id}`, {
33124
- method: "DELETE"
33125
- });
33436
+ if (validTaxRows.length > 0) {
33437
+ const wantedTaxIds = new Set(validTaxRows.map((r) => Number(r.taxId)));
33438
+ for (const ep of existingPc) {
33439
+ if (ep.taxId == null || !wantedTaxIds.has(Number(ep.taxId))) {
33440
+ await fetch(`/api/product_config/${ep.id}`, {
33441
+ method: "DELETE"
33442
+ });
33443
+ }
33126
33444
  }
33127
- }
33128
- const survivors = existingPc.filter((ep) => wantedConfig.has(ep.taxId));
33129
- for (const [taxId, cfg] of wantedConfig) {
33130
- const ep = survivors.find((e) => e.taxId === taxId);
33131
- if (!ep) {
33445
+ for (const row of validTaxRows) {
33446
+ const taxId = Number(row.taxId);
33447
+ const rate = parseTaxRate(row.rate);
33448
+ const ep = existingPc.find((e) => e.taxId != null && Number(e.taxId) === taxId);
33449
+ if (!ep) {
33450
+ await fetch("/api/product_config", {
33451
+ method: "POST",
33452
+ headers: {
33453
+ "Content-Type": "application/json"
33454
+ },
33455
+ body: JSON.stringify({
33456
+ productId: Number(savedId),
33457
+ taxId,
33458
+ rate,
33459
+ refundPolicyId
33460
+ })
33461
+ });
33462
+ } else {
33463
+ const existingRate = ep.rate == null || String(ep.rate).trim() === "" ? null : Number(ep.rate);
33464
+ const er = Number.isFinite(existingRate) ? existingRate : null;
33465
+ const policyChanged = ep.refundPolicyId !== refundPolicyId;
33466
+ if (ratesDiffer(er, rate) || policyChanged) {
33467
+ await fetch(`/api/product_config/${ep.id}`, {
33468
+ method: "PUT",
33469
+ headers: {
33470
+ "Content-Type": "application/json"
33471
+ },
33472
+ body: JSON.stringify({
33473
+ rate,
33474
+ refundPolicyId
33475
+ })
33476
+ });
33477
+ }
33478
+ }
33479
+ }
33480
+ } else if (refundPolicyId != null) {
33481
+ const nullTaxRow = existingPc.find((e) => e.taxId == null);
33482
+ for (const ep of existingPc) {
33483
+ if (ep.id !== nullTaxRow?.id) {
33484
+ await fetch(`/api/product_config/${ep.id}`, {
33485
+ method: "DELETE"
33486
+ });
33487
+ }
33488
+ }
33489
+ if (!nullTaxRow) {
33132
33490
  await fetch("/api/product_config", {
33133
33491
  method: "POST",
33134
33492
  headers: {
@@ -33136,27 +33494,27 @@ function ProductEditPage({ productId }) {
33136
33494
  },
33137
33495
  body: JSON.stringify({
33138
33496
  productId: Number(savedId),
33139
- taxId,
33140
- rate: cfg.rate,
33141
- refundPolicyId: cfg.refundPolicyId
33497
+ taxId: null,
33498
+ rate: null,
33499
+ refundPolicyId
33500
+ })
33501
+ });
33502
+ } else if (nullTaxRow.refundPolicyId !== refundPolicyId) {
33503
+ await fetch(`/api/product_config/${nullTaxRow.id}`, {
33504
+ method: "PUT",
33505
+ headers: {
33506
+ "Content-Type": "application/json"
33507
+ },
33508
+ body: JSON.stringify({
33509
+ refundPolicyId
33142
33510
  })
33143
33511
  });
33144
- } else {
33145
- const existingRate = ep.rate == null || String(ep.rate).trim() === "" ? null : Number(ep.rate);
33146
- const er = Number.isFinite(existingRate) ? existingRate : null;
33147
- const policyChanged = ep.refundPolicyId !== cfg.refundPolicyId;
33148
- if (ratesDiffer(er, cfg.rate) || policyChanged) {
33149
- await fetch(`/api/product_config/${ep.id}`, {
33150
- method: "PUT",
33151
- headers: {
33152
- "Content-Type": "application/json"
33153
- },
33154
- body: JSON.stringify({
33155
- rate: cfg.rate,
33156
- refundPolicyId: cfg.refundPolicyId
33157
- })
33158
- });
33159
- }
33512
+ }
33513
+ } else {
33514
+ for (const ep of existingPc) {
33515
+ await fetch(`/api/product_config/${ep.id}`, {
33516
+ method: "DELETE"
33517
+ });
33160
33518
  }
33161
33519
  }
33162
33520
  if (hasVariants) {
@@ -34072,7 +34430,8 @@ function CollectionEditPage({ collectionId }) {
34072
34430
  ]);
34073
34431
  return;
34074
34432
  }
34075
- if (!slug.trim()) {
34433
+ const normalizedSlug = normalizeSlug(slug);
34434
+ if (!normalizedSlug) {
34076
34435
  setErrors([
34077
34436
  "Slug is required"
34078
34437
  ]);
@@ -34080,10 +34439,10 @@ function CollectionEditPage({ collectionId }) {
34080
34439
  }
34081
34440
  setSaving(true);
34082
34441
  try {
34083
- const savedSeoId = await saveSeo(seo, slug.trim(), seoId);
34442
+ const savedSeoId = await saveSeo(seo, normalizedSlug, seoId);
34084
34443
  const payload = {
34085
34444
  name: name.trim(),
34086
- slug: slug.trim(),
34445
+ slug: normalizedSlug,
34087
34446
  hsn: hsn.trim() || null,
34088
34447
  categoryId: categoryId || null,
34089
34448
  brandId: brandId || null,
@@ -34252,7 +34611,9 @@ function CollectionEditPage({ collectionId }) {
34252
34611
  }, "Slug *"), /* @__PURE__ */ React.createElement("input", {
34253
34612
  type: "text",
34254
34613
  value: slug,
34255
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
34614
+ placeholder: "collection-slug",
34615
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
34616
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
34256
34617
  className: inputCls4,
34257
34618
  required: true
34258
34619
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
@@ -34488,6 +34849,7 @@ var init_CollectionEditPage = __esm({
34488
34849
  init_admin_config_context();
34489
34850
  init_category_related_product_labels();
34490
34851
  init_ImageOrUrlField();
34852
+ init_slug_sanitizer();
34491
34853
  isCreate3 = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
34492
34854
  emptySlide = /* @__PURE__ */ __name(() => ({
34493
34855
  url: "",
@@ -35667,7 +36029,9 @@ var init_event_entity_types = __esm({
35667
36029
  // src/admin/pages/EventEditPage.tsx
35668
36030
  var EventEditPage_exports = {};
35669
36031
  __export(EventEditPage_exports, {
35670
- default: () => EventEditPage
36032
+ default: () => EventEditPage,
36033
+ normalizeSlug: () => normalizeSlug2,
36034
+ sanitizeSlugInput: () => sanitizeSlugInput2
35671
36035
  });
35672
36036
  function RequiredLabel({ children }) {
35673
36037
  return /* @__PURE__ */ React.createElement("label", {
@@ -35676,6 +36040,12 @@ function RequiredLabel({ children }) {
35676
36040
  className: "text-red-600"
35677
36041
  }, "*"));
35678
36042
  }
36043
+ function sanitizeSlugInput2(value) {
36044
+ return value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-");
36045
+ }
36046
+ function normalizeSlug2(value) {
36047
+ return sanitizeSlugInput2(value).replace(/^-+|-+$/g, "");
36048
+ }
35679
36049
  function toIsoOrNull(value, timezone) {
35680
36050
  const trimmed = value.trim();
35681
36051
  if (!trimmed) return null;
@@ -35766,7 +36136,7 @@ function EventEditPage({ eventId }) {
35766
36136
  const [supportContact, setSupportContact] = React26.useState("");
35767
36137
  const [allowGroupOrders, setAllowGroupOrders] = React26.useState(false);
35768
36138
  const [sendIndividualTicketPDFToAttendees, setSendIndividualTicketPDFToAttendees] = React26.useState(false);
35769
- const [maxGroupPurchaseQuantity, setMaxGroupPurchaseQuantity] = React26.useState("");
36139
+ const [maxGroupPurchaseQuantity, setMaxGroupPurchaseQuantity] = React26.useState("1");
35770
36140
  const [sponsors, setSponsors] = React26.useState([]);
35771
36141
  const [agenda, setAgenda] = React26.useState([]);
35772
36142
  const [workshops, setWorkshops] = React26.useState([]);
@@ -35892,9 +36262,10 @@ function EventEditPage({ eventId }) {
35892
36262
  }
35893
36263
  setOfficialWebsiteUrl(data.officialWebsiteUrl ?? "");
35894
36264
  setSupportContact(data.supportContact ?? "");
35895
- setAllowGroupOrders(data.allowGroupOrders ?? false);
36265
+ const loadedAllowGroup = data.allowGroupOrders ?? false;
36266
+ setAllowGroupOrders(loadedAllowGroup);
35896
36267
  setSendIndividualTicketPDFToAttendees(data.sendIndividualTicketPDFToAttendees ?? false);
35897
- setMaxGroupPurchaseQuantity(data.maxGroupPurchaseQuantity != null ? String(data.maxGroupPurchaseQuantity) : "");
36268
+ setMaxGroupPurchaseQuantity(data.maxGroupPurchaseQuantity != null ? String(data.maxGroupPurchaseQuantity) : loadedAllowGroup ? "10" : "1");
35898
36269
  setSponsors(parseNamedListFromApi(data.sponsors, "sponsor"));
35899
36270
  setAgenda(parseNamedListFromApi(data.agenda, "agenda"));
35900
36271
  setWorkshops(parseNamedListFromApi(data.workshops, "workshop"));
@@ -35940,8 +36311,13 @@ function EventEditPage({ eventId }) {
35940
36311
  ]);
35941
36312
  const buildPayload = /* @__PURE__ */ __name(() => {
35942
36313
  const nextErrors = [];
36314
+ const normalizedSlug = normalizeSlug2(slug);
35943
36315
  if (!name.trim()) nextErrors.push("Name is required");
35944
- if (!slug.trim()) nextErrors.push("Slug is required");
36316
+ if (!normalizedSlug) {
36317
+ nextErrors.push("Slug is required");
36318
+ } else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(normalizedSlug)) {
36319
+ nextErrors.push("Slug must contain only lowercase letters, numbers, and hyphens without leading or trailing hyphens or slashes");
36320
+ }
35945
36321
  if (!startDate.trim()) nextErrors.push("Event Start Date & Time is required");
35946
36322
  if (!endDate.trim()) nextErrors.push("Event End Date & Time is required");
35947
36323
  if (startDate.trim() && !timezone.trim()) {
@@ -35967,6 +36343,20 @@ function EventEditPage({ eventId }) {
35967
36343
  nextErrors.push("Event End Date & Time must be after Event Start Date & Time");
35968
36344
  }
35969
36345
  }
36346
+ if (registrationOpensAt.trim() && registrationClosesAt.trim()) {
36347
+ const regOpen = new Date(toIsoOrNull(registrationOpensAt, timezone) ?? "");
36348
+ const regClose = new Date(toIsoOrNull(registrationClosesAt, timezone) ?? "");
36349
+ if (!Number.isNaN(regOpen.getTime()) && !Number.isNaN(regClose.getTime()) && regClose < regOpen) {
36350
+ nextErrors.push("Registration Closes Date & Time must be after Registration Opens Date & Time");
36351
+ }
36352
+ }
36353
+ if (registrationClosesAt.trim() && endDate.trim()) {
36354
+ const regClose = new Date(toIsoOrNull(registrationClosesAt, timezone) ?? "");
36355
+ const end = new Date(toIsoOrNull(endDate, timezone) ?? "");
36356
+ if (!Number.isNaN(regClose.getTime()) && !Number.isNaN(end.getTime()) && regClose > end) {
36357
+ nextErrors.push("Registration cannot close after Event End Date & Time");
36358
+ }
36359
+ }
35970
36360
  if (additionalVenueDetails.length > ADDITIONAL_VENUE_MAX_CHARS) {
35971
36361
  nextErrors.push(`Additional Venue Details must be ${ADDITIONAL_VENUE_MAX_CHARS} characters or fewer`);
35972
36362
  }
@@ -35976,7 +36366,7 @@ function EventEditPage({ eventId }) {
35976
36366
  }
35977
36367
  const payload = {
35978
36368
  name: name.trim(),
35979
- slug: slug.trim(),
36369
+ slug: normalizedSlug,
35980
36370
  description: description.trim() || null,
35981
36371
  isActive: create && vendorPortal && approvalOn ? false : isActive,
35982
36372
  comingSoon,
@@ -36004,7 +36394,7 @@ function EventEditPage({ eventId }) {
36004
36394
  supportContact: supportContact.trim() || null,
36005
36395
  allowGroupOrders,
36006
36396
  sendIndividualTicketPDFToAttendees,
36007
- maxGroupPurchaseQuantity: maxGroupPurchaseQuantity.trim() ? Number(maxGroupPurchaseQuantity) : null,
36397
+ maxGroupPurchaseQuantity: allowGroupOrders ? Number(maxGroupPurchaseQuantity) > 0 ? Number(maxGroupPurchaseQuantity) : 10 : 1,
36008
36398
  sponsors: namedListToPayload(sponsors, {
36009
36399
  legacySponsorKeys: true
36010
36400
  }),
@@ -36272,9 +36662,16 @@ function EventEditPage({ eventId }) {
36272
36662
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
36273
36663
  className: labelCls6
36274
36664
  }, "Slug"), /* @__PURE__ */ React.createElement("input", {
36665
+ id: "event-slug",
36275
36666
  type: "text",
36276
36667
  value: slug,
36277
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
36668
+ placeholder: "event-slug",
36669
+ onChange: /* @__PURE__ */ __name((e) => {
36670
+ setSlug(sanitizeSlugInput2(e.target.value));
36671
+ }, "onChange"),
36672
+ onBlur: /* @__PURE__ */ __name(() => {
36673
+ setSlug((prev) => normalizeSlug2(prev));
36674
+ }, "onBlur"),
36278
36675
  className: inputCls6
36279
36676
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
36280
36677
  className: labelCls6
@@ -36534,7 +36931,16 @@ function EventEditPage({ eventId }) {
36534
36931
  }, "Max group purchase quantity"), /* @__PURE__ */ React.createElement("input", {
36535
36932
  type: "number",
36536
36933
  value: maxGroupPurchaseQuantity,
36537
- onChange: /* @__PURE__ */ __name((e) => setMaxGroupPurchaseQuantity(e.target.value), "onChange"),
36934
+ onChange: /* @__PURE__ */ __name((e) => {
36935
+ const val = e.target.value;
36936
+ setMaxGroupPurchaseQuantity(val);
36937
+ const num = Number(val);
36938
+ if (Number.isFinite(num) && num > 1) {
36939
+ setAllowGroupOrders(true);
36940
+ } else if (Number.isFinite(num) && num <= 1) {
36941
+ setAllowGroupOrders(false);
36942
+ }
36943
+ }, "onChange"),
36538
36944
  className: inputCls6,
36539
36945
  min: 1
36540
36946
  })), /* @__PURE__ */ React.createElement("div", {
@@ -36544,7 +36950,18 @@ function EventEditPage({ eventId }) {
36544
36950
  }, /* @__PURE__ */ React.createElement("input", {
36545
36951
  type: "checkbox",
36546
36952
  checked: allowGroupOrders,
36547
- onChange: /* @__PURE__ */ __name((e) => setAllowGroupOrders(e.target.checked), "onChange"),
36953
+ onChange: /* @__PURE__ */ __name((e) => {
36954
+ const checked = e.target.checked;
36955
+ setAllowGroupOrders(checked);
36956
+ if (!checked) {
36957
+ setMaxGroupPurchaseQuantity("1");
36958
+ } else {
36959
+ const current = Number(maxGroupPurchaseQuantity);
36960
+ if (!Number.isFinite(current) || current <= 1) {
36961
+ setMaxGroupPurchaseQuantity("10");
36962
+ }
36963
+ }
36964
+ }, "onChange"),
36548
36965
  className: "h-4 w-4 rounded border-gray-300"
36549
36966
  }), /* @__PURE__ */ React.createElement("span", {
36550
36967
  className: "text-sm text-gray-900"
@@ -36618,6 +37035,8 @@ var init_EventEditPage = __esm({
36618
37035
  labelCls6 = "block text-xs font-medium text-gray-600 mb-1";
36619
37036
  inputCls6 = "w-full rounded-md border border-gray-300 px-2 py-1.5 text-sm";
36620
37037
  __name(RequiredLabel, "RequiredLabel");
37038
+ __name(sanitizeSlugInput2, "sanitizeSlugInput");
37039
+ __name(normalizeSlug2, "normalizeSlug");
36621
37040
  ENTITY_TYPE_OPTIONS = [
36622
37041
  ...EVENT_ENTITY_TYPE_OPTIONS
36623
37042
  ];
@@ -36996,7 +37415,7 @@ function ComboEditPage({ comboId }) {
36996
37415
  const resolvedEventId = trimmedEventId && /^\d+$/.test(trimmedEventId) ? Number(trimmedEventId) : null;
36997
37416
  const payload = {
36998
37417
  name: trimmedName,
36999
- slug: slug.trim() || void 0,
37418
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
37000
37419
  desc: desc || null,
37001
37420
  eventId: resolvedEventId,
37002
37421
  price,
@@ -37091,8 +37510,9 @@ function ComboEditPage({ comboId }) {
37091
37510
  }, "Slug"), /* @__PURE__ */ React.createElement("input", {
37092
37511
  type: "text",
37093
37512
  value: slug,
37094
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
37095
- placeholder: "Auto generate from name",
37513
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
37514
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
37515
+ placeholder: "combo-slug",
37096
37516
  className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
37097
37517
  })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
37098
37518
  className: "block text-xs font-medium text-gray-600 mb-1"
@@ -37258,6 +37678,7 @@ var init_ComboEditPage = __esm({
37258
37678
  init_DetailPageHeader();
37259
37679
  init_inventory_validation();
37260
37680
  init_admin_config_context();
37681
+ init_slug_sanitizer();
37261
37682
  isCreate5 = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
37262
37683
  __name(formatDateTimeLocal, "formatDateTimeLocal");
37263
37684
  __name(fetchEventDefaultCurrency, "fetchEventDefaultCurrency");
@@ -37489,7 +37910,7 @@ function VendorEditPage({ vendorId }) {
37489
37910
  const vendorPayload = /* @__PURE__ */ __name(() => ({
37490
37911
  name: name.trim(),
37491
37912
  legalName: legalName.trim() || null,
37492
- slug: slug.trim() || void 0,
37913
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
37493
37914
  businessType: businessType || null,
37494
37915
  description: description.trim() || null,
37495
37916
  website: website.trim() || null,
@@ -37831,7 +38252,7 @@ function VendorEditPage({ vendorId }) {
37831
38252
  signal: abortController.signal,
37832
38253
  body: JSON.stringify({
37833
38254
  ...vendorPayload(),
37834
- slug: slug.trim(),
38255
+ slug: slug.trim() ? normalizeSlug(slug) : void 0,
37835
38256
  metadata: (() => {
37836
38257
  const next = {
37837
38258
  ...metadata ?? {}
@@ -37954,8 +38375,9 @@ function VendorEditPage({ vendorId }) {
37954
38375
  }, "Slug ", create ? "(optional)" : "*"), /* @__PURE__ */ React26__namespace.default.createElement(Input, {
37955
38376
  id: "vendorSlug",
37956
38377
  value: slug,
37957
- onChange: /* @__PURE__ */ __name((e) => setSlug(e.target.value), "onChange"),
37958
- placeholder: create ? "auto from name" : void 0,
38378
+ onChange: /* @__PURE__ */ __name((e) => setSlug(sanitizeSlugInput(e.target.value)), "onChange"),
38379
+ onBlur: /* @__PURE__ */ __name(() => setSlug((prev) => normalizeSlug(prev)), "onBlur"),
38380
+ placeholder: "vendor-slug",
37959
38381
  className: `mt-1 ${fieldClass}`
37960
38382
  })), /* @__PURE__ */ React26__namespace.default.createElement("div", null, /* @__PURE__ */ React26__namespace.default.createElement(FieldLabel, {
37961
38383
  htmlFor: "businessType"
@@ -38296,6 +38718,7 @@ var init_VendorEditPage = __esm({
38296
38718
  init_checkbox();
38297
38719
  init_dialog();
38298
38720
  init_vendor_profile();
38721
+ init_slug_sanitizer();
38299
38722
  init_vendor_list_config();
38300
38723
  init_vendor_access_denied();
38301
38724
  init_vendor_access_denied();