@infuro/cms-core 1.0.36 → 1.0.39

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 (29) hide show
  1. package/dist/admin.cjs +459 -496
  2. package/dist/admin.js +459 -496
  3. package/dist/api.cjs +36 -36
  4. package/dist/api.js +4 -4
  5. package/dist/auth.cjs +51 -51
  6. package/dist/auth.js +3 -3
  7. package/dist/{chunk-COBORIJH.cjs → chunk-22FFHLTO.cjs} +39 -56
  8. package/dist/{chunk-X6H5VBQ2.cjs → chunk-E6WVP7BK.cjs} +6 -6
  9. package/dist/{chunk-2AEVFOQP.cjs → chunk-ERJK5QXN.cjs} +418 -139
  10. package/dist/{chunk-VMY2S32H.js → chunk-NNQBWDCI.js} +39 -56
  11. package/dist/{chunk-WO2DW4AU.cjs → chunk-OIGCAGH7.cjs} +7 -7
  12. package/dist/{chunk-UF3BYUXY.js → chunk-P7QCLE5W.js} +4 -3
  13. package/dist/{chunk-FG6I64HP.js → chunk-SBRBR3VO.js} +1 -1
  14. package/dist/{chunk-JS2W7XZ4.js → chunk-SFDZMGHD.js} +351 -72
  15. package/dist/{chunk-LI2IXVAY.js → chunk-UXMDSNIG.js} +1 -1
  16. package/dist/{chunk-KPKFNLQQ.cjs → chunk-WMXPKKXU.cjs} +4 -3
  17. package/dist/index.cjs +260 -260
  18. package/dist/index.d.cts +12 -5
  19. package/dist/index.d.ts +12 -5
  20. package/dist/index.js +8 -8
  21. package/dist/migrations/1775500000000-RssFeedsAndArticles.ts +0 -1
  22. package/dist/migrations/1775700000000-JobSchedules.ts +0 -1
  23. package/dist/migrations/1781260000000-RenameWhatsappTriggerBindingsToOrderNotificationBindings.ts +92 -0
  24. package/dist/migrations/1781270000000-ComboVendorId.ts +58 -0
  25. package/dist/{whatsapp-trigger-dispatcher-ONHXI7UY.cjs → order-notification-dispatcher-RU7BTGGX.cjs} +3 -4
  26. package/dist/{whatsapp-trigger-dispatcher-PWFI4FKP.js → order-notification-dispatcher-XWIJYDGA.js} +1 -2
  27. package/dist/{rbac-debug-db-HWP65A4O.cjs → rbac-debug-db-KHG4FJC3.cjs} +2 -2
  28. package/dist/{rbac-debug-db-QJH3AD34.js → rbac-debug-db-QWHOENBR.js} +1 -1
  29. package/package.json +147 -147
package/dist/admin.cjs CHANGED
@@ -454,7 +454,8 @@ var init_vendor_scope = __esm({
454
454
  "vendor_customers",
455
455
  "discounts",
456
456
  "events",
457
- "refund_policies"
457
+ "refund_policies",
458
+ "combos"
458
459
  ]);
459
460
  VENDOR_STORE_RBAC_ENTITIES = /* @__PURE__ */ new Set([
460
461
  ...VENDOR_SCOPED_STORE_ENTITIES,
@@ -491,7 +492,7 @@ var init_admin_config_context = __esm({
491
492
  var CMS_VERSION;
492
493
  var init_cms_version = __esm({
493
494
  "src/lib/cms-version.ts"() {
494
- CMS_VERSION = "1.0.36" ;
495
+ CMS_VERSION = "1.0.39" ;
495
496
  }
496
497
  });
497
498
  function useCatalogCategories(enabled = true) {
@@ -14303,203 +14304,149 @@ var init_email_recipients = __esm({
14303
14304
  __name(serializeEmailRecipients, "serializeEmailRecipients");
14304
14305
  }
14305
14306
  });
14306
-
14307
- // src/message-templates/event-order-defaults.ts
14308
- var EVENT_ORDER_WHATSAPP_PARAM_KEYS, EVENT_ORDER_TEMPLATE_VARIABLE_HINTS;
14309
- var init_event_order_defaults = __esm({
14310
- "src/message-templates/event-order-defaults.ts"() {
14311
- EVENT_ORDER_WHATSAPP_PARAM_KEYS = [
14312
- "customerName",
14313
- "eventName",
14314
- "orderNumber",
14315
- "orderTotal",
14316
- "currency",
14317
- "venue",
14318
- "startDate",
14319
- "trackUrl"
14320
- ];
14321
- EVENT_ORDER_TEMPLATE_VARIABLE_HINTS = {
14322
- eventName: "Event title",
14323
- eventSlug: "Event URL slug",
14324
- venue: "Venue name",
14325
- startDate: "Formatted event start",
14326
- orderNumber: "Order number",
14327
- orderTotal: "Order total amount",
14328
- currency: "Currency code",
14329
- customerName: "Buyer name",
14330
- customerEmail: "Buyer email",
14331
- customerPhone: "Buyer phone",
14332
- productNames: "Comma-separated ticket names",
14333
- trackUrl: "Public order tracking link (QR destination)",
14334
- qrImageUrl: 'QR code image URL for email (use in <img src="\u2026">)',
14335
- orderDetails: "Plain-text order summary with line items",
14336
- orderDetailsHtml: "HTML order summary block",
14337
- trackUrlWhatsApp: "Same as trackUrl \u2014 include in Meta template as last param"
14338
- };
14307
+ function getEmailTriggerDefaults(triggerKey) {
14308
+ return EMAIL_TRIGGER_DEFAULTS[triggerKey] ?? {
14309
+ subject: "",
14310
+ body: "",
14311
+ subjectPlaceholder: "e.g. Order update for {{orderNumber}}",
14312
+ bodyPlaceholder: "Hi {{customerName}},\n\nYour order {{orderNumber}} has been updated."
14313
+ };
14314
+ }
14315
+ async function jsonFetch(url, init) {
14316
+ const res = await fetch(url, {
14317
+ credentials: "include",
14318
+ ...init
14319
+ });
14320
+ const data = await res.json().catch(() => ({}));
14321
+ if (!res.ok) {
14322
+ throw new Error(data?.error || `Request failed (${res.status})`);
14339
14323
  }
14340
- });
14341
- function mergeChannelUpdate(allItems, channel, updatedChannelItems) {
14342
- return [
14343
- ...allItems.filter((item) => item.channel !== channel),
14344
- ...updatedChannelItems
14345
- ];
14324
+ return data;
14346
14325
  }
14347
- function EventOrderGlobalTemplates({ items, onChange, channel, settingsGroup = "notifications" }) {
14348
- const [templateVariables, setTemplateVariables] = React26.useState([]);
14349
- const copy = CHANNEL_COPY[channel];
14350
- const visibleItems = React26.useMemo(() => items.filter((item) => item.channel === channel), [
14351
- items,
14326
+ function useOrderNotificationTriggerBindings(channel) {
14327
+ const [triggers, setTriggers] = React26.useState([]);
14328
+ const [bindings, setBindings] = React26.useState([]);
14329
+ const [loading, setLoading] = React26.useState(true);
14330
+ const load = React26.useCallback(async () => {
14331
+ setLoading(true);
14332
+ try {
14333
+ const [triggersRes, bindingsRes] = await Promise.all([
14334
+ jsonFetch("/api/order_notification_triggers?limit=100&sortField=sortOrder&sortOrder=asc"),
14335
+ // Bindings for all channels are fetched and filtered client-side rather
14336
+ // than relying on server-side ?channel= filtering, since that's not
14337
+ // confirmed to be supported by the generic CRUD layer.
14338
+ jsonFetch("/api/order_notification_bindings?limit=200")
14339
+ ]);
14340
+ setTriggers((triggersRes.data ?? []).filter((t) => t.enabled));
14341
+ setBindings((bindingsRes.data ?? []).filter((b) => b.channel === channel));
14342
+ } catch (e) {
14343
+ sonner.toast.error(e instanceof Error ? e.message : "Failed to load trigger bindings");
14344
+ } finally {
14345
+ setLoading(false);
14346
+ }
14347
+ }, [
14352
14348
  channel
14353
14349
  ]);
14354
- const displayVariables = React26.useMemo(() => {
14355
- if (channel === "email") return EMAIL_VARIABLES;
14356
- return [
14357
- ...EVENT_ORDER_WHATSAPP_PARAM_KEYS
14358
- ];
14350
+ React26.useEffect(() => {
14351
+ void load();
14359
14352
  }, [
14353
+ load
14354
+ ]);
14355
+ const rebind = React26.useCallback(async (triggerKey, messageTemplateId, templateLabel) => {
14356
+ const existing = bindings.find((b) => b.triggerKey === triggerKey);
14357
+ try {
14358
+ if (existing) {
14359
+ const updated = await jsonFetch(`/api/order_notification_bindings/${existing.id}`, {
14360
+ method: "PUT",
14361
+ headers: {
14362
+ "Content-Type": "application/json"
14363
+ },
14364
+ body: JSON.stringify({
14365
+ messageTemplateId
14366
+ })
14367
+ });
14368
+ setBindings((prev) => prev.map((b) => b.id === updated.id ? updated : b));
14369
+ } else {
14370
+ const created = await jsonFetch("/api/order_notification_bindings", {
14371
+ method: "POST",
14372
+ headers: {
14373
+ "Content-Type": "application/json"
14374
+ },
14375
+ body: JSON.stringify({
14376
+ triggerKey,
14377
+ channel,
14378
+ messageTemplateId,
14379
+ enabled: true
14380
+ })
14381
+ });
14382
+ setBindings((prev) => [
14383
+ ...prev,
14384
+ created
14385
+ ]);
14386
+ }
14387
+ sonner.toast.success(`"${triggerKey}" bound to "${templateLabel}"`);
14388
+ return true;
14389
+ } catch (e) {
14390
+ sonner.toast.error(e instanceof Error ? e.message : "Failed to update binding");
14391
+ return false;
14392
+ }
14393
+ }, [
14394
+ bindings,
14360
14395
  channel
14361
14396
  ]);
14362
- React26.useEffect(() => {
14363
- fetch("/api/message-templates/event-order").then((r) => r.ok ? r.json() : null).then((data) => {
14364
- if (data?.templateVariables) setTemplateVariables(data.templateVariables);
14365
- }).catch(() => void 0);
14366
- }, []);
14367
- const patchVisible = /* @__PURE__ */ __name((nextVisible) => {
14368
- onChange(mergeChannelUpdate(items, channel, nextVisible));
14369
- }, "patchVisible");
14370
- if (!visibleItems.length) return null;
14371
- return /* @__PURE__ */ React.createElement("div", {
14372
- className: "space-y-3 rounded-md border border-gray-200 dark:border-gray-600 p-3"
14373
- }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", {
14374
- className: "text-sm font-medium text-gray-900 dark:text-white"
14375
- }, copy.title), /* @__PURE__ */ React.createElement("p", {
14376
- className: "text-xs text-gray-500 dark:text-gray-400 mt-1"
14377
- }, copy.description)), /* @__PURE__ */ React.createElement("div", {
14378
- className: "rounded-md bg-gray-50 dark:bg-gray-800/50 p-2"
14379
- }, /* @__PURE__ */ React.createElement("p", {
14380
- className: "text-[11px] font-medium text-gray-600 dark:text-gray-400 mb-1"
14381
- }, "Variables"), /* @__PURE__ */ React.createElement("div", {
14382
- className: "flex flex-wrap gap-1"
14383
- }, (displayVariables.length ? displayVariables : templateVariables).map((v) => /* @__PURE__ */ React.createElement("span", {
14384
- key: v,
14385
- className: "inline-flex rounded border border-gray-200 bg-white px-1.5 py-0.5 font-mono text-[10px] text-gray-700 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-300",
14386
- title: EVENT_ORDER_TEMPLATE_VARIABLE_HINTS[v] ?? v
14387
- }, `{{${v}}}`)))), visibleItems.map((t) => /* @__PURE__ */ React.createElement("div", {
14388
- key: t.channel,
14389
- className: "space-y-3 border-t border-gray-100 pt-3 dark:border-gray-700"
14390
- }, /* @__PURE__ */ React.createElement("div", {
14391
- className: "flex flex-wrap items-center justify-between gap-2"
14392
- }, /* @__PURE__ */ React.createElement("div", {
14393
- className: "text-sm font-medium"
14394
- }, t.name), /* @__PURE__ */ React.createElement("div", {
14395
- className: "flex items-center gap-2"
14396
- }, /* @__PURE__ */ React.createElement(Label3, {
14397
- htmlFor: `${settingsGroup}-evt-${t.channel}-enabled`,
14398
- className: "text-xs text-gray-600"
14399
- }, "Enabled"), /* @__PURE__ */ React.createElement(Switch, {
14400
- id: `${settingsGroup}-evt-${t.channel}-enabled`,
14401
- checked: t.enabled,
14402
- onCheckedChange: /* @__PURE__ */ __name((v) => patchVisible(visibleItems.map((x) => x.channel === t.channel ? {
14403
- ...x,
14404
- enabled: v
14405
- } : x)), "onCheckedChange")
14406
- }))), t.channel === "email" ? /* @__PURE__ */ React.createElement("div", {
14407
- className: "space-y-1"
14408
- }, /* @__PURE__ */ React.createElement(Label3, {
14409
- className: "text-xs"
14410
- }, "Subject"), /* @__PURE__ */ React.createElement(Input, {
14411
- value: t.subject,
14412
- onChange: /* @__PURE__ */ __name((e) => patchVisible(visibleItems.map((x) => x.channel === t.channel ? {
14413
- ...x,
14414
- subject: e.target.value
14415
- } : x)), "onChange"),
14416
- className: "h-8 text-sm"
14417
- })) : null, /* @__PURE__ */ React.createElement("div", {
14418
- className: "space-y-1"
14419
- }, /* @__PURE__ */ React.createElement(Label3, {
14420
- className: "text-xs"
14421
- }, t.channel === "email" ? "Body (HTML)" : "Body (reference only)"), /* @__PURE__ */ React.createElement(Textarea, {
14422
- value: t.body,
14423
- onChange: /* @__PURE__ */ __name((e) => patchVisible(visibleItems.map((x) => x.channel === t.channel ? {
14424
- ...x,
14425
- body: e.target.value
14426
- } : x)), "onChange"),
14427
- rows: t.channel === "email" ? 8 : 3,
14428
- className: "text-sm font-mono"
14429
- })), t.channel === "whatsapp" ? /* @__PURE__ */ React.createElement("div", {
14430
- className: "space-y-1"
14431
- }, /* @__PURE__ */ React.createElement(Label3, {
14432
- className: "text-xs"
14433
- }, "Meta approved template name"), /* @__PURE__ */ React.createElement(Input, {
14434
- value: t.externalTemplateRef,
14435
- onChange: /* @__PURE__ */ __name((e) => patchVisible(visibleItems.map((x) => x.channel === t.channel ? {
14436
- ...x,
14437
- externalTemplateRef: e.target.value
14438
- } : x)), "onChange"),
14439
- placeholder: "event_order_placed",
14440
- className: "h-8 text-sm"
14441
- })) : null)));
14442
- }
14443
- async function fetchEventOrderTemplateRows() {
14444
- const res = await fetch("/api/message-templates/event-order");
14445
- if (!res.ok) return [];
14446
- const data = await res.json();
14447
- return data.items ?? [];
14448
- }
14449
- async function saveEventOrderTemplateRows(items) {
14450
- const res = await fetch("/api/message-templates/event-order", {
14451
- method: "PUT",
14452
- headers: {
14453
- "Content-Type": "application/json"
14454
- },
14455
- body: JSON.stringify({
14456
- items: items.map((t) => ({
14457
- channel: t.channel,
14458
- subject: t.subject,
14459
- body: t.body,
14460
- externalTemplateRef: t.externalTemplateRef,
14461
- enabled: t.enabled
14462
- }))
14463
- })
14464
- });
14465
- return res.ok;
14397
+ return {
14398
+ triggers,
14399
+ bindings,
14400
+ loading,
14401
+ reload: load,
14402
+ rebind
14403
+ };
14466
14404
  }
14467
- var CHANNEL_COPY, EMAIL_VARIABLES;
14468
- var init_EventOrderGlobalTemplates = __esm({
14469
- "src/admin/components/EventOrderGlobalTemplates.tsx"() {
14405
+ var KNOWN_VARS, EMAIL_TRIGGER_DEFAULTS;
14406
+ var init_order_notification_bindings_shared = __esm({
14407
+ "src/admin/components/order-notification-bindings-shared.ts"() {
14470
14408
  "use client";
14471
- init_label();
14472
- init_input();
14473
- init_textarea();
14474
- init_switch();
14475
- init_event_order_defaults();
14476
- CHANNEL_COPY = {
14477
- email: {
14478
- title: "Email template",
14479
- description: "HTML email with order details and QR code. Use {{qrImageUrl}} for the QR image and {{orderDetailsHtml}} for line items."
14480
- },
14481
- whatsapp: {
14482
- title: "WhatsApp template",
14483
- description: "Must match a Meta-approved template. Body params are sent in order: " + EVENT_ORDER_WHATSAPP_PARAM_KEYS.join(", ") + "."
14484
- }
14485
- };
14486
- EMAIL_VARIABLES = [
14487
- "customerName",
14488
- "eventName",
14409
+ KNOWN_VARS = [
14410
+ "orderId",
14489
14411
  "orderNumber",
14490
- "orderTotal",
14412
+ "customerName",
14413
+ "customerPhone",
14414
+ "total",
14491
14415
  "currency",
14492
- "productNames",
14493
- "orderDetailsHtml",
14494
- "venue",
14495
- "startDate",
14496
- "qrImageUrl",
14497
- "trackUrl"
14416
+ "vendorId",
14417
+ "refundAmount"
14498
14418
  ];
14499
- __name(mergeChannelUpdate, "mergeChannelUpdate");
14500
- __name(EventOrderGlobalTemplates, "EventOrderGlobalTemplates");
14501
- __name(fetchEventOrderTemplateRows, "fetchEventOrderTemplateRows");
14502
- __name(saveEventOrderTemplateRows, "saveEventOrderTemplateRows");
14419
+ EMAIL_TRIGGER_DEFAULTS = {
14420
+ order_placed: {
14421
+ subject: "Your order {{orderNumber}} is confirmed",
14422
+ subjectPlaceholder: "e.g. Your order {{orderNumber}} is confirmed",
14423
+ body: `Hi {{customerName}},
14424
+
14425
+ Thank you for your order! Your order {{orderNumber}} for {{total}} {{currency}} has been confirmed.
14426
+
14427
+ We'll keep you updated on any changes.
14428
+
14429
+ Thank you!`,
14430
+ bodyPlaceholder: "Hi {{customerName}},\n\nYour order {{orderNumber}} for {{total}} {{currency}} is confirmed.\n\nThank you!"
14431
+ },
14432
+ order_cancelled: {
14433
+ subject: "Your order {{orderNumber}} has been cancelled",
14434
+ subjectPlaceholder: "e.g. Your order {{orderNumber}} has been cancelled",
14435
+ body: `Hi {{customerName}},
14436
+
14437
+ Your order {{orderNumber}} for {{total}} {{currency}} has been cancelled.
14438
+
14439
+ If a refund is due, you will receive {{refundAmount}} {{currency}} back to your original payment method.
14440
+
14441
+ If you have any questions, please contact us.
14442
+
14443
+ Thank you!`,
14444
+ bodyPlaceholder: "Hi {{customerName}},\n\nYour order {{orderNumber}} has been cancelled.\n\nIf a refund is due, it will be returned to your original payment method."
14445
+ }
14446
+ };
14447
+ __name(getEmailTriggerDefaults, "getEmailTriggerDefaults");
14448
+ __name(jsonFetch, "jsonFetch");
14449
+ __name(useOrderNotificationTriggerBindings, "useOrderNotificationTriggerBindings");
14503
14450
  }
14504
14451
  });
14505
14452
  function varCount(t) {
@@ -14545,22 +14492,10 @@ function contextBeforePlaceholder(bodyText, n) {
14545
14492
  if (idx === -1) return "";
14546
14493
  return bodyText.slice(Math.max(0, idx - 25), idx);
14547
14494
  }
14548
- async function jsonFetch(url, init) {
14549
- const res = await fetch(url, {
14550
- credentials: "include",
14551
- ...init
14552
- });
14553
- const data = await res.json().catch(() => ({}));
14554
- if (!res.ok) {
14555
- throw new Error(data?.error || `Request failed (${res.status})`);
14556
- }
14557
- return data;
14558
- }
14559
14495
  function WhatsAppTriggerBindings() {
14560
- const [triggers, setTriggers] = React26.useState([]);
14561
- const [bindings, setBindings] = React26.useState([]);
14496
+ const { triggers, bindings, loading, reload, rebind } = useOrderNotificationTriggerBindings("whatsapp");
14562
14497
  const [templates, setTemplates] = React26.useState([]);
14563
- const [loading, setLoading] = React26.useState(true);
14498
+ const [templatesLoaded, setTemplatesLoaded] = React26.useState(false);
14564
14499
  const [syncing, setSyncing] = React26.useState(false);
14565
14500
  const [editingTemplateId, setEditingTemplateId] = React26.useState(null);
14566
14501
  const templatesById = React26.useMemo(() => {
@@ -14570,27 +14505,20 @@ function WhatsAppTriggerBindings() {
14570
14505
  }, [
14571
14506
  templates
14572
14507
  ]);
14573
- const load = React26.useCallback(async () => {
14574
- setLoading(true);
14508
+ const loadTemplates = /* @__PURE__ */ __name(async () => {
14575
14509
  try {
14576
- const [triggersRes, bindingsRes, templatesRes] = await Promise.all([
14577
- jsonFetch("/api/order_notification_triggers?limit=100&sortField=sortOrder&sortOrder=asc"),
14578
- jsonFetch("/api/whatsapp_trigger_bindings?limit=100"),
14579
- jsonFetch("/api/message-templates/whatsapp")
14580
- ]);
14581
- setTriggers((triggersRes.data ?? []).filter((t) => t.enabled));
14582
- setBindings(bindingsRes.data ?? []);
14583
- setTemplates(templatesRes.items ?? []);
14510
+ const res = await jsonFetch("/api/message-templates/whatsapp");
14511
+ setTemplates(res.items ?? []);
14584
14512
  } catch (e) {
14585
- sonner.toast.error(e instanceof Error ? e.message : "Failed to load WhatsApp trigger bindings");
14513
+ sonner.toast.error(e instanceof Error ? e.message : "Failed to load WhatsApp templates");
14586
14514
  } finally {
14587
- setLoading(false);
14515
+ setTemplatesLoaded(true);
14588
14516
  }
14589
- }, []);
14590
- React26.useEffect(() => {
14591
- void load();
14517
+ }, "loadTemplates");
14518
+ React26.useMemo(() => {
14519
+ if (!templatesLoaded) void loadTemplates();
14592
14520
  }, [
14593
- load
14521
+ templatesLoaded
14594
14522
  ]);
14595
14523
  const handleSync = /* @__PURE__ */ __name(async () => {
14596
14524
  setSyncing(true);
@@ -14598,7 +14526,10 @@ function WhatsAppTriggerBindings() {
14598
14526
  await jsonFetch("/api/message-templates/whatsapp/sync", {
14599
14527
  method: "POST"
14600
14528
  });
14601
- await load();
14529
+ await Promise.all([
14530
+ loadTemplates(),
14531
+ reload()
14532
+ ]);
14602
14533
  sonner.toast.success("Synced templates from Meta");
14603
14534
  } catch (e) {
14604
14535
  sonner.toast.error(e instanceof Error ? e.message : "Sync failed");
@@ -14607,40 +14538,8 @@ function WhatsAppTriggerBindings() {
14607
14538
  }
14608
14539
  }, "handleSync");
14609
14540
  const handleRebind = /* @__PURE__ */ __name(async (triggerKey, messageTemplateId) => {
14610
- const existing = bindings.find((b) => b.triggerKey === triggerKey);
14611
- try {
14612
- if (existing) {
14613
- const updated = await jsonFetch(`/api/whatsapp_trigger_bindings/${existing.id}`, {
14614
- method: "PUT",
14615
- headers: {
14616
- "Content-Type": "application/json"
14617
- },
14618
- body: JSON.stringify({
14619
- messageTemplateId
14620
- })
14621
- });
14622
- setBindings((prev) => prev.map((b) => b.id === updated.id ? updated : b));
14623
- } else {
14624
- const created = await jsonFetch("/api/whatsapp_trigger_bindings", {
14625
- method: "POST",
14626
- headers: {
14627
- "Content-Type": "application/json"
14628
- },
14629
- body: JSON.stringify({
14630
- triggerKey,
14631
- messageTemplateId,
14632
- enabled: true
14633
- })
14634
- });
14635
- setBindings((prev) => [
14636
- ...prev,
14637
- created
14638
- ]);
14639
- }
14640
- sonner.toast.success(`"${triggerKey}" bound to "${templatesById.get(messageTemplateId)?.name ?? messageTemplateId}"`);
14641
- } catch (e) {
14642
- sonner.toast.error(e instanceof Error ? e.message : "Failed to update binding");
14643
- }
14541
+ const label = templatesById.get(messageTemplateId)?.name ?? String(messageTemplateId);
14542
+ await rebind(triggerKey, messageTemplateId, label);
14644
14543
  }, "handleRebind");
14645
14544
  const handleSaveParamOrder = /* @__PURE__ */ __name(async (templateId, paramOrder) => {
14646
14545
  try {
@@ -14660,7 +14559,7 @@ function WhatsAppTriggerBindings() {
14660
14559
  sonner.toast.error(e instanceof Error ? e.message : "Failed to save param order");
14661
14560
  }
14662
14561
  }, "handleSaveParamOrder");
14663
- if (loading) {
14562
+ if (loading || !templatesLoaded) {
14664
14563
  return /* @__PURE__ */ React.createElement("div", {
14665
14564
  className: "flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400"
14666
14565
  }, /* @__PURE__ */ React.createElement(LucideIcons.Loader2, {
@@ -14675,7 +14574,7 @@ function WhatsAppTriggerBindings() {
14675
14574
  className: "text-sm font-medium text-gray-900 dark:text-white"
14676
14575
  }, "Order trigger templates"), /* @__PURE__ */ React.createElement("p", {
14677
14576
  className: "text-xs text-gray-500 dark:text-gray-400 mt-1"
14678
- }, "When an order is placed or cancelled, the matching template is sent on WhatsApp. Sync templates from Meta, then pick one for each trigger below.")), /* @__PURE__ */ React.createElement(Button, {
14577
+ }, "When a trigger fires, the matching template is sent on WhatsApp. Sync templates from Meta, then pick one for each trigger below.")), /* @__PURE__ */ React.createElement(Button, {
14679
14578
  type: "button",
14680
14579
  variant: "outline",
14681
14580
  size: "sm",
@@ -14852,24 +14751,15 @@ function ParamOrderEditor({ template, onCancel, onSave }) {
14852
14751
  onClick: /* @__PURE__ */ __name(() => onSave(values), "onClick")
14853
14752
  }, "Save"))));
14854
14753
  }
14855
- var KNOWN_VARS, CONTEXT_HINTS;
14754
+ var CONTEXT_HINTS;
14856
14755
  var init_whatsappTriggerBindingsPage = __esm({
14857
14756
  "src/admin/pages/whatsappTriggerBindingsPage.tsx"() {
14858
14757
  "use client";
14859
14758
  init_button();
14860
14759
  init_select();
14760
+ init_order_notification_bindings_shared();
14861
14761
  __name(varCount, "varCount");
14862
14762
  __name(bodyExample, "bodyExample");
14863
- KNOWN_VARS = [
14864
- "orderId",
14865
- "orderNumber",
14866
- "customerName",
14867
- "customerPhone",
14868
- "total",
14869
- "currency",
14870
- "vendorId",
14871
- "refundAmount"
14872
- ];
14873
14763
  CONTEXT_HINTS = [
14874
14764
  {
14875
14765
  pattern: /refund/,
@@ -14908,42 +14798,280 @@ var init_whatsappTriggerBindingsPage = __esm({
14908
14798
  __name(guessFromExampleShape, "guessFromExampleShape");
14909
14799
  __name(guessKnownVar, "guessKnownVar");
14910
14800
  __name(contextBeforePlaceholder, "contextBeforePlaceholder");
14911
- __name(jsonFetch, "jsonFetch");
14912
14801
  __name(WhatsAppTriggerBindings, "WhatsAppTriggerBindings");
14913
14802
  __name(ParamOrderEditor, "ParamOrderEditor");
14914
14803
  }
14915
14804
  });
14916
- function EventNotificationsPluginSettings({ settingsGroup, enabled, onEnabledChange, onSaveSettings, saving }) {
14917
- const [tab, setTab] = React26.useState("email");
14918
- const [templateItems, setTemplateItems] = React26.useState([]);
14919
- const [loadingTemplates, setLoadingTemplates] = React26.useState(true);
14805
+ function EmailTriggerBindings() {
14806
+ const { triggers, bindings, loading, rebind } = useOrderNotificationTriggerBindings("email");
14807
+ const [templates, setTemplates] = React26.useState([]);
14808
+ const [templatesLoaded, setTemplatesLoaded] = React26.useState(false);
14809
+ const templatesById = React26.useMemo(() => {
14810
+ const m = /* @__PURE__ */ new Map();
14811
+ for (const t of templates) m.set(t.id, t);
14812
+ return m;
14813
+ }, [
14814
+ templates
14815
+ ]);
14920
14816
  React26.useEffect(() => {
14921
14817
  let cancelled = false;
14922
- setLoadingTemplates(true);
14923
- fetchEventOrderTemplateRows().then((rows) => {
14924
- if (!cancelled) setTemplateItems(rows);
14925
- }).finally(() => {
14926
- if (!cancelled) setLoadingTemplates(false);
14927
- });
14818
+ (async () => {
14819
+ try {
14820
+ const res = await jsonFetch("/api/message-templates/email");
14821
+ if (!cancelled) setTemplates(res.items ?? []);
14822
+ } catch (e) {
14823
+ if (!cancelled) sonner.toast.error(e instanceof Error ? e.message : "Failed to load email templates");
14824
+ } finally {
14825
+ if (!cancelled) setTemplatesLoaded(true);
14826
+ }
14827
+ })();
14928
14828
  return () => {
14929
14829
  cancelled = true;
14930
14830
  };
14931
14831
  }, []);
14932
- const handleSaveAll = /* @__PURE__ */ __name(async () => {
14933
- const settingsOk = await onSaveSettings();
14934
- if (!settingsOk) {
14935
- sonner.toast.error("Failed to save plugin settings");
14936
- return;
14832
+ const handleTemplateSaved = /* @__PURE__ */ __name((template) => {
14833
+ setTemplates((prev) => {
14834
+ const exists = prev.some((t) => t.id === template.id);
14835
+ return exists ? prev.map((t) => t.id === template.id ? template : t) : [
14836
+ ...prev,
14837
+ template
14838
+ ].sort((a, b) => a.name.localeCompare(b.name));
14839
+ });
14840
+ }, "handleTemplateSaved");
14841
+ if (loading || !templatesLoaded) {
14842
+ return /* @__PURE__ */ React.createElement("div", {
14843
+ className: "flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400"
14844
+ }, /* @__PURE__ */ React.createElement(LucideIcons.Loader2, {
14845
+ className: "h-4 w-4 animate-spin"
14846
+ }), "Loading trigger bindings\u2026");
14847
+ }
14848
+ return /* @__PURE__ */ React.createElement("div", {
14849
+ className: "space-y-3 rounded-md border border-gray-200 dark:border-gray-600 p-3"
14850
+ }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", {
14851
+ className: "text-sm font-medium text-gray-900 dark:text-white"
14852
+ }, "Order trigger emails"), /* @__PURE__ */ React.createElement("p", {
14853
+ className: "text-xs text-gray-500 dark:text-gray-400 mt-1"
14854
+ }, "Write the subject and body for each trigger below. Use", " ", /* @__PURE__ */ React.createElement("code", {
14855
+ className: "text-[11px]"
14856
+ }, "{{variable}}"), " tokens \u2014 click a variable chip to insert it at your cursor.")), /* @__PURE__ */ React.createElement("div", {
14857
+ className: "space-y-4"
14858
+ }, triggers.length === 0 ? /* @__PURE__ */ React.createElement("p", {
14859
+ className: "text-xs text-amber-600 dark:text-amber-400"
14860
+ }, "No order triggers found. Run database migrations to seed default triggers.") : null, triggers.map(({ triggerKey, label, description }) => {
14861
+ const binding = bindings.find((b) => b.triggerKey === triggerKey) ?? null;
14862
+ const template = binding?.messageTemplateId != null ? templatesById.get(binding.messageTemplateId) : void 0;
14863
+ return /* @__PURE__ */ React.createElement(TriggerEmailEditor, {
14864
+ key: triggerKey,
14865
+ triggerKey,
14866
+ label,
14867
+ description,
14868
+ template,
14869
+ onSaved: handleTemplateSaved,
14870
+ onBind: /* @__PURE__ */ __name((templateId, templateLabel) => rebind(triggerKey, templateId, templateLabel), "onBind")
14871
+ });
14872
+ })));
14873
+ }
14874
+ function TriggerEmailEditor({ triggerKey, label, description, template, onSaved, onBind }) {
14875
+ const defaults = React26.useMemo(() => getEmailTriggerDefaults(triggerKey), [
14876
+ triggerKey
14877
+ ]);
14878
+ const [subject, setSubject] = React26.useState(() => template?.subject ?? defaults.subject);
14879
+ const [body, setBody] = React26.useState(() => template?.body ?? defaults.body);
14880
+ const [saving, setSaving] = React26.useState(false);
14881
+ const [focusedField, setFocusedField] = React26.useState("body");
14882
+ const subjectRef = React26.useRef(null);
14883
+ const bodyRef = React26.useRef(null);
14884
+ React26.useEffect(() => {
14885
+ if (template) {
14886
+ setSubject(template.subject ?? "");
14887
+ setBody(template.body ?? "");
14888
+ } else {
14889
+ setSubject(defaults.subject);
14890
+ setBody(defaults.body);
14937
14891
  }
14938
- if (templateItems.length > 0) {
14939
- const tplOk = await saveEventOrderTemplateRows(templateItems);
14940
- if (!tplOk) {
14941
- sonner.toast.error("Failed to save notification templates");
14892
+ }, [
14893
+ template,
14894
+ defaults.subject,
14895
+ defaults.body
14896
+ ]);
14897
+ const savedSubject = template?.subject ?? defaults.subject;
14898
+ const savedBody = template?.body ?? defaults.body;
14899
+ const isDirty = subject !== savedSubject || body !== savedBody;
14900
+ function insertVariable(v) {
14901
+ const token = `{{${v}}}`;
14902
+ if (focusedField === "subject") {
14903
+ const el = subjectRef.current;
14904
+ const start = el?.selectionStart ?? subject.length;
14905
+ const end = el?.selectionEnd ?? subject.length;
14906
+ const next = subject.slice(0, start) + token + subject.slice(end);
14907
+ setSubject(next);
14908
+ requestAnimationFrame(() => {
14909
+ el?.focus();
14910
+ el?.setSelectionRange(start + token.length, start + token.length);
14911
+ });
14912
+ } else {
14913
+ const el = bodyRef.current;
14914
+ const start = el?.selectionStart ?? body.length;
14915
+ const end = el?.selectionEnd ?? body.length;
14916
+ const next = body.slice(0, start) + token + body.slice(end);
14917
+ setBody(next);
14918
+ requestAnimationFrame(() => {
14919
+ el?.focus();
14920
+ el?.setSelectionRange(start + token.length, start + token.length);
14921
+ });
14922
+ }
14923
+ }
14924
+ __name(insertVariable, "insertVariable");
14925
+ const canSave = body.trim() !== "";
14926
+ const handleSave = /* @__PURE__ */ __name(async () => {
14927
+ if (!canSave) return;
14928
+ setSaving(true);
14929
+ try {
14930
+ const templateName = template?.name ?? `${label} email`;
14931
+ let saved;
14932
+ if (template) {
14933
+ const res = await jsonFetch(`/api/message-templates/email/${template.id}`, {
14934
+ method: "PATCH",
14935
+ headers: {
14936
+ "Content-Type": "application/json"
14937
+ },
14938
+ body: JSON.stringify({
14939
+ name: templateName,
14940
+ subject,
14941
+ body
14942
+ })
14943
+ });
14944
+ saved = res.item;
14945
+ sonner.toast.success(`"${label}" email template saved`);
14946
+ } else {
14947
+ const res = await jsonFetch("/api/message-templates/email", {
14948
+ method: "POST",
14949
+ headers: {
14950
+ "Content-Type": "application/json"
14951
+ },
14952
+ body: JSON.stringify({
14953
+ name: templateName,
14954
+ subject,
14955
+ body
14956
+ })
14957
+ });
14958
+ saved = res.item;
14959
+ onSaved(saved);
14960
+ const bound = await onBind(saved.id, saved.name);
14961
+ if (!bound) {
14962
+ sonner.toast.success(`"${label}" email template created`);
14963
+ }
14942
14964
  return;
14943
14965
  }
14966
+ onSaved(saved);
14967
+ } catch (e) {
14968
+ sonner.toast.error(e instanceof Error ? e.message : "Failed to save template");
14969
+ } finally {
14970
+ setSaving(false);
14944
14971
  }
14945
- sonner.toast.success("Event notification settings saved");
14946
- }, "handleSaveAll");
14972
+ }, "handleSave");
14973
+ return /* @__PURE__ */ React.createElement("div", {
14974
+ className: "space-y-3 border-t border-gray-100 pt-4 first:border-t-0 first:pt-0 dark:border-gray-700"
14975
+ }, /* @__PURE__ */ React.createElement("div", {
14976
+ className: "flex items-start justify-between gap-2"
14977
+ }, /* @__PURE__ */ React.createElement("div", {
14978
+ className: "min-w-0"
14979
+ }, /* @__PURE__ */ React.createElement("div", {
14980
+ className: "flex items-center gap-2"
14981
+ }, /* @__PURE__ */ React.createElement(LucideIcons.Mail, {
14982
+ className: "h-3.5 w-3.5 text-gray-400 shrink-0"
14983
+ }), /* @__PURE__ */ React.createElement("span", {
14984
+ className: "text-sm font-medium text-gray-900 dark:text-gray-100"
14985
+ }, label), /* @__PURE__ */ React.createElement("span", {
14986
+ className: "text-[11px] font-mono text-gray-400"
14987
+ }, triggerKey)), description ? /* @__PURE__ */ React.createElement("p", {
14988
+ className: "text-[11px] text-gray-500 dark:text-gray-400 mt-0.5 ml-5"
14989
+ }, description) : null), /* @__PURE__ */ React.createElement(Button, {
14990
+ type: "button",
14991
+ size: "sm",
14992
+ className: "h-8 gap-1.5 shrink-0",
14993
+ disabled: !canSave || saving || !isDirty && Boolean(template),
14994
+ onClick: /* @__PURE__ */ __name(() => void handleSave(), "onClick")
14995
+ }, saving ? /* @__PURE__ */ React.createElement(LucideIcons.Loader2, {
14996
+ className: "h-3.5 w-3.5 animate-spin"
14997
+ }) : /* @__PURE__ */ React.createElement(LucideIcons.Save, {
14998
+ className: "h-3.5 w-3.5"
14999
+ }), template ? "Save" : "Create")), !template ? /* @__PURE__ */ React.createElement("p", {
15000
+ className: "text-xs text-amber-600 dark:text-amber-400 ml-5"
15001
+ }, "No email saved for this trigger yet \u2014 review the draft below and click Create.") : null, /* @__PURE__ */ React.createElement("div", {
15002
+ className: "ml-5 space-y-3 rounded-md border border-gray-100 bg-gray-50/50 p-3 dark:border-gray-700 dark:bg-gray-800/30"
15003
+ }, /* @__PURE__ */ React.createElement("div", {
15004
+ className: "space-y-1"
15005
+ }, /* @__PURE__ */ React.createElement(Label3, {
15006
+ className: "text-xs text-gray-600 dark:text-gray-400"
15007
+ }, "Subject"), /* @__PURE__ */ React.createElement(Input, {
15008
+ ref: subjectRef,
15009
+ value: subject,
15010
+ onChange: /* @__PURE__ */ __name((e) => setSubject(e.target.value), "onChange"),
15011
+ onFocus: /* @__PURE__ */ __name(() => setFocusedField("subject"), "onFocus"),
15012
+ placeholder: defaults.subjectPlaceholder,
15013
+ className: "h-8 text-sm bg-white dark:bg-gray-900"
15014
+ })), /* @__PURE__ */ React.createElement("div", {
15015
+ className: "space-y-1"
15016
+ }, /* @__PURE__ */ React.createElement(Label3, {
15017
+ className: "text-xs text-gray-600 dark:text-gray-400"
15018
+ }, "Body"), /* @__PURE__ */ React.createElement(Textarea, {
15019
+ ref: bodyRef,
15020
+ value: body,
15021
+ onChange: /* @__PURE__ */ __name((e) => setBody(e.target.value), "onChange"),
15022
+ onFocus: /* @__PURE__ */ __name(() => setFocusedField("body"), "onFocus"),
15023
+ rows: 6,
15024
+ placeholder: defaults.bodyPlaceholder,
15025
+ className: "text-sm font-mono bg-white dark:bg-gray-900 resize-y min-h-[120px]"
15026
+ })), /* @__PURE__ */ React.createElement("div", {
15027
+ className: "space-y-1.5"
15028
+ }, /* @__PURE__ */ React.createElement(Label3, {
15029
+ className: "text-xs text-gray-500 dark:text-gray-400"
15030
+ }, "Variables \u2014 inserts into ", focusedField === "subject" ? "subject" : "body", ":"), /* @__PURE__ */ React.createElement("div", {
15031
+ className: "flex flex-wrap gap-1.5"
15032
+ }, KNOWN_VARS.map((v) => /* @__PURE__ */ React.createElement("button", {
15033
+ key: v,
15034
+ type: "button",
15035
+ title: VAR_HINTS[v],
15036
+ onClick: /* @__PURE__ */ __name(() => insertVariable(v), "onClick"),
15037
+ className: "rounded border border-gray-200 bg-white px-2 py-1 font-mono text-[11px] text-gray-700 hover:border-blue-300 hover:bg-blue-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-blue-700 dark:hover:bg-blue-950/40"
15038
+ }, `{{${v}}}`))), /* @__PURE__ */ React.createElement("p", {
15039
+ className: "text-[10px] text-gray-400 dark:text-gray-500"
15040
+ }, "Hover a chip to see what each variable contains at send time."))));
15041
+ }
15042
+ var VAR_HINTS;
15043
+ var init_emailTriggersBindingsPage = __esm({
15044
+ "src/admin/pages/emailTriggersBindingsPage.tsx"() {
15045
+ "use client";
15046
+ init_button();
15047
+ init_input();
15048
+ init_textarea();
15049
+ init_label();
15050
+ init_order_notification_bindings_shared();
15051
+ VAR_HINTS = {
15052
+ orderId: "Internal order ID",
15053
+ orderNumber: "Display order number",
15054
+ customerName: "Customer full name",
15055
+ customerPhone: "Customer phone",
15056
+ total: "Order total amount",
15057
+ currency: "Currency code (e.g. INR)",
15058
+ vendorId: "Vendor / store ID",
15059
+ refundAmount: "Refund amount (refund triggers)"
15060
+ };
15061
+ __name(EmailTriggerBindings, "EmailTriggerBindings");
15062
+ __name(TriggerEmailEditor, "TriggerEmailEditor");
15063
+ }
15064
+ });
15065
+ function EventNotificationsPluginSettings({ settingsGroup, enabled, onEnabledChange, onSaveSettings, saving }) {
15066
+ const [tab, setTab] = React26.useState("email");
15067
+ const handleSaveEnabled = /* @__PURE__ */ __name(async () => {
15068
+ const ok = await onSaveSettings();
15069
+ if (!ok) {
15070
+ sonner.toast.error("Failed to save plugin settings");
15071
+ return;
15072
+ }
15073
+ sonner.toast.success("Settings saved");
15074
+ }, "handleSaveEnabled");
14947
15075
  return /* @__PURE__ */ React.createElement("div", {
14948
15076
  className: "space-y-5"
14949
15077
  }, /* @__PURE__ */ React.createElement("div", {
@@ -14958,11 +15086,13 @@ function EventNotificationsPluginSettings({ settingsGroup, enabled, onEnabledCha
14958
15086
  className: "font-medium text-gray-900 dark:text-white"
14959
15087
  }, "Order notifications"), /* @__PURE__ */ React.createElement("ol", {
14960
15088
  className: "list-decimal pl-4 space-y-1 text-xs leading-relaxed"
14961
- }, /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("strong", null, "Email"), " tab: configure the order confirmation email template."), /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("strong", null, "WhatsApp"), " tab: sync Meta templates, then link each trigger from the database list to a template."), /* @__PURE__ */ React.createElement("li", null, "Triggers are stored in ", /* @__PURE__ */ React.createElement("code", {
15089
+ }, /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("strong", null, "Email"), " tab: each trigger has its own subject and body \u2014 use", " ", /* @__PURE__ */ React.createElement("code", {
15090
+ className: "text-[11px]"
15091
+ }, "{{variable}}"), " tokens and click Create or Save."), /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("strong", null, "WhatsApp"), " tab: sync Meta-approved templates, then bind a trigger to one."), /* @__PURE__ */ React.createElement("li", null, "Both tabs bind against the same trigger catalog (", /* @__PURE__ */ React.createElement("code", {
14962
15092
  className: "text-[11px]"
14963
- }, "order_notification_triggers"), " (migration seeds defaults). Bindings live in", " ", /* @__PURE__ */ React.createElement("code", {
15093
+ }, "order_notification_triggers"), ") and share", " ", /* @__PURE__ */ React.createElement("code", {
14964
15094
  className: "text-[11px]"
14965
- }, "whatsapp_trigger_bindings"), ".")), /* @__PURE__ */ React.createElement("p", {
15095
+ }, "order_notification_bindings"), ", keyed by trigger + channel \u2014 so the same trigger (e.g. order placed) can have its own email template and its own WhatsApp template at the same time.")), /* @__PURE__ */ React.createElement("p", {
14966
15096
  className: "text-xs text-gray-500 dark:text-gray-400"
14967
15097
  }, "Requires ", /* @__PURE__ */ React.createElement("strong", null, "Email"), " plugin (SMTP) for email and ", /* @__PURE__ */ React.createElement("strong", null, "WhatsApp"), " plugin (Meta credentials) for WhatsApp delivery.")))), /* @__PURE__ */ React.createElement("div", {
14968
15098
  className: "flex items-center justify-between gap-2"
@@ -14989,16 +15119,9 @@ function EventNotificationsPluginSettings({ settingsGroup, enabled, onEnabledCha
14989
15119
  className: `inline-flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 ${tab === "whatsapp" ? "border-gray-800 text-gray-800 dark:border-gray-200 dark:text-gray-100" : "border-transparent text-gray-500 hover:text-gray-700"}`
14990
15120
  }, /* @__PURE__ */ React.createElement(LucideIcons.MessageCircle, {
14991
15121
  className: "h-4 w-4"
14992
- }), "WhatsApp template"))), tab === "whatsapp" ? /* @__PURE__ */ React.createElement(WhatsAppTriggerBindings, null) : loadingTemplates ? /* @__PURE__ */ React.createElement("p", {
14993
- className: "text-sm text-gray-500"
14994
- }, "Loading templates\u2026") : /* @__PURE__ */ React.createElement(EventOrderGlobalTemplates, {
14995
- items: templateItems,
14996
- onChange: setTemplateItems,
14997
- channel: tab,
14998
- settingsGroup
14999
- }), /* @__PURE__ */ React.createElement(Button, {
15122
+ }), "WhatsApp template"))), tab === "whatsapp" ? /* @__PURE__ */ React.createElement(WhatsAppTriggerBindings, null) : /* @__PURE__ */ React.createElement(EmailTriggerBindings, null), /* @__PURE__ */ React.createElement(Button, {
15000
15123
  size: "sm",
15001
- onClick: /* @__PURE__ */ __name(() => void handleSaveAll(), "onClick"),
15124
+ onClick: /* @__PURE__ */ __name(() => void handleSaveEnabled(), "onClick"),
15002
15125
  disabled: saving,
15003
15126
  className: "gap-1"
15004
15127
  }, /* @__PURE__ */ React.createElement(LucideIcons.Save, {
@@ -15011,8 +15134,8 @@ var init_EventNotificationsPluginSettings = __esm({
15011
15134
  init_button();
15012
15135
  init_label();
15013
15136
  init_switch();
15014
- init_EventOrderGlobalTemplates();
15015
15137
  init_whatsappTriggerBindingsPage();
15138
+ init_emailTriggersBindingsPage();
15016
15139
  __name(EventNotificationsPluginSettings, "EventNotificationsPluginSettings");
15017
15140
  }
15018
15141
  });
@@ -26151,47 +26274,6 @@ var init_ProductIncomeCalculatorDialog = __esm({
26151
26274
  });
26152
26275
 
26153
26276
  // src/lib/currency-prices.ts
26154
- function normalizeCurrencyCode(code) {
26155
- return code.trim().toUpperCase();
26156
- }
26157
- function parseCurrencyPrices(raw) {
26158
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
26159
- const out = {};
26160
- for (const [key, value] of Object.entries(raw)) {
26161
- const code = normalizeCurrencyCode(key);
26162
- if (!code) continue;
26163
- const n = Number(value);
26164
- if (Number.isFinite(n) && n >= 0) out[code] = n;
26165
- }
26166
- return out;
26167
- }
26168
- function serializeCurrencyPrices(defaultCurrency, defaultPrice, additional) {
26169
- const base = normalizeCurrencyCode(defaultCurrency) || "INR";
26170
- const out = {
26171
- [base]: defaultPrice
26172
- };
26173
- for (const [code, amount] of Object.entries(additional)) {
26174
- const normalized = normalizeCurrencyCode(code);
26175
- if (!normalized || normalized === base) continue;
26176
- if (Number.isFinite(amount) && amount >= 0) out[normalized] = amount;
26177
- }
26178
- return Object.keys(out).length ? out : null;
26179
- }
26180
- function currencyPricesToForm(defaultCurrency, storedPrice, currencyPrices) {
26181
- const base = normalizeCurrencyCode(defaultCurrency) || "INR";
26182
- const map = parseCurrencyPrices(currencyPrices);
26183
- const defaultFromMap = map[base];
26184
- const defaultPrice = defaultFromMap != null ? String(defaultFromMap) : storedPrice != null && Number.isFinite(Number(storedPrice)) ? String(storedPrice) : "";
26185
- const additional = {};
26186
- for (const [code, amount] of Object.entries(map)) {
26187
- if (code === base) continue;
26188
- additional[code] = String(amount);
26189
- }
26190
- return {
26191
- defaultPrice,
26192
- additional
26193
- };
26194
- }
26195
26277
  function parseDefaultProductPrice(defaultPriceRaw) {
26196
26278
  const trimmed = defaultPriceRaw.trim();
26197
26279
  if (!trimmed) return null;
@@ -26201,31 +26283,9 @@ function parseDefaultProductPrice(defaultPriceRaw) {
26201
26283
  price: Math.round(price * 100) / 100
26202
26284
  };
26203
26285
  }
26204
- function formToCurrencyPricesPayload(defaultCurrency, defaultPriceRaw, additionalForm) {
26205
- const base = normalizeCurrencyCode(defaultCurrency) || "INR";
26206
- const defaultPrice = Number(defaultPriceRaw.trim());
26207
- if (!Number.isFinite(defaultPrice) || defaultPrice < 0) return null;
26208
- const additional = {};
26209
- for (const [code, raw] of Object.entries(additionalForm)) {
26210
- const trimmed = raw.trim();
26211
- if (!trimmed) continue;
26212
- const n = Number(trimmed);
26213
- if (!Number.isFinite(n) || n < 0) continue;
26214
- additional[normalizeCurrencyCode(code)] = n;
26215
- }
26216
- return {
26217
- price: defaultPrice,
26218
- currencyPrices: serializeCurrencyPrices(base, defaultPrice, additional)
26219
- };
26220
- }
26221
26286
  var init_currency_prices = __esm({
26222
26287
  "src/lib/currency-prices.ts"() {
26223
- __name(normalizeCurrencyCode, "normalizeCurrencyCode");
26224
- __name(parseCurrencyPrices, "parseCurrencyPrices");
26225
- __name(serializeCurrencyPrices, "serializeCurrencyPrices");
26226
- __name(currencyPricesToForm, "currencyPricesToForm");
26227
26288
  __name(parseDefaultProductPrice, "parseDefaultProductPrice");
26228
- __name(formToCurrencyPricesPayload, "formToCurrencyPricesPayload");
26229
26289
  }
26230
26290
  });
26231
26291
 
@@ -29663,44 +29723,6 @@ function SupportedCurrenciesPicker({ currencies, defaultCurrency, supported, onD
29663
29723
  className: "mt-1 text-xs text-gray-500"
29664
29724
  }, "Default currency is always included.")));
29665
29725
  }
29666
- function MultiCurrencyPriceFields({ defaultCurrency, currencies, defaultPrice, additionalPrices, onDefaultPriceChange, onAdditionalPriceChange, labelCls: labelClassName = "block text-xs font-medium text-gray-600 mb-1", inputCls: inputClassName = "w-full rounded-md border border-gray-300 px-2 py-1.5 text-sm" }) {
29667
- const base = defaultCurrency.trim().toUpperCase() || "INR";
29668
- const others = currencies.filter((c) => c.code.toUpperCase() !== base);
29669
- return /* @__PURE__ */ React.createElement("div", {
29670
- className: "space-y-4"
29671
- }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
29672
- className: labelClassName
29673
- }, base, " price ", /* @__PURE__ */ React.createElement("span", {
29674
- className: "text-red-600"
29675
- }, "*")), /* @__PURE__ */ React.createElement("input", {
29676
- type: "number",
29677
- min: 0,
29678
- step: "any",
29679
- value: defaultPrice,
29680
- onChange: /* @__PURE__ */ __name((e) => onDefaultPriceChange(e.target.value), "onChange"),
29681
- className: inputClassName,
29682
- required: true
29683
- })), others.length > 0 ? /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", {
29684
- className: `${labelClassName} mb-2`
29685
- }, "Additional currencies (optional)"), /* @__PURE__ */ React.createElement("div", {
29686
- className: "grid gap-3 sm:grid-cols-2"
29687
- }, others.map((c) => {
29688
- const code = c.code.toUpperCase();
29689
- return /* @__PURE__ */ React.createElement("div", {
29690
- key: code
29691
- }, /* @__PURE__ */ React.createElement("label", {
29692
- className: labelClassName
29693
- }, code), /* @__PURE__ */ React.createElement("input", {
29694
- type: "number",
29695
- min: 0,
29696
- step: "any",
29697
- value: additionalPrices[code] ?? "",
29698
- onChange: /* @__PURE__ */ __name((e) => onAdditionalPriceChange(code, e.target.value), "onChange"),
29699
- className: inputClassName,
29700
- placeholder: "Optional"
29701
- }));
29702
- }))) : null);
29703
- }
29704
29726
  var inputCls5, labelCls5, ICONS;
29705
29727
  var init_EventManagementFields = __esm({
29706
29728
  "src/components/Admin/EventManagementFields.tsx"() {
@@ -29722,7 +29744,6 @@ var init_EventManagementFields = __esm({
29722
29744
  __name(SocialMediaLinksDisplay, "SocialMediaLinksDisplay");
29723
29745
  __name(TimezoneSelect, "TimezoneSelect");
29724
29746
  __name(SupportedCurrenciesPicker, "SupportedCurrenciesPicker");
29725
- __name(MultiCurrencyPriceFields, "MultiCurrencyPriceFields");
29726
29747
  }
29727
29748
  });
29728
29749
 
@@ -29874,28 +29895,11 @@ function intersectSupportedCurrencies(supported, defaultCurrency, options) {
29874
29895
  ])
29875
29896
  ];
29876
29897
  }
29877
- function currencyOptionsIncluding(options, codes) {
29878
- const map = new Map(options.map((o) => [
29879
- o.code.toUpperCase(),
29880
- o
29881
- ]));
29882
- for (const code of codes) {
29883
- const normalized = code.trim().toUpperCase();
29884
- if (!normalized || map.has(normalized)) continue;
29885
- map.set(normalized, {
29886
- code: normalized
29887
- });
29888
- }
29889
- return [
29890
- ...map.values()
29891
- ].sort((a, b) => a.code.localeCompare(b.code));
29892
- }
29893
29898
  var init_currency_options = __esm({
29894
29899
  "src/lib/currency-options.ts"() {
29895
29900
  __name(parseCurrencyApiRows, "parseCurrencyApiRows");
29896
29901
  __name(fetchActiveCurrencyOptions, "fetchActiveCurrencyOptions");
29897
29902
  __name(intersectSupportedCurrencies, "intersectSupportedCurrencies");
29898
- __name(currencyOptionsIncluding, "currencyOptionsIncluding");
29899
29903
  }
29900
29904
  });
29901
29905
 
@@ -30875,25 +30879,14 @@ function formatDateTimeLocal(value) {
30875
30879
  if (isNaN(date.getTime())) return "";
30876
30880
  return date.toISOString().slice(0, 16);
30877
30881
  }
30878
- async function fetchEventPricing(eventId) {
30882
+ async function fetchEventDefaultCurrency(eventId) {
30879
30883
  try {
30880
30884
  const res = await fetch(`/api/events/${eventId}`);
30881
- if (!res.ok) return DEFAULT_PRICING;
30885
+ if (!res.ok) return "INR";
30882
30886
  const data = await res.json();
30883
- const defaultCurrency = String(data.defaultCurrency ?? "INR").trim().toUpperCase() || "INR";
30884
- const supported = Array.isArray(data.supportedCurrencies) ? data.supportedCurrencies.map((c) => String(c).trim().toUpperCase()).filter((c) => Boolean(c)) : [
30885
- defaultCurrency
30886
- ];
30887
- const set = new Set(supported);
30888
- set.add(defaultCurrency);
30889
- return {
30890
- defaultCurrency,
30891
- supportedCurrencies: [
30892
- ...set
30893
- ]
30894
- };
30887
+ return String(data.defaultCurrency ?? "INR").trim().toUpperCase() || "INR";
30895
30888
  } catch {
30896
- return DEFAULT_PRICING;
30889
+ return "INR";
30897
30890
  }
30898
30891
  }
30899
30892
  async function validateComboProductInventory(productIds) {
@@ -30935,10 +30928,8 @@ function ComboEditPage({ comboId }) {
30935
30928
  const [name, setName] = React26.useState("");
30936
30929
  const [desc, setDesc] = React26.useState("");
30937
30930
  const [eventId, setEventId] = React26.useState("");
30938
- const [defaultPriceStr, setDefaultPriceStr] = React26.useState("");
30939
- const [additionalPrices, setAdditionalPrices] = React26.useState({});
30940
- const [pricingConfig, setPricingConfig] = React26.useState(DEFAULT_PRICING);
30941
- const [currencySchemaOptions, setCurrencySchemaOptions] = React26.useState([]);
30931
+ const [priceStr, setPriceStr] = React26.useState("");
30932
+ const [defaultCurrency, setDefaultCurrency] = React26.useState("INR");
30942
30933
  const [minSelectableItems, setMinSelectableItems] = React26.useState(1);
30943
30934
  const [maxSelectableItems, setMaxSelectableItems] = React26.useState(1);
30944
30935
  const [status, setStatus] = React26.useState("active");
@@ -30948,23 +30939,14 @@ function ComboEditPage({ comboId }) {
30948
30939
  const [productOptions, setProductOptions] = React26.useState([]);
30949
30940
  const [fixedItems, setFixedItems] = React26.useState([]);
30950
30941
  const [addonItems, setAddonItems] = React26.useState([]);
30951
- React26.useEffect(() => {
30952
- let cancelled = false;
30953
- fetchActiveCurrencyOptions().then(({ options }) => {
30954
- if (!cancelled && options.length) setCurrencySchemaOptions(options);
30955
- });
30956
- return () => {
30957
- cancelled = true;
30958
- };
30959
- }, []);
30960
30942
  React26.useEffect(() => {
30961
30943
  if (!eventId) {
30962
- setPricingConfig(DEFAULT_PRICING);
30944
+ setDefaultCurrency("INR");
30963
30945
  return;
30964
30946
  }
30965
30947
  let cancelled = false;
30966
- fetchEventPricing(eventId).then((config) => {
30967
- if (!cancelled) setPricingConfig(config);
30948
+ fetchEventDefaultCurrency(eventId).then((code) => {
30949
+ if (!cancelled) setDefaultCurrency(code);
30968
30950
  });
30969
30951
  return () => {
30970
30952
  cancelled = true;
@@ -30972,10 +30954,6 @@ function ComboEditPage({ comboId }) {
30972
30954
  }, [
30973
30955
  eventId
30974
30956
  ]);
30975
- const pricingCurrencyOptions = currencyOptionsIncluding(currencySchemaOptions, [
30976
- pricingConfig.defaultCurrency,
30977
- ...pricingConfig.supportedCurrencies
30978
- ]).filter((c) => pricingConfig.supportedCurrencies.map((x) => x.toUpperCase()).includes(c.code.toUpperCase()));
30979
30957
  React26.useEffect(() => {
30980
30958
  let cancelled = false;
30981
30959
  (async () => {
@@ -31064,11 +31042,11 @@ function ComboEditPage({ comboId }) {
31064
31042
  setName(data.name ?? "");
31065
31043
  setDesc(data.desc ?? "");
31066
31044
  setEventId(data.eventId != null ? String(data.eventId) : "");
31067
- const eventPricing = data.eventId != null ? await fetchEventPricing(String(data.eventId)) : DEFAULT_PRICING;
31068
- if (!cancelled) setPricingConfig(eventPricing);
31069
- const { defaultPrice, additional } = currencyPricesToForm(eventPricing.defaultCurrency, data.price != null ? Number(data.price) : null, data.currencyPrices);
31070
- setDefaultPriceStr(defaultPrice);
31071
- setAdditionalPrices(additional);
31045
+ if (data.eventId != null) {
31046
+ const currency = await fetchEventDefaultCurrency(String(data.eventId));
31047
+ if (!cancelled) setDefaultCurrency(currency);
31048
+ }
31049
+ setPriceStr(data.price != null && Number.isFinite(Number(data.price)) ? String(data.price) : "");
31072
31050
  setMinSelectableItems(Number(data.minSelectableItems) || 1);
31073
31051
  setMaxSelectableItems(Number(data.maxSelectableItems) || 1);
31074
31052
  setStatus(data.status ?? "active");
@@ -31119,10 +31097,6 @@ function ComboEditPage({ comboId }) {
31119
31097
  create,
31120
31098
  duplicateSourceId
31121
31099
  ]);
31122
- /* @__PURE__ */ new Set([
31123
- ...fixedItems.map((i) => i.productId),
31124
- ...addonItems.map((i) => i.productId)
31125
- ]);
31126
31100
  const availableForFixed = productOptions.filter((p) => !addonItems.some((i) => i.productId === p.value));
31127
31101
  const availableForAddon = productOptions.filter((p) => !fixedItems.some((i) => i.productId === p.value));
31128
31102
  const addFixed = /* @__PURE__ */ __name((val) => {
@@ -31163,16 +31137,16 @@ function ComboEditPage({ comboId }) {
31163
31137
  ]);
31164
31138
  return;
31165
31139
  }
31166
- if (!defaultPriceStr.trim()) {
31140
+ if (!priceStr.trim()) {
31167
31141
  setErrors([
31168
- `${pricingConfig.defaultCurrency} price is required`
31142
+ `Price (${defaultCurrency}) is required`
31169
31143
  ]);
31170
31144
  return;
31171
31145
  }
31172
- const pricingPayload = formToCurrencyPricesPayload(pricingConfig.defaultCurrency, defaultPriceStr, additionalPrices);
31173
- if (!pricingPayload) {
31146
+ const price = Number(priceStr);
31147
+ if (!Number.isFinite(price) || price < 0) {
31174
31148
  setErrors([
31175
- `${pricingConfig.defaultCurrency} price must be a valid number`
31149
+ `Price (${defaultCurrency}) must be a valid non-negative number`
31176
31150
  ]);
31177
31151
  return;
31178
31152
  }
@@ -31220,8 +31194,8 @@ function ComboEditPage({ comboId }) {
31220
31194
  name: trimmedName,
31221
31195
  desc: desc || null,
31222
31196
  eventId: Number(trimmedEventId),
31223
- price: pricingPayload.price,
31224
- currencyPrices: pricingPayload.currencyPrices,
31197
+ price,
31198
+ currencyPrices: null,
31225
31199
  minSelectableItems,
31226
31200
  maxSelectableItems,
31227
31201
  status,
@@ -31388,21 +31362,19 @@ function ComboEditPage({ comboId }) {
31388
31362
  className: "text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2"
31389
31363
  }, "Pricing & add-on selection"), /* @__PURE__ */ React.createElement("div", {
31390
31364
  className: "min-w-0 overflow-hidden border border-gray-200 rounded-lg p-4 bg-gray-50/50 space-y-4"
31391
- }, eventId ? /* @__PURE__ */ React.createElement(MultiCurrencyPriceFields, {
31392
- defaultCurrency: pricingConfig.defaultCurrency,
31393
- currencies: pricingCurrencyOptions,
31394
- defaultPrice: defaultPriceStr,
31395
- additionalPrices,
31396
- onDefaultPriceChange: setDefaultPriceStr,
31397
- onAdditionalPriceChange: /* @__PURE__ */ __name((code, value) => setAdditionalPrices((prev) => ({
31398
- ...prev,
31399
- [code]: value
31400
- })), "onAdditionalPriceChange"),
31401
- labelCls: "block text-xs font-medium text-gray-600 mb-1",
31402
- inputCls: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
31403
- }) : /* @__PURE__ */ React.createElement("p", {
31404
- className: "text-sm text-gray-500"
31405
- }, "Select an event to configure combo pricing currencies."), /* @__PURE__ */ React.createElement("div", {
31365
+ }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
31366
+ className: "block text-xs font-medium text-gray-600 mb-1"
31367
+ }, "Price (", defaultCurrency, ") *"), /* @__PURE__ */ React.createElement("input", {
31368
+ type: "number",
31369
+ min: 0,
31370
+ step: "0.01",
31371
+ value: priceStr,
31372
+ onChange: /* @__PURE__ */ __name((e) => setPriceStr(e.target.value), "onChange"),
31373
+ disabled: !eventId,
31374
+ className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm disabled:bg-gray-100"
31375
+ }), /* @__PURE__ */ React.createElement("p", {
31376
+ className: "text-xs text-gray-400 mt-1"
31377
+ }, eventId ? "Other currencies use the event\u2019s supported currencies and exchange rates." : "Select an event to set the combo price.")), /* @__PURE__ */ React.createElement("div", {
31406
31378
  className: "grid grid-cols-2 gap-4"
31407
31379
  }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
31408
31380
  className: "block text-xs font-medium text-gray-600 mb-1"
@@ -31460,26 +31432,17 @@ function ComboEditPage({ comboId }) {
31460
31432
  sidebar: /* @__PURE__ */ React.createElement(React.Fragment, null)
31461
31433
  }));
31462
31434
  }
31463
- var isCreate5, DEFAULT_PRICING;
31435
+ var isCreate5;
31464
31436
  var init_ComboEditPage = __esm({
31465
31437
  "src/admin/pages/ComboEditPage.tsx"() {
31466
31438
  "use client";
31467
31439
  init_admin_list_return_url();
31468
31440
  init_DetailPageLayout();
31469
31441
  init_DetailPageHeader();
31470
- init_EventManagementFields();
31471
- init_currency_options();
31472
- init_currency_prices();
31473
31442
  init_inventory_validation();
31474
31443
  isCreate5 = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
31475
31444
  __name(formatDateTimeLocal, "formatDateTimeLocal");
31476
- DEFAULT_PRICING = {
31477
- defaultCurrency: "INR",
31478
- supportedCurrencies: [
31479
- "INR"
31480
- ]
31481
- };
31482
- __name(fetchEventPricing, "fetchEventPricing");
31445
+ __name(fetchEventDefaultCurrency, "fetchEventDefaultCurrency");
31483
31446
  __name(validateComboProductInventory, "validateComboProductInventory");
31484
31447
  __name(ComboEditPage, "ComboEditPage");
31485
31448
  }