@burdenoff/microfe-billing 2026.530.3 → 2026.531.1

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 (30) hide show
  1. package/dist/billing/BillingAdminRoutes.js +87 -61
  2. package/dist/billing/BillingAdminRoutes.js.map +1 -1
  3. package/dist/billing/BillingRoot.js +81 -73
  4. package/dist/billing/BillingRoot.js.map +1 -1
  5. package/dist/billing/modules/addons/pages/AddonDetailPage.js +85 -84
  6. package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
  7. package/dist/billing/modules/addons/pages/AddonEditPage.js +176 -195
  8. package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
  9. package/dist/billing/modules/addons/pages/AddonsBrowsePage.js +173 -172
  10. package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
  11. package/dist/billing/modules/addons/pages/AddonsListPage.js +216 -215
  12. package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
  13. package/dist/billing/modules/checkout/hooks/useCheckout.js +165 -161
  14. package/dist/billing/modules/checkout/hooks/useCheckout.js.map +1 -1
  15. package/dist/billing/modules/coupons/pages/CreateCouponPage.js +344 -167
  16. package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
  17. package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js +79 -78
  18. package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
  19. package/dist/billing/modules/plans/pages/PlanEditPage.js +29 -20
  20. package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
  21. package/dist/billing/modules/promotions/index.js +4 -0
  22. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js +289 -0
  23. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -0
  24. package/dist/billing/modules/promotions/pages/PromotionsListPage.js +168 -0
  25. package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -0
  26. package/dist/billing/modules/promotions/pages/index.js +2 -0
  27. package/dist/generated/global-operations.js +182 -75
  28. package/dist/generated/global-operations.js.map +1 -1
  29. package/dist/generated/global-types.js.map +1 -1
  30. package/package.json +1 -1
@@ -21,22 +21,31 @@ var y = () => {
21
21
  let { t: y } = v(), b = (e, t) => {
22
22
  let n = y(e);
23
23
  return n === e ? t : n;
24
- }, { planId: x } = m(), S = c(), ee = s(), { plan: C, isLoading: w, error: T, refetch: E } = l(x), { quotas: D, isLoading: O } = d({ onlyActive: !0 }), { updatePlan: k, isUpdating: A } = u(), [j, M] = p(""), [N, P] = p(""), [F, I] = p("USD"), [L, R] = p(i.MONTHLY), [z, B] = p(!0), [V, H] = p(!0), [U, W] = p([]), [G, K] = p([]), [q, J] = p(null), [Y, X] = p(!1);
24
+ }, { planId: x } = m(), S = c(), C = s(), { plan: w, isLoading: T, error: E, refetch: D } = l(x), { quotas: O, isLoading: k } = d({ onlyActive: !0 }), { updatePlan: ee, isUpdating: A } = u(), [j, M] = p(""), [N, P] = p(""), [F, I] = p("USD"), [L, R] = p(i.MONTHLY), [z, B] = p(!0), [V, H] = p(!0), [U, W] = p([]), [G, K] = p([]), [q, J] = p(null), [Y, X] = p(!1);
25
25
  if (f(() => {
26
- if (C && (M(C.name ?? ""), P(String(C.price ?? "")), I(C.currency ?? "USD"), R(C.duration ?? i.MONTHLY), B(C.isActive ?? !0), H(C.isPurchasable ?? !0), C.features && C.features.length > 0)) {
27
- let e = [], t = /* @__PURE__ */ new Map();
28
- for (let e of C.features) if (e.quota?.id) {
29
- let n = e.quota.id, r = o(e) || 1;
30
- t.set(n, r);
26
+ if (w) {
27
+ if (M(w.name ?? ""), P(String(w.price ?? "")), I(w.currency ?? "USD"), R(w.duration ?? i.MONTHLY), B(w.isActive ?? !0), H(w.isPurchasable ?? !0), w.currencyPrices && typeof w.currencyPrices == "object") {
28
+ let e = w.currencyPrices;
29
+ K(Object.entries(e).filter(([, e]) => typeof e == "number").map(([e, t]) => ({
30
+ currency: e,
31
+ amount: t
32
+ })));
33
+ }
34
+ if (w.features && w.features.length > 0) {
35
+ let e = [], t = /* @__PURE__ */ new Map();
36
+ for (let e of w.features) if (e.quota?.id) {
37
+ let n = e.quota.id, r = o(e) || 1;
38
+ t.set(n, r);
39
+ }
40
+ for (let [n, r] of t) e.push({
41
+ quotaId: n,
42
+ quantity: r
43
+ });
44
+ W(e);
31
45
  }
32
- for (let [n, r] of t) e.push({
33
- quotaId: n,
34
- quantity: r
35
- });
36
- W(e);
37
46
  }
38
- }, [C]), !ee.canManagePlans) return /* @__PURE__ */ g(e, { message: b("billing.plans.noEditPermission", "You don't have permission to edit plans.") });
39
- if (w || O) return /* @__PURE__ */ _("div", {
47
+ }, [w]), !C.canManagePlans) return /* @__PURE__ */ g(e, { message: b("billing.plans.noEditPermission", "You don't have permission to edit plans.") });
48
+ if (T || k) return /* @__PURE__ */ _("div", {
40
49
  className: "space-y-6 p-6",
41
50
  children: [/* @__PURE__ */ g("div", { className: "h-8 w-48 bg-muted animate-pulse rounded" }), /* @__PURE__ */ g("div", {
42
51
  className: "max-w-2xl space-y-4",
@@ -49,16 +58,16 @@ var y = () => {
49
58
  ].map((e) => /* @__PURE__ */ g("div", { className: "h-16 bg-muted animate-pulse rounded" }, e))
50
59
  })]
51
60
  });
52
- if (T && a(T)) return /* @__PURE__ */ g("div", {
61
+ if (E && a(E)) return /* @__PURE__ */ g("div", {
53
62
  className: "p-6",
54
63
  children: /* @__PURE__ */ g(n, {
55
64
  title: b("billing.plans.serverUnavailable", "Server Unavailable"),
56
65
  message: b("billing.plans.serverUnavailableLoadPlan", "Unable to load plan. The server might be down or experiencing issues."),
57
- onRetry: () => E(),
66
+ onRetry: () => D(),
58
67
  showRetry: !0
59
68
  })
60
69
  });
61
- if (!C) return /* @__PURE__ */ g("div", {
70
+ if (!w) return /* @__PURE__ */ g("div", {
62
71
  className: "flex items-center justify-center h-full min-h-[400px]",
63
72
  children: /* @__PURE__ */ _("div", {
64
73
  className: "text-center space-y-4",
@@ -87,7 +96,7 @@ var y = () => {
87
96
  return /* @__PURE__ */ _("div", {
88
97
  className: "space-y-6 p-6",
89
98
  children: [/* @__PURE__ */ g(t, {
90
- title: `Edit Plan: ${C.name}`,
99
+ title: `Edit Plan: ${w.name}`,
91
100
  description: b("billing.plans.editPlanDesc", "Update the plan details and quota allocations")
92
101
  }), /* @__PURE__ */ _("form", {
93
102
  onSubmit: async (e) => {
@@ -122,7 +131,7 @@ var y = () => {
122
131
  ...G.length > 0 ? { currencyPriceOverrides: G } : {}
123
132
  };
124
133
  try {
125
- await k(t), X(!0), setTimeout(() => S(`/plans/${x}`), 1500);
134
+ await ee(t), X(!0), setTimeout(() => S(`/plans/${x}`), 1500);
126
135
  } catch (e) {
127
136
  J(e instanceof Error ? e.message : "Failed to update plan");
128
137
  }
@@ -299,7 +308,7 @@ var y = () => {
299
308
  className: "text-sm text-muted-foreground",
300
309
  children: b("billing.plans.noQuotasAllocated", "No quotas allocated. Click \"Add Quota\" to add one.")
301
310
  }) : U.map((e, t) => {
302
- let n = D.find((t) => t.id === e.quotaId), r = n?.limits, i = r?.value ?? 0, a = r?.type ?? "unknown", o = i * e.quantity;
311
+ let n = O.find((t) => t.id === e.quotaId), r = n?.limits, i = r?.value ?? 0, a = r?.type ?? "unknown", o = i * e.quantity;
303
312
  return /* @__PURE__ */ _("div", {
304
313
  className: "p-3 border border-border rounded-lg bg-muted/20 space-y-3",
305
314
  children: [/* @__PURE__ */ _("div", {
@@ -312,7 +321,7 @@ var y = () => {
312
321
  value: "",
313
322
  disabled: !0,
314
323
  children: b("billing.plans.selectQuota", "Select a quota...")
315
- }), D.map((e) => /* @__PURE__ */ g("option", {
324
+ }), O.map((e) => /* @__PURE__ */ g("option", {
316
325
  value: e.id,
317
326
  children: e.name
318
327
  }, e.id))]
@@ -1 +1 @@
1
- {"version":3,"file":"PlanEditPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanEditPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plan Edit Page\n * Form to edit an existing plan\n */\n\nimport { useState, useEffect, type FC, type FormEvent } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlan, usePlanMutations, useQuotas } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport {\n AccessDenied,\n CurrencyPriceOverridesEditor,\n PageHeader,\n ServerError,\n} from '../../../shared/components';\nimport { getPlanFeatureQuantity, isServerError } from '../../../shared/utils';\nimport {\n PlanDuration,\n type CurrencyPriceOverrideInput,\n type UpdatePlanInput,\n} from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface QuotaAllocation {\n quotaId: string;\n quantity: number;\n}\n\nexport const PlanEditPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { planId } = useParams<{ planId: string }>();\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const { plan, isLoading: isPlanLoading, error: planError, refetch } = usePlan(planId);\n const { quotas, isLoading: isQuotasLoading } = useQuotas({ onlyActive: true });\n const { updatePlan, isUpdating } = usePlanMutations();\n\n // Form state\n const [name, setName] = useState('');\n const [price, setPrice] = useState('');\n const [currency, setCurrency] = useState('USD');\n const [duration, setDuration] = useState<PlanDuration>(PlanDuration.MONTHLY);\n const [isActive, setIsActive] = useState(true);\n const [isPurchasable, setIsPurchasable] = useState(true);\n const [quotaAllocations, setQuotaAllocations] = useState<QuotaAllocation[]>([]);\n const [currencyPriceOverrides, setCurrencyPriceOverrides] = useState<\n CurrencyPriceOverrideInput[]\n >([]);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n // Populate form when plan data loads\n useEffect(() => {\n if (plan) {\n setName(plan.name ?? '');\n setPrice(String(plan.price ?? ''));\n setCurrency(plan.currency ?? 'USD');\n setDuration(plan.duration ?? PlanDuration.MONTHLY);\n setIsActive(plan.isActive ?? true);\n setIsPurchasable(plan.isPurchasable ?? true);\n // Extract quota allocations from features\n if (plan.features && plan.features.length > 0) {\n const allocations: QuotaAllocation[] = [];\n const quotaMap = new Map<string, number>();\n\n for (const feature of plan.features) {\n if (feature.quota?.id) {\n const quotaId = feature.quota.id;\n // SubscriptionFeature now stores per-plan override in context.quantity.\n // Fall back to quota.limits.value for legacy autoseeded plans.\n const value = getPlanFeatureQuantity(feature) || 1;\n quotaMap.set(quotaId, value);\n }\n }\n\n for (const [quotaId, quantity] of quotaMap) {\n allocations.push({ quotaId, quantity });\n }\n\n setQuotaAllocations(allocations);\n }\n }\n }, [plan]);\n\n // Permission check\n if (!permissions.canManagePlans) {\n return (\n <AccessDenied\n message={tr('billing.plans.noEditPermission', \"You don't have permission to edit plans.\")}\n />\n );\n }\n\n // Loading state\n if (isPlanLoading || isQuotasLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-muted animate-pulse rounded\" />\n <div className=\"max-w-2xl space-y-4\">\n {[1, 2, 3, 4, 5].map((i) => (\n <div key={i} className=\"h-16 bg-muted animate-pulse rounded\" />\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (planError && isServerError(planError)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.plans.serverUnavailable', 'Server Unavailable')}\n message={tr(\n 'billing.plans.serverUnavailableLoadPlan',\n 'Unable to load plan. The server might be down or experiencing issues.'\n )}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!plan) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.plans.planNotFound', 'Plan not found')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n );\n }\n\n const handleAddQuota = () => {\n // Add new quota at the top with empty quotaId (placeholder)\n setQuotaAllocations([{ quotaId: '', quantity: 1 }, ...quotaAllocations]);\n };\n\n const handleRemoveQuota = (index: number) => {\n setQuotaAllocations(quotaAllocations.filter((_, i) => i !== index));\n };\n\n const handleQuotaChange = (\n index: number,\n field: 'quotaId' | 'quantity',\n value: string | number\n ) => {\n const updated = [...quotaAllocations];\n if (field === 'quotaId') {\n updated[index].quotaId = value as string;\n } else {\n updated[index].quantity = Number(value);\n }\n setQuotaAllocations(updated);\n };\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setError(null);\n setSuccess(false);\n\n // Validation\n if (!name.trim()) {\n setError('Plan name is required');\n return;\n }\n if (!price || isNaN(Number(price)) || Number(price) < 0) {\n setError('Valid price is required');\n return;\n }\n if (quotaAllocations.length === 0) {\n setError('At least one quota is required');\n return;\n }\n // Check for unselected quotas\n const unselectedQuotas = quotaAllocations.filter((a) => a.quotaId === '');\n if (unselectedQuotas.length > 0) {\n setError('Please select a quota for all allocations');\n return;\n }\n\n const input: UpdatePlanInput = {\n id: planId!,\n name: name.trim(),\n price: Number(price),\n currency,\n duration,\n isActive,\n isPurchasable,\n quotas: quotaAllocations.map((a) => ({\n quotaId: a.quotaId,\n quantity: a.quantity,\n })),\n ...(currencyPriceOverrides.length > 0 ? { currencyPriceOverrides } : {}),\n };\n\n try {\n await updatePlan(input);\n setSuccess(true);\n // Navigate back after short delay\n setTimeout(() => navigateTo(`/plans/${planId}`), 1500);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to update plan');\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={`Edit Plan: ${plan.name}`}\n description={tr(\n 'billing.plans.editPlanDesc',\n 'Update the plan details and quota allocations'\n )}\n />\n\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Error/Success Messages */}\n {error && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n )}\n {success && (\n <div className=\"p-4 bg-status-success-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-success-text\">\n {tr('billing.plans.updateSuccess', 'Plan updated successfully! Redirecting...')}\n </p>\n </div>\n )}\n\n {/* Two-column layout on desktop */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n {/* Basic Info */}\n <div className=\"border border-border rounded-lg bg-card overflow-hidden h-fit\">\n <div className=\"p-4 border-b border-border bg-muted/30\">\n <h2 className=\"font-semibold text-foreground\">\n {tr('billing.plans.basicInfo', 'Basic Information')}\n </h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Name */}\n <div>\n <label htmlFor=\"name\" className=\"block text-sm font-medium text-foreground mb-1\">\n {tr('billing.plans.planNameLabel', 'Plan Name')} *\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary\"\n placeholder={tr('billing.plans.planNamePlaceholder', 'e.g., Pro Plan')}\n required\n />\n </div>\n\n {/* Price & Currency */}\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label htmlFor=\"price\" className=\"block text-sm font-medium text-foreground mb-1\">\n {tr('billing.plans.priceLabel', 'Price')} *\n </label>\n <input\n id=\"price\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n value={price}\n onChange={(e) => setPrice(e.target.value)}\n className=\"w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary\"\n placeholder=\"49.99\"\n required\n />\n </div>\n <div>\n <label\n htmlFor=\"currency\"\n className=\"block text-sm font-medium text-foreground mb-1\"\n >\n {tr('billing.plans.currencyLabel', 'Currency')} *\n </label>\n <select\n id=\"currency\"\n value={currency}\n onChange={(e) => setCurrency(e.target.value)}\n className=\"w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"USD\">USD ($)</option>\n <option value=\"EUR\">EUR</option>\n <option value=\"GBP\">GBP</option>\n <option value=\"INR\">INR</option>\n <option value=\"AUD\">AUD</option>\n <option value=\"CAD\">CAD</option>\n <option value=\"JPY\">JPY</option>\n <option value=\"SGD\">SGD</option>\n </select>\n </div>\n </div>\n\n {/* Duration */}\n <div>\n <label\n htmlFor=\"duration\"\n className=\"block text-sm font-medium text-foreground mb-1\"\n >\n {tr('billing.plans.billingPeriodLabel', 'Billing Period')} *\n </label>\n <select\n id=\"duration\"\n value={duration}\n onChange={(e) => setDuration(e.target.value as PlanDuration)}\n className=\"w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value={PlanDuration.MONTHLY}>Monthly</option>\n <option value={PlanDuration.YEARLY}>Yearly</option>\n </select>\n </div>\n\n {/* Status */}\n <div className=\"flex items-center gap-3\">\n <input\n id=\"isActive\"\n type=\"checkbox\"\n checked={isActive}\n onChange={(e) => setIsActive(e.target.checked)}\n className=\"size-4 rounded border-border text-primary focus:ring-primary\"\n />\n <label htmlFor=\"isActive\" className=\"text-sm font-medium text-foreground\">\n {tr('billing.plans.planIsActive', 'Plan is active')}\n </label>\n </div>\n\n {/* Purchasable toggle */}\n <div className=\"flex items-start justify-between gap-4 pt-1\">\n <div>\n <p className=\"text-sm font-medium text-foreground\">\n {tr('billing.plans.isPurchasableLabel', 'Publicly purchasable')}\n </p>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {isPurchasable\n ? tr(\n 'billing.plans.isPurchasableOn',\n 'Visible on pricing page — users can subscribe directly.'\n )\n : tr(\n 'billing.plans.isPurchasableOff',\n 'Internal / custom plan — hidden from pricing page, assign via API only.'\n )}\n </p>\n </div>\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={isPurchasable}\n onClick={() => setIsPurchasable((v) => !v)}\n className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 ${\n isPurchasable ? 'bg-primary' : 'bg-muted-foreground/30'\n }`}\n >\n <span\n className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${\n isPurchasable ? 'translate-x-5' : 'translate-x-0'\n }`}\n />\n </button>\n </div>\n </div>\n </div>\n\n {/* Quota Allocations */}\n <div className=\"border border-border rounded-lg bg-card overflow-hidden h-fit\">\n <div className=\"p-4 border-b border-border bg-muted/30 flex items-center justify-between\">\n <h2 className=\"font-semibold text-foreground\">\n {tr('billing.plans.quotaAllocations', 'Quota Allocations')}\n </h2>\n <button\n type=\"button\"\n onClick={handleAddQuota}\n className=\"px-3 py-1 text-sm font-medium text-primary hover:bg-primary/10 rounded-md transition-colors\"\n >\n + Add Quota\n </button>\n </div>\n <div className=\"p-4 space-y-3\">\n {quotaAllocations.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'billing.plans.noQuotasAllocated',\n 'No quotas allocated. Click \"Add Quota\" to add one.'\n )}\n </p>\n ) : (\n quotaAllocations.map((allocation, index) => {\n const selectedQuota = quotas.find((q) => q.id === allocation.quotaId);\n const limits = selectedQuota?.limits as { type?: string; value?: number } | null;\n const limitValue = limits?.value ?? 0;\n const limitType = limits?.type ?? 'unknown';\n const totalValue = limitValue * allocation.quantity;\n\n return (\n <div\n key={index}\n className=\"p-3 border border-border rounded-lg bg-muted/20 space-y-3\"\n >\n <div className=\"flex items-center gap-3\">\n <select\n value={allocation.quotaId}\n onChange={(e) => handleQuotaChange(index, 'quotaId', e.target.value)}\n className={`flex-1 px-3 py-2 border border-border rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-primary ${\n allocation.quotaId === '' ? 'text-muted-foreground' : 'text-foreground'\n }`}\n >\n <option value=\"\" disabled>\n {tr('billing.plans.selectQuota', 'Select a quota...')}\n </option>\n {quotas.map((quota) => (\n <option key={quota.id} value={quota.id}>\n {quota.name}\n </option>\n ))}\n </select>\n <button\n type=\"button\"\n onClick={() => handleRemoveQuota(index)}\n className=\"p-2 text-status-error-text hover:bg-status-error-bg-subtle rounded-md transition-colors\"\n title=\"Remove quota\"\n >\n <svg\n className=\"size-5\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\"\n />\n </svg>\n </button>\n </div>\n\n {/* Quota Info - only show when quota is selected */}\n {selectedQuota && (\n <>\n <div className=\"flex items-center justify-between text-sm\">\n <div className=\"text-muted-foreground\">\n <span className=\"capitalize\">Type: {limitType}</span>\n <span className=\"mx-2\">•</span>\n <span>\n Value per unit:{' '}\n <span className=\"font-medium text-foreground\">\n {limitValue.toLocaleString()}\n </span>\n </span>\n </div>\n </div>\n\n {/* Quantity & Total */}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex items-center gap-2\">\n <label className=\"text-sm text-muted-foreground\">\n {tr('billing.plans.quantity', 'Quantity')}:\n </label>\n <input\n type=\"number\"\n min=\"1\"\n value={allocation.quantity}\n onChange={(e) =>\n handleQuotaChange(index, 'quantity', e.target.value)\n }\n className=\"w-24 px-3 py-1.5 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary text-center\"\n />\n </div>\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">=</span>\n <span className=\"font-semibold text-status-success-text\">\n {totalValue.toLocaleString()} total\n </span>\n </div>\n </div>\n </>\n )}\n </div>\n );\n })\n )}\n </div>\n </div>\n </div>\n\n <CurrencyPriceOverridesEditor\n overrides={currencyPriceOverrides}\n onChange={setCurrencyPriceOverrides}\n />\n\n {/* Actions */}\n <div className=\"flex items-center gap-3 pt-4\">\n <button\n type=\"submit\"\n disabled={isUpdating}\n className=\"px-6 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors disabled:opacity-50\"\n >\n {isUpdating\n ? tr('billing.plans.saving', 'Saving...')\n : tr('billing.plans.saveChanges', 'Save Changes')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/plans/${planId}`)}\n className=\"px-6 py-2 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n Cancel\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"PlanEditPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanEditPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plan Edit Page\n * Form to edit an existing plan\n */\n\nimport { useState, useEffect, type FC, type FormEvent } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlan, usePlanMutations, useQuotas } from '../hooks';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport {\n AccessDenied,\n CurrencyPriceOverridesEditor,\n PageHeader,\n ServerError,\n} from '../../../shared/components';\nimport { getPlanFeatureQuantity, isServerError } from '../../../shared/utils';\nimport {\n PlanDuration,\n type CurrencyPriceOverrideInput,\n type UpdatePlanInput,\n} from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface QuotaAllocation {\n quotaId: string;\n quantity: number;\n}\n\nexport const PlanEditPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { planId } = useParams<{ planId: string }>();\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const { plan, isLoading: isPlanLoading, error: planError, refetch } = usePlan(planId);\n const { quotas, isLoading: isQuotasLoading } = useQuotas({ onlyActive: true });\n const { updatePlan, isUpdating } = usePlanMutations();\n\n // Form state\n const [name, setName] = useState('');\n const [price, setPrice] = useState('');\n const [currency, setCurrency] = useState('USD');\n const [duration, setDuration] = useState<PlanDuration>(PlanDuration.MONTHLY);\n const [isActive, setIsActive] = useState(true);\n const [isPurchasable, setIsPurchasable] = useState(true);\n const [quotaAllocations, setQuotaAllocations] = useState<QuotaAllocation[]>([]);\n const [currencyPriceOverrides, setCurrencyPriceOverrides] = useState<\n CurrencyPriceOverrideInput[]\n >([]);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n // Populate form when plan data loads\n useEffect(() => {\n if (plan) {\n setName(plan.name ?? '');\n setPrice(String(plan.price ?? ''));\n setCurrency(plan.currency ?? 'USD');\n setDuration(plan.duration ?? PlanDuration.MONTHLY);\n setIsActive(plan.isActive ?? true);\n setIsPurchasable(plan.isPurchasable ?? true);\n // Pre-populate currency price overrides so existing prices are preserved on save\n if (plan.currencyPrices && typeof plan.currencyPrices === 'object') {\n const existing = plan.currencyPrices as Record<string, number>;\n setCurrencyPriceOverrides(\n Object.entries(existing)\n .filter(([, v]) => typeof v === 'number')\n .map(([currency, amount]) => ({ currency, amount }))\n );\n }\n // Extract quota allocations from features\n if (plan.features && plan.features.length > 0) {\n const allocations: QuotaAllocation[] = [];\n const quotaMap = new Map<string, number>();\n\n for (const feature of plan.features) {\n if (feature.quota?.id) {\n const quotaId = feature.quota.id;\n // SubscriptionFeature now stores per-plan override in context.quantity.\n // Fall back to quota.limits.value for legacy autoseeded plans.\n const value = getPlanFeatureQuantity(feature) || 1;\n quotaMap.set(quotaId, value);\n }\n }\n\n for (const [quotaId, quantity] of quotaMap) {\n allocations.push({ quotaId, quantity });\n }\n\n setQuotaAllocations(allocations);\n }\n }\n }, [plan]);\n\n // Permission check\n if (!permissions.canManagePlans) {\n return (\n <AccessDenied\n message={tr('billing.plans.noEditPermission', \"You don't have permission to edit plans.\")}\n />\n );\n }\n\n // Loading state\n if (isPlanLoading || isQuotasLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-muted animate-pulse rounded\" />\n <div className=\"max-w-2xl space-y-4\">\n {[1, 2, 3, 4, 5].map((i) => (\n <div key={i} className=\"h-16 bg-muted animate-pulse rounded\" />\n ))}\n </div>\n </div>\n );\n }\n\n // Error state\n if (planError && isServerError(planError)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.plans.serverUnavailable', 'Server Unavailable')}\n message={tr(\n 'billing.plans.serverUnavailableLoadPlan',\n 'Unable to load plan. The server might be down or experiencing issues.'\n )}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!plan) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('billing.plans.planNotFound', 'Plan not found')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n );\n }\n\n const handleAddQuota = () => {\n // Add new quota at the top with empty quotaId (placeholder)\n setQuotaAllocations([{ quotaId: '', quantity: 1 }, ...quotaAllocations]);\n };\n\n const handleRemoveQuota = (index: number) => {\n setQuotaAllocations(quotaAllocations.filter((_, i) => i !== index));\n };\n\n const handleQuotaChange = (\n index: number,\n field: 'quotaId' | 'quantity',\n value: string | number\n ) => {\n const updated = [...quotaAllocations];\n if (field === 'quotaId') {\n updated[index].quotaId = value as string;\n } else {\n updated[index].quantity = Number(value);\n }\n setQuotaAllocations(updated);\n };\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setError(null);\n setSuccess(false);\n\n // Validation\n if (!name.trim()) {\n setError('Plan name is required');\n return;\n }\n if (!price || isNaN(Number(price)) || Number(price) < 0) {\n setError('Valid price is required');\n return;\n }\n if (quotaAllocations.length === 0) {\n setError('At least one quota is required');\n return;\n }\n // Check for unselected quotas\n const unselectedQuotas = quotaAllocations.filter((a) => a.quotaId === '');\n if (unselectedQuotas.length > 0) {\n setError('Please select a quota for all allocations');\n return;\n }\n\n const input: UpdatePlanInput = {\n id: planId!,\n name: name.trim(),\n price: Number(price),\n currency,\n duration,\n isActive,\n isPurchasable,\n quotas: quotaAllocations.map((a) => ({\n quotaId: a.quotaId,\n quantity: a.quantity,\n })),\n ...(currencyPriceOverrides.length > 0 ? { currencyPriceOverrides } : {}),\n };\n\n try {\n await updatePlan(input);\n setSuccess(true);\n // Navigate back after short delay\n setTimeout(() => navigateTo(`/plans/${planId}`), 1500);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to update plan');\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={`Edit Plan: ${plan.name}`}\n description={tr(\n 'billing.plans.editPlanDesc',\n 'Update the plan details and quota allocations'\n )}\n />\n\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Error/Success Messages */}\n {error && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n )}\n {success && (\n <div className=\"p-4 bg-status-success-bg-subtle border border-border-subtle rounded-lg\">\n <p className=\"text-sm text-status-success-text\">\n {tr('billing.plans.updateSuccess', 'Plan updated successfully! Redirecting...')}\n </p>\n </div>\n )}\n\n {/* Two-column layout on desktop */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n {/* Basic Info */}\n <div className=\"border border-border rounded-lg bg-card overflow-hidden h-fit\">\n <div className=\"p-4 border-b border-border bg-muted/30\">\n <h2 className=\"font-semibold text-foreground\">\n {tr('billing.plans.basicInfo', 'Basic Information')}\n </h2>\n </div>\n <div className=\"p-4 space-y-4\">\n {/* Name */}\n <div>\n <label htmlFor=\"name\" className=\"block text-sm font-medium text-foreground mb-1\">\n {tr('billing.plans.planNameLabel', 'Plan Name')} *\n </label>\n <input\n id=\"name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary\"\n placeholder={tr('billing.plans.planNamePlaceholder', 'e.g., Pro Plan')}\n required\n />\n </div>\n\n {/* Price & Currency */}\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label htmlFor=\"price\" className=\"block text-sm font-medium text-foreground mb-1\">\n {tr('billing.plans.priceLabel', 'Price')} *\n </label>\n <input\n id=\"price\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n value={price}\n onChange={(e) => setPrice(e.target.value)}\n className=\"w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary\"\n placeholder=\"49.99\"\n required\n />\n </div>\n <div>\n <label\n htmlFor=\"currency\"\n className=\"block text-sm font-medium text-foreground mb-1\"\n >\n {tr('billing.plans.currencyLabel', 'Currency')} *\n </label>\n <select\n id=\"currency\"\n value={currency}\n onChange={(e) => setCurrency(e.target.value)}\n className=\"w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"USD\">USD ($)</option>\n <option value=\"EUR\">EUR</option>\n <option value=\"GBP\">GBP</option>\n <option value=\"INR\">INR</option>\n <option value=\"AUD\">AUD</option>\n <option value=\"CAD\">CAD</option>\n <option value=\"JPY\">JPY</option>\n <option value=\"SGD\">SGD</option>\n </select>\n </div>\n </div>\n\n {/* Duration */}\n <div>\n <label\n htmlFor=\"duration\"\n className=\"block text-sm font-medium text-foreground mb-1\"\n >\n {tr('billing.plans.billingPeriodLabel', 'Billing Period')} *\n </label>\n <select\n id=\"duration\"\n value={duration}\n onChange={(e) => setDuration(e.target.value as PlanDuration)}\n className=\"w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value={PlanDuration.MONTHLY}>Monthly</option>\n <option value={PlanDuration.YEARLY}>Yearly</option>\n </select>\n </div>\n\n {/* Status */}\n <div className=\"flex items-center gap-3\">\n <input\n id=\"isActive\"\n type=\"checkbox\"\n checked={isActive}\n onChange={(e) => setIsActive(e.target.checked)}\n className=\"size-4 rounded border-border text-primary focus:ring-primary\"\n />\n <label htmlFor=\"isActive\" className=\"text-sm font-medium text-foreground\">\n {tr('billing.plans.planIsActive', 'Plan is active')}\n </label>\n </div>\n\n {/* Purchasable toggle */}\n <div className=\"flex items-start justify-between gap-4 pt-1\">\n <div>\n <p className=\"text-sm font-medium text-foreground\">\n {tr('billing.plans.isPurchasableLabel', 'Publicly purchasable')}\n </p>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {isPurchasable\n ? tr(\n 'billing.plans.isPurchasableOn',\n 'Visible on pricing page — users can subscribe directly.'\n )\n : tr(\n 'billing.plans.isPurchasableOff',\n 'Internal / custom plan — hidden from pricing page, assign via API only.'\n )}\n </p>\n </div>\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={isPurchasable}\n onClick={() => setIsPurchasable((v) => !v)}\n className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 ${\n isPurchasable ? 'bg-primary' : 'bg-muted-foreground/30'\n }`}\n >\n <span\n className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${\n isPurchasable ? 'translate-x-5' : 'translate-x-0'\n }`}\n />\n </button>\n </div>\n </div>\n </div>\n\n {/* Quota Allocations */}\n <div className=\"border border-border rounded-lg bg-card overflow-hidden h-fit\">\n <div className=\"p-4 border-b border-border bg-muted/30 flex items-center justify-between\">\n <h2 className=\"font-semibold text-foreground\">\n {tr('billing.plans.quotaAllocations', 'Quota Allocations')}\n </h2>\n <button\n type=\"button\"\n onClick={handleAddQuota}\n className=\"px-3 py-1 text-sm font-medium text-primary hover:bg-primary/10 rounded-md transition-colors\"\n >\n + Add Quota\n </button>\n </div>\n <div className=\"p-4 space-y-3\">\n {quotaAllocations.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'billing.plans.noQuotasAllocated',\n 'No quotas allocated. Click \"Add Quota\" to add one.'\n )}\n </p>\n ) : (\n quotaAllocations.map((allocation, index) => {\n const selectedQuota = quotas.find((q) => q.id === allocation.quotaId);\n const limits = selectedQuota?.limits as { type?: string; value?: number } | null;\n const limitValue = limits?.value ?? 0;\n const limitType = limits?.type ?? 'unknown';\n const totalValue = limitValue * allocation.quantity;\n\n return (\n <div\n key={index}\n className=\"p-3 border border-border rounded-lg bg-muted/20 space-y-3\"\n >\n <div className=\"flex items-center gap-3\">\n <select\n value={allocation.quotaId}\n onChange={(e) => handleQuotaChange(index, 'quotaId', e.target.value)}\n className={`flex-1 px-3 py-2 border border-border rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-primary ${\n allocation.quotaId === '' ? 'text-muted-foreground' : 'text-foreground'\n }`}\n >\n <option value=\"\" disabled>\n {tr('billing.plans.selectQuota', 'Select a quota...')}\n </option>\n {quotas.map((quota) => (\n <option key={quota.id} value={quota.id}>\n {quota.name}\n </option>\n ))}\n </select>\n <button\n type=\"button\"\n onClick={() => handleRemoveQuota(index)}\n className=\"p-2 text-status-error-text hover:bg-status-error-bg-subtle rounded-md transition-colors\"\n title=\"Remove quota\"\n >\n <svg\n className=\"size-5\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\"\n />\n </svg>\n </button>\n </div>\n\n {/* Quota Info - only show when quota is selected */}\n {selectedQuota && (\n <>\n <div className=\"flex items-center justify-between text-sm\">\n <div className=\"text-muted-foreground\">\n <span className=\"capitalize\">Type: {limitType}</span>\n <span className=\"mx-2\">•</span>\n <span>\n Value per unit:{' '}\n <span className=\"font-medium text-foreground\">\n {limitValue.toLocaleString()}\n </span>\n </span>\n </div>\n </div>\n\n {/* Quantity & Total */}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex items-center gap-2\">\n <label className=\"text-sm text-muted-foreground\">\n {tr('billing.plans.quantity', 'Quantity')}:\n </label>\n <input\n type=\"number\"\n min=\"1\"\n value={allocation.quantity}\n onChange={(e) =>\n handleQuotaChange(index, 'quantity', e.target.value)\n }\n className=\"w-24 px-3 py-1.5 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary text-center\"\n />\n </div>\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">=</span>\n <span className=\"font-semibold text-status-success-text\">\n {totalValue.toLocaleString()} total\n </span>\n </div>\n </div>\n </>\n )}\n </div>\n );\n })\n )}\n </div>\n </div>\n </div>\n\n <CurrencyPriceOverridesEditor\n overrides={currencyPriceOverrides}\n onChange={setCurrencyPriceOverrides}\n />\n\n {/* Actions */}\n <div className=\"flex items-center gap-3 pt-4\">\n <button\n type=\"submit\"\n disabled={isUpdating}\n className=\"px-6 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors disabled:opacity-50\"\n >\n {isUpdating\n ? tr('billing.plans.saving', 'Saving...')\n : tr('billing.plans.saveChanges', 'Save Changes')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/plans/${planId}`)}\n className=\"px-6 py-2 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n Cancel\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { PromotionsListPage as e } from "./pages/PromotionsListPage.js";
2
+ import { CreatePromotionPage as t } from "./pages/CreatePromotionPage.js";
3
+ import "./pages/index.js";
4
+ export { t as CreatePromotionPage, e as PromotionsListPage };
@@ -0,0 +1,289 @@
1
+ import { AccessDenied as e } from "../../../shared/components/AccessDenied.js";
2
+ import { PageHeader as t } from "../../../shared/components/PageHeader.js";
3
+ import "../../../shared/components/index.js";
4
+ import { useBillingPermissions as n } from "../../../hooks/useBillingPermissions.js";
5
+ import { useCreatePromotionMutation as r } from "../../../../generated/global-operations.js";
6
+ import { useBillingNavigate as i } from "../../../hooks/useBillingNavigate.js";
7
+ import { useState as a } from "react";
8
+ import { ArrowLeft as o, Gift as s, Info as c, Save as l } from "lucide-react";
9
+ import { jsx as u, jsxs as d } from "react/jsx-runtime";
10
+ //#region src/billing/modules/promotions/pages/CreatePromotionPage.tsx
11
+ var f = [
12
+ "vibecontrols",
13
+ "workspaces",
14
+ "botlit",
15
+ "fluidgrids",
16
+ "healthybowl"
17
+ ], p = () => {
18
+ let p = i(), m = n(), [h] = r(), [g, _] = a(!1), [v, y] = a({}), b = (/* @__PURE__ */ new Date()).toISOString().split("T")[0], [x, S] = a({
19
+ name: "",
20
+ description: "",
21
+ startsAt: b,
22
+ expiresAt: "",
23
+ bonusMonths: 1,
24
+ maxRedemptions: "",
25
+ applicableProductIds: [],
26
+ applicablePlanIds: ""
27
+ });
28
+ if (!m.canManagePlans) return /* @__PURE__ */ u(e, { message: "You don't have permission to create promotions." });
29
+ let C = (e, t) => S((n) => ({
30
+ ...n,
31
+ [e]: t
32
+ })), w = (e) => {
33
+ C("applicableProductIds", x.applicableProductIds.includes(e) ? x.applicableProductIds.filter((t) => t !== e) : [...x.applicableProductIds, e]);
34
+ }, T = () => {
35
+ let e = {};
36
+ return x.name.trim() || (e.name = "Name is required"), x.startsAt || (e.startsAt = "Start date is required"), x.expiresAt || (e.expiresAt = "End date is required"), x.expiresAt && x.expiresAt <= x.startsAt && (e.expiresAt = "End date must be after start date"), x.bonusMonths < 1 && (e.bonusMonths = "Must be at least 1"), y(e), Object.keys(e).length === 0;
37
+ };
38
+ return /* @__PURE__ */ d("div", {
39
+ className: "max-w-2xl mx-auto p-4 sm:p-6 space-y-6",
40
+ children: [/* @__PURE__ */ d("div", {
41
+ className: "flex items-center gap-4",
42
+ children: [/* @__PURE__ */ u("button", {
43
+ type: "button",
44
+ onClick: () => p("/promotions"),
45
+ className: "p-2 rounded-lg hover:bg-muted text-muted-foreground",
46
+ children: /* @__PURE__ */ u(o, { className: "size-5" })
47
+ }), /* @__PURE__ */ u(t, {
48
+ title: "Create Promotion",
49
+ description: "Offer bonus free renewal months to subscribers who sign up within the promotion window",
50
+ icon: /* @__PURE__ */ u(s, { className: "size-6" })
51
+ })]
52
+ }), /* @__PURE__ */ d("form", {
53
+ onSubmit: async (e) => {
54
+ if (e.preventDefault(), T()) {
55
+ _(!0);
56
+ try {
57
+ let e = x.applicablePlanIds.split(",").map((e) => e.trim()).filter(Boolean);
58
+ await h({ variables: { input: {
59
+ name: x.name.trim(),
60
+ description: x.description.trim() || void 0,
61
+ startsAt: new Date(x.startsAt).toISOString(),
62
+ expiresAt: new Date(x.expiresAt).toISOString(),
63
+ bonusMonths: x.bonusMonths,
64
+ maxRedemptions: x.maxRedemptions ? parseInt(x.maxRedemptions) : void 0,
65
+ applicableProductIds: x.applicableProductIds,
66
+ applicablePlanIds: e
67
+ } } }), p("/promotions");
68
+ } catch (e) {
69
+ y({ name: e instanceof Error ? e.message : "Failed to create promotion" });
70
+ } finally {
71
+ _(!1);
72
+ }
73
+ }
74
+ },
75
+ className: "space-y-5",
76
+ children: [
77
+ /* @__PURE__ */ d("div", {
78
+ className: "bg-card border border-border rounded-xl p-5 space-y-4",
79
+ children: [
80
+ /* @__PURE__ */ u("h3", {
81
+ className: "font-semibold text-foreground",
82
+ children: "Promotion Details"
83
+ }),
84
+ /* @__PURE__ */ d("div", { children: [
85
+ /* @__PURE__ */ u("label", {
86
+ className: "block text-sm font-medium text-foreground mb-1",
87
+ children: "Name *"
88
+ }),
89
+ /* @__PURE__ */ u("input", {
90
+ type: "text",
91
+ value: x.name,
92
+ onChange: (e) => C("name", e.target.value),
93
+ placeholder: "e.g. June Launch Offer",
94
+ className: `w-full px-3 py-2 rounded-lg border ${v.name ? "border-destructive" : "border-border"} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`
95
+ }),
96
+ v.name && /* @__PURE__ */ u("p", {
97
+ className: "text-xs text-destructive mt-1",
98
+ children: v.name
99
+ })
100
+ ] }),
101
+ /* @__PURE__ */ d("div", { children: [/* @__PURE__ */ u("label", {
102
+ className: "block text-sm font-medium text-foreground mb-1",
103
+ children: "Description"
104
+ }), /* @__PURE__ */ u("textarea", {
105
+ value: x.description,
106
+ onChange: (e) => C("description", e.target.value),
107
+ placeholder: "Subscribe in June and get 2 extra months free!",
108
+ rows: 2,
109
+ className: "w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
110
+ })] })
111
+ ]
112
+ }),
113
+ /* @__PURE__ */ d("div", {
114
+ className: "bg-card border border-border rounded-xl p-5 space-y-4",
115
+ children: [
116
+ /* @__PURE__ */ u("h3", {
117
+ className: "font-semibold text-foreground",
118
+ children: "Bonus Configuration"
119
+ }),
120
+ /* @__PURE__ */ d("div", { children: [
121
+ /* @__PURE__ */ u("label", {
122
+ className: "block text-sm font-medium text-foreground mb-1",
123
+ children: "Bonus Months *"
124
+ }),
125
+ /* @__PURE__ */ u("input", {
126
+ type: "number",
127
+ value: x.bonusMonths,
128
+ min: 1,
129
+ max: 12,
130
+ onChange: (e) => C("bonusMonths", parseInt(e.target.value) || 1),
131
+ className: `w-32 px-3 py-2 rounded-lg border ${v.bonusMonths ? "border-destructive" : "border-border"} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`
132
+ }),
133
+ v.bonusMonths && /* @__PURE__ */ u("p", {
134
+ className: "text-xs text-destructive mt-1",
135
+ children: v.bonusMonths
136
+ }),
137
+ /* @__PURE__ */ d("div", {
138
+ className: "mt-2 flex items-start gap-2 p-3 bg-primary/5 rounded-lg text-sm text-muted-foreground",
139
+ children: [/* @__PURE__ */ u(c, { className: "size-4 shrink-0 mt-0.5 text-primary" }), /* @__PURE__ */ d("span", { children: [
140
+ "Users who subscribe during this window get",
141
+ " ",
142
+ /* @__PURE__ */ d("strong", {
143
+ className: "text-foreground",
144
+ children: [
145
+ x.bonusMonths,
146
+ " free renewal",
147
+ x.bonusMonths === 1 ? "" : "s"
148
+ ]
149
+ }),
150
+ " ",
151
+ "after their first paid period — their quota resets each month as normal."
152
+ ] })]
153
+ })
154
+ ] }),
155
+ /* @__PURE__ */ d("div", { children: [
156
+ /* @__PURE__ */ u("label", {
157
+ className: "block text-sm font-medium text-foreground mb-1",
158
+ children: "Max Redemptions"
159
+ }),
160
+ /* @__PURE__ */ u("input", {
161
+ type: "number",
162
+ value: x.maxRedemptions,
163
+ min: 1,
164
+ onChange: (e) => C("maxRedemptions", e.target.value),
165
+ placeholder: "Unlimited",
166
+ className: "w-48 px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
167
+ }),
168
+ /* @__PURE__ */ u("p", {
169
+ className: "text-xs text-muted-foreground mt-1",
170
+ children: "Leave empty for no cap"
171
+ })
172
+ ] })
173
+ ]
174
+ }),
175
+ /* @__PURE__ */ d("div", {
176
+ className: "bg-card border border-border rounded-xl p-5 space-y-4",
177
+ children: [
178
+ /* @__PURE__ */ u("h3", {
179
+ className: "font-semibold text-foreground",
180
+ children: "Active Window"
181
+ }),
182
+ /* @__PURE__ */ d("div", {
183
+ className: "grid grid-cols-2 gap-4",
184
+ children: [/* @__PURE__ */ d("div", { children: [
185
+ /* @__PURE__ */ u("label", {
186
+ className: "block text-sm font-medium text-foreground mb-1",
187
+ children: "Start Date *"
188
+ }),
189
+ /* @__PURE__ */ u("input", {
190
+ type: "date",
191
+ value: x.startsAt,
192
+ onChange: (e) => C("startsAt", e.target.value),
193
+ className: `w-full px-3 py-2 rounded-lg border ${v.startsAt ? "border-destructive" : "border-border"} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`
194
+ }),
195
+ v.startsAt && /* @__PURE__ */ u("p", {
196
+ className: "text-xs text-destructive mt-1",
197
+ children: v.startsAt
198
+ })
199
+ ] }), /* @__PURE__ */ d("div", { children: [
200
+ /* @__PURE__ */ u("label", {
201
+ className: "block text-sm font-medium text-foreground mb-1",
202
+ children: "End Date *"
203
+ }),
204
+ /* @__PURE__ */ u("input", {
205
+ type: "date",
206
+ value: x.expiresAt,
207
+ onChange: (e) => C("expiresAt", e.target.value),
208
+ min: x.startsAt,
209
+ className: `w-full px-3 py-2 rounded-lg border ${v.expiresAt ? "border-destructive" : "border-border"} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`
210
+ }),
211
+ v.expiresAt && /* @__PURE__ */ u("p", {
212
+ className: "text-xs text-destructive mt-1",
213
+ children: v.expiresAt
214
+ })
215
+ ] })]
216
+ }),
217
+ /* @__PURE__ */ u("p", {
218
+ className: "text-xs text-muted-foreground",
219
+ children: "Only purchases made between these dates will receive the bonus months"
220
+ })
221
+ ]
222
+ }),
223
+ /* @__PURE__ */ d("div", {
224
+ className: "bg-card border border-border rounded-xl p-5 space-y-4",
225
+ children: [
226
+ /* @__PURE__ */ u("h3", {
227
+ className: "font-semibold text-foreground",
228
+ children: "Scope"
229
+ }),
230
+ /* @__PURE__ */ d("div", { children: [
231
+ /* @__PURE__ */ u("label", {
232
+ className: "block text-sm font-medium text-foreground mb-2",
233
+ children: "Applicable Products"
234
+ }),
235
+ /* @__PURE__ */ u("div", {
236
+ className: "flex flex-wrap gap-2",
237
+ children: f.map((e) => /* @__PURE__ */ u("button", {
238
+ type: "button",
239
+ onClick: () => w(e),
240
+ className: `px-3 py-1.5 rounded-lg border text-sm transition-colors ${x.applicableProductIds.includes(e) ? "border-primary bg-primary/10 text-primary font-medium" : "border-border hover:border-primary/50"}`,
241
+ children: e
242
+ }, e))
243
+ }),
244
+ /* @__PURE__ */ u("p", {
245
+ className: "text-xs text-muted-foreground mt-1",
246
+ children: x.applicableProductIds.length === 0 ? "No selection = applies to all products" : `Applies to: ${x.applicableProductIds.join(", ")}`
247
+ })
248
+ ] }),
249
+ /* @__PURE__ */ d("div", { children: [
250
+ /* @__PURE__ */ u("label", {
251
+ className: "block text-sm font-medium text-foreground mb-1",
252
+ children: "Plan IDs (optional)"
253
+ }),
254
+ /* @__PURE__ */ u("input", {
255
+ type: "text",
256
+ value: x.applicablePlanIds,
257
+ onChange: (e) => C("applicablePlanIds", e.target.value),
258
+ placeholder: "plan-vibecontrols-pro-monthly, plan-vibecontrols-pro-yearly",
259
+ className: "w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
260
+ }),
261
+ /* @__PURE__ */ u("p", {
262
+ className: "text-xs text-muted-foreground mt-1",
263
+ children: "Comma-separated. Leave empty to apply to all plans"
264
+ })
265
+ ] })
266
+ ]
267
+ }),
268
+ /* @__PURE__ */ d("div", {
269
+ className: "flex gap-3 justify-end",
270
+ children: [/* @__PURE__ */ u("button", {
271
+ type: "button",
272
+ onClick: () => p("/promotions"),
273
+ className: "px-5 py-2 rounded-lg border border-border text-foreground hover:bg-muted text-sm",
274
+ children: "Cancel"
275
+ }), /* @__PURE__ */ d("button", {
276
+ type: "submit",
277
+ disabled: g,
278
+ className: "px-5 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 text-sm disabled:opacity-50",
279
+ children: [g ? /* @__PURE__ */ u("div", { className: "size-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin" }) : /* @__PURE__ */ u(l, { className: "size-4" }), "Create Promotion"]
280
+ })]
281
+ })
282
+ ]
283
+ })]
284
+ });
285
+ };
286
+ //#endregion
287
+ export { p as CreatePromotionPage };
288
+
289
+ //# sourceMappingURL=CreatePromotionPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CreatePromotionPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/CreatePromotionPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { ArrowLeft, Save, Gift, Info } from 'lucide-react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useCreatePromotionMutation } from '../../../../generated/global-operations';\n\nconst KNOWN_PRODUCTS = ['vibecontrols', 'workspaces', 'botlit', 'fluidgrids', 'healthybowl'];\n\ninterface FormData {\n name: string;\n description: string;\n startsAt: string;\n expiresAt: string;\n bonusMonths: number;\n maxRedemptions: string;\n applicableProductIds: string[];\n applicablePlanIds: string;\n}\n\nexport const CreatePromotionPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [createPromotion] = useCreatePromotionMutation();\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});\n\n const today = new Date().toISOString().split('T')[0];\n const [form, setForm] = useState<FormData>({\n name: '',\n description: '',\n startsAt: today,\n expiresAt: '',\n bonusMonths: 1,\n maxRedemptions: '',\n applicableProductIds: [],\n applicablePlanIds: '',\n });\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to create promotions.\" />;\n }\n\n const set = <K extends keyof FormData>(k: K, v: FormData[K]) =>\n setForm((prev) => ({ ...prev, [k]: v }));\n\n const toggleProduct = (pid: string) => {\n set(\n 'applicableProductIds',\n form.applicableProductIds.includes(pid)\n ? form.applicableProductIds.filter((p) => p !== pid)\n : [...form.applicableProductIds, pid]\n );\n };\n\n const validate = () => {\n const e: typeof errors = {};\n if (!form.name.trim()) e.name = 'Name is required';\n if (!form.startsAt) e.startsAt = 'Start date is required';\n if (!form.expiresAt) e.expiresAt = 'End date is required';\n if (form.expiresAt && form.expiresAt <= form.startsAt)\n e.expiresAt = 'End date must be after start date';\n if (form.bonusMonths < 1) e.bonusMonths = 'Must be at least 1';\n setErrors(e);\n return Object.keys(e).length === 0;\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n setIsSubmitting(true);\n try {\n const planIds = form.applicablePlanIds\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n await createPromotion({\n variables: {\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n startsAt: new Date(form.startsAt).toISOString(),\n expiresAt: new Date(form.expiresAt).toISOString(),\n bonusMonths: form.bonusMonths,\n maxRedemptions: form.maxRedemptions ? parseInt(form.maxRedemptions) : undefined,\n applicableProductIds: form.applicableProductIds,\n applicablePlanIds: planIds,\n },\n },\n });\n navigateTo('/promotions');\n } catch (err) {\n setErrors({ name: err instanceof Error ? err.message : 'Failed to create promotion' });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div className=\"max-w-2xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/promotions')}\n className=\"p-2 rounded-lg hover:bg-muted text-muted-foreground\"\n >\n <ArrowLeft className=\"size-5\" />\n </button>\n <PageHeader\n title=\"Create Promotion\"\n description=\"Offer bonus free renewal months to subscribers who sign up within the promotion window\"\n icon={<Gift className=\"size-6\" />}\n />\n </div>\n\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Basic info */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Promotion Details</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Name *</label>\n <input\n type=\"text\"\n value={form.name}\n onChange={(e) => set('name', e.target.value)}\n placeholder=\"e.g. June Launch Offer\"\n className={`w-full px-3 py-2 rounded-lg border ${errors.name ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.name && <p className=\"text-xs text-destructive mt-1\">{errors.name}</p>}\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Description</label>\n <textarea\n value={form.description}\n onChange={(e) => set('description', e.target.value)}\n placeholder=\"Subscribe in June and get 2 extra months free!\"\n rows={2}\n className=\"w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n </div>\n </div>\n\n {/* Bonus config */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Bonus Configuration</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Bonus Months *</label>\n <input\n type=\"number\"\n value={form.bonusMonths}\n min={1}\n max={12}\n onChange={(e) => set('bonusMonths', parseInt(e.target.value) || 1)}\n className={`w-32 px-3 py-2 rounded-lg border ${errors.bonusMonths ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.bonusMonths && (\n <p className=\"text-xs text-destructive mt-1\">{errors.bonusMonths}</p>\n )}\n <div className=\"mt-2 flex items-start gap-2 p-3 bg-primary/5 rounded-lg text-sm text-muted-foreground\">\n <Info className=\"size-4 shrink-0 mt-0.5 text-primary\" />\n <span>\n Users who subscribe during this window get{' '}\n <strong className=\"text-foreground\">\n {form.bonusMonths} free renewal{form.bonusMonths !== 1 ? 's' : ''}\n </strong>{' '}\n after their first paid period — their quota resets each month as normal.\n </span>\n </div>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Max Redemptions\n </label>\n <input\n type=\"number\"\n value={form.maxRedemptions}\n min={1}\n onChange={(e) => set('maxRedemptions', e.target.value)}\n placeholder=\"Unlimited\"\n className=\"w-48 px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n <p className=\"text-xs text-muted-foreground mt-1\">Leave empty for no cap</p>\n </div>\n </div>\n\n {/* Date window */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Active Window</h3>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Start Date *</label>\n <input\n type=\"date\"\n value={form.startsAt}\n onChange={(e) => set('startsAt', e.target.value)}\n className={`w-full px-3 py-2 rounded-lg border ${errors.startsAt ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.startsAt && (\n <p className=\"text-xs text-destructive mt-1\">{errors.startsAt}</p>\n )}\n </div>\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">End Date *</label>\n <input\n type=\"date\"\n value={form.expiresAt}\n onChange={(e) => set('expiresAt', e.target.value)}\n min={form.startsAt}\n className={`w-full px-3 py-2 rounded-lg border ${errors.expiresAt ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.expiresAt && (\n <p className=\"text-xs text-destructive mt-1\">{errors.expiresAt}</p>\n )}\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n Only purchases made between these dates will receive the bonus months\n </p>\n </div>\n\n {/* Scope */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Scope</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-2\">\n Applicable Products\n </label>\n <div className=\"flex flex-wrap gap-2\">\n {KNOWN_PRODUCTS.map((pid) => (\n <button\n key={pid}\n type=\"button\"\n onClick={() => toggleProduct(pid)}\n className={`px-3 py-1.5 rounded-lg border text-sm transition-colors ${\n form.applicableProductIds.includes(pid)\n ? 'border-primary bg-primary/10 text-primary font-medium'\n : 'border-border hover:border-primary/50'\n }`}\n >\n {pid}\n </button>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground mt-1\">\n {form.applicableProductIds.length === 0\n ? 'No selection = applies to all products'\n : `Applies to: ${form.applicableProductIds.join(', ')}`}\n </p>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Plan IDs (optional)\n </label>\n <input\n type=\"text\"\n value={form.applicablePlanIds}\n onChange={(e) => set('applicablePlanIds', e.target.value)}\n placeholder=\"plan-vibecontrols-pro-monthly, plan-vibecontrols-pro-yearly\"\n className=\"w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n <p className=\"text-xs text-muted-foreground mt-1\">\n Comma-separated. Leave empty to apply to all plans\n </p>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex gap-3 justify-end\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/promotions')}\n className=\"px-5 py-2 rounded-lg border border-border text-foreground hover:bg-muted text-sm\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting}\n className=\"px-5 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 text-sm disabled:opacity-50\"\n >\n {isSubmitting ? (\n <div className=\"size-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin\" />\n ) : (\n <Save className=\"size-4\" />\n )}\n Create Promotion\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}