@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.js CHANGED
@@ -419,7 +419,8 @@ var init_vendor_scope = __esm({
419
419
  "vendor_customers",
420
420
  "discounts",
421
421
  "events",
422
- "refund_policies"
422
+ "refund_policies",
423
+ "combos"
423
424
  ]);
424
425
  VENDOR_STORE_RBAC_ENTITIES = /* @__PURE__ */ new Set([
425
426
  ...VENDOR_SCOPED_STORE_ENTITIES,
@@ -456,7 +457,7 @@ var init_admin_config_context = __esm({
456
457
  var CMS_VERSION;
457
458
  var init_cms_version = __esm({
458
459
  "src/lib/cms-version.ts"() {
459
- CMS_VERSION = "1.0.36" ;
460
+ CMS_VERSION = "1.0.39" ;
460
461
  }
461
462
  });
462
463
  function useCatalogCategories(enabled = true) {
@@ -14268,203 +14269,149 @@ var init_email_recipients = __esm({
14268
14269
  __name(serializeEmailRecipients, "serializeEmailRecipients");
14269
14270
  }
14270
14271
  });
14271
-
14272
- // src/message-templates/event-order-defaults.ts
14273
- var EVENT_ORDER_WHATSAPP_PARAM_KEYS, EVENT_ORDER_TEMPLATE_VARIABLE_HINTS;
14274
- var init_event_order_defaults = __esm({
14275
- "src/message-templates/event-order-defaults.ts"() {
14276
- EVENT_ORDER_WHATSAPP_PARAM_KEYS = [
14277
- "customerName",
14278
- "eventName",
14279
- "orderNumber",
14280
- "orderTotal",
14281
- "currency",
14282
- "venue",
14283
- "startDate",
14284
- "trackUrl"
14285
- ];
14286
- EVENT_ORDER_TEMPLATE_VARIABLE_HINTS = {
14287
- eventName: "Event title",
14288
- eventSlug: "Event URL slug",
14289
- venue: "Venue name",
14290
- startDate: "Formatted event start",
14291
- orderNumber: "Order number",
14292
- orderTotal: "Order total amount",
14293
- currency: "Currency code",
14294
- customerName: "Buyer name",
14295
- customerEmail: "Buyer email",
14296
- customerPhone: "Buyer phone",
14297
- productNames: "Comma-separated ticket names",
14298
- trackUrl: "Public order tracking link (QR destination)",
14299
- qrImageUrl: 'QR code image URL for email (use in <img src="\u2026">)',
14300
- orderDetails: "Plain-text order summary with line items",
14301
- orderDetailsHtml: "HTML order summary block",
14302
- trackUrlWhatsApp: "Same as trackUrl \u2014 include in Meta template as last param"
14303
- };
14272
+ function getEmailTriggerDefaults(triggerKey) {
14273
+ return EMAIL_TRIGGER_DEFAULTS[triggerKey] ?? {
14274
+ subject: "",
14275
+ body: "",
14276
+ subjectPlaceholder: "e.g. Order update for {{orderNumber}}",
14277
+ bodyPlaceholder: "Hi {{customerName}},\n\nYour order {{orderNumber}} has been updated."
14278
+ };
14279
+ }
14280
+ async function jsonFetch(url, init) {
14281
+ const res = await fetch(url, {
14282
+ credentials: "include",
14283
+ ...init
14284
+ });
14285
+ const data = await res.json().catch(() => ({}));
14286
+ if (!res.ok) {
14287
+ throw new Error(data?.error || `Request failed (${res.status})`);
14304
14288
  }
14305
- });
14306
- function mergeChannelUpdate(allItems, channel, updatedChannelItems) {
14307
- return [
14308
- ...allItems.filter((item) => item.channel !== channel),
14309
- ...updatedChannelItems
14310
- ];
14289
+ return data;
14311
14290
  }
14312
- function EventOrderGlobalTemplates({ items, onChange, channel, settingsGroup = "notifications" }) {
14313
- const [templateVariables, setTemplateVariables] = useState([]);
14314
- const copy = CHANNEL_COPY[channel];
14315
- const visibleItems = useMemo(() => items.filter((item) => item.channel === channel), [
14316
- items,
14291
+ function useOrderNotificationTriggerBindings(channel) {
14292
+ const [triggers, setTriggers] = useState([]);
14293
+ const [bindings, setBindings] = useState([]);
14294
+ const [loading, setLoading] = useState(true);
14295
+ const load = useCallback(async () => {
14296
+ setLoading(true);
14297
+ try {
14298
+ const [triggersRes, bindingsRes] = await Promise.all([
14299
+ jsonFetch("/api/order_notification_triggers?limit=100&sortField=sortOrder&sortOrder=asc"),
14300
+ // Bindings for all channels are fetched and filtered client-side rather
14301
+ // than relying on server-side ?channel= filtering, since that's not
14302
+ // confirmed to be supported by the generic CRUD layer.
14303
+ jsonFetch("/api/order_notification_bindings?limit=200")
14304
+ ]);
14305
+ setTriggers((triggersRes.data ?? []).filter((t) => t.enabled));
14306
+ setBindings((bindingsRes.data ?? []).filter((b) => b.channel === channel));
14307
+ } catch (e) {
14308
+ toast.error(e instanceof Error ? e.message : "Failed to load trigger bindings");
14309
+ } finally {
14310
+ setLoading(false);
14311
+ }
14312
+ }, [
14317
14313
  channel
14318
14314
  ]);
14319
- const displayVariables = useMemo(() => {
14320
- if (channel === "email") return EMAIL_VARIABLES;
14321
- return [
14322
- ...EVENT_ORDER_WHATSAPP_PARAM_KEYS
14323
- ];
14315
+ useEffect(() => {
14316
+ void load();
14324
14317
  }, [
14318
+ load
14319
+ ]);
14320
+ const rebind = useCallback(async (triggerKey, messageTemplateId, templateLabel) => {
14321
+ const existing = bindings.find((b) => b.triggerKey === triggerKey);
14322
+ try {
14323
+ if (existing) {
14324
+ const updated = await jsonFetch(`/api/order_notification_bindings/${existing.id}`, {
14325
+ method: "PUT",
14326
+ headers: {
14327
+ "Content-Type": "application/json"
14328
+ },
14329
+ body: JSON.stringify({
14330
+ messageTemplateId
14331
+ })
14332
+ });
14333
+ setBindings((prev) => prev.map((b) => b.id === updated.id ? updated : b));
14334
+ } else {
14335
+ const created = await jsonFetch("/api/order_notification_bindings", {
14336
+ method: "POST",
14337
+ headers: {
14338
+ "Content-Type": "application/json"
14339
+ },
14340
+ body: JSON.stringify({
14341
+ triggerKey,
14342
+ channel,
14343
+ messageTemplateId,
14344
+ enabled: true
14345
+ })
14346
+ });
14347
+ setBindings((prev) => [
14348
+ ...prev,
14349
+ created
14350
+ ]);
14351
+ }
14352
+ toast.success(`"${triggerKey}" bound to "${templateLabel}"`);
14353
+ return true;
14354
+ } catch (e) {
14355
+ toast.error(e instanceof Error ? e.message : "Failed to update binding");
14356
+ return false;
14357
+ }
14358
+ }, [
14359
+ bindings,
14325
14360
  channel
14326
14361
  ]);
14327
- useEffect(() => {
14328
- fetch("/api/message-templates/event-order").then((r) => r.ok ? r.json() : null).then((data) => {
14329
- if (data?.templateVariables) setTemplateVariables(data.templateVariables);
14330
- }).catch(() => void 0);
14331
- }, []);
14332
- const patchVisible = /* @__PURE__ */ __name((nextVisible) => {
14333
- onChange(mergeChannelUpdate(items, channel, nextVisible));
14334
- }, "patchVisible");
14335
- if (!visibleItems.length) return null;
14336
- return /* @__PURE__ */ React.createElement("div", {
14337
- className: "space-y-3 rounded-md border border-gray-200 dark:border-gray-600 p-3"
14338
- }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", {
14339
- className: "text-sm font-medium text-gray-900 dark:text-white"
14340
- }, copy.title), /* @__PURE__ */ React.createElement("p", {
14341
- className: "text-xs text-gray-500 dark:text-gray-400 mt-1"
14342
- }, copy.description)), /* @__PURE__ */ React.createElement("div", {
14343
- className: "rounded-md bg-gray-50 dark:bg-gray-800/50 p-2"
14344
- }, /* @__PURE__ */ React.createElement("p", {
14345
- className: "text-[11px] font-medium text-gray-600 dark:text-gray-400 mb-1"
14346
- }, "Variables"), /* @__PURE__ */ React.createElement("div", {
14347
- className: "flex flex-wrap gap-1"
14348
- }, (displayVariables.length ? displayVariables : templateVariables).map((v) => /* @__PURE__ */ React.createElement("span", {
14349
- key: v,
14350
- 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",
14351
- title: EVENT_ORDER_TEMPLATE_VARIABLE_HINTS[v] ?? v
14352
- }, `{{${v}}}`)))), visibleItems.map((t) => /* @__PURE__ */ React.createElement("div", {
14353
- key: t.channel,
14354
- className: "space-y-3 border-t border-gray-100 pt-3 dark:border-gray-700"
14355
- }, /* @__PURE__ */ React.createElement("div", {
14356
- className: "flex flex-wrap items-center justify-between gap-2"
14357
- }, /* @__PURE__ */ React.createElement("div", {
14358
- className: "text-sm font-medium"
14359
- }, t.name), /* @__PURE__ */ React.createElement("div", {
14360
- className: "flex items-center gap-2"
14361
- }, /* @__PURE__ */ React.createElement(Label3, {
14362
- htmlFor: `${settingsGroup}-evt-${t.channel}-enabled`,
14363
- className: "text-xs text-gray-600"
14364
- }, "Enabled"), /* @__PURE__ */ React.createElement(Switch, {
14365
- id: `${settingsGroup}-evt-${t.channel}-enabled`,
14366
- checked: t.enabled,
14367
- onCheckedChange: /* @__PURE__ */ __name((v) => patchVisible(visibleItems.map((x) => x.channel === t.channel ? {
14368
- ...x,
14369
- enabled: v
14370
- } : x)), "onCheckedChange")
14371
- }))), t.channel === "email" ? /* @__PURE__ */ React.createElement("div", {
14372
- className: "space-y-1"
14373
- }, /* @__PURE__ */ React.createElement(Label3, {
14374
- className: "text-xs"
14375
- }, "Subject"), /* @__PURE__ */ React.createElement(Input, {
14376
- value: t.subject,
14377
- onChange: /* @__PURE__ */ __name((e) => patchVisible(visibleItems.map((x) => x.channel === t.channel ? {
14378
- ...x,
14379
- subject: e.target.value
14380
- } : x)), "onChange"),
14381
- className: "h-8 text-sm"
14382
- })) : null, /* @__PURE__ */ React.createElement("div", {
14383
- className: "space-y-1"
14384
- }, /* @__PURE__ */ React.createElement(Label3, {
14385
- className: "text-xs"
14386
- }, t.channel === "email" ? "Body (HTML)" : "Body (reference only)"), /* @__PURE__ */ React.createElement(Textarea, {
14387
- value: t.body,
14388
- onChange: /* @__PURE__ */ __name((e) => patchVisible(visibleItems.map((x) => x.channel === t.channel ? {
14389
- ...x,
14390
- body: e.target.value
14391
- } : x)), "onChange"),
14392
- rows: t.channel === "email" ? 8 : 3,
14393
- className: "text-sm font-mono"
14394
- })), t.channel === "whatsapp" ? /* @__PURE__ */ React.createElement("div", {
14395
- className: "space-y-1"
14396
- }, /* @__PURE__ */ React.createElement(Label3, {
14397
- className: "text-xs"
14398
- }, "Meta approved template name"), /* @__PURE__ */ React.createElement(Input, {
14399
- value: t.externalTemplateRef,
14400
- onChange: /* @__PURE__ */ __name((e) => patchVisible(visibleItems.map((x) => x.channel === t.channel ? {
14401
- ...x,
14402
- externalTemplateRef: e.target.value
14403
- } : x)), "onChange"),
14404
- placeholder: "event_order_placed",
14405
- className: "h-8 text-sm"
14406
- })) : null)));
14407
- }
14408
- async function fetchEventOrderTemplateRows() {
14409
- const res = await fetch("/api/message-templates/event-order");
14410
- if (!res.ok) return [];
14411
- const data = await res.json();
14412
- return data.items ?? [];
14413
- }
14414
- async function saveEventOrderTemplateRows(items) {
14415
- const res = await fetch("/api/message-templates/event-order", {
14416
- method: "PUT",
14417
- headers: {
14418
- "Content-Type": "application/json"
14419
- },
14420
- body: JSON.stringify({
14421
- items: items.map((t) => ({
14422
- channel: t.channel,
14423
- subject: t.subject,
14424
- body: t.body,
14425
- externalTemplateRef: t.externalTemplateRef,
14426
- enabled: t.enabled
14427
- }))
14428
- })
14429
- });
14430
- return res.ok;
14362
+ return {
14363
+ triggers,
14364
+ bindings,
14365
+ loading,
14366
+ reload: load,
14367
+ rebind
14368
+ };
14431
14369
  }
14432
- var CHANNEL_COPY, EMAIL_VARIABLES;
14433
- var init_EventOrderGlobalTemplates = __esm({
14434
- "src/admin/components/EventOrderGlobalTemplates.tsx"() {
14370
+ var KNOWN_VARS, EMAIL_TRIGGER_DEFAULTS;
14371
+ var init_order_notification_bindings_shared = __esm({
14372
+ "src/admin/components/order-notification-bindings-shared.ts"() {
14435
14373
  "use client";
14436
- init_label();
14437
- init_input();
14438
- init_textarea();
14439
- init_switch();
14440
- init_event_order_defaults();
14441
- CHANNEL_COPY = {
14442
- email: {
14443
- title: "Email template",
14444
- description: "HTML email with order details and QR code. Use {{qrImageUrl}} for the QR image and {{orderDetailsHtml}} for line items."
14445
- },
14446
- whatsapp: {
14447
- title: "WhatsApp template",
14448
- description: "Must match a Meta-approved template. Body params are sent in order: " + EVENT_ORDER_WHATSAPP_PARAM_KEYS.join(", ") + "."
14449
- }
14450
- };
14451
- EMAIL_VARIABLES = [
14452
- "customerName",
14453
- "eventName",
14374
+ KNOWN_VARS = [
14375
+ "orderId",
14454
14376
  "orderNumber",
14455
- "orderTotal",
14377
+ "customerName",
14378
+ "customerPhone",
14379
+ "total",
14456
14380
  "currency",
14457
- "productNames",
14458
- "orderDetailsHtml",
14459
- "venue",
14460
- "startDate",
14461
- "qrImageUrl",
14462
- "trackUrl"
14381
+ "vendorId",
14382
+ "refundAmount"
14463
14383
  ];
14464
- __name(mergeChannelUpdate, "mergeChannelUpdate");
14465
- __name(EventOrderGlobalTemplates, "EventOrderGlobalTemplates");
14466
- __name(fetchEventOrderTemplateRows, "fetchEventOrderTemplateRows");
14467
- __name(saveEventOrderTemplateRows, "saveEventOrderTemplateRows");
14384
+ EMAIL_TRIGGER_DEFAULTS = {
14385
+ order_placed: {
14386
+ subject: "Your order {{orderNumber}} is confirmed",
14387
+ subjectPlaceholder: "e.g. Your order {{orderNumber}} is confirmed",
14388
+ body: `Hi {{customerName}},
14389
+
14390
+ Thank you for your order! Your order {{orderNumber}} for {{total}} {{currency}} has been confirmed.
14391
+
14392
+ We'll keep you updated on any changes.
14393
+
14394
+ Thank you!`,
14395
+ bodyPlaceholder: "Hi {{customerName}},\n\nYour order {{orderNumber}} for {{total}} {{currency}} is confirmed.\n\nThank you!"
14396
+ },
14397
+ order_cancelled: {
14398
+ subject: "Your order {{orderNumber}} has been cancelled",
14399
+ subjectPlaceholder: "e.g. Your order {{orderNumber}} has been cancelled",
14400
+ body: `Hi {{customerName}},
14401
+
14402
+ Your order {{orderNumber}} for {{total}} {{currency}} has been cancelled.
14403
+
14404
+ If a refund is due, you will receive {{refundAmount}} {{currency}} back to your original payment method.
14405
+
14406
+ If you have any questions, please contact us.
14407
+
14408
+ Thank you!`,
14409
+ 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."
14410
+ }
14411
+ };
14412
+ __name(getEmailTriggerDefaults, "getEmailTriggerDefaults");
14413
+ __name(jsonFetch, "jsonFetch");
14414
+ __name(useOrderNotificationTriggerBindings, "useOrderNotificationTriggerBindings");
14468
14415
  }
14469
14416
  });
14470
14417
  function varCount(t) {
@@ -14510,22 +14457,10 @@ function contextBeforePlaceholder(bodyText, n) {
14510
14457
  if (idx === -1) return "";
14511
14458
  return bodyText.slice(Math.max(0, idx - 25), idx);
14512
14459
  }
14513
- async function jsonFetch(url, init) {
14514
- const res = await fetch(url, {
14515
- credentials: "include",
14516
- ...init
14517
- });
14518
- const data = await res.json().catch(() => ({}));
14519
- if (!res.ok) {
14520
- throw new Error(data?.error || `Request failed (${res.status})`);
14521
- }
14522
- return data;
14523
- }
14524
14460
  function WhatsAppTriggerBindings() {
14525
- const [triggers, setTriggers] = useState([]);
14526
- const [bindings, setBindings] = useState([]);
14461
+ const { triggers, bindings, loading, reload, rebind } = useOrderNotificationTriggerBindings("whatsapp");
14527
14462
  const [templates, setTemplates] = useState([]);
14528
- const [loading, setLoading] = useState(true);
14463
+ const [templatesLoaded, setTemplatesLoaded] = useState(false);
14529
14464
  const [syncing, setSyncing] = useState(false);
14530
14465
  const [editingTemplateId, setEditingTemplateId] = useState(null);
14531
14466
  const templatesById = useMemo(() => {
@@ -14535,27 +14470,20 @@ function WhatsAppTriggerBindings() {
14535
14470
  }, [
14536
14471
  templates
14537
14472
  ]);
14538
- const load = useCallback(async () => {
14539
- setLoading(true);
14473
+ const loadTemplates = /* @__PURE__ */ __name(async () => {
14540
14474
  try {
14541
- const [triggersRes, bindingsRes, templatesRes] = await Promise.all([
14542
- jsonFetch("/api/order_notification_triggers?limit=100&sortField=sortOrder&sortOrder=asc"),
14543
- jsonFetch("/api/whatsapp_trigger_bindings?limit=100"),
14544
- jsonFetch("/api/message-templates/whatsapp")
14545
- ]);
14546
- setTriggers((triggersRes.data ?? []).filter((t) => t.enabled));
14547
- setBindings(bindingsRes.data ?? []);
14548
- setTemplates(templatesRes.items ?? []);
14475
+ const res = await jsonFetch("/api/message-templates/whatsapp");
14476
+ setTemplates(res.items ?? []);
14549
14477
  } catch (e) {
14550
- toast.error(e instanceof Error ? e.message : "Failed to load WhatsApp trigger bindings");
14478
+ toast.error(e instanceof Error ? e.message : "Failed to load WhatsApp templates");
14551
14479
  } finally {
14552
- setLoading(false);
14480
+ setTemplatesLoaded(true);
14553
14481
  }
14554
- }, []);
14555
- useEffect(() => {
14556
- void load();
14482
+ }, "loadTemplates");
14483
+ useMemo(() => {
14484
+ if (!templatesLoaded) void loadTemplates();
14557
14485
  }, [
14558
- load
14486
+ templatesLoaded
14559
14487
  ]);
14560
14488
  const handleSync = /* @__PURE__ */ __name(async () => {
14561
14489
  setSyncing(true);
@@ -14563,7 +14491,10 @@ function WhatsAppTriggerBindings() {
14563
14491
  await jsonFetch("/api/message-templates/whatsapp/sync", {
14564
14492
  method: "POST"
14565
14493
  });
14566
- await load();
14494
+ await Promise.all([
14495
+ loadTemplates(),
14496
+ reload()
14497
+ ]);
14567
14498
  toast.success("Synced templates from Meta");
14568
14499
  } catch (e) {
14569
14500
  toast.error(e instanceof Error ? e.message : "Sync failed");
@@ -14572,40 +14503,8 @@ function WhatsAppTriggerBindings() {
14572
14503
  }
14573
14504
  }, "handleSync");
14574
14505
  const handleRebind = /* @__PURE__ */ __name(async (triggerKey, messageTemplateId) => {
14575
- const existing = bindings.find((b) => b.triggerKey === triggerKey);
14576
- try {
14577
- if (existing) {
14578
- const updated = await jsonFetch(`/api/whatsapp_trigger_bindings/${existing.id}`, {
14579
- method: "PUT",
14580
- headers: {
14581
- "Content-Type": "application/json"
14582
- },
14583
- body: JSON.stringify({
14584
- messageTemplateId
14585
- })
14586
- });
14587
- setBindings((prev) => prev.map((b) => b.id === updated.id ? updated : b));
14588
- } else {
14589
- const created = await jsonFetch("/api/whatsapp_trigger_bindings", {
14590
- method: "POST",
14591
- headers: {
14592
- "Content-Type": "application/json"
14593
- },
14594
- body: JSON.stringify({
14595
- triggerKey,
14596
- messageTemplateId,
14597
- enabled: true
14598
- })
14599
- });
14600
- setBindings((prev) => [
14601
- ...prev,
14602
- created
14603
- ]);
14604
- }
14605
- toast.success(`"${triggerKey}" bound to "${templatesById.get(messageTemplateId)?.name ?? messageTemplateId}"`);
14606
- } catch (e) {
14607
- toast.error(e instanceof Error ? e.message : "Failed to update binding");
14608
- }
14506
+ const label = templatesById.get(messageTemplateId)?.name ?? String(messageTemplateId);
14507
+ await rebind(triggerKey, messageTemplateId, label);
14609
14508
  }, "handleRebind");
14610
14509
  const handleSaveParamOrder = /* @__PURE__ */ __name(async (templateId, paramOrder) => {
14611
14510
  try {
@@ -14625,7 +14524,7 @@ function WhatsAppTriggerBindings() {
14625
14524
  toast.error(e instanceof Error ? e.message : "Failed to save param order");
14626
14525
  }
14627
14526
  }, "handleSaveParamOrder");
14628
- if (loading) {
14527
+ if (loading || !templatesLoaded) {
14629
14528
  return /* @__PURE__ */ React.createElement("div", {
14630
14529
  className: "flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400"
14631
14530
  }, /* @__PURE__ */ React.createElement(Loader2, {
@@ -14640,7 +14539,7 @@ function WhatsAppTriggerBindings() {
14640
14539
  className: "text-sm font-medium text-gray-900 dark:text-white"
14641
14540
  }, "Order trigger templates"), /* @__PURE__ */ React.createElement("p", {
14642
14541
  className: "text-xs text-gray-500 dark:text-gray-400 mt-1"
14643
- }, "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, {
14542
+ }, "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, {
14644
14543
  type: "button",
14645
14544
  variant: "outline",
14646
14545
  size: "sm",
@@ -14817,24 +14716,15 @@ function ParamOrderEditor({ template, onCancel, onSave }) {
14817
14716
  onClick: /* @__PURE__ */ __name(() => onSave(values), "onClick")
14818
14717
  }, "Save"))));
14819
14718
  }
14820
- var KNOWN_VARS, CONTEXT_HINTS;
14719
+ var CONTEXT_HINTS;
14821
14720
  var init_whatsappTriggerBindingsPage = __esm({
14822
14721
  "src/admin/pages/whatsappTriggerBindingsPage.tsx"() {
14823
14722
  "use client";
14824
14723
  init_button();
14825
14724
  init_select();
14725
+ init_order_notification_bindings_shared();
14826
14726
  __name(varCount, "varCount");
14827
14727
  __name(bodyExample, "bodyExample");
14828
- KNOWN_VARS = [
14829
- "orderId",
14830
- "orderNumber",
14831
- "customerName",
14832
- "customerPhone",
14833
- "total",
14834
- "currency",
14835
- "vendorId",
14836
- "refundAmount"
14837
- ];
14838
14728
  CONTEXT_HINTS = [
14839
14729
  {
14840
14730
  pattern: /refund/,
@@ -14873,42 +14763,280 @@ var init_whatsappTriggerBindingsPage = __esm({
14873
14763
  __name(guessFromExampleShape, "guessFromExampleShape");
14874
14764
  __name(guessKnownVar, "guessKnownVar");
14875
14765
  __name(contextBeforePlaceholder, "contextBeforePlaceholder");
14876
- __name(jsonFetch, "jsonFetch");
14877
14766
  __name(WhatsAppTriggerBindings, "WhatsAppTriggerBindings");
14878
14767
  __name(ParamOrderEditor, "ParamOrderEditor");
14879
14768
  }
14880
14769
  });
14881
- function EventNotificationsPluginSettings({ settingsGroup, enabled, onEnabledChange, onSaveSettings, saving }) {
14882
- const [tab, setTab] = useState("email");
14883
- const [templateItems, setTemplateItems] = useState([]);
14884
- const [loadingTemplates, setLoadingTemplates] = useState(true);
14770
+ function EmailTriggerBindings() {
14771
+ const { triggers, bindings, loading, rebind } = useOrderNotificationTriggerBindings("email");
14772
+ const [templates, setTemplates] = useState([]);
14773
+ const [templatesLoaded, setTemplatesLoaded] = useState(false);
14774
+ const templatesById = useMemo(() => {
14775
+ const m = /* @__PURE__ */ new Map();
14776
+ for (const t of templates) m.set(t.id, t);
14777
+ return m;
14778
+ }, [
14779
+ templates
14780
+ ]);
14885
14781
  useEffect(() => {
14886
14782
  let cancelled = false;
14887
- setLoadingTemplates(true);
14888
- fetchEventOrderTemplateRows().then((rows) => {
14889
- if (!cancelled) setTemplateItems(rows);
14890
- }).finally(() => {
14891
- if (!cancelled) setLoadingTemplates(false);
14892
- });
14783
+ (async () => {
14784
+ try {
14785
+ const res = await jsonFetch("/api/message-templates/email");
14786
+ if (!cancelled) setTemplates(res.items ?? []);
14787
+ } catch (e) {
14788
+ if (!cancelled) toast.error(e instanceof Error ? e.message : "Failed to load email templates");
14789
+ } finally {
14790
+ if (!cancelled) setTemplatesLoaded(true);
14791
+ }
14792
+ })();
14893
14793
  return () => {
14894
14794
  cancelled = true;
14895
14795
  };
14896
14796
  }, []);
14897
- const handleSaveAll = /* @__PURE__ */ __name(async () => {
14898
- const settingsOk = await onSaveSettings();
14899
- if (!settingsOk) {
14900
- toast.error("Failed to save plugin settings");
14901
- return;
14797
+ const handleTemplateSaved = /* @__PURE__ */ __name((template) => {
14798
+ setTemplates((prev) => {
14799
+ const exists = prev.some((t) => t.id === template.id);
14800
+ return exists ? prev.map((t) => t.id === template.id ? template : t) : [
14801
+ ...prev,
14802
+ template
14803
+ ].sort((a, b) => a.name.localeCompare(b.name));
14804
+ });
14805
+ }, "handleTemplateSaved");
14806
+ if (loading || !templatesLoaded) {
14807
+ return /* @__PURE__ */ React.createElement("div", {
14808
+ className: "flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400"
14809
+ }, /* @__PURE__ */ React.createElement(Loader2, {
14810
+ className: "h-4 w-4 animate-spin"
14811
+ }), "Loading trigger bindings\u2026");
14812
+ }
14813
+ return /* @__PURE__ */ React.createElement("div", {
14814
+ className: "space-y-3 rounded-md border border-gray-200 dark:border-gray-600 p-3"
14815
+ }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", {
14816
+ className: "text-sm font-medium text-gray-900 dark:text-white"
14817
+ }, "Order trigger emails"), /* @__PURE__ */ React.createElement("p", {
14818
+ className: "text-xs text-gray-500 dark:text-gray-400 mt-1"
14819
+ }, "Write the subject and body for each trigger below. Use", " ", /* @__PURE__ */ React.createElement("code", {
14820
+ className: "text-[11px]"
14821
+ }, "{{variable}}"), " tokens \u2014 click a variable chip to insert it at your cursor.")), /* @__PURE__ */ React.createElement("div", {
14822
+ className: "space-y-4"
14823
+ }, triggers.length === 0 ? /* @__PURE__ */ React.createElement("p", {
14824
+ className: "text-xs text-amber-600 dark:text-amber-400"
14825
+ }, "No order triggers found. Run database migrations to seed default triggers.") : null, triggers.map(({ triggerKey, label, description }) => {
14826
+ const binding = bindings.find((b) => b.triggerKey === triggerKey) ?? null;
14827
+ const template = binding?.messageTemplateId != null ? templatesById.get(binding.messageTemplateId) : void 0;
14828
+ return /* @__PURE__ */ React.createElement(TriggerEmailEditor, {
14829
+ key: triggerKey,
14830
+ triggerKey,
14831
+ label,
14832
+ description,
14833
+ template,
14834
+ onSaved: handleTemplateSaved,
14835
+ onBind: /* @__PURE__ */ __name((templateId, templateLabel) => rebind(triggerKey, templateId, templateLabel), "onBind")
14836
+ });
14837
+ })));
14838
+ }
14839
+ function TriggerEmailEditor({ triggerKey, label, description, template, onSaved, onBind }) {
14840
+ const defaults = useMemo(() => getEmailTriggerDefaults(triggerKey), [
14841
+ triggerKey
14842
+ ]);
14843
+ const [subject, setSubject] = useState(() => template?.subject ?? defaults.subject);
14844
+ const [body, setBody] = useState(() => template?.body ?? defaults.body);
14845
+ const [saving, setSaving] = useState(false);
14846
+ const [focusedField, setFocusedField] = useState("body");
14847
+ const subjectRef = useRef(null);
14848
+ const bodyRef = useRef(null);
14849
+ useEffect(() => {
14850
+ if (template) {
14851
+ setSubject(template.subject ?? "");
14852
+ setBody(template.body ?? "");
14853
+ } else {
14854
+ setSubject(defaults.subject);
14855
+ setBody(defaults.body);
14902
14856
  }
14903
- if (templateItems.length > 0) {
14904
- const tplOk = await saveEventOrderTemplateRows(templateItems);
14905
- if (!tplOk) {
14906
- toast.error("Failed to save notification templates");
14857
+ }, [
14858
+ template,
14859
+ defaults.subject,
14860
+ defaults.body
14861
+ ]);
14862
+ const savedSubject = template?.subject ?? defaults.subject;
14863
+ const savedBody = template?.body ?? defaults.body;
14864
+ const isDirty = subject !== savedSubject || body !== savedBody;
14865
+ function insertVariable(v) {
14866
+ const token = `{{${v}}}`;
14867
+ if (focusedField === "subject") {
14868
+ const el = subjectRef.current;
14869
+ const start = el?.selectionStart ?? subject.length;
14870
+ const end = el?.selectionEnd ?? subject.length;
14871
+ const next = subject.slice(0, start) + token + subject.slice(end);
14872
+ setSubject(next);
14873
+ requestAnimationFrame(() => {
14874
+ el?.focus();
14875
+ el?.setSelectionRange(start + token.length, start + token.length);
14876
+ });
14877
+ } else {
14878
+ const el = bodyRef.current;
14879
+ const start = el?.selectionStart ?? body.length;
14880
+ const end = el?.selectionEnd ?? body.length;
14881
+ const next = body.slice(0, start) + token + body.slice(end);
14882
+ setBody(next);
14883
+ requestAnimationFrame(() => {
14884
+ el?.focus();
14885
+ el?.setSelectionRange(start + token.length, start + token.length);
14886
+ });
14887
+ }
14888
+ }
14889
+ __name(insertVariable, "insertVariable");
14890
+ const canSave = body.trim() !== "";
14891
+ const handleSave = /* @__PURE__ */ __name(async () => {
14892
+ if (!canSave) return;
14893
+ setSaving(true);
14894
+ try {
14895
+ const templateName = template?.name ?? `${label} email`;
14896
+ let saved;
14897
+ if (template) {
14898
+ const res = await jsonFetch(`/api/message-templates/email/${template.id}`, {
14899
+ method: "PATCH",
14900
+ headers: {
14901
+ "Content-Type": "application/json"
14902
+ },
14903
+ body: JSON.stringify({
14904
+ name: templateName,
14905
+ subject,
14906
+ body
14907
+ })
14908
+ });
14909
+ saved = res.item;
14910
+ toast.success(`"${label}" email template saved`);
14911
+ } else {
14912
+ const res = await jsonFetch("/api/message-templates/email", {
14913
+ method: "POST",
14914
+ headers: {
14915
+ "Content-Type": "application/json"
14916
+ },
14917
+ body: JSON.stringify({
14918
+ name: templateName,
14919
+ subject,
14920
+ body
14921
+ })
14922
+ });
14923
+ saved = res.item;
14924
+ onSaved(saved);
14925
+ const bound = await onBind(saved.id, saved.name);
14926
+ if (!bound) {
14927
+ toast.success(`"${label}" email template created`);
14928
+ }
14907
14929
  return;
14908
14930
  }
14931
+ onSaved(saved);
14932
+ } catch (e) {
14933
+ toast.error(e instanceof Error ? e.message : "Failed to save template");
14934
+ } finally {
14935
+ setSaving(false);
14909
14936
  }
14910
- toast.success("Event notification settings saved");
14911
- }, "handleSaveAll");
14937
+ }, "handleSave");
14938
+ return /* @__PURE__ */ React.createElement("div", {
14939
+ className: "space-y-3 border-t border-gray-100 pt-4 first:border-t-0 first:pt-0 dark:border-gray-700"
14940
+ }, /* @__PURE__ */ React.createElement("div", {
14941
+ className: "flex items-start justify-between gap-2"
14942
+ }, /* @__PURE__ */ React.createElement("div", {
14943
+ className: "min-w-0"
14944
+ }, /* @__PURE__ */ React.createElement("div", {
14945
+ className: "flex items-center gap-2"
14946
+ }, /* @__PURE__ */ React.createElement(Mail, {
14947
+ className: "h-3.5 w-3.5 text-gray-400 shrink-0"
14948
+ }), /* @__PURE__ */ React.createElement("span", {
14949
+ className: "text-sm font-medium text-gray-900 dark:text-gray-100"
14950
+ }, label), /* @__PURE__ */ React.createElement("span", {
14951
+ className: "text-[11px] font-mono text-gray-400"
14952
+ }, triggerKey)), description ? /* @__PURE__ */ React.createElement("p", {
14953
+ className: "text-[11px] text-gray-500 dark:text-gray-400 mt-0.5 ml-5"
14954
+ }, description) : null), /* @__PURE__ */ React.createElement(Button, {
14955
+ type: "button",
14956
+ size: "sm",
14957
+ className: "h-8 gap-1.5 shrink-0",
14958
+ disabled: !canSave || saving || !isDirty && Boolean(template),
14959
+ onClick: /* @__PURE__ */ __name(() => void handleSave(), "onClick")
14960
+ }, saving ? /* @__PURE__ */ React.createElement(Loader2, {
14961
+ className: "h-3.5 w-3.5 animate-spin"
14962
+ }) : /* @__PURE__ */ React.createElement(Save, {
14963
+ className: "h-3.5 w-3.5"
14964
+ }), template ? "Save" : "Create")), !template ? /* @__PURE__ */ React.createElement("p", {
14965
+ className: "text-xs text-amber-600 dark:text-amber-400 ml-5"
14966
+ }, "No email saved for this trigger yet \u2014 review the draft below and click Create.") : null, /* @__PURE__ */ React.createElement("div", {
14967
+ 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"
14968
+ }, /* @__PURE__ */ React.createElement("div", {
14969
+ className: "space-y-1"
14970
+ }, /* @__PURE__ */ React.createElement(Label3, {
14971
+ className: "text-xs text-gray-600 dark:text-gray-400"
14972
+ }, "Subject"), /* @__PURE__ */ React.createElement(Input, {
14973
+ ref: subjectRef,
14974
+ value: subject,
14975
+ onChange: /* @__PURE__ */ __name((e) => setSubject(e.target.value), "onChange"),
14976
+ onFocus: /* @__PURE__ */ __name(() => setFocusedField("subject"), "onFocus"),
14977
+ placeholder: defaults.subjectPlaceholder,
14978
+ className: "h-8 text-sm bg-white dark:bg-gray-900"
14979
+ })), /* @__PURE__ */ React.createElement("div", {
14980
+ className: "space-y-1"
14981
+ }, /* @__PURE__ */ React.createElement(Label3, {
14982
+ className: "text-xs text-gray-600 dark:text-gray-400"
14983
+ }, "Body"), /* @__PURE__ */ React.createElement(Textarea, {
14984
+ ref: bodyRef,
14985
+ value: body,
14986
+ onChange: /* @__PURE__ */ __name((e) => setBody(e.target.value), "onChange"),
14987
+ onFocus: /* @__PURE__ */ __name(() => setFocusedField("body"), "onFocus"),
14988
+ rows: 6,
14989
+ placeholder: defaults.bodyPlaceholder,
14990
+ className: "text-sm font-mono bg-white dark:bg-gray-900 resize-y min-h-[120px]"
14991
+ })), /* @__PURE__ */ React.createElement("div", {
14992
+ className: "space-y-1.5"
14993
+ }, /* @__PURE__ */ React.createElement(Label3, {
14994
+ className: "text-xs text-gray-500 dark:text-gray-400"
14995
+ }, "Variables \u2014 inserts into ", focusedField === "subject" ? "subject" : "body", ":"), /* @__PURE__ */ React.createElement("div", {
14996
+ className: "flex flex-wrap gap-1.5"
14997
+ }, KNOWN_VARS.map((v) => /* @__PURE__ */ React.createElement("button", {
14998
+ key: v,
14999
+ type: "button",
15000
+ title: VAR_HINTS[v],
15001
+ onClick: /* @__PURE__ */ __name(() => insertVariable(v), "onClick"),
15002
+ 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"
15003
+ }, `{{${v}}}`))), /* @__PURE__ */ React.createElement("p", {
15004
+ className: "text-[10px] text-gray-400 dark:text-gray-500"
15005
+ }, "Hover a chip to see what each variable contains at send time."))));
15006
+ }
15007
+ var VAR_HINTS;
15008
+ var init_emailTriggersBindingsPage = __esm({
15009
+ "src/admin/pages/emailTriggersBindingsPage.tsx"() {
15010
+ "use client";
15011
+ init_button();
15012
+ init_input();
15013
+ init_textarea();
15014
+ init_label();
15015
+ init_order_notification_bindings_shared();
15016
+ VAR_HINTS = {
15017
+ orderId: "Internal order ID",
15018
+ orderNumber: "Display order number",
15019
+ customerName: "Customer full name",
15020
+ customerPhone: "Customer phone",
15021
+ total: "Order total amount",
15022
+ currency: "Currency code (e.g. INR)",
15023
+ vendorId: "Vendor / store ID",
15024
+ refundAmount: "Refund amount (refund triggers)"
15025
+ };
15026
+ __name(EmailTriggerBindings, "EmailTriggerBindings");
15027
+ __name(TriggerEmailEditor, "TriggerEmailEditor");
15028
+ }
15029
+ });
15030
+ function EventNotificationsPluginSettings({ settingsGroup, enabled, onEnabledChange, onSaveSettings, saving }) {
15031
+ const [tab, setTab] = useState("email");
15032
+ const handleSaveEnabled = /* @__PURE__ */ __name(async () => {
15033
+ const ok = await onSaveSettings();
15034
+ if (!ok) {
15035
+ toast.error("Failed to save plugin settings");
15036
+ return;
15037
+ }
15038
+ toast.success("Settings saved");
15039
+ }, "handleSaveEnabled");
14912
15040
  return /* @__PURE__ */ React.createElement("div", {
14913
15041
  className: "space-y-5"
14914
15042
  }, /* @__PURE__ */ React.createElement("div", {
@@ -14923,11 +15051,13 @@ function EventNotificationsPluginSettings({ settingsGroup, enabled, onEnabledCha
14923
15051
  className: "font-medium text-gray-900 dark:text-white"
14924
15052
  }, "Order notifications"), /* @__PURE__ */ React.createElement("ol", {
14925
15053
  className: "list-decimal pl-4 space-y-1 text-xs leading-relaxed"
14926
- }, /* @__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", {
15054
+ }, /* @__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", {
15055
+ className: "text-[11px]"
15056
+ }, "{{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", {
14927
15057
  className: "text-[11px]"
14928
- }, "order_notification_triggers"), " (migration seeds defaults). Bindings live in", " ", /* @__PURE__ */ React.createElement("code", {
15058
+ }, "order_notification_triggers"), ") and share", " ", /* @__PURE__ */ React.createElement("code", {
14929
15059
  className: "text-[11px]"
14930
- }, "whatsapp_trigger_bindings"), ".")), /* @__PURE__ */ React.createElement("p", {
15060
+ }, "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", {
14931
15061
  className: "text-xs text-gray-500 dark:text-gray-400"
14932
15062
  }, "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", {
14933
15063
  className: "flex items-center justify-between gap-2"
@@ -14954,16 +15084,9 @@ function EventNotificationsPluginSettings({ settingsGroup, enabled, onEnabledCha
14954
15084
  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"}`
14955
15085
  }, /* @__PURE__ */ React.createElement(MessageCircle, {
14956
15086
  className: "h-4 w-4"
14957
- }), "WhatsApp template"))), tab === "whatsapp" ? /* @__PURE__ */ React.createElement(WhatsAppTriggerBindings, null) : loadingTemplates ? /* @__PURE__ */ React.createElement("p", {
14958
- className: "text-sm text-gray-500"
14959
- }, "Loading templates\u2026") : /* @__PURE__ */ React.createElement(EventOrderGlobalTemplates, {
14960
- items: templateItems,
14961
- onChange: setTemplateItems,
14962
- channel: tab,
14963
- settingsGroup
14964
- }), /* @__PURE__ */ React.createElement(Button, {
15087
+ }), "WhatsApp template"))), tab === "whatsapp" ? /* @__PURE__ */ React.createElement(WhatsAppTriggerBindings, null) : /* @__PURE__ */ React.createElement(EmailTriggerBindings, null), /* @__PURE__ */ React.createElement(Button, {
14965
15088
  size: "sm",
14966
- onClick: /* @__PURE__ */ __name(() => void handleSaveAll(), "onClick"),
15089
+ onClick: /* @__PURE__ */ __name(() => void handleSaveEnabled(), "onClick"),
14967
15090
  disabled: saving,
14968
15091
  className: "gap-1"
14969
15092
  }, /* @__PURE__ */ React.createElement(Save, {
@@ -14976,8 +15099,8 @@ var init_EventNotificationsPluginSettings = __esm({
14976
15099
  init_button();
14977
15100
  init_label();
14978
15101
  init_switch();
14979
- init_EventOrderGlobalTemplates();
14980
15102
  init_whatsappTriggerBindingsPage();
15103
+ init_emailTriggersBindingsPage();
14981
15104
  __name(EventNotificationsPluginSettings, "EventNotificationsPluginSettings");
14982
15105
  }
14983
15106
  });
@@ -26116,47 +26239,6 @@ var init_ProductIncomeCalculatorDialog = __esm({
26116
26239
  });
26117
26240
 
26118
26241
  // src/lib/currency-prices.ts
26119
- function normalizeCurrencyCode(code) {
26120
- return code.trim().toUpperCase();
26121
- }
26122
- function parseCurrencyPrices(raw) {
26123
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
26124
- const out = {};
26125
- for (const [key, value] of Object.entries(raw)) {
26126
- const code = normalizeCurrencyCode(key);
26127
- if (!code) continue;
26128
- const n = Number(value);
26129
- if (Number.isFinite(n) && n >= 0) out[code] = n;
26130
- }
26131
- return out;
26132
- }
26133
- function serializeCurrencyPrices(defaultCurrency, defaultPrice, additional) {
26134
- const base = normalizeCurrencyCode(defaultCurrency) || "INR";
26135
- const out = {
26136
- [base]: defaultPrice
26137
- };
26138
- for (const [code, amount] of Object.entries(additional)) {
26139
- const normalized = normalizeCurrencyCode(code);
26140
- if (!normalized || normalized === base) continue;
26141
- if (Number.isFinite(amount) && amount >= 0) out[normalized] = amount;
26142
- }
26143
- return Object.keys(out).length ? out : null;
26144
- }
26145
- function currencyPricesToForm(defaultCurrency, storedPrice, currencyPrices) {
26146
- const base = normalizeCurrencyCode(defaultCurrency) || "INR";
26147
- const map = parseCurrencyPrices(currencyPrices);
26148
- const defaultFromMap = map[base];
26149
- const defaultPrice = defaultFromMap != null ? String(defaultFromMap) : storedPrice != null && Number.isFinite(Number(storedPrice)) ? String(storedPrice) : "";
26150
- const additional = {};
26151
- for (const [code, amount] of Object.entries(map)) {
26152
- if (code === base) continue;
26153
- additional[code] = String(amount);
26154
- }
26155
- return {
26156
- defaultPrice,
26157
- additional
26158
- };
26159
- }
26160
26242
  function parseDefaultProductPrice(defaultPriceRaw) {
26161
26243
  const trimmed = defaultPriceRaw.trim();
26162
26244
  if (!trimmed) return null;
@@ -26166,31 +26248,9 @@ function parseDefaultProductPrice(defaultPriceRaw) {
26166
26248
  price: Math.round(price * 100) / 100
26167
26249
  };
26168
26250
  }
26169
- function formToCurrencyPricesPayload(defaultCurrency, defaultPriceRaw, additionalForm) {
26170
- const base = normalizeCurrencyCode(defaultCurrency) || "INR";
26171
- const defaultPrice = Number(defaultPriceRaw.trim());
26172
- if (!Number.isFinite(defaultPrice) || defaultPrice < 0) return null;
26173
- const additional = {};
26174
- for (const [code, raw] of Object.entries(additionalForm)) {
26175
- const trimmed = raw.trim();
26176
- if (!trimmed) continue;
26177
- const n = Number(trimmed);
26178
- if (!Number.isFinite(n) || n < 0) continue;
26179
- additional[normalizeCurrencyCode(code)] = n;
26180
- }
26181
- return {
26182
- price: defaultPrice,
26183
- currencyPrices: serializeCurrencyPrices(base, defaultPrice, additional)
26184
- };
26185
- }
26186
26251
  var init_currency_prices = __esm({
26187
26252
  "src/lib/currency-prices.ts"() {
26188
- __name(normalizeCurrencyCode, "normalizeCurrencyCode");
26189
- __name(parseCurrencyPrices, "parseCurrencyPrices");
26190
- __name(serializeCurrencyPrices, "serializeCurrencyPrices");
26191
- __name(currencyPricesToForm, "currencyPricesToForm");
26192
26253
  __name(parseDefaultProductPrice, "parseDefaultProductPrice");
26193
- __name(formToCurrencyPricesPayload, "formToCurrencyPricesPayload");
26194
26254
  }
26195
26255
  });
26196
26256
 
@@ -29628,44 +29688,6 @@ function SupportedCurrenciesPicker({ currencies, defaultCurrency, supported, onD
29628
29688
  className: "mt-1 text-xs text-gray-500"
29629
29689
  }, "Default currency is always included.")));
29630
29690
  }
29631
- 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" }) {
29632
- const base = defaultCurrency.trim().toUpperCase() || "INR";
29633
- const others = currencies.filter((c) => c.code.toUpperCase() !== base);
29634
- return /* @__PURE__ */ React.createElement("div", {
29635
- className: "space-y-4"
29636
- }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
29637
- className: labelClassName
29638
- }, base, " price ", /* @__PURE__ */ React.createElement("span", {
29639
- className: "text-red-600"
29640
- }, "*")), /* @__PURE__ */ React.createElement("input", {
29641
- type: "number",
29642
- min: 0,
29643
- step: "any",
29644
- value: defaultPrice,
29645
- onChange: /* @__PURE__ */ __name((e) => onDefaultPriceChange(e.target.value), "onChange"),
29646
- className: inputClassName,
29647
- required: true
29648
- })), others.length > 0 ? /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", {
29649
- className: `${labelClassName} mb-2`
29650
- }, "Additional currencies (optional)"), /* @__PURE__ */ React.createElement("div", {
29651
- className: "grid gap-3 sm:grid-cols-2"
29652
- }, others.map((c) => {
29653
- const code = c.code.toUpperCase();
29654
- return /* @__PURE__ */ React.createElement("div", {
29655
- key: code
29656
- }, /* @__PURE__ */ React.createElement("label", {
29657
- className: labelClassName
29658
- }, code), /* @__PURE__ */ React.createElement("input", {
29659
- type: "number",
29660
- min: 0,
29661
- step: "any",
29662
- value: additionalPrices[code] ?? "",
29663
- onChange: /* @__PURE__ */ __name((e) => onAdditionalPriceChange(code, e.target.value), "onChange"),
29664
- className: inputClassName,
29665
- placeholder: "Optional"
29666
- }));
29667
- }))) : null);
29668
- }
29669
29691
  var inputCls5, labelCls5, ICONS;
29670
29692
  var init_EventManagementFields = __esm({
29671
29693
  "src/components/Admin/EventManagementFields.tsx"() {
@@ -29687,7 +29709,6 @@ var init_EventManagementFields = __esm({
29687
29709
  __name(SocialMediaLinksDisplay, "SocialMediaLinksDisplay");
29688
29710
  __name(TimezoneSelect, "TimezoneSelect");
29689
29711
  __name(SupportedCurrenciesPicker, "SupportedCurrenciesPicker");
29690
- __name(MultiCurrencyPriceFields, "MultiCurrencyPriceFields");
29691
29712
  }
29692
29713
  });
29693
29714
 
@@ -29839,28 +29860,11 @@ function intersectSupportedCurrencies(supported, defaultCurrency, options) {
29839
29860
  ])
29840
29861
  ];
29841
29862
  }
29842
- function currencyOptionsIncluding(options, codes) {
29843
- const map = new Map(options.map((o) => [
29844
- o.code.toUpperCase(),
29845
- o
29846
- ]));
29847
- for (const code of codes) {
29848
- const normalized = code.trim().toUpperCase();
29849
- if (!normalized || map.has(normalized)) continue;
29850
- map.set(normalized, {
29851
- code: normalized
29852
- });
29853
- }
29854
- return [
29855
- ...map.values()
29856
- ].sort((a, b) => a.code.localeCompare(b.code));
29857
- }
29858
29863
  var init_currency_options = __esm({
29859
29864
  "src/lib/currency-options.ts"() {
29860
29865
  __name(parseCurrencyApiRows, "parseCurrencyApiRows");
29861
29866
  __name(fetchActiveCurrencyOptions, "fetchActiveCurrencyOptions");
29862
29867
  __name(intersectSupportedCurrencies, "intersectSupportedCurrencies");
29863
- __name(currencyOptionsIncluding, "currencyOptionsIncluding");
29864
29868
  }
29865
29869
  });
29866
29870
 
@@ -30840,25 +30844,14 @@ function formatDateTimeLocal(value) {
30840
30844
  if (isNaN(date.getTime())) return "";
30841
30845
  return date.toISOString().slice(0, 16);
30842
30846
  }
30843
- async function fetchEventPricing(eventId) {
30847
+ async function fetchEventDefaultCurrency(eventId) {
30844
30848
  try {
30845
30849
  const res = await fetch(`/api/events/${eventId}`);
30846
- if (!res.ok) return DEFAULT_PRICING;
30850
+ if (!res.ok) return "INR";
30847
30851
  const data = await res.json();
30848
- const defaultCurrency = String(data.defaultCurrency ?? "INR").trim().toUpperCase() || "INR";
30849
- const supported = Array.isArray(data.supportedCurrencies) ? data.supportedCurrencies.map((c) => String(c).trim().toUpperCase()).filter((c) => Boolean(c)) : [
30850
- defaultCurrency
30851
- ];
30852
- const set = new Set(supported);
30853
- set.add(defaultCurrency);
30854
- return {
30855
- defaultCurrency,
30856
- supportedCurrencies: [
30857
- ...set
30858
- ]
30859
- };
30852
+ return String(data.defaultCurrency ?? "INR").trim().toUpperCase() || "INR";
30860
30853
  } catch {
30861
- return DEFAULT_PRICING;
30854
+ return "INR";
30862
30855
  }
30863
30856
  }
30864
30857
  async function validateComboProductInventory(productIds) {
@@ -30900,10 +30893,8 @@ function ComboEditPage({ comboId }) {
30900
30893
  const [name, setName] = useState("");
30901
30894
  const [desc, setDesc] = useState("");
30902
30895
  const [eventId, setEventId] = useState("");
30903
- const [defaultPriceStr, setDefaultPriceStr] = useState("");
30904
- const [additionalPrices, setAdditionalPrices] = useState({});
30905
- const [pricingConfig, setPricingConfig] = useState(DEFAULT_PRICING);
30906
- const [currencySchemaOptions, setCurrencySchemaOptions] = useState([]);
30896
+ const [priceStr, setPriceStr] = useState("");
30897
+ const [defaultCurrency, setDefaultCurrency] = useState("INR");
30907
30898
  const [minSelectableItems, setMinSelectableItems] = useState(1);
30908
30899
  const [maxSelectableItems, setMaxSelectableItems] = useState(1);
30909
30900
  const [status, setStatus] = useState("active");
@@ -30913,23 +30904,14 @@ function ComboEditPage({ comboId }) {
30913
30904
  const [productOptions, setProductOptions] = useState([]);
30914
30905
  const [fixedItems, setFixedItems] = useState([]);
30915
30906
  const [addonItems, setAddonItems] = useState([]);
30916
- useEffect(() => {
30917
- let cancelled = false;
30918
- fetchActiveCurrencyOptions().then(({ options }) => {
30919
- if (!cancelled && options.length) setCurrencySchemaOptions(options);
30920
- });
30921
- return () => {
30922
- cancelled = true;
30923
- };
30924
- }, []);
30925
30907
  useEffect(() => {
30926
30908
  if (!eventId) {
30927
- setPricingConfig(DEFAULT_PRICING);
30909
+ setDefaultCurrency("INR");
30928
30910
  return;
30929
30911
  }
30930
30912
  let cancelled = false;
30931
- fetchEventPricing(eventId).then((config) => {
30932
- if (!cancelled) setPricingConfig(config);
30913
+ fetchEventDefaultCurrency(eventId).then((code) => {
30914
+ if (!cancelled) setDefaultCurrency(code);
30933
30915
  });
30934
30916
  return () => {
30935
30917
  cancelled = true;
@@ -30937,10 +30919,6 @@ function ComboEditPage({ comboId }) {
30937
30919
  }, [
30938
30920
  eventId
30939
30921
  ]);
30940
- const pricingCurrencyOptions = currencyOptionsIncluding(currencySchemaOptions, [
30941
- pricingConfig.defaultCurrency,
30942
- ...pricingConfig.supportedCurrencies
30943
- ]).filter((c) => pricingConfig.supportedCurrencies.map((x) => x.toUpperCase()).includes(c.code.toUpperCase()));
30944
30922
  useEffect(() => {
30945
30923
  let cancelled = false;
30946
30924
  (async () => {
@@ -31029,11 +31007,11 @@ function ComboEditPage({ comboId }) {
31029
31007
  setName(data.name ?? "");
31030
31008
  setDesc(data.desc ?? "");
31031
31009
  setEventId(data.eventId != null ? String(data.eventId) : "");
31032
- const eventPricing = data.eventId != null ? await fetchEventPricing(String(data.eventId)) : DEFAULT_PRICING;
31033
- if (!cancelled) setPricingConfig(eventPricing);
31034
- const { defaultPrice, additional } = currencyPricesToForm(eventPricing.defaultCurrency, data.price != null ? Number(data.price) : null, data.currencyPrices);
31035
- setDefaultPriceStr(defaultPrice);
31036
- setAdditionalPrices(additional);
31010
+ if (data.eventId != null) {
31011
+ const currency = await fetchEventDefaultCurrency(String(data.eventId));
31012
+ if (!cancelled) setDefaultCurrency(currency);
31013
+ }
31014
+ setPriceStr(data.price != null && Number.isFinite(Number(data.price)) ? String(data.price) : "");
31037
31015
  setMinSelectableItems(Number(data.minSelectableItems) || 1);
31038
31016
  setMaxSelectableItems(Number(data.maxSelectableItems) || 1);
31039
31017
  setStatus(data.status ?? "active");
@@ -31084,10 +31062,6 @@ function ComboEditPage({ comboId }) {
31084
31062
  create,
31085
31063
  duplicateSourceId
31086
31064
  ]);
31087
- /* @__PURE__ */ new Set([
31088
- ...fixedItems.map((i) => i.productId),
31089
- ...addonItems.map((i) => i.productId)
31090
- ]);
31091
31065
  const availableForFixed = productOptions.filter((p) => !addonItems.some((i) => i.productId === p.value));
31092
31066
  const availableForAddon = productOptions.filter((p) => !fixedItems.some((i) => i.productId === p.value));
31093
31067
  const addFixed = /* @__PURE__ */ __name((val) => {
@@ -31128,16 +31102,16 @@ function ComboEditPage({ comboId }) {
31128
31102
  ]);
31129
31103
  return;
31130
31104
  }
31131
- if (!defaultPriceStr.trim()) {
31105
+ if (!priceStr.trim()) {
31132
31106
  setErrors([
31133
- `${pricingConfig.defaultCurrency} price is required`
31107
+ `Price (${defaultCurrency}) is required`
31134
31108
  ]);
31135
31109
  return;
31136
31110
  }
31137
- const pricingPayload = formToCurrencyPricesPayload(pricingConfig.defaultCurrency, defaultPriceStr, additionalPrices);
31138
- if (!pricingPayload) {
31111
+ const price = Number(priceStr);
31112
+ if (!Number.isFinite(price) || price < 0) {
31139
31113
  setErrors([
31140
- `${pricingConfig.defaultCurrency} price must be a valid number`
31114
+ `Price (${defaultCurrency}) must be a valid non-negative number`
31141
31115
  ]);
31142
31116
  return;
31143
31117
  }
@@ -31185,8 +31159,8 @@ function ComboEditPage({ comboId }) {
31185
31159
  name: trimmedName,
31186
31160
  desc: desc || null,
31187
31161
  eventId: Number(trimmedEventId),
31188
- price: pricingPayload.price,
31189
- currencyPrices: pricingPayload.currencyPrices,
31162
+ price,
31163
+ currencyPrices: null,
31190
31164
  minSelectableItems,
31191
31165
  maxSelectableItems,
31192
31166
  status,
@@ -31353,21 +31327,19 @@ function ComboEditPage({ comboId }) {
31353
31327
  className: "text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2"
31354
31328
  }, "Pricing & add-on selection"), /* @__PURE__ */ React.createElement("div", {
31355
31329
  className: "min-w-0 overflow-hidden border border-gray-200 rounded-lg p-4 bg-gray-50/50 space-y-4"
31356
- }, eventId ? /* @__PURE__ */ React.createElement(MultiCurrencyPriceFields, {
31357
- defaultCurrency: pricingConfig.defaultCurrency,
31358
- currencies: pricingCurrencyOptions,
31359
- defaultPrice: defaultPriceStr,
31360
- additionalPrices,
31361
- onDefaultPriceChange: setDefaultPriceStr,
31362
- onAdditionalPriceChange: /* @__PURE__ */ __name((code, value) => setAdditionalPrices((prev) => ({
31363
- ...prev,
31364
- [code]: value
31365
- })), "onAdditionalPriceChange"),
31366
- labelCls: "block text-xs font-medium text-gray-600 mb-1",
31367
- inputCls: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
31368
- }) : /* @__PURE__ */ React.createElement("p", {
31369
- className: "text-sm text-gray-500"
31370
- }, "Select an event to configure combo pricing currencies."), /* @__PURE__ */ React.createElement("div", {
31330
+ }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
31331
+ className: "block text-xs font-medium text-gray-600 mb-1"
31332
+ }, "Price (", defaultCurrency, ") *"), /* @__PURE__ */ React.createElement("input", {
31333
+ type: "number",
31334
+ min: 0,
31335
+ step: "0.01",
31336
+ value: priceStr,
31337
+ onChange: /* @__PURE__ */ __name((e) => setPriceStr(e.target.value), "onChange"),
31338
+ disabled: !eventId,
31339
+ className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm disabled:bg-gray-100"
31340
+ }), /* @__PURE__ */ React.createElement("p", {
31341
+ className: "text-xs text-gray-400 mt-1"
31342
+ }, eventId ? "Other currencies use the event\u2019s supported currencies and exchange rates." : "Select an event to set the combo price.")), /* @__PURE__ */ React.createElement("div", {
31371
31343
  className: "grid grid-cols-2 gap-4"
31372
31344
  }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", {
31373
31345
  className: "block text-xs font-medium text-gray-600 mb-1"
@@ -31425,26 +31397,17 @@ function ComboEditPage({ comboId }) {
31425
31397
  sidebar: /* @__PURE__ */ React.createElement(React.Fragment, null)
31426
31398
  }));
31427
31399
  }
31428
- var isCreate5, DEFAULT_PRICING;
31400
+ var isCreate5;
31429
31401
  var init_ComboEditPage = __esm({
31430
31402
  "src/admin/pages/ComboEditPage.tsx"() {
31431
31403
  "use client";
31432
31404
  init_admin_list_return_url();
31433
31405
  init_DetailPageLayout();
31434
31406
  init_DetailPageHeader();
31435
- init_EventManagementFields();
31436
- init_currency_options();
31437
- init_currency_prices();
31438
31407
  init_inventory_validation();
31439
31408
  isCreate5 = /* @__PURE__ */ __name((id) => id === "create", "isCreate");
31440
31409
  __name(formatDateTimeLocal, "formatDateTimeLocal");
31441
- DEFAULT_PRICING = {
31442
- defaultCurrency: "INR",
31443
- supportedCurrencies: [
31444
- "INR"
31445
- ]
31446
- };
31447
- __name(fetchEventPricing, "fetchEventPricing");
31410
+ __name(fetchEventDefaultCurrency, "fetchEventDefaultCurrency");
31448
31411
  __name(validateComboProductInventory, "validateComboProductInventory");
31449
31412
  __name(ComboEditPage, "ComboEditPage");
31450
31413
  }