@burdenoff/microfe-billing 2026.530.2 → 2026.530.4

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 (40) hide show
  1. package/dist/billing/BillingAdminRoutes.js +110 -71
  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/BillingUserRoutes.js +103 -90
  6. package/dist/billing/BillingUserRoutes.js.map +1 -1
  7. package/dist/billing/modules/billing/index.js +2 -1
  8. package/dist/billing/modules/billing/pages/InvoicesListPage.js +135 -126
  9. package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
  10. package/dist/billing/modules/billing/pages/UserRefundsPage.js +417 -0
  11. package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -0
  12. package/dist/billing/modules/billing/pages/index.js +1 -0
  13. package/dist/billing/modules/checkout/hooks/useCheckout.js +163 -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/plans/constants/featuredQuotas.js +37 -0
  18. package/dist/billing/modules/plans/constants/featuredQuotas.js.map +1 -0
  19. package/dist/billing/modules/plans/index.js +2 -1
  20. package/dist/billing/modules/plans/pages/PlanAssignFreePage.js +205 -0
  21. package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -0
  22. package/dist/billing/modules/plans/pages/PlanCreatePage.js +51 -33
  23. package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
  24. package/dist/billing/modules/plans/pages/PlanEditPage.js +78 -60
  25. package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
  26. package/dist/billing/modules/plans/pages/PlansBrowsePage.js +246 -126
  27. package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
  28. package/dist/billing/modules/plans/pages/index.js +1 -0
  29. package/dist/billing/modules/promotions/index.js +4 -0
  30. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js +289 -0
  31. package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -0
  32. package/dist/billing/modules/promotions/pages/PromotionsListPage.js +168 -0
  33. package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -0
  34. package/dist/billing/modules/promotions/pages/index.js +2 -0
  35. package/dist/billing/shared/types/graphql.js.map +1 -1
  36. package/dist/generated/global-operations.js +422 -290
  37. package/dist/generated/global-operations.js.map +1 -1
  38. package/dist/generated/global-types.js +3 -1
  39. package/dist/generated/global-types.js.map +1 -1
  40. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CreateCouponPage.js","names":[],"sources":["../../../../../src/billing/modules/coupons/pages/CreateCouponPage.tsx"],"sourcesContent":["/**\n * Create Coupon Page\n *\n * Form for creating new coupon codes with validation\n */\n\nimport { type FC, useState } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { ArrowLeft, Save, Tag, Percent, DollarSign, Package } from 'lucide-react';\n\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useCreateCouponMutation } from '../../../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface CouponFormData {\n code: string;\n name: string;\n description: string;\n scope: 'GENERIC' | 'USER_SPECIFIC' | 'REFERRAL' | 'CAMPAIGN';\n type: 'PERCENTAGE' | 'FIXED_AMOUNT' | 'FREE_SHIPPING';\n value: number;\n currency: string;\n minPurchaseAmount: number | null;\n maxDiscountAmount: number | null;\n maxTotalUses: number | null;\n maxUsesPerUser: number;\n validFrom: string;\n validUntil: string;\n restrictedToUserIds: string[];\n applicableProducts: string[];\n applicableServices: string[];\n campaignName: string;\n}\n\nexport const CreateCouponPage: 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 navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n\n const [formData, setFormData] = useState<CouponFormData>({\n code: '',\n name: '',\n description: '',\n scope: 'GENERIC',\n type: 'PERCENTAGE',\n value: 0,\n currency: 'USD',\n minPurchaseAmount: null,\n maxDiscountAmount: null,\n maxTotalUses: null,\n maxUsesPerUser: 1,\n validFrom: new Date().toISOString().split('T')[0],\n validUntil: '',\n restrictedToUserIds: [],\n applicableProducts: [],\n applicableServices: [],\n campaignName: '',\n });\n\n const [errors, setErrors] = useState<Partial<Record<keyof CouponFormData, string>>>({});\n const [isSubmitting, setIsSubmitting] = useState(false);\n\n // GraphQL mutation\n const [createCouponMutation] = useCreateCouponMutation();\n\n // Check permissions\n if (!permissions.canManageDiscounts) {\n return <AccessDenied message=\"You don't have permission to create coupons.\" />;\n }\n\n // Handle input change\n const handleChange = (\n field: keyof CouponFormData,\n value: CouponFormData[keyof CouponFormData]\n ) => {\n setFormData((prev) => ({ ...prev, [field]: value }));\n\n // Clear error for this field\n if (errors[field]) {\n setErrors((prev) => {\n const newErrors = { ...prev };\n delete newErrors[field];\n return newErrors;\n });\n }\n };\n\n // Validate form\n const validateForm = (): boolean => {\n const newErrors: Partial<Record<keyof CouponFormData, string>> = {};\n\n // Code validation\n if (!formData.code.trim()) {\n newErrors.code = 'Coupon code is required';\n } else if (!/^[A-Z0-9-_]+$/.test(formData.code)) {\n newErrors.code =\n 'Code must contain only uppercase letters, numbers, hyphens, and underscores';\n } else if (formData.code.length < 3) {\n newErrors.code = 'Code must be at least 3 characters';\n } else if (formData.code.length > 50) {\n newErrors.code = 'Code must be at most 50 characters';\n }\n\n // Name validation\n if (!formData.name.trim()) {\n newErrors.name = 'Coupon name is required';\n }\n\n // Value validation\n if (formData.value <= 0) {\n newErrors.value = 'Value must be greater than 0';\n }\n if (formData.type === 'PERCENTAGE' && formData.value > 100) {\n newErrors.value = 'Percentage cannot exceed 100%';\n }\n\n // Date validation\n if (!formData.validFrom) {\n newErrors.validFrom = 'Start date is required';\n }\n if (formData.validUntil && formData.validUntil < formData.validFrom) {\n newErrors.validUntil = 'End date must be after start date';\n }\n\n setErrors(newErrors);\n return Object.keys(newErrors).length === 0;\n };\n\n // Handle submit\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!validateForm()) {\n return;\n }\n\n setIsSubmitting(true);\n\n try {\n // Prepare input data - convert dates to ISO format\n const input = {\n code: formData.code,\n name: formData.name,\n description: formData.description,\n scope: formData.scope,\n type: formData.type,\n value: formData.value,\n currency: formData.currency,\n minPurchaseAmount: formData.minPurchaseAmount,\n maxDiscountAmount: formData.maxDiscountAmount,\n maxTotalUses: formData.maxTotalUses,\n maxUsesPerUser: formData.maxUsesPerUser,\n validFrom: new Date(formData.validFrom).toISOString(),\n validUntil: formData.validUntil ? new Date(formData.validUntil).toISOString() : undefined,\n restrictedToUserIds: formData.restrictedToUserIds,\n applicableProducts: formData.applicableProducts,\n applicableServices: formData.applicableServices,\n excludedProducts: [],\n excludedServices: [],\n campaignName: formData.campaignName || undefined,\n };\n\n // Call GraphQL mutation\n const result = await createCouponMutation({\n variables: { input },\n });\n\n if (result.data?.createCoupon) {\n // Success - navigate to coupons list\n navigateTo('/coupons');\n } else {\n throw new Error('Failed to create coupon');\n }\n } catch (error) {\n console.error('Error creating coupon:', error);\n const errorMessage =\n error instanceof Error ? error.message : 'Failed to create coupon. Please try again.';\n setErrors({ code: errorMessage });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div className=\"max-w-4xl mx-auto p-4 sm:p-6 space-y-6\">\n {/* Header */}\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/coupons')}\n className=\"p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground\"\n >\n <ArrowLeft className=\"size-5\" />\n </button>\n <PageHeader\n title={tr('billing.coupons.createTitle', 'Create Coupon')}\n description=\"Create a new discount code or promotional offer\"\n icon={<Tag className=\"size-6\" />}\n />\n </div>\n\n {/* Form */}\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Basic Information */}\n <div className=\"bg-card border border-border rounded-lg p-6 space-y-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">Basic Information</h3>\n\n {/* Code */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Coupon Code *</label>\n <input\n type=\"text\"\n value={formData.code}\n onChange={(e) => handleChange('code', e.target.value.toUpperCase())}\n placeholder=\"e.g., WELCOME20\"\n className={`w-full px-4 py-2 rounded-lg border ${\n errors.code ? 'border-destructive' : 'border-border'\n } bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.code && <p className=\"text-sm text-destructive mt-1\">{errors.code}</p>}\n <p className=\"text-xs text-muted-foreground mt-1\">\n Use uppercase letters, numbers, hyphens, and underscores\n </p>\n </div>\n\n {/* Name */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Display Name *</label>\n <input\n type=\"text\"\n value={formData.name}\n onChange={(e) => handleChange('name', e.target.value)}\n placeholder=\"e.g., Welcome 20% Off\"\n className={`w-full px-4 py-2 rounded-lg border ${\n errors.name ? 'border-destructive' : 'border-border'\n } bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.name && <p className=\"text-sm text-destructive mt-1\">{errors.name}</p>}\n </div>\n\n {/* Description */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Description</label>\n <textarea\n value={formData.description}\n onChange={(e) => handleChange('description', e.target.value)}\n placeholder=\"Optional description for internal use\"\n rows={3}\n className=\"w-full px-4 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 </div>\n </div>\n\n {/* Discount Configuration */}\n <div className=\"bg-card border border-border rounded-lg p-6 space-y-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">Discount Configuration</h3>\n\n {/* Type */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-2\">\n Discount Type *\n </label>\n <div className=\"grid grid-cols-3 gap-3\">\n {[\n { value: 'PERCENTAGE', label: 'Percentage', icon: Percent },\n { value: 'FIXED_AMOUNT', label: 'Fixed Amount', icon: DollarSign },\n { value: 'FREE_SHIPPING', label: 'Free Shipping', icon: Package },\n ].map(({ value, label, icon: Icon }) => (\n <button\n key={value}\n type=\"button\"\n onClick={() => handleChange('type', value)}\n className={`p-4 rounded-lg border-2 transition-colors ${\n formData.type === value\n ? 'border-primary bg-primary/10'\n : 'border-border hover:border-primary/50'\n }`}\n >\n <Icon className=\"size-5 mx-auto mb-2\" />\n <p className=\"text-sm font-medium\">{label}</p>\n </button>\n ))}\n </div>\n </div>\n\n {/* Value */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Discount Value *\n </label>\n <div className=\"relative\">\n {formData.type === 'PERCENTAGE' ? (\n <>\n <Percent className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground\" />\n <input\n type=\"number\"\n value={formData.value}\n onChange={(e) => handleChange('value', parseFloat(e.target.value))}\n min=\"0\"\n max=\"100\"\n step=\"0.01\"\n className={`w-full pl-10 pr-4 py-2 rounded-lg border ${\n errors.value ? 'border-destructive' : 'border-border'\n } bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n </>\n ) : (\n <>\n <DollarSign className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground\" />\n <input\n type=\"number\"\n value={formData.value}\n onChange={(e) => handleChange('value', parseFloat(e.target.value))}\n min=\"0\"\n step=\"0.01\"\n className={`w-full pl-10 pr-4 py-2 rounded-lg border ${\n errors.value ? 'border-destructive' : 'border-border'\n } bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n </>\n )}\n </div>\n {errors.value && <p className=\"text-sm text-destructive mt-1\">{errors.value}</p>}\n </div>\n\n {/* Min Purchase Amount */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Minimum Purchase Amount\n </label>\n <div className=\"relative\">\n <DollarSign className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground\" />\n <input\n type=\"number\"\n value={formData.minPurchaseAmount || ''}\n onChange={(e) =>\n handleChange(\n 'minPurchaseAmount',\n e.target.value ? parseFloat(e.target.value) : null\n )\n }\n min=\"0\"\n step=\"0.01\"\n placeholder=\"No minimum\"\n className=\"w-full pl-10 pr-4 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 </div>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Order must be at least this amount to use coupon\n </p>\n </div>\n\n {/* Max Discount Amount */}\n {formData.type === 'PERCENTAGE' && (\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Maximum Discount Amount\n </label>\n <div className=\"relative\">\n <DollarSign className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground\" />\n <input\n type=\"number\"\n value={formData.maxDiscountAmount || ''}\n onChange={(e) =>\n handleChange(\n 'maxDiscountAmount',\n e.target.value ? parseFloat(e.target.value) : null\n )\n }\n min=\"0\"\n step=\"0.01\"\n placeholder=\"No maximum\"\n className=\"w-full pl-10 pr-4 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 </div>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Cap the discount amount for percentage-based coupons\n </p>\n </div>\n )}\n </div>\n\n {/* Usage Limits */}\n <div className=\"bg-card border border-border rounded-lg p-6 space-y-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">Usage Limits</h3>\n\n {/* Max Total Uses */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Total Usage Limit\n </label>\n <input\n type=\"number\"\n value={formData.maxTotalUses || ''}\n onChange={(e) =>\n handleChange('maxTotalUses', e.target.value ? parseInt(e.target.value) : null)\n }\n min=\"1\"\n placeholder=\"Unlimited\"\n className=\"w-full px-4 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\">\n Maximum number of times this coupon can be used across all users\n </p>\n </div>\n\n {/* Max Uses Per User */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Per-User Limit *\n </label>\n <input\n type=\"number\"\n value={formData.maxUsesPerUser}\n onChange={(e) => handleChange('maxUsesPerUser', parseInt(e.target.value))}\n min=\"1\"\n className=\"w-full px-4 py-2 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n <p className=\"text-xs text-muted-foreground mt-1\">\n Maximum times a single user can use this coupon\n </p>\n </div>\n </div>\n\n {/* Validity Period */}\n <div className=\"bg-card border border-border rounded-lg p-6 space-y-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">Validity Period</h3>\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n {/* Valid From */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Start Date *</label>\n <input\n type=\"date\"\n value={formData.validFrom}\n onChange={(e) => handleChange('validFrom', e.target.value)}\n className={`w-full px-4 py-2 rounded-lg border ${\n errors.validFrom ? 'border-destructive' : 'border-border'\n } bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.validFrom && (\n <p className=\"text-sm text-destructive mt-1\">{errors.validFrom}</p>\n )}\n </div>\n\n {/* Valid Until */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">End Date</label>\n <input\n type=\"date\"\n value={formData.validUntil}\n onChange={(e) => handleChange('validUntil', e.target.value)}\n className={`w-full px-4 py-2 rounded-lg border ${\n errors.validUntil ? 'border-destructive' : 'border-border'\n } bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.validUntil && (\n <p className=\"text-sm text-destructive mt-1\">{errors.validUntil}</p>\n )}\n <p className=\"text-xs text-muted-foreground mt-1\">Leave empty for no expiry</p>\n </div>\n </div>\n </div>\n\n {/* Scope */}\n <div className=\"bg-card border border-border rounded-lg p-6 space-y-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">Coupon Scope</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-2\">\n Who can use this coupon? *\n </label>\n <select\n value={formData.scope}\n onChange={(e) => handleChange('scope', e.target.value)}\n className=\"w-full px-4 py-2 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n >\n <option value=\"GENERIC\">Anyone (Public)</option>\n <option value=\"USER_SPECIFIC\">Specific Users Only</option>\n <option value=\"REFERRAL\">Referral Code</option>\n <option value=\"CAMPAIGN\">Campaign-Specific</option>\n </select>\n </div>\n\n {/* Campaign Name (if scope is CAMPAIGN) */}\n {formData.scope === 'CAMPAIGN' && (\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Campaign Name\n </label>\n <input\n type=\"text\"\n value={formData.campaignName}\n onChange={(e) => handleChange('campaignName', e.target.value)}\n placeholder=\"e.g., Summer Sale 2024\"\n className=\"w-full px-4 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 </div>\n )}\n </div>\n\n {/* Actions */}\n <div className=\"flex items-center justify-end gap-3\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/coupons')}\n className=\"px-6 py-3 rounded-lg border border-border text-foreground hover:bg-muted\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting}\n className=\"px-6 py-3 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isSubmitting ? (\n <>\n <div className=\"size-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin\" />\n Creating…\n </>\n ) : (\n <>\n <Save className=\"size-4\" />\n Create Coupon\n </>\n )}\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"CreateCouponPage.js","names":[],"sources":["../../../../../src/billing/modules/coupons/pages/CreateCouponPage.tsx"],"sourcesContent":["/**\n * Create Coupon Page\n *\n * Form for creating new coupon codes with validation\n */\n\nimport { type FC, useState, useRef, type KeyboardEvent } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { ArrowLeft, Save, Tag, Percent, Package, X, Users } from 'lucide-react';\n\nconst CURRENCIES: { code: string; symbol: string; name: string }[] = [\n { code: 'USD', symbol: '$', name: 'US Dollar' },\n { code: 'EUR', symbol: '€', name: 'Euro' },\n { code: 'GBP', symbol: '£', name: 'British Pound' },\n { code: 'INR', symbol: '₹', name: 'Indian Rupee' },\n { code: 'AUD', symbol: 'A$', name: 'Australian Dollar' },\n { code: 'CAD', symbol: 'C$', name: 'Canadian Dollar' },\n { code: 'SGD', symbol: 'S$', name: 'Singapore Dollar' },\n { code: 'JPY', symbol: '¥', name: 'Japanese Yen' },\n { code: 'MXN', symbol: 'MX$', name: 'Mexican Peso' },\n { code: 'BRL', symbol: 'R$', name: 'Brazilian Real' },\n { code: 'CHF', symbol: 'Fr', name: 'Swiss Franc' },\n { code: 'HKD', symbol: 'HK$', name: 'Hong Kong Dollar' },\n { code: 'NZD', symbol: 'NZ$', name: 'New Zealand Dollar' },\n { code: 'SEK', symbol: 'kr', name: 'Swedish Krona' },\n { code: 'KRW', symbol: '₩', name: 'South Korean Won' },\n];\n\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useCreateCouponMutation } from '../../../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface CouponFormData {\n code: string;\n name: string;\n description: string;\n scope: 'GENERIC' | 'USER_SPECIFIC' | 'REFERRAL' | 'CAMPAIGN';\n type: 'PERCENTAGE' | 'FIXED_AMOUNT' | 'FREE_SHIPPING';\n value: number;\n currency: string;\n minPurchaseAmount: number | null;\n maxDiscountAmount: number | null;\n maxTotalUses: number | null;\n maxUsesPerUser: number;\n validFrom: string;\n validUntil: string;\n restrictedToUserIds: string[];\n applicableProducts: string[];\n applicableServices: string[];\n campaignName: string;\n}\n\nexport const CreateCouponPage: 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 navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n\n const [userIdInput, setUserIdInput] = useState('');\n const userIdInputRef = useRef<HTMLInputElement>(null);\n\n const [formData, setFormData] = useState<CouponFormData>({\n code: '',\n name: '',\n description: '',\n scope: 'GENERIC',\n type: 'PERCENTAGE',\n value: 0,\n currency: 'USD',\n minPurchaseAmount: null,\n maxDiscountAmount: null,\n maxTotalUses: null,\n maxUsesPerUser: 1,\n validFrom: new Date().toISOString().split('T')[0],\n validUntil: '',\n restrictedToUserIds: [],\n applicableProducts: [],\n applicableServices: [],\n campaignName: '',\n });\n\n const [errors, setErrors] = useState<Partial<Record<keyof CouponFormData, string>>>({});\n const [isSubmitting, setIsSubmitting] = useState(false);\n\n const currencySymbol =\n CURRENCIES.find((c) => c.code === formData.currency)?.symbol ?? formData.currency;\n\n // GraphQL mutation\n const [createCouponMutation] = useCreateCouponMutation();\n\n // Check permissions\n if (!permissions.canManageDiscounts) {\n return <AccessDenied message=\"You don't have permission to create coupons.\" />;\n }\n\n // Handle input change\n const handleChange = (\n field: keyof CouponFormData,\n value: CouponFormData[keyof CouponFormData]\n ) => {\n setFormData((prev) => ({ ...prev, [field]: value }));\n\n // Clear error for this field\n if (errors[field]) {\n setErrors((prev) => {\n const newErrors = { ...prev };\n delete newErrors[field];\n return newErrors;\n });\n }\n };\n\n // Validate form\n const validateForm = (): boolean => {\n const newErrors: Partial<Record<keyof CouponFormData, string>> = {};\n\n // Code validation\n if (!formData.code.trim()) {\n newErrors.code = 'Coupon code is required';\n } else if (!/^[A-Z0-9-_]+$/.test(formData.code)) {\n newErrors.code =\n 'Code must contain only uppercase letters, numbers, hyphens, and underscores';\n } else if (formData.code.length < 3) {\n newErrors.code = 'Code must be at least 3 characters';\n } else if (formData.code.length > 50) {\n newErrors.code = 'Code must be at most 50 characters';\n }\n\n // Name validation\n if (!formData.name.trim()) {\n newErrors.name = 'Coupon name is required';\n }\n\n // Value validation\n if (formData.value <= 0) {\n newErrors.value = 'Value must be greater than 0';\n }\n if (formData.type === 'PERCENTAGE' && formData.value > 100) {\n newErrors.value = 'Percentage cannot exceed 100%';\n }\n\n // User-specific validation\n if (formData.scope === 'USER_SPECIFIC' && formData.restrictedToUserIds.length === 0) {\n newErrors.restrictedToUserIds = 'Add at least one user email or ID';\n }\n\n // Date validation\n if (!formData.validFrom) {\n newErrors.validFrom = 'Start date is required';\n }\n if (formData.validUntil && formData.validUntil < formData.validFrom) {\n newErrors.validUntil = 'End date must be after start date';\n }\n\n setErrors(newErrors);\n return Object.keys(newErrors).length === 0;\n };\n\n const addUserId = (value: string) => {\n const trimmed = value.trim();\n if (!trimmed) return;\n if (!formData.restrictedToUserIds.includes(trimmed)) {\n handleChange('restrictedToUserIds', [...formData.restrictedToUserIds, trimmed]);\n }\n setUserIdInput('');\n };\n\n const removeUserId = (id: string) => {\n handleChange(\n 'restrictedToUserIds',\n formData.restrictedToUserIds.filter((u) => u !== id)\n );\n };\n\n const handleUserIdKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter' || e.key === ',') {\n e.preventDefault();\n addUserId(userIdInput);\n } else if (e.key === 'Backspace' && !userIdInput && formData.restrictedToUserIds.length > 0) {\n removeUserId(formData.restrictedToUserIds[formData.restrictedToUserIds.length - 1]);\n }\n };\n\n // Handle submit\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!validateForm()) {\n return;\n }\n\n setIsSubmitting(true);\n\n try {\n // Prepare input data - convert dates to ISO format\n const input = {\n code: formData.code,\n name: formData.name,\n description: formData.description,\n scope: formData.scope,\n type: formData.type,\n value: formData.value,\n currency: formData.currency,\n minPurchaseAmount: formData.minPurchaseAmount,\n maxDiscountAmount: formData.maxDiscountAmount,\n maxTotalUses: formData.maxTotalUses,\n maxUsesPerUser: formData.maxUsesPerUser,\n validFrom: new Date(formData.validFrom).toISOString(),\n validUntil: formData.validUntil ? new Date(formData.validUntil).toISOString() : undefined,\n restrictedToUserIds: formData.restrictedToUserIds,\n applicableProducts: formData.applicableProducts,\n applicableServices: formData.applicableServices,\n excludedProducts: [],\n excludedServices: [],\n campaignName: formData.campaignName || undefined,\n };\n\n // Call GraphQL mutation\n const result = await createCouponMutation({\n variables: { input },\n });\n\n if (result.data?.createCoupon) {\n // Success - navigate to coupons list\n navigateTo('/coupons');\n } else {\n throw new Error('Failed to create coupon');\n }\n } catch (error) {\n console.error('Error creating coupon:', error);\n const errorMessage =\n error instanceof Error ? error.message : 'Failed to create coupon. Please try again.';\n setErrors({ code: errorMessage });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div className=\"max-w-4xl mx-auto p-4 sm:p-6 space-y-6\">\n {/* Header */}\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/coupons')}\n className=\"p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground\"\n >\n <ArrowLeft className=\"size-5\" />\n </button>\n <PageHeader\n title={tr('billing.coupons.createTitle', 'Create Coupon')}\n description=\"Create a new discount code or promotional offer\"\n icon={<Tag className=\"size-6\" />}\n />\n </div>\n\n {/* Form */}\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Basic Information */}\n <div className=\"bg-card border border-border rounded-lg p-6 space-y-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">Basic Information</h3>\n\n {/* Code */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Coupon Code *</label>\n <input\n type=\"text\"\n value={formData.code}\n onChange={(e) => handleChange('code', e.target.value.toUpperCase())}\n placeholder=\"e.g., WELCOME20\"\n className={`w-full px-4 py-2 rounded-lg border ${\n errors.code ? 'border-destructive' : 'border-border'\n } bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.code && <p className=\"text-sm text-destructive mt-1\">{errors.code}</p>}\n <p className=\"text-xs text-muted-foreground mt-1\">\n Use uppercase letters, numbers, hyphens, and underscores\n </p>\n </div>\n\n {/* Name */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Display Name *</label>\n <input\n type=\"text\"\n value={formData.name}\n onChange={(e) => handleChange('name', e.target.value)}\n placeholder=\"e.g., Welcome 20% Off\"\n className={`w-full px-4 py-2 rounded-lg border ${\n errors.name ? 'border-destructive' : 'border-border'\n } bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.name && <p className=\"text-sm text-destructive mt-1\">{errors.name}</p>}\n </div>\n\n {/* Description */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Description</label>\n <textarea\n value={formData.description}\n onChange={(e) => handleChange('description', e.target.value)}\n placeholder=\"Optional description for internal use\"\n rows={3}\n className=\"w-full px-4 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 </div>\n </div>\n\n {/* Discount Configuration */}\n <div className=\"bg-card border border-border rounded-lg p-6 space-y-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">Discount Configuration</h3>\n\n {/* Currency */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Coupon Currency *\n </label>\n <select\n value={formData.currency}\n onChange={(e) => handleChange('currency', e.target.value)}\n className=\"w-full px-4 py-2 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n >\n {CURRENCIES.map(({ code, symbol, name }) => (\n <option key={code} value={code}>\n {symbol} {code} — {name}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Amounts below are in this currency. Users paying in a different currency (e.g. INR)\n will have the amounts automatically converted using live exchange rates at validation\n time.\n </p>\n </div>\n\n {/* Type */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-2\">\n Discount Type *\n </label>\n <div className=\"grid grid-cols-3 gap-3\">\n {[\n { value: 'PERCENTAGE', label: 'Percentage', icon: Percent },\n { value: 'FIXED_AMOUNT', label: `Fixed (${currencySymbol})`, icon: null },\n { value: 'FREE_SHIPPING', label: 'Free Shipping', icon: Package },\n ].map(({ value, label, icon: Icon }) => (\n <button\n key={value}\n type=\"button\"\n onClick={() => handleChange('type', value)}\n className={`p-4 rounded-lg border-2 transition-colors ${\n formData.type === value\n ? 'border-primary bg-primary/10'\n : 'border-border hover:border-primary/50'\n }`}\n >\n {Icon ? (\n <Icon className=\"size-5 mx-auto mb-2\" />\n ) : (\n <span className=\"block text-lg font-bold mb-2\">{currencySymbol}</span>\n )}\n <p className=\"text-sm font-medium\">{label}</p>\n </button>\n ))}\n </div>\n </div>\n\n {/* Value */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Discount Value *\n </label>\n <div className=\"relative\">\n {formData.type === 'PERCENTAGE' ? (\n <>\n <Percent className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground\" />\n <input\n type=\"number\"\n value={formData.value}\n onChange={(e) => handleChange('value', parseFloat(e.target.value))}\n min=\"0\"\n max=\"100\"\n step=\"0.01\"\n className={`w-full pl-10 pr-4 py-2 rounded-lg border ${\n errors.value ? 'border-destructive' : 'border-border'\n } bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n </>\n ) : (\n <>\n <span className=\"absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground font-medium\">\n {currencySymbol}\n </span>\n <input\n type=\"number\"\n value={formData.value}\n onChange={(e) => handleChange('value', parseFloat(e.target.value))}\n min=\"0\"\n step=\"0.01\"\n className={`w-full pl-10 pr-4 py-2 rounded-lg border ${\n errors.value ? 'border-destructive' : 'border-border'\n } bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n </>\n )}\n </div>\n {errors.value && <p className=\"text-sm text-destructive mt-1\">{errors.value}</p>}\n </div>\n\n {/* Min Purchase Amount */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Minimum Purchase Amount ({formData.currency})\n </label>\n <div className=\"relative\">\n <span className=\"absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground font-medium\">\n {currencySymbol}\n </span>\n <input\n type=\"number\"\n value={formData.minPurchaseAmount || ''}\n onChange={(e) =>\n handleChange(\n 'minPurchaseAmount',\n e.target.value ? parseFloat(e.target.value) : null\n )\n }\n min=\"0\"\n step=\"0.01\"\n placeholder=\"No minimum\"\n className=\"w-full pl-10 pr-4 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 </div>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Order must be at least this amount in {formData.currency} (auto-converted for other\n currencies)\n </p>\n </div>\n\n {/* Max Discount Amount */}\n {formData.type === 'PERCENTAGE' && (\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Maximum Discount Amount ({formData.currency})\n </label>\n <div className=\"relative\">\n <span className=\"absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground font-medium\">\n {currencySymbol}\n </span>\n <input\n type=\"number\"\n value={formData.maxDiscountAmount || ''}\n onChange={(e) =>\n handleChange(\n 'maxDiscountAmount',\n e.target.value ? parseFloat(e.target.value) : null\n )\n }\n min=\"0\"\n step=\"0.01\"\n placeholder=\"No maximum\"\n className=\"w-full pl-10 pr-4 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 </div>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Cap the discount in {formData.currency} — auto-converted to the user's payment\n currency\n </p>\n </div>\n )}\n </div>\n\n {/* Usage Limits */}\n <div className=\"bg-card border border-border rounded-lg p-6 space-y-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">Usage Limits</h3>\n\n {/* Max Total Uses */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Total Usage Limit\n </label>\n <input\n type=\"number\"\n value={formData.maxTotalUses || ''}\n onChange={(e) =>\n handleChange('maxTotalUses', e.target.value ? parseInt(e.target.value) : null)\n }\n min=\"1\"\n placeholder=\"Unlimited\"\n className=\"w-full px-4 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\">\n Maximum number of times this coupon can be used across all users\n </p>\n </div>\n\n {/* Max Uses Per User */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Per-User Limit *\n </label>\n <input\n type=\"number\"\n value={formData.maxUsesPerUser}\n onChange={(e) => handleChange('maxUsesPerUser', parseInt(e.target.value))}\n min=\"1\"\n className=\"w-full px-4 py-2 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n <p className=\"text-xs text-muted-foreground mt-1\">\n Maximum times a single user can use this coupon\n </p>\n </div>\n </div>\n\n {/* Validity Period */}\n <div className=\"bg-card border border-border rounded-lg p-6 space-y-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">Validity Period</h3>\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n {/* Valid From */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Start Date *</label>\n <input\n type=\"date\"\n value={formData.validFrom}\n onChange={(e) => handleChange('validFrom', e.target.value)}\n className={`w-full px-4 py-2 rounded-lg border ${\n errors.validFrom ? 'border-destructive' : 'border-border'\n } bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.validFrom && (\n <p className=\"text-sm text-destructive mt-1\">{errors.validFrom}</p>\n )}\n </div>\n\n {/* Valid Until */}\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">End Date</label>\n <input\n type=\"date\"\n value={formData.validUntil}\n onChange={(e) => handleChange('validUntil', e.target.value)}\n className={`w-full px-4 py-2 rounded-lg border ${\n errors.validUntil ? 'border-destructive' : 'border-border'\n } bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.validUntil && (\n <p className=\"text-sm text-destructive mt-1\">{errors.validUntil}</p>\n )}\n <p className=\"text-xs text-muted-foreground mt-1\">Leave empty for no expiry</p>\n </div>\n </div>\n </div>\n\n {/* Scope */}\n <div className=\"bg-card border border-border rounded-lg p-6 space-y-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">Coupon Scope</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-2\">\n Who can use this coupon? *\n </label>\n <select\n value={formData.scope}\n onChange={(e) => handleChange('scope', e.target.value)}\n className=\"w-full px-4 py-2 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n >\n <option value=\"GENERIC\">Anyone (Public)</option>\n <option value=\"USER_SPECIFIC\">Specific Users Only</option>\n <option value=\"REFERRAL\">Referral Code</option>\n <option value=\"CAMPAIGN\">Campaign-Specific</option>\n </select>\n </div>\n\n {/* Campaign Name (if scope is CAMPAIGN) */}\n {formData.scope === 'CAMPAIGN' && (\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Campaign Name\n </label>\n <input\n type=\"text\"\n value={formData.campaignName}\n onChange={(e) => handleChange('campaignName', e.target.value)}\n placeholder=\"e.g., Summer Sale 2024\"\n className=\"w-full px-4 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 </div>\n )}\n\n {/* Restricted Users (if scope is USER_SPECIFIC) */}\n {formData.scope === 'USER_SPECIFIC' && (\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n <span className=\"flex items-center gap-1.5\">\n <Users className=\"size-4\" />\n Restricted Users *\n </span>\n </label>\n {/* Tag input container */}\n <div\n className={`min-h-[42px] flex flex-wrap gap-2 px-3 py-2 rounded-lg border ${\n errors.restrictedToUserIds ? 'border-destructive' : 'border-border'\n } bg-background cursor-text`}\n onClick={() => userIdInputRef.current?.focus()}\n >\n {formData.restrictedToUserIds.map((id) => (\n <span\n key={id}\n className=\"inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-primary/10 text-primary text-sm font-medium\"\n >\n {id}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n removeUserId(id);\n }}\n className=\"hover:text-destructive\"\n >\n <X className=\"size-3\" />\n </button>\n </span>\n ))}\n <input\n ref={userIdInputRef}\n type=\"text\"\n value={userIdInput}\n onChange={(e) => setUserIdInput(e.target.value)}\n onKeyDown={handleUserIdKeyDown}\n onBlur={() => addUserId(userIdInput)}\n placeholder={\n formData.restrictedToUserIds.length === 0\n ? 'Enter user email or ID, press Enter to add'\n : 'Add another…'\n }\n className=\"flex-1 min-w-[200px] bg-transparent text-foreground placeholder:text-muted-foreground text-sm focus:outline-none\"\n />\n </div>\n {errors.restrictedToUserIds && (\n <p className=\"text-sm text-destructive mt-1\">{errors.restrictedToUserIds}</p>\n )}\n <p className=\"text-xs text-muted-foreground mt-1\">\n Type an email or user ID and press Enter or comma to add. Only these users can\n redeem this coupon.\n </p>\n </div>\n )}\n </div>\n\n {/* Actions */}\n <div className=\"flex items-center justify-end gap-3\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/coupons')}\n className=\"px-6 py-3 rounded-lg border border-border text-foreground hover:bg-muted\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting}\n className=\"px-6 py-3 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isSubmitting ? (\n <>\n <div className=\"size-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin\" />\n Creating…\n </>\n ) : (\n <>\n <Save className=\"size-4\" />\n Create Coupon\n </>\n )}\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ //#region src/billing/modules/plans/constants/featuredQuotas.ts
2
+ var e = [
3
+ "vibecontrols.agents.count",
4
+ "vibecontrols.sessions.active",
5
+ "vibecontrols.vibes.count",
6
+ "vibecontrols.sandbox.runtime_hours",
7
+ "vibecontrols.tunnels.custom_domains",
8
+ "vibecontrols.audit.retention_days",
9
+ "vibecontrols.actions.count",
10
+ "vibecontrols.vibedeck.boards",
11
+ "vibecontrols.scheduler.executions",
12
+ "vibecontrols.security.vaults",
13
+ "vibecontrols.catalogs.count",
14
+ "vibecontrols.docs.compilations",
15
+ "vibecontrols.organizations.count"
16
+ ], t = {
17
+ "vibecontrols.agents.count": "Agents",
18
+ "vibecontrols.sessions.active": "Concurrent Sessions",
19
+ "vibecontrols.vibes.count": "Vibes",
20
+ "vibecontrols.sandbox.runtime_hours": "Sandbox Minutes",
21
+ "vibecontrols.tunnels.custom_domains": "Custom Domains (VibeTunnels)",
22
+ "vibecontrols.audit.retention_days": "Audit Log Retention",
23
+ "vibecontrols.actions.count": "Actions",
24
+ "vibecontrols.vibedeck.boards": "Vibe Decks",
25
+ "vibecontrols.scheduler.executions": "Scheduled Task Executions",
26
+ "vibecontrols.security.vaults": "Vaults",
27
+ "vibecontrols.catalogs.count": "Catalogs",
28
+ "vibecontrols.docs.compilations": "Doc Builds",
29
+ "vibecontrols.organizations.count": "Organizations & Workspaces"
30
+ };
31
+ function n(e, t) {
32
+ return e === "vibecontrols.sandbox.runtime_hours" ? `${(t * 60).toLocaleString()} min` : e === "vibecontrols.audit.retention_days" ? `${t.toLocaleString()} days` : t.toLocaleString();
33
+ }
34
+ //#endregion
35
+ export { t as FEATURED_QUOTA_LABELS, e as FEATURED_QUOTA_NAMES, n as formatFeaturedQuotaValue };
36
+
37
+ //# sourceMappingURL=featuredQuotas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"featuredQuotas.js","names":[],"sources":["../../../../../src/billing/modules/plans/constants/featuredQuotas.ts"],"sourcesContent":["/**\n * The subset of quotas shown prominently on plan cards.\n * Order here determines display order. All other quotas are shown only on\n * the plan detail page.\n */\nexport const FEATURED_QUOTA_NAMES = [\n 'vibecontrols.agents.count',\n 'vibecontrols.sessions.active',\n 'vibecontrols.vibes.count',\n 'vibecontrols.sandbox.runtime_hours',\n 'vibecontrols.tunnels.custom_domains',\n 'vibecontrols.audit.retention_days',\n 'vibecontrols.actions.count',\n 'vibecontrols.vibedeck.boards',\n 'vibecontrols.scheduler.executions',\n 'vibecontrols.security.vaults',\n 'vibecontrols.catalogs.count',\n 'vibecontrols.docs.compilations',\n 'vibecontrols.organizations.count',\n] as const;\n\nexport const FEATURED_QUOTA_LABELS: Record<string, string> = {\n 'vibecontrols.agents.count': 'Agents',\n 'vibecontrols.sessions.active': 'Concurrent Sessions',\n 'vibecontrols.vibes.count': 'Vibes',\n 'vibecontrols.sandbox.runtime_hours': 'Sandbox Minutes',\n 'vibecontrols.tunnels.custom_domains': 'Custom Domains (VibeTunnels)',\n 'vibecontrols.audit.retention_days': 'Audit Log Retention',\n 'vibecontrols.actions.count': 'Actions',\n 'vibecontrols.vibedeck.boards': 'Vibe Decks',\n 'vibecontrols.scheduler.executions': 'Scheduled Task Executions',\n 'vibecontrols.security.vaults': 'Vaults',\n 'vibecontrols.catalogs.count': 'Catalogs',\n 'vibecontrols.docs.compilations': 'Doc Builds',\n 'vibecontrols.organizations.count': 'Organizations & Workspaces',\n};\n\n/** Format a featured quota value for display (e.g. hours → minutes, days suffix). */\nexport function formatFeaturedQuotaValue(name: string, value: number): string {\n if (name === 'vibecontrols.sandbox.runtime_hours') {\n const minutes = value * 60;\n return `${minutes.toLocaleString()} min`;\n }\n if (name === 'vibecontrols.audit.retention_days') {\n return `${value.toLocaleString()} days`;\n }\n return value.toLocaleString();\n}\n"],"mappings":";AAKA,IAAa,IAAuB;CAClC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,EAEY,IAAgD;CAC3D,6BAA6B;CAC7B,gCAAgC;CAChC,4BAA4B;CAC5B,sCAAsC;CACtC,uCAAuC;CACvC,qCAAqC;CACrC,8BAA8B;CAC9B,gCAAgC;CAChC,qCAAqC;CACrC,gCAAgC;CAChC,+BAA+B;CAC/B,kCAAkC;CAClC,oCAAoC;CACrC;AAGD,SAAgB,EAAyB,GAAc,GAAuB;AAQ5E,QAPI,MAAS,uCAEJ,IADS,IAAQ,IACN,gBAAgB,CAAC,QAEjC,MAAS,sCACJ,GAAG,EAAM,gBAAgB,CAAC,SAE5B,EAAM,gBAAgB"}
@@ -7,5 +7,6 @@ import { PlanEditPage as n } from "./pages/PlanEditPage.js";
7
7
  import { PlanCreatePage as r } from "./pages/PlanCreatePage.js";
8
8
  import { PlansBrowsePage as i } from "./pages/PlansBrowsePage.js";
9
9
  import { PlanBrowseDetailPage as a } from "./pages/PlanBrowseDetailPage.js";
10
+ import { PlanAssignFreePage as o } from "./pages/PlanAssignFreePage.js";
10
11
  import "./pages/index.js";
11
- export { a as PlanBrowseDetailPage, r as PlanCreatePage, t as PlanDetailPage, n as PlanEditPage, i as PlansBrowsePage, e as PlansListPage };
12
+ export { o as PlanAssignFreePage, a as PlanBrowseDetailPage, r as PlanCreatePage, t as PlanDetailPage, n as PlanEditPage, i as PlansBrowsePage, e as PlansListPage };
@@ -0,0 +1,205 @@
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 { useAssignPlanFreeMutation as r } from "../../../../generated/global-operations.js";
6
+ import { useBillingNavigate as i } from "../../../hooks/useBillingNavigate.js";
7
+ import { usePlans as a } from "../hooks/usePlans.js";
8
+ import "../hooks/index.js";
9
+ import { useState as o } from "react";
10
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
11
+ import { useI18n as l } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
12
+ //#region src/billing/modules/plans/pages/PlanAssignFreePage.tsx
13
+ var u = () => {
14
+ let { t: u } = l(), d = (e, t) => {
15
+ let n = u(e);
16
+ return n === e ? t : n;
17
+ }, f = i(), p = n(), [m, h] = o(""), [g, _] = o(""), [v, y] = o(""), [b, x] = o(null), { plans: S, isLoading: C } = a({ includeFree: !0 }), [w, { loading: T }] = r();
18
+ if (!p.canManagePlans) return /* @__PURE__ */ s(e, { message: d("billing.assignFree.noPermission", "You don't have permission to access this page.") });
19
+ let E = async (e) => {
20
+ if (e.preventDefault(), x(null), !m.trim() || !g.trim() || !v) {
21
+ x({
22
+ success: !1,
23
+ message: "All fields are required."
24
+ });
25
+ return;
26
+ }
27
+ try {
28
+ let { data: e } = await w({ variables: {
29
+ secret: m.trim(),
30
+ billingAccountId: g.trim(),
31
+ planId: v
32
+ } });
33
+ if (e?.assignPlanFree) {
34
+ let t = e.assignPlanFree;
35
+ x({
36
+ success: !0,
37
+ message: `Plan assigned. Subscription ID: ${t.id} · Status: ${t.status} · Valid: ${new Date(t.startDate).toLocaleDateString()} → ${new Date(t.endDate).toLocaleDateString()}`
38
+ }), h(""), _(""), y("");
39
+ }
40
+ } catch (e) {
41
+ x({
42
+ success: !1,
43
+ message: e instanceof Error ? e.message : "Failed to assign plan."
44
+ });
45
+ }
46
+ }, D = S.find((e) => e.id === v);
47
+ return /* @__PURE__ */ c("div", {
48
+ className: "space-y-6 p-6 max-w-2xl",
49
+ children: [
50
+ /* @__PURE__ */ s(t, {
51
+ title: d("billing.assignFree.title", "Assign Plan for Free"),
52
+ description: d("billing.assignFree.description", "Internally assign any plan to a billing account at no charge. Requires the team secret."),
53
+ actions: /* @__PURE__ */ s("button", {
54
+ type: "button",
55
+ onClick: () => f("/plans"),
56
+ className: "px-3 py-2 text-sm border border-border rounded-md text-foreground hover:bg-muted transition-colors",
57
+ children: "← Back to Plans"
58
+ })
59
+ }),
60
+ /* @__PURE__ */ c("div", {
61
+ className: "flex items-start gap-3 p-4 rounded-lg border border-status-warning-border bg-status-warning-bg/10",
62
+ children: [/* @__PURE__ */ s("svg", {
63
+ className: "size-5 text-status-warning-text shrink-0 mt-0.5",
64
+ fill: "none",
65
+ viewBox: "0 0 24 24",
66
+ stroke: "currentColor",
67
+ children: /* @__PURE__ */ s("path", {
68
+ strokeLinecap: "round",
69
+ strokeLinejoin: "round",
70
+ strokeWidth: 2,
71
+ d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.962-.833-2.732 0L3.072 16.5c-.77.833.192 2.5 1.732 2.5z"
72
+ })
73
+ }), /* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("p", {
74
+ className: "text-sm font-medium text-status-warning-text",
75
+ children: "Internal admin tool"
76
+ }), /* @__PURE__ */ s("p", {
77
+ className: "text-xs text-status-warning-text/80 mt-0.5",
78
+ children: "This bypasses all payment flows. The plan is assigned immediately with no charge. Use only for enterprise deals or internal accounts."
79
+ })] })]
80
+ }),
81
+ /* @__PURE__ */ c("form", {
82
+ onSubmit: E,
83
+ className: "border border-border rounded-lg bg-card overflow-hidden",
84
+ children: [
85
+ /* @__PURE__ */ s("div", {
86
+ className: "p-4 border-b border-border bg-muted/30",
87
+ children: /* @__PURE__ */ s("h2", {
88
+ className: "font-semibold text-foreground",
89
+ children: "Assignment Details"
90
+ })
91
+ }),
92
+ /* @__PURE__ */ c("div", {
93
+ className: "p-4 space-y-4",
94
+ children: [
95
+ /* @__PURE__ */ c("div", { children: [
96
+ /* @__PURE__ */ c("label", {
97
+ htmlFor: "secret",
98
+ className: "block text-sm font-medium text-foreground mb-1",
99
+ children: ["Team Secret ", /* @__PURE__ */ s("span", {
100
+ className: "text-status-error-text",
101
+ children: "*"
102
+ })]
103
+ }),
104
+ /* @__PURE__ */ s("input", {
105
+ id: "secret",
106
+ type: "password",
107
+ value: m,
108
+ onChange: (e) => h(e.target.value),
109
+ autoComplete: "off",
110
+ 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 font-mono",
111
+ placeholder: "Enter PLAN_ASSIGN_SECRET",
112
+ required: !0
113
+ }),
114
+ /* @__PURE__ */ s("p", {
115
+ className: "text-xs text-muted-foreground mt-1",
116
+ children: "The secret key shared within the team. Never expose this to users."
117
+ })
118
+ ] }),
119
+ /* @__PURE__ */ c("div", { children: [
120
+ /* @__PURE__ */ c("label", {
121
+ htmlFor: "billingAccountId",
122
+ className: "block text-sm font-medium text-foreground mb-1",
123
+ children: ["Billing Account ID ", /* @__PURE__ */ s("span", {
124
+ className: "text-status-error-text",
125
+ children: "*"
126
+ })]
127
+ }),
128
+ /* @__PURE__ */ s("input", {
129
+ id: "billingAccountId",
130
+ type: "text",
131
+ value: g,
132
+ onChange: (e) => _(e.target.value),
133
+ 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 font-mono text-sm",
134
+ placeholder: "e.g. ba_01jk3n2...",
135
+ required: !0
136
+ }),
137
+ /* @__PURE__ */ s("p", {
138
+ className: "text-xs text-muted-foreground mt-1",
139
+ children: "Find this in the user's billing account settings or via the billing accounts list."
140
+ })
141
+ ] }),
142
+ /* @__PURE__ */ c("div", { children: [
143
+ /* @__PURE__ */ c("label", {
144
+ htmlFor: "planId",
145
+ className: "block text-sm font-medium text-foreground mb-1",
146
+ children: ["Plan ", /* @__PURE__ */ s("span", {
147
+ className: "text-status-error-text",
148
+ children: "*"
149
+ })]
150
+ }),
151
+ C ? /* @__PURE__ */ s("div", { className: "h-10 bg-muted animate-pulse rounded-md" }) : /* @__PURE__ */ c("select", {
152
+ id: "planId",
153
+ value: v,
154
+ onChange: (e) => y(e.target.value),
155
+ 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",
156
+ required: !0,
157
+ children: [/* @__PURE__ */ s("option", {
158
+ value: "",
159
+ disabled: !0,
160
+ children: "Select a plan…"
161
+ }), S.map((e) => /* @__PURE__ */ c("option", {
162
+ value: e.id,
163
+ children: [
164
+ e.name,
165
+ e.isPurchasable ? "" : " (internal)",
166
+ e.price != null && e.price > 0 ? ` — ${e.currency ?? "USD"} ${e.price}` : ""
167
+ ]
168
+ }, e.id))]
169
+ }),
170
+ D && /* @__PURE__ */ c("p", {
171
+ className: "text-xs text-muted-foreground mt-1",
172
+ children: [D.isPurchasable ? "Public plan" : "Internal / non-purchasable plan", D.productID ? ` · Product: ${D.productID}` : ""]
173
+ })
174
+ ] })
175
+ ]
176
+ }),
177
+ b && /* @__PURE__ */ s("div", {
178
+ className: `mx-4 mb-4 p-3 rounded-lg border text-sm ${b.success ? "border-status-success-border bg-status-success-bg/10 text-status-success-text" : "border-status-error-border bg-status-error-bg/10 text-status-error-text"}`,
179
+ children: b.message
180
+ }),
181
+ /* @__PURE__ */ c("div", {
182
+ className: "px-4 pb-4 flex items-center gap-3",
183
+ children: [/* @__PURE__ */ s("button", {
184
+ type: "submit",
185
+ disabled: T || !m || !g || !v,
186
+ className: "px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
187
+ children: T ? "Assigning…" : "Assign Plan for Free"
188
+ }), /* @__PURE__ */ s("button", {
189
+ type: "button",
190
+ onClick: () => {
191
+ h(""), _(""), y(""), x(null);
192
+ },
193
+ className: "px-4 py-2 text-sm border border-border rounded-md text-foreground hover:bg-muted transition-colors",
194
+ children: "Clear"
195
+ })]
196
+ })
197
+ ]
198
+ })
199
+ ]
200
+ });
201
+ };
202
+ //#endregion
203
+ export { u as PlanAssignFreePage };
204
+
205
+ //# sourceMappingURL=PlanAssignFreePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlanAssignFreePage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanAssignFreePage.tsx"],"sourcesContent":["/**\n * Plans Module - Assign Plan Free (Admin only)\n *\n * Internal tool for assigning any plan to a billing account at no charge.\n * Protected by PLAN_ASSIGN_SECRET — only team members who know the secret\n * can use this page. Never expose to end users.\n *\n * Route: /billing/plans/assign-free\n */\n\nimport { useState, type FC, type FormEvent } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useAssignPlanFreeMutation } from '../../../../generated/global-operations';\nimport { usePlans } from '../hooks';\n\nexport const PlanAssignFreePage: 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\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n\n const [secret, setSecret] = useState('');\n const [billingAccountId, setBillingAccountId] = useState('');\n const [planId, setPlanId] = useState('');\n const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);\n\n // Fetch all plans (including non-purchasable internal ones) for the dropdown\n const { plans, isLoading: isPlansLoading } = usePlans({ includeFree: true });\n\n const [assignPlanFree, { loading: isAssigning }] = useAssignPlanFreeMutation();\n\n if (!permissions.canManagePlans) {\n return (\n <AccessDenied\n message={tr(\n 'billing.assignFree.noPermission',\n \"You don't have permission to access this page.\"\n )}\n />\n );\n }\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setResult(null);\n\n if (!secret.trim() || !billingAccountId.trim() || !planId) {\n setResult({ success: false, message: 'All fields are required.' });\n return;\n }\n\n try {\n const { data } = await assignPlanFree({\n variables: {\n secret: secret.trim(),\n billingAccountId: billingAccountId.trim(),\n planId,\n },\n });\n\n if (data?.assignPlanFree) {\n const sub = data.assignPlanFree;\n setResult({\n success: true,\n message: `Plan assigned. Subscription ID: ${sub.id} · Status: ${sub.status} · Valid: ${new Date(sub.startDate).toLocaleDateString()} → ${new Date(sub.endDate).toLocaleDateString()}`,\n });\n // Clear sensitive fields\n setSecret('');\n setBillingAccountId('');\n setPlanId('');\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to assign plan.';\n setResult({ success: false, message: msg });\n }\n };\n\n const selectedPlan = plans.find((p) => p.id === planId);\n\n return (\n <div className=\"space-y-6 p-6 max-w-2xl\">\n <PageHeader\n title={tr('billing.assignFree.title', 'Assign Plan for Free')}\n description={tr(\n 'billing.assignFree.description',\n 'Internally assign any plan to a billing account at no charge. Requires the team secret.'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-3 py-2 text-sm border border-border rounded-md text-foreground hover:bg-muted transition-colors\"\n >\n ← Back to Plans\n </button>\n }\n />\n\n {/* Warning banner */}\n <div className=\"flex items-start gap-3 p-4 rounded-lg border border-status-warning-border bg-status-warning-bg/10\">\n <svg\n className=\"size-5 text-status-warning-text shrink-0 mt-0.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=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.962-.833-2.732 0L3.072 16.5c-.77.833.192 2.5 1.732 2.5z\"\n />\n </svg>\n <div>\n <p className=\"text-sm font-medium text-status-warning-text\">Internal admin tool</p>\n <p className=\"text-xs text-status-warning-text/80 mt-0.5\">\n This bypasses all payment flows. The plan is assigned immediately with no charge. Use\n only for enterprise deals or internal accounts.\n </p>\n </div>\n </div>\n\n <form\n onSubmit={handleSubmit}\n className=\"border border-border rounded-lg bg-card overflow-hidden\"\n >\n <div className=\"p-4 border-b border-border bg-muted/30\">\n <h2 className=\"font-semibold text-foreground\">Assignment Details</h2>\n </div>\n\n <div className=\"p-4 space-y-4\">\n {/* Secret */}\n <div>\n <label htmlFor=\"secret\" className=\"block text-sm font-medium text-foreground mb-1\">\n Team Secret <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"secret\"\n type=\"password\"\n value={secret}\n onChange={(e) => setSecret(e.target.value)}\n autoComplete=\"off\"\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 font-mono\"\n placeholder=\"Enter PLAN_ASSIGN_SECRET\"\n required\n />\n <p className=\"text-xs text-muted-foreground mt-1\">\n The secret key shared within the team. Never expose this to users.\n </p>\n </div>\n\n {/* Billing Account ID */}\n <div>\n <label\n htmlFor=\"billingAccountId\"\n className=\"block text-sm font-medium text-foreground mb-1\"\n >\n Billing Account ID <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"billingAccountId\"\n type=\"text\"\n value={billingAccountId}\n onChange={(e) => setBillingAccountId(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 font-mono text-sm\"\n placeholder=\"e.g. ba_01jk3n2...\"\n required\n />\n <p className=\"text-xs text-muted-foreground mt-1\">\n Find this in the user's billing account settings or via the billing accounts list.\n </p>\n </div>\n\n {/* Plan selector */}\n <div>\n <label htmlFor=\"planId\" className=\"block text-sm font-medium text-foreground mb-1\">\n Plan <span className=\"text-status-error-text\">*</span>\n </label>\n {isPlansLoading ? (\n <div className=\"h-10 bg-muted animate-pulse rounded-md\" />\n ) : (\n <select\n id=\"planId\"\n value={planId}\n onChange={(e) => setPlanId(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 required\n >\n <option value=\"\" disabled>\n Select a plan…\n </option>\n {plans.map((plan) => (\n <option key={plan.id} value={plan.id}>\n {plan.name}\n {!plan.isPurchasable ? ' (internal)' : ''}\n {plan.price != null && plan.price > 0\n ? ` — ${plan.currency ?? 'USD'} ${plan.price}`\n : ''}\n </option>\n ))}\n </select>\n )}\n {selectedPlan && (\n <p className=\"text-xs text-muted-foreground mt-1\">\n {selectedPlan.isPurchasable ? 'Public plan' : 'Internal / non-purchasable plan'}\n {selectedPlan.productID ? ` · Product: ${selectedPlan.productID}` : ''}\n </p>\n )}\n </div>\n </div>\n\n {/* Result banner */}\n {result && (\n <div\n className={`mx-4 mb-4 p-3 rounded-lg border text-sm ${\n result.success\n ? 'border-status-success-border bg-status-success-bg/10 text-status-success-text'\n : 'border-status-error-border bg-status-error-bg/10 text-status-error-text'\n }`}\n >\n {result.message}\n </div>\n )}\n\n <div className=\"px-4 pb-4 flex items-center gap-3\">\n <button\n type=\"submit\"\n disabled={isAssigning || !secret || !billingAccountId || !planId}\n className=\"px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isAssigning ? 'Assigning…' : 'Assign Plan for Free'}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setSecret('');\n setBillingAccountId('');\n setPlanId('');\n setResult(null);\n }}\n className=\"px-4 py-2 text-sm border border-border rounded-md text-foreground hover:bg-muted transition-colors\"\n >\n Clear\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}