@burdenoff/microfe-billing 2026.625.4 → 2026.625.5

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 (96) hide show
  1. package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js +192 -187
  2. package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js.map +1 -1
  3. package/dist/billing/modules/addons/pages/AddonCreatePage.js +322 -317
  4. package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
  5. package/dist/billing/modules/addons/pages/AddonDetailPage.js +190 -185
  6. package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
  7. package/dist/billing/modules/addons/pages/AddonEditPage.js +297 -292
  8. package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
  9. package/dist/billing/modules/addons/pages/AddonsBrowsePage.js +60 -75
  10. package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
  11. package/dist/billing/modules/addons/pages/AddonsListPage.js +86 -101
  12. package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
  13. package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +124 -121
  14. package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
  15. package/dist/billing/modules/analytics/pages/AnalyticsPage.js +103 -111
  16. package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
  17. package/dist/billing/modules/billing/pages/InvoiceDetailPage.js +196 -160
  18. package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
  19. package/dist/billing/modules/billing/pages/InvoicesListPage.js +58 -73
  20. package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
  21. package/dist/billing/modules/billing/pages/PaymentMethodsPage.js +65 -75
  22. package/dist/billing/modules/billing/pages/PaymentMethodsPage.js.map +1 -1
  23. package/dist/billing/modules/billing/pages/RefundsPage.js +399 -399
  24. package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
  25. package/dist/billing/modules/billing/pages/TransactionsPage.js +49 -48
  26. package/dist/billing/modules/billing/pages/TransactionsPage.js.map +1 -1
  27. package/dist/billing/modules/billing/pages/UserRefundsPage.js +144 -151
  28. package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -1
  29. package/dist/billing/modules/checkout/pages/CheckoutPage.js +430 -425
  30. package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
  31. package/dist/billing/modules/coupons/pages/CouponDetailPage.js +121 -93
  32. package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
  33. package/dist/billing/modules/coupons/pages/CouponsListPage.js +154 -152
  34. package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
  35. package/dist/billing/modules/coupons/pages/CreateCouponPage.js +467 -462
  36. package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
  37. package/dist/billing/modules/coupons/pages/EditCouponPage.js +294 -286
  38. package/dist/billing/modules/coupons/pages/EditCouponPage.js.map +1 -1
  39. package/dist/billing/modules/credits/pages/AdminCreditsPage.js +123 -117
  40. package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
  41. package/dist/billing/modules/credits/pages/CreditTransactionsPage.js +43 -42
  42. package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
  43. package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js +192 -207
  44. package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
  45. package/dist/billing/modules/credits/pages/CreditsPage.js +40 -56
  46. package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
  47. package/dist/billing/modules/dashboard/pages/OverviewPage.js +128 -121
  48. package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
  49. package/dist/billing/modules/discounts/pages/DiscountsListPage.js +165 -180
  50. package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
  51. package/dist/billing/modules/earnings/pages/EarningsPage.js +103 -100
  52. package/dist/billing/modules/earnings/pages/EarningsPage.js.map +1 -1
  53. package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +175 -175
  54. package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
  55. package/dist/billing/modules/earnings/pages/PayoutsPage.js +96 -96
  56. package/dist/billing/modules/earnings/pages/PayoutsPage.js.map +1 -1
  57. package/dist/billing/modules/plans/pages/PlanAssignFreePage.js +42 -40
  58. package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -1
  59. package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js +45 -38
  60. package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
  61. package/dist/billing/modules/plans/pages/PlanCreatePage.js +339 -334
  62. package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
  63. package/dist/billing/modules/plans/pages/PlanDetailPage.js +80 -74
  64. package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
  65. package/dist/billing/modules/plans/pages/PlanEditPage.js +342 -337
  66. package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
  67. package/dist/billing/modules/plans/pages/PlansBrowsePage.js +38 -53
  68. package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
  69. package/dist/billing/modules/plans/pages/PlansListPage.js +60 -75
  70. package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
  71. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js +242 -237
  72. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -1
  73. package/dist/billing/modules/promotions/pages/PromotionsListPage.js +29 -39
  74. package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -1
  75. package/dist/billing/modules/quota/pages/QuotaPage.js +1 -0
  76. package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js +299 -294
  77. package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
  78. package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js +191 -186
  79. package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
  80. package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js +308 -303
  81. package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
  82. package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js +57 -78
  83. package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
  84. package/dist/billing/modules/settings/pages/SettingsPage.js +79 -78
  85. package/dist/billing/modules/settings/pages/SettingsPage.js.map +1 -1
  86. package/dist/billing/modules/settings/pages/TeamPermissionsPage.js +15 -13
  87. package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
  88. package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js +338 -299
  89. package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
  90. package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js +82 -103
  91. package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
  92. package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js +59 -54
  93. package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
  94. package/dist/billing/modules/usage/pages/UsagePage.js +172 -215
  95. package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
  96. package/package.json +2 -2
@@ -1 +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({ refetchQueries: ['ListPromotions'] });\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.split(',').flatMap((s) => {\n const trimmed = s.trim();\n return trimmed ? [trimmed] : [];\n });\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 aria-label=\"Back to promotions\"\n className=\"p-2 rounded-button hover:bg-bg-sunken text-text-muted hover:text-text-primary transition-colors\"\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-bg-surface border border-border-seam rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Promotion Details</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary 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-input border ${errors.name ? 'border-status-error-border' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.name && <p className=\"text-xs text-status-error-text mt-1\">{errors.name}</p>}\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary 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-input border border-border-subtle bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n\n {/* Bonus config */}\n <div className=\"bg-bg-surface border border-border-seam rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Bonus Configuration</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Bonus Months *\n </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-input border ${errors.bonusMonths ? 'border-status-error-border' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.bonusMonths && (\n <p className=\"text-xs text-status-error-text mt-1\">{errors.bonusMonths}</p>\n )}\n <div className=\"mt-2 flex items-start gap-2 p-3 bg-[var(--color-accent-soft)] rounded-lg text-sm text-text-muted\">\n <Info className=\"size-4 shrink-0 mt-0.5 text-text-link\" />\n <span>\n Users who subscribe during this window get{' '}\n <strong className=\"text-text-primary\">\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-text-primary 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-input border border-border-subtle bg-bg-surface text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <p className=\"text-xs text-text-muted mt-1\">Leave empty for no cap</p>\n </div>\n </div>\n\n {/* Date window */}\n <div className=\"bg-bg-surface border border-border-seam rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Active Window</h3>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Start Date *\n </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-input border ${errors.startsAt ? 'border-status-error-border' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.startsAt && (\n <p className=\"text-xs text-status-error-text mt-1\">{errors.startsAt}</p>\n )}\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary 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-input border ${errors.expiresAt ? 'border-status-error-border' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.expiresAt && (\n <p className=\"text-xs text-status-error-text mt-1\">{errors.expiresAt}</p>\n )}\n </div>\n </div>\n <p className=\"text-xs text-text-muted\">\n Only purchases made between these dates will receive the bonus months\n </p>\n </div>\n\n {/* Scope */}\n <div className=\"bg-bg-surface border border-border-seam rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Scope</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary 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-action-primary-bg bg-[var(--color-accent-soft)] text-text-link font-medium'\n : 'border-border-subtle hover:border-action-primary-bg/50'\n }`}\n >\n {pid}\n </button>\n ))}\n </div>\n <p className=\"text-xs text-text-muted 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-text-primary 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-input border border-border-subtle bg-bg-surface text-text-primary placeholder:text-text-muted text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <p className=\"text-xs text-text-muted 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-button border border-border-subtle text-text-primary hover:bg-bg-sunken text-sm font-medium transition-colors\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting}\n className=\"px-5 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover flex items-center gap-2 text-sm font-medium transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-50\"\n >\n {isSubmitting ? (\n <div className=\"size-4 border-2 border-action-primary-text 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":";;;;;;;;;;AAOA,IAAM,IAAiB;CAAC;CAAgB;CAAc;CAAU;CAAc;CAAc,EAa/E,UAAgC;CAC3C,IAAM,IAAa,GAAoB,EACjC,IAAc,GAAuB,EACrC,CAAC,KAAmB,EAA2B,EAAE,gBAAgB,CAAC,iBAAiB,EAAE,CAAC,EACtF,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,CAAC,GAAQ,KAAa,EAAkD,EAAE,CAAC,EAE3E,qBAAQ,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,IAAI,CAAC,IAC5C,CAAC,GAAM,KAAW,EAAmB;EACzC,MAAM;EACN,aAAa;EACb,UAAU;EACV,WAAW;EACX,aAAa;EACb,gBAAgB;EAChB,sBAAsB,EAAE;EACxB,mBAAmB;EACpB,CAAC;AAEF,KAAI,CAAC,EAAY,eACf,QAAO,kBAAC,GAAD,EAAc,SAAQ,mDAAoD,CAAA;CAGnF,IAAM,KAAiC,GAAM,MAC3C,GAAS,OAAU;EAAE,GAAG;GAAO,IAAI;EAAG,EAAE,EAEpC,KAAiB,MAAgB;AACrC,IACE,wBACA,EAAK,qBAAqB,SAAS,EAAI,GACnC,EAAK,qBAAqB,QAAQ,MAAM,MAAM,EAAI,GAClD,CAAC,GAAG,EAAK,sBAAsB,EAAI,CACxC;IAGG,UAAiB;EACrB,IAAM,IAAmB,EAAE;AAQ3B,SAPK,EAAK,KAAK,MAAM,KAAE,EAAE,OAAO,qBAC3B,EAAK,aAAU,EAAE,WAAW,2BAC5B,EAAK,cAAW,EAAE,YAAY,yBAC/B,EAAK,aAAa,EAAK,aAAa,EAAK,aAC3C,EAAE,YAAY,sCACZ,EAAK,cAAc,MAAG,EAAE,cAAc,uBAC1C,EAAU,EAAE,EACL,OAAO,KAAK,EAAE,CAAC,WAAW;;AAmCnC,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,cAAc;IACxC,cAAW;IACX,WAAU;cAEV,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA;IACzB,CAAA,EACT,kBAAC,GAAD;IACE,OAAM;IACN,aAAY;IACZ,MAAM,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;IACjC,CAAA,CACE;MAEN,kBAAC,QAAD;GAAM,UAlDW,OAAO,MAAuB;AACjD,UAAE,gBAAgB,EACb,GAAU,EACf;OAAgB,GAAK;AACrB,SAAI;MACF,IAAM,IAAU,EAAK,kBAAkB,MAAM,IAAI,CAAC,SAAS,MAAM;OAC/D,IAAM,IAAU,EAAE,MAAM;AACxB,cAAO,IAAU,CAAC,EAAQ,GAAG,EAAE;QAC/B;AAgBF,MAdA,MAAM,EAAgB,EACpB,WAAW,EACT,OAAO;OACL,MAAM,EAAK,KAAK,MAAM;OACtB,aAAa,EAAK,YAAY,MAAM,IAAI,KAAA;OACxC,UAAU,IAAI,KAAK,EAAK,SAAS,CAAC,aAAa;OAC/C,WAAW,IAAI,KAAK,EAAK,UAAU,CAAC,aAAa;OACjD,aAAa,EAAK;OAClB,gBAAgB,EAAK,iBAAiB,SAAS,EAAK,eAAe,GAAG,KAAA;OACtE,sBAAsB,EAAK;OAC3B,mBAAmB;OACpB,EACF,EACF,CAAC,EACF,EAAW,cAAc;cAClB,GAAK;AACZ,QAAU,EAAE,MAAM,aAAe,QAAQ,EAAI,UAAU,8BAA8B,CAAC;eAC9E;AACR,QAAgB,GAAM;;;;GAsBQ,WAAU;aAAxC;IAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,WAAU;iBAAkC;OAAsB,CAAA;MAEtE,kBAAC,OAAD,EAAA,UAAA;OACE,kBAAC,SAAD;QAAO,WAAU;kBAAmD;QAAc,CAAA;OAClF,kBAAC,SAAD;QACE,MAAK;QACL,OAAO,EAAK;QACZ,WAAW,MAAM,EAAI,QAAQ,EAAE,OAAO,MAAM;QAC5C,aAAY;QACZ,WAAW,yCAAyC,EAAO,OAAO,+BAA+B,uBAAuB;QACxH,CAAA;OACD,EAAO,QAAQ,kBAAC,KAAD;QAAG,WAAU;kBAAuC,EAAO;QAAS,CAAA;OAChF,EAAA,CAAA;MAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,WAAU;iBAAmD;OAAmB,CAAA,EACvF,kBAAC,YAAD;OACE,OAAO,EAAK;OACZ,WAAW,MAAM,EAAI,eAAe,EAAE,OAAO,MAAM;OACnD,aAAY;OACZ,MAAM;OACN,WAAU;OACV,CAAA,CACE,EAAA,CAAA;MACF;;IAGN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,WAAU;iBAAkC;OAAwB,CAAA;MAExE,kBAAC,OAAD,EAAA,UAAA;OACE,kBAAC,SAAD;QAAO,WAAU;kBAAmD;QAE5D,CAAA;OACR,kBAAC,SAAD;QACE,MAAK;QACL,OAAO,EAAK;QACZ,KAAK;QACL,KAAK;QACL,WAAW,MAAM,EAAI,eAAe,SAAS,EAAE,OAAO,MAAM,IAAI,EAAE;QAClE,WAAW,uCAAuC,EAAO,cAAc,+BAA+B,uBAAuB;QAC7H,CAAA;OACD,EAAO,eACN,kBAAC,KAAD;QAAG,WAAU;kBAAuC,EAAO;QAAgB,CAAA;OAE7E,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAM,WAAU,yCAA0C,CAAA,EAC1D,kBAAC,QAAD,EAAA,UAAA;SAAM;SACuC;SAC3C,kBAAC,UAAD;UAAQ,WAAU;oBAAlB;WACG,EAAK;WAAY;WAAc,EAAK,gBAAgB,IAAU,KAAN;WAClD;;SAAC;SAAI;SAET,EAAA,CAAA,CACH;;OACF,EAAA,CAAA;MAEN,kBAAC,OAAD,EAAA,UAAA;OACE,kBAAC,SAAD;QAAO,WAAU;kBAAmD;QAE5D,CAAA;OACR,kBAAC,SAAD;QACE,MAAK;QACL,OAAO,EAAK;QACZ,KAAK;QACL,WAAW,MAAM,EAAI,kBAAkB,EAAE,OAAO,MAAM;QACtD,aAAY;QACZ,WAAU;QACV,CAAA;OACF,kBAAC,KAAD;QAAG,WAAU;kBAA+B;QAA0B,CAAA;OAClE,EAAA,CAAA;MACF;;IAGN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,WAAU;iBAAkC;OAAkB,CAAA;MAElE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA;QACE,kBAAC,SAAD;SAAO,WAAU;mBAAmD;SAE5D,CAAA;QACR,kBAAC,SAAD;SACE,MAAK;SACL,OAAO,EAAK;SACZ,WAAW,MAAM,EAAI,YAAY,EAAE,OAAO,MAAM;SAChD,WAAW,yCAAyC,EAAO,WAAW,+BAA+B,uBAAuB;SAC5H,CAAA;QACD,EAAO,YACN,kBAAC,KAAD;SAAG,WAAU;mBAAuC,EAAO;SAAa,CAAA;QAEtE,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA;QACE,kBAAC,SAAD;SAAO,WAAU;mBAAmD;SAAkB,CAAA;QACtF,kBAAC,SAAD;SACE,MAAK;SACL,OAAO,EAAK;SACZ,WAAW,MAAM,EAAI,aAAa,EAAE,OAAO,MAAM;SACjD,KAAK,EAAK;SACV,WAAW,yCAAyC,EAAO,YAAY,+BAA+B,uBAAuB;SAC7H,CAAA;QACD,EAAO,aACN,kBAAC,KAAD;SAAG,WAAU;mBAAuC,EAAO;SAAc,CAAA;QAEvE,EAAA,CAAA,CACF;;MACN,kBAAC,KAAD;OAAG,WAAU;iBAA0B;OAEnC,CAAA;MACA;;IAGN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,WAAU;iBAAkC;OAAU,CAAA;MAE1D,kBAAC,OAAD,EAAA,UAAA;OACE,kBAAC,SAAD;QAAO,WAAU;kBAAmD;QAE5D,CAAA;OACR,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAe,KAAK,MACnB,kBAAC,UAAD;SAEE,MAAK;SACL,eAAe,EAAc,EAAI;SACjC,WAAW,2DACT,EAAK,qBAAqB,SAAS,EAAI,GACnC,sFACA;mBAGL;SACM,EAVF,EAUE,CACT;QACE,CAAA;OACN,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAK,qBAAqB,WAAW,IAClC,2CACA,eAAe,EAAK,qBAAqB,KAAK,KAAK;QACrD,CAAA;OACA,EAAA,CAAA;MAEN,kBAAC,OAAD,EAAA,UAAA;OACE,kBAAC,SAAD;QAAO,WAAU;kBAAmD;QAE5D,CAAA;OACR,kBAAC,SAAD;QACE,MAAK;QACL,OAAO,EAAK;QACZ,WAAW,MAAM,EAAI,qBAAqB,EAAE,OAAO,MAAM;QACzD,aAAY;QACZ,WAAU;QACV,CAAA;OACF,kBAAC,KAAD;QAAG,WAAU;kBAA+B;QAExC,CAAA;OACA,EAAA,CAAA;MACF;;IAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAW,cAAc;MACxC,WAAU;gBACX;MAEQ,CAAA,EACT,kBAAC,UAAD;MACE,MAAK;MACL,UAAU;MACV,WAAU;gBAHZ,CAKG,IACC,kBAAC,OAAD,EAAK,WAAU,6FAA8F,CAAA,GAE7G,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,EAC3B,mBAEK;QACL;;IACD;KACH"}
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 { PagePurpose } from '@burdenoff/fe-libs/ui';\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({ refetchQueries: ['ListPromotions'] });\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.split(',').flatMap((s) => {\n const trimmed = s.trim();\n return trimmed ? [trimmed] : [];\n });\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 aria-label=\"Back to promotions\"\n className=\"p-2 rounded-button hover:bg-bg-sunken text-text-muted hover:text-text-primary transition-colors\"\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 <PagePurpose>\n Set up a time-bounded campaign that rewards new subscribers with bonus free renewal months.\n Define the bonus, the window during which it applies, an optional redemption cap and which\n products it covers — then activate it to start offering it.\n </PagePurpose>\n\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Basic info */}\n <div className=\"bg-bg-surface border border-border-seam rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Promotion Details</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary 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-input border ${errors.name ? 'border-status-error-border' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.name && <p className=\"text-xs text-status-error-text mt-1\">{errors.name}</p>}\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary 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-input border border-border-subtle bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n\n {/* Bonus config */}\n <div className=\"bg-bg-surface border border-border-seam rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Bonus Configuration</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Bonus Months *\n </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-input border ${errors.bonusMonths ? 'border-status-error-border' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.bonusMonths && (\n <p className=\"text-xs text-status-error-text mt-1\">{errors.bonusMonths}</p>\n )}\n <div className=\"mt-2 flex items-start gap-2 p-3 bg-[var(--color-accent-soft)] rounded-lg text-sm text-text-muted\">\n <Info className=\"size-4 shrink-0 mt-0.5 text-text-link\" />\n <span>\n Users who subscribe during this window get{' '}\n <strong className=\"text-text-primary\">\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-text-primary 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-input border border-border-subtle bg-bg-surface text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <p className=\"text-xs text-text-muted mt-1\">Leave empty for no cap</p>\n </div>\n </div>\n\n {/* Date window */}\n <div className=\"bg-bg-surface border border-border-seam rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Active Window</h3>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Start Date *\n </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-input border ${errors.startsAt ? 'border-status-error-border' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.startsAt && (\n <p className=\"text-xs text-status-error-text mt-1\">{errors.startsAt}</p>\n )}\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary 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-input border ${errors.expiresAt ? 'border-status-error-border' : 'border-border-subtle'} bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]`}\n />\n {errors.expiresAt && (\n <p className=\"text-xs text-status-error-text mt-1\">{errors.expiresAt}</p>\n )}\n </div>\n </div>\n <p className=\"text-xs text-text-muted\">\n Only purchases made between these dates will receive the bonus months\n </p>\n </div>\n\n {/* Scope */}\n <div className=\"bg-bg-surface border border-border-seam rounded-card p-5 space-y-4 shadow-[var(--shadow-elevation-1)]\">\n <h3 className=\"font-semibold text-text-primary\">Scope</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary 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-action-primary-bg bg-[var(--color-accent-soft)] text-text-link font-medium'\n : 'border-border-subtle hover:border-action-primary-bg/50'\n }`}\n >\n {pid}\n </button>\n ))}\n </div>\n <p className=\"text-xs text-text-muted 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-text-primary 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-input border border-border-subtle bg-bg-surface text-text-primary placeholder:text-text-muted text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <p className=\"text-xs text-text-muted 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-button border border-border-subtle text-text-primary hover:bg-bg-sunken text-sm font-medium transition-colors\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting}\n className=\"px-5 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover flex items-center gap-2 text-sm font-medium transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-50\"\n >\n {isSubmitting ? (\n <div className=\"size-4 border-2 border-action-primary-text 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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"}
@@ -7,24 +7,25 @@ import { useBillingNavigate as o } from "../../../hooks/useBillingNavigate.js";
7
7
  import { useState as s } from "react";
8
8
  import { Calendar as c, Gift as l, Plus as u, Tag as d, ToggleLeft as f, ToggleRight as p, Users as m } from "lucide-react";
9
9
  import { jsx as h, jsxs as g } from "react/jsx-runtime";
10
+ import { IllustratedEmptyState as _, PagePurpose as v } from "@burdenoff/fe-libs/ui";
10
11
  //#region src/billing/modules/promotions/pages/PromotionsListPage.tsx
11
- var _ = () => {
12
- let _ = o(), v = n(), [y, b] = s(!1), [x, S] = s(null), { data: C, loading: w, refetch: T } = i({ variables: { includeInactive: y } }), [E] = r(), [D] = a();
13
- if (!v.canManagePlans) return /* @__PURE__ */ h(e, { message: "You don't have permission to manage promotions." });
14
- let O = C?.listPromotions ?? [], k = async (e, t) => {
15
- S(e);
12
+ var y = () => {
13
+ let y = o(), b = n(), [x, S] = s(!1), [C, w] = s(null), { data: T, loading: E, refetch: D } = i({ variables: { includeInactive: x } }), [O] = r(), [k] = a();
14
+ if (!b.canManagePlans) return /* @__PURE__ */ h(e, { message: "You don't have permission to manage promotions." });
15
+ let A = T?.listPromotions ?? [], j = async (e, t) => {
16
+ w(e);
16
17
  try {
17
- t ? await E({ variables: { id: e } }) : await D({ variables: {
18
+ t ? await O({ variables: { id: e } }) : await k({ variables: {
18
19
  id: e,
19
20
  input: { isActive: !0 }
20
- } }), await T();
21
+ } }), await D();
21
22
  } finally {
22
- S(null);
23
+ w(null);
23
24
  }
24
- }, A = /* @__PURE__ */ new Date(), j = (e) => e.isActive ? new Date(e.expiresAt) < A ? {
25
+ }, M = /* @__PURE__ */ new Date(), N = (e) => e.isActive ? new Date(e.expiresAt) < M ? {
25
26
  text: "Expired",
26
27
  cls: "bg-status-error-bg-subtle text-status-error-text"
27
- } : new Date(e.startsAt) > A ? {
28
+ } : new Date(e.startsAt) > M ? {
28
29
  text: "Upcoming",
29
30
  cls: "bg-[var(--color-accent-soft)] text-text-link"
30
31
  } : {
@@ -44,51 +45,40 @@ var _ = () => {
44
45
  description: "Time-bounded offers that grant bonus free renewal months to subscribers",
45
46
  icon: /* @__PURE__ */ h(l, { className: "size-6" })
46
47
  }), /* @__PURE__ */ g("button", {
47
- onClick: () => _("/promotions/create"),
48
+ onClick: () => y("/promotions/create"),
48
49
  className: "flex items-center gap-2 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover text-sm font-medium shrink-0 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
49
50
  children: [/* @__PURE__ */ h(u, { className: "size-4" }), "New Promotion"]
50
51
  })]
51
52
  }),
53
+ /* @__PURE__ */ h(v, { children: "Promotions are limited-time offers that reward subscribers with bonus free renewal months — great for launches or win-back campaigns. Set the bonus, the active window and an optional redemption cap, then activate or deactivate them as your campaign runs." }),
52
54
  /* @__PURE__ */ h("div", {
53
55
  className: "flex items-center gap-3",
54
56
  children: /* @__PURE__ */ g("label", {
55
57
  className: "flex items-center gap-2 text-sm text-text-muted cursor-pointer select-none",
56
58
  children: [/* @__PURE__ */ h("input", {
57
59
  type: "checkbox",
58
- checked: y,
59
- onChange: (e) => b(e.target.checked),
60
+ checked: x,
61
+ onChange: (e) => S(e.target.checked),
60
62
  className: "rounded border-border-subtle"
61
63
  }), "Show inactive / expired"]
62
64
  })
63
65
  }),
64
- w ? /* @__PURE__ */ h("div", {
66
+ E ? /* @__PURE__ */ h("div", {
65
67
  className: "text-center py-16 text-text-muted",
66
68
  children: "Loading promotions…"
67
- }) : O.length === 0 ? /* @__PURE__ */ g("div", {
68
- className: "flex flex-col items-center justify-center text-center py-16 border border-dashed border-border-seam rounded-card bg-bg-surface",
69
- children: [
70
- /* @__PURE__ */ h("div", {
71
- className: "flex size-12 items-center justify-center rounded-full bg-[var(--color-accent-soft)] mb-4",
72
- children: /* @__PURE__ */ h(l, { className: "size-6 text-text-link" })
73
- }),
74
- /* @__PURE__ */ h("p", {
75
- className: "text-base font-semibold text-text-primary",
76
- children: "No promotions yet"
77
- }),
78
- /* @__PURE__ */ h("p", {
79
- className: "text-sm text-text-muted mt-1",
80
- children: "Create one to offer bonus months to early subscribers"
81
- }),
82
- /* @__PURE__ */ h("button", {
83
- onClick: () => _("/promotions/create"),
84
- className: "mt-4 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text text-sm font-medium transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
85
- children: "Create Promotion"
86
- })
87
- ]
69
+ }) : A.length === 0 ? /* @__PURE__ */ h(_, {
70
+ illustration: "empty-data",
71
+ title: "No promotions yet",
72
+ description: "Create one to offer bonus months to early subscribers",
73
+ action: /* @__PURE__ */ h("button", {
74
+ onClick: () => y("/promotions/create"),
75
+ className: "px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text text-sm font-medium transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
76
+ children: "Create Promotion"
77
+ })
88
78
  }) : /* @__PURE__ */ h("div", {
89
79
  className: "space-y-3",
90
- children: O.map((e) => {
91
- let { text: t, cls: n } = j(e), r = x === e.id;
80
+ children: A.map((e) => {
81
+ let { text: t, cls: n } = N(e), r = C === e.id;
92
82
  return /* @__PURE__ */ g("div", {
93
83
  className: "bg-bg-surface border border-border-seam rounded-card p-5 flex flex-col sm:flex-row sm:items-center gap-4 shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)]",
94
84
  children: [/* @__PURE__ */ g("div", {
@@ -152,7 +142,7 @@ var _ = () => {
152
142
  }), /* @__PURE__ */ h("div", {
153
143
  className: "flex items-center gap-2 shrink-0",
154
144
  children: /* @__PURE__ */ h("button", {
155
- onClick: () => k(e.id, e.isActive),
145
+ onClick: () => j(e.id, e.isActive),
156
146
  disabled: r,
157
147
  title: e.isActive ? "Deactivate" : "Reactivate",
158
148
  className: `p-2 rounded-button transition-colors ${e.isActive ? "text-status-success-text hover:bg-status-success-bg-subtle" : "text-text-muted hover:bg-bg-sunken"} disabled:opacity-50`,
@@ -166,6 +156,6 @@ var _ = () => {
166
156
  });
167
157
  };
168
158
  //#endregion
169
- export { _ as PromotionsListPage };
159
+ export { y as PromotionsListPage };
170
160
 
171
161
  //# sourceMappingURL=PromotionsListPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PromotionsListPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/PromotionsListPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { Plus, Tag, Calendar, Users, ToggleLeft, ToggleRight, Gift } from 'lucide-react';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport {\n useListPromotionsQuery,\n useDeactivatePromotionMutation,\n useUpdatePromotionMutation,\n} from '../../../../generated/global-operations';\n\nexport const PromotionsListPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [showInactive, setShowInactive] = useState(false);\n const [actionId, setActionId] = useState<string | null>(null);\n\n const { data, loading, refetch } = useListPromotionsQuery({\n variables: { includeInactive: showInactive },\n });\n\n const [deactivatePromotion] = useDeactivatePromotionMutation();\n const [updatePromotion] = useUpdatePromotionMutation();\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to manage promotions.\" />;\n }\n\n const promotions = data?.listPromotions ?? [];\n\n const handleToggle = async (id: string, currentlyActive: boolean) => {\n setActionId(id);\n try {\n if (currentlyActive) {\n await deactivatePromotion({ variables: { id } });\n } else {\n await updatePromotion({ variables: { id, input: { isActive: true } } });\n }\n await refetch();\n } finally {\n setActionId(null);\n }\n };\n\n const now = new Date();\n const statusLabel = (p: { isActive: boolean; startsAt: string; expiresAt: string }) => {\n if (!p.isActive) return { text: 'Inactive', cls: 'bg-bg-sunken text-text-muted' };\n if (new Date(p.expiresAt) < now)\n return { text: 'Expired', cls: 'bg-status-error-bg-subtle text-status-error-text' };\n if (new Date(p.startsAt) > now)\n return { text: 'Upcoming', cls: 'bg-[var(--color-accent-soft)] text-text-link' };\n return { text: 'Live', cls: 'bg-status-success-bg-subtle text-status-success-text' };\n };\n\n return (\n <div className=\"max-w-5xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center justify-between gap-4\">\n <PageHeader\n title=\"Promotions\"\n description=\"Time-bounded offers that grant bonus free renewal months to subscribers\"\n icon={<Gift className=\"size-6\" />}\n />\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"flex items-center gap-2 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover text-sm font-medium shrink-0 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Plus className=\"size-4\" />\n New Promotion\n </button>\n </div>\n\n {/* Filter */}\n <div className=\"flex items-center gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-text-muted cursor-pointer select-none\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"rounded border-border-subtle\"\n />\n Show inactive / expired\n </label>\n </div>\n\n {loading ? (\n <div className=\"text-center py-16 text-text-muted\">Loading promotions…</div>\n ) : promotions.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center text-center py-16 border border-dashed border-border-seam rounded-card bg-bg-surface\">\n <div className=\"flex size-12 items-center justify-center rounded-full bg-[var(--color-accent-soft)] mb-4\">\n <Gift className=\"size-6 text-text-link\" />\n </div>\n <p className=\"text-base font-semibold text-text-primary\">No promotions yet</p>\n <p className=\"text-sm text-text-muted mt-1\">\n Create one to offer bonus months to early subscribers\n </p>\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"mt-4 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text text-sm font-medium transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n Create Promotion\n </button>\n </div>\n ) : (\n <div className=\"space-y-3\">\n {promotions.map((p: (typeof promotions)[number]) => {\n const { text, cls } = statusLabel(p);\n const busy = actionId === p.id;\n return (\n <div\n key={p.id}\n className=\"bg-bg-surface border border-border-seam rounded-card p-5 flex flex-col sm:flex-row sm:items-center gap-4 shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)]\"\n >\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-1\">\n <span className=\"font-semibold text-text-primary truncate\">{p.name}</span>\n <span className={`text-xs font-medium px-2 py-0.5 rounded-full ${cls}`}>\n {text}\n </span>\n </div>\n {p.description && (\n <p className=\"text-sm text-text-muted mb-2 line-clamp-1\">{p.description}</p>\n )}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-muted\">\n <span className=\"flex items-center gap-1\">\n <Gift className=\"size-3\" />\n <strong className=\"text-text-primary\">{p.bonusMonths}</strong> bonus month\n {p.bonusMonths !== 1 ? 's' : ''}\n </span>\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"size-3\" />\n {new Date(p.startsAt).toLocaleDateString()} –{' '}\n {new Date(p.expiresAt).toLocaleDateString()}\n </span>\n <span className=\"flex items-center gap-1\">\n <Users className=\"size-3\" />\n {p.currentRedemptions}\n {p.maxRedemptions ? ` / ${p.maxRedemptions}` : ''} redemptions\n </span>\n {p.applicableProductIds.length > 0 && (\n <span className=\"flex items-center gap-1\">\n <Tag className=\"size-3\" />\n {p.applicableProductIds.join(', ')}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2 shrink-0\">\n <button\n onClick={() => handleToggle(p.id, p.isActive)}\n disabled={busy}\n title={p.isActive ? 'Deactivate' : 'Reactivate'}\n className={`p-2 rounded-button transition-colors ${\n p.isActive\n ? 'text-status-success-text hover:bg-status-success-bg-subtle'\n : 'text-text-muted hover:bg-bg-sunken'\n } disabled:opacity-50`}\n >\n {p.isActive ? (\n <ToggleRight className=\"size-5\" />\n ) : (\n <ToggleLeft className=\"size-5\" />\n )}\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"PromotionsListPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/PromotionsListPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { Plus, Tag, Calendar, Users, ToggleLeft, ToggleRight, Gift } from 'lucide-react';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport {\n useListPromotionsQuery,\n useDeactivatePromotionMutation,\n useUpdatePromotionMutation,\n} from '../../../../generated/global-operations';\n\nexport const PromotionsListPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [showInactive, setShowInactive] = useState(false);\n const [actionId, setActionId] = useState<string | null>(null);\n\n const { data, loading, refetch } = useListPromotionsQuery({\n variables: { includeInactive: showInactive },\n });\n\n const [deactivatePromotion] = useDeactivatePromotionMutation();\n const [updatePromotion] = useUpdatePromotionMutation();\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to manage promotions.\" />;\n }\n\n const promotions = data?.listPromotions ?? [];\n\n const handleToggle = async (id: string, currentlyActive: boolean) => {\n setActionId(id);\n try {\n if (currentlyActive) {\n await deactivatePromotion({ variables: { id } });\n } else {\n await updatePromotion({ variables: { id, input: { isActive: true } } });\n }\n await refetch();\n } finally {\n setActionId(null);\n }\n };\n\n const now = new Date();\n const statusLabel = (p: { isActive: boolean; startsAt: string; expiresAt: string }) => {\n if (!p.isActive) return { text: 'Inactive', cls: 'bg-bg-sunken text-text-muted' };\n if (new Date(p.expiresAt) < now)\n return { text: 'Expired', cls: 'bg-status-error-bg-subtle text-status-error-text' };\n if (new Date(p.startsAt) > now)\n return { text: 'Upcoming', cls: 'bg-[var(--color-accent-soft)] text-text-link' };\n return { text: 'Live', cls: 'bg-status-success-bg-subtle text-status-success-text' };\n };\n\n return (\n <div className=\"max-w-5xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center justify-between gap-4\">\n <PageHeader\n title=\"Promotions\"\n description=\"Time-bounded offers that grant bonus free renewal months to subscribers\"\n icon={<Gift className=\"size-6\" />}\n />\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"flex items-center gap-2 px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover text-sm font-medium shrink-0 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Plus className=\"size-4\" />\n New Promotion\n </button>\n </div>\n\n <PagePurpose>\n Promotions are limited-time offers that reward subscribers with bonus free renewal months —\n great for launches or win-back campaigns. Set the bonus, the active window and an optional\n redemption cap, then activate or deactivate them as your campaign runs.\n </PagePurpose>\n\n {/* Filter */}\n <div className=\"flex items-center gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-text-muted cursor-pointer select-none\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"rounded border-border-subtle\"\n />\n Show inactive / expired\n </label>\n </div>\n\n {loading ? (\n <div className=\"text-center py-16 text-text-muted\">Loading promotions…</div>\n ) : promotions.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No promotions yet\"\n description=\"Create one to offer bonus months to early subscribers\"\n action={\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"px-4 py-2 rounded-button bg-action-primary-bg text-action-primary-text text-sm font-medium transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n Create Promotion\n </button>\n }\n />\n ) : (\n <div className=\"space-y-3\">\n {promotions.map((p: (typeof promotions)[number]) => {\n const { text, cls } = statusLabel(p);\n const busy = actionId === p.id;\n return (\n <div\n key={p.id}\n className=\"bg-bg-surface border border-border-seam rounded-card p-5 flex flex-col sm:flex-row sm:items-center gap-4 shadow-[var(--shadow-elevation-1)] transition-all duration-200 hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)]\"\n >\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-1\">\n <span className=\"font-semibold text-text-primary truncate\">{p.name}</span>\n <span className={`text-xs font-medium px-2 py-0.5 rounded-full ${cls}`}>\n {text}\n </span>\n </div>\n {p.description && (\n <p className=\"text-sm text-text-muted mb-2 line-clamp-1\">{p.description}</p>\n )}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-muted\">\n <span className=\"flex items-center gap-1\">\n <Gift className=\"size-3\" />\n <strong className=\"text-text-primary\">{p.bonusMonths}</strong> bonus month\n {p.bonusMonths !== 1 ? 's' : ''}\n </span>\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"size-3\" />\n {new Date(p.startsAt).toLocaleDateString()} –{' '}\n {new Date(p.expiresAt).toLocaleDateString()}\n </span>\n <span className=\"flex items-center gap-1\">\n <Users className=\"size-3\" />\n {p.currentRedemptions}\n {p.maxRedemptions ? ` / ${p.maxRedemptions}` : ''} redemptions\n </span>\n {p.applicableProductIds.length > 0 && (\n <span className=\"flex items-center gap-1\">\n <Tag className=\"size-3\" />\n {p.applicableProductIds.join(', ')}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2 shrink-0\">\n <button\n onClick={() => handleToggle(p.id, p.isActive)}\n disabled={busy}\n title={p.isActive ? 'Deactivate' : 'Reactivate'}\n className={`p-2 rounded-button transition-colors ${\n p.isActive\n ? 'text-status-success-text hover:bg-status-success-bg-subtle'\n : 'text-text-muted hover:bg-bg-sunken'\n } disabled:opacity-50`}\n >\n {p.isActive ? (\n <ToggleRight className=\"size-5\" />\n ) : (\n <ToggleLeft className=\"size-5\" />\n )}\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -5,3 +5,4 @@ import "../hooks/useQuota.js";
5
5
  import "react";
6
6
  import "react/jsx-runtime";
7
7
  import "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
8
+ import "@burdenoff/fe-libs/ui";